/**
 * LegacyQueueCommandBridge — WP-12.
 *
 * Old queue API shape (enqueue / pause / resume / clear running / claim next)
 * writes through to per-worker DoorbellMailbox + hold markers.
 *
 * Shared agent-task-queue.json is refreshed as a **derived** projection only.
 *
 * Hard rules:
 * - Do NOT use shell doorbell-buffer as a delivery queue.
 * - Do NOT treat shared JSON as Core truth.
 * - Do NOT enable production Active / rewire TaskDispatcher delivery here.
 */

import type { DoorbellMessage } from "../contracts/DoorbellMessage.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";
import {
  DoorbellMailbox,
  type EnqueueResult,
} from "../store/DoorbellMailbox.ts";
import {
  AgentQueueProjection,
  sortPendingByPriorityFifo,
  type DerivedAgentTaskQueueFile,
  type PriorityRank,
} from "./AgentQueueProjection.js";
import {
  QueueHoldStore,
  type QueueHoldRecord,
} from "./QueueHoldStore.js";

export interface BridgeEnqueueInput {
  workerId: string;
  taskId: string;
  dedupeKey: string;
  summary?: string;
  topic?: string;
  priority?: PriorityRank;
  sourceFactRefs?: string[];
  createdAt?: string;
  doorbellId?: string;
}

export interface BridgeEnqueueResult {
  enqueue: EnqueueResult;
  /** False when duplicate — mailbox unchanged. */
  accepted: boolean;
  projection?: DerivedAgentTaskQueueFile;
}

export interface LegacyQueueCommandBridgeOptions {
  paths: SwitchboardPaths;
  /** Known worker ids for projection refresh. */
  workerIds?: string[];
  projectRoot?: string;
  /** When true (default), rewrite derived agent-task-queue.json after mutations. */
  writeDerived?: boolean;
  now?: () => Date;
}

function makeDoorbell(input: BridgeEnqueueInput, now: Date): DoorbellMessage {
  const taskId = input.taskId.trim().toUpperCase();
  const doorbellId =
    input.doorbellId?.trim() ||
    `db-${taskId.toLowerCase()}-${now.getTime()}`;
  return {
    schema: "codeflowmu.doorbell/v1",
    doorbellId,
    recipientWorkerId: input.workerId,
    topic: input.topic ?? "task.ready",
    sourceFactRefs: input.sourceFactRefs ?? [taskId],
    summary: input.summary ?? `Queued ${taskId}`,
    priority: input.priority ?? "P2",
    dedupeKey: input.dedupeKey,
    createdAt: input.createdAt ?? now.toISOString(),
    extension: { taskId },
  };
}

export class LegacyQueueCommandBridge {
  readonly #paths: SwitchboardPaths;
  readonly #mailbox: DoorbellMailbox;
  readonly #holds: QueueHoldStore;
  readonly #projection: AgentQueueProjection;
  readonly #projectRoot?: string;
  readonly #writeDerived: boolean;
  readonly #now: () => Date;
  #workerIds: string[];

  constructor(opts: LegacyQueueCommandBridgeOptions) {
    this.#paths = opts.paths;
    this.#mailbox = new DoorbellMailbox(opts.paths);
    this.#holds = new QueueHoldStore(opts.paths);
    this.#projection = new AgentQueueProjection({
      paths: opts.paths,
      mailbox: this.#mailbox,
      holds: this.#holds,
      now: opts.now,
    });
    this.#projectRoot = opts.projectRoot;
    this.#writeDerived = opts.writeDerived !== false;
    this.#now = opts.now ?? (() => new Date());
    this.#workerIds = [...(opts.workerIds ?? [])];
  }

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

  get projection(): AgentQueueProjection {
    return this.#projection;
  }

  /** Explicit non-goal: activity/doorbell-buffer is observation, not delivery. */
  static readonly DELIVERY_QUEUE_IS =
    "per-worker DoorbellMailbox (file doorbells)" as const;
  static readonly DELIVERY_QUEUE_IS_NOT =
    "shell doorbell-buffer / activity event buffer" as const;

  registerWorker(workerId: string): void {
    if (!this.#workerIds.includes(workerId)) {
      this.#workerIds.push(workerId);
    }
  }

  async enqueue(input: BridgeEnqueueInput): Promise<BridgeEnqueueResult> {
    this.registerWorker(input.workerId);
    const msg = makeDoorbell(input, this.#now());
    const enqueue = await this.#mailbox.enqueue(msg);
    const projection = await this.#refresh();
    return {
      enqueue,
      accepted: enqueue.status === "enqueued",
      projection,
    };
  }

  /**
   * Claim next pending doorbell for a worker (priority + FIFO).
   * If worker already has a claimed item, returns busy — new work stays queued.
   */
  async claimNext(
    workerId: string,
  ): Promise<
    | { status: "claimed"; message: DoorbellMessage; path: string }
    | { status: "busy"; claimed: DoorbellMessage[] }
    | { status: "empty" }
  > {
    this.registerWorker(workerId);
    const claimed = await this.#mailbox.list(workerId, "claimed");
    if (claimed.length > 0) {
      return { status: "busy", claimed };
    }
    const pending = sortPendingByPriorityFifo(
      await this.#mailbox.list(workerId, "pending"),
    );
    // Skip paused
    const holds = await this.#holds.list(workerId);
    const paused = new Set(holds.map((h) => h.taskId.toUpperCase()));
    const next = pending.find((m) => {
      const tid =
        typeof m.extension?.["taskId"] === "string"
          ? String(m.extension["taskId"]).toUpperCase()
          : m.dedupeKey.toUpperCase();
      return !paused.has(tid);
    });
    if (!next) {
      await this.#refresh();
      return { status: "empty" };
    }
    const path = await this.#mailbox.claim(workerId, next.doorbellId);
    await this.#refresh();
    return { status: "claimed", message: next, path };
  }

  async pause(input: {
    workerId: string;
    taskId: string;
    pausedBy?: string;
    pauseReason?: string;
    doorbellId?: string;
  }): Promise<QueueHoldRecord> {
    this.registerWorker(input.workerId);
    const record: QueueHoldRecord = {
      schema: "codeflowmu.queue-hold/v1",
      taskId: input.taskId.trim().toUpperCase(),
      workerId: input.workerId,
      pausedAt: this.#now().toISOString(),
      pausedBy: input.pausedBy ?? "PM",
      pauseReason: input.pauseReason ?? "paused",
      doorbellId: input.doorbellId,
    };
    await this.#holds.pause(record);
    await this.#refresh();
    return record;
  }

  async resume(workerId: string, taskId: string): Promise<boolean> {
    this.registerWorker(workerId);
    const ok = await this.#holds.resume(workerId, taskId.trim().toUpperCase());
    await this.#refresh();
    return ok;
  }

  /**
   * Clear running (= settle claimed doorbell). Pending queue untouched.
   */
  async clearRunning(
    workerId: string,
  ): Promise<{ status: "settled" | "idle"; doorbellId?: string }> {
    this.registerWorker(workerId);
    const claimed = await this.#mailbox.list(workerId, "claimed");
    if (!claimed[0]) {
      await this.#refresh();
      return { status: "idle" };
    }
    const id = claimed[0].doorbellId;
    await this.#mailbox.settle(workerId, id);
    await this.#refresh();
    return { status: "settled", doorbellId: id };
  }

  async readProjection(): Promise<DerivedAgentTaskQueueFile> {
    return this.#projection.project(this.#workerIds);
  }

  async #refresh(): Promise<DerivedAgentTaskQueueFile | undefined> {
    if (!this.#writeDerived || !this.#projectRoot) {
      return this.#projection.project(this.#workerIds);
    }
    const { file } = await this.#projection.writeDerivedFile(
      this.#projectRoot,
      this.#workerIds,
    );
    return file;
  }
}
