import { safeFunctionName } from "@specverse/types";
import { matchStep } from "./step-conventions.js";
import { validateImportWhitelist } from "@specverse/engines/ai";
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;
  }
}
function validateImports(code) {
  const offenders = validateImportWhitelist(code);
  if (offenders.length === 0) return null;
  return `import not in whitelist: ${offenders.join(", ")} (allowed: jsonwebtoken | bcryptjs | uuid | crypto | expr-eval)`;
}
async function validateTypeScriptTypes(code) {
  let ts;
  try {
    ts = await import("typescript");
    if (ts.default) ts = ts.default;
  } catch {
    return null;
  }
  try {
    const fileName = "aiBehavior.ts";
    const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ES2022, true);
    const compilerOptions = {
      target: ts.ScriptTarget.ES2022,
      module: ts.ModuleKind.ESNext,
      moduleResolution: ts.ModuleResolutionKind.NodeNext,
      strict: true,
      noImplicitAny: false,
      // body uses `any` extensively for inputs
      noUnusedLocals: true,
      noUnusedParameters: true,
      noImplicitReturns: true,
      noUncheckedIndexedAccess: true,
      noFallthroughCasesInSwitch: true,
      noEmit: true,
      skipLibCheck: true,
      types: [],
      // Without `lib`, tsc can't resolve Promise / Array / RegExp etc., and
      // every body fails the basic-types check before it even gets to the
      // strict-null-check rules we actually want to validate.
      lib: ["lib.es2022.d.ts"]
    };
    const defaultHost = ts.createCompilerHost(compilerOptions);
    const host = {
      ...defaultHost,
      getSourceFile: (n, target) => {
        if (n === fileName) return sourceFile;
        return defaultHost.getSourceFile(n, target);
      },
      writeFile: () => {
      },
      fileExists: (n) => n === fileName || defaultHost.fileExists(n),
      readFile: (n) => n === fileName ? code : defaultHost.readFile(n)
    };
    const program = ts.createProgram([fileName], compilerOptions, host);
    const diagnostics = [
      ...program.getSyntacticDiagnostics(sourceFile),
      ...program.getSemanticDiagnostics(sourceFile)
    ];
    if (diagnostics.length === 0) return null;
    const formatted = diagnostics.slice(0, 5).map((d) => {
      const msg = ts.flattenDiagnosticMessageText(d.messageText, "\n");
      const pos = d.file && d.start !== void 0 ? d.file.getLineAndCharacterOfPosition(d.start) : null;
      return pos ? `line ${pos.line + 1}, col ${pos.character + 1}: ${msg}` : msg;
    });
    return formatted.join("; ");
  } catch {
    return null;
  }
}
const PROMPT_VERSION = "9.8.1";
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, matcher) {
  const { controller, model } = context;
  if (!controller?.actions) return "";
  const stepMatcher = matcher || matchStep;
  const modelName = model?.name || controller.model || "Model";
  const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
  const availableModels = (context.models || []).map((m) => m?.name).filter(Boolean);
  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 = stepMatcher(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, 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 { name: safeName, reserved: nameReserved } = safeFunctionName(functionName);
    const stripLiteralsAndComments = (src) => {
      let out = src.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length)).replace(/(['"])(?:\\.|(?!\1).)*\1/g, (m) => m[0] + " ".repeat(m.length - 2) + m[0]);
      out = out.replace(/`((?:\\.|\$\{[^}]*\}|(?!`).)*)`/g, (full, content) => {
        let result = "`";
        let i = 0;
        while (i < content.length) {
          if (content[i] === "\\" && i + 1 < content.length) {
            result += "  ";
            i += 2;
            continue;
          }
          if (content[i] === "$" && content[i + 1] === "{") {
            const end = content.indexOf("}", i);
            const slice = end >= 0 ? content.slice(i, end + 1) : content.slice(i);
            result += slice;
            i += slice.length;
            continue;
          }
          result += " ";
          i++;
        }
        return result + "`";
      });
      return out;
    };
    const bodyHandlesInputItself = (body2) => {
      const codeOnly = stripLiteralsAndComments(body2);
      if (/(?:const|let|var)\s*\{[^}]+\}\s*=\s*input\b/.test(codeOnly)) return true;
      if (/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*input\.[A-Za-z_$][\w$]*/.test(codeOnly)) return true;
      return false;
    };
    const isRealReference = (n, codeOnly) => {
      const escaped = n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      const re = new RegExp(`(?<![A-Za-z0-9_$.])${escaped}(?![A-Za-z0-9_$:])`, "g");
      return re.test(codeOnly);
    };
    const buildSignatureAndDestructure = (body2) => {
      if (inputs.length === 0) {
        const codeOnly2 = stripLiteralsAndComments(body2);
        const usesInputObject2 = /(?<![A-Za-z0-9_$])input(?![A-Za-z0-9_$])/.test(codeOnly2);
        return { signature: `${usesInputObject2 ? "input" : "_input"}: Record<string, never>`, destructure: "" };
      }
      const codeOnly = stripLiteralsAndComments(body2);
      const referenced = inputs.filter((n) => isRealReference(n, codeOnly));
      const usesInputObject = /(?<![A-Za-z0-9_$])input(?![A-Za-z0-9_$])/.test(codeOnly);
      const sigName = referenced.length > 0 || usesInputObject ? "input" : "_input";
      const sig = `${sigName}: { ${inputs.map((n) => `${n}: any`).join("; ")} }`;
      const bodyAccessesInputProps = /(?<![A-Za-z0-9_$])input\.[A-Za-z_$]/.test(codeOnly);
      const destructure = referenced.length > 0 && !bodyHandlesInputItself(body2) && !bodyAccessesInputProps ? `  const { ${referenced.join(", ")} } = input;` : "";
      return { signature: sig, destructure };
    };
    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 buildTestCode = (rawBody) => {
      const { signature, destructure } = buildSignatureAndDestructure(rawBody);
      const destructureLine = destructure ? destructure + "\n" : "";
      return `export async function ${safeName}(${signature}): Promise<${returnType}> {
${destructureLine}${rawBody}
}`;
    };
    const key = cacheKey(step, modelName, operationName, functionName, inputs);
    let body = cacheRead(key);
    let source = body ? "AI-CACHED" : "STUB";
    if (body) {
      const testCode = buildTestCode(body);
      const syntaxError = await validateTypeScript(testCode);
      const typeError = syntaxError ? null : await validateTypeScriptTypes(testCode);
      const importError = syntaxError || typeError ? null : validateImports(testCode);
      if (syntaxError || typeError || importError) {
        console.warn(`      [ai-validate] cached ${functionName} failed validation: ${syntaxError || typeError || importError}`);
        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,
          spec,
          returnType
          // Pass declared return type to Claude
        });
        if (body) {
          const testCode = buildTestCode(body);
          const syntaxError = await validateTypeScript(testCode);
          if (syntaxError) {
            console.warn(`      [ai-validate] ${functionName} has syntax error: ${syntaxError}`);
            body = `// AI-generated code failed validation: ${syntaxError}
  // Step: ${step}
  throw new Error('AI behavior has invalid syntax \u2014 see comment above');`;
            source = "AI-INVALID";
          } else {
            const typeError = await validateTypeScriptTypes(testCode);
            const importError = typeError ? null : validateImports(testCode);
            if (typeError || importError) {
              console.warn(`      [ai-validate] ${functionName} ${typeError ? "type errors: " + typeError : "whitelist violation: " + importError}`);
              try {
                const errorParts = [];
                if (typeError) errorParts.push(`TypeScript type errors:
${typeError}`);
                if (importError) errorParts.push(`Import-whitelist violation: ${importError}.
Only these libraries may be dynamic-imported: jsonwebtoken, bcryptjs, uuid, crypto, expr-eval. Anything else is forbidden \u2014 throw an Error if the step needs an unsupported library.`);
                const retryHint = `Your previous output had problems:

${errorParts.join("\n\n")}

Fix these specifically \u2014 common type-error causes:
- RegExp match indices are 'string | undefined'; use non-null assertion or extract to a typed variable
- Strict null checks: guard or assert before use
- Don't declare locals you never reference

IMPORTANT: The destructure line \`const { ... } = input;\` is added by the wrapper, NOT by you. Output ONLY the function body that goes AFTER that line \u2014 do not repeat the destructure or you will produce duplicate-declaration errors.`;
                const retried = await aiService.generateBehavior({
                  step: `${step}

${retryHint}`,
                  modelName,
                  operationName,
                  functionName,
                  parameterNames: inputs,
                  availableModels,
                  spec,
                  returnType
                });
                if (retried) {
                  const retryCode = buildTestCode(retried);
                  const retrySyntaxError = await validateTypeScript(retryCode);
                  const retryTypeError = retrySyntaxError ? null : await validateTypeScriptTypes(retryCode);
                  const retryImportError = retrySyntaxError || retryTypeError ? null : validateImports(retryCode);
                  if (!retrySyntaxError && !retryTypeError && !retryImportError) {
                    body = retried;
                    source = "AI-GENERATED";
                    cacheWrite(key, body);
                  } else {
                    source = "AI-INVALID";
                    cacheWrite(key, body);
                  }
                } else {
                  source = "AI-INVALID";
                  cacheWrite(key, body);
                }
              } catch {
                source = "AI-INVALID";
                cacheWrite(key, body);
              }
            } 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}
` : "";
    const reExportLine = nameReserved ? `
export { ${safeName} as ${functionName} };` : "";
    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."}
 */
${nameReserved ? "async" : "export async"} function ${safeName}(${(() => {
      const { signature: sig } = buildSignatureAndDestructure(body);
      return sig;
    })()}): Promise<${returnType}> {
${(() => {
      const { destructure } = buildSignatureAndDestructure(body);
      return destructure ? destructure + "\n" : "";
    })()}${body}
}${reExportLine}`);
  }
  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.
 *
 * PURE-FUNCTION CONTRACT \u2014 these bodies must NOT touch the database, the
 * event bus, or any external service. Persistence and side effects happen
 * in the calling controller; this file does pure transformations only.
 *
 * 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 by the realize engine (no
 * AI needed) \u2014 these are emitted inline in the controller, not here:
 *   "Find {Model} by {field}" \u2192 ORM-specific find call
 *   "Create {Model}"          \u2192 ORM-specific create call
 *   "Update {Model} {field} to {value}"
 *   "Delete {Model}"
 *   "Transition {Model} to {state}"
 *   "Count {Model}s per {Group}"
 *   See the ORM's step-conventions module for the full list.
 *
 * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
 */

${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
};
