/**
 * 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.
 */

export interface StepConvention {
  name: string;
  pattern: RegExp;
  generateCall: (match: RegExpMatchArray, ctx: StepContext) => string;
  generateMethod?: (match: RegExpMatchArray, ctx: StepContext) => string;
}

export interface StepContext {
  modelName: string;
  prismaModel: string;
  serviceName: string;
  operationName: string;
  stepNum: number;
  /** Parameter names from the action (e.g., ['pollId', 'optionId']) */
  parameterNames?: string[];
  /** Variables already declared (to avoid redeclaration) */
  declaredVars?: Set<string>;
  /** Named result variable (from spec's `as:` clause) */
  resultName?: string;
}

function toVar(name: string): string {
  return name.charAt(0).toLowerCase() + name.slice(1);
}

function toMethod(words: string): string {
  return words.trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, c => c.toLowerCase());
}

/**
 * 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 ---
  {
    name: 'find',
    pattern: /^find\s+(\w+)\s+by\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const field = m[2];
      const modelVar = toVar(model);
      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+(\w+)(?:\s+(?:with\s+)?(.+))?/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = toVar(model);
      // 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+(\w+)\s+(\w+)\s+to\s+(.+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const field = m[2];
      const rawValue = m[3];
      const modelVar = toVar(model);
      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+(\w+)(?:\s+(.+))?/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = toVar(model);
      return `    // Step ${ctx.stepNum}: Update ${model}
    await prisma.${modelVar}.update({
      where: { id: ${modelVar}.id },
      data: params,
    });`;
    },
  },

  // --- Delete ---
  {
    name: 'delete',
    pattern: /^delete\s+(\w+)/i,
    generateCall: (m, ctx) => {
      const model = m[1];
      const modelVar = toVar(model);
      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 = toVar(model);
      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.prismaModel);
      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.prismaModel);
      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.prismaModel);
      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.prismaModel)}Id: ${toVar(ctx.prismaModel)}.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.prismaModel)}Id: ${toVar(ctx.prismaModel)}.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];
      return `    // Step ${ctx.stepNum}: Call ${service}.${method}
    await ${toVar(service)}.${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.prismaModel)};`;
      }
      return `    // Step ${ctx.stepNum}: Return ${value}
    return ${value};`;
    },
  },
];

/**
 * 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;
} {
  for (const convention of STEP_CONVENTIONS) {
    const match = step.match(convention.pattern);
    if (match) {
      return {
        call: convention.generateCall(match, ctx),
        helperMethod: convention.generateMethod?.(match, ctx),
        matched: true,
      };
    }
  }

  // No match — pure AI function call
  // Inputs = all variables declared by previous steps + operation parameters
  const functionName = toMethod(step);
  const declared = Array.from(ctx.declaredVars || []);
  const paramNames = ctx.parameterNames || [];
  const inputs = [...paramNames, ...declared];

  // Use named result from spec (`as:`) or default to stepNResult
  const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
  const inputObj = inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}';

  // Register the result variable so subsequent steps can reference it
  if (ctx.declaredVars) ctx.declaredVars.add(resultVar);

  return {
    call: `    // Step ${ctx.stepNum}: ${step} [AI-generated — pure function]
    const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
    matched: false,
    functionName,
    inputs,
    resultVar,
  };
}
