// SPEC: PJ2026-01060703 CI/CD branch follower reconcile timeline visibility.
// Responsibility: bounded controller-loop timing summaries for branch-follower state/status output.

export type ReconcileTimelineStep = {
  follower: string;
  step: string;
  status: string;
  startedAt: string;
  finishedAt?: string;
  elapsedMs?: number;
  observedSha?: string;
  targetSha?: string;
  phase?: string;
  pipelineRun?: string;
  object?: string;
  message?: string;
  reason?: string;
  exitCode?: number;
};

export type ReconcileTimeline = {
  startedAt: string;
  finishedAt?: string;
  elapsedMs?: number;
  controller: boolean;
  dryRun: boolean;
  confirm: boolean;
  wait: boolean;
  followerCount: number;
  followers: string[];
  bounded: true;
  steps: ReconcileTimelineStep[];
};

export type ReconcileStepMarker = {
  readonly step: ReconcileTimelineStep;
  readonly startedMs: number;
};

export function startReconcileTimeline(input: { controller: boolean; dryRun: boolean; confirm: boolean; wait: boolean; followerIds: string[] }): ReconcileTimeline {
  return {
    startedAt: new Date().toISOString(),
    controller: input.controller,
    dryRun: input.dryRun,
    confirm: input.confirm,
    wait: input.wait,
    followerCount: input.followerIds.length,
    followers: input.followerIds.slice(0, 8),
    bounded: true,
    steps: [],
  };
}

export function finishReconcileTimeline(timeline: ReconcileTimeline): ReconcileTimeline {
  const finishedMs = Date.now();
  timeline.finishedAt = new Date(finishedMs).toISOString();
  timeline.elapsedMs = elapsedMs(timeline.startedAt, finishedMs);
  timeline.steps = compactSteps(timeline.steps, null, 32);
  return timeline;
}

export function startReconcileStep(timeline: ReconcileTimeline, follower: string, step: string): ReconcileStepMarker {
  const startedMs = Date.now();
  const record: ReconcileTimelineStep = {
    follower: safeText(follower, 80) ?? "-",
    step: safeText(step, 80) ?? "-",
    status: "running",
    startedAt: new Date(startedMs).toISOString(),
  };
  timeline.steps.push(record);
  return { step: record, startedMs };
}

export function finishReconcileStep(marker: ReconcileStepMarker, fields: Record<string, unknown> = {}): ReconcileTimelineStep {
  const finishedMs = Date.now();
  marker.step.finishedAt = new Date(finishedMs).toISOString();
  marker.step.elapsedMs = Math.max(0, finishedMs - marker.startedMs);
  marker.step.status = safeText(fields.status, 80) ?? (fields.ok === false ? "failed" : "ok");
  setText(marker.step, "observedSha", fields.observedSha, 80);
  setText(marker.step, "targetSha", fields.targetSha, 80);
  setText(marker.step, "phase", fields.phase, 80);
  setText(marker.step, "pipelineRun", fields.pipelineRun, 120);
  setText(marker.step, "object", fields.object, 120);
  setText(marker.step, "message", fields.message, 180);
  setText(marker.step, "reason", fields.reason, 180);
  const exitCode = numberOrNull(fields.exitCode);
  if (exitCode !== null) marker.step.exitCode = exitCode;
  return marker.step;
}

export function attachReconcileTimeline(command: Record<string, unknown> | undefined, timeline: ReconcileTimeline, followerId: string): Record<string, unknown> | undefined {
  const compact = compactReconcileTimeline(timeline, followerId);
  if (compact === null) return command;
  return { ...(command ?? {}), reconcileTimeline: compact };
}

export function compactReconcileTimeline(value: unknown, followerId?: string | null): Record<string, unknown> | null {
  const source = asRecord(value);
  if (source === null) return null;
  const steps = compactSteps(arrayRecords(source.steps), followerId ?? null, followerId === undefined || followerId === null ? 32 : 16);
  return {
    startedAt: safeText(source.startedAt, 80),
    finishedAt: safeText(source.finishedAt, 80),
    elapsedMs: numberOrNull(source.elapsedMs),
    controller: source.controller === true,
    dryRun: source.dryRun === true,
    confirm: source.confirm === true,
    wait: source.wait === true,
    followerCount: numberOrNull(source.followerCount),
    followers: Array.isArray(source.followers) ? source.followers.map((item) => safeText(item, 80)).filter((item): item is string => item !== null).slice(0, 8) : [],
    bounded: true,
    omittedStepCount: Math.max(0, arrayRecords(source.steps).length - steps.length),
    steps,
  };
}

function compactSteps(values: Record<string, unknown>[], followerId: string | null, maxSteps: number): ReconcileTimelineStep[] {
  const filtered = values
    .filter((item) => followerId === null || item.follower === "*" || item.follower === followerId)
    .slice(-maxSteps);
  return filtered.map((item) => {
    const step: ReconcileTimelineStep = {
      follower: safeText(item.follower, 80) ?? "-",
      step: safeText(item.step, 80) ?? "-",
      status: safeText(item.status, 80) ?? "-",
      startedAt: safeText(item.startedAt, 80) ?? "-",
    };
    setText(step, "finishedAt", item.finishedAt, 80);
    const elapsed = numberOrNull(item.elapsedMs);
    if (elapsed !== null) step.elapsedMs = elapsed;
    setText(step, "observedSha", item.observedSha, 80);
    setText(step, "targetSha", item.targetSha, 80);
    setText(step, "phase", item.phase, 80);
    setText(step, "pipelineRun", item.pipelineRun, 120);
    setText(step, "object", item.object, 120);
    setText(step, "message", item.message, 180);
    setText(step, "reason", item.reason, 180);
    const exitCode = numberOrNull(item.exitCode);
    if (exitCode !== null) step.exitCode = exitCode;
    return step;
  });
}

function setText(target: ReconcileTimelineStep, key: keyof ReconcileTimelineStep, value: unknown, maxLength: number): void {
  const text = safeText(value, maxLength);
  if (text !== null) (target as Record<string, unknown>)[key] = text;
}

function elapsedMs(startedAt: string, finishedMs: number): number | undefined {
  const startedMs = Date.parse(startedAt);
  if (!Number.isFinite(startedMs)) return undefined;
  return Math.max(0, finishedMs - startedMs);
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}

function arrayRecords(value: unknown): Record<string, unknown>[] {
  return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
}

function numberOrNull(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function safeText(value: unknown, maxLength: number): string | null {
  if (typeof value !== "string" || value.length === 0) return null;
  const text = value.replace(/\s+/gu, " ");
  return text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 3))}...`;
}
