// SPEC: PJ2026-01060703 CI/CD branch follower timing attribution.
// Responsibility: derive bounded wall-clock attribution from stored totals and stage/native intervals.
import type { FollowerState, StageTiming } from "./cicd-types";

export function timingAttributionSummary(timings: FollowerState["timings"], nativeGate: Record<string, unknown>): Record<string, unknown> {
  const totalStartedMs = timestampMs(timings.startedAt);
  const totalFinishedMs = timestampMs(timings.finishedAt);
  const totalSeconds = timings.totalSeconds;
  if (totalSeconds === null || totalStartedMs === null || totalFinishedMs === null || totalFinishedMs < totalStartedMs) {
    return {
      status: "unknown",
      source: "stored-total-vs-stage-intervals",
      totalSeconds,
      knownIntervalCoverageSeconds: null,
      unknownWallClockSeconds: null,
      missingIntervalStages: [],
      reason: "stored total range is missing or invalid; old wall-clock attribution cannot be reconstructed",
    };
  }
  const stageIntervals = timings.stages
    .map((stage) => timingIntervalOverlap(stage.stage, stage.source, stage.object, stage.startedAt, stage.finishedAt, totalStartedMs, totalFinishedMs))
    .filter((item): item is TimingInterval => item !== null);
  const nativeIntervals = [
    timingIntervalOverlap("pipelinerun", "tekton-native-gate", String(nativeGate.pipelineRunObject ?? nativeGate.pipelineRunName ?? ""), nativeGate.pipelineRunStartedAt, nativeGate.pipelineRunFinishedAt, totalStartedMs, totalFinishedMs),
    timingIntervalOverlap("argo", "argocd-native-gate", String(nativeGate.argoApplication ?? ""), nativeGate.argoOperationStartedAt, nativeGate.argoOperationFinishedAt, totalStartedMs, totalFinishedMs),
  ].filter((item): item is TimingInterval => item !== null);
  const intervals = dedupeIntervals([...stageIntervals, ...nativeIntervals]);
  const coverageSeconds = mergedIntervalCoverageSeconds(intervals);
  const unknownSeconds = roundSeconds(Math.max(0, totalSeconds - coverageSeconds));
  const missingIntervalCount = countMissingStageIntervals(timings.stages);
  const missingIntervalStages = missingStageIntervals(timings.stages);
  return {
    status: unknownSeconds > 0 ? "partial" : "covered",
    source: "stored-total-vs-stage-intervals",
    totalSeconds,
    totalStartedAt: timings.startedAt,
    totalFinishedAt: timings.finishedAt,
    knownIntervalCoverageSeconds: coverageSeconds,
    unknownWallClockSeconds: unknownSeconds,
    intervalCount: intervals.length,
    stageIntervalCount: stageIntervals.length,
    nativeIntervalCount: nativeIntervals.length,
    excludedIntervalCount: intervals.filter((item) => !item.inStoredTotal).length,
    missingIntervalCount,
    missingIntervalReason: missingIntervalCount > 0 ? "missing-startedAt-or-finishedAt" : null,
    missingIntervalStages,
    intervals: intervals
      .filter((item) => item.inStoredTotal)
      .map(({ startMs: _startMs, endMs: _endMs, overlapStartMs: _overlapStartMs, overlapEndMs: _overlapEndMs, ...item }) => item),
    reason: unknownSeconds > 0
      ? missingIntervalCount > 0
        ? "some timed stages lack startedAt/finishedAt; remaining wall-clock must stay unknown until future state records those intervals"
        : "no stored historical stage intervals cover the remaining wall-clock; old data cannot be reconstructed"
      : null,
  };
}

type TimingInterval = {
  stage: string;
  source: string;
  object: string | null;
  startedAt: string;
  finishedAt: string;
  overlapSeconds: number;
  inStoredTotal: boolean;
  startMs: number;
  endMs: number;
  overlapStartMs: number;
  overlapEndMs: number;
};

function timingIntervalOverlap(stage: string, source: string, object: unknown, startedAtValue: unknown, finishedAtValue: unknown, totalStartedMs: number, totalFinishedMs: number): TimingInterval | null {
  const startedAt = stringOrNull(startedAtValue);
  const finishedAt = stringOrNull(finishedAtValue);
  const startMs = timestampMs(startedAt);
  const endMs = timestampMs(finishedAt);
  if (startedAt === null || finishedAt === null || startMs === null || endMs === null || endMs < startMs) return null;
  const overlapStartMs = Math.max(startMs, totalStartedMs);
  const overlapEndMs = Math.min(endMs, totalFinishedMs);
  const overlapSeconds = overlapEndMs > overlapStartMs ? roundSeconds((overlapEndMs - overlapStartMs) / 1000) : 0;
  return {
    stage,
    source,
    object: stringOrNull(object),
    startedAt,
    finishedAt,
    overlapSeconds,
    inStoredTotal: overlapSeconds > 0,
    startMs,
    endMs,
    overlapStartMs,
    overlapEndMs,
  };
}

function missingStageIntervals(stages: readonly StageTiming[]): Record<string, unknown>[] {
  return stages
    .filter((stage) => stage.seconds !== null && (timestampMs(stage.startedAt ?? null) === null || timestampMs(stage.finishedAt ?? null) === null))
    .slice(0, 3)
    .map((stage) => {
      const row: Record<string, unknown> = {
        stage: stage.stage,
        source: stage.source,
        seconds: stage.seconds,
      };
      if (stage.object !== null) row.object = stage.object;
      return row;
    });
}

function countMissingStageIntervals(stages: readonly StageTiming[]): number {
  return stages.filter((stage) => stage.seconds !== null && (timestampMs(stage.startedAt ?? null) === null || timestampMs(stage.finishedAt ?? null) === null)).length;
}

function dedupeIntervals(intervals: TimingInterval[]): TimingInterval[] {
  const byKey = new Map<string, TimingInterval>();
  for (const interval of intervals) {
    const key = `${interval.stage}\t${interval.source}\t${interval.startedAt}\t${interval.finishedAt}`;
    const previous = byKey.get(key);
    if (previous === undefined || previous.inStoredTotal === false && interval.inStoredTotal === true) byKey.set(key, interval);
  }
  return [...byKey.values()];
}

function mergedIntervalCoverageSeconds(intervals: TimingInterval[]): number {
  const ranges = intervals
    .filter((item) => item.overlapSeconds > 0)
    .map((item) => ({ startMs: item.overlapStartMs, endMs: item.overlapEndMs }))
    .sort((a, b) => a.startMs - b.startMs);
  let coveredMs = 0;
  let currentStart: number | null = null;
  let currentEnd: number | null = null;
  for (const range of ranges) {
    if (currentStart === null || currentEnd === null) {
      currentStart = range.startMs;
      currentEnd = range.endMs;
      continue;
    }
    if (range.startMs <= currentEnd) {
      currentEnd = Math.max(currentEnd, range.endMs);
      continue;
    }
    coveredMs += currentEnd - currentStart;
    currentStart = range.startMs;
    currentEnd = range.endMs;
  }
  if (currentStart !== null && currentEnd !== null) coveredMs += currentEnd - currentStart;
  return roundSeconds(coveredMs / 1000);
}

function timestampMs(value: string | null): number | null {
  if (value === null) return null;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function roundSeconds(value: number): number {
  return Math.round(value * 10) / 10;
}

function stringOrNull(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}
