import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import type { Authenticate } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { errorSchema } from '../../../shared/swagger.js';
import { BackofficeLogin } from '../application/login.js';
import { BackofficeLogout } from '../application/logout.js';
import { InMemoryBackofficeLoginRateLimiter } from '../infrastructure/backoffice-login-rate-limiter.js';
import type { BackofficeLoginRateLimiter } from '../domain/ports.js';
import { Argon2BackofficePasswordHasher } from '../infrastructure/backoffice-password-hasher.js';
import { PgBackofficeUserRepository } from '../infrastructure/pg-backoffice-user-repository.js';
import { PgBackofficeSessionRepository } from '../infrastructure/pg-backoffice-session-repository.js';
import {
  generateBackofficeToken,
  hashBackofficeToken,
} from '../infrastructure/backoffice-session-token.js';
import { BACKOFFICE_SESSION_TTL_MS } from '../domain/backoffice-session.js';
import { BackofficeInvalidCredentialsError, BackofficeRateLimitedError } from '../domain/errors.js';

export const BACKOFFICE_SESSION_COOKIE_NAME = 'backoffice_session';

export interface BackofficeRoutesDeps {
  pool: pg.Pool;
  hasher?: import('../domain/ports.js').BackofficePasswordHasher;
  rateLimiter?: BackofficeLoginRateLimiter;
  /** Session authenticator injected by the composition root. */
  authenticate?: Authenticate;
}

const credentialsSchema = z.object({
  email: z.email(),
  password: z.string().min(8).max(128),
});

export async function registerBackofficeRoutes(
  app: FastifyInstance,
  deps: BackofficeRoutesDeps,
): Promise<void> {
  const hasher = deps.hasher ?? new Argon2BackofficePasswordHasher();
  const users = new PgBackofficeUserRepository(deps.pool);
  const sessions = new PgBackofficeSessionRepository(deps.pool);
  const rateLimiter = deps.rateLimiter ?? new InMemoryBackofficeLoginRateLimiter();
  const login = new BackofficeLogin({
    users,
    sessions,
    hasher,
    rateLimiter,
    generateToken: generateBackofficeToken,
    hashToken: hashBackofficeToken,
  });
  const logout = new BackofficeLogout(sessions, hashBackofficeToken);

  const loginSchema: FastifySchema = {
    tags: ['Backoffice'],
    summary: 'Backoffice login',
    description:
      'Inicia sesión en el backoffice y devuelve una cookie `backoffice_session` (válida 8h).',
    body: {
      type: 'object',
      required: ['email', 'password'],
      properties: {
        email: { type: 'string', format: 'email' },
        password: { type: 'string' },
      },
    },
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string', format: 'uuid' },
          email: { type: 'string', format: 'email' },
          role: { type: 'string', enum: ['admin', 'editor'] },
        },
      },
      401: errorSchema,
      429: errorSchema,
    },
  };

  const logoutSchema: FastifySchema = {
    tags: ['Backoffice'],
    summary: 'Backoffice logout',
    response: { 204: { type: 'null' } },
  };

  const meSchema: FastifySchema = {
    tags: ['Backoffice'],
    summary: 'Current backoffice user',
    description: 'Devuelve el usuario backoffice autenticado o `{user: null}` si no hay sesión.',
    response: {
      200: {
        oneOf: [
          {
            type: 'object',
            properties: {
              id: { type: 'string', format: 'uuid' },
              email: { type: 'string', format: 'email' },
              role: { type: 'string', enum: ['admin', 'editor'] },
            },
          },
          { type: 'object', properties: { user: { type: 'null' } } },
        ],
      },
    },
  };

  app.post('/backoffice/auth/login', { schema: loginSchema }, async (request, reply) => {
    const parseResult = credentialsSchema.safeParse(request.body);
    if (!parseResult.success) {
      throw new AppError(
        400,
        'VALIDATION_ERROR',
        'Invalid request payload',
        parseResult.error.issues.map((issue) => ({
          path: issue.path.join('.'),
          message: issue.message,
          code: issue.code,
        })),
      );
    }
    const input = parseResult.data;
    try {
      const result = await login.execute(input);
      setCookie(reply, result.token, isSecureRequest(request));
      return reply
        .code(200)
        .send({ id: result.user.id, email: result.user.email, role: result.user.role });
    } catch (error) {
      if (error instanceof BackofficeRateLimitedError) {
        void reply.header('Retry-After', String(Math.ceil(error.retryAfterMs / 1000)));
        throw new AppError(429, 'TOO_MANY_ATTEMPTS', 'Too many attempts');
      }
      if (error instanceof BackofficeInvalidCredentialsError) {
        throw new AppError(401, 'INVALID_CREDENTIALS', 'Invalid credentials');
      }
      throw error;
    }
  });

  app.post('/backoffice/auth/logout', { schema: logoutSchema }, async (request, reply) => {
    const token = request.cookies[BACKOFFICE_SESSION_COOKIE_NAME];
    await logout.execute(token);
    clearCookie(reply, isSecureRequest(request));
    return reply.code(204).send();
  });

  app.get('/backoffice/auth/me', { schema: meSchema }, async (request, reply) => {
    try {
      const user = await deps.authenticate!(request);
      return reply.send({ id: user.id, email: user.email, role: user.role });
    } catch (error) {
      if (error instanceof AppError && error.statusCode === 401) {
        return reply.send({ user: null });
      }
      throw error;
    }
  });
}

function isSecureRequest(request: FastifyRequest): boolean {
  const forwardedProto = request.headers['x-forwarded-proto'];
  const proto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
  return request.protocol === 'https' || proto === 'https';
}

function setCookie(reply: FastifyReply, token: string, secure: boolean): void {
  void reply.setCookie(BACKOFFICE_SESSION_COOKIE_NAME, token, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure,
    maxAge: Math.floor(BACKOFFICE_SESSION_TTL_MS / 1000),
  });
}

function clearCookie(reply: FastifyReply, secure: boolean): void {
  void reply.clearCookie(BACKOFFICE_SESSION_COOKIE_NAME, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure,
  });
}
