/**
 * AgentQueueProjection — WP-12.
 *
 * Builds a legacy `agent-task-queue.json`-shaped view from per-worker
 * DoorbellMailbox + holds + optional active Runs.
 *
 * The shared JSON is **derived** only. It must never become Core truth again.
 * Doorbell activity buffers (shell doorbell-buffer) are NOT a delivery queue.
 */

import { mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";

import type {
  AgentQueueItem,
  AgentQueueSlot,
  AgentTaskQueueFile,
  PausedTaskEntry,
} from "../../pm/agentTaskQueue.ts";
import { AtomicFile } from "../atomic/AtomicFile.ts";
import type { DoorbellMessage } from "../contracts/DoorbellMessage.ts";
import type { RunRecord } from "../contracts/RunRecord.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import { DoorbellMailbox } from "../store/DoorbellMailbox.ts";
import { RunStore } from "../store/RunStore.ts";
import { QueueHoldStore, type QueueHoldRecord } from "./QueueHoldStore.ts";

export const QUEUE_PROJECTION_SOURCE = "switchboard-mailboxes" as const;

export type PriorityRank = "P0" | "P1" | "P2" | "P3";

const PRIORITY_WEIGHT: Record<PriorityRank, number> = {
  P0: 0,
  P1: 1,
  P2: 2,
  P3: 3, // lowest — legacy UI treats P3 as bottom
};

export interface DerivedAgentTaskQueueFile extends AgentTaskQueueFile {
  derived: true;
  source: typeof QUEUE_PROJECTION_SOURCE;
  /** Explicit: shared JSON is a UI/debug projection, not Core truth. */
  authoritative: false;
}

export interface ProjectionBuildOptions {
  paths: SwitchboardPaths;
  workerIds: string[];
  /** Optional project root — when set, also write derived JSON under pm-governance. */
  projectRoot?: string;
  now?: () => Date;
}

function taskIdFromDoorbell(msg: DoorbellMessage): string {
  const fromExt = msg.extension?.["taskId"];
  if (typeof fromExt === "string" && fromExt.trim()) {
    return fromExt.trim().toUpperCase();
  }
  for (const ref of msg.sourceFactRefs) {
    const m = String(ref).match(/TASK-\d{8}-\d+/i);
    if (m) return m[0]!.toUpperCase();
  }
  return msg.dedupeKey.toUpperCase();
}

/** Stable priority + FIFO (createdAt) ordering. P3 ranks lowest. */
export function compareDoorbellPriorityFifo(
  a: DoorbellMessage,
  b: DoorbellMessage,
): number {
  const wa = PRIORITY_WEIGHT[a.priority] ?? 99;
  const wb = PRIORITY_WEIGHT[b.priority] ?? 99;
  if (wa !== wb) return wa - wb;
  const ta = a.createdAt || "";
  const tb = b.createdAt || "";
  if (ta !== tb) return ta.localeCompare(tb);
  return a.doorbellId.localeCompare(b.doorbellId);
}

export function sortPendingByPriorityFifo(
  messages: DoorbellMessage[],
): DoorbellMessage[] {
  return [...messages].sort(compareDoorbellPriorityFifo);
}

function toQueueItem(msg: DoorbellMessage): AgentQueueItem {
  const taskId = taskIdFromDoorbell(msg);
  return {
    task_id: taskId,
    agent_id: msg.recipientWorkerId,
    queued_at: msg.createdAt,
    reason: msg.summary || msg.topic,
    role: msg.recipientWorkerId.split("-")[0],
    filename: `${msg.doorbellId}.md`,
    recipient: msg.recipientWorkerId,
  };
}

function holdToPaused(h: QueueHoldRecord): PausedTaskEntry {
  return {
    task_id: h.taskId.toUpperCase(),
    agent_id: h.workerId,
    dispatch_status: "paused",
    paused_at: h.pausedAt,
    paused_by: h.pausedBy,
    pause_reason: h.pauseReason,
  };
}

export class AgentQueueProjection {
  readonly #paths: SwitchboardPaths;
  readonly #mailbox: DoorbellMailbox;
  readonly #holds: QueueHoldStore;
  readonly #runs: RunStore;
  readonly #now: () => Date;

  constructor(opts: {
    paths: SwitchboardPaths;
    mailbox?: DoorbellMailbox;
    holds?: QueueHoldStore;
    runs?: RunStore;
    now?: () => Date;
  }) {
    this.#paths = opts.paths;
    this.#mailbox = opts.mailbox ?? new DoorbellMailbox(opts.paths);
    this.#holds = opts.holds ?? new QueueHoldStore(opts.paths);
    this.#runs = opts.runs ?? new RunStore(opts.paths);
    this.#now = opts.now ?? (() => new Date());
  }

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

  get holds(): QueueHoldStore {
    return this.#holds;
  }

  /**
   * Build legacy-shaped queue view from Mailbox truth.
   */
  async project(workerIds: string[]): Promise<DerivedAgentTaskQueueFile> {
    const agents: Record<string, AgentQueueSlot> = {};
    const paused: Record<string, PausedTaskEntry> = {};

    for (const workerId of workerIds) {
      const pending = sortPendingByPriorityFifo(
        await this.#mailbox.list(workerId, "pending"),
      );
      const claimed = await this.#mailbox.list(workerId, "claimed");
      const holds = await this.#holds.list(workerId);
      const holdTaskIds = new Set(
        holds.map((h) => h.taskId.toUpperCase()),
      );

      // Paused tasks are excluded from the live pending projection.
      const livePending = pending.filter(
        (m) => !holdTaskIds.has(taskIdFromDoorbell(m)),
      );

      let running: AgentQueueSlot["running"] = null;
      if (claimed[0]) {
        const msg = claimed[0];
        running = {
          task_id: taskIdFromDoorbell(msg),
          session_id: msg.doorbellId,
          started_at: msg.createdAt,
        };
      } else {
        // Fallback: active run file for this worker
        const activeRuns = await this.#listActiveRuns(workerId);
        const first = activeRuns[0];
        if (first?.taskId) {
          running = {
            task_id: first.taskId.toUpperCase(),
            session_id: first.runId,
            started_at: first.startedAt ?? this.#now().toISOString(),
          };
        }
      }

      agents[workerId] = {
        running,
        queue: livePending.map(toQueueItem),
      };

      for (const h of holds) {
        paused[h.taskId.toUpperCase()] = holdToPaused(h);
      }
    }

    return {
      version: "1.0.0",
      updated_at: this.#now().toISOString(),
      agents,
      paused,
      derived: true,
      source: QUEUE_PROJECTION_SOURCE,
      authoritative: false,
    };
  }

  /**
   * Write derived JSON for old UI/debug. Never treat this file as Core truth.
   */
  async writeDerivedFile(
    projectRoot: string,
    workerIds: string[],
  ): Promise<{ path: string; file: DerivedAgentTaskQueueFile }> {
    const file = await this.project(workerIds);
    const path = join(
      projectRoot,
      ".codeflowmu",
      "pm-governance",
      "agent-task-queue.json",
    );
    await mkdir(dirname(path), { recursive: true });
    await AtomicFile.writeText(path, `${JSON.stringify(file, null, 2)}\n`);
    return { path, file };
  }

  async #listActiveRuns(workerId: string): Promise<RunRecord[]> {
    try {
      return await this.#runs.list(workerId, "active");
    } catch {
      return [];
    }
  }
}

/** Guard helper for tests / audits. */
export function assertDerivedProjection(
  file: DerivedAgentTaskQueueFile | AgentTaskQueueFile,
): asserts file is DerivedAgentTaskQueueFile {
  const f = file as DerivedAgentTaskQueueFile;
  if (f.derived !== true || f.source !== QUEUE_PROJECTION_SOURCE) {
    throw new Error(
      "agent-task-queue.json must be marked derived from switchboard-mailboxes",
    );
  }
  if (f.authoritative !== false) {
    throw new Error("derived queue must set authoritative=false");
  }
}
