/**
 * Phase 2 Guards Generator — SHARED across prisma / mongodb-native / postgres-native.
 *
 * Emits `<Model>.guards.ts` containing the transpiled TS guard functions
 * for one model's `constraints[]`, plus a small runtime that the
 * controller's existing `validate()` method calls. The output is
 * ORM-agnostic (operates on `self` + `actor` parameters; doesn't touch
 * the DB), so the same generator file backs all three ORM factories.
 *
 * One file per constrained model. Models with no `constraints` field skip
 * this template entirely (the realize entry point gates the per-model loop
 * on `model.constraints?.length > 0`).
 *
 * Output shape:
 *   - Re-exports the transpiled guard fns (one per constraint record)
 *   - `MODEL_CONSTRAINTS` — table of `{on: string[], guard: GuardFn, name, source}`
 *   - `runGuards(input, op, actor?)` — filters by `on` matcher, invokes each
 *     matching guard with (input, actor), returns `Violation[]` (empty = pass)
 *
 * `actor` is `null` by default in Slice 1 (route handlers don't yet thread
 * a user context through to controllers). Constraints referencing `actor.*`
 * paths will fail at runtime when invoked with `null` — the route handler
 * should pass an authenticated user object when one is available.
 */

import type { TemplateContext, ModelConstraintSpec } from '@specverse/types';
import { transpilePhase2Guard } from '@specverse/engines/inference';

export default function generatePrismaGuards(context: TemplateContext): string {
  const { model } = context;

  if (!model) {
    throw new Error('Model is required for guards generation');
  }

  const constraints: ModelConstraintSpec[] = (model as any).constraints ?? [];
  if (constraints.length === 0) {
    // Defensive: should not be invoked when constraints is empty, but if
    // realize entry gating is bypassed, emit a stub that hides emptiness.
    return generateEmptyGuardsModule(model.name);
  }

  const guardFns: string[] = [];
  const constraintsTable: string[] = [];
  let hasSubqueryGuard = false;

  for (const c of constraints) {
    // transpilePhase2Guard wraps body in a synthetic pure def and runs the
    // existing Quint → TS pipeline. The `==` → `===` strict-equality
    // upgrade happens inside applyQuintRewrites.
    const transpiled = transpilePhase2Guard(
      c.requires.name,
      c.requires.params ?? `self: any, actor: any`,
      c.requires.body,
    );

    // Phase 2 Slice 15b — subquery rewrite. The Quint transpiler converts
    // `Vote.exists(__v => ...)` to `Vote.some((__v: any) => ...)`, but
    // `Vote` is undefined at runtime (it's a Quint Set reference, not a
    // JS variable). We detect any bare capitalized identifier `.some(...)`
    // pattern in the transpiled body, treat it as a SUBQUERY (query the
    // backing store for entities of that model), and rewrite the guard
    // to an ASYNC function that takes a ctx argument carrying the query
    // helper. Sync (pure self/actor) guards pass through unchanged.
    const rewritten = rewriteSubqueriesAsync(transpiled.typescript, c.requires.name);
    guardFns.push(rewritten);
    if (rewritten.includes('ctx?.query?.')) hasSubqueryGuard = true;

    const onArrayLiteral = `[${c.on.map((o) => JSON.stringify(o)).join(', ')}]`;
    const sourceLiteral = JSON.stringify(
      c.requires.source.input ?? '',
    );
    constraintsTable.push(
      `  {\n` +
      `    on: ${onArrayLiteral},\n` +
      `    guard: ${c.requires.name},\n` +
      `    name: ${JSON.stringify(c.requires.name)},\n` +
      `    source: ${sourceLiteral}\n` +
      `  }`,
    );
  }

  return `/**
 * Auto-generated by SpecVerse Phase 2 (Validate-Centric Constraints).
 *
 * Guard functions for ${model.name}'s declared constraints.
 * DO NOT EDIT — regenerated on every \`spv realize all\`.
 *
 * Slice 1 limitations:
 *   - \`self\` is the input payload (not the loaded entity). For Update/Delete/
 *     Evolve, paths through \`self\` see only the fields the caller sent.
 *   - \`actor\` defaults to null. Constraints using \`actor.*\` paths fail at
 *     runtime when called without a user context.
 */

export interface Violation {
  constraint: string;
  scope: string;
  source: string;
  message: string;
}

/**
 * GuardContext is provided by the controller's validate() method. The
 * \`query\` accessor returns a per-model helper that backs subquery sugars
 * like \`{Actor} has not {verb} on {Target}\` (which the transpiler converts
 * to \`<Model>.some(predicate)\`). Implementations call the ORM's findMany
 * (or the in-memory store for dynamic interpreters).
 */
export interface GuardContext {
  query?: (modelName: string) => {
    exists: (predicate: (entity: any) => boolean) => Promise<boolean>;
  } | undefined;
}

export interface ConstraintRecord {
  on: string[];
  // Pure self/actor guards stay sync; subquery guards (those that reference
  // bare-Capitalized model names like \`Vote.some(...)\`) are rewritten by
  // the generator into async functions that take a third ctx argument.
  // runGuards awaits the result either way.
  guard: (self: any, actor: any, ctx?: GuardContext) => boolean | Promise<boolean>;
  name: string;
  source: string;
}
${hasSubqueryGuard ? `
/**
 * Date-safe operand coercion for relational (< <= > >=) comparisons in
 * subquery guards. A stored value arrives as a Date (from the ORM) while the
 * input payload carries an ISO string; comparing them raw yields NaN. Map
 * either to epoch ms; pass non-dates (numbers, ids) through unchanged.
 */
function __cmp(x: any): any {
  if (x instanceof Date) return x.getTime();
  if (typeof x === 'string' && /^\\d{4}-\\d{2}-\\d{2}T/.test(x)) {
    const t = Date.parse(x);
    return Number.isNaN(t) ? x : t;
  }
  return x;
}
` : ''}
${guardFns.join('\n\n')}

export const MODEL_CONSTRAINTS: ConstraintRecord[] = [
${constraintsTable.join(',\n')}
];

/**
 * Match a runtime operation against a constraint's \`on:\` array.
 *
 * Op shapes:
 *   - exact string: 'create', 'update', 'delete', 'retrieve'
 *   - 'evolve.<transition>': specific lifecycle transition
 *
 * \`on:\` entry shapes:
 *   - '*': always matches
 *   - 'evolve.*': matches any 'evolve.X' op
 *   - else: exact-string equality
 */
export function matchesOp(constraintOn: string[], op: string): boolean {
  for (const o of constraintOn) {
    if (o === '*') return true;
    if (o === op) return true;
    if (o === 'evolve.*' && op.startsWith('evolve.')) return true;
  }
  return false;
}

/**
 * Run all constraints whose \`on:\` matches the requested op. Each guard
 * is invoked with (input, actor). Returns the collected violations.
 *
 * Failure-mode policy (fail-OPEN on exceptions):
 *   - Guard returns false → one Violation appended
 *   - Guard returns true  → skip
 *   - Guard throws        → console.error + treat as PASS (no violation)
 *
 * Rationale: a throw indicates a guard-internal defect (transpile bug,
 * undefined-path traversal, type mismatch) rather than a real constraint
 * failure. Blocking the mutation would deny legitimate user actions for
 * a bug we already log loudly. Real data-integrity violations still fall
 * back to database constraints + Slice 3 mode-γ preflight retries.
 */
export async function runGuards(
  input: any,
  op: string,
  actor: any = null,
  ctx: GuardContext | null = null,
): Promise<Violation[]> {
  const violations: Violation[] = [];
  for (const c of MODEL_CONSTRAINTS) {
    if (!matchesOp(c.on, op)) continue;
    let passed = true;
    try {
      // await wraps both sync (boolean) and async (Promise<boolean>) guards
      // uniformly. Subquery guards need ctx; sync guards ignore it.
      passed = await c.guard(input, actor, ctx ?? undefined);
    } catch (e: any) {
      // Fail-open: log loudly, do NOT block the mutation. See policy above.
      console.error(
        \`[runGuards] constraint "\${c.name}" (source: \${c.source}) threw during op="\${op}" — treating as PASS:\`,
        e?.stack ?? e?.message ?? e,
      );
      continue;
    }
    if (!passed) {
      violations.push({
        constraint: c.name,
        scope: op,
        source: c.source,
        message: \`Constraint "\${c.source}" failed\`,
      });
    }
  }
  return violations;
}
`;
}

function generateEmptyGuardsModule(modelName: string): string {
  return `/**
 * Auto-generated by SpecVerse Phase 2 — no constraints declared on ${modelName}.
 * Empty stub kept for symmetry; controllers gate their imports on this file.
 */

export interface Violation {
  constraint: string;
  scope: string;
  source: string;
  message: string;
}

export const MODEL_CONSTRAINTS: any[] = [];

export function matchesOp(_constraintOn: string[], _op: string): boolean {
  return false;
}

export async function runGuards(_input: any, _op: string, _actor: any = null, _ctx: any = null): Promise<Violation[]> {
  return [];
}
`;
}

/**
 * Phase 2 Slice 15b — subquery rewrite.
 *
 * The Quint transpiler converts `Vote.exists(__v => predicate)` to
 * `Vote.some((__v: any) => predicate)`. `Vote` is a bare identifier left
 * over from the Quint source — it's a model reference, not a JS variable
 * in scope. We detect these patterns and rewrite the guard to:
 *
 *   1. Become async (returns Promise<boolean>)
 *   2. Accept a third `ctx` argument carrying the per-model query helper
 *   3. Shim each referenced model name to a const that calls ctx.query()
 *   4. Replace `<Model>.some(` with `await __<Model>.exists(`
 *
 * Pure self/actor guards (no bare-Capitalized .some patterns) pass
 * through unchanged — they stay sync, ignore ctx.
 *
 * Fail-open is preserved at the SHIM level: when ctx?.query?.('Vote')
 * returns undefined (no ctx provided, e.g. test path), the shim's
 * `if (!__Vote) return true;` short-circuits to pass. This matches the
 * runGuards-level fail-open contract.
 */
function rewriteSubqueriesAsync(transpiled: string, guardName: string): string {
  // Match bare capitalized identifier followed by `.some(` — NOT preceded by
  // a word char or dot, so `x.Vote.some(` and `obj.Vote.some(` don't match.
  // Negative lookbehind requires modern node (✓ — we target 20+).
  const subqueryPattern = /(?<![\w.])([A-Z][A-Za-z0-9_]*)\.some\(/g;
  const matches = Array.from(transpiled.matchAll(subqueryPattern));

  if (matches.length === 0) {
    return transpiled; // pure guard — no rewrite needed
  }

  // Collect unique model names referenced.
  const modelNames = Array.from(new Set(matches.map((m) => m[1])));

  // Replace `<Model>.some(` with `await __<Model>.exists(` throughout.
  let body = transpiled.replace(subqueryPattern, (_match, modelName) => {
    return `await __${modelName}.exists(`;
  });

  // The function signature was emitted by the transpiler as:
  //   `export function <name>(self: <T>, actor: <U>): boolean { ... }`
  // Convert to async + accept ctx + return Promise<boolean>.
  body = body.replace(
    /export\s+function\s+(\w+)\(([^)]*)\)\s*:\s*boolean\s*\{/,
    (_match, fnName, params) => {
      return `export async function ${fnName}(${params}, ctx?: any): Promise<boolean> {`;
    },
  );

  // Prepend shim consts inside the function body. The function body starts
  // after the opening `{` of the function declaration. We insert immediately
  // after that `{`.
  const shimLines = modelNames.map((m) =>
    `  const __${m} = ctx?.query?.(${JSON.stringify(m)});\n` +
    `  if (!__${m}) return true; // fail-open: no query ctx, skip subquery`,
  ).join('\n');

  // Inject after the first `{` following the (now async) function signature.
  // Use a marker to find the right position — the transpiled body has a
  // `: Promise<boolean> {` we just emitted.
  body = body.replace(
    /(:\s*Promise<boolean>\s*\{\n)/,
    `$1${shimLines}\n`,
  );

  // Date-safe relational comparisons. A cross-row range check (e.g. a
  // no-overlap subquery: `__o.check_in < self.check_out`) compares a STORED
  // value (the ORM returns Date objects) against the INPUT payload (ISO
  // strings). Raw `Date < string` coerces to NaN — always false — so the
  // guard silently never fires. Wrap the member-access operands of the
  // relational operators (< <= > >=) in `__cmp`, which maps a Date or ISO-8601
  // string to epoch ms and passes everything else (numbers, ids) through. The
  // operand pattern requires a `.` so it targets `row.field` accesses and
  // never matches bare identifiers / `Promise<boolean>` / the `=>` arrow.
  body = body.replace(
    /([A-Za-z_$][\w$]*(?:\.[\w$]+)+)\s*(<=?|>=?)\s*([A-Za-z_$][\w$]*(?:\.[\w$]+)+)/g,
    '__cmp($1) $2 __cmp($3)',
  );

  void guardName; // reserved for future per-guard logging
  return body;
}
