/**
 * Password reset use cases.
 *
 * - RequestPasswordReset: always succeeds at the HTTP level to avoid user enumeration.
 *   Stores hashed token, rate-limited per IP and per email, triggers email.
 * - ConfirmPasswordReset: validates the token, updates the password hash, marks token used.
 */
import { randomBytes } from 'node:crypto';
import { hashSessionToken } from '../infrastructure/session-token.js';
import {
  InvalidResetTokenError,
  RESET_TOKEN_TTL_MS,
  isTokenUsable,
  type PasswordResetToken,
} from '../domain/password-reset.js';
import type {
  PasswordHasher,
  PasswordResetTokenRepository,
  ResetRateLimiter,
  PasswordResetMailer,
  UserRepository,
} from '../domain/ports.js';
import { normalizeEmail } from '../domain/user.js';

export interface RequestPasswordResetDeps {
  users: UserRepository;
  tokens: PasswordResetTokenRepository;
  rateLimiter: ResetRateLimiter;
  mailer: PasswordResetMailer;
  /** Build the absolute URL the customer will click. */
  buildResetUrl: (rawToken: string) => string;
  /** Optional logger/audit sink. No-op by default. */
  audit?: (entry: { action: string; userId?: string; email?: string; ip?: string }) => void;
  /** Equalises timing when user does not exist. */
  noUserDelay?: () => Promise<void>;
  /** Locale for the email template. */
  defaultLocale?: string;
}

export class RequestPasswordReset {
  constructor(private readonly deps: RequestPasswordResetDeps) {}

  async execute(input: { email: string; ip?: string; userAgent?: string }): Promise<void> {
    const email = normalizeEmail(input.email);
    const ipKey = `ip:${input.ip ?? 'unknown'}`;
    const emailKey = `email:${email}`;
    const [ipOk, emailOk] = await Promise.all([
      this.deps.rateLimiter.allow(ipKey),
      this.deps.rateLimiter.allow(emailKey),
    ]);
    if (!ipOk || !emailOk) {
      // Silently drop; do not reveal rate limiting state.
      return;
    }

    const userWithHash = await this.deps.users.findByEmail(email);
    if (!userWithHash) {
      await (this.deps.noUserDelay?.() ?? defaultEqualiseDelay());
      return;
    }

    await this.deps.tokens.invalidateAllForUser(userWithHash.id);

    const rawToken = randomBytes(32).toString('hex');
    const tokenHash = hashToken(rawToken);
    const expiresAt = new Date(Date.now() + RESET_TOKEN_TTL_MS);
    await this.deps.tokens.create({
      userId: userWithHash.id,
      tokenHash,
      expiresAt,
      requestIp: input.ip,
      requestUserAgent: input.userAgent,
    });

    this.deps.audit?.({
      action: 'password_reset.requested',
      userId: userWithHash.id,
      email,
      ip: input.ip,
    });

    const resetUrl = this.deps.buildResetUrl(rawToken);
    await this.deps.mailer.sendPasswordReset({
      email,
      resetUrl,
      locale: this.deps.defaultLocale ?? 'es',
    });
  }
}

export interface ConfirmPasswordResetDeps {
  tokens: PasswordResetTokenRepository;
  users: UserRepository;
  hasher: PasswordHasher;
  audit?: (entry: { action: string; userId?: string; ip?: string }) => void;
}

export class ConfirmPasswordReset {
  constructor(private readonly deps: ConfirmPasswordResetDeps) {}

  async execute(input: { token: string; newPassword: string }): Promise<void> {
    if (input.newPassword.length < 8 || input.newPassword.length > 128) {
      throw new InvalidResetTokenError();
    }
    const tokenHash = hashToken(input.token);
    const token = await this.deps.tokens.findByTokenHash(tokenHash);
    if (!token || !isTokenUsable(token)) {
      throw new InvalidResetTokenError();
    }

    const newHash = await this.deps.hasher.hash(input.newPassword);
    await this.deps.users.updateUser(token.userId, { passwordHash: newHash });
    await this.deps.tokens.markUsed(token.id);

    this.deps.audit?.({
      action: 'password_reset.confirmed',
      userId: token.userId,
    });
  }
}

/** Same hash algorithm as session tokens. */
export const hashToken = hashSessionToken;

async function defaultEqualiseDelay(): Promise<void> {
  // ~80ms equalises with the hasher path roughly. Tune if needed.
  await new Promise((r) => setTimeout(r, 80));
}

// Re-export for tests
export type { PasswordResetToken };