import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  GoneException,
  HttpException,
  HttpStatus,
  Injectable,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { randomBytes, randomInt, randomUUID } from 'crypto';
import { OAuth2Client } from 'google-auth-library';
import {
  compareHash,
  compareStoredHighEntropyValue,
  hashHighEntropyValue,
  hashValue,
} from '../../common/utils/hash.util';
import { EmailService } from '../email/email.service';
import { UsersService } from '../users/users.service';
import { ChangePasswordDto } from './dto/change-password.dto';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { GoogleTokenLoginDto } from './dto/google-token-login.dto';
import { LoginDto } from './dto/login.dto';
import { RegisterBasicDto } from './dto/register-basic.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { RegisterDto } from './dto/register.dto';
import { SendEmailVerificationDto } from './dto/send-email-verification.dto';
import { SignupCompleteDto } from './dto/signup-complete.dto';
import { SignupStartDto } from './dto/signup-start.dto';
import { SignupVerifyDto } from './dto/signup-verify.dto';
import { SuperAdminLoginDto } from './dto/super-admin-login.dto';
import { VerifyEmailDto } from './dto/verify-email.dto';
import { VerifyResetCodeDto } from './dto/verify-reset-code.dto';
import { AuthRepository } from './auth.repository';
import { AuthResult, TokenPair } from './types/token-pair.type';
import { DEFAULT_SUPERADMIN_PERMISSIONS } from '../superadmin/superadmin-permissions';
import { verifyTotp } from '../../common/utils/totp.util';

type UploadedImageFile = {
  mimetype?: string;
  size: number;
  buffer: Buffer;
  originalname?: string;
};

@Injectable()
export class AuthService {
  private readonly googleOAuthClient = new OAuth2Client();
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private readonly usersService: UsersService,
    private readonly authRepository: AuthRepository,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly emailService: EmailService,
  ) {}

  async register(dto: RegisterDto): Promise<{ message: string; email: string; debugCode?: string }> {
    if (dto.password !== dto.confirmPassword) {
      throw new BadRequestException('Password confirmation does not match');
    }

    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const passwordHash = await hashValue(dto.password, saltRounds);
    const generatedUsername = dto.username ?? (await this.generateUniqueUsernameFromEmail(dto.email));
    const resolvedName = dto.name ?? dto.stageName ?? generatedUsername;

    const { confirmPassword: _, ...registerPayload } = dto;
    const user = await this.usersService.create({
      ...registerPayload,
      name: resolvedName,
      username: generatedUsername,
      password: passwordHash,
    });
    const code = await this.issueEmailVerificationCode(user.id, user.email);
    const message = 'Registration successful. Verification code sent to email.';
    const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
    return nodeEnv !== 'production'
      ? { message, email: user.email, debugCode: code }
      : { message, email: user.email };
  }

  async registerBasic(dto: RegisterBasicDto): Promise<{ message: string; email: string; debugCode?: string }> {
    if (dto.password !== dto.confirmPassword) {
      throw new BadRequestException('Password confirmation does not match');
    }

    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const passwordHash = await hashValue(dto.password, saltRounds);
    const generatedUsername = await this.generateUniqueUsernameFromEmail(dto.email);

    const user = await this.usersService.create({
      name: generatedUsername,
      username: generatedUsername,
      email: dto.email,
      password: passwordHash,
    });
    const code = await this.issueEmailVerificationCode(user.id, user.email);
    const message = 'Registration successful. Verification code sent to email.';
    const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
    return nodeEnv !== 'production'
      ? { message, email: user.email, debugCode: code }
      : { message, email: user.email };
  }

  async signupStart(
    dto: SignupStartDto,
  ): Promise<{ message: string; email: string; signupId: string; debugCode?: string }> {
    if (dto.password !== dto.confirmPassword) {
      throw new BadRequestException('Password confirmation does not match');
    }

    const normalizedEmail = dto.email.toLowerCase();
    await this.assertEmailNotUsed(normalizedEmail);

    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const [passwordHash, code] = await Promise.all([
      hashValue(dto.password, saltRounds),
      Promise.resolve(this.generateResetCode()),
    ]);
    const codeHash = await hashValue(code, saltRounds);
    const ttlMinutes = this.getSignupDraftTtlMinutes();
    const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000);

    const draft = await this.authRepository.upsertSignupDraft({
      email: normalizedEmail,
      passwordHash,
      codeHash,
      codeExpiresAt: expiresAt,
      expiresAt,
    });

    await this.emailService.sendVerificationCode(normalizedEmail, code, ttlMinutes);
    this.logger.log(`Signup draft verification code generated for ${normalizedEmail}`);

    const response = {
      message: 'Verification code sent',
      email: normalizedEmail,
      signupId: draft.id,
    };
    const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
    return nodeEnv !== 'production' ? { ...response, debugCode: code } : response;
  }

  async signupVerify(dto: SignupVerifyDto): Promise<{ message: string; signupToken: string }> {
    const normalizedEmail = dto.email.toLowerCase();
    const draft = await this.authRepository.findSignupDraftWithSecrets(dto.signupId);
    this.assertUsableSignupDraft(draft, normalizedEmail);

    const maxAttempts = this.getSignupMaxAttempts();
    if (draft.attempts >= maxAttempts) {
      throw new HttpException('Too many verification attempts', HttpStatus.TOO_MANY_REQUESTS);
    }

    const isMatch = await compareHash(dto.code, draft.codeHash);
    if (!isMatch) {
      await this.authRepository.incrementSignupDraftAttempts(draft.id);
      if (draft.attempts + 1 >= maxAttempts) {
        throw new HttpException('Too many verification attempts', HttpStatus.TOO_MANY_REQUESTS);
      }
      throw new BadRequestException('Invalid verification code');
    }

    const signupToken = randomBytes(32).toString('base64url');
    const signupTokenHash = hashHighEntropyValue(signupToken, this.getSignupTokenHashSecret());
    const signupTokenExpiresAt = new Date(Date.now() + this.getSignupTokenTtlMinutes() * 60 * 1000);
    await this.authRepository.markSignupDraftVerified(draft.id, signupTokenHash, signupTokenExpiresAt);

    return { message: 'Email verified', signupToken };
  }

  async signupComplete(dto: SignupCompleteDto, avatarFile?: UploadedImageFile): Promise<AuthResult & { message: string }> {
    const signupTokenHash = hashHighEntropyValue(dto.signupToken, this.getSignupTokenHashSecret());
    const draft = await this.authRepository.findSignupDraftByTokenHash(signupTokenHash);
    this.assertCompletableSignupDraft(draft);

    await this.assertEmailNotUsed(draft.email);

    const username = dto.username?.trim()
      ? dto.username.trim()
      : await this.generateUniqueUsernameFromEmail(draft.email);
    const createdUser = await this.usersService.createWithOptionalAvatarFile(
      {
        name: dto.name,
        stageName: dto.stageName,
        username,
        email: draft.email,
        password: draft.passwordHash,
        bio: dto.bio,
        location: dto.location,
        latitude: dto.latitude,
        longitude: dto.longitude,
        isPrivate: dto.isPrivate,
        musicRoles: dto.musicRoles,
        musicGenres: dto.musicGenres,
        experienceLevel: dto.experienceLevel,
        favoriteInstruments: dto.favoriteInstruments,
        favoriteMaqamat: dto.favoriteMaqamat,
        isEmailVerified: true,
      },
      avatarFile,
    );

    try {
      const tokens = await this.generateAndStoreTokenPair(
        createdUser.id,
        createdUser.username,
        createdUser.role ?? 'user',
      );
      await this.authRepository.markSignupDraftConsumed(draft.id);
      const safeUser = await this.usersService.findByIdOrFail(createdUser.id);
      return {
        message: 'Account created successfully',
        ...tokens,
        user: safeUser.toObject() as unknown as Record<string, unknown>,
      };
    } catch (error) {
      await this.usersService.rollbackSignupCreatedUser(createdUser.id, createdUser.avatar);
      throw error;
    }
  }

  async login(dto: LoginDto): Promise<AuthResult> {
    const user = await this.usersService.findByEmailWithPassword(dto.email);
    if (!user || !user.password) {
      throw new UnauthorizedException('Invalid credentials');
    }
    if (user.isDisabled) {
      throw new ForbiddenException('Account is disabled');
    }

    const isMatch = await compareHash(dto.password, user.password);
    if (!isMatch) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const tokens = await this.generateAndStoreTokenPair(user.id, user.username, user.role ?? 'user');
    const safeUser = await this.usersService.findByIdOrFail(user.id);
    return { ...tokens, user: safeUser.toObject() as unknown as Record<string, unknown> };
  }

  async sendEmailVerification(
    dto: SendEmailVerificationDto,
  ): Promise<{ message: string; debugCode?: string }> {
    const normalizedEmail = dto.email.toLowerCase();
    const user = await this.usersService.findByEmail(normalizedEmail);
    const message = 'If this email exists, a verification code was sent';
    if (!user || user.isDisabled) {
      return { message };
    }
    if (user.isEmailVerified) {
      return { message: 'Email is already verified' };
    }

    const code = await this.issueEmailVerificationCode(user.id, normalizedEmail);
    const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
    return nodeEnv !== 'production' ? { message, debugCode: code } : { message };
  }

  async verifyEmail(dto: VerifyEmailDto): Promise<{ message: string }> {
    const normalizedEmail = dto.email.toLowerCase();
    const user = await this.usersService.findByEmail(normalizedEmail);
    if (!user || user.isDisabled) {
      throw new UnauthorizedException('Invalid or expired verification code');
    }
    if (user.isEmailVerified) {
      return { message: 'Email is already verified' };
    }

    const codeRecord = await this.authRepository.findLatestActiveEmailVerificationCode(user.id);
    if (!codeRecord) {
      throw new UnauthorizedException('Invalid or expired verification code');
    }

    const maxAttempts = this.configService.get<number>('emailVerification.maxAttempts', { infer: true });
    if (codeRecord.attempts >= maxAttempts) {
      await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id);
      throw new UnauthorizedException('Verification code attempts exceeded');
    }

    const isMatch = await compareHash(dto.code, codeRecord.codeHash);
    if (!isMatch) {
      await this.authRepository.incrementEmailVerificationAttempts(codeRecord.id);
      const attemptsAfter = codeRecord.attempts + 1;
      if (attemptsAfter >= maxAttempts) {
        await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id);
      }
      throw new UnauthorizedException('Invalid or expired verification code');
    }

    await Promise.all([
      this.usersService.markEmailVerified(user.id),
      this.authRepository.markEmailVerificationCodeUsed(codeRecord.id),
      this.authRepository.markAllEmailVerificationCodesUsedByUser(user.id),
    ]);

    return { message: 'Email verified successfully' };
  }

  async refresh(dto: RefreshTokenDto): Promise<AuthResult> {
    const decoded = this.jwtService.verify<{ sub: string; username: string; tokenType: string; jti?: string }>(
      dto.refreshToken,
      {
        secret: this.configService.get<string>('jwt.refreshSecret', { infer: true }),
      },
    );

    if (decoded.tokenType !== 'refresh' || !decoded.jti) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    const tokenRecord = await this.authRepository.findActiveUserTokenByJti(decoded.sub, decoded.jti);
    if (!tokenRecord) {
      await this.authRepository.markCompromisedAndRevokeAll(decoded.sub);
      throw new UnauthorizedException('Refresh token reuse detected');
    }

    const isMatch = await compareStoredHighEntropyValue(
      dto.refreshToken,
      tokenRecord.tokenHash,
      this.getRefreshTokenHashSecret(),
    );
    if (!isMatch) {
      await this.authRepository.markCompromisedAndRevokeAll(decoded.sub);
      throw new UnauthorizedException('Refresh token reuse detected');
    }

    const safeUser = await this.usersService.findByIdOrFail(decoded.sub);
    if (safeUser.isDisabled) {
      throw new ForbiddenException('Account is disabled');
    }

    const consumed = await this.authRepository.consumeUserTokenByJti(decoded.sub, decoded.jti);
    if (!consumed) {
      await this.authRepository.markCompromisedAndRevokeAll(decoded.sub);
      throw new UnauthorizedException('Refresh token reuse detected');
    }
    const authTokens = await this.generateAndStoreTokenPair(
      decoded.sub,
      safeUser.username,
      safeUser.role,
    );
    return { ...authTokens, user: safeUser.toObject() as unknown as Record<string, unknown> };
  }

  async logout(dto: RefreshTokenDto): Promise<void> {
    try {
      const decoded = this.jwtService.verify<{ sub: string }>(dto.refreshToken, {
        secret: this.configService.get<string>('jwt.refreshSecret', { infer: true }),
      });
      await this.authRepository.revokeAllUserTokens(decoded.sub);
      await this.authRepository.removeExpiredAndRevoked(decoded.sub);
    } catch {
      throw new BadRequestException('Invalid refresh token');
    }
  }

  async changePassword(userId: string, dto: ChangePasswordDto): Promise<{ message: string }> {
    const user = await this.usersService.findByIdWithPassword(userId);
    if (!user || !user.password) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const currentPasswordMatches = await compareHash(dto.currentPassword, user.password);
    if (!currentPasswordMatches) {
      throw new UnauthorizedException('Current password is incorrect');
    }

    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const passwordHash = await hashValue(dto.newPassword, saltRounds);
    await this.usersService.updatePassword(userId, passwordHash);
    await this.authRepository.revokeAllUserTokens(userId);
    await this.authRepository.removeExpiredAndRevoked(userId);

    return { message: 'Password changed successfully' };
  }

  async loginWithGoogle(googleUser: {
    googleId: string;
    email: string;
    name: string;
    avatar?: string;
  }): Promise<AuthResult> {
    let user = await this.usersService.findByGoogleId(googleUser.googleId);

    if (!user) {
      user = await this.usersService.findByEmail(googleUser.email);
    }

    if (!user) {
      const generatedUsername = await this.generateUniqueUsernameFromEmail(googleUser.email);
      const randomPassword = randomBytes(24).toString('hex');
      const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
      const passwordHash = await hashValue(randomPassword, saltRounds);

      user = await this.usersService.create({
        name: googleUser.name,
        username: generatedUsername,
        email: googleUser.email,
        password: passwordHash,
        avatar: googleUser.avatar ?? '',
        isVerified: false,
        isEmailVerified: true,
      });
    }

    if (!user.googleId) {
      user = await this.usersService.linkGoogleAccount(user.id, googleUser.googleId, googleUser.avatar);
    }

    if (user.isDisabled) {
      throw new ForbiddenException('Account is disabled');
    }

    const tokens = await this.generateAndStoreTokenPair(user.id, user.username, user.role ?? 'user');
    const safeUser = await this.usersService.findByIdOrFail(user.id);
    return { ...tokens, user: safeUser.toObject() as unknown as Record<string, unknown> };
  }

  async loginWithGoogleIdToken(dto: GoogleTokenLoginDto): Promise<AuthResult> {
    const clientId = this.getGoogleClientId();
    if (!clientId) {
      throw new BadRequestException('Google client is not configured');
    }

    let payload:
      | {
          sub?: string;
          email?: string;
          email_verified?: boolean;
          name?: string;
          picture?: string;
        }
      | undefined;
    try {
      const ticket = await this.googleOAuthClient.verifyIdToken({
        idToken: dto.idToken,
        audience: clientId,
      });
      payload = ticket.getPayload();
    } catch (error) {
      this.logger.warn(`Google id token verification failed: ${this.getErrorMessage(error)}`);
      throw new UnauthorizedException('Invalid Google id token');
    }

    if (!payload?.sub || !payload.email || payload.email_verified !== true) {
      throw new UnauthorizedException('Google account data is invalid');
    }

    return this.loginWithGoogle({
      googleId: payload.sub,
      email: payload.email.toLowerCase(),
      name: payload.name ?? payload.email.split('@')[0],
      avatar: payload.picture,
    });
  }

  async superAdminLogin(dto: SuperAdminLoginDto): Promise<{
    accessToken: string;
    refreshToken: string;
    superAdmin: { email: string };
  }> {
    const configuredEmail = this.configService.get<string>('superAdmin.email', { infer: true });
    const configuredPassword = this.configService.get<string>('superAdmin.password', { infer: true });
    const configuredPasswordHash = this.configService.get<string>('superAdmin.passwordHash', { infer: true });
    const totpSecret = this.configService.get<string>('superAdmin.totpSecret', { infer: true }) ?? '';
    const passwordMatches = configuredPasswordHash
      ? await compareHash(dto.password, configuredPasswordHash)
      : !!configuredPassword && dto.password === configuredPassword;

    if (
      !configuredEmail ||
      dto.email.toLowerCase() !== configuredEmail.toLowerCase() ||
      !passwordMatches
    ) {
      throw new UnauthorizedException('Invalid superadmin credentials');
    }

    if (totpSecret && (!dto.otpCode || !verifyTotp(totpSecret, dto.otpCode))) {
      throw new UnauthorizedException('Invalid superadmin verification code');
    }

    const tokens = await this.generateAndStoreSuperAdminTokenPair(configuredEmail);
    return { ...tokens, superAdmin: { email: configuredEmail } };
  }

  async superAdminRefresh(dto: RefreshTokenDto): Promise<{
    accessToken: string;
    refreshToken: string;
    superAdmin: { email: string };
  }> {
    const decoded = this.jwtService.verify<{ email: string; tokenType: string; jti?: string }>(
      dto.refreshToken,
      {
        secret: this.configService.get<string>('superAdmin.refreshSecret', { infer: true }),
      },
    );

    if (decoded.tokenType !== 'superadmin_refresh' || !decoded.email || !decoded.jti) {
      throw new UnauthorizedException('Invalid superadmin refresh token');
    }

    const tokenRecord = await this.authRepository.findActiveSuperAdminTokenByJti(
      decoded.email,
      decoded.jti,
    );
    if (!tokenRecord) {
      await this.authRepository.revokeAllSuperAdminTokens(decoded.email);
      throw new UnauthorizedException('Superadmin refresh token reuse detected');
    }

    const isMatch = await compareStoredHighEntropyValue(
      dto.refreshToken,
      tokenRecord.tokenHash,
      this.getRefreshTokenHashSecret(),
    );
    if (!isMatch) {
      await this.authRepository.revokeAllSuperAdminTokens(decoded.email);
      throw new UnauthorizedException('Superadmin refresh token reuse detected');
    }

    const consumed = await this.authRepository.consumeSuperAdminTokenByJti(decoded.email, decoded.jti);
    if (!consumed) {
      await this.authRepository.revokeAllSuperAdminTokens(decoded.email);
      throw new UnauthorizedException('Superadmin refresh token reuse detected');
    }
    const tokens = await this.generateAndStoreSuperAdminTokenPair(decoded.email);
    return { ...tokens, superAdmin: { email: decoded.email } };
  }

  async superAdminLogout(dto: RefreshTokenDto): Promise<void> {
    try {
      const decoded = this.jwtService.verify<{ email: string; jti?: string }>(dto.refreshToken, {
        secret: this.configService.get<string>('superAdmin.refreshSecret', { infer: true }),
      });
      if (decoded.jti) {
        await this.authRepository.revokeSuperAdminTokenByJti(decoded.email, decoded.jti);
      } else {
        await this.authRepository.revokeAllSuperAdminTokens(decoded.email);
      }
      await this.authRepository.removeExpiredAndRevokedSuperAdmin(decoded.email);
    } catch {
      throw new BadRequestException('Invalid superadmin refresh token');
    }
  }

  async listUserSessions(userId: string): Promise<{ items: Array<{ jti: string; createdAt: Date; expiresAt: Date }> }> {
    const sessions = await this.authRepository.listUserSessions(userId);
    return {
      items: sessions.map((s) => ({
        jti: s.jti,
        createdAt: (s as unknown as { createdAt: Date }).createdAt,
        expiresAt: s.expiresAt,
      })),
    };
  }

  async revokeUserSession(userId: string, jti: string): Promise<void> {
    await this.authRepository.revokeUserTokenByJti(userId, jti);
  }

  async listSuperAdminSessions(
    adminEmail: string,
  ): Promise<{ items: Array<{ id: string; jti: string; createdAt: Date; expiresAt: Date }> }> {
    const sessions = await this.authRepository.listSuperAdminSessions(adminEmail);
    return {
      items: sessions.map((session) => ({
        id: session.id,
        jti: session.jti,
        createdAt: (session as unknown as { createdAt: Date }).createdAt,
        expiresAt: session.expiresAt,
      })),
    };
  }

  async revokeSuperAdminSession(adminEmail: string, sessionId: string): Promise<void> {
    const revoked = await this.authRepository.revokeSuperAdminSessionById(adminEmail, sessionId);
    if (!revoked) {
      throw new BadRequestException('Superadmin session not found');
    }
  }

  async forgotPassword(dto: ForgotPasswordDto): Promise<{ message: string; debugCode?: string }> {
    const normalizedEmail = dto.email.toLowerCase();
    const user = await this.usersService.findByEmail(normalizedEmail);
    const message = 'If this email exists, a reset code was sent';

    if (!user || user.isDisabled) {
      return { message };
    }

    const code = this.generateResetCode();
    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const codeHash = await hashValue(code, saltRounds);
    const expiresMinutes = this.configService.get<number>('passwordReset.codeExpiresMinutes', {
      infer: true,
    });
    const expiresAt = new Date(Date.now() + expiresMinutes * 60 * 1000);

    await this.authRepository.invalidateActivePasswordResetCodes(user.id);
    await this.authRepository.createPasswordResetCode(user.id, codeHash, expiresAt);

    await this.emailService.sendPasswordResetCode(normalizedEmail, code, expiresMinutes);
    this.logger.log(`Password reset code generated for ${normalizedEmail}`);

    const nodeEnv = this.configService.get<string>('nodeEnv', { infer: true });
    if (nodeEnv !== 'production') {
      return { message, debugCode: code };
    }

    return { message };
  }

  async verifyResetCode(
    dto: VerifyResetCodeDto,
  ): Promise<{ resetToken: string; expiresIn: string }> {
    const normalizedEmail = dto.email.toLowerCase();
    const user = await this.usersService.findByEmail(normalizedEmail);
    if (!user || user.isDisabled) {
      throw new UnauthorizedException('Invalid or expired reset code');
    }

    const codeRecord = await this.authRepository.findLatestActivePasswordResetCode(user.id);
    if (!codeRecord) {
      throw new UnauthorizedException('Invalid or expired reset code');
    }

    const maxAttempts = this.configService.get<number>('passwordReset.maxAttempts', { infer: true });
    if (codeRecord.attempts >= maxAttempts) {
      await this.authRepository.markPasswordResetCodeUsed(codeRecord.id);
      throw new UnauthorizedException('Reset code attempts exceeded');
    }

    const isMatch = await compareHash(dto.code, codeRecord.codeHash);
    if (!isMatch) {
      await this.authRepository.incrementPasswordResetAttempts(codeRecord.id);
      const attemptsAfter = codeRecord.attempts + 1;
      if (attemptsAfter >= maxAttempts) {
        await this.authRepository.markPasswordResetCodeUsed(codeRecord.id);
      }
      throw new UnauthorizedException('Invalid or expired reset code');
    }

    await this.authRepository.markPasswordResetCodeVerified(codeRecord.id);
    const resetTokenExpiresIn =
      this.configService.get<string>('passwordReset.tokenExpiresIn', {
        infer: true,
      }) ?? '15m';
    const resetToken = await this.jwtService.signAsync(
      { sub: user.id, tokenType: 'password_reset', prcId: codeRecord.id },
      {
        secret: this.configService.get<string>('passwordReset.tokenSecret', { infer: true }),
        expiresIn: resetTokenExpiresIn as JwtSignOptions['expiresIn'],
      },
    );

    return {
      resetToken,
      expiresIn: resetTokenExpiresIn,
    };
  }

  async resetPassword(dto: ResetPasswordDto): Promise<{ message: string }> {
    if (dto.newPassword !== dto.confirmPassword) {
      throw new BadRequestException('Password confirmation does not match');
    }

    let decoded: { sub: string; tokenType: string; prcId: string };
    try {
      decoded = this.jwtService.verify(dto.resetToken, {
        secret: this.configService.get<string>('passwordReset.tokenSecret', { infer: true }),
      });
    } catch {
      throw new UnauthorizedException('Invalid or expired reset token');
    }

    if (decoded.tokenType !== 'password_reset' || !decoded.prcId || !decoded.sub) {
      throw new UnauthorizedException('Invalid or expired reset token');
    }

    const codeRecord = await this.authRepository.consumeValidVerifiedPasswordResetCode(
      decoded.prcId,
      decoded.sub,
    );
    if (!codeRecord) {
      throw new UnauthorizedException('Invalid or expired reset token');
    }

    const user = await this.usersService.findByIdOrFail(decoded.sub);
    if (user.isDisabled) {
      throw new ForbiddenException('Account is disabled');
    }

    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const passwordHash = await hashValue(dto.newPassword, saltRounds);
    await this.usersService.updatePassword(decoded.sub, passwordHash);

    await this.authRepository.markPasswordResetCodeUsedByUser(decoded.sub);
    await this.authRepository.revokeAllUserTokens(decoded.sub);
    await this.authRepository.removeExpiredAndRevoked(decoded.sub);

    return { message: 'Password reset successfully' };
  }

  private async generateAndStoreTokenPair(
    userId: string,
    username: string,
    role: string,
  ): Promise<TokenPair> {
    const refreshJti = randomUUID();
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        { sub: userId, username, role, tokenType: 'access' },
        {
          secret: this.configService.get<string>('jwt.accessSecret', { infer: true }),
          expiresIn: this.configService.get<string>('jwt.accessExpiresIn', { infer: true }) as JwtSignOptions['expiresIn'],
        },
      ),
      this.jwtService.signAsync(
        { sub: userId, username, role, tokenType: 'refresh', jti: refreshJti },
        {
          secret: this.configService.get<string>('jwt.refreshSecret', { infer: true }),
          expiresIn: this.configService.get<string>('jwt.refreshExpiresIn', { infer: true }) as JwtSignOptions['expiresIn'],
        },
      ),
    ]);

    const tokenHash = hashHighEntropyValue(refreshToken, this.getRefreshTokenHashSecret());

    const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn', {
      infer: true,
    });
    const refreshExpiresInMs = this.parseExpiresInToMs(refreshExpiresIn ?? '30d');

    await this.authRepository.createRefreshToken(userId, refreshJti, tokenHash, new Date(Date.now() + refreshExpiresInMs));

    return { accessToken, refreshToken };
  }

  private async generateAndStoreSuperAdminTokenPair(adminEmail: string): Promise<TokenPair> {
    const permissions = this.getSuperAdminPermissions();
    const refreshJti = randomUUID();
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        {
          sub: 'superadmin',
          username: 'superadmin',
          email: adminEmail.toLowerCase(),
          role: 'superadmin',
          tokenType: 'superadmin_access',
          permissions,
        },
        {
          secret: this.configService.get<string>('superAdmin.accessSecret', { infer: true }),
          expiresIn: this.configService.get<string>('superAdmin.accessExpiresIn', { infer: true }) as JwtSignOptions['expiresIn'],
        },
      ),
      this.jwtService.signAsync(
        {
          sub: 'superadmin',
          username: 'superadmin',
          email: adminEmail.toLowerCase(),
          role: 'superadmin',
          tokenType: 'superadmin_refresh',
          jti: refreshJti,
        },
        {
          secret: this.configService.get<string>('superAdmin.refreshSecret', { infer: true }),
          expiresIn: this.configService.get<string>('superAdmin.refreshExpiresIn', { infer: true }) as JwtSignOptions['expiresIn'],
        },
      ),
    ]);

    const tokenHash = hashHighEntropyValue(refreshToken, this.getRefreshTokenHashSecret());
    const refreshExpiresIn = this.configService.get<string>('superAdmin.refreshExpiresIn', {
      infer: true,
    });
    const refreshExpiresInMs = this.parseExpiresInToMs(refreshExpiresIn ?? '30d');

    await this.authRepository.createSuperAdminRefreshToken(
      adminEmail,
      refreshJti,
      tokenHash,
      new Date(Date.now() + refreshExpiresInMs),
    );

    return { accessToken, refreshToken };
  }

  private parseExpiresInToMs(expiresIn: string): number {
    const regex = /^(\d+)([smhd])$/;
    const match = expiresIn.match(regex);
    if (!match) {
      return 30 * 24 * 60 * 60 * 1000;
    }

    const value = Number(match[1]);
    const unit = match[2];

    const multipliers: Record<string, number> = {
      s: 1000,
      m: 60 * 1000,
      h: 60 * 60 * 1000,
      d: 24 * 60 * 60 * 1000,
    };

    return value * multipliers[unit];
  }

  private async generateUniqueUsernameFromEmail(email: string): Promise<string> {
    const base = email.split('@')[0].replace(/[^a-zA-Z0-9_.]/g, '').toLowerCase() || 'user';
    let candidate = base.slice(0, 24);
    let counter = 1;

    while (await this.usersService.findByUsername(candidate)) {
      const suffix = `_${counter}`;
      const maxBaseLength = 30 - suffix.length;
      candidate = `${base.slice(0, Math.max(1, maxBaseLength))}${suffix}`;
      counter += 1;
    }

    return candidate;
  }

  private generateResetCode(): string {
    return String(randomInt(100000, 1000000));
  }

  private getRefreshTokenHashSecret(): string {
    return (
      this.configService.get<string>('security.refreshTokenHashSecret', { infer: true }) ??
      this.configService.get<string>('jwt.refreshSecret', { infer: true }) ??
      ''
    );
  }

  private getSignupTokenHashSecret(): string {
    return this.getRefreshTokenHashSecret();
  }

  private getSignupDraftTtlMinutes(): number {
    const configured = this.configService.get<number>('signup.draftTtlMinutes', { infer: true });
    return this.clampMinutes(configured ?? 20, 15, 30);
  }

  private getSignupTokenTtlMinutes(): number {
    const configured = this.configService.get<number>('signup.tokenTtlMinutes', { infer: true });
    return this.clampMinutes(configured ?? 15, 5, 30);
  }

  private getSignupMaxAttempts(): number {
    return this.configService.get<number>('emailVerification.maxAttempts', { infer: true }) ?? 5;
  }

  private clampMinutes(value: number, min: number, max: number): number {
    if (!Number.isFinite(value)) {
      return min;
    }

    return Math.min(max, Math.max(min, Math.floor(value)));
  }

  private async assertEmailNotUsed(email: string): Promise<void> {
    const existingUser = await this.usersService.findByEmail(email);
    if (existingUser) {
      throw new ConflictException('Email already used');
    }
  }

  private assertUsableSignupDraft(
    draft: Awaited<ReturnType<AuthRepository['findSignupDraftWithSecrets']>>,
    email: string,
  ): asserts draft is NonNullable<typeof draft> {
    if (!draft || draft.consumed || draft.email !== email) {
      throw new BadRequestException('Invalid signup draft');
    }
    if (draft.expiresAt <= new Date() || draft.codeExpiresAt <= new Date()) {
      throw new GoneException('Signup draft expired');
    }
  }

  private assertCompletableSignupDraft(
    draft: Awaited<ReturnType<AuthRepository['findSignupDraftByTokenHash']>>,
  ): asserts draft is NonNullable<typeof draft> {
    if (!draft || !draft.verifiedAt || !draft.signupTokenExpiresAt) {
      throw new UnauthorizedException('Invalid or expired signup token');
    }
    if (draft.expiresAt <= new Date() || draft.signupTokenExpiresAt <= new Date()) {
      throw new UnauthorizedException('Invalid or expired signup token');
    }
  }

  private getSuperAdminPermissions(): string[] {
    return [...DEFAULT_SUPERADMIN_PERMISSIONS];
  }

  private getGoogleClientId(): string {
    return (this.configService.get<string>('google.clientId', { infer: true }) ?? '').trim();
  }

  private getErrorMessage(error: unknown): string {
    return error instanceof Error ? error.message : String(error);
  }

  private maskToken(token: string): string {
    if (token.length <= 16) {
      return '[redacted]';
    }

    return `${token.slice(0, 8)}...${token.slice(-8)}`;
  }

  private async issueEmailVerificationCode(userId: string, email: string): Promise<string> {
    const code = this.generateResetCode();
    const saltRounds = this.configService.get<number>('security.bcryptSaltRounds', { infer: true });
    const codeHash = await hashValue(code, saltRounds);
    const expiresMinutes = this.configService.get<number>('emailVerification.codeExpiresMinutes', {
      infer: true,
    });
    const expiresAt = new Date(Date.now() + expiresMinutes * 60 * 1000);

    await this.authRepository.invalidateActiveEmailVerificationCodes(userId);
    await this.authRepository.createEmailVerificationCode(userId, codeHash, expiresAt);

    await this.emailService.sendVerificationCode(email, code, expiresMinutes);
    this.logger.log(`Email verification code generated for ${email}`);
    return code;
  }
}
