/**
 * DoorbellMailbox — per-worker file mailbox (WP-02). Not an RPC channel.
 * Dedupes by dedupeKey across buckets for a single worker.
 */

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

import { renderFrontmatter } from "../../ledger/frontmatter.ts";
import { AtomicFile } from "../atomic/AtomicFile.ts";
import type { DoorbellMessage } from "../contracts/DoorbellMessage.ts";
import {
  SwitchboardPaths,
  type MailboxBucket,
} from "../paths/SwitchboardPaths.ts";

const BUCKETS: MailboxBucket[] = ["pending", "claimed", "settled", "failed"];

function doorbellMarkdown(msg: DoorbellMessage, bucket: MailboxBucket): string {
  return [
    renderFrontmatter({
      protocol: "codeflowmu.switchboard",
      schema: msg.schema,
      doorbell_id: msg.doorbellId,
      worker_id: msg.recipientWorkerId,
      topic: msg.topic,
      priority: msg.priority,
      dedupe_key: msg.dedupeKey,
      bucket,
      created_at: msg.createdAt,
    }),
    "",
    `# Doorbell ${msg.doorbellId}`,
    "",
    msg.summary,
    "",
    "```json",
    JSON.stringify(msg, null, 2),
    "```",
    "",
  ].join("\n");
}

function parseDoorbell(raw: string): DoorbellMessage {
  const jsonMatch = raw.match(/```json\s*([\s\S]*?)```/);
  if (!jsonMatch?.[1]) {
    throw new Error("doorbell markdown missing json payload");
  }
  return JSON.parse(jsonMatch[1]) as DoorbellMessage;
}

export type EnqueueResult =
  | { status: "enqueued"; path: string; bucket: "pending" }
  | { status: "duplicate"; path: string; bucket: MailboxBucket };

export class DoorbellMailbox {
  readonly #paths: SwitchboardPaths;

  constructor(paths: SwitchboardPaths) {
    this.#paths = paths;
  }

  async findByDedupeKey(
    workerId: string,
    dedupeKey: string,
  ): Promise<{ bucket: MailboxBucket; path: string; message: DoorbellMessage } | null> {
    for (const bucket of BUCKETS) {
      const dir = this.#paths.mailboxDir(workerId, bucket);
      let names: string[] = [];
      try {
        names = await fs.readdir(dir);
      } catch {
        continue;
      }
      for (const name of names) {
        if (!name.endsWith(".md") || name.includes(".tmp")) continue;
        const path = join(dir, name);
        const msg = parseDoorbell(await AtomicFile.readText(path));
        if (msg.dedupeKey === dedupeKey) {
          return { bucket, path, message: msg };
        }
      }
    }
    return null;
  }

  async enqueue(message: DoorbellMessage): Promise<EnqueueResult> {
    const workerId = message.recipientWorkerId;
    const existing = await this.findByDedupeKey(workerId, message.dedupeKey);
    if (existing) {
      return {
        status: "duplicate",
        path: existing.path,
        bucket: existing.bucket,
      };
    }
    const path = this.#paths.doorbellFile(workerId, "pending", message.doorbellId);
    await AtomicFile.writeText(path, doorbellMarkdown(message, "pending"));
    return { status: "enqueued", path, bucket: "pending" };
  }

  async list(
    workerId: string,
    bucket: MailboxBucket,
  ): Promise<DoorbellMessage[]> {
    const dir = this.#paths.mailboxDir(workerId, bucket);
    let names: string[] = [];
    try {
      names = await fs.readdir(dir);
    } catch {
      return [];
    }
    const out: DoorbellMessage[] = [];
    for (const name of names.sort()) {
      if (!name.endsWith(".md") || name.includes(".tmp")) continue;
      out.push(parseDoorbell(await AtomicFile.readText(join(dir, name))));
    }
    return out;
  }

  async #move(
    workerId: string,
    doorbellId: string,
    from: MailboxBucket,
    to: MailboxBucket,
  ): Promise<string> {
    const src = this.#paths.doorbellFile(workerId, from, doorbellId);
    const dst = this.#paths.doorbellFile(workerId, to, doorbellId);
    const raw = await AtomicFile.readText(src);
    const msg = parseDoorbell(raw);
    await AtomicFile.writeText(dst, doorbellMarkdown(msg, to));
    await fs.unlink(src).catch(() => undefined);
    return dst;
  }

  async claim(workerId: string, doorbellId: string): Promise<string> {
    return this.#move(workerId, doorbellId, "pending", "claimed");
  }

  async settle(workerId: string, doorbellId: string): Promise<string> {
    // claimed → settled (also allow pending → settled for crash recovery)
    const claimed = this.#paths.doorbellFile(workerId, "claimed", doorbellId);
    if (await AtomicFile.exists(claimed)) {
      return this.#move(workerId, doorbellId, "claimed", "settled");
    }
    return this.#move(workerId, doorbellId, "pending", "settled");
  }

  async fail(workerId: string, doorbellId: string): Promise<string> {
    const claimed = this.#paths.doorbellFile(workerId, "claimed", doorbellId);
    if (await AtomicFile.exists(claimed)) {
      return this.#move(workerId, doorbellId, "claimed", "failed");
    }
    return this.#move(workerId, doorbellId, "pending", "failed");
  }

  /** Crash recovery for one worker mailbox tree. */
  async recoverOrphans(workerId: string): Promise<string[]> {
    const removed: string[] = [];
    for (const bucket of BUCKETS) {
      const dir = this.#paths.mailboxDir(workerId, bucket);
      const result = await AtomicFile.recoverOrphanTmp(dir);
      removed.push(...result.removed.map((n) => `${bucket}/${n}`));
    }
    return removed;
  }
}

export function doorbellBasename(path: string): string {
  return basename(path, ".md");
}
