import { randomInt } from 'crypto';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
import {
  CachePrefix,
  CacheService,
  ConflictException,
  DomainException,
  LoggerService,
  NotFoundException,
  RedisService,
  ValidationException,
} from '@modules/shared';
import { EmailChangeRequestedEvent } from '../../../domain/events/email-change-requested.event';
import { PhoneChangeRequestedEvent } from '../../../domain/events/phone-change-requested.event';
import { type IUserRepository, USER_REPOSITORY } from '../../../domain/repositories/user.repository';
import { Email } from '../../../domain/value-objects/email.vo';
import { Phone } from '../../../domain/value-objects/phone.vo';
import { UpdateProfileCommand } from './update-profile.command';

/** TTL for email-change OTP codes stored in Redis (10 minutes). */
const EMAIL_CHANGE_OTP_TTL = 600;

/** Redis key prefix for pending email-change OTP. */
export const EMAIL_CHANGE_OTP_PREFIX = 'auth:email_change_otp';

/** TTL for phone-change OTP codes stored in Redis (10 minutes). */
const PHONE_CHANGE_OTP_TTL = 600;

/** Redis key prefix for pending phone-change OTP. */
export const PHONE_CHANGE_OTP_PREFIX = 'auth:phone_change_otp';

export interface UpdateProfileResultDto {
  id: string;
  fullName: string;
  avatarUrl: string | null;
  email: string | null;
  phoneNumber: string;
  emailChangePending?: boolean;
  phoneChangePending?: boolean;
  updatedAt: Date;
}

@CommandHandler(UpdateProfileCommand)
export class UpdateProfileHandler implements ICommandHandler<UpdateProfileCommand> {
  constructor(
    @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
    private readonly cache: CacheService,
    private readonly redis: RedisService,
    private readonly eventBus: EventBus,
    private readonly logger: LoggerService,
  ) {}

  async execute(command: UpdateProfileCommand): Promise<UpdateProfileResultDto> {
    try {
      const user = await this.userRepo.findById(command.userId);
      if (!user) {
        throw new NotFoundException('Người dùng', command.userId);
      }

      let emailChangePending = false;
      let phoneChangePending = false;

      // Validate and handle email change via OTP
      if (command.email !== undefined) {
        const emailResult = Email.create(command.email);
        if (emailResult.isErr) {
          throw new ValidationException(emailResult.unwrapErr());
        }
        const email = emailResult.unwrap();

        // Check if email is actually changing
        if (user.email?.value !== email.value) {
          // Check uniqueness
          const existingUser = await this.userRepo.findByEmail(email.value);
          if (existingUser && existingUser.id !== command.userId) {
            throw new ConflictException('Email đã được sử dụng bởi tài khoản khác');
          }

          // Generate OTP and store pending change in Redis
          const otpCode = String(randomInt(100_000, 999_999));
          const payload = JSON.stringify({ newEmail: email.value, code: otpCode });
          await this.redis.set(
            `${EMAIL_CHANGE_OTP_PREFIX}:${command.userId}`,
            payload,
            EMAIL_CHANGE_OTP_TTL,
          );

          // Emit event so notifications module can send the OTP email
          this.eventBus.publish(
            new EmailChangeRequestedEvent(command.userId, email.value, otpCode),
          );

          emailChangePending = true;
        }
      }

      // Validate and handle phone change via SMS OTP
      if (command.phoneNumber !== undefined) {
        const phoneResult = Phone.create(command.phoneNumber);
        if (phoneResult.isErr) {
          throw new ValidationException(phoneResult.unwrapErr());
        }
        const phone = phoneResult.unwrap();

        // Check if phone is actually changing
        if (user.phone.value !== phone.value) {
          // Check uniqueness
          const existingUser = await this.userRepo.findByPhone(phone.value);
          if (existingUser && existingUser.id !== command.userId) {
            throw new ConflictException('Số điện thoại đã được sử dụng bởi tài khoản khác');
          }

          // Generate OTP and store pending change in Redis
          const otpCode = String(randomInt(100_000, 999_999));
          const payload = JSON.stringify({ newPhone: phone.value, code: otpCode });
          await this.redis.set(
            `${PHONE_CHANGE_OTP_PREFIX}:${command.userId}`,
            payload,
            PHONE_CHANGE_OTP_TTL,
          );

          // Emit event so notifications module can send the SMS OTP
          this.eventBus.publish(
            new PhoneChangeRequestedEvent(command.userId, phone.value, otpCode),
          );

          phoneChangePending = true;
        }
      }

      // Apply non-email / non-phone fields immediately
      user.updateProfile(command.fullName, command.avatarUrl, undefined);
      await this.userRepo.update(user);

      await this.cache.invalidate(
        CacheService.buildKey(CachePrefix.USER_PROFILE, command.userId),
      );

      return {
        id: user.id,
        fullName: user.fullName,
        avatarUrl: user.avatarUrl,
        email: user.email?.value ?? null,
        phoneNumber: user.phone.value,
        ...(emailChangePending ? { emailChangePending: true } : {}),
        ...(phoneChangePending ? { phoneChangePending: true } : {}),
        updatedAt: user.updatedAt,
      };
    } catch (error) {
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to update profile: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException('Không thể cập nhật hồ sơ người dùng');
    }
  }
}
