/**
 * SwitchboardDiagnostics — WP-14.
 * Diagnose Binding/claim damage and emit auditable repair actions.
 * Auto-repairs MUST leave an audit record; never silent rewrite.
 */

import { promises as fs } from "node:fs";
import { join } from "node:path";

import { AtomicFile } from "../atomic/AtomicFile.js";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.js";
import { DoorbellMailbox } from "../store/DoorbellMailbox.js";
import { HostClaimStore } from "../store/HostClaimStore.js";
import { RunStore } from "../store/RunStore.js";
import type { RunRecord } from "../contracts/RunRecord.js";
import {
  SwitchboardHealth,
  type SwitchboardHealthReport,
} from "./SwitchboardHealth.js";

export const DIAGNOSTICS_AUDIT_SCHEMA =
  "codeflowmu.switchboard-diagnostics-audit/v1" as const;

export interface DiagnosticsAuditEntry {
  at: string;
  action: string;
  target: string;
  reason: string;
  result: "applied" | "skipped" | "failed";
  detail?: Record<string, unknown>;
}

export interface DiagnosticsReport {
  schema: "codeflowmu.switchboard-diagnostics/v1";
  generatedAt: string;
  health: SwitchboardHealthReport;
  audits: DiagnosticsAuditEntry[];
  productionActiveEnabled: false;
}

export interface DiagnosticsOptions {
  paths: SwitchboardPaths;
  workerIds: string[];
  hostIds?: string[];
  projectRoot?: string;
  /** When true, apply safe repairs with audit trail. */
  applyRepairs?: boolean;
  now?: () => Date;
}

export class SwitchboardDiagnostics {
  readonly #health = new SwitchboardHealth();

  async diagnose(opts: DiagnosticsOptions): Promise<DiagnosticsReport> {
    const now = opts.now?.() ?? new Date();
    const health = await this.#health.check({
      paths: opts.paths,
      workerIds: opts.workerIds,
      hostIds: opts.hostIds,
      projectRoot: opts.projectRoot,
      now: () => now,
    });
    const audits: DiagnosticsAuditEntry[] = [];

    if (opts.applyRepairs) {
      const mailbox = new DoorbellMailbox(opts.paths);
      const runs = new RunStore(opts.paths);
      const claims = new HostClaimStore(opts.paths);

      // Recover orphan tmp files (safe)
      for (const workerId of opts.workerIds) {
        const removed = await mailbox.recoverOrphans(workerId);
        if (removed.length > 0) {
          audits.push({
            at: now.toISOString(),
            action: "recover_mailbox_orphans",
            target: workerId,
            reason: "orphan .tmp after crash",
            result: "applied",
            detail: { removed },
          });
        }
        const runRemoved = await runs.recoverOrphans(workerId);
        if (runRemoved.length > 0) {
          audits.push({
            at: now.toISOString(),
            action: "recover_run_orphans",
            target: workerId,
            reason: "orphan run .tmp after crash",
            result: "applied",
            detail: { removed: runRemoved },
          });
        }

        // Mark starting runs without confirmation as delivery_unknown (audited)
        const active = await runs.list(workerId, "active");
        for (const run of active.filter((r) => r.state === "starting")) {
          const next: RunRecord = {
            ...run,
            state: "delivery_unknown",
            settledAt: now.toISOString(),
            failure: {
              kind: "delivery_unknown",
              message: "WP-14 diagnostics: starting without confirmation",
              retryable: false,
              rawCode: "diagnostics.starting_unconfirmed",
            },
          };
          await runs.put(next);
          audits.push({
            at: now.toISOString(),
            action: "mark_delivery_unknown",
            target: run.runId,
            reason: "starting without confirmed result",
            result: "applied",
            detail: { workerId, doorbellId: run.doorbellId },
          });
        }
      }

      // Drop expired claims (audited)
      for (const hostId of opts.hostIds ?? []) {
        let names: string[] = [];
        try {
          names = await fs.readdir(opts.paths.claimsDir(hostId));
        } catch {
          continue;
        }
        for (const name of names) {
          if (!name.endsWith(".claim") || name.includes(".tmp")) continue;
          const sessionKey = name.replace(/\.claim$/, "");
          const claim = await claims.get(hostId, sessionKey);
          if (!claim?.expiresAt) continue;
          if (Date.parse(claim.expiresAt) >= now.getTime()) continue;
          const path = opts.paths.claimFile(hostId, sessionKey);
          await fs.unlink(path).catch(() => undefined);
          audits.push({
            at: now.toISOString(),
            action: "release_expired_claim",
            target: `${hostId}/${sessionKey}`,
            reason: "claim expired",
            result: "applied",
            detail: { expiresAt: claim.expiresAt, workerId: claim.workerId },
          });
        }
      }

      await this.#writeAuditLog(opts.paths, audits, now);
    }

    return {
      schema: "codeflowmu.switchboard-diagnostics/v1",
      generatedAt: now.toISOString(),
      health,
      audits,
      productionActiveEnabled: false,
    };
  }

  async #writeAuditLog(
    paths: SwitchboardPaths,
    audits: DiagnosticsAuditEntry[],
    now: Date,
  ): Promise<void> {
    if (audits.length === 0) return;
    const dir = join(paths.root, "diagnostics");
    await fs.mkdir(dir, { recursive: true });
    const stamp = now.toISOString().replace(/[:.]/g, "-");
    const path = join(dir, `AUDIT-${stamp}.json`);
    await AtomicFile.writeJson(path, {
      schema: DIAGNOSTICS_AUDIT_SCHEMA,
      createdAt: now.toISOString(),
      entries: audits,
    });
  }
}
