import path from "node:path";

export type MailboxBucket = "pending" | "claimed" | "settled" | "failed";
export type RunBucket = "active" | "settled";

export class SwitchboardPathError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "SwitchboardPathError";
  }
}

/**
 * Sanitize an id for use as a single path segment.
 * Original ids remain in file bodies; only filesystem names are sanitized.
 */
export function sanitizePathSegment(id: string, label = "id"): string {
  if (typeof id !== "string") {
    throw new SwitchboardPathError(`${label} must be a string`);
  }
  const trimmed = id.trim();
  if (trimmed.length === 0) {
    throw new SwitchboardPathError(`${label} must not be empty`);
  }
  if (trimmed === "." || trimmed === "..") {
    throw new SwitchboardPathError(`${label} must not be '.' or '..'`);
  }
  if (trimmed.includes("..")) {
    throw new SwitchboardPathError(`${label} must not contain '..'`);
  }
  if (trimmed.includes("/") || trimmed.includes("\\")) {
    throw new SwitchboardPathError(`${label} must not contain path separators`);
  }
  if (path.isAbsolute(trimmed) || /^[a-zA-Z]:/.test(trimmed)) {
    throw new SwitchboardPathError(`${label} must not be an absolute path`);
  }
  return trimmed.replace(/[^A-Za-z0-9._@+-]/g, (ch) => {
    const hex = Buffer.from(ch, "utf8").toString("hex");
    return `_${hex}`;
  });
}

export interface SwitchboardPathsOptions {
  /**
   * Persist root that contains the `switchboard/` directory
   * (typically `<project>/.codeflowmu/state`).
   */
  persistDir: string;
}

/**
 * Canonical filesystem layout under `.codeflowmu/state/switchboard/`.
 */
export class SwitchboardPaths {
  readonly persistDir: string;
  readonly root: string;

  constructor(options: SwitchboardPathsOptions) {
    if (!options?.persistDir || typeof options.persistDir !== "string") {
      throw new SwitchboardPathError("persistDir is required");
    }
    this.persistDir = path.resolve(options.persistDir);
    this.root = path.join(this.persistDir, "switchboard");
  }

  hostsDir(): string {
    return path.join(this.root, "hosts");
  }

  hostFile(hostId: string): string {
    const safe = sanitizePathSegment(hostId, "hostId");
    return path.join(this.hostsDir(), `${safe}.json`);
  }

  workersDir(): string {
    return path.join(this.root, "workers");
  }

  workerIdentityFile(workerId: string): string {
    const safe = sanitizePathSegment(workerId, "workerId");
    return path.join(this.workersDir(), safe, "IDENTITY.json");
  }

  bindingsDir(workerId: string): string {
    const safe = sanitizePathSegment(workerId, "workerId");
    return path.join(this.root, "bindings", safe);
  }

  bindingFile(workerId: string, timestamp: string, revision: number | string): string {
    const safeWorker = sanitizePathSegment(workerId, "workerId");
    const safeTs = sanitizePathSegment(timestamp, "timestamp");
    const rev = sanitizePathSegment(String(revision), "revision");
    return path.join(this.root, "bindings", safeWorker, `BINDING-${safeTs}-${rev}.md`);
  }

  mailboxDir(workerId: string, bucket: MailboxBucket): string {
    const safe = sanitizePathSegment(workerId, "workerId");
    return path.join(this.root, "mailboxes", safe, bucket);
  }

  doorbellFile(workerId: string, bucket: MailboxBucket, doorbellId: string): string {
    const safeDoor = sanitizePathSegment(doorbellId, "doorbellId");
    return path.join(this.mailboxDir(workerId, bucket), `${safeDoor}.md`);
  }

  runsDir(workerId: string, bucket: RunBucket): string {
    const safe = sanitizePathSegment(workerId, "workerId");
    return path.join(this.root, "runs", safe, bucket);
  }

  runFile(workerId: string, bucket: RunBucket, runId: string): string {
    const safeRun = sanitizePathSegment(runId, "runId");
    return path.join(this.runsDir(workerId, bucket), `${safeRun}.json`);
  }

  claimsDir(hostId: string): string {
    const safe = sanitizePathSegment(hostId, "hostId");
    return path.join(this.root, "claims", safe);
  }

  claimFile(hostId: string, sessionKey: string): string {
    const safeKey = sanitizePathSegment(sessionKey, "sessionKey");
    return path.join(this.claimsDir(hostId), `${safeKey}.claim`);
  }

  projectionDir(runId: string): string {
    const safe = sanitizePathSegment(runId, "runId");
    return path.join(this.root, "projections", safe);
  }

  projectionContextFile(runId: string): string {
    return path.join(this.projectionDir(runId), "CONTEXT.md");
  }

  projectionManifestFile(runId: string): string {
    return path.join(this.projectionDir(runId), "MANIFEST.json");
  }

  shadowReportFile(dateStamp: string): string {
    const safe = sanitizePathSegment(dateStamp, "dateStamp");
    return path.join(this.root, `shadow-report-${safe}.md`);
  }

  /** Host event journal for a run (WP-06 Shadow). */
  eventsDir(runId: string): string {
    const safe = sanitizePathSegment(runId, "runId");
    return path.join(this.root, "events", safe);
  }

  eventFile(runId: string, eventId: string): string {
    const safeEvt = sanitizePathSegment(eventId, "eventId");
    return path.join(this.eventsDir(runId), `${safeEvt}.json`);
  }

  evidenceDir(): string {
    return path.join(this.root, "evidence");
  }

  evidenceFile(deliveryAttemptId: string): string {
    const safe = sanitizePathSegment(deliveryAttemptId, "deliveryAttemptId");
    return path.join(this.evidenceDir(), `${safe}.json`);
  }

  /** Durable alignment ledger for Shadow vs legacy dispatch facts. */
  alignmentLedgerFile(): string {
    return path.join(this.root, "ALIGNMENT.json");
  }
}
