/**
 * Step Conventions — data-driven pattern matching for behavior steps.
 *
 * Each convention maps a natural language step pattern to generated code.
 * Patterns are tried in order; first match wins. Unmatched steps get stubs.
 *
 * The match-walker + toMethod/toVar identifier helpers live in
 * `../_shared/step-matching.ts` so the same machinery powers
 * `mongodb-native/step-conventions.ts`. This file is now (almost
 * entirely) the prisma-specific conventions array.  (#43K-D)
 */

import {
  toMethod,
  toVar,
  matchAgainstConventions,
  type SharedConvention,
  type SharedStepContext,
} from '../_shared/step-matching.js';
import { safeEntityName } from '@specverse/types';

export type StepConvention = SharedConvention<StepContext>;

export interface StepContext extends SharedStepContext {
  // Optional: legacy callers (ai-behaviors-generator, controller-generator)
  // supply this as a pre-lowercased model var. The per-action emitter
  // (engines/src/realize/per-action-emitter.ts) builds a plain
  // SharedStepContext and doesn't set it. Conventions below read
  // ctx.modelName (canonical) and call toVar() locally — same result
  // either way, but the per-action path no longer crashes with
  // `Cannot read properties of undefined (reading 'charAt')` when a
  // convention reads ctx.prismaModel that was never supplied.
  prismaModel?: string;
}

/**
 * Resolve a value expression in a step to a TypeScript expression.
 *
 * Handles:
 * - "current time", "now", "timestamp" → new Date().toISOString()
 * - References to declared variables (e.g., "discount.amount") → discount.amount
 * - References to operation parameters → parameter name
 * - References to stepNResult → stepNResult
 * - Numbers → numeric literal
 * - Boolean literals → true/false
 * - Everything else → quoted string literal
 */
function resolveValue(rawValue: string, ctx: StepContext): string {
  const value = rawValue.trim().replace(/^['"]|['"]$/g, '');

  // Time expressions
  if (/^(current\s*time|now|timestamp)$/i.test(value)) {
    return 'new Date().toISOString()';
  }

  // Boolean literals
  if (value === 'true' || value === 'false') return value;

  // Numeric literal
  if (/^-?\d+(\.\d+)?$/.test(value)) return value;

  const declared = ctx.declaredVars || new Set();
  const params = ctx.parameterNames || [];

  // Reference: "varName" or "varName.field" — check if root matches a declared var or parameter
  const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
  if (rootMatch) {
    const root = rootMatch[1];
    if (declared.has(root) || params.includes(root)) {
      return value;  // use as-is (e.g., "discount.amount")
    }
  }

  // Natural language reference: "calculated amount", "the discount" etc.
  // Try to match to a declared var by tokenising
  const tokens = value.toLowerCase().split(/\s+/);
  for (const declaredName of [...declared, ...params]) {
    const normalized = declaredName.toLowerCase();
    if (tokens.includes(normalized)) return declaredName;
  }

  // "calculated X" / "computed X" / "the X" — look for a stepNResult with
  // matching field. Since we don't know the AI function's return shape at
  // compile time, emit a typed access with a TODO comment the developer can fix.
  const calcMatch = value.match(/^(?:the\s+|calculated\s+|computed\s+|resulting\s+)(\w+)/i);
  if (calcMatch) {
    const field = calcMatch[1];
    // Find most recent stepNResult
    const stepResults = [...declared].filter(v => /^step\d+Result$/.test(v)).sort().reverse();
    if (stepResults.length > 0) {
      return `(${stepResults[0]} as any).${field} /* TODO: verify field name */`;
    }
  }

  // Looks like natural language (multiple words with spaces) — emit TODO
  if (/\s/.test(value)) {
    return `/* TODO: resolve "${value}" — no matching declared variable */ null`;
  }

  // Fallback: quoted string literal
  return `'${value.replace(/'/g, "\\'")}'`;
}

export const STEP_CONVENTIONS: StepConvention[] = [
  // --- Validation ---
  {
    name: 'validate',
    pattern: /^validate\s+(.+)/i,
    generateCall: (m, ctx) => `    // Step ${ctx.stepNum}: Validate ${m[1]}
    // TODO: Add validation logic for ${m[1]}
    }`,
  },

  // --- Check / Guard ---
  {
    name: 'check',
    pattern: /^check\s+(.+)/i,
    generateCall: (m, ctx) => {
      const condition = m[1];
      const methodName = toMethod('check ' + condition);
      return `    // Step ${ctx.stepNum}: Check ${condition}
    // TODO: Implement check — ${condition}
    // const checkResult = ...
    // if (!checkResult) throw new Error('Check failed: ${condition}');`;
    },
  },

  // --- Find / Lookup ---
  //
  // All entity-targeting patterns (find / create / update / delete) share
  // two safeguards:
  //   1. A pattern-level leading-adjective skip
  //      `(?:(?:new|existing|current|the|a|an)\s+)?` so NL phrasings like
  //      "Find existing Player by id" or "Create new Player with ..."
  //      extract the noun, not the adjective. Kept in sync with
  //      `NL_LEADING_ADJECTIVES` in `_shared/step-matching.ts`.
  //   2. `safeEntityName(m[1])` — the reserved-word-safe primitive from
  //      `@specverse/types`. Returns the lowerCamel identifier, or `null`
  //      when the extraction would emit a TS reserved word
  //      (e.g. `prisma.new.create`). On null, generateCall returns `''` so
  //      the matcher falls through to the AI [WRITE] fallback.
  {
    name: 'find',
    pattern: /^find\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+by\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      const field = m[2];
      const params = ctx.parameterNames || [];
      const declared = ctx.declaredVars || new Set();

      // Find the ID parameter: modelId (e.g., pollId) or field directly
      const idParam = field === 'id'
        ? (params.find(p => p === `${modelVar}Id`) || params.find(p => p === 'id') || `${modelVar}Id`)
        : (params.find(p => p === field) || `params.${field}`);

      // Skip if already declared by precondition
      if (declared.has(modelVar)) {
        return `    // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
      }
      declared.add(modelVar);

      return `    // Step ${ctx.stepNum}: Find ${model} by ${field}
    const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { ${field}: ${idParam} } });`;
    },
  },

  // --- Create ---
  {
    name: 'create',
    pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      // Build data from parameter names if available
      const paramNames = ctx.parameterNames || [];
      const dataFields = paramNames.length > 0
        ? `{ ${paramNames.join(', ')} }`
        : 'data';
      return `    // Step ${ctx.stepNum}: Create ${model}
    const ${modelVar} = await prisma.${modelVar}.create({ data: ${dataFields} });`;
    },
  },

  // --- Update field ---
  {
    name: 'update-field',
    pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      const field = m[2];
      const rawValue = m[3];
      const val = resolveValue(rawValue, ctx);
      return `    // Step ${ctx.stepNum}: Update ${model} ${field} to ${rawValue.trim()}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: { ${field}: ${val} },
    });`;
    },
  },

  // --- Generic update ---
  {
    name: 'update',
    pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      return `    // Step ${ctx.stepNum}: Update ${model}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: params,
    });`;
    },
  },

  // --- Delete ---
  {
    name: 'delete',
    pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      return `    // Step ${ctx.stepNum}: Delete ${model}
    await prisma.${modelVar}.delete({ where: { id: ${modelVar}.id } });`;
    },
  },

  // --- Transition / Evolve ---
  {
    name: 'transition',
    pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const state = m[2];
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      return `    // Step ${ctx.stepNum}: Transition ${model} to ${state}
    if (${modelVar}.status === '${state}') throw new Error('${model} is already ${state}');
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: { status: '${state}' },
    });`;
    },
  },

  // --- Set field ---
  {
    name: 'set',
    pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
    generateCall: (m, ctx) => {
      const field = m[1];
      const rawValue = m[2];
      const modelVar = toVar(ctx.modelName);
      const val = resolveValue(rawValue, ctx);
      return `    // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: { ${field}: ${val} },
    });`;
    },
  },

  // --- Increment ---
  {
    name: 'increment',
    pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const field = m[1];
      const amount = m[2];
      const modelVar = toVar(ctx.modelName);
      const amountVal = /^\d+$/.test(amount) ? amount : amount;
      return `    // Step ${ctx.stepNum}: Increment ${field} by ${amount}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: { ${field}: { increment: ${amountVal} } },
    });`;
    },
  },

  // --- Decrement ---
  {
    name: 'decrement',
    pattern: /^decrement\s+(\w+)\s+by\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const field = m[1];
      const amount = m[2];
      const modelVar = toVar(ctx.modelName);
      const amountVal = /^\d+$/.test(amount) ? amount : amount;
      return `    // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: { ${field}: { decrement: ${amountVal} } },
    });`;
    },
  },

  // NOTE: "calculate X" is intentionally NOT a convention — it falls through to AI
  // behaviors because calculations are domain-specific pure functions that benefit
  // from AI generation with the full input context.

  // --- Send event ---
  {
    name: 'send-event',
    pattern: /^(?:send|emit|publish)\s+(\w+)\s+event/i,
    generateCall: (m, ctx) => {
      const event = m[1];
      return `    // Step ${ctx.stepNum}: Emit ${event} event
    await eventBus.publish('${event}', { ${toVar(ctx.modelName)}Id: ${toVar(ctx.modelName)}.id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() });`;
    },
  },

  // --- Send notification ---
  {
    name: 'send-notification',
    pattern: /^send\s+(\w+)\s+notification/i,
    generateCall: (m, ctx) => {
      const type = m[1];
      return `    // Step ${ctx.stepNum}: Send ${type} notification
    await eventBus.publish('${type}Notification', { ${toVar(ctx.modelName)}Id: ${toVar(ctx.modelName)}.id, operation: '${ctx.operationName}' });`;
    },
  },

  // --- Call service ---
  {
    name: 'call-service',
    pattern: /^call\s+(\w+)\.(\w+)/i,
    generateCall: (m, ctx) => {
      const service = m[1];
      const method = m[2];
      const serviceVar = safeEntityName(service);
      if (!serviceVar) return '';
      return `    // Step ${ctx.stepNum}: Call ${service}.${method}
    await ${serviceVar}.${method}({ ${(ctx.parameterNames || []).join(', ')} });`;
    },
  },

  // --- Return ---
  {
    name: 'return',
    pattern: /^return\s+(.+)/i,
    generateCall: (m, ctx) => {
      const value = m[1].trim();
      const isModel = /^(updated|created|the)\s+/i.test(value) || value === ctx.modelName;
      if (isModel) {
        return `    // Step ${ctx.stepNum}: Return result
    return ${toVar(ctx.modelName)};`;
      }
      // Only emit a typed `return <expr>;` when `value` is a single
      // bareword identifier (or a single dotted/bracketed accessor) —
      // anything that's syntactically a valid TypeScript expression
      // already declared in the action's scope. Spec text like
      // "logout confirmation" or "204 No Content" produces a
      // grammatical English phrase but unparseable TS:
      //   return logout confirmation;        // two bare identifiers
      //   return 204 No Content;             // number + identifiers
      // Returning the empty string here makes the matcher treat this
      // as "convention regex matched but no safe emission" — the
      // matcher falls through to the AI-fallback ([WRITE]) shape so
      // the per-action / per-owner LLM hook handles it. Verified
      // empirically by the 2026-05-11 idle-meta per-owner trial:
      // the LLM correctly stubbed actions where prose returns had
      // been bound to broken pre-baked snippets.
      const isSafeIdentifier = /^[A-Za-z_$][\w$]*$/.test(value);
      const isSafePropertyAccess = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value);
      if (isSafeIdentifier || isSafePropertyAccess) {
        // Reference a single in-scope variable — but only if it was
        // declared earlier in this action's emitted body (via the
        // matcher's declaredVars set). Otherwise we'd emit
        // `return AuthRegisterResponse;` referencing a type name
        // that never resolves at runtime.
        const head = value.split('.')[0]!;
        const declared = ctx.declaredVars instanceof Set
          ? ctx.declaredVars.has(head)
          : false;
        if (declared) {
          return `    // Step ${ctx.stepNum}: Return ${value}
    return ${value};`;
        }
      }
      // Bounce to [WRITE] — LLM will author this return.
      return '';
    },
  },
];

/**
 * Match a step string against all conventions. Returns the generated call
 * code and optionally a helper method to add to the class.
 *
 * For unmatched steps, the call is wired to a pure AI function that takes
 * all currently-declared variables as a typed input object and returns
 * a result that subsequent steps can use.
 */
export function matchStep(
  step: string,
  ctx: StepContext
): {
  call: string;
  helperMethod?: string;
  matched: boolean;
  functionName?: string;
  inputs?: string[];
  resultVar?: string;
} {
  // Prisma's AI-fallback uses plain field shorthand `{ a, b, c }` (no
  // `args.X` indirection — the controller-generator destructures params
  // at function entry).
  const aiArgsExpr = (inputs: string[], _paramNames: string[]) =>
    inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}';
  return matchAgainstConventions(step, ctx, STEP_CONVENTIONS, aiArgsExpr);
}
