/**
 * HostSwitchboard — WP-08 formal Host wake / Run path.
 *
 * Flow (when mode=shadow|active and authorized for active):
 *   claim Doorbell → load Binding → host claim → deliveryAttemptId →
 *   RunRecord starting → adapter.startRun → running → events → settle
 *
 * delivery_unknown: after Host accept / before running persisted (or inspect fails).
 * high/irreversible risk: never auto-redeliver on delivery_unknown.
 *
 * Default production posture: mode=off; Active requires Gate B token.
 * Does not rewire TaskDispatcher — callers opt in explicitly.
 */

import { randomUUID } from "node:crypto";

import type { AgentHostAdapter } from "./contracts/AgentHostAdapter.ts";
import type { RunInspectionCapableHost } from "./contracts/AgentHostAdapter.ts";
import type { HostRunRef } from "./contracts/HostRun.ts";
import type { HostSessionRef } from "./contracts/HostSession.ts";
import type { RunRecord } from "./contracts/RunRecord.ts";
import type { DoorbellMessage } from "./contracts/DoorbellMessage.ts";
import { AgentHostRegistry } from "./hosts/AgentHostRegistry.ts";
import { SwitchboardPaths } from "./paths/SwitchboardPaths.ts";
import { DoorbellMailbox } from "./store/DoorbellMailbox.ts";
import { HostClaimStore } from "./store/HostClaimStore.ts";
import { RunStore } from "./store/RunStore.ts";
import { WorkerBindingStore } from "./store/WorkerBindingStore.ts";
import {
  DualDeliveryGuard,
} from "./DualDeliveryGuard.ts";
import type {
  ActiveAuthorization,
  SwitchboardDispatchMode,
} from "./SwitchboardDispatchMode.ts";
import {
  SwitchboardModeError,
  assertActiveAuthorized,
} from "./SwitchboardDispatchMode.ts";

export interface HostSwitchboardOptions {
  paths: SwitchboardPaths;
  registry: AgentHostRegistry;
  mode?: SwitchboardDispatchMode;
  activeAuthorization?: ActiveAuthorization | null;
  dualDeliveryGuard?: DualDeliveryGuard;
  mailbox?: DoorbellMailbox;
  bindings?: WorkerBindingStore;
  runs?: RunStore;
  claims?: HostClaimStore;
  now?: () => Date;
  idFactory?: () => string;
  /**
   * Test hook: after startRun returns, before writing state=running.
   * Throw / crash simulation for delivery_unknown.
   */
  afterHostAcceptedBeforeRunningWrite?: (ctx: {
    runId: string;
    deliveryAttemptId: string;
  }) => void | Promise<void>;
}

export interface WakeWorkerInput {
  workerId: string;
  doorbellId: string;
  /** Optional prompt override; defaults to doorbell summary. */
  prompt?: string;
  roleId?: string;
}

export type WakeWorkerResult =
  | {
      status: "started" | "completed";
      runId: string;
      deliveryAttemptId: string;
      doorbellId: string;
      hostId: string;
      externalRunId?: string;
      mode: SwitchboardDispatchMode;
      invokedHostStartRun: true;
    }
  | {
      status: "delivery_unknown";
      runId: string;
      deliveryAttemptId: string;
      doorbellId: string;
      hostId: string;
      mode: SwitchboardDispatchMode;
      reason: string;
      autoRedeliver: false;
    }
  | {
      status: "blocked";
      reason: string;
      mode: SwitchboardDispatchMode;
    };

function isInspectCapable(
  adapter: AgentHostAdapter,
): adapter is AgentHostAdapter & RunInspectionCapableHost {
  return (
    typeof (adapter as unknown as RunInspectionCapableHost).inspectRun ===
    "function"
  );
}

export class HostSwitchboard {
  readonly #paths: SwitchboardPaths;
  readonly #registry: AgentHostRegistry;
  readonly #mailbox: DoorbellMailbox;
  readonly #bindings: WorkerBindingStore;
  readonly #runs: RunStore;
  readonly #claims: HostClaimStore;
  readonly #guard: DualDeliveryGuard;
  readonly #now: () => Date;
  readonly #idFactory: () => string;
  readonly #afterHostAcceptedBeforeRunningWrite?: HostSwitchboardOptions["afterHostAcceptedBeforeRunningWrite"];
  #mode: SwitchboardDispatchMode;
  #auth: ActiveAuthorization | null;

  constructor(opts: HostSwitchboardOptions) {
    this.#paths = opts.paths;
    this.#registry = opts.registry;
    this.#mailbox = opts.mailbox ?? new DoorbellMailbox(opts.paths);
    this.#bindings = opts.bindings ?? new WorkerBindingStore(opts.paths);
    this.#runs = opts.runs ?? new RunStore(opts.paths);
    this.#claims = opts.claims ?? new HostClaimStore(opts.paths);
    this.#mode = opts.mode ?? "off";
    this.#auth = opts.activeAuthorization ?? null;
    if (this.#mode === "active") {
      assertActiveAuthorized(this.#mode, this.#auth);
    }
    this.#guard =
      opts.dualDeliveryGuard ??
      new DualDeliveryGuard({
        mode: this.#mode,
        activeAuthorization: this.#auth,
      });
    this.#now = opts.now ?? (() => new Date());
    this.#idFactory = opts.idFactory ?? (() => randomUUID());
    this.#afterHostAcceptedBeforeRunningWrite =
      opts.afterHostAcceptedBeforeRunningWrite;
  }

  get mode(): SwitchboardDispatchMode {
    return this.#mode;
  }

  get paths(): SwitchboardPaths {
    return this.#paths;
  }

  get dualDeliveryGuard(): DualDeliveryGuard {
    return this.#guard;
  }

  get mailbox(): DoorbellMailbox {
    return this.#mailbox;
  }

  get runs(): RunStore {
    return this.#runs;
  }

  setMode(
    mode: SwitchboardDispatchMode,
    auth?: ActiveAuthorization | null,
  ): void {
    this.#guard.setMode(mode, auth);
    this.#mode = mode;
    this.#auth = auth ?? (mode === "active" ? this.#auth : null);
  }

  emergencyStop(reason = "emergency_stop"): void {
    this.#guard.emergencyStop(reason);
    this.#mode = "off";
    this.#auth = null;
  }

  /**
   * Resolve an existing delivery_unknown run via Host inspectRun when available.
   * Never auto-redelivers high / irreversible risk doorbells.
   */
  async resolveDeliveryUnknown(input: {
    workerId: string;
    runId: string;
  }): Promise<{
    resolved: boolean;
    state: RunRecord["state"];
    autoRedeliver: boolean;
    reason: string;
  }> {
    const found = await this.#runs.get(input.workerId, input.runId);
    if (!found) {
      return {
        resolved: false,
        state: "delivery_unknown",
        autoRedeliver: false,
        reason: "run_not_found",
      };
    }
    const record = found.record;
    if (record.state !== "delivery_unknown") {
      return {
        resolved: true,
        state: record.state,
        autoRedeliver: false,
        reason: "already_settled",
      };
    }

    const doorbell = await this.#loadDoorbellAnyBucket(
      input.workerId,
      record.doorbellId,
    );
    const risk = doorbell?.riskLevel;
    const autoRedeliverForbidden =
      risk === "high" || risk === "irreversible" || risk === undefined;

    let adapter: AgentHostAdapter;
    try {
      adapter = this.#registry.get(record.hostId);
    } catch {
      return {
        resolved: false,
        state: "delivery_unknown",
        autoRedeliver: false,
        reason: "host_missing",
      };
    }

    if (!isInspectCapable(adapter) || !record.externalRunId) {
      return {
        resolved: false,
        state: "delivery_unknown",
        autoRedeliver: false,
        reason: "inspect_unavailable",
      };
    }

    const binding = await this.#bindings.latest(input.workerId);
    const session: HostSessionRef = binding?.host ?? {
      hostId: record.hostId,
      externalSessionId: "unknown",
    };
    const runRef: HostRunRef = {
      hostId: record.hostId,
      externalRunId: record.externalRunId,
      session,
    };
    const inspected = await adapter.inspectRun(runRef);
    if (inspected === "running" || inspected === "queued") {
      const next: RunRecord = {
        ...record,
        state: "running",
        startedAt: record.startedAt ?? this.#now().toISOString(),
      };
      await this.#runs.put(next);
      return {
        resolved: true,
        state: "running",
        autoRedeliver: false,
        reason: `inspect=${inspected}`,
      };
    }
    if (inspected === "completed" || inspected === "failed" || inspected === "cancelled") {
      const next: RunRecord = {
        ...record,
        state:
          inspected === "completed"
            ? "completed"
            : inspected === "cancelled"
              ? "cancelled"
              : "failed",
        settledAt: this.#now().toISOString(),
      };
      await this.#runs.put(next);
      await this.#mailbox.settle(input.workerId, record.doorbellId).catch(() => undefined);
      return {
        resolved: true,
        state: next.state,
        autoRedeliver: false,
        reason: `inspect=${inspected}`,
      };
    }

    return {
      resolved: false,
      state: "delivery_unknown",
      autoRedeliver: autoRedeliverForbidden ? false : false,
      reason: autoRedeliverForbidden
        ? `inspect=unknown; auto_redeliver_forbidden risk=${risk ?? "unset"}`
        : "inspect=unknown; auto_redeliver_not_implemented",
    };
  }

  async wakeWorker(input: WakeWorkerInput): Promise<WakeWorkerResult> {
    if (this.#mode === "off" || this.#guard.emergencyStopped) {
      return {
        status: "blocked",
        reason: "mode=off (or emergency stop) — HostSwitchboard will not wake",
        mode: this.#mode,
      };
    }
    if (this.#mode === "active") {
      try {
        assertActiveAuthorized(this.#mode, this.#auth);
      } catch (err) {
        return {
          status: "blocked",
          reason: err instanceof Error ? err.message : String(err),
          mode: this.#mode,
        };
      }
    }

    const workerId = String(input.workerId ?? "").trim();
    const doorbellId = String(input.doorbellId ?? "").trim();
    const workKey = `${workerId}:${doorbellId}`;

    try {
      this.#guard.assertSwitchboardSendAllowed(workKey);
    } catch (err) {
      return {
        status: "blocked",
        reason: err instanceof Error ? err.message : String(err),
        mode: this.#mode,
      };
    }

    const pending = await this.#mailbox.list(workerId, "pending");
    const message = pending.find((m) => m.doorbellId === doorbellId);
    if (!message) {
      return {
        status: "blocked",
        reason: `doorbell not pending: ${doorbellId}`,
        mode: this.#mode,
      };
    }

    // Atomic-ish claim: move pending → claimed (second wake fails).
    await this.#mailbox.claim(workerId, doorbellId);

    const binding = await this.#bindings.latest(workerId);
    if (!binding || binding.state === "closed" || binding.state === "lost") {
      await this.#mailbox.fail(workerId, doorbellId).catch(() => undefined);
      return {
        status: "blocked",
        reason: "worker binding missing or not ready",
        mode: this.#mode,
      };
    }

    const hostId = binding.host.hostId;
    let adapter: AgentHostAdapter;
    try {
      adapter = this.#registry.get(hostId);
    } catch {
      await this.#mailbox.fail(workerId, doorbellId).catch(() => undefined);
      return {
        status: "blocked",
        reason: `host not registered: ${hostId}`,
        mode: this.#mode,
      };
    }

    const sessionKey = `${hostId}::${binding.host.externalSessionId}`;
    await this.#claims.acquire({
      schema: "codeflowmu.host-claim/v1",
      hostId,
      sessionKey,
      workerId,
      acquiredAt: this.#now().toISOString(),
      metadata: { doorbellId, mode: this.#mode },
    });

    const runId = `run-${this.#idFactory()}`;
    const deliveryAttemptId = `attempt-${this.#idFactory()}`;

    // Persist starting + deliveryAttemptId BEFORE external Host call.
    const starting: RunRecord = {
      schema: "codeflowmu.run/v1",
      runId,
      workerId,
      doorbellId,
      bindingId: binding.bindingId,
      hostId,
      deliveryAttemptId,
      state: "starting",
      taskId:
        typeof message.extension?.["taskId"] === "string"
          ? message.extension["taskId"]
          : undefined,
      sourceFactRefs: [...message.sourceFactRefs],
      continuationMode: "same_session",
      startedAt: this.#now().toISOString(),
    };
    await this.#runs.put(starting);

    let hostRef: HostRunRef;
    try {
      // Ensure session exists on host (idempotent prepare with existing).
      await adapter.prepareSession({
        worker: {
          workerId,
          roleId: input.roleId?.trim() || "DEV",
          layer: "worker",
        },
        existing: binding.host,
      });
      hostRef = await adapter.startRun(binding.host, {
        runId,
        workerId,
        prompt: input.prompt?.trim() || message.summary,
        deliveryAttemptId,
        sourceFactRefs: message.sourceFactRefs,
        modelHint: binding.modelHint,
      });
    } catch (err) {
      const failed: RunRecord = {
        ...starting,
        state: "failed",
        settledAt: this.#now().toISOString(),
        failure: adapter.classifyError(err),
      };
      await this.#runs.put(failed);
      await this.#mailbox.fail(workerId, doorbellId).catch(() => undefined);
      await this.#claims.release(hostId, sessionKey).catch(() => undefined);
      return {
        status: "blocked",
        reason: err instanceof Error ? err.message : String(err),
        mode: this.#mode,
      };
    }

    // Crash window: Host accepted, running not yet written.
    try {
      await this.#afterHostAcceptedBeforeRunningWrite?.({
        runId,
        deliveryAttemptId,
      });
    } catch (err) {
      const unknown: RunRecord = {
        ...starting,
        state: "delivery_unknown",
        externalRunId: hostRef.externalRunId,
        settledAt: this.#now().toISOString(),
        failure: {
          kind: "unknown",
          message: err instanceof Error ? err.message : String(err),
          retryable: false,
        },
      };
      await this.#runs.put(unknown);
      await this.#mailbox.fail(workerId, doorbellId).catch(() => undefined);
      await this.#claims.release(hostId, sessionKey).catch(() => undefined);
      return {
        status: "delivery_unknown",
        runId,
        deliveryAttemptId,
        doorbellId,
        hostId,
        mode: this.#mode,
        reason: "host_accepted_before_running_write",
        autoRedeliver: false,
      };
    }

    const running: RunRecord = {
      ...starting,
      state: "running",
      externalRunId: hostRef.externalRunId,
    };
    await this.#runs.put(running);

    // Drain events for InMemory / sync hosts (E2E smoke).
    for await (const _evt of adapter.streamEvents(hostRef)) {
      void _evt;
    }

    const completed: RunRecord = {
      ...running,
      state: "completed",
      settledAt: this.#now().toISOString(),
    };
    await this.#runs.put(completed);
    await this.#mailbox.settle(workerId, doorbellId);
    await this.#claims.release(hostId, sessionKey).catch(() => undefined);

    return {
      status: "completed",
      runId,
      deliveryAttemptId,
      doorbellId,
      hostId,
      externalRunId: hostRef.externalRunId,
      mode: this.#mode,
      invokedHostStartRun: true,
    };
  }

  async #loadDoorbellAnyBucket(
    workerId: string,
    doorbellId: string,
  ): Promise<DoorbellMessage | null> {
    for (const bucket of ["pending", "claimed", "settled", "failed"] as const) {
      const list = await this.#mailbox.list(workerId, bucket);
      const hit = list.find((m) => m.doorbellId === doorbellId);
      if (hit) return hit;
    }
    return null;
  }
}
