import { readFile } from "node:fs/promises";
import { z } from "zod";
import { adapterRegistryFile, incidentDispatchCapabilitiesFile, incidentTaxonomyFile } from "../paths";
import { defaultRoutingRuntimeDir, readProviderAccounts, type RegisteredAccount, type RoutingProvider } from "../routing-config";
import { parseTaxonomy } from "./dispatch-brief";

const nonEmpty = z.string().trim().min(1);
const permission = z.enum(["safe", "unsafe"]);
const AdapterSchema = z.object({ id: nonEmpty, wrapper: nonEmpty, models: z.array(nonEmpty).min(1) }).passthrough();
const AdaptersSchema = z.object({ version: z.literal("adapters/v1"), adapters: z.array(AdapterSchema).min(1) }).strict();
const EffortSchema = z.object({ id: nonEmpty, wrapperModel: nonEmpty }).strict();
const AccountModeSchema = z.union([
  z.object({ kind: z.literal("profile"), provider: z.enum(["codex", "claude"]) }).strict(),
  z.object({ kind: z.literal("fixed"), account: nonEmpty, label: nonEmpty }).strict(),
]);
const CapabilitySchema = z.object({
  cli: nonEmpty, label: nonEmpty, adapter: nonEmpty, wrapperContract: z.literal("incident-wrapper/v1"),
  models: z.array(z.object({ id: nonEmpty, efforts: z.array(EffortSchema).min(1) }).strict()).min(1),
  accountMode: AccountModeSchema,
  timeoutSeconds: z.number().int().min(60).max(86_400),
  permissionModes: z.array(permission).min(1),
}).strict();
const ManifestSchema = z.object({ version: z.literal("incident-dispatch-capabilities/v1"), capabilities: z.array(CapabilitySchema).min(1) }).strict();

export type IncidentCapability = z.infer<typeof CapabilitySchema>;
export type AdapterRegistry = z.infer<typeof AdaptersSchema>;

// v1 is deliberately an exact authority, not an extensible capability format.  The
// generic schema above only makes malformed JSON diagnosable; this matrix is the
// security boundary that prevents a self-declared Claude/Cursor capability from
// becoming dispatchable.
const V1_MODELS = [
  ["gpt-5.6-sol", ["gpt-5.6-sol-low", "gpt-5.6-sol-medium", "gpt-5.6-sol-high"]],
  ["gpt-5.6-terra", ["gpt-5.6-terra-low", "gpt-5.6-terra-medium", "gpt-5.6-terra-high"]],
  ["gpt-5.6-luna", ["gpt-5.6-luna-low", "gpt-5.6-luna-medium", "gpt-5.6-luna-high"]],
] as const;
const V1_EFFORTS = ["low", "medium", "high"] as const;

function assertExactV1Manifest(manifest: z.infer<typeof ManifestSchema>, adapters: z.infer<typeof AdaptersSchema>): void {
  if (manifest.capabilities.length !== 1) throw new Error("v1 requires exactly one capability");
  const capability = manifest.capabilities[0];
  if (!capability || capability.cli !== "codex" || capability.label !== "Codex" || capability.adapter !== "codex" || capability.wrapperContract !== "incident-wrapper/v1") throw new Error("v1 codex capability mismatch");
  if (capability.accountMode.kind !== "profile" || capability.accountMode.provider !== "codex") throw new Error("v1 account mode mismatch");
  if (capability.timeoutSeconds !== 3_600) throw new Error("v1 timeout mismatch");
  if (JSON.stringify(capability.permissionModes) !== JSON.stringify(["safe"])) throw new Error("v1 permission modes mismatch");
  if (capability.models.length !== V1_MODELS.length) throw new Error("v1 model count mismatch");
  for (let index = 0; index < V1_MODELS.length; index += 1) {
    const expected = V1_MODELS[index]!;
    const model = capability.models[index];
    if (!model || model.id !== expected[0] || model.efforts.length !== V1_EFFORTS.length) throw new Error("v1 model mapping mismatch");
    for (let effortIndex = 0; effortIndex < V1_EFFORTS.length; effortIndex += 1) {
      const effort = model.efforts[effortIndex];
      if (!effort || effort.id !== V1_EFFORTS[effortIndex] || effort.wrapperModel !== expected[1][effortIndex]) throw new Error("v1 effort mapping mismatch");
    }
  }
  const adapter = adapters.adapters.find((item) => item.id === "codex");
  if (!adapter || adapter.wrapper !== "wrappers/codex.sh") throw new Error("v1 codex adapter binding mismatch");
  for (const [, wrapperModels] of V1_MODELS) for (const wrapperModel of wrapperModels) {
    if (!adapter.models.includes(wrapperModel)) throw new Error(`v1 wrapper model absent from codex adapter: ${wrapperModel}`);
  }
}

export interface IncidentOptionsResponse {
  types: Array<{ id: string; title: string; keywords: string[] }>;
  clis: Array<{ id: string; label: string; models: Array<{ id: string; efforts: string[] }>; accounts: Array<{ slug: string; label: string; ready: boolean; fixed: boolean }>; permissionModes: Array<"safe" | "unsafe"> }>;
}
export type IncidentDispatchSelection = {
  wrapperModel: string;
  permissionMode: "safe" | "unsafe";
  fixed: boolean;
  wrapper?: string;
  timeoutSeconds?: number;
};
type Selection = IncidentDispatchSelection;
export interface LoadedIncidentOptions extends IncidentOptionsResponse {
  /** Paths are diagnostic provenance only; they never cross the HTTP boundary. */
  sourcePaths: string[];
  internal: Map<string, Selection>;
}
export class IncidentOptionsError extends Error {
  constructor(public readonly kind: "assets" | "accounts", public readonly detail: string) { super(detail); this.name = "IncidentOptionsError"; }
}
export class InvalidIncidentDispatchError extends Error { constructor() { super("invalid incident dispatch"); this.name = "InvalidIncidentDispatchError"; } }
export class InvalidIncidentTypeError extends Error { constructor() { super("invalid incident type"); this.name = "InvalidIncidentTypeError"; } }
export interface ValidatedIncidentDispatchSelection extends Selection {}

function ensureUnique(values: readonly string[], label: string): void {
  const seen = new Set<string>();
  for (const value of values) { if (seen.has(value)) throw new Error(`duplicate ${label}: ${value}`); seen.add(value); }
}
function selectionKey(cli: string, model: string, effort: string, account: string, mode: string): string { return [cli, model, effort, account, mode].join("\0"); }
export interface LoadIncidentOptionsDeps {
  read?: (path: string) => Promise<string>;
  adaptersPath?: string;
  manifestPath?: string;
  taxonomyPath?: string;
  accounts?: (provider: RoutingProvider) => Promise<RegisteredAccount[]>;
}

/**
 * Generic capability join (profile + fixed account modes). Deployed v1 loading still
 * pins the initial matrix via assertExactV1Manifest; design tests call this seam
 * directly so fixed-account behavior stays covered without weakening that pin.
 */
export async function materializeIncidentCapability(
  capability: IncidentCapability,
  adapters: AdapterRegistry,
  resolveAccounts: (provider: RoutingProvider) => Promise<RegisteredAccount[]>,
): Promise<{ cli: IncidentOptionsResponse["clis"][number]; internal: Map<string, Selection> }> {
  const adapter = adapters.adapters.find((item) => item.id === capability.adapter);
  if (!adapter || adapter.wrapper !== `wrappers/${capability.cli}.sh`) throw new Error(`adapter binding invalid for ${capability.cli}`);
  ensureUnique(capability.models.map((item) => item.id), `model for ${capability.cli}`);
  ensureUnique(capability.permissionModes, `permission mode for ${capability.cli}`);
  let accounts: Array<{ slug: string; label: string; ready: boolean; fixed: boolean }>;
  if (capability.accountMode.kind === "fixed") {
    const account = capability.accountMode.account.trim();
    const label = capability.accountMode.label.trim();
    if (!account || !label) throw new Error(`empty fixed account for ${capability.cli}`);
    accounts = [{ slug: account, label, ready: true, fixed: true }];
  } else {
    const provider = capability.accountMode.provider;
    let registered: RegisteredAccount[];
    try { registered = await resolveAccounts(provider); }
    catch (error) {
      if (error instanceof IncidentOptionsError) throw error;
      throw new IncidentOptionsError("accounts", `provider ${provider}: ${String(error)}`);
    }
    if (registered.length === 0) throw new IncidentOptionsError("accounts", `provider ${provider}: no accounts`);
    accounts = registered.map((account) => ({ ...account, ready: true, fixed: false }));
    try { ensureUnique(accounts.map((item) => item.slug), `account for ${capability.cli}`); }
    catch (error) { throw new IncidentOptionsError("accounts", `provider ${provider}: ${String(error)}`); }
  }
  if (accounts.length === 0) throw new IncidentOptionsError("accounts", `no accounts for ${capability.cli}`);
  const wrapperModels: string[] = [];
  const models = capability.models.map((model) => {
    ensureUnique(model.efforts.map((item) => item.id), `effort for ${capability.cli}/${model.id}`);
    if (model.efforts.length === 0) throw new Error(`empty efforts for ${capability.cli}/${model.id}`);
    for (const effort of model.efforts) wrapperModels.push(effort.wrapperModel);
    for (const effort of model.efforts) if (!adapter.models.includes(effort.wrapperModel)) throw new Error(`wrapper model absent from ${adapter.id}: ${effort.wrapperModel}`);
    return { id: model.id, efforts: model.efforts.map((item) => item.id) };
  });
  ensureUnique(wrapperModels, `wrapper model for ${capability.cli}`);
  if (!models.length || models.every((model) => model.efforts.length === 0) || !capability.permissionModes.length) {
    throw new Error(`empty capability ${capability.cli}`);
  }
  const cli = { id: capability.cli, label: capability.label, models, accounts, permissionModes: capability.permissionModes };
  const internal = new Map<string, Selection>();
  for (const model of capability.models) for (const effort of model.efforts) for (const account of accounts) for (const mode of capability.permissionModes) {
    const key = selectionKey(capability.cli, model.id, effort.id, account.slug, mode);
    if (internal.has(key)) throw new Error(`duplicate dispatch selection for ${capability.cli}`);
    internal.set(key, {
      wrapperModel: effort.wrapperModel,
      permissionMode: mode,
      fixed: account.fixed,
      wrapper: adapter.wrapper,
      timeoutSeconds: capability.timeoutSeconds,
    });
  }
  return { cli, internal };
}

/** Loads all deployed authorities together. Any incomplete authority is unusable. */
export async function loadIncidentOptions(deps: LoadIncidentOptionsDeps = {}): Promise<LoadedIncidentOptions> {
  const read = deps.read ?? ((path: string) => readFile(path, "utf8"));
  const adaptersPath = deps.adaptersPath ?? adapterRegistryFile();
  const manifestPath = deps.manifestPath ?? incidentDispatchCapabilitiesFile();
  const taxonomyPath = deps.taxonomyPath ?? incidentTaxonomyFile();
  let adapters: AdapterRegistry;
  let manifest: z.infer<typeof ManifestSchema>;
  let types: IncidentOptionsResponse["types"];
  try {
    adapters = AdaptersSchema.parse(JSON.parse(await read(adaptersPath)));
    manifest = ManifestSchema.parse(JSON.parse(await read(manifestPath)));
    types = parseTaxonomy(await read(taxonomyPath)).map(({ id, title, keywords }) => ({ id, title, keywords }));
    ensureUnique(adapters.adapters.map((item) => item.id), "adapter id");
    ensureUnique(manifest.capabilities.map((item) => item.cli), "CLI id");
    for (const adapter of adapters.adapters) ensureUnique(adapter.models, `adapter model for ${adapter.id}`);
    assertExactV1Manifest(manifest, adapters);
  } catch (error) { throw new IncidentOptionsError("assets", `${adaptersPath}, ${manifestPath}, ${taxonomyPath}: ${String(error)}`); }

  const clis: IncidentOptionsResponse["clis"] = [];
  const internal = new Map<string, Selection>();
  const sourcePaths = [adaptersPath, manifestPath, taxonomyPath];
  const resolveAccounts = deps.accounts ?? ((item: RoutingProvider) => readProviderAccounts(defaultRoutingRuntimeDir(), item));
  for (const capability of manifest.capabilities) {
    try {
      const materialized = await materializeIncidentCapability(capability, adapters, resolveAccounts);
      clis.push(materialized.cli);
      for (const [key, selection] of materialized.internal) {
        if (internal.has(key)) throw new Error(`duplicate dispatch selection for ${capability.cli}`);
        internal.set(key, selection);
      }
    } catch (error) {
      if (error instanceof IncidentOptionsError) throw error;
      throw new IncidentOptionsError("assets", `${manifestPath}: ${String(error)}`);
    }
  }
  if (!clis.length || !internal.size) throw new IncidentOptionsError("assets", `${manifestPath}: empty capability result`);
  return { types, clis, sourcePaths, internal };
}

export function serializeIncidentOptions(options: LoadedIncidentOptions): IncidentOptionsResponse {
  return { types: options.types, clis: options.clis.map((cli) => ({ ...cli, models: cli.models.map((model) => ({ ...model, efforts: [...model.efforts] })), accounts: cli.accounts.map((account) => ({ ...account })), permissionModes: [...cli.permissionModes] })) };
}
export function validateIncidentType(incidentType: string | undefined, options: LoadedIncidentOptions): void {
  if (incidentType === undefined) return;
  if (!options.types.some((type) => type.id === incidentType)) throw new InvalidIncidentTypeError();
}

export function validateIncidentDispatchSelection(request: { cli: string; model: string; reasoningEffort: string; account: string; unsafe: boolean }, options: LoadedIncidentOptions): ValidatedIncidentDispatchSelection {
  const mode = request.unsafe ? "unsafe" : "safe";
  const cli = options.clis.find((item) => item.id === request.cli);
  if (!cli || !cli.permissionModes.includes(mode)) throw new InvalidIncidentDispatchError();
  const result = options.internal.get(selectionKey(request.cli, request.model, request.reasoningEffort, request.account, mode));
  if (!result || result.permissionMode !== mode) throw new InvalidIncidentDispatchError();
  const account = cli.accounts.find((item) => item.slug === request.account);
  if (!account?.ready) throw new InvalidIncidentDispatchError();
  return result;
}
