import { Inject, Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { type OAuthProvider, type Prisma } from '@prisma/client';
import { PrismaService, LoggerService } from '@modules/shared';
import { UserEntity } from '../../domain/entities/user.entity';
import { UserRegisteredEvent } from '../../domain/events/user-registered.event';
import { USER_REPOSITORY, type IUserRepository } from '../../domain/repositories/user.repository';
import { Email } from '../../domain/value-objects/email.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
import { TokenService, type TokenPair } from './token.service';

export interface OAuthUserProfile {
  provider: OAuthProvider;
  providerUserId: string;
  email?: string;
  phone?: string;
  fullName: string;
  avatarUrl?: string;
  accessToken?: string;
  refreshToken?: string;
  expiresAt?: Date;
  rawProfile?: Record<string, unknown>;
}

@Injectable()
export class OAuthService {
  constructor(
    @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
    private readonly tokenService: TokenService,
    private readonly prisma: PrismaService,
    private readonly eventBus: EventBus,
    private readonly logger: LoggerService,
  ) {}

  /**
   * Authenticate via OAuth: find existing linked account, link to existing user,
   * or create a new user. Returns JWT token pair.
   */
  async authenticateOAuth(profile: OAuthUserProfile): Promise<TokenPair> {
    // 1. Check if this OAuth account already exists
    const existingOAuth = await this.prisma.oAuthAccount.findUnique({
      where: {
        provider_providerUserId: {
          provider: profile.provider,
          providerUserId: profile.providerUserId,
        },
      },
      include: { user: true },
    });

    if (existingOAuth) {
      // Update tokens
      await this.prisma.oAuthAccount.update({
        where: { id: existingOAuth.id },
        data: {
          accessToken: profile.accessToken ?? existingOAuth.accessToken,
          refreshToken: profile.refreshToken ?? existingOAuth.refreshToken,
          expiresAt: profile.expiresAt ?? existingOAuth.expiresAt,
          profile: (profile.rawProfile ?? existingOAuth.profile) as Prisma.InputJsonValue,
        },
      });

      if (!existingOAuth.user.isActive) {
        throw new Error('Tài khoản đã bị vô hiệu hóa');
      }

      this.logger.log(`OAuth login: existing account for ${profile.provider}/${profile.providerUserId}`, 'OAuthService');
      return this.generateTokensForUser(existingOAuth.user);
    }

    // 2. Try to link to existing user by email
    if (profile.email) {
      const existingUser = await this.userRepo.findByEmail(profile.email);
      if (existingUser) {
        if (!existingUser.isActive) {
          throw new Error('Tài khoản đã bị vô hiệu hóa');
        }
        await this.createOAuthAccount(existingUser.id, profile);
        this.logger.log(`OAuth link: linked ${profile.provider} to existing user by email`, 'OAuthService');
        return this.generateTokensForUser({
          id: existingUser.id,
          phone: existingUser.phone.value,
          role: existingUser.role,
        });
      }
    }

    // 3. Try to link to existing user by phone
    if (profile.phone) {
      const phoneVo = Phone.create(profile.phone);
      if (phoneVo.isOk) {
        const existingUser = await this.userRepo.findByPhone(phoneVo.unwrap().value);
        if (existingUser) {
          if (!existingUser.isActive) {
            throw new Error('Tài khoản đã bị vô hiệu hóa');
          }
          await this.createOAuthAccount(existingUser.id, profile);
          this.logger.log(`OAuth link: linked ${profile.provider} to existing user by phone`, 'OAuthService');
          return this.generateTokensForUser({
            id: existingUser.id,
            phone: existingUser.phone.value,
            role: existingUser.role,
          });
        }
      }
    }

    // 4. Create new user + OAuth account
    const userId = createId();
    const phone = this.resolvePhone(profile.phone);
    const email = profile.email ? Email.create(profile.email) : null;

    const user = new UserEntity(userId, {
      email: email?.isOk ? email.unwrap() : null,
      phone,
      passwordHash: null, // OAuth users have no password
      fullName: profile.fullName,
      avatarUrl: profile.avatarUrl ?? null,
      role: 'BUYER',
      kycStatus: 'NONE',
      kycData: null,
      isActive: true,
      totpSecret: null,
      totpEnabled: false,
      totpBackupCodes: [],
      totpEnabledAt: null,
    });

    await this.userRepo.save(user);
    await this.createOAuthAccount(userId, profile);

    // Publish domain event
    this.eventBus.publish(new UserRegisteredEvent(userId, phone.value, 'BUYER'));

    this.logger.log(`OAuth register: new user created via ${profile.provider}`, 'OAuthService');
    return this.generateTokensForUser({
      id: userId,
      phone: phone.value,
      role: 'BUYER',
    });
  }

  private async createOAuthAccount(userId: string, profile: OAuthUserProfile): Promise<void> {
    await this.prisma.oAuthAccount.create({
      data: {
        id: createId(),
        userId,
        provider: profile.provider,
        providerUserId: profile.providerUserId,
        accessToken: profile.accessToken,
        refreshToken: profile.refreshToken,
        expiresAt: profile.expiresAt,
        profile: (profile.rawProfile as Prisma.InputJsonValue) ?? undefined,
      },
    });
  }

  private generateTokensForUser(user: { id: string; phone: string; role: string }): Promise<TokenPair> {
    return this.tokenService.generateTokenPair({
      sub: user.id,
      phone: user.phone,
      role: user.role,
    });
  }

  /**
   * Generate a placeholder phone for OAuth users who don't provide one.
   * Vietnamese phone format required by the schema — uses 09xx prefix
   * to satisfy validation. User should update their phone later.
   */
  private resolvePhone(phone?: string): Phone {
    if (phone) {
      const result = Phone.create(phone);
      if (result.isOk) return result.unwrap();
    }
    // Generate a valid Vietnamese placeholder: +849XXXXXXXX (10 digits after +84)
    const random7 = Math.floor(1000000 + Math.random() * 9000000).toString();
    const placeholder = `+8490${random7}`;
    return Phone.create(placeholder).unwrap();
  }
}
