/**
 * Fastify Routes Generator
 *
 * Generates Fastify route handlers from SpecVerse controllers
 */

import type { TemplateContext } from '@specverse/types';
import { inferHttpMethod as sharedInferHttpMethod, inferPath as sharedInferPath, curedToEndpoints as sharedCuredToEndpoints } from '@specverse/types/spec-rules';

/**
 * Generate Fastify routes for a controller
 */
export default function generateFastifyRoutes(context: TemplateContext): string {
  const { controller, model, spec, implType } = context;

  if (!controller) {
    throw new Error('Controller is required in template context');
  }

  const modelName = model?.name || controller.modelReference;
  const controllerName = controller.name;
  const routeName = controllerName || `${modelName}Controller`;

  // Determine if this is a model controller or abstract service
  const isModelController = !!modelName;
  const handlerName = isModelController ? `${modelName}Controller` : controllerName;

  // Generate imports — defer until after handlers are built so we
  // know whether FastifyRequest/FastifyReply are referenced.
  // (Variable populated below.)
  let imports: string;

  // Convert CURED operations to endpoints if endpoints don't exist
  let endpoints = controller.endpoints;
  if (!endpoints || endpoints.length === 0) {
    if (controller.cured) {
      endpoints = curedToEndpoints(controller.cured, modelName);
    }
  }

  // Add custom action endpoints. Each action can declare:
  //   path:   explicit HTTP path (absolute or relative)
  //   method: explicit HTTP method
  // If `path` starts with `/api/` (or any absolute non-relative form), it
  // ESCAPES the controller's prefix and gets mounted at root level via the
  // `registerExternalRoutes` exported function (the main.ts generator
  // collects these across controllers and registers them without a prefix).
  if (controller.actions) {
    if (!endpoints) endpoints = [];
    for (const [actionName, action] of Object.entries(controller.actions) as [string, any][]) {
      const params = action.parameters || {};
      const hasIdParam = Object.keys(params).some(p => p === 'id' || p === `${modelName?.charAt(0).toLowerCase()}${modelName?.slice(1)}Id`);
      const explicitPath: string | undefined = action.path;
      const explicitMethod: string | undefined = action.method;
      const isExternal = !!(explicitPath && /^\/api\//.test(explicitPath));
      endpoints.push({
        operation: actionName,
        method: explicitMethod || 'POST',
        path: explicitPath || (hasIdParam ? `/:id/${actionName}` : `/${actionName}`),
        external: isExternal,
        parameters: params,
        description: action.description || `Custom action: ${actionName}`,
      });
    }
  }

  // Generate route handlers for each endpoint
  if (!endpoints || endpoints.length === 0) {
    console.warn(`Warning: Controller ${controllerName} has no endpoints. Generating empty routes file.`);
  }

  // Split endpoints by routing scope:
  //   - internal: mounted under the controller's `/api/<plural>` prefix
  //     (the default plugin export). Both CURVED ops and relative-path
  //     custom actions live here.
  //   - external: actions that declared an absolute `path` like
  //     `/api/v2/auth/register`. Mounted at root via the separately
  //     exported `registerExternalRoutes` function so they escape the
  //     controller prefix.
  const internalEndpoints = endpoints?.filter((e: any) => !e.external) || [];
  const externalEndpoints = endpoints?.filter((e: any) => e.external) || [];

  const internalHandlers = internalEndpoints
    .map((endpoint: any) => generateRouteHandler(endpoint, modelName, handlerName, isModelController, implType, controllerName))
    .join('\n\n');
  const externalHandlers = externalEndpoints
    .map((endpoint: any) => generateRouteHandler(endpoint, modelName, handlerName, isModelController, implType, controllerName))
    .join('\n\n');

  // When there are no internal handlers, suppress unused-param TS6133 by
  // prefixing `fastify`/`options` with `_` and skip the `handler` decl
  // entirely. Same for the external-routes export.
  const hasInternalHandlers = internalEndpoints.length > 0;
  const hasExternalHandlers = externalEndpoints.length > 0;

  // Determine which fastify types the emitted body actually references.
  // FastifyInstance is always used (function signatures). FastifyRequest +
  // FastifyReply only appear inside individual route handlers.
  const handlersText = internalHandlers + '\n' + externalHandlers;
  const usesFastifyRequest = /\bFastifyRequest\b/.test(handlersText);
  const usesFastifyReply = /\bFastifyReply\b/.test(handlersText);
  imports = generateImports(
    controller, modelName, handlerName, isModelController, implType,
    { usesFastifyRequest, usesFastifyReply },
  );

  const externalRoutesExport = hasExternalHandlers ? `
/**
 * Mount the external (root-prefix) routes for this controller.
 *
 * Called by the generated main.ts at root scope (no prefix) so action.path
 * declarations like '/api/v2/auth/register' land at exactly that URL,
 * bypassing the controller's '/api/<prefix>' prefix.
 */
export async function registerExternalRoutes(
  fastify: FastifyInstance,
  options: any
) {
  const handler = ${isModelController ? 'options.controllers' : 'options.services'}.${handlerName};

${externalHandlers.split('\n').map(line => '  ' + line).join('\n')}
}
` : '';

  // Body fragments — only declare `handler` when there are handlers to
  // reference it; suppress unused-param TS6133 by underscore-prefixing
  // when the function body is empty.
  const mainBody = hasInternalHandlers
    ? `  const handler = ${isModelController ? 'options.controllers' : 'options.services'}.${handlerName};\n\n${internalHandlers.split('\n').map(line => '  ' + line).join('\n')}`
    : '  // No endpoints declared on this controller — empty routes plugin.';
  const mainFastifyParam = hasInternalHandlers ? 'fastify' : '_fastify';
  const mainOptionsParam = hasInternalHandlers ? 'options' : '_options';

  // Generate the complete route file
  return `${imports}

/**
 * ${routeName} Routes
 * Generated from SpecVerse specification
 *
 * ${isModelController ? `Model: ${modelName}` : `Service: ${controllerName}`}
 * Operations: ${controller.endpoints?.map((e: any) => e.operation).join(', ') || 'CURED'}
 */
export default async function ${routeName.replace('Controller', '')}Routes(
  ${mainFastifyParam}: FastifyInstance,
  ${mainOptionsParam}: any
) {
${mainBody}
}
${externalRoutesExport}`;
}

/**
 * Generate imports for the route file. The fastify type imports are
 * gated on actual handler usage to avoid unused-import TS6133 on
 * empty-routes plugins (e.g. backend-only ProductController without
 * any CURVED ops or custom actions).
 */
function generateImports(
  _controller: any,
  modelName: string,
  _handlerName: string,
  _isModelController: boolean,
  implType: any,
  usage: { usesFastifyRequest: boolean; usesFastifyReply: boolean } =
    { usesFastifyRequest: true, usesFastifyReply: true },
): string {
  const fastifyTypes = ['FastifyInstance'];
  if (usage.usesFastifyRequest) fastifyTypes.push('FastifyRequest');
  if (usage.usesFastifyReply) fastifyTypes.push('FastifyReply');
  const imports = [
    `import { ${fastifyTypes.join(', ')} } from 'fastify';`,
  ];

  // Don't import controller/service - they're passed via options
  // Handlers are accessed via options.controllers or options.services

  // Add validation imports if Zod is configured
  if (implType?.technology?.validation === 'zod' && modelName) {
    imports.push(`import { ${modelName}Schema } from '../validation/${modelName}.schema.js';`);
  }

  return imports.join('\n');
}

/**
 * Generate a single route handler
 */
function generateRouteHandler(
  endpoint: any,
  modelName: string,
  handlerName: string,
  isModelController: boolean,
  implType: any,
  controllerName?: string
): string {
  // Try to get operation from multiple sources
  let operation = endpoint.operation || endpoint.name;

  // If operation is 'custom' from serviceOperation, extract from path
  if (!operation || operation === 'custom') {
    const serviceOp = endpoint.serviceOperation?.type;
    if (serviceOp === 'custom' && endpoint.path) {
      // Extract action name from path like "/user/attach-profile" → "attachProfile"
      const pathParts = endpoint.path.split('/').filter((p: string) => p);
      const lastPart = pathParts[pathParts.length - 1];
      // Convert kebab-case to camelCase
      operation = lastPart.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
    } else if (!operation) {
      operation = serviceOp;
    }
  }

  // If still no operation, try to infer from method and path
  if (!operation) {
    operation = inferOperationFromMethodAndPath(endpoint.method, endpoint.path);
  }

  // Only warn if we couldn't infer operation after all attempts
  if (!operation || operation === 'unknown') {
    console.warn(`Warning: Could not determine operation for endpoint in ${controllerName || 'controller'}. Endpoint data:`, {
      operation: endpoint.operation,
      name: endpoint.name,
      method: endpoint.method,
      path: endpoint.path,
      serviceOperation: endpoint.serviceOperation
    });
  }

  const method = endpoint.method?.toLowerCase() || inferHttpMethod(operation);
  const path = inferPath(operation, endpoint); // Always call inferPath to handle path extraction
  const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint);

  let route = `// ${operation} ${modelName}\n`;
  route += `fastify.${method}('${path}', {\n`;

  // Add schema if available
  if (implType?.technology?.validation === 'zod') {
    route += `  schema: {\n`;

    if (operation === 'create' || operation === 'update' || operation === 'evolve') {
      route += `    body: ${modelName}Schema.${operation},\n`;
    }

    if (operation === 'retrieve' || operation === 'update' || operation === 'delete') {
      route += `    params: ${modelName}Schema.params,\n`;
    }

    route += `    response: {\n`;
    route += `      200: ${modelName}Schema.response\n`;
    route += `    }\n`;
    route += `  },\n`;
  }

  // Use 'request' for operations that need request data (including custom actions with parameters)
  const usesRequest = ['create', 'retrieve', 'update', 'evolve', 'delete', 'validate'].includes(operation) ||
                      !['list'].includes(operation); // Custom actions likely need request data
  const requestParam = usesRequest ? 'request' : '_request';

  route += `  handler: async (${requestParam}: FastifyRequest, reply: FastifyReply) => {\n`;
  route += handler.split('\n').map(line => '    ' + line).join('\n') + '\n';
  route += `  }\n`;
  route += `});`;

  return route;
}

/**
 * Generate handler body for an operation
 */
function generateHandlerBody(
  operation: string,
  modelName: string,
  handlerName: string,
  isModelController: boolean,
  implType: any,
  endpoint?: any
): string {
  const rawLowerModel = modelName?.toLowerCase() || 'item';
  // Avoid JavaScript reserved words as variable names
  const RESERVED_WORDS = 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 lowerModel = RESERVED_WORDS.has(rawLowerModel) ? `${rawLowerModel}Item` : rawLowerModel;

  // Map service operation types to CURED operations
  const operationMap: { [key: string]: string } = {
    'custom': 'create',       // POST without ID -> create
    'findUnique': 'retrieve', // GET with ID -> retrieve
    'findMany': 'list',       // GET without ID -> list
    'updateUnique': 'update', // PUT with ID -> update
    'deleteUnique': 'delete'  // DELETE with ID -> delete
  };

  const mappedOperation = operationMap[operation] || operation;

  switch (mappedOperation) {
    case 'create':
      return `try {
  // Phase 2 actor wiring — pulled from request.user (decorated by auth
  // middleware like @fastify/jwt). null when no auth wired; constraint
  // guards that reference actor.* paths fail-open (logged, treated as pass).
  const _actor = (request as any).user ?? null;
  const ${lowerModel} = await handler.create(request.body as any, _actor);
  return reply.status(201).send(${lowerModel});
} catch (error) {
  return reply.status(400).send({
    error: 'Failed to create ${lowerModel}',
    message: error instanceof Error ? error.message : String(error)
  });
}`;

    case 'retrieve':
      return `try {
  const { id } = request.params as { id: string };
  const ${lowerModel} = await handler.retrieve(id);

  if (!${lowerModel}) {
    return reply.status(404).send({ error: '${modelName} not found' });
  }

  return reply.send(${lowerModel});
} catch (error) {
  return reply.status(500).send({
    error: 'Failed to retrieve ${lowerModel}',
    message: error instanceof Error ? error.message : String(error)
  });
}`;

    case 'update':
    case 'evolve':
      return `try {
  const { id } = request.params as { id: string };
  const _actor = (request as any).user ?? null;
  const ${lowerModel} = await handler.${operation}(id, request.body as any, _actor);
  return reply.send(${lowerModel});
} catch (error) {
  return reply.status(400).send({
    error: 'Failed to ${operation} ${lowerModel}',
    message: error instanceof Error ? error.message : String(error)
  });
}`;

    case 'delete':
      return `try {
  const { id } = request.params as { id: string };
  const _actor = (request as any).user ?? null;
  await handler.delete(id, _actor);
  return reply.status(204).send();
} catch (error) {
  // Prisma's P2003 is the foreign-key-constraint-violated error. When
  // a ${lowerModel} has children (hasMany relationships not declared
  // with 'cascade'), SQLite / Postgres refuse the delete and we land
  // here. Return a 409 Conflict with a specific hint rather than a 500
  // with a raw Prisma dump — the frontend shows the message verbatim.
  const msg = error instanceof Error ? error.message : String(error);
  if (msg.includes('P2003') || msg.toLowerCase().includes('foreign key constraint')) {
    return reply.status(409).send({
      error: 'Cannot delete ${lowerModel}',
      message: \`This ${lowerModel} has related records. Delete those first, or declare the relationship with 'cascade' in your spec to delete them automatically.\`
    });
  }
  return reply.status(500).send({
    error: 'Failed to delete ${lowerModel}',
    message: msg
  });
}`;

    case 'list':
      return `try {
  const ${lowerModel}s = await handler.retrieveAll();
  return reply.send(${lowerModel}s);
} catch (error) {
  return reply.status(500).send({
    error: 'Failed to list ${lowerModel}s',
    message: error instanceof Error ? error.message : String(error)
  });
}`;

    case 'validate':
      return `try {
  const { data, operation: op } = request.body as { data: any; operation: string };
  const _actor = (request as any).user ?? null;
  // Slice 15b — validate() is async (awaits guard ctx subqueries).
  const result = await handler.validate(data, { operation: op }, _actor);
  return reply.send(result);
} catch (error) {
  return reply.status(400).send({
    error: 'Validation failed',
    message: error instanceof Error ? error.message : String(error)
  });
}`;

    default: {
      // Custom action — merge URL params + body into a single `args` object
      // and pass it to the handler. The merged shape lets the action method
      // read fields uniformly without caring whether they came from `:id`
      // in the path or the JSON body. Validation still fires on declared
      // parameters but is skipped for any param name that's also a URL
      // path parameter (those come from request.params, not the body).
      const path = endpoint?.path || '';
      const urlParamNames = new Set<string>();
      const urlParamRegex = /:([A-Za-z_][\w]*)/g;
      let urlMatch: RegExpExecArray | null;
      while ((urlMatch = urlParamRegex.exec(path)) !== null) {
        urlParamNames.add(urlMatch[1]);
      }

      const params = endpoint?.parameters || {};

      // Generate per-parameter body validation, skipping URL-path-sourced params
      const validations: string[] = [];
      for (const [pName, pDef] of Object.entries(params) as [string, any][]) {
        if (urlParamNames.has(pName)) continue;
        const required = typeof pDef === 'string' ? pDef.includes('required') : pDef?.required;
        const typeStr = typeof pDef === 'string' ? pDef.split(' ')[0] : pDef?.type || 'String';
        if (required) {
          validations.push(`  if (args.${pName} === undefined || args.${pName} === null) errors.push({ field: '${pName}', message: '${pName} is required' });`);
        }
        // Type checks (skip when undefined — `required` covers the missing case)
        if (typeStr === 'UUID' || typeStr === 'String' || typeStr === 'Email') {
          validations.push(`  if (args.${pName} !== undefined && typeof args.${pName} !== 'string') errors.push({ field: '${pName}', message: '${pName} must be a string' });`);
        } else if (typeStr === 'Integer' || typeStr === 'Number' || typeStr === 'Float') {
          validations.push(`  if (args.${pName} !== undefined && typeof args.${pName} !== 'number') errors.push({ field: '${pName}', message: '${pName} must be a number' });`);
        } else if (typeStr === 'Boolean') {
          validations.push(`  if (args.${pName} !== undefined && typeof args.${pName} !== 'boolean') errors.push({ field: '${pName}', message: '${pName} must be a boolean' });`);
        }
      }

      const validationBlock = validations.length > 0
        ? `  const errors: Array<{ field: string; message: string }> = [];\n${validations.join('\n')}\n  if (errors.length > 0) return reply.status(400).send({ error: 'Validation failed', details: errors });`
        : '';

      return `try {
  const params = (request.params || {}) as Record<string, any>;
  const body = (request.body || {}) as Record<string, any>;
  const args = { ...params, ...body };
  const _actor = (request as any).user ?? null;
${validationBlock}
  const result = await handler.${operation}(args, _actor);
  return reply.send(result || { success: true });
} catch (error) {
  const msg = error instanceof Error ? error.message : String(error);
  // 501 for TODO-stubbed action bodies (#43F); 400 for real failures.
  if (msg.includes('is not implemented')) {
    return reply.status(501).send({ error: 'Not Implemented', message: msg });
  }
  return reply.status(400).send({
    error: 'Failed to execute ${operation}',
    message: msg
  });
}`;
    }
  }
}

/**
 * Infer operation from HTTP method and path
 */
function inferOperationFromMethodAndPath(method: string, path: string): string {
  if (!method || !path) return 'unknown';

  const methodLower = method.toLowerCase();
  const hasIdParam = path.includes(':id');

  if (methodLower === 'post' && !hasIdParam) return 'create';
  if (methodLower === 'get' && hasIdParam) return 'retrieve';
  if (methodLower === 'get' && !hasIdParam) return 'list';
  if (methodLower === 'put' && hasIdParam) return 'update';
  if (methodLower === 'patch' && hasIdParam) return 'evolve';
  if (methodLower === 'delete' && hasIdParam) return 'delete';

  return 'unknown';
}

/**
 * Infer HTTP method from operation name (delegates to shared spec-rules)
 */
function inferHttpMethod(operation: string): string {
  return sharedInferHttpMethod(operation);
}

/**
 * Infer path from operation name.
 * Delegates to shared spec-rules for standard CURVED ops,
 * handles custom service operation paths locally.
 */
function inferPath(operation: string, endpoint: any): string {
  // If endpoint has an explicit path, use it directly
  if (endpoint?.path) {
    return endpoint.path;
  }

  return sharedInferPath(operation);
}

/**
 * Convert CURED operations to endpoints (delegates to shared spec-rules)
 */
function curedToEndpoints(cured: any, _modelName: string): any[] {
  return sharedCuredToEndpoints(cured);
}
