import type {
  ActivityReportSection,
  HistoryReportSection,
  HistoricalMetric,
  HistoricalSeries,
  ObservabilityReport,
  ObservabilityReportQuery,
  ReportBucket,
  ReportCoverageGap,
  ReportCoverageStatus,
  ReportMetricUnit,
  ReportSeriesGap,
} from "@overdeck/report-contract";
import { DEFAULT_REPORT_HISTORY_RETENTION_DAYS, type ReportHistoryStore } from "./history-store";

const HOUR_MS = 60 * 60 * 1_000;
export const HISTORY_BACKFILL_HOURS = 14 * 24;
export const HISTORY_BACKFILL_BUILDS_PER_REQUEST = 12;

interface MetricDefinition {
  key: string;
  sourceId: "recorded-events" | "failures";
  label: string;
  unit: ReportMetricUnit;
  version: number;
}

const METRICS: MetricDefinition[] = [
  { key: "activity.recorded-events", sourceId: "recorded-events", label: "Recorded events", unit: "count", version: 1 },
  { key: "activity.failures", sourceId: "failures", label: "Recorded failures", unit: "count", version: 1 },
];

function floorHour(value: number): number {
  return Math.floor(value / HOUR_MS) * HOUR_MS;
}

function ceilHour(value: number): number {
  return Math.ceil(value / HOUR_MS) * HOUR_MS;
}

function coverageRank(status: ReportCoverageStatus): number {
  return status === "unavailable" ? 3 : status === "partial" ? 2 : status === "stale" ? 1 : 0;
}

function worstCoverage(values: ReportCoverageStatus[]): ReportCoverageStatus {
  return values.reduce<ReportCoverageStatus>((worst, value) => coverageRank(value) > coverageRank(worst) ? value : worst, "complete");
}

function activitySection(report: ObservabilityReport): ActivityReportSection | undefined {
  return report.sections.find((section): section is ActivityReportSection => section.kind === "activity");
}

function coverageKey(report: ObservabilityReport, status: ReportCoverageStatus): string {
  const sources = report.coverage.sources
    .map((source) => `${source.id}:${source.authority}:${source.status}`)
    .sort();
  return JSON.stringify({ status, sources });
}

function sourceWatermark(report: ObservabilityReport): Record<string, string | null> {
  return Object.fromEntries(report.coverage.sources
    .map((source) => [source.id, source.latest ?? null] as const)
    .sort(([left], [right]) => left.localeCompare(right)));
}

function metricGaps(report: ObservabilityReport, metricKey: string): ReportCoverageGap[] {
  return report.coverage.gaps.filter((gap) => !gap.metric || gap.metric === metricKey || gap.metric === metricKey.split(".").at(-1));
}

function historyDimensionKey(query: ObservabilityReportQuery): string {
  return JSON.stringify({
    projects: [...(query.projects ?? [])].sort(),
    sources: [...(query.sources ?? [])].sort(),
    severity: query.severity ?? null,
    q: query.q ?? null,
  });
}

export function reportHistoryBuckets(
  report: ObservabilityReport,
  bucketStart: string,
  bucketEnd: string,
  backfilled: boolean,
): ReportBucket[] {
  const activity = activitySection(report);
  if (!activity) return [];
  const watermarks = sourceWatermark(report);
  return METRICS.flatMap((definition) => {
    const metric = activity.metrics.find((candidate) => candidate.id === definition.sourceId);
    if (!metric || metric.value === null) return [];
    return [{
      bucketStart,
      bucketEnd,
      metricKey: definition.key,
      metricVersion: definition.version,
      label: definition.label,
      unit: definition.unit,
      dimensionKey: historyDimensionKey(report.query),
      value: metric.value,
      coverage: {
        status: metric.coverage,
        key: coverageKey(report, metric.coverage),
        gaps: metricGaps(report, definition.key),
      },
      sourceWatermark: watermarks,
      generatedAt: bucketEnd,
      backfilled,
    } satisfies ReportBucket];
  });
}

export function backfillReportHistory(options: {
  store: ReportHistoryStore;
  nowMs: number;
  buildReport(query: ObservabilityReportQuery): ObservabilityReport;
  baseQuery: Omit<ObservabilityReportQuery, "from" | "to">;
  maxHours?: number;
  maxBuilds?: number;
}): { inserted: number; unchanged: number; attemptedHours: number } {
  const end = floorHour(options.nowMs);
  const maxHours = Math.max(0, Math.min(options.maxHours ?? HISTORY_BACKFILL_HOURS, HISTORY_BACKFILL_HOURS));
  const maxBuilds = Math.max(0, Math.min(options.maxBuilds ?? HISTORY_BACKFILL_BUILDS_PER_REQUEST, HISTORY_BACKFILL_HOURS));
  const start = end - maxHours * HOUR_MS;
  const dimensionKey = historyDimensionKey({ ...options.baseQuery, from: new Date(start).toISOString(), to: new Date(end).toISOString() });
  const existing = new Set(options.store.readReportBuckets(new Date(start).toISOString(), new Date(end).toISOString()).buckets
    .filter((bucket) => bucket.dimensionKey === dimensionKey)
    .map((bucket) => `${bucket.bucketStart}:${bucket.metricKey}:${bucket.metricVersion}`));
  let inserted = 0;
  let unchanged = 0;
  let attemptedHours = 0;
  for (let bucketStart = end - HOUR_MS; bucketStart >= start && attemptedHours < maxBuilds; bucketStart -= HOUR_MS) {
    const bucketStartIso = new Date(bucketStart).toISOString();
    if (METRICS.every((metric) => existing.has(`${bucketStartIso}:${metric.key}:${metric.version}`))) continue;
    attemptedHours += 1;
    const bucketEnd = bucketStart + HOUR_MS;
    const report = options.buildReport({
      ...options.baseQuery,
      from: new Date(bucketStart).toISOString(),
      to: new Date(bucketEnd - 1).toISOString(),
    });
    for (const bucket of reportHistoryBuckets(report, new Date(bucketStart).toISOString(), new Date(bucketEnd).toISOString(), true)) {
      const outcome = options.store.writeReportBucket(bucket);
      if (outcome === "inserted") inserted += 1;
      else unchanged += 1;
    }
  }
  options.store.prune(options.nowMs);
  return { inserted, unchanged, attemptedHours };
}

function expectedStarts(fromMs: number, toMs: number): number[] {
  const values: number[] = [];
  for (let start = ceilHour(fromMs); start + HOUR_MS <= toMs; start += HOUR_MS) values.push(start);
  return values;
}

function coalescedGaps(expected: number[], present: Set<number>, reason: string): ReportSeriesGap[] {
  const gaps: ReportSeriesGap[] = [];
  let start: number | undefined;
  let previous: number | undefined;
  for (const value of expected) {
    if (!present.has(value)) {
      start ??= value;
      previous = value;
      continue;
    }
    if (start !== undefined && previous !== undefined) {
      gaps.push({ from: new Date(start).toISOString(), to: new Date(previous + HOUR_MS).toISOString(), reason });
      start = undefined;
      previous = undefined;
    }
  }
  if (start !== undefined && previous !== undefined) gaps.push({ from: new Date(start).toISOString(), to: new Date(previous + HOUR_MS).toISOString(), reason });
  return gaps;
}

function latestVersion(rows: ReportBucket[]): number | undefined {
  return rows.reduce<number | undefined>((latest, row) => latest === undefined || row.metricVersion > latest ? row.metricVersion : latest, undefined);
}

function periodRows(rows: ReportBucket[], key: string, version: number | undefined): ReportBucket[] {
  if (version === undefined) return [];
  return rows.filter((row) => row.metricKey === key && row.metricVersion === version);
}

function periodCoverage(rows: ReportBucket[], expected: number[]): ReportCoverageStatus {
  if (rows.length === 0) return "unavailable";
  const present = new Set(rows.map((row) => Date.parse(row.bucketStart)));
  return present.size < expected.length ? "partial" : worstCoverage(rows.map((row) => row.coverage.status));
}

function direction(delta: number | null): HistoricalMetric["direction"] {
  if (delta === null) return "unknown";
  if (delta > 0) return "up";
  if (delta < 0) return "down";
  return "flat";
}

export function buildUnavailableHistoryReportSection(
  query: ObservabilityReportQuery,
  reason = "Retained report history could not be read.",
): HistoryReportSection {
  const duration = Date.parse(query.to) - Date.parse(query.from);
  const gap: ReportCoverageGap = { domain: "history", sourceId: "report-history", reason };
  return {
    kind: "history",
    status: "unavailable",
    title: "Durable history and comparison",
    bucketHours: 1,
    rangeDays: duration / (24 * HOUR_MS),
    retentionDays: DEFAULT_REPORT_HISTORY_RETENTION_DAYS,
    metrics: METRICS.map((definition) => ({
      key: definition.key,
      label: definition.label,
      unit: definition.unit,
      metricVersion: definition.version,
      current: null,
      previous: null,
      delta: null,
      direction: "unknown",
      coverage: "unavailable",
      comparisonCompatible: false,
      comparisonReason: reason,
    })),
    series: METRICS.map((definition) => ({
      key: definition.key,
      label: definition.label,
      unit: definition.unit,
      metricVersion: definition.version,
      points: [],
      gaps: [],
    })),
    gaps: [gap],
    quarantinedRows: 0,
    errors: [reason],
  };
}

export function buildHistoryReportSection(
  query: ObservabilityReportQuery,
  store: ReportHistoryStore,
): HistoryReportSection {
  const fromMs = Date.parse(query.from);
  const toMs = Date.parse(query.to);
  const duration = toMs - fromMs;
  const previousFromMs = fromMs - duration;
  const currentRead = store.readReportBuckets(new Date(ceilHour(fromMs)).toISOString(), new Date(floorHour(toMs)).toISOString());
  const previousRead = store.readReportBuckets(new Date(ceilHour(previousFromMs)).toISOString(), new Date(floorHour(fromMs)).toISOString());
  const currentExpected = expectedStarts(fromMs, toMs);
  const previousExpected = expectedStarts(previousFromMs, fromMs);
  const dimensionKey = historyDimensionKey(query);
  const currentBuckets = currentRead.buckets.filter((bucket) => bucket.dimensionKey === dimensionKey);
  const previousBuckets = previousRead.buckets.filter((bucket) => bucket.dimensionKey === dimensionKey);
  const metrics: HistoricalMetric[] = [];
  const series: HistoricalSeries[] = [];
  const gaps: ReportCoverageGap[] = [];

  for (const definition of METRICS) {
    const currentCandidates = currentBuckets.filter((row) => row.metricKey === definition.key);
    const previousCandidates = previousBuckets.filter((row) => row.metricKey === definition.key);
    const currentVersion = latestVersion(currentCandidates);
    const previousVersion = latestVersion(previousCandidates);
    const current = periodRows(currentBuckets, definition.key, currentVersion);
    const previous = periodRows(previousBuckets, definition.key, previousVersion);
    const currentPresent = new Set(current.map((row) => Date.parse(row.bucketStart)));
    const previousPresent = new Set(previous.map((row) => Date.parse(row.bucketStart)));
    const currentGaps = coalescedGaps(currentExpected, currentPresent, "No compatible retained history bucket was recorded for this interval.");
    const previousGaps = coalescedGaps(previousExpected, previousPresent, "The preceding period has no compatible retained history bucket for this interval.");
    const versionsAvailable = currentVersion !== undefined && previousVersion !== undefined;
    const versionsCompatible = versionsAvailable && currentVersion === previousVersion;
    const coverageKeys = new Set([...current, ...previous].map((row) => row.coverage.key));
    const coverageCompatible = current.length === currentExpected.length
      && previous.length === previousExpected.length
      && currentExpected.length > 0
      && coverageKeys.size === 1
      && [...current, ...previous].every((row) => row.coverage.status !== "unavailable");
    const comparisonCompatible = versionsCompatible && coverageCompatible;
    const currentValue = current.length > 0 ? current.reduce((total, row) => total + row.value, 0) : null;
    const previousValue = previous.length > 0 ? previous.reduce((total, row) => total + row.value, 0) : null;
    const delta = comparisonCompatible && currentValue !== null && previousValue !== null ? currentValue - previousValue : null;
    const reason = !versionsAvailable
      ? "The current and preceding periods do not both have retained metric records."
      : versionsCompatible
        ? coverageCompatible
          ? undefined
          : "The two periods do not have matching bucket and coverage records."
        : "The two periods do not use the same metric version.";
    const coverage = periodCoverage(current, currentExpected);
    metrics.push({
      key: definition.key,
      label: definition.label,
      unit: definition.unit,
      metricVersion: currentVersion ?? definition.version,
      current: currentValue,
      previous: comparisonCompatible ? previousValue : null,
      delta,
      direction: direction(delta),
      coverage,
      comparisonCompatible,
      ...(reason ? { comparisonReason: reason } : {}),
    });
    series.push({
      key: definition.key,
      label: definition.label,
      unit: definition.unit,
      metricVersion: currentVersion ?? definition.version,
      points: current.map((row) => ({ ts: row.bucketStart, value: row.value })),
      gaps: currentGaps,
    });
    for (const gap of currentGaps) gaps.push({ domain: "history", metric: definition.key, from: gap.from, to: gap.to, reason: gap.reason });
    for (const gap of previousGaps) gaps.push({
      domain: "history",
      metric: definition.key,
      from: gap.from,
      to: gap.to,
      reason: gap.reason,
    });
    if (!versionsCompatible && currentVersion !== undefined && previousVersion !== undefined) gaps.push({ domain: "history", metric: definition.key, reason: `Metric version ${currentVersion} cannot be compared with preceding version ${previousVersion}.` });
  }

  const newlyQuarantinedRows = currentRead.quarantinedRows + previousRead.quarantinedRows;
  const quarantinedRows = store.quarantineCount();
  if (quarantinedRows > 0) gaps.push({
    domain: "history",
    sourceId: "report-history",
    reason: newlyQuarantinedRows > 0
      ? `${newlyQuarantinedRows.toLocaleString()} corrupt history ${newlyQuarantinedRows === 1 ? "row was" : "rows were"} quarantined while this report was read; ${quarantinedRows.toLocaleString()} quarantined ${quarantinedRows === 1 ? "row is" : "rows are"} retained for diagnosis.`
      : `${quarantinedRows.toLocaleString()} corrupt history ${quarantinedRows === 1 ? "row is" : "rows are"} quarantined and excluded from this report.`,
  });
  const status = metrics.length === 0 ? "unavailable" : worstCoverage(metrics.map((metric) => metric.coverage));
  return {
    kind: "history",
    status,
    title: "Durable history and comparison",
    bucketHours: 1,
    rangeDays: duration / (24 * HOUR_MS),
    retentionDays: store.retentionDays,
    ...(store.earliestAvailable() ? { earliestAvailable: store.earliestAvailable() } : {}),
    metrics,
    series,
    gaps,
    quarantinedRows,
    errors: [],
  };
}
