import {
  Injectable,
  BadRequestException,
  NotFoundException,
  Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { authenticator } from 'otplib';
import * as QRCode from 'qrcode';
import * as crypto from 'crypto';
import * as bcrypt from 'bcrypt';
import { User } from '../../database/entities';
import { EmailService } from '../../common/services/email.service';
import { ConfigService } from '@nestjs/config';

export interface MFASetupResult {
  secret: string;
  qrCodeUrl: string;
  backupCodes: string[];
}

export interface MFAStatus {
  enabled: boolean;
  method?: 'totp' | 'email';
  hasBackupCodes: boolean;
}

@Injectable()
export class MFAService {
  private readonly logger = new Logger(MFAService.name);
  private readonly appName: string;

  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    private emailService: EmailService,
    private configService: ConfigService,
  ) {
    this.appName = this.configService.get<string>('APP_NAME', 'Maternal App');
  }

  /**
   * Get MFA status for a user
   */
  async getMFAStatus(userId: string): Promise<MFAStatus> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'mfaEnabled', 'mfaMethod', 'mfaBackupCodes'],
    });

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

    return {
      enabled: user.mfaEnabled,
      method: user.mfaMethod as 'totp' | 'email' | undefined,
      hasBackupCodes: !!user.mfaBackupCodes && user.mfaBackupCodes.length > 0,
    };
  }

  /**
   * Setup TOTP (Google Authenticator) MFA
   */
  async setupTOTP(userId: string): Promise<MFASetupResult> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'email', 'name'],
    });

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

    // Generate secret for TOTP
    const secret = authenticator.generateSecret();

    // Generate QR code
    const otpauthUrl = authenticator.keyuri(user.email, this.appName, secret);
    const qrCodeUrl = await QRCode.toDataURL(otpauthUrl);

    // Generate backup codes
    const backupCodes = this.generateBackupCodes();
    const hashedBackupCodes = await Promise.all(
      backupCodes.map((code) => bcrypt.hash(code, 10)),
    );

    // Save secret and backup codes (but don't enable MFA yet)
    await this.userRepository.update(userId, {
      totpSecret: secret,
      mfaBackupCodes: hashedBackupCodes,
    });

    this.logger.log(`TOTP setup initiated for user ${userId}`);

    return {
      secret,
      qrCodeUrl,
      backupCodes,
    };
  }

  /**
   * Verify TOTP code and enable TOTP MFA
   */
  async verifyAndEnableTOTP(
    userId: string,
    code: string,
  ): Promise<{ success: boolean; message: string }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'totpSecret', 'mfaEnabled'],
    });

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

    if (!user.totpSecret) {
      throw new BadRequestException(
        'TOTP is not set up. Please set up TOTP first.',
      );
    }

    // Verify the TOTP code
    const isValid = authenticator.verify({
      token: code,
      secret: user.totpSecret,
    });

    if (!isValid) {
      throw new BadRequestException('Invalid verification code');
    }

    // Enable MFA with TOTP method
    await this.userRepository.update(userId, {
      mfaEnabled: true,
      mfaMethod: 'totp',
    });

    this.logger.log(`TOTP MFA enabled for user ${userId}`);

    return {
      success: true,
      message: 'Two-factor authentication enabled successfully',
    };
  }

  /**
   * Setup Email MFA
   */
  async setupEmailMFA(
    userId: string,
  ): Promise<{ success: boolean; message: string }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'email', 'name'],
    });

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

    // Generate backup codes
    const backupCodes = this.generateBackupCodes();
    const hashedBackupCodes = await Promise.all(
      backupCodes.map((code) => bcrypt.hash(code, 10)),
    );

    // Enable Email MFA
    await this.userRepository.update(userId, {
      mfaEnabled: true,
      mfaMethod: 'email',
      mfaBackupCodes: hashedBackupCodes,
    });

    // Send confirmation email
    try {
      await this.emailService.sendEmail({
        to: user.email,
        subject: 'Email Two-Factor Authentication Enabled',
        html: `
          <h2>Two-Factor Authentication Enabled</h2>
          <p>Hi ${user.name},</p>
          <p>Email-based two-factor authentication has been enabled for your account.</p>
          <p>You will now receive a verification code via email each time you log in.</p>
          <h3>Backup Codes</h3>
          <p>Save these backup codes in a safe place. You can use them to access your account if you don't have access to your email:</p>
          <ul>
            ${backupCodes.map((code) => `<li><code>${code}</code></li>`).join('')}
          </ul>
          <p>Each backup code can only be used once.</p>
        `,
      });
    } catch (error) {
      this.logger.error(
        `Failed to send MFA setup confirmation email: ${error.message}`,
      );
    }

    this.logger.log(`Email MFA enabled for user ${userId}`);

    return {
      success: true,
      message:
        'Email-based two-factor authentication enabled successfully. Check your email for backup codes.',
    };
  }

  /**
   * Send email MFA code
   */
  async sendEmailMFACode(
    userId: string,
  ): Promise<{ success: boolean; message: string }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'email', 'name', 'mfaEnabled', 'mfaMethod'],
    });

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

    if (!user.mfaEnabled || user.mfaMethod !== 'email') {
      throw new BadRequestException('Email MFA is not enabled for this user');
    }

    // Generate 6-digit code
    const code = Math.floor(100000 + Math.random() * 900000).toString();
    const expiresAt = new Date();
    expiresAt.setMinutes(expiresAt.getMinutes() + 5); // Code expires in 5 minutes

    // Save code
    await this.userRepository.update(userId, {
      emailMfaCode: code,
      emailMfaCodeExpiresAt: expiresAt,
    });

    // Send email with code
    try {
      await this.emailService.sendEmail({
        to: user.email,
        subject: `Your verification code is ${code}`,
        html: `
          <h2>Verification Code</h2>
          <p>Hi ${user.name},</p>
          <p>Your verification code is:</p>
          <h1 style="font-size: 36px; letter-spacing: 8px; font-family: monospace;">${code}</h1>
          <p>This code will expire in 5 minutes.</p>
          <p>If you didn't request this code, please ignore this email.</p>
        `,
      });

      this.logger.log(`Email MFA code sent to user ${userId}`);
    } catch (error) {
      this.logger.error(`Failed to send MFA email code: ${error.message}`);
      throw new BadRequestException(
        'Failed to send verification code. Please try again.',
      );
    }

    return {
      success: true,
      message: 'Verification code sent to your email',
    };
  }

  /**
   * Verify MFA code (TOTP or Email or Backup code)
   */
  async verifyMFACode(
    userId: string,
    code: string,
  ): Promise<{ success: boolean; message: string }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: [
        'id',
        'mfaEnabled',
        'mfaMethod',
        'totpSecret',
        'emailMfaCode',
        'emailMfaCodeExpiresAt',
        'mfaBackupCodes',
      ],
    });

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

    if (!user.mfaEnabled) {
      throw new BadRequestException('MFA is not enabled for this user');
    }

    // Try TOTP verification
    if (user.mfaMethod === 'totp' && user.totpSecret) {
      const isValid = authenticator.verify({
        token: code,
        secret: user.totpSecret,
      });

      if (isValid) {
        return {
          success: true,
          message: 'Code verified successfully',
        };
      }
    }

    // Try Email code verification
    if (user.mfaMethod === 'email' && user.emailMfaCode) {
      if (
        !user.emailMfaCodeExpiresAt ||
        new Date() > user.emailMfaCodeExpiresAt
      ) {
        throw new BadRequestException(
          'Verification code has expired. Please request a new one.',
        );
      }

      if (code === user.emailMfaCode) {
        // Clear the used code
        await this.userRepository.update(userId, {
          emailMfaCode: null,
          emailMfaCodeExpiresAt: null,
        });

        return {
          success: true,
          message: 'Code verified successfully',
        };
      }
    }

    // Try backup code verification
    if (user.mfaBackupCodes && user.mfaBackupCodes.length > 0) {
      for (let i = 0; i < user.mfaBackupCodes.length; i++) {
        const isValid = await bcrypt.compare(code, user.mfaBackupCodes[i]);
        if (isValid) {
          // Remove used backup code
          const updatedBackupCodes = [...user.mfaBackupCodes];
          updatedBackupCodes.splice(i, 1);
          await this.userRepository.update(userId, {
            mfaBackupCodes: updatedBackupCodes,
          });

          this.logger.log(
            `Backup code used for user ${userId}. ${updatedBackupCodes.length} codes remaining.`,
          );

          return {
            success: true,
            message: `Backup code verified. You have ${updatedBackupCodes.length} backup codes remaining.`,
          };
        }
      }
    }

    throw new BadRequestException('Invalid verification code');
  }

  /**
   * Disable MFA
   */
  async disableMFA(
    userId: string,
  ): Promise<{ success: boolean; message: string }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'mfaEnabled'],
    });

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

    if (!user.mfaEnabled) {
      return {
        success: true,
        message: 'MFA is already disabled',
      };
    }

    // Disable MFA and clear all MFA data
    await this.userRepository.update(userId, {
      mfaEnabled: false,
      mfaMethod: null,
      totpSecret: null,
      mfaBackupCodes: null,
      emailMfaCode: null,
      emailMfaCodeExpiresAt: null,
    });

    this.logger.log(`MFA disabled for user ${userId}`);

    return {
      success: true,
      message: 'Two-factor authentication disabled successfully',
    };
  }

  /**
   * Regenerate backup codes
   */
  async regenerateBackupCodes(userId: string): Promise<string[]> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      select: ['id', 'mfaEnabled'],
    });

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

    if (!user.mfaEnabled) {
      throw new BadRequestException(
        'MFA is not enabled. Please enable MFA first.',
      );
    }

    // Generate new backup codes
    const backupCodes = this.generateBackupCodes();
    const hashedBackupCodes = await Promise.all(
      backupCodes.map((code) => bcrypt.hash(code, 10)),
    );

    await this.userRepository.update(userId, {
      mfaBackupCodes: hashedBackupCodes,
    });

    this.logger.log(`Backup codes regenerated for user ${userId}`);

    return backupCodes;
  }

  /**
   * Generate backup codes (10 codes, 8 characters each)
   */
  private generateBackupCodes(): string[] {
    const codes: string[] = [];
    for (let i = 0; i < 10; i++) {
      const code = crypto.randomBytes(4).toString('hex').toUpperCase();
      codes.push(code);
    }
    return codes;
  }
}
