/**
 * WorkerIdentityService — Agent → Worker mapping + IDENTITY.json (WP-05).
 * Registration does NOT open a Host Session (separation invariant).
 * Shadow-only; production Active / SessionManager path unchanged.
 */

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

import type { Agent, AgentLayer } from "@codeflowmu/protocol";

import type { AgentRecord } from "../../types/state.ts";
import { AtomicFile } from "../atomic/AtomicFile.ts";
import type { WorkerIdentity } from "../contracts/HostSession.ts";
import { SwitchboardPaths } from "../paths/SwitchboardPaths.ts";

export const WORKER_IDENTITY_SCHEMA = "codeflowmu.worker-identity/v1" as const;

export interface WorkerIdentityRecord {
  schema: typeof WORKER_IDENTITY_SCHEMA;
  worker: WorkerIdentity;
  registeredAt: string;
  registeredBy: string;
  /** Always false at register time — Host Session is a separate step. */
  sessionOpened: false;
  mode: "shadow" | "off";
  /** Optional source agent_id when mapped from Agent PCB. */
  sourceAgentId?: string;
}

export type AgentLike = Pick<
  Agent,
  "agent_id" | "role" | "layer" | "workspace" | "labels"
>;

function isAgentRecord(value: unknown): value is AgentRecord {
  return (
    typeof value === "object" &&
    value !== null &&
    "protocol" in value &&
    typeof (value as AgentRecord).protocol === "object" &&
    (value as AgentRecord).protocol !== null
  );
}

/** Map an Agent (or AgentRecord.protocol) onto WorkerIdentity. */
export function mapAgentToWorker(source: AgentLike | AgentRecord): WorkerIdentity {
  const agent: AgentLike = isAgentRecord(source) ? source.protocol : source;
  const workerId = String(agent.agent_id ?? "").trim();
  if (!workerId) {
    throw new Error("mapAgentToWorker: agent_id is required");
  }
  const roleId = String(agent.role ?? "").trim();
  if (!roleId) {
    throw new Error("mapAgentToWorker: role is required");
  }
  const layer = agent.layer as AgentLayer;
  if (!layer) {
    throw new Error("mapAgentToWorker: layer is required");
  }
  return {
    workerId,
    roleId,
    layer,
    ...(agent.workspace ? { workspaceId: agent.workspace } : {}),
    ...(agent.labels ? { labels: { ...agent.labels } } : {}),
  };
}

export interface WorkerIdentityServiceOptions {
  paths: SwitchboardPaths;
  /** Defaults to "shadow". */
  mode?: "shadow" | "off";
  registeredBy?: string;
}

/**
 * Persists worker identity files under switchboard/workers/<id>/IDENTITY.json.
 * Never calls AgentSdkAdapter / AgentHostAdapter / SessionManager.
 */
export class WorkerIdentityService {
  readonly #paths: SwitchboardPaths;
  readonly #mode: "shadow" | "off";
  readonly #registeredBy: string;

  constructor(opts: WorkerIdentityServiceOptions) {
    this.#paths = opts.paths;
    this.#mode = opts.mode ?? "shadow";
    this.#registeredBy = opts.registeredBy?.trim() || "WorkerIdentityService";
  }

  /** Pure mapping helper (also exported as function). */
  mapAgent(source: AgentLike | AgentRecord): WorkerIdentity {
    return mapAgentToWorker(source);
  }

  /**
   * Register a worker identity on disk.
   * Does NOT open a Host Session (`sessionOpened` stays false).
   */
  async register(identity: WorkerIdentity): Promise<{
    record: WorkerIdentityRecord;
    path: string;
  }> {
    const workerId = String(identity.workerId ?? "").trim();
    if (!workerId) throw new Error("WorkerIdentityService.register: workerId required");
    const record: WorkerIdentityRecord = {
      schema: WORKER_IDENTITY_SCHEMA,
      worker: { ...identity, workerId },
      registeredAt: new Date().toISOString(),
      registeredBy: this.#registeredBy,
      sessionOpened: false,
      mode: this.#mode,
      sourceAgentId: workerId,
    };
    const file = this.#paths.workerIdentityFile(workerId);
    await AtomicFile.writeJson(file, record);
    return { record, path: file };
  }

  /**
   * WP-10 alias — register long-lived post identity without Host Session.
   * Platform identity (sdk_agent_id / externalSessionId) is NOT required here.
   */
  async registerIdentity(identity: WorkerIdentity): Promise<{
    record: WorkerIdentityRecord;
    path: string;
  }> {
    return this.register(identity);
  }

  /** Map Agent → Worker then register (still no Host Session). */
  async registerFromAgent(source: AgentLike | AgentRecord): Promise<{
    identity: WorkerIdentity;
    record: WorkerIdentityRecord;
    path: string;
  }> {
    const identity = mapAgentToWorker(source);
    const { record, path } = await this.register(identity);
    return { identity, record, path };
  }

  /** WP-10 alias for registerFromAgent. */
  async registerIdentityFromAgent(source: AgentLike | AgentRecord): Promise<{
    identity: WorkerIdentity;
    record: WorkerIdentityRecord;
    path: string;
  }> {
    return this.registerFromAgent(source);
  }

  async get(workerId: string): Promise<WorkerIdentityRecord | null> {
    const file = this.#paths.workerIdentityFile(workerId);
    if (!(await AtomicFile.exists(file))) return null;
    return (await AtomicFile.readJson(file)) as WorkerIdentityRecord;
  }

  /** WP-10 alias. */
  async getIdentity(workerId: string): Promise<WorkerIdentityRecord | null> {
    return this.get(workerId);
  }

  async has(workerId: string): Promise<boolean> {
    return AtomicFile.exists(this.#paths.workerIdentityFile(workerId));
  }

  async list(): Promise<WorkerIdentityRecord[]> {
    const root = this.#paths.workersDir();
    let names: string[] = [];
    try {
      names = await fs.readdir(root);
    } catch {
      return [];
    }
    const out: WorkerIdentityRecord[] = [];
    for (const name of names.sort()) {
      const file = join(root, name, "IDENTITY.json");
      if (!(await AtomicFile.exists(file))) continue;
      out.push((await AtomicFile.readJson(file)) as WorkerIdentityRecord);
    }
    return out;
  }

  /** WP-10 alias. */
  async listIdentities(): Promise<WorkerIdentityRecord[]> {
    return this.list();
  }

  /** Directory containing IDENTITY.json (for diagnostics). */
  identityDir(workerId: string): string {
    return dirname(this.#paths.workerIdentityFile(workerId));
  }
}
