// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.

/**
 * ADR-214 — generator for `required-schemas.generated.ts`.
 *
 * Reads three inputs:
 *   1. ToolRegistry (instantiated from src) → tool inputSchemas
 *   2. mcp-server/src/resources/required-schemas.source.ts → narrative + companion_calls
 *   3. mcp-server/src/resources/schema-mirror.json → CHECK constraint enums
 *
 * Joins them and emits `mcp-server/src/resources/required-schemas.generated.ts`,
 * a TypeScript module exporting `REQUIRED_SCHEMAS` consumed by ResourceRegistry.
 *
 * Determinism: object keys sorted alphabetically, arrays preserved in source
 * order. Re-running with no source changes produces a byte-identical file.
 *
 * Wired into mcp-server/package.json as part of `prebuild` (after
 * bundle-apps.ts). Runs via tsx; no build step required.
 *
 * Failure modes:
 *   - source.ts references a tool not in ToolRegistry → exit 1, name file to fix
 *   - companion_calls[].tool not in ToolRegistry → exit 1
 *   - enum_tables references missing table/column in mirror → exit 1
 */

import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
  REQUIRED_SCHEMAS_SOURCE,
  type RequiredSchemaSource,
} from "../src/resources/required-schemas.source.js";
import { ToolRegistry, type ToolDefinition } from "../src/tools/index.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const MCP_SERVER_ROOT = join(__dirname, "..");
const MIRROR_PATH = join(MCP_SERVER_ROOT, "src/resources/schema-mirror.json");
const OUTPUT_PATH = join(
  MCP_SERVER_ROOT,
  "src/resources/required-schemas.generated.ts",
);

interface SchemaMirror {
  [table: string]: string[] | Record<string, string[]> | string | undefined;
}

function loadMirror(): SchemaMirror {
  const raw = readFileSync(MIRROR_PATH, "utf-8");
  return JSON.parse(raw) as SchemaMirror;
}

function loadRegistry(): Map<string, ToolDefinition> {
  const stubPica = {} as never;
  const stubConfig = {
    picaApiKey: "stub",
    picaApiUrl: "https://stub.example.com/api",
    serverName: "schema-generator",
    version: "0.0.0",
    debug: false,
    lobbyMode: false,
    credentialsPath: "",
    discoveryMode: false,
  } as never;

  const registry = new ToolRegistry(
    stubPica,
    stubConfig,
    () => {},
    { callerIdentity: "schema-generator", transport: "stdio" } as never,
    () => {},
  );

  const map = new Map<string, ToolDefinition>();
  // ToolRegistry.tools is private but read-accessible from sibling code in
  // the same workspace (matches existing pattern in scripts/lint-mcp-tools.ts).
  const tools = (
    registry as unknown as {
      tools: Map<string, { definition: ToolDefinition }>;
    }
  ).tools;
  for (const [name, entry] of tools) {
    map.set(name, entry.definition);
  }
  return map;
}

function fail(msg: string): never {
  console.error(`[build-required-schemas] ERROR: ${msg}`);
  process.exit(1);
}

const RESPONSE_SWITCHES = new Set(["verbose"]);

function getRequiredAndProperties(
  tools: Map<string, ToolDefinition>,
  toolName: string,
  source: string,
): { required: string[]; properties: string[] } {
  const def = tools.get(toolName);
  if (!def) {
    fail(
      `${source} references tool '${toolName}' which is not in ToolRegistry. ` +
        `Edit mcp-server/src/resources/required-schemas.source.ts and remove or rename the reference.`,
    );
  }
  const schema = def.inputSchema;
  const required = Array.isArray(schema.required) ? [...schema.required] : [];
  // `verbose` shapes the tool's ANSWER (src/tools/write-receipt.ts), not the
  // record — it is not a field to recommend filling in.
  const properties = Object.keys(schema.properties ?? {}).filter(
    (p) => !RESPONSE_SWITCHES.has(p),
  );
  return { required, properties };
}

function buildEnums(
  mirror: SchemaMirror,
  tables: RequiredSchemaSource["enum_tables"],
  workflow: string,
): Record<string, string[]> {
  if (!tables || tables.length === 0) return {};
  const out: Record<string, string[]> = {};
  for (const { table, columns } of tables) {
    const tableEntry = mirror[table];
    if (
      !tableEntry ||
      typeof tableEntry !== "object" ||
      Array.isArray(tableEntry)
    ) {
      fail(
        `Workflow '${workflow}' references table '${table}' missing from schema-mirror.json. ` +
          `Run \`tsx mcp-server/scripts/refresh-schema-mirror.ts\` against staging to regenerate.`,
      );
    }
    const tableMap = tableEntry as Record<string, string[]>;
    for (const col of columns) {
      const values = tableMap[col];
      if (!Array.isArray(values)) {
        fail(
          `Workflow '${workflow}' references ${table}.${col} missing from schema-mirror.json.`,
        );
      }
      out[col] = [...values].sort();
    }
  }
  return out;
}

function buildCompanion(
  tools: Map<string, ToolDefinition>,
  companion: RequiredSchemaSource["companion_calls"][number],
  workflow: string,
): Record<string, unknown> {
  if (!tools.has(companion.tool)) {
    fail(
      `Workflow '${workflow}' companion '${companion.tool}' not in ToolRegistry. ` +
        `Edit required-schemas.source.ts.`,
    );
  }
  // Parity gate (2026-07-18): the hand-authored companion `required` array
  // MUST byte-equal the live tool's inputSchema.required. ADR-214's holdout
  // (scenarios/214/required-schemas-match.spec.ts) enforces exactly that
  // byte-equality against the deployed surface, and the WS5 write-merge
  // (b477cd175) proved the hand arrays silently drift when a tool goes
  // multi-mode (required → []). The arrays stay authored in source.ts for
  // readability, but drift is now a build failure instead of a holdout red.
  const { required: liveRequired } = getRequiredAndProperties(
    tools,
    companion.tool,
    `Workflow '${workflow}' companion`,
  );
  const declaredSorted = [...companion.required].sort();
  const liveSorted = [...liveRequired].sort();
  if (
    declaredSorted.length !== liveSorted.length ||
    declaredSorted.some((v, i) => v !== liveSorted[i])
  ) {
    fail(
      `Workflow '${workflow}' companion '${companion.tool}' declares ` +
        `required=${JSON.stringify(companion.required)} but the live ` +
        `inputSchema.required is ${JSON.stringify(liveRequired)}. ` +
        `Update required-schemas.source.ts to match the tool schema ` +
        `(the ADR-214 holdout asserts byte-equality).`,
    );
  }
  const out: Record<string, unknown> = {
    tool: companion.tool,
    when: companion.when,
    required: companion.required,
  };
  if (companion.per_row_required) {
    out.per_row_required = companion.per_row_required;
  }
  if (companion.constraint) {
    out.constraint = companion.constraint;
  }
  return out;
}

function buildResource(
  tools: Map<string, ToolDefinition>,
  mirror: SchemaMirror,
  source: RequiredSchemaSource,
): Record<string, unknown> {
  const { required, properties } = getRequiredAndProperties(
    tools,
    source.primary_tool,
    `Workflow '${source.workflow}' primary_tool`,
  );
  const recommended = properties.filter((p) => !required.includes(p));
  const enums = buildEnums(mirror, source.enum_tables, source.workflow);
  const companions = source.companion_calls.map((c) =>
    buildCompanion(tools, c, source.workflow),
  );

  return {
    workflow: source.workflow,
    summary: source.summary,
    primary_tool: source.primary_tool,
    primary_required: required,
    primary_recommended: recommended,
    enums,
    companion_calls: companions,
    example: source.example,
  };
}

function emit(resources: Record<string, Record<string, unknown>>): string {
  // Sort keys deterministically so re-runs produce byte-identical output.
  const sortedKeys = Object.keys(resources).sort();
  const sortedResources: Record<string, Record<string, unknown>> = {};
  for (const k of sortedKeys) sortedResources[k] = resources[k];

  const json = JSON.stringify(sortedResources, null, 2);
  return [
    "// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.",
    "//",
    "// AUTO-GENERATED by mcp-server/scripts/build-required-schemas.ts.",
    "// Do not edit by hand. Re-run via `cd mcp-server && npm run prebuild`.",
    "// Source: required-schemas.source.ts + schema-mirror.json + ToolRegistry inputSchemas.",
    "// ADR-214 — required-fields contracts per workflow.",
    "",
    "export const REQUIRED_SCHEMAS = " + json + " as const;",
    "",
    "export type RequiredSchemaKey = keyof typeof REQUIRED_SCHEMAS;",
    "",
  ].join("\n");
}

function main(): void {
  const tools = loadRegistry();
  const mirror = loadMirror();
  const out: Record<string, Record<string, unknown>> = {};
  for (const [key, source] of Object.entries(REQUIRED_SCHEMAS_SOURCE)) {
    out[key] = buildResource(tools, mirror, source);
  }
  const text = emit(out);
  writeFileSync(OUTPUT_PATH, text, "utf-8");
  console.log(
    `[build-required-schemas] wrote ${Object.keys(out).length} schemas to ${OUTPUT_PATH}`,
  );
}

main();
