/**
 * Shared step-matching machinery — common helpers + the canonical
 * `matchAgainstConventions` walker that any ORM-specific step-conventions
 * library can compose with.
 *
 * Each ORM library (prisma, mongodb-native) exports its own
 * `STEP_CONVENTIONS` array of `{name, pattern, generateCall}` items
 * describing what code to emit for matched patterns. The matcher itself
 * is identical across libraries: walk the conventions in order, first
 * match wins; if nothing matches, compute the AI-fallback shape
 * (functionName + inputs + resultVar) deterministically.
 *
 * Extracted from prisma/step-conventions.ts and mongodb-native/step-conventions.ts
 * so future ORM libraries can drop in just the conventions array
 * without re-implementing matching logic. (#43K-D)
 */

/** Camel-case a step's text into a TS identifier. Identical algorithm
 * across all ORM libraries — function names emitted in `aiBehaviors.<X>(...)`
 * calls must agree with the function declarations the AI-behaviors-generator
 * emits. Both sides go through this. */
export function toMethod(words: string): string {
  const cleaned = words.trim().replace(/[^A-Za-z0-9\s]+/g, ' ');
  const camel = cleaned.replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
  const safe = camel.replace(/[^A-Za-z0-9_$]/g, '');
  return safe || 'unnamedStep';
}

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

/**
 * Common English adjectives/articles that can leak into entity-noun
 * extraction from NL spec steps. The matcher's pattern fragments use
 * this to skip a leading adjective when capturing the entity noun, so
 * "Create new Player with…" extracts `Player`, not `new`.
 *
 * Concern-separation note: this is NL-input-side. The TS-output-side
 * counterpart is `TS_RESERVED_WORDS` from `@specverse/types`. Don't
 * merge the two lists — they have distinct consumers and tuning
 * lifecycles. NL adjectives are open-ended (extend as we see leakage);
 * reserved identifiers are fixed by the target language spec.
 */
export const NL_LEADING_ADJECTIVES: readonly string[] = [
  'new', 'existing', 'current', 'the', 'a', 'an',
];

/**
 * Regex fragment for the optional leading-adjective skip, e.g.
 *   pattern: new RegExp(`^create\\s+${NL_ADJECTIVE_PREFIX}(\\w+)…`, 'i')
 *
 * Patterns that prefer regex literals can inline the equivalent group:
 *   /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)…/i
 * — keep this in sync if you do.
 */
export const NL_ADJECTIVE_PREFIX = `(?:(?:${NL_LEADING_ADJECTIVES.join('|')})\\s+)?`;

// Safe-identifier primitives live in `@specverse/types`:
//   - `safeEntityName(name)`   → returns the lowerCamel identifier, or
//                                 null when the name would emit a TS
//                                 reserved word as an entity reference
//                                 (`prisma.<X>.create`). Use this in
//                                 step-conventions' `generateCall` to
//                                 replace the toVar+predicate pair.
//   - `safeFunctionName(name)` → returns `{ name, reserved }`; on
//                                 reserved input, name is suffix-renamed
//                                 (`delete` → `delete_`) and the caller
//                                 emits a re-export so consumers
//                                 importing the original name still
//                                 resolve. Used by the γ-stub renderers.
// Import directly from `@specverse/types`; this file no longer
// re-exports the predicate (was `isUnsafeEntityName`, retired in favour
// of the more expressive primitives).

// #54 (2026-05-10): SharedStepContext, SharedConvention, MatchResult
// and MatcherFn are now canonical in @specverse/types. Re-exported here
// so existing dynamic-import call sites that grab types from this path
// keep working. New code should import directly from @specverse/types.
export type {
  SharedStepContext,
  SharedConvention,
  MatchResult,
  MatcherFn,
} from '@specverse/types';
import type { SharedStepContext, SharedConvention, MatchResult } from '@specverse/types';

// ── Audit-trail recorder hook ──────────────────────────────────────────
// Forward-logging discipline (engines 6.22.0+): the matcher records every
// dispatch decision at the moment it's made. realize/index.ts attaches
// a recorder before generation begins; each call to matchAgainstConventions
// (from any of the 6 call sites — pre-scan, prisma/mongo/pg controller
// generators, ai-behaviors-generator) emits a record. The recorder dedupes
// by (owner, action, ordinal) since matcher is pure and produces identical
// results across phases. See plan:
// specverse-self/docs/plans/2026-05-03-PIPELINE-AUDIT-TRAIL-INSTRUMENTATION.md
export interface StepMatchRecord {
  ownerName: string;
  actionName: string;
  stepOrdinal: number;
  stepText: string;
  matched: boolean;
  helperMethod?: string;
  aiFunctionName?: string;
}
let _stepRecorder: ((r: StepMatchRecord) => void) | null = null;
export function setStepMatchRecorder(fn: ((r: StepMatchRecord) => void) | null): void {
  _stepRecorder = fn;
}

/** Walk conventions in order. First match wins (with empty-string fallthrough).
 * Unmatched: compute the AI-delegate call site shape so the controller and
 * the AI-behaviors-generator agree on function name + inputs + resultVar. */
export function matchAgainstConventions<C extends SharedStepContext>(
  step: string,
  ctx: C,
  conventions: ReadonlyArray<SharedConvention<C>>,
  aiArgsExpr: (inputs: string[], paramNames: string[]) => string,
): MatchResult {
  let result: MatchResult | null = null;
  for (const convention of conventions) {
    const m = step.match(convention.pattern);
    if (m) {
      const call = convention.generateCall(m, ctx);
      if (call) {
        result = {
          matched: true,
          call,
          helperMethod: convention.generateMethod?.(m, ctx),
        };
        break;
      }
    }
  }

  if (!result) {
    // AI fallback shape — same across all ORMs.
    const functionName = toMethod(step);
    const declared = Array.from(ctx.declaredVars || []);
    const paramNames = ctx.parameterNames || [];
    const inputs = [...paramNames, ...declared];
    const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
    if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
    const inputObj = aiArgsExpr(inputs, paramNames);

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

  // Forward-log the decision. ctx.serviceName carries the spec owner
  // (controller or service — the shared context uses one field for both).
  if (_stepRecorder) {
    try {
      _stepRecorder({
        ownerName: ctx.serviceName,
        actionName: ctx.operationName,
        stepOrdinal: ctx.stepNum,
        stepText: step,
        matched: result.matched,
        helperMethod: result.helperMethod,
        aiFunctionName: result.functionName,
      });
    } catch { /* recorder must never break generation */ }
  }

  return result;
}
