import {
  buildIncidentLifecycleEvent,
  type ControllerEvent,
} from "./events";
import type { ClusterScheduler } from "./scheduler";
import type { ControllerStore, IncidentRecord } from "./store";

export type IncidentSeverity = "info" | "page" | "high";

export interface IncidentNotification {
  severity: IncidentSeverity;
  incident: IncidentRecord;
}

export interface IncidentNotifier {
  notify(notification: IncidentNotification): void;
}

export interface IncidentRule {
  id: string;
  openReasons: readonly string[];
  resolveReasons: readonly string[];
  remediation: string;
  autoResolveCondition: string;
  notification: IncidentSeverity | null;
  cooldownMs: number;
  key(event: ControllerEvent): string;
}

const hostKey = (prefix: string) => (event: ControllerEvent): string =>
  `${prefix}:${event.host || "cluster"}`;

export const INCIDENT_RULES: readonly IncidentRule[] = [
  {
    id: "transient-transport",
    openReasons: ["transport-interrupted"],
    resolveReasons: ["transport-restored"],
    remediation: "reconnect and retry once",
    autoResolveCondition: "transport succeeds after reconnect and one retry",
    notification: null,
    cooldownMs: 0,
    key: (event) => `transient-transport:${event.job || event.host || "cluster"}`,
  },
  {
    id: "dead-lease",
    openReasons: ["dead-lease"],
    resolveReasons: ["lease-reconciled"],
    remediation: "reconcile lease ownership",
    autoResolveCondition: "lease owner is live or dead lease is removed",
    notification: "info",
    cooldownMs: 15 * 60_000,
    key: hostKey("dead-lease"),
  },
  {
    id: "repeated-command-not-found",
    openReasons: ["capability-missing"],
    resolveReasons: ["capability-restored"],
    remediation: "quarantine command capability on affected host",
    autoResolveCondition: "half-open capability probe succeeds",
    notification: "page",
    cooldownMs: 30 * 60_000,
    key: (event) => `repeated-command-not-found:${event.host}:${event.stage}`,
  },
  {
    id: "queue-stalled-while-idle",
    openReasons: ["queue-stalled-while-idle"],
    resolveReasons: ["queue-dispatch-resumed"],
    remediation: "run admission reconciliation once",
    autoResolveCondition: "queued work dispatches or no idle capacity remains",
    notification: "page",
    cooldownMs: 30 * 60_000,
    key: hostKey("queue-stalled-while-idle"),
  },
  {
    id: "fallback-lease-or-drain-stuck",
    openReasons: ["fallback-lease-expired", "drain-stuck"],
    resolveReasons: ["fallback-lease-cleared", "drain-completed"],
    remediation: "clear expired fallback lease or complete blocked drain",
    autoResolveCondition: "fallback lease is cleared or drain completes",
    notification: "page",
    cooldownMs: 30 * 60_000,
    key: (event) => {
      const kind = event.reason.startsWith("fallback-") ? "fallback-lease-expired" : "drain-stuck";
      return `${kind}:${event.host || "cluster"}`;
    },
  },
  {
    id: "artifact-cas-mismatch",
    openReasons: ["artifact-publication-blocked"],
    resolveReasons: [],
    remediation: "stop publication and preserve staging for operator verification",
    autoResolveCondition: "none; operator must verify and explicitly republish",
    notification: "high",
    cooldownMs: 60 * 60_000,
    key: (event) => `artifact-cas-mismatch:${event.job || event.host || "unknown"}`,
  },
  {
    id: "control-service-down",
    openReasons: ["controller-down", "collector-down"],
    resolveReasons: ["controller-healthy", "collector-healthy"],
    remediation: "watchdog attempts one service restart",
    autoResolveCondition: "service health endpoint succeeds",
    notification: "page",
    cooldownMs: 30 * 60_000,
    key: (event) => `${event.reason.startsWith("controller-") ? "controller-down" : "collector-down"}:${event.host || "laptop"}`,
  },
] as const;

const CRITICAL_TEMPERATURE = {
  remediation: "pause new dispatch to host until package temperature clears",
  autoResolveCondition: "package temperature falls below critical threshold",
  cooldownMs: 30 * 60_000,
} as const;

export class IncidentReducer {
  private cursor: number;

  constructor(
    private readonly store: ControllerStore,
    private readonly notifier: IncidentNotifier,
    private readonly now: () => number = () => Date.now(),
  ) {
    this.cursor = store.getIncidentCursor();
  }

  reduce(event: ControllerEvent, sourceRevision?: number): IncidentRecord | null {
    const resolveRule = INCIDENT_RULES.find((rule) => rule.resolveReasons.includes(event.reason));
    if (resolveRule) return this.resolve(resolveRule.key(event), event.host, sourceRevision);

    const openRule = INCIDENT_RULES.find((rule) => rule.openReasons.includes(event.reason));
    if (!openRule) {
      if (sourceRevision !== undefined) this.store.setIncidentCursor(sourceRevision);
      return null;
    }
    return this.open({
      key: openRule.key(event),
      host: event.host,
      affectedJobs: event.job ? [event.job] : [],
      remediation: openRule.remediation,
      autoResolveCondition: openRule.autoResolveCondition,
      notification: openRule.notification,
      cooldownMs: openRule.cooldownMs,
    }, sourceRevision);
  }

  reducePending(): void {
    const events = this.store.readEvents(this.cursor);
    for (const stored of events) {
      this.reduce(stored.event, stored.revision);
      this.cursor = stored.revision;
    }
  }

  observeTemperature(
    temperature: { host: string; pkg: number; crit: number },
    scheduler: Pick<ClusterScheduler, "setCriticalTemperature">,
  ): IncidentRecord | null {
    const key = `critical-temperature:${temperature.host}`;
    if (temperature.pkg < temperature.crit) {
      scheduler.setCriticalTemperature(temperature.host, false);
      return this.resolve(key, temperature.host);
    }

    scheduler.setCriticalTemperature(temperature.host, true);
    return this.open({
      key,
      host: temperature.host,
      affectedJobs: [],
      remediation: CRITICAL_TEMPERATURE.remediation,
      autoResolveCondition: CRITICAL_TEMPERATURE.autoResolveCondition,
      notification: "page",
      cooldownMs: CRITICAL_TEMPERATURE.cooldownMs,
    });
  }

  private open(input: {
    key: string;
    host: string;
    affectedJobs: string[];
    remediation: string;
    autoResolveCondition: string;
    notification: IncidentSeverity | null;
    cooldownMs: number;
  }, sourceRevision?: number): IncidentRecord {
    const existing = this.store.getIncident(input.key);
    const now = this.now();
    const shouldNotify = input.notification !== null && (
      existing === null ||
      existing.state === "resolved" ||
      existing.cooldownUntil === null ||
      Date.parse(existing.cooldownUntil) <= now
    );
    const cooldownUntil = input.notification === null
      ? null
      : shouldNotify
        ? new Date(now + input.cooldownMs).toISOString()
        : existing?.cooldownUntil ?? null;
    const { incident } = this.store.upsertIncident(
      {
        key: input.key,
        lastSeen: new Date(now).toISOString(),
        affectedJobs: input.affectedJobs,
        remediation: input.remediation,
        cooldownUntil,
        autoResolveCondition: input.autoResolveCondition,
        state: "open",
      },
      buildIncidentLifecycleEvent(
        input.key,
        "incident-opened",
        input.host,
        this.now,
      ),
      sourceRevision,
    );
    if (shouldNotify && input.notification) {
      this.notifier.notify({ severity: input.notification, incident });
    }
    return incident;
  }

  private resolve(
    key: string,
    host: string,
    sourceRevision?: number,
  ): IncidentRecord | null {
    return this.store.resolveIncident(
      key,
      new Date(this.now()).toISOString(),
      buildIncidentLifecycleEvent(key, "incident-resolved", host, this.now),
      sourceRevision,
    );
  }
}
