/**
 * Step Conventions for PostgreSQL native (pg) — mirrors the mongo-native
 * library's pattern set but emits raw-SQL via the pgClient helpers
 * (`insertOne` / `findOneByField` / `updateOneById` / etc.) instead of
 * collection methods.
 *
 * Same convention names + same pattern texts, so spec authors can swap
 * MongoDBNativeDriver for PostgresNativeDriver in the manifest without
 * touching their .specly. The shared matcher (`matchAgainstConventions`
 * in `../_shared/step-matching.ts`) drives both libraries — this file
 * is purely the conventions array + a pg-flavoured `matchPgStep`
 * delegate. (#43K-D follow-up: third ORM library demonstrating the
 * abstraction.)
 */

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

export type PgStepConvention = SharedConvention<PgStepContext>;

export interface PgStepContext extends SharedStepContext {
  /** Table name for the model (lowercased + 's' or storage.table). */
  tableName: string;
  /** Full model registry so conventions can read attributes for default
   *  values, types, and FK target tables. */
  models?: Record<string, any>;
}

/**
 * Compute sensible default values for a model's attributes — the
 * postgres-native counterpart of mongodb-native's `deriveModelDefaults`.
 * Same heuristics; same conventionManaged exclusions.
 *
 * Defaults emitted as `key: expression` strings ready to inline into an
 * object-literal record body (the body is then handed to `insertOne`,
 * which does the SQL plumbing). Empty if the model isn't in the registry.
 */
export function deriveModelDefaults(
  modelName: string,
  ctx: PgStepContext,
): string[] {
  if (!ctx.models) return [];
  const model = ctx.models[modelName] || ctx.models[modelName.charAt(0).toUpperCase() + modelName.slice(1)];
  if (!model) return [];
  const attrs = model.attributes;
  if (!attrs) return [];
  const list = Array.isArray(attrs)
    ? attrs.map((a: any) => [a.name, a])
    : Object.entries(attrs);

  const declaredVars = ctx.declaredVars || new Set();
  const out: string[] = [];

  // The "id" column is database-managed (BIGSERIAL or DEFAULT gen_random_uuid())
  // so we don't emit it from the convention. createdAt/updatedAt come from
  // the convention footer.
  const conventionManaged = new Set(['createdAt', 'updatedAt', 'id']);
  for (const [name, attr] of list as [string, any][]) {
    if (!name) continue;
    if (conventionManaged.has(name)) continue;
    const required = !!attr.required;
    const hasDefault = attr.default !== undefined;
    if (!required && !hasDefault) continue;
    if (hasDefault) {
      out.push(`${name}: ${formatDefault(attr.default, attr.type)}`);
      continue;
    }
    const type = (attr.type || 'String').toLowerCase();
    if (type === 'integer' || type === 'int' || type === 'number' || type === 'float') {
      const min = attr.min ?? 0;
      out.push(`${name}: ${min}`);
    } else if (type === 'boolean') {
      out.push(`${name}: false`);
    } else if (type === 'datetime' || type === 'date') {
      out.push(`${name}: new Date().toISOString()`);
    } else {
      // String / UUID / Text — try FK resolution first.
      const selfVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
      const fkMatch = name.match(/^(.+)Id$/);
      if (fkMatch && fkMatch[1] !== selfVar && declaredVars.has(fkMatch[1])) {
        out.push(`${name}: (${fkMatch[1]} as any)?.id`);
      } else {
        out.push(`${name}: ''`);
      }
    }
  }
  return out;
}

function formatDefault(value: any, type?: string): string {
  if (value === null || value === undefined) return 'null';
  if (typeof value === 'boolean') return String(value);
  if (typeof value === 'number') return String(value);
  if (typeof value === 'string') {
    if (/^-?\d+(\.\d+)?$/.test(value)) return value;
    if (value === 'true' || value === 'false') return value;
    if (value === 'now' && (type === 'DateTime' || type === 'Date')) {
      return 'new Date().toISOString()';
    }
    return JSON.stringify(value);
  }
  return JSON.stringify(value);
}

function toTable(modelName: string): string {
  return modelName.toLowerCase() + 's';
}

/** Resolve a value reference in step text to a TS expression. Mirrors
 *  mongo's resolveValue so spec phrases map identically. */
function resolveValue(rawValue: string, ctx: PgStepContext): string {
  const value = rawValue.trim().replace(/^['"]|['"]$/g, '');
  if (/^(current\s*time|now|timestamp)$/i.test(value)) return 'new Date().toISOString()';
  if (/^-?\d+(\.\d+)?$/.test(value)) return value;
  if (value === 'true' || value === 'false') return value;
  if (value === 'null') return 'null';
  const declared = ctx.declaredVars || new Set();
  const params = ctx.parameterNames || [];
  const head = value.split('.')[0]!;
  if (declared.has(head)) return value;
  if (params.includes(head)) return `args.${value}`;
  return `'${value.replace(/'/g, "\\'")}'`;
}

function mostRecentStepResult(ctx: PgStepContext): string | null {
  const declared = Array.from(ctx.declaredVars || []);
  const stepResults = declared.filter((v) => /^step\d+Result$/.test(v))
    .sort((a, b) => parseInt(b.slice(4), 10) - parseInt(a.slice(4), 10));
  return stepResults[0] ?? null;
}

function pascal(model: string): string {
  return model.charAt(0).toUpperCase() + model.slice(1);
}

export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
  // --- Find / Lookup by single field ---
  {
    name: 'find-by-field',
    pattern: /^(?:look\s+up|find|fetch|get)\s+(\w+)\s+by\s+(\w+)(?:\s+or\s+fail.*)?$/i,
    generateCall: (m, ctx) => {
      const model = m[1]!;
      const field = m[2]!;
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      const table = toTable(model);
      const params = ctx.parameterNames || [];
      const declared = ctx.declaredVars || new Set();
      const idVal = field === 'id'
        ? (params.includes(`${modelVar}Id`) ? `args.${modelVar}Id` : 'args.id')
        : `args.${field}`;

      if (declared.has(modelVar)) {
        return `    // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
      }
      declared.add(modelVar);
      const failOnMissing = /or\s+fail/i.test(m[0]);
      // `let` so subsequent conditional-create can reassign on null lookup.
      return `    // Step ${ctx.stepNum}: Find ${model} by ${field}
    let ${modelVar} = await findOneByField('${table}', '${field}', ${idVal}) as any;${failOnMissing ? `
    if (!${modelVar}) throw new Error('${model} not found');` : ''}`;
    },
  },

  // --- Find by composite (two fields) ---
  {
    name: 'find-by-composite',
    pattern: /^(?:look\s+up|find|fetch|get)(?:\s+existing)?\s+(\w+)\s+by\s+(\w+)\s+and\s+(\w+)$/i,
    generateCall: (m, ctx) => {
      const model = m[1]!;
      const f1 = m[2]!;
      const f2 = m[3]!;
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      const table = toTable(model);
      const declared = ctx.declaredVars || new Set();
      if (declared.has(modelVar)) {
        return `    // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2} (already loaded)`;
      }
      declared.add(modelVar);
      const resolveFieldSource = (f: string) => {
        const stripIdMatch = f.match(/^(.+)Id$/);
        if (stripIdMatch) {
          const candidate = stripIdMatch[1]!;
          if (candidate !== modelVar && declared.has(candidate)) {
            return `(${candidate} as any)?.id`;
          }
        }
        return `args.${f}`;
      };
      const f1Src = resolveFieldSource(f1);
      const f2Src = resolveFieldSource(f2);
      return `    // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2}
    let ${modelVar} = await findOneByFields('${table}', { ${f1}: ${f1Src}, ${f2}: ${f2Src} }) as any;`;
    },
  },

  // --- Create model record ---
  {
    name: 'create',
    // Leading-adjective skip (NL_LEADING_ADJECTIVES) + isUnsafeEntityName
    // safety net — see comment in prisma/step-conventions.ts "find" block.
    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 '';
      const table = toTable(model);
      const params = ctx.parameterNames || [];
      const declared = ctx.declaredVars || new Set();
      declared.add(modelVar);
      const dataExpr = params.length > 0 ? `{ ${params.join(', ')} }` : 'args';
      return `    // Step ${ctx.stepNum}: Create ${model}
    const ${modelVar} = await insertOne('${table}', ${dataExpr}) as any;`;
    },
  },

  // --- Update specific field on previously-loaded model ---
  {
    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]!;
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const table = toTable(model);
      const val = resolveValue(rawValue, ctx);
      return `    // Step ${ctx.stepNum}: Update ${model}.${field} to ${rawValue.trim()}
    await updateOneById('${table}', ${modelVar}.id, { ${field}: ${val} });`;
    },
  },

  // --- Update field timestamp ---
  {
    name: 'update-field-timestamp',
    pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+timestamp$/i,
    generateCall: (m, ctx) => {
      const model = m[1]!;
      const modelVar = safeEntityName(model);
      if (!modelVar) return '';
      const field = m[2]!;
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const table = toTable(model);
      return `    // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
    await updateOneById('${table}', ${modelVar}.id, { ${field}: new Date().toISOString() });`;
    },
  },

  // --- Generic "Update X" (writes args back) ---
  {
    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 '';
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const table = toTable(model);
      return `    // Step ${ctx.stepNum}: Update ${model}
    await updateOneById('${table}', ${modelVar}.id, args);`;
    },
  },

  // --- Delete model record ---
  {
    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 '';
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const table = toTable(model);
      return `    // Step ${ctx.stepNum}: Delete ${model}
    await deleteOneById('${table}', ${modelVar}.id);`;
    },
  },

  // --- Transition to lifecycle state ---
  {
    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 '';
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const table = toTable(model);
      return `    // Step ${ctx.stepNum}: Transition ${model} to ${state}
    if ((${modelVar} as any).status === '${state}') throw new Error('${model} is already ${state}');
    await updateOneById('${table}', ${modelVar}.id, { status: '${state}' });`;
    },
  },

  // --- Set field to value (on the controller's primary model) ---
  {
    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 updateOneById(TABLE_NAME, (${modelVar} as any).id, { ${field}: ${val} });`;
    },
  },

  // --- Increment / Decrement ---
  // Postgres has no Mongo $inc; emit a column = column +/- N update. The
  // helper updateOneById doesn't support raw SQL fragments, so emit query
  // directly here.
  {
    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);
      return `    // Step ${ctx.stepNum}: Increment ${field} by ${amount}
    await query(\`UPDATE "\${TABLE_NAME}" SET "${field}" = "${field}" + $1 WHERE "id" = $2\`, [${amount}, (${modelVar} as any).id]);`;
    },
  },
  {
    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);
      return `    // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
    await query(\`UPDATE "\${TABLE_NAME}" SET "${field}" = "${field}" - $1 WHERE "id" = $2\`, [${amount}, (${modelVar} as any).id]);`;
    },
  },

  // --- Persist / Save / Store ---
  {
    name: 'persist',
    pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
    generateCall: (m, ctx) => {
      const target = safeEntityName(m[1]!.replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
      if (!target) return '';
      const table = toTable(target);
      const recordSrc = mostRecentStepResult(ctx) ?? 'args';
      // Ensure `record` is an object before insertOne — wrap primitives.
      return `    // Step ${ctx.stepNum}: Persist ${m[1]}
    {
      const _rec: any = ${recordSrc} && typeof ${recordSrc} === 'object' && !Array.isArray(${recordSrc})
        ? ${recordSrc} : { value: ${recordSrc} };
      await insertOne('${table}', _rec);
    }`;
    },
  },

  // --- Conditional create ---
  {
    name: 'conditional-create',
    pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
    generateCall: (m, ctx) => {
      const modelVar = safeEntityName(m[1]!);
      if (!modelVar) return '';
      const Model = pascal(m[2]!);
      const table = toTable(Model);
      if (!ctx.declaredVars?.has(modelVar)) return '';
      const defaults = deriveModelDefaults(Model, ctx);
      const defaultsBlock = defaults.length > 0 ? defaults.join(', ') + ',' : '';
      return `    // Step ${ctx.stepNum}: If ${modelVar} does not exist, create new ${Model}
    if (!${modelVar}) {
      const _newRecord = { ${defaultsBlock} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
      ${modelVar} = await insertOne('${table}', _newRecord) as any;
    }`;
    },
  },

  // --- Conditional update ---
  {
    name: 'conditional-update',
    pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
    generateCall: (m, ctx) => {
      const modelVar = safeEntityName(m[1]!);
      if (!modelVar) return '';
      const field = m[2]!;
      const table = toTable(m[1]!);
      if (!ctx.declaredVars?.has(modelVar)) return '';
      return `    // Step ${ctx.stepNum}: If ${modelVar} exists, update ${field}
    if (${modelVar}) {
      await updateOneById('${table}', (${modelVar} as any).id, { ${field}: new Date().toISOString() });
    }`;
    },
  },

  // --- Auto-create / Bulk fan-out ---
  {
    name: 'auto-create-loop',
    pattern: /^(?:auto-create|bulk\s+create)\s+(\w+)\s+(?:profile|record|entry|entries)?s?\s+for\s+(?:all\s+)?(?:available\s+)?(\w+)$/i,
    generateCall: (m, ctx) => {
      const Model = pascal(m[1]!);
      const targetTable = toTable(Model);
      const sourceTable = m[2]!.toLowerCase();
      const declared = Array.from(ctx.declaredVars || []);
      const ownerVar = declared.find((v) => !/^step\d+Result$/.test(v) && v !== 'args');
      const sourceSingular = sourceTable.replace(/s$/, '');
      const linkField = sourceSingular + 'Id';
      const ownerLinkField = ownerVar ? ownerVar + 'Id' : 'ownerId';

      const modelDefaults = deriveModelDefaults(Model, ctx);
      const defaultsBlock = modelDefaults.length > 0
        ? modelDefaults.filter((d) => !d.startsWith(`${ownerLinkField}:`) && !d.startsWith(`${linkField}:`)).join(', ')
        : '';

      return `    // Step ${ctx.stepNum}: Auto-create ${m[1]} ${m[2]} for all ${m[2]}
    {
      const _allItems = await findAll('${sourceTable}');
      const _ownerId = ${ownerVar ? `(${ownerVar} as any)?.id` : 'null'};
      const _records = _allItems.map((_item: any) => ({
        ${defaultsBlock ? defaultsBlock + ',' : ''}
        ${ownerLinkField}: _ownerId,
        ${linkField}: (_item as any).${sourceSingular}Id ?? (_item as any).id,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      }));
      if (_records.length > 0) {
        await insertMany('${targetTable}', _records);
      }
    }`;
    },
  },

  // --- "Otherwise create new X record" ---
  {
    name: 'otherwise-create',
    pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
    generateCall: (m, ctx) => {
      const Model = pascal(m[1]!);
      const modelVar = safeEntityName(Model);
      if (!modelVar) return '';
      const table = toTable(Model);
      const wasDeclared = ctx.declaredVars?.has(modelVar);
      const declared = Array.from(ctx.declaredVars || []);
      const defaults = deriveModelDefaults(Model, ctx);
      const supplied = new Set<string>();
      for (const entry of defaults) {
        const colonIdx = entry.indexOf(':');
        if (colonIdx > 0) supplied.add(entry.slice(0, colonIdx).trim());
      }
      const fkAssignments = declared
        .filter((v) => v !== modelVar && !/^step\d+Result$/.test(v) && v !== 'args')
        .filter((v) => !supplied.has(v + 'Id'))
        .map((v) => {
          supplied.add(v + 'Id');
          return `${v}Id: (${v} as any)?.id`;
        })
        .join(', ');
      const defaultsBlock = defaults.length > 0 ? defaults.join(', ') + ',' : '';
      ctx.declaredVars?.add(modelVar);
      return `    // Step ${ctx.stepNum}: Otherwise create new ${Model} record
    else {
      const _newRecord = { ${defaultsBlock} ${fkAssignments ? fkAssignments + ',' : ''} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
      ${wasDeclared ? `${modelVar} = await insertOne('${table}', _newRecord) as any;` : `const ${modelVar} = await insertOne('${table}', _newRecord) as any;
      void ${modelVar};`}
    }`;
    },
  },

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

  // --- 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 '';
      const args = (ctx.parameterNames || []).join(', ');
      return `    // Step ${ctx.stepNum}: Call ${service}.${method}
    await (${serviceVar} as any).${method}({ ${args} });`;
    },
  },

  // --- Return ---
  {
    name: 'return-model',
    pattern: /^return\s+(?:the\s+|updated\s+|created\s+)?(\w+)\s*$/i,
    generateCall: (m, ctx) => {
      const valueRaw = m[1]!.trim();
      const declared = ctx.declaredVars || new Set();
      const params = ctx.parameterNames || [];
      const modelVar = toVar(ctx.modelName);
      if (valueRaw.toLowerCase() === ctx.modelName.toLowerCase() || valueRaw === modelVar) {
        return `    // Step ${ctx.stepNum}: Return ${ctx.modelName}
    return ${modelVar};`;
      }
      if (declared.has(valueRaw)) {
        return `    // Step ${ctx.stepNum}: Return ${valueRaw}
    return ${valueRaw};`;
      }
      if (params.includes(valueRaw)) {
        return `    // Step ${ctx.stepNum}: Return ${valueRaw}
    return args.${valueRaw};`;
      }
      return `    // Step ${ctx.stepNum}: Return ${valueRaw} — TODO: resolve binding
    return null;`;
    },
  },
];

/**
 * Match a step against the postgres-native conventions. Same shape as
 * `matchStep` (prisma) and `matchMongoStep` — the orm-agnostic
 * AI-behaviors-generator can drive any of the three.
 */
export function matchPgStep(
  step: string,
  ctx: PgStepContext,
): {
  matched: boolean;
  call: string;
  helperMethod?: string;
  functionName?: string;
  inputs?: string[];
  resultVar?: string;
} {
  // Same `args.X` plumbing as mongo-native — the controller wraps the
  // action body with a single `args` parameter rather than destructuring.
  const aiArgsExpr = (inputs: string[], paramNames: string[]) =>
    inputs.length > 0
      ? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(', ')} }`
      : '{}';
  return matchAgainstConventions(step, ctx, PG_STEP_CONVENTIONS, aiArgsExpr);
}
