import type { CurrentStreak, MetricsBucket, TrendPoint } from "../shared/api";

const BUCKET_STEPS: Array<{ label: MetricsBucket; ms: number }> = [
  { label: "30s", ms: 30_000 },
  { label: "1m", ms: 60_000 },
  { label: "5m", ms: 300_000 },
  { label: "15m", ms: 900_000 },
  { label: "30m", ms: 1_800_000 },
  { label: "1h", ms: 3_600_000 },
  { label: "3h", ms: 10_800_000 },
  { label: "6h", ms: 21_600_000 },
  { label: "12h", ms: 43_200_000 },
  { label: "1d", ms: 86_400_000 },
];

const DESIRED_POINTS = 168;

export interface IncidentAnalysis {
  incidentCount: number;
  longestOutage: null | number;
  mttr: null | number;
}

export interface MetricCheckpoint {
  durationMs: null | number;
  matched: boolean;
  timestamp: string;
}

export function analyzeIncidentSequence(checkpoints: MetricCheckpoint[], from: string, to: string): IncidentAnalysis {
  const sorted = sortCheckpoints(checkpoints);
  const fromTime = new Date(from).getTime();
  const toTime = new Date(to).getTime();
  const recoveredDurations: number[] = [];

  let incidentCount = 0;
  let longestOutage: null | number = null;
  let outageStart: null | number = null;
  let outageStartedAtWindowBoundary = false;
  let previousMatched: boolean | null = null;

  for (const checkpoint of sorted) {
    const timestamp = new Date(checkpoint.timestamp).getTime();

    if (!checkpoint.matched) {
      if (previousMatched !== false) {
        incidentCount++;
        outageStart = previousMatched === null ? fromTime : timestamp;
        outageStartedAtWindowBoundary = previousMatched === null;
      }
    } else if (previousMatched === false && outageStart !== null) {
      const duration = Math.max(0, timestamp - outageStart);
      longestOutage = maxNullable(longestOutage, duration);
      if (!outageStartedAtWindowBoundary) {
        recoveredDurations.push(duration);
      }
      outageStart = null;
      outageStartedAtWindowBoundary = false;
    }

    previousMatched = checkpoint.matched;
  }

  if (previousMatched === false && outageStart !== null) {
    const duration = Math.max(0, toTime - outageStart);
    longestOutage = maxNullable(longestOutage, duration);
  }

  return {
    incidentCount,
    longestOutage,
    mttr: calculateAverageDuration(recoveredDurations),
  };
}

export function buildTrend(
  checkpoints: MetricCheckpoint[],
  from: string,
  to: string,
  bucket: MetricsBucket,
): TrendPoint[] {
  const bucketMs = getBucketDurationMs(bucket);
  const fromTime = new Date(from).getTime();
  const toTime = new Date(to).getTime();

  const firstBucketStart = alignToBucketBoundary(fromTime, bucketMs);

  const buckets = new Map<
    number,
    {
      downChecks: number;
      durations: number[];
      totalChecks: number;
      upChecks: number;
    }
  >();

  for (const checkpoint of checkpoints) {
    const ts = new Date(checkpoint.timestamp).getTime();
    const offset = ts - firstBucketStart;
    const bucketIndex = Math.max(0, Math.floor(offset / bucketMs));
    const bucketStart = firstBucketStart + bucketIndex * bucketMs;

    const b = buckets.get(bucketStart) ?? { downChecks: 0, durations: [], totalChecks: 0, upChecks: 0 };
    b.totalChecks++;
    if (checkpoint.matched) {
      b.upChecks++;
      if (checkpoint.durationMs !== null) {
        b.durations.push(checkpoint.durationMs);
      }
    } else {
      b.downChecks++;
    }
    buckets.set(bucketStart, b);
  }

  const result: TrendPoint[] = [];
  for (let start = firstBucketStart; start < toTime; start += bucketMs) {
    if (start + bucketMs <= fromTime) continue;
    const effectiveStart = Math.max(start, fromTime);
    const effectiveEnd = Math.min(start + bucketMs, toTime);
    const b = buckets.get(start);
    if (b) {
      result.push({
        availability: calculateAvailability(b.upChecks, b.totalChecks),
        avgDurationMs: calculateAverageDuration(b.durations),
        bucketEnd: new Date(effectiveEnd).toISOString(),
        bucketStart: new Date(effectiveStart).toISOString(),
        downChecks: b.downChecks,
        maxDurationMs: b.durations.length > 0 ? Math.max(...b.durations) : null,
        minDurationMs: b.durations.length > 0 ? Math.min(...b.durations) : null,
        p95DurationMs: calculatePercentile(b.durations, 95),
        totalChecks: b.totalChecks,
        upChecks: b.upChecks,
      });
    } else {
      result.push({
        availability: 0,
        avgDurationMs: null,
        bucketEnd: new Date(effectiveEnd).toISOString(),
        bucketStart: new Date(effectiveStart).toISOString(),
        downChecks: 0,
        maxDurationMs: null,
        minDurationMs: null,
        p95DurationMs: null,
        totalChecks: 0,
        upChecks: 0,
      });
    }
  }

  return result;
}

export function calculateAvailability(upChecks: number, totalChecks: number): number {
  if (totalChecks <= 0) return 0;
  return roundToTwo((upChecks / totalChecks) * 100);
}

export function calculateAverageDuration(durations: number[]): null | number {
  if (durations.length === 0) return null;
  const total = durations.reduce((sum, duration) => sum + duration, 0);
  return roundToTwo(total / durations.length);
}

export function calculateCurrentStreak(checkpoints: MetricCheckpoint[], limit?: number): CurrentStreak | null {
  const sorted = sortCheckpoints(checkpoints);
  const latest = sorted.at(-1);
  if (!latest) return null;

  let count = 0;
  for (let index = sorted.length - 1; index >= 0; index--) {
    const checkpoint = sorted[index];
    if (checkpoint?.matched !== latest.matched) break;
    count++;
  }

  return {
    ...(limit !== undefined && count >= limit ? { capped: true } : {}),
    count,
    up: latest.matched,
  };
}

export function calculatePercentile(durations: number[], percentile: number): null | number {
  if (durations.length === 0) return null;

  const sorted = [...durations].sort((a, b) => a - b);
  const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((sorted.length * percentile) / 100) - 1));
  return sorted[index] ?? null;
}

export function getBucketDurationMs(bucket: MetricsBucket): number {
  return BUCKET_STEPS.find((s) => s.label === bucket)?.ms ?? 3_600_000;
}

export function resolveAutoBucket(intervalMs: number, windowMs: number): MetricsBucket {
  const roughBucket = Math.max(intervalMs, windowMs / DESIRED_POINTS);
  for (const step of BUCKET_STEPS) {
    if (step.ms >= roughBucket) return step.label;
  }
  return "1d";
}

function alignToBucketBoundary(timestampMs: number, bucketMs: number): number {
  return Math.floor(timestampMs / bucketMs) * bucketMs;
}

function maxNullable(left: null | number, right: number): number {
  return left === null ? right : Math.max(left, right);
}

function roundToTwo(value: number): number {
  return Math.round(value * 100) / 100;
}

function sortCheckpoints(checkpoints: MetricCheckpoint[]): MetricCheckpoint[] {
  return [...checkpoints].sort((left, right) => left.timestamp.localeCompare(right.timestamp));
}
