import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { AppDataSource } from '../../../config/typeorm.js';
import { User } from '../entities/user.entity';
import { config } from '../../../config';
import { AppError } from '../../../shared/types';

export class MfaService {
  private static get userRepository() {
    return AppDataSource.getRepository(User);
  }

  private static get appName() {
    return 'ERP Core'; // Or config.appName if available
  }

  /**
   * Initialize MFA setup - generate secret and QR code
   */
  static async setupMfa(userId: string) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    if (user.mfaEnabled) {
      throw new AppError('MFA is already enabled', 400);
    }

    // Generate TOTP secret
    const secret = speakeasy.generateSecret({
      name: `${this.appName} (${user.email})`,
      length: 20,
    });

    // Generate QR code
    const qrCodeDataUrl = await QRCode.toDataURL(secret.otpauth_url!);

    // Generate backup codes
    const backupCodes = this.generateBackupCodes();

    return {
      secret: secret.base32,
      qrCodeDataUrl,
      backupCodes,
    };
  }

  /**
   * Verify MFA setup and enable
   */
  static async verifyMfaSetup(
    userId: string,
    secretToken: string,
    code: string
  ) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    if (user.mfaEnabled) {
      throw new AppError('MFA is already enabled', 400);
    }

    // Verify the TOTP code
    const isValid = speakeasy.totp.verify({
      secret: secretToken,
      encoding: 'base32',
      token: code,
      window: 1, // Allow 1 step (30 seconds) tolerance
    });

    if (!isValid) {
      throw new AppError('Invalid verification code', 400);
    }

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

    // Enable MFA and store secret
    user.mfaEnabled = true;
    user.mfaSecretEncrypted = this.encryptSecret(secretToken);
    user.mfaBackupCodes = hashedBackupCodes;
    // user.mfaEnabledAt = new Date(); // Field not in entity yet, skipping

    await this.userRepository.save(user);

    return {
      success: true,
      message: 'MFA enabled successfully. Please save your backup codes.',
      backupCodes, // Return unhashed backup codes one last time
    };
  }

  /**
   * Verify TOTP code during login
   */
  static async verifyMfaCode(
    userId: string,
    code: string,
    isBackupCode: boolean = false
  ): Promise<boolean> {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    if (!user.mfaEnabled || !user.mfaSecretEncrypted) {
      throw new AppError('MFA is not enabled for this user', 400);
    }

    if (isBackupCode) {
      return this.verifyBackupCode(user, code);
    }

    // Decrypt secret and verify TOTP
    const secret = this.decryptSecret(user.mfaSecretEncrypted);

    const isValid = speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token: code,
      window: 1,
    });

    return isValid;
  }

  /**
   * Disable MFA for user
   */
  static async disableMfa(userId: string, code: string, password?: string) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    if (!user.mfaEnabled) {
      throw new AppError('MFA is not enabled', 400);
    }

    // Optional: Verify password if provided
    if (password) {
      const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
      if (!isPasswordValid) {
        throw new AppError('Invalid password', 401);
      }
    }

    // Verify MFA code
    const isMfaValid = await this.verifyMfaCode(userId, code, code.length > 6);
    if (!isMfaValid) {
      throw new AppError('Invalid verification code', 400);
    }

    // Disable MFA
    user.mfaEnabled = false;
    user.mfaSecretEncrypted = null as any; // TypeORM nullable handling
    user.mfaBackupCodes = null as any;

    await this.userRepository.save(user);

    return {
      success: true,
      message: 'MFA disabled successfully',
    };
  }

  /**
   * Get MFA status for user
   */
  static async getMfaStatus(userId: string) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    const backupCodesRemaining = user.mfaBackupCodes?.length || 0;

    return {
      enabled: user.mfaEnabled || false,
      backupCodesRemaining,
    };
  }

  /**
   * Regenerate backup codes
   */
  static async regenerateBackupCodes(
    userId: string,
    code: string,
    password?: string
  ) {
    const user = await this.userRepository.findOne({ where: { id: userId } });

    if (!user) {
      throw new AppError('User not found', 404);
    }

    if (!user.mfaEnabled) {
      throw new AppError('MFA is not enabled', 400);
    }

     // Optional: Verify password if provided
     if (password) {
        const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
        if (!isPasswordValid) {
          throw new AppError('Invalid password', 401);
        }
      }

    // Verify MFA code
    const isMfaValid = await this.verifyMfaCode(userId, code);
    if (!isMfaValid) {
      throw new AppError('Invalid verification code', 400);
    }

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

    // Update user
    user.mfaBackupCodes = hashedBackupCodes;
    await this.userRepository.save(user);

    return {
      backupCodes,
      message: 'New backup codes generated. Please save them securely.',
    };
  }

  // ==================== Private Methods ====================

  /**
   * Generate 10 random backup codes
   */
  private static generateBackupCodes(): string[] {
    const codes: string[] = [];
    for (let i = 0; i < 10; i++) {
      const code = crypto.randomBytes(4).toString('hex').toUpperCase();
      // Format as XXXX-XXXX for readability
      codes.push(`${code.slice(0, 4)}-${code.slice(4)}`);
    }
    return codes;
  }

  /**
   * Verify a backup code
   */
  private static async verifyBackupCode(user: User, code: string): Promise<boolean> {
    if (!user.mfaBackupCodes || user.mfaBackupCodes.length === 0) {
      return false;
    }

    // Normalize code (remove dashes, uppercase)
    const normalizedCode = code.replace(/-/g, '').toUpperCase();
    const formattedCode = `${normalizedCode.slice(0, 4)}-${normalizedCode.slice(4)}`;

    // Check each hashed backup code
    for (let i = 0; i < user.mfaBackupCodes.length; i++) {
      const isMatch = await bcrypt.compare(formattedCode, user.mfaBackupCodes[i]);
      if (isMatch) {
        // Remove the used backup code
        const updatedCodes = [...user.mfaBackupCodes];
        updatedCodes.splice(i, 1);
        user.mfaBackupCodes = updatedCodes;
        await this.userRepository.save(user);
        return true;
      }
    }

    return false;
  }

  /**
   * Encrypt MFA secret for storage
   */
  private static encryptSecret(secret: string): string {
    const encryptionKey = config.jwt.secret; // Using JWT secret as fallback

    // Use first 32 bytes of key for AES-256
    const key = crypto.createHash('sha256').update(encryptionKey).digest();
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

    let encrypted = cipher.update(secret, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    // Return IV + encrypted data
    return iv.toString('hex') + ':' + encrypted;
  }

  /**
   * Decrypt MFA secret from storage
   */
  private static decryptSecret(encryptedSecret: string): string {
    const encryptionKey = config.jwt.secret;

    const key = crypto.createHash('sha256').update(encryptionKey).digest();
    const [ivHex, encrypted] = encryptedSecret.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);

    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}
