/**
 * Identity API adapters. Thin HTTP layer: validates input (parseJson hook),
 * calls use cases, maps domain errors to the shared error envelope.
 */
import type { FastifyInstance, FastifyReply } from 'fastify';
import type { FastifySchema } from 'fastify';
import { errorSchema } from '../../../shared/swagger.js';
import { z } from 'zod';
import type { Authenticate } from '../../../shared/auth.js';
import { parseJson } from '../../../shared/http-input.js';
import { AppError } from '../../../shared/errors.js';
import type pg from 'pg';
import { RegisterUser } from '../application/register-user.js';
import { Login } from '../application/login.js';
import { Logout } from '../application/logout.js';
import {
  InMemoryLoginRateLimiter,
  type LoginRateLimiter,
} from '../application/login-rate-limiter.js';
import { Argon2PasswordHasher } from '../infrastructure/argon2-password-hasher.js';
import { PgUserRepository } from '../infrastructure/pg-user-repository.js';
import { PgSessionRepository } from '../infrastructure/pg-session-repository.js';
import { generateSessionToken, hashSessionToken } from '../infrastructure/session-token.js';
import type {
  PasswordHasher,
  PasswordResetMailer,
  PasswordResetTokenRepository,
  ResetRateLimiter,
  WelcomeMailer,
} from '../domain/ports.js';
import {
  EmailAlreadyRegisteredError,
  InvalidCredentialsError,
  RateLimitedError,
} from '../domain/errors.js';
import { SESSION_TTL_MS } from '../domain/session.js';
import { PgPasswordResetTokenRepository } from '../infrastructure/pg-password-reset-token-repository.js';
import { ConfirmPasswordReset, RequestPasswordReset } from '../application/password-reset.js';
import { InvalidResetTokenError } from '../domain/password-reset.js';
import { createPasswordResetMailer } from '../infrastructure/smtp-password-reset-mailer.js';

export const SESSION_COOKIE_NAME = 'mdv_session';

export interface IdentityRoutesDeps {
  pool: pg.Pool;
  /** Secure cookie flag (config-driven; default true). */
  cookieSecure?: boolean;
  /** Test seams; production uses defaults. */
  hasher?: PasswordHasher;
  rateLimiter?: LoginRateLimiter;
  /** Session authenticator. Created by createSessionAuthenticator in build-app.ts. */
  authenticate?: Authenticate;
  /** Optional: enables POST /auth/password-reset/request and /confirm. */
  passwordReset?: {
    tokens?: PasswordResetTokenRepository;
    rateLimiter?: ResetRateLimiter;
    mailer?: PasswordResetMailer;
    /** Build absolute reset URL from raw token. Defaults to storefront path. */
    buildResetUrl?: (rawToken: string) => string;
    /** Audit sink (defaults to no-op). */
    audit?: (entry: { action: string; userId?: string; email?: string; ip?: string }) => void;
  };
  /** Best-effort welcome email sent on account creation (F-152). */
  welcomeMailer?: WelcomeMailer;
}

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

export async function registerIdentityRoutes(
  app: FastifyInstance,
  deps: IdentityRoutesDeps,
): Promise<void> {
  const cookieSecure = deps.cookieSecure ?? true;
  const hasher = deps.hasher ?? new Argon2PasswordHasher();
  const users = new PgUserRepository(deps.pool);
  const sessions = new PgSessionRepository(deps.pool);
  const rateLimiter = deps.rateLimiter ?? new InMemoryLoginRateLimiter();

  const registerUser = new RegisterUser(users, hasher);
  const welcomeMailer = deps.welcomeMailer;
  const login = new Login({
    users,
    sessions,
    hasher,
    rateLimiter,
    generateToken: generateSessionToken,
    hashToken: hashSessionToken,
  });
  const logout = new Logout(sessions, hashSessionToken);

  const registerSchema: FastifySchema = {
    tags: ['Auth'],
    summary: 'Register new user',
    description: 'Crea una cuenta de usuario. Por defecto el rol es `customer`.',
    body: {
      type: 'object',
      required: ['email', 'password'],
      properties: {
        email: { type: 'string', format: 'email', maxLength: 255, description: 'Email único' },
        password: {
          type: 'string',
          minLength: 8,
          maxLength: 128,
          description: 'Contraseña (mín. 8 caracteres)',
        },
      },
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'string', format: 'uuid' },
          email: { type: 'string', format: 'email' },
          role: { type: 'string', enum: ['customer', 'editor', 'admin'] },
          createdAt: { type: 'string', format: 'date-time' },
        },
      },
      409: errorSchema,
    },
  };

  const loginSchema: FastifySchema = {
    tags: ['Auth'],
    summary: 'Login',
    description: 'Inicia sesión y devuelve una cookie de sesión `mdv_session` (válida 7 días).',
    body: {
      type: 'object',
      required: ['email', 'password'],
      properties: {
        email: { type: 'string', format: 'email', description: 'Email' },
        password: { type: 'string', description: 'Contraseña' },
      },
    },
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string', format: 'uuid' },
          email: { type: 'string', format: 'email' },
          role: { type: 'string', enum: ['customer', 'editor', 'admin'] },
        },
      },
      401: errorSchema,
      429: errorSchema,
    },
  };

  const logoutSchema: FastifySchema = {
    tags: ['Auth'],
    summary: 'Logout',
    description: 'Invalida la sesión actual y elimina la cookie.',
    response: { 204: { type: 'null' } },
  };

  const meSchema: FastifySchema = {
    tags: ['Auth'],
    summary: 'Current user',
    description:
      'Devuelve el usuario autenticado (cookie de sesión) 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: ['customer', 'editor', 'admin'] },
            },
          },
          {
            type: 'object',
            properties: {
              user: { type: 'null' },
            },
          },
        ],
      },
    },
  };

  app.post('/auth/register', { schema: registerSchema }, async (request, reply) => {
    const input = parseJson(credentialsSchema, request.body);
    try {
      const user = await registerUser.execute(input);
      // F-152: best-effort welcome email. Never blocks account creation; a
      // delivery failure is logged and swallowed.
      if (welcomeMailer) {
        void welcomeMailer
          .sendWelcome({ email: user.email })
          .catch((error) =>
            request.log.warn({ err: error, userId: user.id }, 'welcome_email_failed'),
          );
      }
      return reply
        .code(201)
        .send({ id: user.id, email: user.email, role: user.role, createdAt: user.createdAt });
    } catch (error) {
      if (error instanceof EmailAlreadyRegisteredError) {
        throw new AppError(409, 'EMAIL_ALREADY_REGISTERED', 'Email already registered');
      }
      throw error;
    }
  });

  app.post('/auth/login', { schema: loginSchema }, async (request, reply) => {
    const input = parseJson(credentialsSchema, request.body);
    try {
      const result = await login.execute(input);
      setSessionCookie(reply, result.token, cookieSecure);
      return reply
        .code(200)
        .send({ id: result.user.id, email: result.user.email, role: result.user.role });
    } catch (error) {
      if (error instanceof RateLimitedError) {
        void reply.header('Retry-After', String(Math.ceil(error.retryAfterMs / 1000)));
        throw new AppError(429, 'TOO_MANY_ATTEMPTS', 'Too many attempts');
      }
      if (error instanceof InvalidCredentialsError) {
        throw new AppError(401, 'INVALID_CREDENTIALS', 'Invalid credentials');
      }
      throw error;
    }
  });

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

  app.get('/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;
    }
  });

  app.patch('/auth/me/password', async (request, reply) => {
    const user = await deps.authenticate!(request);
    const input = parseJson(
      z.object({
        currentPassword: z.string().min(1).max(128),
        newPassword: z.string().min(8).max(128),
      }),
      request.body,
    );
    const record = await users.findByEmail(user.email);
    if (!record || !(await hasher.verify(record.passwordHash, input.currentPassword))) {
      throw new AppError(400, 'INVALID_CURRENT_PASSWORD', 'La contraseña actual no es válida');
    }
    await users.updateUser(user.id, { passwordHash: await hasher.hash(input.newPassword) });
    return reply.send({ ok: true });
  });

  if (deps.passwordReset) {
    const pr = deps.passwordReset;
    const tokens = pr.tokens ?? new PgPasswordResetTokenRepository(deps.pool);
    const usersRepo = new PgUserRepository(deps.pool);
    const rateLimiter = pr.rateLimiter ?? new InMemoryResetRateLimiter();
    const mailer = pr.mailer ?? createPasswordResetMailer();
    const publicAppUrl = (process.env.PUBLIC_APP_URL ?? 'https://mercadodevida.es').replace(
      /\/$/,
      '',
    );
    const buildResetUrl =
      pr.buildResetUrl ??
      ((token: string) => `${publicAppUrl}/cuenta/restablecer?token=${encodeURIComponent(token)}`);

    const requestReset = new RequestPasswordReset({
      users: usersRepo,
      tokens,
      rateLimiter,
      mailer,
      buildResetUrl,
      audit: pr.audit,
    });
    const confirmReset = new ConfirmPasswordReset({
      tokens,
      users: usersRepo,
      hasher,
      audit: pr.audit,
    });

    const requestSchema: FastifySchema = {
      tags: ['Auth'],
      summary: 'Request password reset',
      description:
        'Solicita un enlace de recuperación por email. Siempre responde 200 para evitar enumeración de usuarios.',
      body: {
        type: 'object',
        required: ['email'],
        properties: {
          email: { type: 'string', format: 'email', maxLength: 255 },
        },
      },
      response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } },
    };

    const confirmSchema: FastifySchema = {
      tags: ['Auth'],
      summary: 'Confirm password reset',
      description: 'Acepta un nuevo password usando el token recibido por email.',
      body: {
        type: 'object',
        required: ['token', 'password'],
        properties: {
          token: { type: 'string', minLength: 16, maxLength: 128 },
          password: { type: 'string', minLength: 8, maxLength: 128 },
        },
      },
      response: {
        200: { type: 'object', properties: { ok: { type: 'boolean' } } },
        400: errorSchema,
      },
    };

    app.post('/auth/password-reset/request', { schema: requestSchema }, async (request, reply) => {
      if (mailer.assertReady) {
        try {
          await mailer.assertReady();
        } catch {
          throw new AppError(
            422,
            'EMAIL_DELIVERY_NOT_CONFIGURED',
            'Configura SMTP en Ajustes → SMTP / Email',
          );
        }
      } else if (mailer.isConfigured && !mailer.isConfigured()) {
        throw new AppError(
          422,
          'EMAIL_DELIVERY_NOT_CONFIGURED',
          'Configura SMTP en Ajustes → SMTP / Email',
        );
      }
      const input = parseJson(z.object({ email: z.email().max(255) }), request.body);
      const ip =
        (request.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() ??
        request.ip;
      const userAgent = request.headers['user-agent'];
      await requestReset.execute({ email: input.email, ip, userAgent });
      return reply.send({ ok: true });
    });

    app.post('/auth/password-reset/confirm', { schema: confirmSchema }, async (request, reply) => {
      const input = parseJson(
        z.object({
          token: z.string().min(16).max(128),
          password: z.string().min(8).max(128),
        }),
        request.body,
      );
      try {
        await confirmReset.execute({ token: input.token, newPassword: input.password });
        return reply.send({ ok: true });
      } catch (error) {
        if (error instanceof InvalidResetTokenError) {
          throw new AppError(400, 'INVALID_RESET_TOKEN', 'Invalid or expired reset token');
        }
        throw error;
      }
    });
  }
}

/** Simple in-memory token-bucket rate limiter for the password reset endpoints. */
export class InMemoryResetRateLimiter implements ResetRateLimiter {
  private buckets = new Map<string, { count: number; resetAt: number }>();
  constructor(
    private readonly windowMs = 60 * 60 * 1000,
    private readonly limit = 5,
  ) {}
  async allow(key: string): Promise<boolean> {
    const now = Date.now();
    const cur = this.buckets.get(key);
    if (!cur || cur.resetAt <= now) {
      this.buckets.set(key, { count: 1, resetAt: now + this.windowMs });
      return true;
    }
    if (cur.count >= this.limit) return false;
    cur.count += 1;
    return true;
  }
}

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

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