import { matchStep } from "./step-conventions.js";
import { createHash } from "crypto";
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
import { dirname, join } from "path";
async function validateTypeScript(code) {
  try {
    const esbuild = await import("esbuild");
    await esbuild.transform(code, {
      loader: "ts",
      format: "esm",
      target: "es2022"
    });
    return null;
  } catch (err) {
    const msg = err?.errors?.[0]?.text || err?.message || "unknown syntax error";
    return msg;
  }
}
const PROMPT_VERSION = "9.1.0";
function cacheKey(step, modelName, operationName, functionName, inputs) {
  const payload = JSON.stringify({ step, modelName, operationName, functionName, inputs: [...inputs].sort(), v: PROMPT_VERSION });
  return createHash("sha256").update(payload).digest("hex").slice(0, 16);
}
function cacheDir() {
  const cwd = process.env.SPECVERSE_USER_CWD || process.cwd();
  return join(cwd, ".specverse", "ai-cache");
}
function cacheRead(key) {
  const path = join(cacheDir(), `${key}.ts`);
  if (!existsSync(path)) return null;
  try {
    return readFileSync(path, "utf8");
  } catch {
    return null;
  }
}
function cacheWrite(key, body) {
  const dir = cacheDir();
  try {
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
    writeFileSync(join(dir, `${key}.ts`), body, "utf8");
  } catch (err) {
    console.warn(`      [ai-cache] write failed: ${err?.message || err}`);
  }
}
async function generateAiBehaviors(context) {
  const { controller, model } = context;
  if (!controller?.actions) return "";
  const modelName = model?.name || controller.model || "Model";
  const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
  const unmatchedFunctions = [];
  for (const [actionName, action] of Object.entries(controller.actions)) {
    const steps = action.steps || [];
    const parameterNames = Object.keys(action.parameters || {});
    const preconditions = action.requires || action.preconditions || [];
    const declaredVars = /* @__PURE__ */ new Set();
    for (const pc of preconditions) {
      const match = pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i);
      if (match) {
        const entity = match[1];
        declaredVars.add(entity.charAt(0).toLowerCase() + entity.slice(1));
      }
    }
    for (let i = 0; i < steps.length; i++) {
      const stepInput = steps[i];
      const stepText = typeof stepInput === "string" ? stepInput : stepInput?.step;
      const stepAs = typeof stepInput === "object" ? stepInput?.as : void 0;
      const stepReturns = typeof stepInput === "object" ? stepInput?.returns : void 0;
      if (typeof stepText !== "string") continue;
      const ctx = {
        modelName,
        prismaModel: modelVar,
        serviceName: `${modelName}Controller`,
        operationName: actionName,
        stepNum: i + 1,
        parameterNames,
        declaredVars,
        resultName: stepAs
      };
      const result = matchStep(stepText, ctx);
      if (!result.matched && result.functionName) {
        const existing = unmatchedFunctions.find((f) => f.functionName === result.functionName);
        if (!existing) {
          unmatchedFunctions.push({
            functionName: result.functionName,
            step: stepText,
            operationName: actionName,
            parameterNames,
            inputs: result.inputs || [],
            returns: stepReturns
          });
        }
      }
    }
  }
  if (unmatchedFunctions.length === 0) return "";
  return generateAiBehaviorsFile({
    ownerName: `${modelName}Controller`,
    unmatchedFunctions: unmatchedFunctions.map((f) => ({
      ...f,
      modelName
    })),
    availableModels,
    spec: context.spec
  });
}
async function generateAiBehaviorsFile(opts) {
  const { ownerName, unmatchedFunctions, availableModels: availableModels2, spec } = opts;
  if (unmatchedFunctions.length === 0) return "";
  let aiService = null;
  try {
    const { BehaviorAIService } = await import("@specverse/engines/ai");
    aiService = new BehaviorAIService();
    if (!aiService.isAvailable) {
      aiService = null;
    } else {
      aiService.startSession(ownerName);
    }
  } catch {
    aiService = null;
  }
  const functions = [];
  let cacheHits = 0;
  let cacheMisses = 0;
  for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
    const signature = inputs.length > 0 ? `input: { ${inputs.map((n) => `${n}: any`).join("; ")} }` : "input: Record<string, never>";
    const destructure = inputs.length > 0 ? `  const { ${inputs.join(", ")} } = input;` : "";
    let returnType = "any";
    if (typeof returns === "string") {
      returnType = returns;
    } else if (returns && typeof returns === "object") {
      const fields = Object.entries(returns).map(([k, v]) => `${k}: ${v}`).join("; ");
      returnType = `{ ${fields} }`;
    }
    const key = cacheKey(step, modelName, operationName, functionName, inputs);
    let body = cacheRead(key);
    let source = body ? "AI-CACHED" : "STUB";
    if (body) {
      const testCode = `export async function ${functionName}(input: any): Promise<any> {
${body}
}`;
      const validationError = await validateTypeScript(testCode);
      if (validationError) {
        console.warn(`      [ai-validate] cached ${functionName} failed validation: ${validationError}`);
        body = null;
        source = "STUB";
      } else {
        cacheHits++;
      }
    }
    if (!body && aiService) {
      cacheMisses++;
      try {
        body = await aiService.generateBehavior({
          step,
          modelName,
          operationName,
          functionName,
          parameterNames: inputs,
          // the actual inputs to the pure function
          availableModels: availableModels2,
          spec,
          returnType
          // Pass declared return type to Claude
        });
        if (body) {
          const testCode = `export async function ${functionName}(input: any): Promise<any> {
${body}
}`;
          const validationError = await validateTypeScript(testCode);
          if (validationError) {
            console.warn(`      [ai-validate] ${functionName} has syntax error: ${validationError}`);
            body = `// AI-generated code failed validation: ${validationError}
  // Step: ${step}
  throw new Error('AI behavior has invalid syntax \u2014 see comment above');`;
            source = "AI-INVALID";
          } else {
            source = "AI-GENERATED";
            cacheWrite(key, body);
          }
        }
      } catch {
      }
    }
    if (!body) {
      body = `  throw new Error('Not implemented: ${functionName} \u2014 see behaviors/${modelName}Controller.ai.ts');`;
    } else {
      body = body.split("\n").map((line) => line ? "  " + line : line).join("\n");
    }
    const inputsDoc = inputs.length > 0 ? ` * Inputs: ${inputs.join(", ")}
` : "";
    const returnsDoc = returnType !== "any" ? ` * Returns: ${returnType}
` : "";
    functions.push(`/**
 * ${functionName}
 *
 * Spec step: "${step}"
 * Called by: ${ownerName}.${operationName}()
${inputsDoc}${returnsDoc} * Source: ${source}
 * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
 *
 * PURE FUNCTION \u2014 no database access, no event publishing, no external services.
 * All data comes in via \`input\`; all effects happen in the calling controller.
 * ${source === "AI-GENERATED" ? "AI-generated implementation. Review and test before deploying." : source === "AI-CACHED" ? "Restored from AI cache (.specverse/ai-cache/). Delete cache entry to regenerate." : source === "AI-INVALID" ? "AI returned code with syntax errors \u2014 function throws at runtime. Fix or regenerate." : "STUB \u2014 Claude CLI unavailable. Install Claude Code or implement manually."}
 */
export async function ${functionName}(${signature}): Promise<${returnType}> {
${destructure ? destructure + "\n" : ""}${body}
}`);
  }
  if (aiService?.endSession) aiService.endSession();
  if (cacheHits > 0 || cacheMisses > 0) {
    console.log(`      AI cache: ${cacheHits} hit(s), ${cacheMisses} miss(es) for ${ownerName}`);
  }
  return `/**
 * ${ownerName} \u2014 AI-Generated Behaviors
 *
 * \u26A0\uFE0F  THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
 *
 * These functions could not be generated from convention patterns.
 * They are called by ${ownerName} when executing operations.
 *
 * Options for each function:
 * - Implement manually (recommended for business-critical logic)
 * - Use AI generation: specverse ai generate <function>
 * - Refactor the spec step to use a convention pattern
 *
 * Convention patterns that ARE auto-generated (no AI needed):
 *   "Find {Model} by {field}" \u2192 prisma.model.findUniqueOrThrow(...)
 *   "Create {Model}" \u2192 prisma.model.create(...)
 *   "Update {Model} {field} to {value}" \u2192 prisma.model.update(...)
 *   "Delete {Model}" \u2192 prisma.model.delete(...)
 *   "Transition {Model} to {state}" \u2192 prisma.model.update({ status: ... })
 *   "Count {Model}s per {Group}" \u2192 prisma.model.groupBy(...)
 *   See step-conventions.ts for the full list.
 *
 * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
 */

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

${functions.join("\n\n")}
`;
}
async function regenerateAiBehaviors(opts) {
  const { spec, ownerName, targetFunction, outputDir, allFunctions } = opts;
  const findOwner = (collection) => {
    if (!collection) return null;
    if (Array.isArray(collection)) return collection.find((c) => c?.name === ownerName) || null;
    return collection[ownerName] || null;
  };
  const controller = findOwner(spec.controllers);
  const service = findOwner(spec.services);
  if (!controller && !service) {
    throw new Error(
      `Owner '${ownerName}' not found in spec. Looked in controllers and services \u2014 check that the name matches exactly (case-sensitive) and that you're pointing at an inferred spec (try 'specs/main-inferred.specly').`
    );
  }
  const unmatched = [];
  const allModels = Object.keys(spec.models || {});
  if (controller) {
    const modelName = controller.model || controller.modelReference || ownerName.replace(/Controller$/, "");
    const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
    const actions = controller.actions || {};
    for (const [actionName, action] of Object.entries(actions)) {
      const steps = action.steps || [];
      const parameterNames = Object.keys(action.parameters || {});
      const declaredVars = /* @__PURE__ */ new Set();
      const preconditions = action.requires || action.preconditions || [];
      for (const pc of preconditions) {
        const m = typeof pc === "string" ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
        if (m) declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
      }
      for (let i = 0; i < steps.length; i++) {
        const stepInput = steps[i];
        const stepText = typeof stepInput === "string" ? stepInput : stepInput?.step;
        const stepReturns = typeof stepInput === "object" ? stepInput?.returns : void 0;
        if (typeof stepText !== "string") continue;
        const ctx = {
          modelName,
          prismaModel: modelVar,
          serviceName: ownerName,
          operationName: actionName,
          stepNum: i + 1,
          parameterNames,
          declaredVars,
          resultName: typeof stepInput === "object" ? stepInput?.as : void 0
        };
        const result = matchStep(stepText, ctx);
        if (!result.matched && result.functionName) {
          if (!unmatched.find((f) => f.functionName === result.functionName)) {
            unmatched.push({
              functionName: result.functionName,
              step: stepText,
              operationName: actionName,
              parameterNames,
              inputs: result.inputs || [],
              returns: stepReturns,
              modelName
            });
          }
        }
      }
    }
  } else if (service) {
    const modelName = service.name?.replace(/Service$/, "") || ownerName;
    const operations = service.operations || {};
    const opEntries = Array.isArray(operations) ? operations.map((op) => [op.name, op]) : Object.entries(operations);
    for (const [opName, operation] of opEntries) {
      const steps = operation.steps || operation.implementation?.steps || [];
      const parameterNames = Object.keys(operation.parameters || {});
      const declaredVars = /* @__PURE__ */ new Set();
      const preconditions = operation.requires || operation.preconditions || [];
      for (const pc of preconditions) {
        const m = typeof pc === "string" ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
        if (m) declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
      }
      for (let i = 0; i < steps.length; i++) {
        const stepInput = steps[i];
        const stepText = typeof stepInput === "string" ? stepInput : stepInput?.step;
        const stepReturns = typeof stepInput === "object" ? stepInput?.returns : void 0;
        if (typeof stepText !== "string") continue;
        const ctx = {
          modelName,
          prismaModel: modelName.charAt(0).toLowerCase() + modelName.slice(1),
          serviceName: ownerName,
          operationName: opName,
          stepNum: i + 1,
          parameterNames,
          declaredVars,
          resultName: typeof stepInput === "object" ? stepInput?.as : void 0
        };
        const result = matchStep(stepText, ctx);
        if (!result.matched && result.functionName) {
          if (!unmatched.find((f) => f.functionName === result.functionName)) {
            unmatched.push({
              functionName: result.functionName,
              step: stepText,
              operationName: opName,
              parameterNames,
              inputs: result.inputs || [],
              returns: stepReturns,
              modelName: ownerName
            });
          }
        }
      }
    }
  }
  if (unmatched.length === 0) {
    throw new Error(
      `No AI behaviors to regenerate for '${ownerName}'. All of its steps match built-in convention patterns (see step-conventions.ts). If you expected an AI-generated function, double-check the spec step text.`
    );
  }
  const targetFound = !targetFunction || unmatched.some((f) => f.functionName === targetFunction);
  if (targetFunction && !targetFound) {
    const known = unmatched.map((f) => f.functionName).join(", ");
    throw new Error(
      `Function '${targetFunction}' not found among '${ownerName}' unmatched functions. Known: ${known || "(none)"}.`
    );
  }
  let cacheCleared = 0;
  const functionsToInvalidate = allFunctions ? unmatched.map((f) => f.functionName) : targetFunction ? [targetFunction] : [];
  for (const fnName of functionsToInvalidate) {
    const fn = unmatched.find((f) => f.functionName === fnName);
    const key = cacheKey(fn.step, fn.modelName, fn.operationName, fn.functionName, fn.inputs);
    const path = join(cacheDir(), `${key}.ts`);
    if (existsSync(path)) {
      try {
        unlinkSync(path);
        cacheCleared++;
      } catch (err) {
        console.warn(`      [ai-cache] failed to delete ${path}: ${err?.message || err}`);
      }
    }
  }
  const code = await generateAiBehaviorsFile({
    ownerName,
    unmatchedFunctions: unmatched,
    availableModels: allModels,
    spec
  });
  const filePath = join(outputDir, "backend", "src", "behaviors", `${ownerName}.ai.ts`);
  const dir = dirname(filePath);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  writeFileSync(filePath, code, "utf8");
  return {
    filePath,
    targetFound,
    unmatchedCount: unmatched.length,
    cacheCleared
  };
}
export {
  generateAiBehaviors as default,
  generateAiBehaviorsFile,
  regenerateAiBehaviors
};
