/**
 * Behavior Generator
 *
 * Generates service/controller method bodies from behavioral metadata:
 * - preconditions -> guard checks
 * - steps -> convention-expanded logic (data-driven pattern matching)
 * - postconditions -> dev-mode assertions
 * - publishes/sideEffects -> event publishing
 * - transactional -> prisma.$transaction wrapper
 *
 * This is L3 generation: structure (L1) and CRUD (L2) are handled by
 * the schema and controller generators. This module generates real
 * business logic from behavioral specifications.
 *
 * Steps are resolved in priority order:
 * 1. Convention patterns (15 common patterns in step-conventions.ts)
 * 2. Stub generation (compiles, throws at runtime)
 */

import { matchStep, type StepContext } from './step-conventions.js';

export interface BehaviorContext {
  modelName: string;
  serviceName: string;
  operationName: string;
  prismaModel?: string;
  /** Parameter names from the action definition (e.g., ['pollId', 'optionId', 'voterName']) */
  parameterNames?: string[];
}

/**
 * A step can be a plain string or an object with extra metadata.
 * String form: "Find Order by id"
 * Object form: { step: "Apply discount", as: "discount", returns: { amount: "number" } }
 */
export type Step = string | {
  step: string;
  /** Name the result variable (default: stepNResult) */
  as?: string;
  /** Declare return type for AI functions so types flow through */
  returns?: Record<string, string> | string;
};

export interface BehaviorMetadata {
  preconditions?: string[];
  postconditions?: string[];
  sideEffects?: string[];
  steps?: Step[];
  transactional?: boolean;
}

export interface OperationMetadata {
  async?: boolean;
  cacheable?: boolean;
  idempotent?: boolean;
}

export interface BehaviorResult {
  body: string;
  helperMethods: string[];
  /** Steps that couldn't be matched by conventions — need AI generation */
  unmatchedSteps: Array<{
    step: string;
    functionName: string;
    operationName: string;
    /** Names of variables passed as the typed input object */
    inputs: string[];
    /** Declared return type from spec ({field: type} or a single type string) */
    returns?: Record<string, string> | string;
    /** Named result variable (default: stepNResult) */
    resultName?: string;
  }>;
}

/**
 * Generate a complete method body from behavioral metadata.
 * Returns the method body AND any helper methods that need to be added to the class.
 */
export function generateBehaviorBody(
  behavior: BehaviorMetadata,
  opMeta: OperationMetadata,
  context: BehaviorContext
): string {
  const result = generateBehaviorWithHelpers(behavior, opMeta, context);
  // For backward compatibility, return just the body.
  // Callers that need helper methods should use generateBehaviorWithHelpers.
  return result.body;
}

/**
 * Generate behavior body + helper methods.
 */
export function generateBehaviorWithHelpers(
  behavior: BehaviorMetadata,
  opMeta: OperationMetadata,
  context: BehaviorContext
): BehaviorResult {
  const parts: string[] = [];
  const helperMethods: string[] = [];

  // Track variables declared by preconditions so steps can reuse them
  const preconditionDeclared = new Set<string>();

  // Precondition guards
  const preconditions = generatePreconditionChecks(
    behavior.preconditions || [], context, preconditionDeclared
  );
  if (preconditions) parts.push(preconditions);

  // Main logic (from steps or inferred from operation semantics)
  const { code, helpers, unmatchedSteps } = generateStepLogic(
    behavior.steps || [], context, preconditionDeclared
  );
  parts.push(code);
  helperMethods.push(...helpers);

  // Postcondition verification
  const postconditions = generatePostconditionVerification(
    behavior.postconditions || []
  );
  if (postconditions) parts.push(postconditions);

  // Event publishing
  const events = generateEventPublishing(
    behavior.sideEffects || [], context.operationName, context.parameterNames
  );
  if (events) parts.push(events);

  let body = parts.join('\n\n');

  // Wrap in transaction if flagged
  if (behavior.transactional) {
    body = generateTransactionWrapper(body, context);
  }

  return { body, helperMethods, unmatchedSteps };
}

/**
 * Generate precondition checks from natural-language strings.
 * Tracks declared variables to avoid duplicates and reuses fetched entities.
 */
export function generatePreconditionChecks(
  preconditions: string[],
  context: BehaviorContext,
  declared?: Set<string>
): string {
  if (preconditions.length === 0) return '';

  if (!declared) declared = new Set<string>();
  const checks = preconditions.map(pc => matchPreconditionPattern(pc, context, declared));
  return `    // === PRECONDITIONS ===\n${checks.join('\n')}`;
}

/** Find the parameter name that refers to a model (e.g., 'pollId' for 'Poll') */
function findIdParam(modelName: string, paramNames: string[]): string {
  const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
  // Check for modelId (e.g., pollId)
  const idParam = paramNames.find(p => p === `${modelVar}Id`);
  if (idParam) return idParam;
  // Check for just 'id'
  if (paramNames.includes('id')) return 'id';
  return `${modelVar}Id`;
}

function matchPreconditionPattern(
  precondition: string,
  context: BehaviorContext,
  declared: Set<string>
): string {
  const pc = precondition.toLowerCase();
  const params = context.parameterNames || [];

  // Pattern: "{Model} exists"
  const existsMatch = precondition.match(/^(\w+)\s+exists/i);
  if (existsMatch) {
    const entity = existsMatch[1];
    const entityVar = entity.charAt(0).toLowerCase() + entity.slice(1);
    const idParam = findIdParam(entity, params);
    if (declared.has(entityVar)) {
      // Already fetched — just check it
      return `    // Guard: ${precondition} (already fetched)`;
    }
    declared.add(entityVar);
    return `    const ${entityVar} = await prisma.${entityVar}.findUniqueOrThrow({ where: { id: ${idParam} } });`;
  }

  // Pattern: "{field} is not empty" / "{field} is required"
  if (pc.includes('is not empty') || pc.includes('is required')) {
    const fieldMatch = precondition.match(/^(\w+)\s+is/i);
    if (fieldMatch) {
      const field = fieldMatch[1];
      const paramRef = params.includes(field) ? field : `params.${field}`;
      return `    if (!${paramRef}) throw new Error('${field} is required');`;
    }
  }

  // Pattern: "{field} is valid"
  if (pc.includes('is valid')) {
    return `    // TODO: Implement validation: ${precondition}`;
  }

  // Pattern: "{X} matches {Y}" / "{X} equals {Y}"
  const matchesMatch = precondition.match(/^(\w+)\s+(?:matches|equals)\s+(.+)/i);
  if (matchesMatch) {
    const left = matchesMatch[1].charAt(0).toLowerCase() + matchesMatch[1].slice(1);
    const right = matchesMatch[2].charAt(0).toLowerCase() + matchesMatch[2].slice(1);
    return `    if (${left} !== ${right}) throw new Error('${matchesMatch[1]} must match ${matchesMatch[2]}');`;
  }

  // Pattern: "{Model} is {state}"
  const stateMatch = precondition.match(/^(\w+)\s+is\s+(\w+)$/i);
  if (stateMatch) {
    const model = stateMatch[1];
    const state = stateMatch[2];
    const modelVar = model.charAt(0).toLowerCase() + model.slice(1);
    const idParam = findIdParam(model, params);

    // If already declared, reuse; otherwise fetch
    if (!declared.has(modelVar)) {
      declared.add(modelVar);
      return `    const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: ${idParam} } });
    if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
    }
    return `    if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
  }

  // Unrecognized
  return `    // TODO: Implement precondition: ${precondition}`;
}

/**
 * Generate step logic using convention-based pattern matching.
 */
function generateStepLogic(
  steps: Step[],
  context: BehaviorContext,
  preconditionDeclared?: Set<string>
): { code: string; helpers: string[]; unmatchedSteps: BehaviorResult['unmatchedSteps'] } {
  const helpers: string[] = [];
  const unmatchedSteps: BehaviorResult['unmatchedSteps'] = [];

  // Shared declared variables across steps — carried through from preconditions
  const declaredVars = preconditionDeclared || new Set<string>();

  if (steps && steps.length > 0) {
    const stepCode = steps.map((stepInput, i) => {
      // Normalize to { text, as, returns }
      const stepText = typeof stepInput === 'string' ? stepInput : stepInput.step;
      const resultName = typeof stepInput === 'object' ? stepInput.as : undefined;
      const returns = typeof stepInput === 'object' ? stepInput.returns : undefined;

      if (typeof stepText !== 'string') {
        return `    // Step ${i + 1}: Complex operation — see expanded definition`;
      }

      const ctx: StepContext = {
        modelName: context.modelName,
        prismaModel: context.prismaModel || context.modelName,
        serviceName: context.serviceName,
        operationName: context.operationName,
        stepNum: i + 1,
        parameterNames: context.parameterNames,
        declaredVars,
        resultName,  // Pass through the named result
      };

      const result = matchStep(stepText, ctx);
      if (result.helperMethod) {
        helpers.push(result.helperMethod);
      }
      if (!result.matched && result.functionName) {
        unmatchedSteps.push({
          step: stepText,
          functionName: result.functionName,
          operationName: context.operationName,
          inputs: result.inputs || [],
          returns,
          resultName: resultName || `step${i + 1}Result`,
        });
      }
      return result.call;
    });

    return {
      code: `    // === EXECUTE ===\n${stepCode.join('\n\n')}`,
      helpers,
      unmatchedSteps,
    };
  }

  // No steps — infer from operation name
  return {
    code: `    // === EXECUTE ===\n${inferLogicFromOperationName(context)}`,
    helpers,
    unmatchedSteps,
  };
}

function inferLogicFromOperationName(context: BehaviorContext): string {
  const op = context.operationName;
  const prismaModel = context.prismaModel || context.modelName;
  const modelVar = prismaModel.charAt(0).toLowerCase() + prismaModel.slice(1);

  if (op.startsWith('handle')) {
    const event = op.replace('handle', '');
    return `    // Event handler: ${op}
    console.log('[${context.serviceName}] Processing ${event}', params);
    return { handled: true, event: '${event}' };`;
  }

  if (op.startsWith('validate')) {
    return `    // Validation: ${op}
    const records = await prisma.${modelVar}.findMany({ where: { id: params.id } });
    return { valid: records.length > 0, checked: records.length };`;
  }

  if (op.startsWith('get') || op.startsWith('list') || op.startsWith('find')) {
    return `    return await prisma.${modelVar}.findMany({});`;
  }

  return `    // TODO: Implement ${op}
    return { success: true };`;
}

/**
 * Generate postcondition verification (dev-mode assertions).
 */
export function generatePostconditionVerification(
  postconditions: string[]
): string {
  if (postconditions.length === 0) return '';

  const checks = postconditions.map(pc =>
    `      console.assert(true, 'POSTCONDITION: ${pc}');`
  );

  return `    // === POSTCONDITIONS (dev-mode) ===
    if (process.env.NODE_ENV === 'development') {
${checks.join('\n')}
    }`;
}

/**
 * Generate event publishing from sideEffects.
 */
export function generateEventPublishing(
  sideEffects: string[],
  operationName: string,
  parameterNames?: string[]
): string {
  if (!sideEffects || sideEffects.length === 0) return '';

  const paramFields = parameterNames?.length
    ? parameterNames.join(', ') + ', '
    : '';

  const publishes = sideEffects.map(event =>
    `    await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() } as any);`
  );

  return `    // === EVENTS ===\n${publishes.join('\n')}`;
}

/**
 * Wrap method body in prisma.$transaction.
 */
export function generateTransactionWrapper(
  body: string,
  context: BehaviorContext
): string {
  return `    return await prisma.$transaction(async (tx) => {
${body}
    });`;
}
