import type { FastifyInstance, FastifyRequest } from 'fastify';
import type {
  CreateTokenRequest,
  CreateTokenResponse,
  LoginRequest,
  LoginResponse,
  MeResponse,
  TokensListResponse,
} from '@arboretum/shared';
import type { AuthService, LoginRateLimiter } from '../auth/service.js';
import type { Db } from '../db/index.js';
import { recordAudit } from '../core/audit-log.js';

// Tailscale Serve / un reverse-proxy TLS posent x-forwarded-proto. On ne sert jamais
// en TLS direct : `secure` n'est posé que derrière un front HTTPS, jamais en localhost http
// (sinon le navigateur refuserait le cookie sur http://127.0.0.1 et le login local casserait).
export function isHttpsRequest(req: FastifyRequest): boolean {
  const xfp = req.headers['x-forwarded-proto'];
  const proto = (Array.isArray(xfp) ? xfp[0] : xfp)?.split(',')[0]?.trim();
  return proto === 'https';
}

export function registerAuthRoutes(
  app: FastifyInstance,
  auth: AuthService,
  limiter: LoginRateLimiter,
  serverVersion: string,
  db: Db,
): void {
  app.post('/api/v1/auth/login', { config: { public: true } }, async (req, reply) => {
    const wait = limiter.check();
    if (wait !== null) {
      return reply.status(429).send({ error: { code: 'RATE_LIMITED', message: `Retry in ${Math.ceil(wait / 1000)}s` } });
    }
    const body = req.body as Partial<LoginRequest> | null;
    const ctx = typeof body?.token === 'string' ? auth.verifyRawToken(body.token) : null;
    if (!ctx) {
      limiter.recordFailure();
      recordAudit(db, { actor: 'anonymous', action: 'login.failure', result: 'denied' });
      return reply.status(401).send({ error: { code: 'BAD_TOKEN', message: 'Invalid token' } });
    }
    limiter.recordSuccess();
    recordAudit(db, { actor: ctx.tokenId, action: 'login.success' });
    void reply.setCookie(auth.cookieName, auth.issueCookie(ctx), {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      secure: isHttpsRequest(req),
      maxAge: 30 * 24 * 3600,
    });
    const res: LoginResponse = { ok: true, label: ctx.label };
    return reply.send(res);
  });

  app.get('/api/v1/auth/me', async (req, reply) => {
    const res: MeResponse = {
      ok: true,
      tokenId: req.authContext?.tokenId ?? '',
      tokenLabel: req.authContext?.label ?? 'unknown',
      serverVersion,
    };
    return reply.send(res);
  });

  app.post('/api/v1/auth/logout', async (req, reply) => {
    // Les attributs doivent matcher ceux posés au login pour que le navigateur efface bien le cookie.
    void reply.clearCookie(auth.cookieName, { path: '/', secure: isHttpsRequest(req) });
    return reply.send({ ok: true });
  });

  // ---- Gestion des tokens d'accès (onglet Réglages) ----
  // Sous l'auth globale (preValidation). On ne renvoie jamais le hash ; la valeur en clair
  // d'un nouveau token n'est exposée qu'une seule fois, à la création.
  app.get('/api/v1/auth/tokens', async (req): Promise<TokensListResponse> => {
    const current = req.authContext?.tokenId;
    return { tokens: auth.listTokens().map((t) => ({ ...t, current: t.id === current })) };
  });

  app.post('/api/v1/auth/tokens', async (req, reply) => {
    const body = req.body as Partial<CreateTokenRequest> | null;
    const label = typeof body?.label === 'string' ? body.label.trim() : '';
    if (label.length < 1 || label.length > 64) {
      return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'label must be 1–64 characters' } });
    }
    const { id, token } = auth.createTokenRecord(label);
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'token.create', resourceId: id, details: { label } });
    return reply.status(201).send({ id, label, token } satisfies CreateTokenResponse);
  });

  app.delete('/api/v1/auth/tokens/:id', async (req, reply) => {
    const { id } = req.params as { id: string };
    const result = auth.revokeToken(id);
    const actor = req.authContext?.tokenId ?? 'unknown';
    if (result === 'not_found') {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No active token with this id' } });
    }
    if (result === 'last') {
      recordAudit(db, { actor, action: 'token.revoke', resourceId: id, result: 'denied', details: { reason: 'last_active_token' } });
      return reply.status(409).send({ error: { code: 'LAST_TOKEN', message: 'Cannot revoke the last active token' } });
    }
    recordAudit(db, { actor, action: 'token.revoke', resourceId: id });
    // 200 + corps JSON (pas 204) : le mini-client REST du front parse toujours la réponse.
    return reply.send({ ok: true });
  });
}
