import Fastify, { type FastifyError, type FastifyInstance, type FastifyRequest } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import fastifyWebsocket from '@fastify/websocket';
import fastifyStatic from '@fastify/static';
import { existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Config } from './config.js';
import type { Db } from './db/index.js';
import { AuthService, LoginRateLimiter, type AuthContext } from './auth/service.js';
import { PtyManager } from './core/pty-manager.js';
import { DiscoveryService } from './core/discovery-service.js';
import { WorktreeManager } from './core/worktree-manager.js';
import { RepoDiscoveryService } from './core/repo-discovery.js';
import { GroupManager } from './core/group-manager.js';
import { PushService } from './core/push-service.js';
import { loadSecretBox } from './core/secret-box.js';
import { registerAuthRoutes } from './routes/auth.js';
import { registerSessionRoutes } from './routes/sessions.js';
import { registerRepoRoutes } from './routes/repos.js';
import { registerGroupRoutes } from './routes/groups.js';
import { registerWorktreeRoutes } from './routes/worktrees.js';
import { registerPushRoutes } from './routes/push.js';
import { registerSettingsRoutes } from './routes/settings.js';
import { registerFsRoutes } from './routes/fs.js';
import { registerAuditRoutes } from './routes/audit.js';
import { registerDataRoutes } from './routes/data.js';
import { registerWsGateway } from './ws/gateway.js';
import { isHttpsRequest } from './routes/auth.js';

// En-têtes de sécurité posés sur TOUTES les réponses (defense-in-depth + conformité scanners
// entreprise). Le terminal web reste du RCE par conception : ces en-têtes durcissent la SPA et
// le transport, ils ne remplacent pas le modèle loopback + auth + Origin.
const SECURITY_HEADERS: Record<string, string> = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'Referrer-Policy': 'no-referrer',
  'Cross-Origin-Opener-Policy': 'same-origin',
  'Permissions-Policy': 'geolocation=(), microphone=(), camera=(), payment=()',
  // CSP calibrée pour la SPA : Tailwind/xterm injectent du style inline ; le WebSocket impose
  // ws:/wss: en connect-src ; le service worker push impose worker-src 'self'.
  'Content-Security-Policy': [
    "default-src 'self'",
    "script-src 'self'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data:",
    "font-src 'self' data:",
    "connect-src 'self' ws: wss:",
    "worker-src 'self'",
    "manifest-src 'self'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "object-src 'none'",
    "form-action 'self'",
  ].join('; '),
};

declare module 'fastify' {
  interface FastifyRequest {
    authContext: AuthContext | null;
  }
  interface FastifyContextConfig {
    public?: boolean;
  }
}

export interface AppBundle {
  app: FastifyInstance;
  auth: AuthService;
  manager: PtyManager;
  discovery: DiscoveryService;
  repoDiscovery: RepoDiscoveryService;
  worktrees: WorktreeManager;
  groups: GroupManager;
  push: PushService;
}

export function buildApp(config: Config, db: Db, serverVersion: string): AppBundle {
  const app = Fastify({ logger: { level: process.env.ARBORETUM_LOG ?? 'info' } });
  // Chiffrement au repos des secrets (server_secret, clé privée VAPID) dans la base.
  const box = loadSecretBox(config.dataDir);
  const auth = new AuthService(db, box);
  const limiter = new LoginRateLimiter();
  const push = new PushService(db, config.vapidContact, undefined, box);
  const manager = new PtyManager(db, config.claudeSessionsDir, push);
  const discovery = new DiscoveryService({
    ptyManager: manager,
    projectsDir: config.claudeProjectsDir,
    sessionsDir: config.claudeSessionsDir,
  });
  const worktrees = new WorktreeManager(db, manager, discovery);
  // Démarré dans runDaemon() (jamais ici) → le scan ne tourne pas pendant les tests qui appellent buildApp.
  const repoDiscovery = new RepoDiscoveryService(db, worktrees);
  const groups = new GroupManager(db);

  // En-têtes de sécurité sur toute réponse + no-store sur les réponses sensibles (API/WS).
  // onSend DOIT retourner le payload (sinon Fastify vide la réponse).
  app.addHook('onSend', async (req, reply, payload) => {
    reply.removeHeader('Server'); // anti-fingerprinting (no-op si absent)
    for (const [k, v] of Object.entries(SECURITY_HEADERS)) reply.header(k, v);
    if (isHttpsRequest(req)) {
      reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    }
    if (req.url.startsWith('/api/') || req.url.startsWith('/ws')) {
      reply.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
    }
    return payload;
  });

  // Garde CSRF defense-in-depth : une mutation porteuse d'un corps DOIT être en application/json
  // (bloque les soumissions de formulaire cross-site en text/plain, que Fastify parserait sinon).
  app.addHook('preHandler', async (req, reply) => {
    if (!['POST', 'PATCH', 'PUT', 'DELETE'].includes(req.method)) return;
    const len = req.headers['content-length'];
    if (!len || len === '0') return; // pas de corps : rien à valider
    const ct = (req.headers['content-type'] ?? '').toLowerCase();
    if (!ct.startsWith('application/json')) {
      return reply.status(415).send({ error: { code: 'UNSUPPORTED_MEDIA_TYPE', message: 'Content-Type must be application/json' } });
    }
  });

  // Handler d'erreur : ne jamais fuiter de stack/chemin au client ; détail complet côté logs.
  app.setErrorHandler((err: FastifyError, req, reply) => {
    const status = err.statusCode && err.statusCode >= 400 && err.statusCode < 600 ? err.statusCode : 500;
    if (status >= 500) {
      req.log.error({ err }, 'unhandled error');
      return reply.status(status).send({ error: { code: 'INTERNAL', message: 'Internal Server Error' } });
    }
    // erreurs client (4xx) : code générique, message court non sensible.
    return reply.status(status).send({ error: { code: err.code ?? 'BAD_REQUEST', message: err.message } });
  });

  void app.register(fastifyCookie);
  void app.register(fastifyWebsocket, {
    options: { maxPayload: 1024 * 1024 },
  });

  const allowedOrigins = new Set<string>([
    `http://127.0.0.1:${config.port}`,
    `http://localhost:${config.port}`,
    ...config.allowedOrigins,
  ]);

  const authenticate = (req: FastifyRequest): AuthContext | null => {
    const bearer = req.headers.authorization;
    if (bearer?.startsWith('Bearer ')) {
      const ctx = auth.verifyRawToken(bearer.slice(7));
      if (ctx) return ctx;
    }
    return auth.verifyCookie(req.cookies[auth.cookieName]);
  };

  // Garde globale : auth sur tout /api/** et /ws ; check Origin strict quand l'en-tête est présent
  // (anti cross-site WebSocket hijacking — le cookie SameSite=Strict ne suffit pas pour les upgrades).
  app.addHook('preValidation', async (req, reply) => {
    const isApi = req.url.startsWith('/api/');
    const isWs = req.url.startsWith('/ws');
    if (!isApi && !isWs) return; // statique : public (la SPA gère son écran de login)
    const origin = req.headers.origin;
    if (origin && !allowedOrigins.has(origin)) {
      return reply.status(403).send({ error: { code: 'BAD_ORIGIN', message: `Origin not allowed: ${origin}` } });
    }
    req.authContext = authenticate(req);
    if (req.routeOptions.config.public) return;
    if (!req.authContext) {
      return reply.status(401).send({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } });
    }
  });

  registerAuthRoutes(app, auth, limiter, serverVersion, db);
  registerSessionRoutes(app, manager, discovery);
  registerRepoRoutes(app, worktrees, db);
  registerGroupRoutes(app, groups, db);
  registerWorktreeRoutes(app, worktrees);
  registerPushRoutes(app, push, db);
  registerSettingsRoutes(app, db, config, serverVersion, push);
  registerFsRoutes(app);
  registerAuditRoutes(app, db);
  registerDataRoutes(app, db, auth);
  // La route websocket doit être déclarée APRÈS le chargement du plugin (contexte
  // encapsulé) — sinon le handler reçoit la signature REST (request, reply).
  void app.register(async (scoped) => {
    registerWsGateway(scoped, manager, discovery, worktrees, groups, serverVersion);
  });

  // SPA buildée embarquée dans le paquet npm (public/) — absente en dev (vite dev sert le front)
  const publicDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'public');
  if (existsSync(publicDir)) {
    void app.register(fastifyStatic, { root: publicDir, wildcard: false });
    app.setNotFoundHandler((req, reply) => {
      if (req.url.startsWith('/api/') || req.url.startsWith('/ws')) {
        return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'Route not found' } });
      }
      return reply.sendFile('index.html');
    });
  }

  return { app, auth, manager, discovery, repoDiscovery, worktrees, groups, push };
}
