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

/**
 * ADR-214 — refresh schema-mirror.json against staging or prod.
 *
 * Connects to the configured Postgres URL (env var SUPABASE_DB_URL or
 * DATABASE_URL) and queries pg_constraint for CHECK constraints on the
 * tables backing Phase 1 schema resources. Parses the constraint
 * definitions, extracts enum values, writes a deterministic JSON file at
 * mcp-server/src/resources/schema-mirror.json.
 *
 * NOT run by CI — humans run this when they suspect or know that a CHECK
 * constraint changed in production. The mirror is committed to git so PR
 * diffs surface drift.
 *
 * Usage:
 *   SUPABASE_DB_URL=postgres://... npx tsx mcp-server/scripts/refresh-schema-mirror.ts
 *
 * Optional dep: `pg` (added as devDependency on mcp-server).
 */

import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

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

interface MirrorContent {
  _generated_at: string;
  _source: string;
  _note: string;
  [table: string]: string | Record<string, string[]>;
}

const TABLES_TO_MIRROR: Array<{ table: string; columns: string[] }> = [
  { table: "audio_files", columns: ["classification", "file_type"] },
  { table: "recording_credits", columns: ["role", "attestation_status"] },
  { table: "recordings", columns: ["version_type"] },
  // ADR-252 WS-A: work_credits is dropped. Composition credits live on
  // work_collaborators, whose only CHECK enum is attestation_status (role is
  // validated tool-side, not by a DB constraint). Master roles → recording_credits.
  { table: "work_collaborators", columns: ["attestation_status"] },
  { table: "works", columns: ["work_status", "work_type"] },
];

function fail(msg: string): never {
  console.error(`[refresh-schema-mirror] ERROR: ${msg}`);
  process.exit(1);
}

/**
 * Parse a CHECK constraint definition string for ANY-of-array shapes.
 * Postgres formats vary; common shapes:
 *   CHECK ((work_type = ANY (ARRAY['song'::text, 'instrumental'::text, ...])))
 *   CHECK ((work_type::text = ANY (ARRAY['song'::text, ...])))
 *   CHECK (work_type IN ('song', 'instrumental', ...))
 * Returns the literal values in source order; caller sorts for determinism.
 */
function parseEnumFromCheckDef(def: string): string[] | null {
  // Match ARRAY[...] form
  const arrayMatch = def.match(/ARRAY\[([^\]]+)\]/);
  if (arrayMatch) {
    const inside = arrayMatch[1];
    const literals = [...inside.matchAll(/'([^']+)'/g)].map((m) => m[1]);
    return literals.length > 0 ? literals : null;
  }
  // Match IN (...) form
  const inMatch = def.match(/IN\s*\(([^)]+)\)/i);
  if (inMatch) {
    const inside = inMatch[1];
    const literals = [...inside.matchAll(/'([^']+)'/g)].map((m) => m[1]);
    return literals.length > 0 ? literals : null;
  }
  return null;
}

async function main(): Promise<void> {
  const dbUrl = process.env.SUPABASE_DB_URL ?? process.env.DATABASE_URL;
  if (!dbUrl) {
    fail(
      "SUPABASE_DB_URL (or DATABASE_URL) env var not set. Provide a Postgres connection string for staging or prod.",
    );
  }

  // Lazy-import pg so the file type-checks even if pg isn't installed.
  // Generator is the hot path; refresh is operator-only.
  let Client: typeof import("pg").Client;
  try {
    ({ Client } = await import("pg"));
  } catch {
    fail(
      "pg npm package not installed. Run: cd mcp-server && npm install --save-dev pg @types/pg",
    );
  }

  const client = new Client({ connectionString: dbUrl });
  await client.connect();

  const out: Record<string, Record<string, string[]>> = {};

  for (const { table, columns } of TABLES_TO_MIRROR) {
    out[table] = {};
    for (const col of columns) {
      // Find a CHECK constraint on this column. Look for any constraint
      // whose pg_get_constraintdef text references the column name and
      // contains an ANY/IN enum list.
      const result = await client.query<{ def: string }>(
        `
        SELECT pg_get_constraintdef(oid) AS def
        FROM pg_constraint
        WHERE conrelid = $1::regclass
          AND contype = 'c'
          AND pg_get_constraintdef(oid) ILIKE $2
        `,
        [`public.${table}`, `%${col}%`],
      );

      let values: string[] | null = null;
      for (const row of result.rows) {
        const parsed = parseEnumFromCheckDef(row.def);
        if (parsed && parsed.length > 0) {
          values = parsed;
          break;
        }
      }
      if (!values) {
        console.warn(
          `[refresh-schema-mirror] warn: no enum CHECK found for ${table}.${col}; leaving empty array`,
        );
        values = [];
      }
      out[table][col] = [...values].sort();
    }
  }

  await client.end();

  // Preserve metadata from existing mirror if present.
  let existingNote =
    "CHECK constraint enums for tables backing ADR-214 Phase 1 schemas. Refresh via `npx tsx mcp-server/scripts/refresh-schema-mirror.ts` against staging. Committed so PR diffs surface schema drift. See ADR-214 Decision section, Drift Prevention #3.";
  try {
    const prev = JSON.parse(
      readFileSync(MIRROR_PATH, "utf-8"),
    ) as MirrorContent;
    if (typeof prev._note === "string") existingNote = prev._note;
  } catch {
    // first run — fine
  }

  const sortedTables = Object.keys(out).sort();
  const sortedOut: Record<string, Record<string, string[]>> = {};
  for (const t of sortedTables) {
    const cols = Object.keys(out[t]).sort();
    sortedOut[t] = {};
    for (const c of cols) sortedOut[t][c] = out[t][c];
  }

  const final: MirrorContent = {
    _generated_at: new Date().toISOString(),
    _source: dbUrl.replace(/:[^:@]+@/, ":***@"),
    _note: existingNote,
    ...sortedOut,
  };

  writeFileSync(MIRROR_PATH, JSON.stringify(final, null, 2) + "\n", "utf-8");
  console.log(
    `[refresh-schema-mirror] wrote ${sortedTables.length} tables to ${MIRROR_PATH}`,
  );
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
