/**
 * SwitchboardHealth — WP-14 startup / runtime health checks.
 * Read-only probes; does not enable production Active delivery.
 */

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

import { SwitchboardPaths } from "../paths/SwitchboardPaths.js";
import {
  SWITCHBOARD_LAYOUT_VERSION,
  SwitchboardVersionStore,
} from "../store/SwitchboardVersionStore.js";
import { DoorbellMailbox } from "../store/DoorbellMailbox.js";
import { HostClaimStore } from "../store/HostClaimStore.js";
import { RunStore } from "../store/RunStore.js";
import { WorkerBindingStore } from "../store/WorkerBindingStore.js";

export type HealthSeverity = "ok" | "warn" | "error";

export interface HealthFinding {
  code: string;
  severity: HealthSeverity;
  message: string;
  detail?: Record<string, unknown>;
}

export interface SwitchboardHealthReport {
  schema: "codeflowmu.switchboard-health/v1";
  generatedAt: string;
  overall: HealthSeverity;
  findings: HealthFinding[];
  productionActiveEnabled: false;
}

export interface SwitchboardHealthInput {
  paths: SwitchboardPaths;
  workerIds: string[];
  hostIds?: string[];
  /** Optional FCoP / project root for workspace readability. */
  projectRoot?: string;
  now?: () => Date;
}

function worst(
  a: HealthSeverity,
  b: HealthSeverity,
): HealthSeverity {
  const rank = { ok: 0, warn: 1, error: 2 } as const;
  return rank[a] >= rank[b] ? a : b;
}

export class SwitchboardHealth {
  async check(input: SwitchboardHealthInput): Promise<SwitchboardHealthReport> {
    const now = input.now?.() ?? new Date();
    const findings: HealthFinding[] = [];
    const versions = new SwitchboardVersionStore(input.paths);
    const mailbox = new DoorbellMailbox(input.paths);
    const runs = new RunStore(input.paths);
    const bindings = new WorkerBindingStore(input.paths);
    const claims = new HostClaimStore(input.paths);

    // 1. layout version
    const ver = await versions.read();
    if (!ver) {
      findings.push({
        code: "version.missing",
        severity: "warn",
        message: "switchboard VERSION.json missing — migration may be needed",
      });
    } else if (ver.layoutVersion !== SWITCHBOARD_LAYOUT_VERSION) {
      findings.push({
        code: "version.mismatch",
        severity: "error",
        message: `layoutVersion ${ver.layoutVersion} != expected ${SWITCHBOARD_LAYOUT_VERSION}`,
        detail: { found: ver.layoutVersion, expected: SWITCHBOARD_LAYOUT_VERSION },
      });
    } else {
      findings.push({
        code: "version.ok",
        severity: "ok",
        message: `layoutVersion=${ver.layoutVersion}`,
      });
    }

    // 2–6. per-worker mailbox / binding / runs
    for (const workerId of input.workerIds) {
      const pending = await mailbox.list(workerId, "pending");
      const claimed = await mailbox.list(workerId, "claimed");
      const failed = await mailbox.list(workerId, "failed");
      const activeRuns = await runs.list(workerId, "active");
      const settled = await runs.list(workerId, "settled");
      const latest = await bindings.latest(workerId);

      if (!latest) {
        findings.push({
          code: "binding.missing",
          severity: "warn",
          message: `worker ${workerId} has no binding`,
          detail: { workerId },
        });
      } else if (latest.state === "lost" || latest.state === "closed") {
        findings.push({
          code: "binding.unusable",
          severity: "warn",
          message: `worker ${workerId} binding state=${latest.state}`,
          detail: { workerId, bindingId: latest.bindingId, state: latest.state },
        });
      }

      if (claimed.length > 0 && activeRuns.length === 0) {
        findings.push({
          code: "doorbell.claimed_orphaned",
          severity: "warn",
          message: `worker ${workerId} has claimed doorbell(s) without active run`,
          detail: {
            workerId,
            claimed: claimed.map((d) => d.doorbellId),
          },
        });
      }

      const starting = activeRuns.filter((r) => r.state === "starting");
      for (const r of starting) {
        findings.push({
          code: "run.starting_unconfirmed",
          severity: "warn",
          message: `run ${r.runId} stuck in starting`,
          detail: { workerId, runId: r.runId, doorbellId: r.doorbellId },
        });
      }

      const deliveryUnknown = settled.filter(
        (r) => r.state === "delivery_unknown",
      );
      if (deliveryUnknown.length > 0) {
        findings.push({
          code: "run.delivery_unknown",
          severity: "warn",
          message: `worker ${workerId} has ${deliveryUnknown.length} delivery_unknown run(s)`,
          detail: {
            workerId,
            runIds: deliveryUnknown.map((r) => r.runId),
          },
        });
      }

      findings.push({
        code: "mailbox.counts",
        severity: "ok",
        message: `worker ${workerId} pending=${pending.length} claimed=${claimed.length} failed=${failed.length}`,
        detail: {
          workerId,
          pending: pending.length,
          claimed: claimed.length,
          failed: failed.length,
        },
      });
    }

    // 4. expired claims
    for (const hostId of input.hostIds ?? []) {
      let names: string[] = [];
      try {
        names = await fs.readdir(input.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()) {
          findings.push({
            code: "claim.expired",
            severity: "warn",
            message: `expired claim ${hostId}/${sessionKey}`,
            detail: {
              hostId,
              sessionKey,
              expiresAt: claim.expiresAt,
              workerId: claim.workerId,
            },
          });
        }
      }
    }

    // 11. FCoP workspace readable
    if (input.projectRoot) {
      const fcop = join(input.projectRoot, "fcop");
      try {
        await fs.access(fcop);
        findings.push({
          code: "fcop.readable",
          severity: "ok",
          message: `fcop root readable: ${fcop}`,
        });
      } catch {
        findings.push({
          code: "fcop.unreadable",
          severity: "error",
          message: `fcop root not readable: ${fcop}`,
        });
      }
    }

    let overall: HealthSeverity = "ok";
    for (const f of findings) overall = worst(overall, f.severity);

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