import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
import { generateBehaviorWithHelpers } from "./behavior-generator.js";
function generatePrismaController(context) {
  const { controller, model, spec, models: allModels } = context;
  if (!controller) {
    throw new Error("Controller is required in template context");
  }
  if (!model) {
    throw new Error("Model is required for controller generation");
  }
  const controllerName = controller.name;
  const modelName = model.name;
  const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
  const RESERVED_WORDS = /* @__PURE__ */ new Set(["import", "export", "default", "class", "function", "return", "delete", "new", "this", "switch", "case", "break", "continue", "for", "while", "do", "if", "else", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "let", "const", "var", "void", "with", "yield", "async", "await", "enum", "implements", "interface", "package", "private", "protected", "public", "static", "super", "extends"]);
  const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
  const prismaDelegate = RESERVED_WORDS.has(rawModelVar) ? `prisma['${rawModelVar}']` : `prisma.${rawModelVar}`;
  const curedOps = controller.cured || {};
  const idAttr = (Array.isArray(model.attributes) ? model.attributes : Object.values(model.attributes || {})).find((a) => a.name === "id");
  const idType = idAttr?.type || "UUID";
  const needsIntParse = idType === "Integer" || idType === "Int" || idType === "Number";
  const customActions = generateCustomActions(controller, modelName, modelVar);
  const hasConstraints = Array.isArray(model.constraints) && model.constraints.length > 0;
  const classBody = [
    generateValidateMethod(model, modelName, hasConstraints),
    curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints) : "",
    curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : "",
    curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints) : "",
    curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints) : "",
    curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints) : "",
    customActions.code
  ].filter(Boolean).join("\n  ");
  const usesPrisma = /\bprisma\b/.test(classBody);
  const usesParseId = /\bparseId\(/.test(classBody);
  const usesEventBus = /\beventBus\./.test(classBody);
  const usesAiBehaviors = customActions.needsAiBehaviors;
  const imports = [
    usesPrisma ? `import { PrismaClient } from '@prisma/client';` : "",
    usesEventBus ? `import { eventBus } from '../events/eventBus.js';` : "",
    usesAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : "",
    hasConstraints ? `import { runGuards as runConstraintGuards } from './${modelName}.guards.js';` : ""
  ].filter(Boolean).join("\n");
  const declarations = [
    usesPrisma ? `const prisma = new PrismaClient();` : "",
    usesParseId ? `/** Parse ID from string to the correct type for this model */
function parseId(id: string): ${needsIntParse ? "number" : "string"} {
  ${needsIntParse ? "return parseInt(id, 10);" : "return id;"}
}` : ""
  ].filter(Boolean).join("\n\n");
  return `/**
 * ${controllerName}
 * Model-specific business logic for ${modelName}
 * ${controller.description || ""}
 */
${imports ? "\n" + imports + "\n" : ""}
${declarations ? declarations + "\n\n" : ""}/**
 * ${controllerName} class
 */
export class ${controllerName} {
  ${classBody}
}

// Export singleton instance
export const ${modelVar}Controller = new ${controllerName}();
export default ${modelVar}Controller;
`;
}
function generateValidateMethod(model, modelName, hasConstraints) {
  const opTypeUnion = hasConstraints ? `'create' | 'update' | 'evolve' | 'delete' | \`evolve.\${string}\`` : `'create' | 'update' | 'evolve'`;
  const constraintsCheck = hasConstraints ? `
    // Phase 2 \u2014 run model.constraints[] guards matching this operation.
    // ctx carries a per-model query helper so subquery sugars (rewritten
    // to async guards by guards-generator) can run their findFirst calls.
    const __guardCtx = {
      query: (modelName: string) => {
        const delegate = (prisma as any)[modelName.charAt(0).toLowerCase() + modelName.slice(1)];
        if (!delegate) return undefined;
        return {
          exists: async (predicate: (e: any) => boolean) => {
            const all = await delegate.findMany();
            return all.some(predicate);
          },
        };
      },
    };
    const constraintViolations = await runConstraintGuards(_data, _context.operation, _actor, __guardCtx);
    for (const v of constraintViolations) errors.push(v.message);` : "";
  return `
  /**
   * Validate ${modelName} data
   * Unified validation method for all operations
   */
  public async validate(
    _data: any,
    _context: { operation: ${opTypeUnion} },
    _actor: any = null
  ): Promise<{ valid: boolean; errors: string[] }> {
    const errors: string[] = [];

    ${generateValidationLogic(model, "_data", "_context")}${constraintsCheck}

    return {
      valid: errors.length === 0,
      errors
    };
  }
`;
}
function generateValidationLogic(model, dataParam = "_data", contextParam = "_context") {
  if (!model.attributes) return "// No validation rules defined";
  const validations = [];
  const attrList = Array.isArray(model.attributes) ? model.attributes.map((a) => [a.name, a]) : Object.entries(model.attributes);
  attrList.forEach(([name, attr]) => {
    if (attr.required && !isAutoField(name, attr)) {
      validations.push(`
    // ${name} is required
    if (${contextParam}.operation === 'create' && !${dataParam}.${name}) {
      errors.push('${name} is required');
    }`);
    }
    if (attr.type === "String" || attr.type === "string") {
      if (attr.min) {
        validations.push(`
    if (${dataParam}.${name} && ${dataParam}.${name}.length < ${attr.min}) {
      errors.push('${name} must be at least ${attr.min} characters');
    }`);
      }
      if (attr.max) {
        validations.push(`
    if (${dataParam}.${name} && ${dataParam}.${name}.length > ${attr.max}) {
      errors.push('${name} must be at most ${attr.max} characters');
    }`);
      }
    }
    if (attr.values && Array.isArray(attr.values)) {
      const values = attr.values.map((v) => `'${v}'`).join(", ");
      validations.push(`
    if (${dataParam}.${name} && ![${values}].includes(${dataParam}.${name})) {
      errors.push('${name} must be one of: ${attr.values.join(", ")}');
    }`);
    }
    if (attr.format === "email") {
      validations.push(`
    if (${dataParam}.${name} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${dataParam}.${name})) {
      errors.push('${name} must be a valid email address');
    }`);
    }
  });
  return validations.join("\n") || "// No validation rules defined";
}
function generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints = false) {
  const belongsToLoad = hasConstraints ? generateBelongsToLoad(model, allModels) : "";
  const validateSelf = belongsToLoad ? "__mergedSelf" : "data";
  return `
  /**
   * Create a new ${modelName}
   */
  public async create(data: any, _actor: any = null): Promise<any> {
${belongsToLoad ? `
    // Phase 2 Slice 15 \u2014 Create-time relation loading. For each belongsTo
    // FK present in input, load the related entity and merge into self
    // so constraints traversing self.<rel>.<field> see the full related
    // record (not just the FK id). Mirrors Slice 8's Update pattern but
    // for the create path. Only fires when the model has declared
    // constraints; one extra round-trip per FK in input.
    const __loadedRels: Record<string, any> = {};
    ${belongsToLoad}
    const __mergedSelf = { ...data, ...__loadedRels };
` : ""}
    // Validate input
    const validationResult = await this.validate(${validateSelf}, { operation: 'create' }, _actor);
    if (!validationResult.valid) {
      throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
    }

    // Transform FK fields to Prisma connect format
    const prismaData = { ...data };
    ${generateFKTransform(model, "prismaData", allModels)}

    // Create record
    const ${modelVar} = await ${prismaDelegate}.create({
      data: prismaData${generateIncludeRelationships(model)}
    });

    // Publish CURED event
    await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);

    return ${modelVar};
  }
`;
}
function generateBelongsToLoad(model, allModels) {
  const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
  const belongsToRels = rels.filter((r) => r.type === "belongsTo");
  if (belongsToRels.length === 0) return "";
  const RESERVED_WORDS = /* @__PURE__ */ new Set(["import", "export", "default", "class", "function", "return", "delete", "new", "this", "switch", "case", "break", "continue", "for", "while", "do", "if", "else", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "let", "const", "var", "void", "with", "yield", "async", "await", "enum", "implements", "interface", "package", "private", "protected", "public", "static", "super", "extends"]);
  return belongsToRels.map((rel) => {
    const relName = rel.name;
    const targetName = rel.target;
    const fkField = `${relName}Id`;
    const targetVar = targetName.charAt(0).toLowerCase() + targetName.slice(1);
    const targetDelegate = RESERVED_WORDS.has(targetVar) ? `prisma['${targetVar}']` : `prisma.${targetVar}`;
    let idExpr = `data.${fkField}`;
    if (allModels) {
      const targetModel = allModels.find((m) => m.name === targetName);
      if (targetModel) {
        const idAttr = (Array.isArray(targetModel.attributes) ? targetModel.attributes : Object.values(targetModel.attributes || {})).find((a) => a.name === "id");
        const idType = idAttr?.type || "String";
        if (idType === "Integer" || idType === "Int" || idType === "Number") {
          idExpr = `parseInt(data.${fkField}, 10)`;
        }
      }
    }
    return `if (data.${fkField}) {
      __loadedRels.${relName} = await ${targetDelegate}.findUnique({ where: { id: ${idExpr} } });
    }`;
  }).join("\n    ");
}
function generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) {
  return `
  /**
   * Retrieve ${modelName} by ID. Returns null when not found so the
   * route handler can translate that into HTTP 404 instead of 500.
   */
  public async retrieve(id: string): Promise<any | null> {
    return await ${prismaDelegate}.findUnique({
      where: { id: parseId(id) }${generateIncludeRelationships(model)}
    });
  }

  /**
   * Retrieve all ${modelName}s
   */
  public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
    return await ${prismaDelegate}.findMany({
      skip: options.skip,
      take: options.take${generateIncludeRelationships(model)}
    });
  }
`;
}
function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints = false) {
  return `
  /**
   * Update ${modelName}
   */
  public async update(id: string, data: any, _actor: any = null): Promise<any> {
${hasConstraints ? `
    // Phase 2 \u2014 Update self-from-DB: load the entity first and validate
    // against the MERGED \`loaded + input\` shape so update-time constraints
    // like \`self.poll.votingStatus == "open"\` see the full record even
    // when the caller only sent a partial payload. Costs one extra DB
    // round-trip; only fires for models with declared constraints.
    const __existing = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) }${generateIncludeRelationships(model)} });
    if (!__existing) throw new Error('${modelName} not found');
    const __merged = { ...__existing, ...data };
    const validationResult = await this.validate(__merged, { operation: 'update' }, _actor);
` : `
    // Validate input
    const validationResult = await this.validate(data, { operation: 'update' }, _actor);
`}
    if (!validationResult.valid) {
      throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
    }

    // Strip nested relations and id \u2014 only send scalar fields to Prisma
    const updateData: any = {};
    for (const [key, value] of Object.entries(data)) {
      if (key === 'id') continue;
      if (Array.isArray(value)) continue;
      if (value !== null && typeof value === 'object' && !(value instanceof Date)) continue;
      updateData[key] = value;
    }

    // Transform FK fields to Prisma connect format
    ${generateFKTransform(model, "updateData", allModels)}

    // Update record
    const ${modelVar} = await ${prismaDelegate}.update({
      where: { id: parseId(id) },
      data: updateData${generateIncludeRelationships(model)}
    });

    // Publish CURED event
    await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);

    return ${modelVar};
  }
`;
}
function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints = false) {
  const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
  const lifecycle = lifecycles[0];
  const lifecycleName = lifecycle?.name || "status";
  const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
  const states = lifecycle?.states && lifecycle.states.length > 0 ? lifecycle.states.map((s) => typeof s === "string" ? s : s.name) : Array.from(/* @__PURE__ */ new Set([
    ...Object.keys(validTransitions),
    ...Object.values(validTransitions).flat()
  ]));
  const evolveOpsMap = {};
  if (lifecycle?.transitions) {
    for (const [actionName, t] of Object.entries(lifecycle.transitions)) {
      const from = t.from;
      const to = t.to;
      if (typeof from === "string" && typeof to === "string") {
        evolveOpsMap[from] = evolveOpsMap[from] ?? {};
        evolveOpsMap[from][to] = actionName;
      }
    }
  }
  return `
  /**
   * Evolve ${modelName} through lifecycle "${lifecycleName}"
   * States: ${states.join(" \u2192 ")}
   *
   * Accepts either the client shape { toState, lifecycleName? } or
   * the direct column shape { ${lifecycleName}: ... }. The frontend
   * runtime's useTransitionStateMutation always sends the former; the
   * realized smoke-parity script can send either.
   */
  public async evolve(id: string, data: any, _actor: any = null): Promise<any> {
    // Get current record to check lifecycle state
    const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
    if (!current) {
      throw new Error('${modelName} not found');
    }

    // Normalize input: translate client { toState: X } shape to the
    // lifecycle's actual column name. If the caller already sent the
    // column directly, keep whatever they sent.
    const targetLifecycle = data?.lifecycleName || '${lifecycleName}';
    const targetState = data?.toState ?? data?.state ?? data?.[targetLifecycle];
    if (!targetState) {
      throw new Error('evolve requires toState (or ${lifecycleName}) in the request body');
    }

    ${states.length > 0 ? `
    // Validate lifecycle transition against declared flow
    const currentState = (current as any)[targetLifecycle];
    const validTransitions: Record<string, string[]> = ${JSON.stringify(validTransitions)};
    const allowed = validTransitions[currentState] || [];
    if (!allowed.includes(targetState)) {
      throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
    }
    ` : ""}
${hasConstraints ? `
    // Phase 2 \u2014 resolve the transition's action name so constraints scoped
    // to \`on: 'evolve.<action>'\` match correctly. EVOLVE_OPS is baked at
    // codegen from the lifecycle definition.
    const EVOLVE_OPS: Record<string, Record<string, string>> = ${JSON.stringify(evolveOpsMap)};
    const currentStateForOp = (current as any)[targetLifecycle];
    const actionName = EVOLVE_OPS[currentStateForOp]?.[targetState] ?? targetState;
    const evolveValidation = await this.validate(
      { ...data, ...current, [targetLifecycle]: targetState },
      { operation: \`evolve.\${actionName}\` as any },
      _actor
    );
    if (!evolveValidation.valid) {
      throw new Error(\`Validation failed: \${evolveValidation.errors.join(', ')}\`);
    }
` : ""}
    // Build the Prisma update payload \u2014 only the lifecycle column
    // changes. Strips toState/lifecycleName/state so Prisma doesn't
    // reject unknown fields.
    const updateData: any = { [targetLifecycle]: targetState };

    // Update record
    const ${modelVar} = await ${prismaDelegate}.update({
      where: { id: parseId(id) },
      data: updateData${generateIncludeRelationships(model)}
    });

    // Publish CURED event
    await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);

    return ${modelVar};
  }
`;
}
function generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints = false) {
  return `
  /**
   * Delete ${modelName}
   */
  public async delete(id: string, _actor: any = null): Promise<void> {
    // Get record before deletion for event
    const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
${hasConstraints ? `
    // Phase 2 \u2014 run delete-scoped constraint guards against the loaded
    // record. \`self\` is the entity being deleted (loaded above), giving
    // delete-time constraints access to the entity's current field values.
    if (${modelVar}) {
      const deleteValidation = await this.validate(${modelVar}, { operation: 'delete' }, _actor);
      if (!deleteValidation.valid) {
        throw new Error(\`Validation failed: \${deleteValidation.errors.join(', ')}\`);
      }
    }
` : ""}
    await ${prismaDelegate}.delete({
      where: { id: parseId(id) }
    });

    // Publish CURED event
    if (${modelVar}) {
      await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
    }
  }
`;
}
function generateCustomActions(controller, modelName, modelVar) {
  if (!controller.actions || Object.keys(controller.actions).length === 0) {
    return { code: "", unmatchedSteps: [], needsAiBehaviors: false };
  }
  const actions = [];
  const allUnmatchedSteps = [];
  Object.entries(controller.actions).forEach(([actionName, action]) => {
    const params = action.parameters || {};
    const paramNames = Object.keys(params);
    const hasSteps = Array.isArray(action.steps) && action.steps.length > 0;
    let body;
    let needsAi = false;
    if (hasSteps) {
      const inputObj = paramNames.length > 0 ? `{ ${paramNames.join(", ")} }` : "{}";
      body = `    const input = ${inputObj};
    return await aiBehaviors.${actionName}(input);`;
      needsAi = true;
    } else {
      const behavior = {
        preconditions: action.requires || action.preconditions || [],
        steps: [],
        postconditions: action.ensures || action.postconditions || [],
        sideEffects: action.publishes || action.events || [],
        transactional: action.transactional
      };
      const ctx = {
        modelName,
        serviceName: `${modelName}Controller`,
        operationName: actionName,
        prismaModel: modelVar,
        parameterNames: paramNames
      };
      const result = generateBehaviorWithHelpers(behavior, {}, ctx);
      allUnmatchedSteps.push(...result.unmatchedSteps);
      body = result.body;
      needsAi = result.unmatchedSteps.length > 0;
      if (result.helperMethods.length > 0) {
        actions.push(...result.helperMethods);
      }
    }
    actions.push(`
  /**
   * ${actionName}
   * ${action.description || ""}
   */
  public async ${actionName}(${generateActionParams(action)}): Promise<any> {
${body}
  }`);
    if (needsAi) allUnmatchedSteps.push({
      step: "<delegate-to-ai-ts>",
      functionName: actionName,
      operationName: actionName
    });
  });
  return {
    code: actions.join("\n"),
    unmatchedSteps: allUnmatchedSteps,
    needsAiBehaviors: allUnmatchedSteps.length > 0
  };
}
function generateActionParams(action) {
  if (!action.parameters || Object.keys(action.parameters).length === 0) {
    return "";
  }
  const params = Object.entries(action.parameters).map(([name, param]) => {
    const optional = !param.required;
    return `${name}${optional ? "?" : ""}: ${mapTypeToTypeScript(param.type)}`;
  });
  return params.join(", ");
}
function mapTypeToTypeScript(type) {
  const typeMap = {
    String: "string",
    Integer: "number",
    Float: "number",
    Boolean: "boolean",
    Date: "Date",
    DateTime: "Date",
    UUID: "string",
    Json: "any",
    // #43C: SpecVerse canonical casing → Prisma's Json
    JSON: "any",
    //       legacy/alt casing
    Jsonb: "any",
    //       Postgres-native variant
    JSONB: "any"
  };
  return typeMap[type] || "any";
}
function generateFKTransform(model, varName = "prismaData", allModels) {
  const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
  const belongsToRels = rels.filter((r) => r.type === "belongsTo");
  if (belongsToRels.length === 0) return "";
  return belongsToRels.map((rel) => {
    const relName = rel.name;
    const fkField = `${relName}Id`;
    let parseExpr = `${varName}.${fkField}`;
    if (allModels) {
      const targetModel = allModels.find((m) => m.name === rel.target);
      if (targetModel) {
        const idAttr = (Array.isArray(targetModel.attributes) ? targetModel.attributes : Object.values(targetModel.attributes || {})).find((a) => a.name === "id");
        const idType = idAttr?.type || "String";
        if (idType === "Integer" || idType === "Int" || idType === "Number") {
          parseExpr = `parseInt(${varName}.${fkField}, 10)`;
        }
      }
    }
    return `if (${varName}.${fkField}) {
      ${varName}.${relName} = { connect: { id: ${parseExpr} } };
      delete ${varName}.${fkField};
    }`;
  }).join("\n    ");
}
function generateIncludeRelationships(model) {
  if (!model.relationships) {
    return "";
  }
  let relationshipKeys;
  if (Array.isArray(model.relationships)) {
    relationshipKeys = model.relationships.map((rel) => {
      if (typeof rel === "string") {
        return rel;
      } else if (typeof rel === "object" && rel.name) {
        return rel.name;
      } else {
        return null;
      }
    }).filter((rel) => rel !== null);
  } else if (typeof model.relationships === "object") {
    relationshipKeys = Object.keys(model.relationships);
  } else {
    return "";
  }
  if (relationshipKeys.length === 0) {
    return "";
  }
  const includes = relationshipKeys.map((rel) => `        ${rel}: true`).join(",\n");
  return `,
      include: {
${includes}
      }`;
}
function hasEventPublishing(curedOps, controller) {
  return true;
}
export {
  generatePrismaController as default
};
