import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { JwtService } from '@nestjs/jwt';
import { createHash, randomBytes } from 'node:crypto';
import { normalizeEmail } from '../common/normalize';
import { comparePassword, hasMinimumPasswordLength, hashPassword } from '../common/password';
import { getAppConfig } from '../config/env';
import { RedisQueueService } from '../jobs/redis-queue.service';
import { MailerService } from '../mailer/mailer.service';
import { PrismaService } from '../prisma/prisma.service';
import { AuthenticatedUser } from './auth.types';
import { buildOtpAuthUrl, decryptSecret, encryptSecret, generateTotpSecret, verifyTotpCode } from './totp';

const config = getAppConfig();
const TWO_FACTOR_LOGIN_CHALLENGE_TTL = '10m';
const LOGIN_LIMITER = {
  scope: 'login',
  maxAttempts: config.authLoginMaxAttempts,
  windowMinutes: config.authLoginWindowMinutes,
  message: 'Too many login attempts.',
} as const;
const TWO_FACTOR_LIMITER = {
  scope: '2fa',
  maxAttempts: config.authTwoFactorMaxAttempts,
  windowMinutes: config.authTwoFactorWindowMinutes,
  message: 'Too many two-factor verification attempts.',
} as const;
const PASSWORD_RESET_LIMITER = {
  scope: 'password-reset',
  maxAttempts: config.authPasswordResetMaxAttempts,
  windowMinutes: config.authPasswordResetWindowMinutes,
  message: 'Too many password reset requests.',
} as const;

function formatSecurityTimestamp(value: Date) {
  return value.toISOString();
}

type TwoFactorChallengePayload = AuthenticatedUser & { purpose: '2fa-login' };
type TwoFactorRecord = {
  twoFactorEnabled: boolean;
  twoFactorSecretEncrypted: string | null;
  twoFactorPendingSecretEncrypted: string | null;
  twoFactorRecoveryCodesHashJson: unknown;
  twoFactorConfirmedAt: Date | null;
};

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly prisma: PrismaService,
    private readonly redisQueueService: RedisQueueService,
    private readonly mailer: MailerService,
  ) {}

  async login(email: string, password: string, ipAddress?: string) {
    const normalizedEmail = normalizeEmail(email);
    await this.assertRateLimit(LOGIN_LIMITER, normalizedEmail, ipAddress);

    const user = await this.prisma.user.findUnique({
      where: { email: normalizedEmail },
    });

    if (!user) {
      await this.recordFailedLogin({
        email: normalizedEmail,
        ipAddress,
        reason: 'Unknown email',
      });
      await this.registerFailedAttempt(LOGIN_LIMITER, normalizedEmail, ipAddress);
      throw new UnauthorizedException('Invalid email or password');
    }

    if (!user.passwordHash) {
      await this.recordFailedLogin({
        userId: user.id,
        name: user.name,
        email: user.email,
        ipAddress,
        reason: 'Password setup incomplete',
      });
      await this.registerFailedAttempt(LOGIN_LIMITER, normalizedEmail, ipAddress);
      throw new UnauthorizedException('User account has not completed password setup');
    }

    const isPasswordValid = await comparePassword(password, user.passwordHash);
    if (!isPasswordValid) {
      await this.recordFailedLogin({
        userId: user.id,
        name: user.name,
        email: user.email,
        ipAddress,
        reason: 'Invalid password',
      });
      await this.registerFailedAttempt(LOGIN_LIMITER, normalizedEmail, ipAddress);
      throw new UnauthorizedException('Invalid email or password');
    }

    if (user.status !== 'active') {
      await this.recordFailedLogin({
        userId: user.id,
        name: user.name,
        email: user.email,
        ipAddress,
        reason: `User status ${user.status}`,
      });
      await this.registerFailedAttempt(LOGIN_LIMITER, normalizedEmail, ipAddress);
      throw new UnauthorizedException('User account is not active');
    }

    const payload: AuthenticatedUser = {
      sub: user.id,
      email: user.email,
      ver: user.sessionVersion,
    };

    await this.clearFailedAttempts(LOGIN_LIMITER, normalizedEmail, ipAddress);

    const twoFactor = await this.getTwoFactorRecord(user.id);
    if (twoFactor?.twoFactorEnabled && twoFactor.twoFactorSecretEncrypted) {
      const challengeToken = await this.jwtService.signAsync(
        {
          ...payload,
          purpose: '2fa-login',
        } satisfies TwoFactorChallengePayload,
        {
          secret: config.jwtSecret,
          expiresIn: TWO_FACTOR_LOGIN_CHALLENGE_TTL,
        },
      );

      return {
        requiresTwoFactor: true,
        challengeToken,
        challengeExpiresIn: TWO_FACTOR_LOGIN_CHALLENGE_TTL,
        user: {
          id: user.id,
          name: user.name,
          email: user.email,
          status: user.status,
        },
      };
    }

    const session = await this.issueSession(payload, user.id);

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Login Success',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Successful login for ${user.email}.`,
      },
    });

    await this.prisma.user.update({
      where: { id: user.id },
      data: { lastLoginAt: new Date() },
    });

    return {
      ...session,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        status: user.status,
      },
    };
  }

  async verifyAccessToken(token: string) {
    const payload = await this.jwtService.verifyAsync<AuthenticatedUser>(token, {
      secret: config.jwtSecret,
    });

    const user = await this.prisma.user.findUnique({
      where: { id: payload.sub },
      select: { id: true, email: true, status: true, sessionVersion: true },
    });

    if (!user || user.status !== 'active' || user.sessionVersion !== payload.ver || user.email !== payload.email) {
      throw new UnauthorizedException('Invalid or expired token');
    }

    return payload;
  }

  async refresh(refreshToken: string, ipAddress?: string) {
    const payload = await this.verifyRefreshToken(refreshToken);
    const user = await this.prisma.user.findUnique({
      where: { id: payload.sub },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
        sessionVersion: true,
        refreshTokenHash: true,
        refreshTokenExpiresAt: true,
      },
    });

    if (
      !user ||
      user.status !== 'active' ||
      user.sessionVersion !== payload.ver ||
      user.email !== payload.email ||
      !user.refreshTokenHash ||
      user.refreshTokenHash !== this.hashToken(refreshToken) ||
      !user.refreshTokenExpiresAt ||
      user.refreshTokenExpiresAt.getTime() <= Date.now()
    ) {
      throw new UnauthorizedException('Invalid or expired refresh token');
    }

    const nextPayload: AuthenticatedUser = {
      sub: user.id,
      email: user.email,
      ver: user.sessionVersion,
    };
    const session = await this.issueSession(nextPayload, user.id);

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Token Refreshed',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Refreshed session for ${user.email}.`,
      },
    });

    return session;
  }

  async logout(input: {
    accessToken?: string;
    refreshToken?: string;
    ipAddress?: string;
  }) {
    let userId: string | null = null;
    let userEmail: string | null = null;
    let actorName: string | null = null;

    if (input.accessToken) {
      try {
        const payload = await this.verifyAccessToken(input.accessToken);
        userId = payload.sub;
        userEmail = payload.email;
      } catch {
        userId = null;
      }
    }

    if (!userId && input.refreshToken) {
      const payload = await this.verifyRefreshToken(input.refreshToken);
      userId = payload.sub;
      userEmail = payload.email;
    }

    if (!userId || !userEmail) {
      throw new UnauthorizedException('Unable to resolve session for logout');
    }

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, name: true, email: true, sessionVersion: true },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    actorName = user.name;

    await this.prisma.user.update({
      where: { id: user.id },
      data: {
        refreshTokenHash: null,
        refreshTokenExpiresAt: null,
        sessionVersion: {
          increment: 1,
        },
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName,
        actorEmail: user.email,
        actionType: 'Logout Success',
        module: 'Auth Gateway',
        ipAddress: input.ipAddress || null,
        severity: 'default',
        details: `Logged out session for ${user.email}.`,
      },
    });

    return { success: true };
  }

  async me(userId: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
        createdAt: true,
        lastLoginAt: true,
      },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    return user;
  }

  async getCurrentSession(authUser: AuthenticatedUser, ipAddress?: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: authUser.sub },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
        lastLoginAt: true,
        refreshTokenExpiresAt: true,
        twoFactorEnabled: true,
        twoFactorConfirmedAt: true,
      },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    return {
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        status: user.status,
        lastLoginAt: user.lastLoginAt,
        twoFactorEnabled: user.twoFactorEnabled,
        twoFactorConfirmedAt: user.twoFactorConfirmedAt,
      },
      session: {
        issuedAt: authUser.iat ? new Date(authUser.iat * 1000).toISOString() : null,
        expiresAt: authUser.exp ? new Date(authUser.exp * 1000).toISOString() : null,
        refreshExpiresAt: user.refreshTokenExpiresAt,
        currentIp: ipAddress || null,
        policy: 'single-session',
      },
    };
  }

  async getTwoFactorStatus(userId: string) {
    const twoFactor = await this.getTwoFactorRecord(userId);
    if (!twoFactor) {
      throw new UnauthorizedException('User not found');
    }

    return {
      enabled: twoFactor.twoFactorEnabled,
      pendingSetup: Boolean(twoFactor.twoFactorPendingSecretEncrypted),
      confirmedAt: twoFactor.twoFactorConfirmedAt,
      recoveryCodesRemaining: this.readRecoveryCodeHashes(twoFactor).length,
    };
  }

  async initiateTwoFactorSetup(userId: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, email: true },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    const secret = generateTotpSecret();
    await this.prisma.$executeRaw(
      Prisma.sql`UPDATE "users" SET "two_factor_pending_secret_encrypted" = ${encryptSecret(secret, config.jwtSecret)} WHERE "id" = CAST(${user.id} AS uuid)`,
    );

    return {
      manualEntryKey: secret,
      otpauthUrl: buildOtpAuthUrl(secret, user.email, 'BizOne'),
    };
  }

  async confirmTwoFactorSetup(userId: string, code: string, ipAddress?: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        name: true,
        email: true,
      },
    });

    const twoFactor = user ? await this.getTwoFactorRecord(user.id) : null;
    if (!user || !twoFactor?.twoFactorPendingSecretEncrypted) {
      throw new UnauthorizedException('Two-factor setup is not initialized');
    }

    const secret = decryptSecret(twoFactor.twoFactorPendingSecretEncrypted, config.jwtSecret);
    if (!verifyTotpCode(secret, code)) {
      throw new UnauthorizedException('Invalid verification code');
    }

    const recoveryCodes = this.generateRecoveryCodes();
    const recoveryCodeHashes = recoveryCodes.map((value) => this.hashRecoveryCode(value));

    await this.prisma.$executeRaw(
      Prisma.sql`
        UPDATE "users"
        SET
          "two_factor_enabled" = true,
          "two_factor_secret_encrypted" = ${twoFactor.twoFactorPendingSecretEncrypted},
          "two_factor_pending_secret_encrypted" = null,
          "two_factor_recovery_codes_hash_json" = ${JSON.stringify(recoveryCodeHashes)}::jsonb,
          "two_factor_confirmed_at" = ${new Date()},
          "refresh_token_hash" = null,
          "refresh_token_expires_at" = null,
          "session_version" = "session_version" + 1
        WHERE "id" = CAST(${user.id} AS uuid)
      `,
    );

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Two Factor Enabled',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Enabled 2FA for ${user.email}.`,
      },
    });

    await this.sendSecurityNotificationSafely({
      to: user.email,
      name: user.name,
      subject: 'BizOne security alert: 2FA enabled',
      heading: 'Two-Factor Authentication Enabled',
      intro: 'Two-factor authentication was enabled for your BizOne admin account.',
      bullets: [
        `Account: ${user.email}`,
        `Time: ${formatSecurityTimestamp(new Date())}`,
        `IP address: ${ipAddress || 'Unavailable'}`,
      ],
      note: 'If you did not enable 2FA yourself, reset your password and contact an administrator immediately.',
    });

    return {
      success: true,
      recoveryCodes,
    };
  }

  async disableTwoFactor(userId: string, code: string, ipAddress?: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        name: true,
        email: true,
      },
    });

    const twoFactor = user ? await this.getTwoFactorRecord(user.id) : null;
    if (!user || !twoFactor?.twoFactorEnabled || !twoFactor.twoFactorSecretEncrypted) {
      throw new UnauthorizedException('Two-factor authentication is not enabled');
    }

    const secret = decryptSecret(twoFactor.twoFactorSecretEncrypted, config.jwtSecret);
    if (!verifyTotpCode(secret, code)) {
      throw new UnauthorizedException('Invalid verification code');
    }

    await this.prisma.$executeRaw(
      Prisma.sql`
        UPDATE "users"
        SET
          "two_factor_enabled" = false,
          "two_factor_secret_encrypted" = null,
          "two_factor_pending_secret_encrypted" = null,
          "two_factor_recovery_codes_hash_json" = null,
          "two_factor_confirmed_at" = null,
          "refresh_token_hash" = null,
          "refresh_token_expires_at" = null,
          "session_version" = "session_version" + 1
        WHERE "id" = CAST(${user.id} AS uuid)
      `,
    );

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Two Factor Disabled',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'alert',
        details: `Disabled 2FA for ${user.email}.`,
      },
    });

    await this.sendSecurityNotificationSafely({
      to: user.email,
      name: user.name,
      subject: 'BizOne security alert: 2FA disabled',
      heading: 'Two-Factor Authentication Disabled',
      intro: 'Two-factor authentication was disabled for your BizOne admin account.',
      bullets: [
        `Account: ${user.email}`,
        `Time: ${formatSecurityTimestamp(new Date())}`,
        `IP address: ${ipAddress || 'Unavailable'}`,
      ],
      note: 'If this was not you, reset your password immediately and re-enable 2FA after regaining control.',
    });

    return { success: true };
  }

  async regenerateTwoFactorRecoveryCodes(userId: string, code: string, ipAddress?: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        name: true,
        email: true,
      },
    });

    const twoFactor = user ? await this.getTwoFactorRecord(user.id) : null;
    if (!user || !twoFactor?.twoFactorEnabled || !twoFactor.twoFactorSecretEncrypted) {
      throw new UnauthorizedException('Two-factor authentication is not enabled');
    }

    const secret = decryptSecret(twoFactor.twoFactorSecretEncrypted, config.jwtSecret);
    if (!verifyTotpCode(secret, code)) {
      throw new UnauthorizedException('Invalid verification code');
    }

    const recoveryCodes = this.generateRecoveryCodes();
    const recoveryCodeHashes = recoveryCodes.map((value) => this.hashRecoveryCode(value));

    await this.prisma.$executeRaw(
      Prisma.sql`
        UPDATE "users"
        SET "two_factor_recovery_codes_hash_json" = ${JSON.stringify(recoveryCodeHashes)}::jsonb
        WHERE "id" = CAST(${user.id} AS uuid)
      `,
    );

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Two Factor Recovery Codes Regenerated',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'alert',
        details: `Regenerated 2FA recovery codes for ${user.email}.`,
      },
    });

    return {
      success: true,
      recoveryCodes,
    };
  }

  async verifyTwoFactorLogin(challengeToken: string, code: string, ipAddress?: string) {
    const payload = await this.jwtService.verifyAsync<TwoFactorChallengePayload>(challengeToken, {
      secret: config.jwtSecret,
    });

    if (payload.purpose !== '2fa-login') {
      throw new UnauthorizedException('Invalid challenge token');
    }

    const user = await this.prisma.user.findUnique({
      where: { id: payload.sub },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
        sessionVersion: true,
      },
    });

    const twoFactor = user ? await this.getTwoFactorRecord(user.id) : null;

    if (
      !user ||
      user.status !== 'active' ||
      user.sessionVersion !== payload.ver ||
      user.email !== payload.email ||
      !twoFactor?.twoFactorEnabled ||
      !twoFactor.twoFactorSecretEncrypted
    ) {
      throw new UnauthorizedException('Invalid challenge token');
    }

    await this.assertRateLimit(TWO_FACTOR_LIMITER, user.email, ipAddress);

    const secret = decryptSecret(twoFactor.twoFactorSecretEncrypted, config.jwtSecret);
    const normalizedCode = this.normalizeRecoveryCode(code);
    const recoveryCodeHashes = this.readRecoveryCodeHashes(twoFactor);
    const isTotpValid = verifyTotpCode(secret, code);
    const matchedRecoveryCodeIndex = recoveryCodeHashes.findIndex(
      (candidate) => candidate === this.hashRecoveryCode(normalizedCode),
    );

    if (!isTotpValid && matchedRecoveryCodeIndex === -1) {
      await this.registerFailedAttempt(TWO_FACTOR_LIMITER, user.email, ipAddress);

      await this.prisma.auditLog.create({
        data: {
          actorUserId: user.id,
          actorName: user.name,
          actorEmail: user.email,
          actionType: 'Two Factor Login Failed',
          module: 'Auth Gateway',
          ipAddress: ipAddress || null,
          severity: 'alert',
          details: `Failed 2FA verification for ${user.email}.`,
        },
      });
      throw new UnauthorizedException('Invalid verification code');
    }

    if (!isTotpValid && matchedRecoveryCodeIndex >= 0) {
      recoveryCodeHashes.splice(matchedRecoveryCodeIndex, 1);
      await this.prisma.$executeRaw(
        Prisma.sql`
          UPDATE "users"
          SET "two_factor_recovery_codes_hash_json" = ${JSON.stringify(recoveryCodeHashes)}::jsonb
          WHERE "id" = CAST(${user.id} AS uuid)
        `,
      );

      await this.prisma.auditLog.create({
        data: {
          actorUserId: user.id,
          actorName: user.name,
          actorEmail: user.email,
          actionType: 'Two Factor Recovery Code Used',
          module: 'Auth Gateway',
          ipAddress: ipAddress || null,
          severity: 'alert',
          details: `Used a backup recovery code for ${user.email}.`,
        },
      });

      await this.sendSecurityNotificationSafely({
        to: user.email,
        name: user.name,
        subject: 'BizOne security alert: recovery code used',
        heading: 'Backup Recovery Code Used',
        intro: 'A backup recovery code was used to complete a BizOne admin login.',
        bullets: [
          `Account: ${user.email}`,
          `Time: ${formatSecurityTimestamp(new Date())}`,
          `IP address: ${ipAddress || 'Unavailable'}`,
          `Recovery codes remaining: ${recoveryCodeHashes.length}`,
        ],
        note: 'If this was not you, rotate your password and regenerate backup codes as soon as possible.',
      });
    }

    await this.clearFailedAttempts(TWO_FACTOR_LIMITER, user.email, ipAddress);

    const session = await this.issueSession(
      {
        sub: user.id,
        email: user.email,
        ver: user.sessionVersion,
      },
      user.id,
    );

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Two Factor Login Success',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Completed 2FA login for ${user.email}.`,
      },
    });

    await this.prisma.user.update({
      where: { id: user.id },
      data: { lastLoginAt: new Date() },
    });

    return {
      ...session,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        status: user.status,
      },
      recoveryCodesRemaining: recoveryCodeHashes.length,
    };
  }

  async requestPasswordReset(email: string, ipAddress?: string) {
    const normalizedEmail = normalizeEmail(email);
    await this.assertRateLimit(PASSWORD_RESET_LIMITER, normalizedEmail, ipAddress);

    const user = await this.prisma.user.findUnique({
      where: { email: normalizedEmail },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
      },
    });

    if (!user || user.status !== 'active') {
      await this.registerFailedAttempt(PASSWORD_RESET_LIMITER, normalizedEmail, ipAddress);
      return { success: true };
    }

    const token = this.generateToken();
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
    const tokenHash = this.hashToken(token);
    const resetUrl = `${config.frontendOrigin}/reset-password/${token}`;

    await this.prisma.user.update({
      where: { id: user.id },
      data: {
        passwordResetTokenHash: tokenHash,
        passwordResetTokenExpiresAt: expiresAt,
      },
    });

    try {
      await this.mailer.sendPasswordResetEmail({
        to: user.email,
        name: user.name,
        resetUrl,
      });
    } catch {
      // Keep the response generic to avoid account enumeration.
    }

    await this.registerFailedAttempt(PASSWORD_RESET_LIMITER, normalizedEmail, ipAddress);

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Password Reset Requested',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Password reset requested for ${user.email}.`,
      },
    });

    await this.sendSecurityNotificationSafely({
      to: user.email,
      name: user.name,
      subject: 'BizOne security alert: password reset completed',
      heading: 'Password Reset Completed',
      intro: 'Your BizOne admin password was changed successfully.',
      bullets: [
        `Account: ${user.email}`,
        `Time: ${formatSecurityTimestamp(new Date())}`,
        `IP address: ${ipAddress || 'Unavailable'}`,
      ],
      note: 'If you did not complete this password reset, secure the account immediately and contact an administrator.',
    });

    return { success: true };
  }

  async getPasswordResetToken(token: string) {
    const user = await this.findPasswordResetUser(token);
    return {
      email: user.email,
      name: user.name,
      expiresAt: user.passwordResetTokenExpiresAt,
    };
  }

  async completePasswordReset(token: string, password: string, ipAddress?: string) {
    if (!hasMinimumPasswordLength(password)) {
      throw new HttpException('Password must be at least 8 characters', HttpStatus.BAD_REQUEST);
    }

    const user = await this.findPasswordResetUser(token);
    const passwordHash = await hashPassword(password);

    await this.prisma.user.update({
      where: { id: user.id },
      data: {
        passwordHash,
        passwordResetTokenHash: null,
        passwordResetTokenExpiresAt: null,
        refreshTokenHash: null,
        refreshTokenExpiresAt: null,
        sessionVersion: {
          increment: 1,
        },
      },
    });

    await this.prisma.auditLog.create({
      data: {
        actorUserId: user.id,
        actorName: user.name,
        actorEmail: user.email,
        actionType: 'Password Reset Completed',
        module: 'Auth Gateway',
        ipAddress: ipAddress || null,
        severity: 'default',
        details: `Password reset completed for ${user.email}.`,
      },
    });

    return { success: true };
  }

  private async assertRateLimit(
    limiter: { scope: string; maxAttempts: number; windowMinutes: number; message: string },
    identity: string,
    ipAddress?: string,
  ) {
    const retryAfterMs = Math.max(
      await this.getRetryAfterMs(this.buildEmailKey(limiter.scope, identity), limiter.maxAttempts),
      ipAddress ? await this.getRetryAfterMs(this.buildIpKey(limiter.scope, ipAddress), limiter.maxAttempts) : 0,
    );

    if (retryAfterMs > 0) {
      const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
      throw new HttpException(
        `${limiter.message} Try again in ${retryAfterSeconds} seconds.`,
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }
  }

  private async registerFailedAttempt(
    limiter: { scope: string; windowMinutes: number },
    identity: string,
    ipAddress?: string,
  ) {
    await this.bumpAttempt(this.buildEmailKey(limiter.scope, identity), limiter.windowMinutes);
    if (ipAddress) {
      await this.bumpAttempt(this.buildIpKey(limiter.scope, ipAddress), limiter.windowMinutes);
    }
  }

  private async clearFailedAttempts(
    limiter: { scope: string },
    identity: string,
    ipAddress?: string,
  ) {
    await this.redisQueueService.deleteKey(this.buildEmailKey(limiter.scope, identity));
    if (ipAddress) {
      await this.redisQueueService.deleteKey(this.buildIpKey(limiter.scope, ipAddress));
    }
  }

  private async bumpAttempt(key: string, windowMinutes: number) {
    await this.redisQueueService.incrementCounter(key, windowMinutes * 60);
  }

  private async getRetryAfterMs(key: string, maxAttempts: number) {
    const count = await this.redisQueueService.getCounter(key);
    if (count < maxAttempts) {
      return 0;
    }

    const ttlSeconds = await this.redisQueueService.getTtlSeconds(key);
    if (ttlSeconds <= 0) {
      return 0;
    }

    return ttlSeconds * 1000;
  }

  private buildEmailKey(scope: string, identity: string) {
    return `auth:${scope}:email:${identity}`;
  }

  private buildIpKey(scope: string, ipAddress: string) {
    return `auth:${scope}:ip:${ipAddress}`;
  }

  private async recordFailedLogin(input: {
    userId?: string;
    name?: string;
    email: string;
    ipAddress?: string;
    reason: string;
  }) {
    await this.prisma.auditLog.create({
      data: {
        actorUserId: input.userId || null,
        actorName: input.name || input.email,
        actorEmail: input.email,
        actionType: 'Login Failed',
        module: 'Auth Gateway',
        ipAddress: input.ipAddress || null,
        severity: 'alert',
        details: `Failed login for ${input.email}. Reason: ${input.reason}.`,
        metadataJson: {
          reason: input.reason,
          email: input.email,
          ipAddress: input.ipAddress || null,
        } as Prisma.InputJsonValue,
      },
    });
  }

  private async findPasswordResetUser(token: string) {
    const user = await this.prisma.user.findFirst({
      where: {
        passwordResetTokenHash: this.hashToken(token),
      },
      select: {
        id: true,
        name: true,
        email: true,
        status: true,
        passwordResetTokenExpiresAt: true,
      },
    });

    if (!user || user.status !== 'active') {
      throw new UnauthorizedException('Invalid or expired reset link');
    }

    if (!user.passwordResetTokenExpiresAt || user.passwordResetTokenExpiresAt.getTime() <= Date.now()) {
      throw new UnauthorizedException('Invalid or expired reset link');
    }

    return user;
  }

  private async issueSession(payload: AuthenticatedUser, userId: string) {
    const access_token = await this.jwtService.signAsync(payload);
    const refresh_token = await this.jwtService.signAsync(payload, {
      secret: config.jwtRefreshSecret,
      expiresIn: config.jwtRefreshExpiresIn,
    });
    const refreshTokenExpiresAt = new Date(Date.now() + this.durationToMs(config.jwtRefreshExpiresIn));

    await this.prisma.user.update({
      where: { id: userId },
      data: {
        refreshTokenHash: this.hashToken(refresh_token),
        refreshTokenExpiresAt,
      },
    });

    return {
      access_token,
      refresh_token,
      expires_in: config.jwtExpiresIn,
      refresh_expires_in: config.jwtRefreshExpiresIn,
      access_token_max_age_seconds: Math.floor(this.durationToMs(config.jwtExpiresIn) / 1000),
      refresh_token_max_age_seconds: Math.floor(this.durationToMs(config.jwtRefreshExpiresIn) / 1000),
    };
  }

  private async verifyRefreshToken(token: string) {
    return this.jwtService.verifyAsync<AuthenticatedUser>(token, {
      secret: config.jwtRefreshSecret,
    });
  }

  private hashToken(token: string) {
    return createHash('sha256').update(token).digest('hex');
  }

  private generateToken() {
    return randomBytes(24).toString('hex');
  }

  private async getTwoFactorRecord(userId: string): Promise<TwoFactorRecord | null> {
    const rows = await this.prisma.$queryRaw<TwoFactorRecord[]>(
      Prisma.sql`
        SELECT
          "two_factor_enabled" as "twoFactorEnabled",
          "two_factor_secret_encrypted" as "twoFactorSecretEncrypted",
          "two_factor_pending_secret_encrypted" as "twoFactorPendingSecretEncrypted",
          "two_factor_recovery_codes_hash_json" as "twoFactorRecoveryCodesHashJson",
          "two_factor_confirmed_at" as "twoFactorConfirmedAt"
        FROM "users"
        WHERE "id" = CAST(${userId} AS uuid)
        LIMIT 1
      `,
    );

    return rows[0] || null;
  }

  private durationToMs(value: string) {
    const normalized = value.trim().toLowerCase();
    const match = normalized.match(/^(\d+)(s|m|h|d)$/);
    if (match) {
      const amount = Number(match[1]);
      const unit = match[2];
      if (unit === 's') return amount * 1000;
      if (unit === 'm') return amount * 60 * 1000;
      if (unit === 'h') return amount * 60 * 60 * 1000;
      return amount * 24 * 60 * 60 * 1000;
    }

    const numeric = Number(normalized);
    if (Number.isFinite(numeric) && numeric > 0) {
      return numeric * 1000;
    }

    throw new Error(`Unsupported duration format: ${value}`);
  }

  private generateRecoveryCodes(count = 8) {
    return Array.from({ length: count }, () => randomBytes(4).toString('hex').toUpperCase().match(/.{1,4}/g)?.join('-') || '');
  }

  private normalizeRecoveryCode(value: string) {
    return value.toUpperCase().replace(/[^A-Z0-9]/g, '');
  }

  private hashRecoveryCode(value: string) {
    return createHash('sha256').update(this.normalizeRecoveryCode(value)).digest('hex');
  }

  private readRecoveryCodeHashes(twoFactor: Pick<TwoFactorRecord, 'twoFactorRecoveryCodesHashJson'>) {
    const raw = twoFactor.twoFactorRecoveryCodesHashJson;
    if (!Array.isArray(raw)) {
      return [];
    }

    return raw.filter((value): value is string => typeof value === 'string' && value.length > 0);
  }

  private async sendSecurityNotificationSafely(input: {
    to: string;
    name: string;
    subject: string;
    heading: string;
    intro: string;
    bullets: string[];
    note?: string;
  }) {
    try {
      await this.mailer.sendSecurityNotificationEmail(input);
    } catch {
      // Avoid breaking the auth flow if SMTP is down.
    }
  }
}
