/**
 * Fastify Server Generator
 *
 * Generates main Fastify server bootstrap file
 */

import type { TemplateContext } from '@specverse/types';
import { normalizeSpec, deriveBasePath } from '@specverse/types/spec-rules';

/** Resolve the manifest's orm choice so the server template adapts its
 * imports + lifecycle hooks. When MongoDBNativeDriver is selected we
 * drop the prisma import + prisma.$disconnect call (no prisma in deps);
 * the mongo client has its own disconnect helper exposed by the
 * client-generator. */
function resolveOrmName(manifest: any): string {
  if (!manifest) return 'PrismaORM';
  const inner = manifest.manifests
    ? Object.values(manifest.manifests)[0] as any
    : manifest;
  if (!inner) return 'PrismaORM';
  const caps = Array.isArray(inner.capabilityMappings) ? inner.capabilityMappings : [];
  const ormCap = caps.find((m: any) => m?.capability === 'orm.client') ||
                 caps.find((m: any) => m?.capability === 'orm.schema');
  if (ormCap) return ormCap.implementation || ormCap.instanceFactory || 'PrismaORM';
  return inner.defaultMappings?.orm || 'PrismaORM';
}

export default function generateFastifyServer(context: TemplateContext): string {
  const { spec, models, manifest } = context as any;
  const orm = resolveOrmName(manifest);
  const isMongoNative = orm === 'MongoDBNativeDriver';

  // Extract model names for route registration
  const allModels = models || (spec?.models ? Object.values(spec.models) : []);
  const modelNames = allModels.map((m: any) => m.name).filter(Boolean);

  // Generate route imports and registrations. We import the routes module
  // as a namespace so the optional `registerExternalRoutes` named export is
  // available at runtime — its presence depends on whether the controller
  // declared any actions with absolute `path:` (escape-the-prefix) routes.
  const routeImports = modelNames.map((name: string) =>
    `import ${name}Routes, * as ${name}RoutesNS from './routes/${name}Controller.js';`
  ).join('\n');

  const routeRegistrations = modelNames.map((name: string) => {
    const path = deriveBasePath(name);
    return `  await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
  }).join('\n');

  // External-route registrations: each controller's namespace may export
  // `registerExternalRoutes` for action.path declarations like
  // '/api/v2/auth/register' that need to escape the controller prefix.
  // Mounted at root scope, no prefix, no plugin scoping.
  const externalRouteRegistrations = modelNames.map((name: string) => {
    return `  if (typeof (${name}RoutesNS as any).registerExternalRoutes === 'function') {
    await (${name}RoutesNS as any).registerExternalRoutes(fastify, { controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });
  }`;
  }).join('\n');

  // Check for events in spec
  const specEvents = spec.events ? Object.keys(spec.events) : [];
  const hasEvents = specEvents.length > 0 || modelNames.length > 0;

  // Service imports (the singleton instances from services/{Name}.ts)
  // Each service has `/services/{Name}.ts` → `export default {name}Service`
  // Each service also gets a routes file at `/routes/{Name}Routes.ts`.
  const servicesList = spec.services ? Object.keys(spec.services) : [];
  const serviceSingletonImports = servicesList.map((name: string) => {
    const lower = name.charAt(0).toLowerCase() + name.slice(1);
    return `import ${lower} from './services/${name}.js';`;
  }).join('\n');
  const serviceRouteImports = servicesList.map((name: string) =>
    `import ${name}Routes from './routes/${name}Routes.js';`
  ).join('\n');
  const serviceRouteRegistrations = servicesList.map((name: string) => {
    const lower = name.charAt(0).toLowerCase() + name.slice(1);
    return `  await fastify.register(${name}Routes, { prefix: '/api/services/${name}', services: { ${name}: ${lower} } });`;
  }).join('\n');

  return `/**
 * Fastify Server
 * Generated from SpecVerse specification
 */

// Load .env from the closest ancestor that has one. Walks up from
// this file's directory so the backend picks up the project-root
// .env whether it's running in the monorepo layout
// (generated/code/backend/src/main.ts with .env at generated/code/)
// or the standalone layout (generated/code/src/main.ts with .env at
// generated/code/). dotenv's default cwd-based behaviour doesn't
// handle the monorepo case because npm workspaces \`cd\`s into the
// workspace before running the script.
import { config as loadEnv } from 'dotenv';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
{
  let dir = dirname(fileURLToPath(import.meta.url));
  while (dir !== '/' && dir !== dirname(dir)) {
    const candidate = join(dir, '.env');
    if (existsSync(candidate)) { loadEnv({ path: candidate }); break; }
    dir = dirname(dir);
  }
}

import Fastify from 'fastify';
import cors from '@fastify/cors';
${isMongoNative
  ? `import { disconnect as disconnectMongo } from './db/mongoClient.js';`
  : `import { PrismaClient } from '@prisma/client';`}
${hasEvents ? `import { eventBus } from './events/eventBus.js';
import { registerWebSocketBridge } from './events/websocket-bridge.js';` : ''}

${isMongoNative
  ? `// MongoDB native driver client lives in db/mongoClient.ts; nothing
// to initialize at module scope (lazy-connect on first getCollection).`
  : `// Initialize Prisma
export const prisma = new PrismaClient();`}

// Initialize Fastify
const fastify = Fastify({
  logger: { level: 'info' },
});

// Register plugins
await fastify.register(cors, { origin: true, credentials: true });

// Health check
fastify.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
fastify.get('/', async () => ({ name: 'SpecVerse Generated API', models: ${JSON.stringify(modelNames)} }));

// Spec endpoint — serves the specification for the runtime frontend
const embeddedSpec = ${JSON.stringify(normalizeSpec(spec), null, 2)};
fastify.get('/api/spec', async () => embeddedSpec);

// Runtime info endpoint — models and controllers for DevShell
fastify.get('/api/runtime/info', async () => ({
  controllers: ${JSON.stringify(modelNames.map((n: string) => `${n}Controller`))},
  models: ${JSON.stringify(modelNames)},
  events: ${JSON.stringify(specEvents)},
  services: ${JSON.stringify(servicesList)}
}));

${hasEvents ? `// Event history endpoint — runtime introspection, not part of
// the user's API surface. Lives under /api/runtime/ so it doesn't
// collide with the auto-generated Event model route (spec models
// named "Event" produce a GET /api/events of their own).
fastify.get('/api/runtime/events', async () => eventBus.getHistory());` : ''}

// Register routes
${routeImports}

${serviceSingletonImports ? `// Service singletons + their Fastify route files\n${serviceSingletonImports}\n${serviceRouteImports}` : ''}

async function registerRoutes() {
${routeRegistrations}
${serviceRouteRegistrations ? '\n  // Service operation routes (RPC-style under /api/services/{ServiceName})\n' + serviceRouteRegistrations : ''}

  // External-prefix actions (action.path declarations like '/api/v2/auth/register')
${externalRouteRegistrations}
}

// Start server
const start = async () => {
  try {
    await registerRoutes();
${hasEvents ? `    // Register WebSocket bridge for real-time frontend events
    await registerWebSocketBridge(fastify);` : ''}
    const port = parseInt(process.env.PORT || '3000');
    // Listen on :: (IPv6 wildcard) instead of 0.0.0.0 so the server
    // accepts both IPv6 and IPv4-mapped connections. Node resolves
    // 'localhost' to ::1 first; if the backend only binds IPv4, the
    // vite dev-server WS proxy hits ECONNREFUSED on ::1 and doesn't
    // fall back to 127.0.0.1 cleanly. Binding :: fixes both cases.
    await fastify.listen({ port, host: '::' });
    console.log(\`Server running at http://localhost:\${port}\`);
    console.log(\`API endpoints: ${modelNames.map((n: string) => `/api/${pluralizeLower(n)}`).join(', ')}\`);
${hasEvents ? `    console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
    console.log(\`Events: ${specEvents.length} wired (GET /api/runtime/events for the full list)\`);` : ''}

    // Graceful shutdown — without these handlers, Ctrl-C leaves open
    // WebSocket connections + Prisma clients hanging, and tsx watch
    // reports "Previous process hasn't exited yet. Force killing...".
    // Drain Fastify (closes HTTP + WS), then disconnect Prisma, then exit.
    const shutdown = async (signal: string) => {
      try {
        fastify.log.info(\`Received \${signal}, closing server...\`);
        await fastify.close();
        try { ${isMongoNative ? 'await disconnectMongo();' : 'await (prisma as any).$disconnect?.();'} } catch { /* ignore */ }
        process.exit(0);
      } catch (err) {
        fastify.log.error({ err }, 'Error during shutdown');
        process.exit(1);
      }
    };
    process.on('SIGINT', () => shutdown('SIGINT'));
    process.on('SIGTERM', () => shutdown('SIGTERM'));
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();
`;
}

/**
 * Minimal English pluralizer — matches the same rules the routes
 * generator uses so the startup banner's route list agrees with the
 * actual routes the server exposes. "Category" → "categories", not
 * "categorys"; "Box" → "boxes"; "Item" → "items".
 */
function pluralizeLower(s: string): string {
  const lower = s.toLowerCase();
  if (/[^aeiou]y$/.test(lower)) return lower.slice(0, -1) + 'ies';
  if (/(s|x|z|ch|sh)$/.test(lower)) return lower + 'es';
  return lower + 's';
}
