import {
  Injectable,
  HttpStatus,
  HttpException,
  ConflictException,
  NotFoundException,
  UnauthorizedException,
  BadRequestException,
} from '@nestjs/common';
import { CreateUserDto, LoginDto, ResetPasswordDto, SocialLoginDto } from './auth.dto';
import { Kysely, NoResultError, sql } from 'kysely';
import { DB } from 'src/database/database.type';
import { DeviceTypePayload } from '../devices/device.dto';
import { AuthHelper } from '../../utils/auth.helpers';
import { Logger } from 'pino-nestjs';
import { DeviceService } from '../devices/device.service';
import { MailService } from '../mail/mail.service';
import { WELCOME_CREDITS } from '@/config/constants';
import { ReferralsService } from '../referrals/referrals.service';
import { BillingService } from '../billing/billing.service';
import { NotificationsService } from '../notifications/notifications.service';
import { containsDisallowedUserContent, MODERATION_REJECTION_MESSAGE } from '@/utils/content-moderation';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { OAuth2Client } from 'google-auth-library';

type SocialProvider = 'google' | 'apple';

type VerifiedSocialProfile = {
  provider: SocialProvider;
  providerSubject: string;
  email: string | null;
  emailVerified: boolean;
  displayName: string | null;
};

type SessionUser = {
  id: string;
  email: string;
  display_name: string;
  phone: string | null;
  role: string;
  premium_until: Date | string | null;
  premium_source: string | null;
  is_premium: boolean;
  email_verified_at: Date | string | null;
  is_email_verified: boolean;
  basic_credits: number;
  additional_credits: number;
  total_credits: number;
};

const APPLE_JWKS_URL = 'https://appleid.apple.com/auth/keys';
const APPLE_ISSUER = 'https://appleid.apple.com';

function parseCsvEnv(value?: string): string[] {
  return String(value || '')
    .split(',')
    .map((entry) => entry.trim())
    .filter(Boolean);
}

function uniqueValues(values: Array<string | undefined>): string[] {
  return Array.from(new Set(values.map((value) => String(value || '').trim()).filter(Boolean)));
}

function normalizeEmail(email?: string | null): string | null {
  const normalized = String(email || '')
    .trim()
    .toLowerCase();
  return normalized || null;
}

const SESSION_USER_COLUMNS = [
  'id',
  'email',
  'display_name',
  'phone',
  'role',
  'premium_until',
  'premium_source',
  'email_verified_at',
  sql<boolean>`premium_until > NOW()`.as('is_premium'),
  sql<boolean>`email_verified_at IS NOT NULL`.as('is_email_verified'),
  'basic_credits',
  'additional_credits',
] as const;

@Injectable()
export class AuthService {
  private readonly googleOAuthClient = new OAuth2Client();
  private appleJwksCache: { fetchedAt: number; keys: any[] } | null = null;

  constructor(
    private db: Kysely<DB>,
    private readonly authHelper: AuthHelper,
    private logger: Logger,
    private readonly deviceService: DeviceService,
    private readonly mailService: MailService,
    private readonly referralsService: ReferralsService,
    private readonly billingService: BillingService,
    private readonly notificationsService: NotificationsService,
  ) {}

  private generateReferralCode(): string {
    const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    let code = '';
    for (let i = 0; i < 6; i++) {
      code += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return code;
  }

  private generateOtp(): string {
    return Math.floor(100000 + Math.random() * 900000).toString();
  }

  private getDevicePayload(data: any): DeviceTypePayload | undefined {
    if (!data || typeof data !== 'object') return undefined;
    if (!data.device || typeof data.device !== 'object') return undefined;
    return data.device as DeviceTypePayload;
  }

  private toSessionUser(row: any): SessionUser {
    const basicCredits = Number(row.basic_credits || 0);
    const additionalCredits = Number(row.additional_credits || 0);
    return {
      id: row.id,
      email: row.email,
      display_name: row.display_name,
      phone: row.phone,
      role: row.role || 'user',
      premium_until: row.premium_until,
      premium_source: row.premium_source,
      is_premium: !!row.is_premium,
      email_verified_at: row.email_verified_at,
      is_email_verified: !!row.is_email_verified,
      basic_credits: basicCredits,
      additional_credits: additionalCredits,
      total_credits: basicCredits + additionalCredits,
    };
  }

  private getSessionToken(user: { id: string; email: string; display_name: string; phone: string | null; role?: string }) {
    return this.authHelper.generateToken({
      id: user.id,
      email: user.email,
      display_name: user.display_name,
      phone: user.phone,
      role: user.role || 'user',
    });
  }

  private async getSessionUserById(userId: string): Promise<SessionUser | null> {
    const row = await this.db
      .selectFrom('users')
      .select(SESSION_USER_COLUMNS)
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();
    return row ? this.toSessionUser(row) : null;
  }

  private async getSessionUserByVerifiedEmail(email: string): Promise<SessionUser | null> {
    const row = await this.db
      .selectFrom('users')
      .select(SESSION_USER_COLUMNS)
      .where('email', '=', email)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();
    return row ? this.toSessionUser(row) : null;
  }

  private async findUserBySocialIdentity(profile: VerifiedSocialProfile): Promise<SessionUser | null> {
    const subjectColumn = profile.provider === 'google' ? 'google_subject' : 'apple_subject';
    const result = await sql<any>`
      SELECT id, email, display_name, phone, role, premium_until, premium_source, email_verified_at,
        (email_verified_at IS NOT NULL) AS is_email_verified,
        (premium_until > NOW()) AS is_premium,
        COALESCE(basic_credits, 0)::int AS basic_credits,
        COALESCE(additional_credits, 0)::int AS additional_credits
      FROM users
      WHERE ${sql.ref(subjectColumn)} = ${profile.providerSubject}
        AND deleted_at IS NULL
      LIMIT 1
    `.execute(this.db);
    return result.rows[0] ? this.toSessionUser(result.rows[0]) : null;
  }

  private async linkSocialIdentity(userId: string, profile: VerifiedSocialProfile): Promise<void> {
    const subjectColumn = profile.provider === 'google' ? 'google_subject' : 'apple_subject';
    await sql`
      UPDATE users
      SET ${sql.ref(subjectColumn)} = ${profile.providerSubject},
          social_auth_provider = COALESCE(social_auth_provider, ${profile.provider}),
          updated_at = NOW()
      WHERE id = ${userId}
        AND deleted_at IS NULL
    `.execute(this.db);
  }

  private async markEmailVerifiedFromProvider(userId: string, shouldVerify: boolean): Promise<void> {
    if (!shouldVerify) return;
    await this.db
      .updateTable('users')
      .set({
        email_verified_at: sql`COALESCE(email_verified_at, NOW())`,
        email_verify_otp: null,
        email_verify_otp_expires_at: null,
      })
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .execute();
  }

  private getGoogleAudiences(): string[] {
    return uniqueValues([
      process.env.GOOGLE_OAUTH_WEB_CLIENT_ID,
      process.env.GOOGLE_OAUTH_IOS_CLIENT_ID,
      process.env.GOOGLE_OAUTH_ANDROID_CLIENT_ID,
      ...parseCsvEnv(process.env.GOOGLE_OAUTH_ALLOWED_CLIENT_IDS),
    ]);
  }

  private getAppleAudience(): string {
    return String(process.env.APPLE_SIGN_IN_AUDIENCE || process.env.APPLE_BUNDLE_ID || '').trim();
  }

  private async fetchAppleJwks(): Promise<any[]> {
    const now = Date.now();
    if (this.appleJwksCache && now - this.appleJwksCache.fetchedAt < 60 * 60 * 1000) {
      return this.appleJwksCache.keys;
    }

    const response = await fetch(APPLE_JWKS_URL);
    if (!response.ok) {
      throw new Error('Unable to fetch Apple Sign In keys');
    }

    const body: any = await response.json();
    this.appleJwksCache = {
      fetchedAt: now,
      keys: Array.isArray(body.keys) ? body.keys : [],
    };
    return this.appleJwksCache.keys;
  }

  private async verifyGoogleSocialToken(idToken: string): Promise<VerifiedSocialProfile> {
    const audiences = this.getGoogleAudiences();
    if (audiences.length === 0) {
      throw new Error('Google OAuth client IDs are not configured');
    }

    const ticket = await this.googleOAuthClient.verifyIdToken({ idToken, audience: audiences });
    const payload = ticket.getPayload();
    const providerSubject = payload?.sub;
    if (!providerSubject) {
      throw new Error('Google token is missing a subject');
    }

    return {
      provider: 'google',
      providerSubject,
      email: normalizeEmail(payload?.email),
      emailVerified: payload?.email_verified === true || String(payload?.email_verified) === 'true',
      displayName: String(payload?.name || '').trim() || null,
    };
  }

  private async verifyAppleSocialToken(idToken: string, nonce?: string | null): Promise<VerifiedSocialProfile> {
    const audience = this.getAppleAudience();
    if (!audience) {
      throw new Error('Apple Sign In audience is not configured');
    }

    const decodedHeader = jwt.decode(idToken, { complete: true })?.header;
    if (!decodedHeader?.kid) {
      throw new Error('Apple token is missing a key id');
    }

    const keys = await this.fetchAppleJwks();
    const jwk = keys.find((key) => key.kid === decodedHeader.kid);
    if (!jwk) {
      this.appleJwksCache = null;
      throw new Error('Apple Sign In key not found');
    }

    const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' });
    const payload = jwt.verify(idToken, publicKey, {
      algorithms: ['RS256'],
      audience,
      issuer: APPLE_ISSUER,
    }) as jwt.JwtPayload;

    if (nonce && payload.nonce && payload.nonce !== nonce) {
      throw new Error('Apple token nonce mismatch');
    }
    if (!payload.sub) {
      throw new Error('Apple token is missing a subject');
    }

    return {
      provider: 'apple',
      providerSubject: String(payload.sub),
      email: normalizeEmail(typeof payload.email === 'string' ? payload.email : null),
      emailVerified: payload.email_verified === true || String(payload.email_verified) === 'true',
      displayName: null,
    };
  }

  private async verifySocialToken(data: SocialLoginDto): Promise<VerifiedSocialProfile> {
    if (data.provider === 'google') {
      return this.verifyGoogleSocialToken(String(data.id_token || ''));
    }
    if (data.provider === 'apple') {
      const profile = await this.verifyAppleSocialToken(
        String(data.id_token || ''),
        data.nonce ? String(data.nonce) : null,
      );
      const displayName = String(data.display_name || '').trim();
      return displayName ? { ...profile, displayName } : profile;
    }
    throw new Error('Unsupported auth provider');
  }

  private hasRequiredSocialSignupConsent(data: SocialLoginDto): boolean {
    return (
      data.consent_privacy === true &&
      data.consent_terms === true &&
      data.consent_ai === true &&
      data.consent_data_storage === true
    );
  }

  private async createSocialUser(
    data: SocialLoginDto,
    profile: VerifiedSocialProfile,
    context?: { ip?: string | null; userAgent?: string | null },
  ) {
    if (!profile.email || !profile.emailVerified) {
      throw new BadRequestException('A verified email is required for social signup');
    }

    if (!this.hasRequiredSocialSignupConsent(data)) {
      throw new HttpException(
        {
          status: false,
          statusCode: HttpStatus.CONFLICT,
          code: 'SOCIAL_SIGNUP_CONSENT_REQUIRED',
          message: 'Consent is required to create your Fur Finder account',
          data: {
            provider: profile.provider,
            email: profile.email,
            display_name: profile.displayName || profile.email.split('@')[0],
          },
        },
        HttpStatus.CONFLICT,
      );
    }

    const displayName = String(data.display_name || profile.displayName || profile.email.split('@')[0]).trim();
    if (!displayName) {
      throw new BadRequestException('Display name is required');
    }
    if (containsDisallowedUserContent([displayName])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    let referredBy: string | null = null;
    if (data.referral_code) {
      const referrer = await this.db
        .selectFrom('users')
        .select('id')
        .where('referral_code', '=', String(data.referral_code).toUpperCase())
        .where('deleted_at', 'is', null)
        .executeTakeFirst();
      if (referrer) referredBy = referrer.id;
    }

    const passwordHash = await this.authHelper.hashPassword(crypto.randomBytes(32).toString('hex'));
    const newReferralCode = this.generateReferralCode();
    const device = this.getDevicePayload(data);

    const inserted = await this.db
      .insertInto('users')
      .values({
        email: profile.email,
        password_hash: passwordHash,
        display_name: displayName,
        phone: String(data.phone || ''),
        consent_privacy: true,
        consent_terms: true,
        consent_ai: true,
        consent_data_storage: true,
        consent_date: new Date(),
        email_verified_at: new Date(),
        referral_code: newReferralCode,
        referred_by: referredBy,
        basic_credits: WELCOME_CREDITS,
        google_subject: profile.provider === 'google' ? profile.providerSubject : null,
        apple_subject: profile.provider === 'apple' ? profile.providerSubject : null,
        social_auth_provider: profile.provider,
      })
      .returning('id')
      .executeTakeFirstOrThrow();

    const userId = inserted.id;

    const signupBonus = await this.billingService.grantSignupBonusPremium({
      user_id: userId,
      platform: device?.platform || 'ios',
    });

    await this.attachDeviceToUser(device, userId);

    if (signupBonus.granted && signupBonus.months) {
      try {
        await this.notificationsService.createSignupBonusNotification(
          userId,
          signupBonus.months,
          signupBonus.premiumUntil,
        );
      } catch (e) {
        this.logger.error('Failed to create signup bonus notification', e);
      }
    }

    if (referredBy) {
      await this.referralsService.createPendingReferralFromSignup({
        referrerUserId: referredBy,
        refereeUserId: userId,
        referralCodeUsed: data.referral_code,
        context: {
          ip: context?.ip || null,
          userAgent: context?.userAgent || null,
          deviceId: device?.device_id || null,
        },
      });

      try {
        await this.referralsService.processReferralValidationForUser(userId);
      } catch (e) {
        this.logger.error('Referral validation during social signup failed', e);
      }
    }

    const user = await this.getSessionUserById(userId);
    if (!user) {
      throw new NotFoundException('User not found');
    }

    return {
      status: true,
      statusCode: HttpStatus.CREATED,
      data: { user, token: this.getSessionToken(user), socialSignup: true },
    };
  }

  async socialLogin(data: SocialLoginDto, context?: { ip?: string | null; userAgent?: string | null }) {
    const device = this.getDevicePayload(data);

    let profile: VerifiedSocialProfile;
    try {
      profile = await this.verifySocialToken(data);
    } catch (e: any) {
      throw new UnauthorizedException(e?.message || 'Social login failed');
    }

    const linkedUser = await this.findUserBySocialIdentity(profile);
    if (linkedUser) {
      await this.attachDeviceToUser(device, linkedUser.id);
      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: { user: linkedUser, token: this.getSessionToken(linkedUser), socialSignup: false },
      };
    }

    if (!profile.email) {
      throw new BadRequestException(
        'This provider did not return an email. Please use a linked account or another login method.',
      );
    }
    if (!profile.emailVerified) {
      throw new BadRequestException('This provider did not return a verified email.');
    }

    const existingUser = await this.getSessionUserByVerifiedEmail(profile.email);
    if (existingUser) {
      await this.linkSocialIdentity(existingUser.id, profile);
      await this.markEmailVerifiedFromProvider(existingUser.id, profile.emailVerified);
      const refreshedUser = await this.getSessionUserById(existingUser.id);
      if (!refreshedUser) {
        throw new NotFoundException('User not found');
      }
      await this.attachDeviceToUser(device, refreshedUser.id);
      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: { user: refreshedUser, token: this.getSessionToken(refreshedUser), socialSignup: false },
      };
    }

    return this.createSocialUser(data, profile, context);
  }

  async register(data: CreateUserDto, context?: { ip?: string | null; userAgent?: string | null }) {
    const device = this.getDevicePayload(data);
    const {
      email,
      password,
      display_name,
      phone,
      consent_privacy,
      consent_terms,
      consent_ai,
      consent_data_storage,
      referral_code,
    } = data;

    if (containsDisallowedUserContent([display_name])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    const existing = await this.db
      .selectFrom('users')
      .select('id')
      .where('email', '=', email)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

    if (existing) {
      throw new ConflictException('An account with this email already exists');
    }

    let referredBy: string | null = null;
    if (referral_code) {
      const referrer = await this.db
        .selectFrom('users')
        .select('id')
        .where('referral_code', '=', referral_code.toUpperCase())
        .where('deleted_at', 'is', null)
        .executeTakeFirst();
      if (referrer) {
        referredBy = referrer.id;
      }
    }

    const passwordHash = await this.authHelper.hashPassword(password);
    const newReferralCode = this.generateReferralCode();
    const emailVerifyOtp = this.generateOtp();
    const emailVerifyOtpExpiresAt = new Date(Date.now() + 10 * 60000);

    try {
      const user = await this.db
        .insertInto('users')
        .values({
          email,
          password_hash: passwordHash,
          display_name,
          phone: phone || '',
          consent_privacy: consent_privacy || false,
          consent_terms: consent_terms || false,
          consent_ai: consent_ai || false,
          consent_data_storage: consent_data_storage || false,
          consent_date: new Date(),
          referral_code: newReferralCode,
          referred_by: referredBy,
          basic_credits: WELCOME_CREDITS,
          email_verify_otp: emailVerifyOtp,
          email_verify_otp_expires_at: emailVerifyOtpExpiresAt,
        })
        .returning([
          'id',
          'email',
          'display_name',
          'phone',
          'referral_code as referralCode',
          'role',
          'premium_until',
          'premium_source',
          'email_verified_at',
          'basic_credits',
          'additional_credits',
        ])
        .executeTakeFirstOrThrow();

      const basicCredits = Number(user.basic_credits || 0);
      const additionalCredits = Number(user.additional_credits || 0);
      const totalCredits = basicCredits + additionalCredits;

      const userData = {
        id: user.id,
        email: user.email,
        display_name: user.display_name,
        phone: user.phone,
        referralCode: user.referralCode,
        role: user.role,
        premium_until: user.premium_until as Date | string | null,
        premium_source: user.premium_source as string | null,
        is_premium: false,
        email_verified_at: user.email_verified_at,
        is_email_verified: false,
        basic_credits: basicCredits,
        additional_credits: additionalCredits,
        total_credits: totalCredits,
      };

      const signupBonus = await this.billingService.grantSignupBonusPremium({
        user_id: userData.id,
        platform: device?.platform || 'ios',
      });

      if (signupBonus.premiumUntil) {
        userData.premium_until = signupBonus.premiumUntil;
        userData.premium_source = 'signup_bonus';
        userData.is_premium = true;
      }

      const token = this.authHelper.generateToken({
        id: userData.id,
        email: userData.email,
        display_name: userData.display_name,
        phone: userData.phone,
        role: userData.role || 'user',
      });

      await this.attachDeviceToUser(device, userData.id);

      if (signupBonus.granted && signupBonus.months) {
        try {
          await this.notificationsService.createSignupBonusNotification(
            userData.id,
            signupBonus.months,
            signupBonus.premiumUntil,
          );
        } catch (e) {
          this.logger.error('Failed to create signup bonus notification', e);
        }
      }

      // Send email verification OTP — fire-and-forget, don't fail registration on mail error
      this.mailService
        .sendOtpEmail(user.email, user.display_name, emailVerifyOtp)
        .catch((e) => this.logger.error('Failed to send email verification OTP', e));

      if (referredBy) {
        await this.referralsService.createPendingReferralFromSignup({
          referrerUserId: referredBy,
          refereeUserId: userData.id,
          referralCodeUsed: referral_code,
          context: {
            ip: context?.ip || null,
            userAgent: context?.userAgent || null,
            deviceId: device?.device_id || null,
          },
        });
      }

      return {
        status: true,
        statusCode: HttpStatus.CREATED,
        data: {
          user: userData,
          token,
          emailVerificationRequired: true,
          otpExpiresAt: emailVerifyOtpExpiresAt.toISOString(),
        },
      };
    } catch (error) {
      this.logger.error('Failed to create user', error);
      throw error;
    }
  }

  async login(data: LoginDto) {
    const { email, password } = data;
    const device = this.getDevicePayload(data);

    const userData = await this.db
      .selectFrom('users')
      .select([
        'id',
        'email',
        'password_hash',
        'display_name',
        'phone',
        'role',
        'premium_until',
        'premium_source',
        'email_verified_at',
        'basic_credits',
        'additional_credits',
      ])
      .select(sql<boolean>`premium_until > NOW()`.as('is_premium'))
      .where('email', '=', email.toLowerCase())
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

    if (!userData) {
      throw new NotFoundException('No user registered');
    }

    if (!userData.password_hash) {
      throw new UnauthorizedException(
        'This account uses Google sign-in. Please continue with Google or reset your password.',
      );
    }

    const validPassword = await this.authHelper.verifyPassword(password, userData.password_hash);
    if (!validPassword) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const basicCredits = Number(userData.basic_credits || 0);
    const additionalCredits = Number(userData.additional_credits || 0);
    const totalCredits = basicCredits + additionalCredits;

    const user = {
      id: userData.id,
      email: userData.email,
      display_name: userData.display_name,
      phone: userData.phone,
      role: userData.role || 'user',
      premium_until: userData.premium_until,
      premium_source: userData.premium_source,
      is_premium: !!userData.is_premium,
      email_verified_at: userData.email_verified_at,
      is_email_verified: !!userData.email_verified_at,
      basic_credits: basicCredits,
      additional_credits: additionalCredits,
      total_credits: totalCredits,
    };

    const token = this.authHelper.generateToken(user);
    await this.attachDeviceToUser(device, user.id);

    return { status: true, statusCode: HttpStatus.OK, data: { user, token } };
  }

  async attachDeviceToUser(device: DeviceTypePayload | undefined, userId: string) {
    if (!device?.device_id) return;

    await this.deviceService.upsertDevice({
      device_id: device.device_id,
      user_id: userId,
      expo_push_token: device.expo_push_token,
      platform: device.platform,
      app_version: device.app_version,
      location: device.location,
    });
  }

  async getCurrentUser(userId: string) {
    try {
      await this.referralsService.processReferralValidationForUser(userId);
    } catch (e) {
      this.logger.error('Referral validation during me() failed', e);
    }

    try {
      const user = await this.db
        .selectFrom('users')
        .select([
          'id',
          'email',
          'display_name',
          'phone',
          'role',
          'premium_until',
          'premium_source',
          'email_verified_at',
          sql<boolean>`premium_until > NOW()`.as('is_premium'),
          sql<boolean>`email_verified_at IS NOT NULL`.as('is_email_verified'),
          sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
          sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
          sql<number>`
        (
          COALESCE(basic_credits, 0) +
          COALESCE(additional_credits, 0)
        )::int
      `.as('total_credits'),
        ])
        .where('id', '=', userId)
        .where('deleted_at', 'is', null)
        .executeTakeFirstOrThrow();

      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: user,
      };
    } catch (error) {
      this.logger.error('Failed to get user', error);
      if (error instanceof NoResultError) throw new NotFoundException('User not found');
      throw error;
    }
  }

  async sendForgotPasswordOTP(email: string) {
    try {
      const user = await this.db
        .selectFrom('users')
        .select(['id', 'email', 'display_name'])
        .where('email', '=', email)
        .where('deleted_at', 'is', null)
        .executeTakeFirst();

      if (!user) {
        return {
          status: true,
          statusCode: HttpStatus.OK,
          message: 'If this email is registered, you will receive an OTP.',
        };
      }

      const otp = Math.floor(100000 + Math.random() * 900000).toString();
      const expiresAt = new Date(Date.now() + 10 * 60000);

      await this.db
        .updateTable('users')
        .set({
          reset_otp: otp,
          reset_otp_expires_at: expiresAt,
        })
        .where('id', '=', user.id)
        .where('deleted_at', 'is', null)
        .execute();

      await this.mailService.sendOtpEmail(user.email, user.display_name, otp);

      return {
        status: true,
        statusCode: HttpStatus.OK,
        message: 'If this email is registered, you will receive an OTP',
      };
    } catch (error) {
      this.logger.error('Error on forgot password request', error);
      throw error;
    }
  }

  async resetPassword(data: ResetPasswordDto) {
    const { email, otp, newPassword, confirmPassword } = data;

    if (newPassword !== confirmPassword) {
      throw new BadRequestException('Passwords do not match');
    }

    try {
      const user = await this.db
        .selectFrom('users')
        .select(['id', 'reset_otp', 'reset_otp_expires_at'])
        .where('email', '=', email)
        .where('deleted_at', 'is', null)
        .where('reset_otp', '=', otp)
        .where('reset_otp_expires_at', '>', new Date())
        .executeTakeFirst();

      if (!user) {
        throw new BadRequestException('Invalid or expired OTP.');
      }

      const passwordHash = await this.authHelper.hashPassword(newPassword);

      await this.db
        .updateTable('users')
        .set({
          password_hash: passwordHash,
          reset_otp: null,
          reset_otp_expires_at: null,
        })
        .where('id', '=', user.id)
        .where('deleted_at', 'is', null)
        .execute();

      return {
        status: true,
        statusCode: HttpStatus.OK,
        message: 'Password reset successful',
      };
    } catch (error) {
      this.logger.error('Error on reset password request', error);
      throw error;
    }
  }

  async requestEmailVerificationOtp(userId: string) {
    const user = await this.db
      .selectFrom('users')
      .select(['id', 'email', 'display_name', 'email_verified_at'])
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

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

    if (user.email_verified_at) {
      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: { message: 'Email already verified' },
      };
    }

    const otp = this.generateOtp();
    const expiresAt = new Date(Date.now() + 10 * 60000);

    await this.db
      .updateTable('users')
      .set({
        email_verify_otp: otp,
        email_verify_otp_expires_at: expiresAt,
      })
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .execute();

    await this.mailService.sendOtpEmail(user.email, user.display_name, otp);

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        message: 'Verification OTP sent',
        otpExpiresAt: expiresAt.toISOString(),
      },
    };
  }

  async verifyEmail(userId: string, otp: string) {
    const user = await this.db
      .selectFrom('users')
      .select(['id', 'email_verified_at', 'email_verify_otp', 'email_verify_otp_expires_at'])
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

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

    if (user.email_verified_at) {
      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: {
          message: 'Email already verified',
          email_verified_at: new Date(user.email_verified_at).toISOString(),
        },
      };
    }

    const otpMatch = String(user.email_verify_otp || '') === otp;
    const notExpired =
      !!user.email_verify_otp_expires_at && new Date(user.email_verify_otp_expires_at) > new Date();

    if (!otpMatch || !notExpired) {
      throw new BadRequestException('Invalid or expired OTP');
    }

    const updated = await this.db
      .updateTable('users')
      .set({
        email_verified_at: new Date(),
        email_verify_otp: null,
        email_verify_otp_expires_at: null,
      })
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .returning('email_verified_at')
      .executeTakeFirstOrThrow();

    try {
      await this.referralsService.processReferralValidationForUser(userId);
    } catch (e) {
      this.logger.error('Referral validation during verifyEmail() failed', e);
    }

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        message: 'Email verified',
        email_verified_at: updated.email_verified_at,
      },
    };
  }
}
