const crypto = require("crypto");
const { authenticator } = require("otplib");
const QRCode = require("qrcode");
const bcrypt = require("bcryptjs");
const logger = require("../utils/logger");

// Configuration
const TOTP_ISSUER = process.env.TOTP_ISSUER || "VillageShare";
const EMAIL_OTP_EXPIRY_MINUTES = parseInt(
  process.env.TWO_FACTOR_EMAIL_OTP_EXPIRY_MINUTES || "10",
  10
);
const STEP_UP_VALIDITY_MINUTES = parseInt(
  process.env.TWO_FACTOR_STEP_UP_VALIDITY_MINUTES || "5",
  10
);
const MAX_EMAIL_OTP_ATTEMPTS = 3;
const RECOVERY_CODE_COUNT = 10;
const BCRYPT_ROUNDS = 12;

// Characters for recovery codes (excludes confusing chars: 0, O, 1, I, L)
const RECOVERY_CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";

class TwoFactorService {
  /**
   * Generate a new TOTP secret and QR code for setup
   * @param {string} email - User's email address
   * @returns {Promise<{qrCodeDataUrl: string, encryptedSecret: string, encryptedSecretIv: string}>}
   */
  static async generateTotpSecret(email) {
    const secret = authenticator.generateSecret();
    const otpAuthUrl = authenticator.keyuri(email, TOTP_ISSUER, secret);

    // Generate QR code as data URL
    const qrCodeDataUrl = await QRCode.toDataURL(otpAuthUrl);

    // Encrypt the secret for storage
    const { encrypted, iv } = this._encryptSecret(secret);

    return {
      qrCodeDataUrl,
      encryptedSecret: encrypted,
      encryptedSecretIv: iv,
    };
  }

  /**
   * Verify a TOTP code against a user's secret
   * @param {string} encryptedSecret - Encrypted TOTP secret
   * @param {string} iv - Initialization vector for decryption
   * @param {string} code - 6-digit TOTP code to verify
   * @returns {boolean}
   */
  static verifyTotpCode(encryptedSecret, iv, code) {
    try {
      // Validate code format
      if (!/^\d{6}$/.test(code)) {
        return false;
      }

      // Decrypt the secret
      const secret = this._decryptSecret(encryptedSecret, iv);

      // Verify with window 0 (only current 30-second period) to prevent replay attacks
      return authenticator.verify({ token: code, secret, window: 0 });
    } catch (error) {
      logger.error("TOTP verification error:", error);
      return false;
    }
  }

  /**
   * Generate a 6-digit email OTP code
   * @returns {{code: string, hashedCode: string, expiry: Date}}
   */
  static generateEmailOtp() {
    // Generate 6-digit numeric code
    const code = crypto.randomInt(100000, 999999).toString();

    // Hash the code for storage (SHA-256)
    const hashedCode = crypto.createHash("sha256").update(code).digest("hex");

    // Calculate expiry
    const expiry = new Date(Date.now() + EMAIL_OTP_EXPIRY_MINUTES * 60 * 1000);

    return { code, hashedCode, expiry };
  }

  /**
   * Verify an email OTP code using timing-safe comparison
   * @param {string} inputCode - Code entered by user
   * @param {string} storedHash - Hashed code stored in database
   * @param {Date} expiry - Expiry timestamp
   * @returns {boolean}
   */
  static verifyEmailOtp(inputCode, storedHash, expiry) {
    try {
      // Validate code format
      if (!/^\d{6}$/.test(inputCode)) {
        return false;
      }

      // Check expiry
      if (!expiry || new Date() > new Date(expiry)) {
        return false;
      }

      // Hash the input code
      const inputHash = crypto
        .createHash("sha256")
        .update(inputCode)
        .digest("hex");

      // Timing-safe comparison
      const inputBuffer = Buffer.from(inputHash, "hex");
      const storedBuffer = Buffer.from(storedHash, "hex");

      if (inputBuffer.length !== storedBuffer.length) {
        return false;
      }

      return crypto.timingSafeEqual(inputBuffer, storedBuffer);
    } catch (error) {
      logger.error("Email OTP verification error:", error);
      return false;
    }
  }

  /**
   * Generate recovery codes (10 codes in XXXX-XXXX format)
   * @returns {Promise<{codes: string[], hashedCodes: string[]}>}
   */
  static async generateRecoveryCodes() {
    const codes = [];
    const hashedCodes = [];

    for (let i = 0; i < RECOVERY_CODE_COUNT; i++) {
      // Generate code in XXXX-XXXX format
      let code = "";
      for (let j = 0; j < 8; j++) {
        if (j === 4) code += "-";
        code +=
          RECOVERY_CODE_CHARS[crypto.randomInt(RECOVERY_CODE_CHARS.length)];
      }
      codes.push(code);

      // Hash the code for storage
      const hashedCode = await bcrypt.hash(code, BCRYPT_ROUNDS);
      hashedCodes.push(hashedCode);
    }

    return { codes, hashedCodes };
  }

  /**
   * Verify a recovery code and return the index if valid
   * @param {string} inputCode - Recovery code entered by user
   * @param {Object} recoveryData - Recovery codes data (structured format)
   * @returns {Promise<{valid: boolean, index: number}>}
   */
  static async verifyRecoveryCode(inputCode, recoveryData) {
    // Normalize input (uppercase, ensure format)
    const normalizedCode = inputCode.toUpperCase().trim();

    // Validate format
    if (!/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(normalizedCode)) {
      return { valid: false, index: -1 };
    }

    // Handle both old format (array) and new format (structured object)
    const codes = recoveryData.version
      ? recoveryData.codes
      : recoveryData.map((hash, i) => ({
          hash,
          used: hash === null,
          index: i,
        }));

    // Check each code
    for (let i = 0; i < codes.length; i++) {
      const codeEntry = codes[i];

      // Skip already used codes
      if (codeEntry.used || !codeEntry.hash) continue;

      const isMatch = await bcrypt.compare(normalizedCode, codeEntry.hash);
      if (isMatch) {
        return { valid: true, index: i };
      }
    }

    return { valid: false, index: -1 };
  }

  /**
   * Validate if a step-up session is still valid
   * @param {Object} user - User object with twoFactorVerifiedAt field
   * @param {number} maxAgeMinutes - Maximum age in minutes (default: 15)
   * @returns {boolean}
   */
  static validateStepUpSession(user, maxAgeMinutes = STEP_UP_VALIDITY_MINUTES) {
    if (!user.twoFactorVerifiedAt) {
      return false;
    }

    const verifiedAt = new Date(user.twoFactorVerifiedAt);
    const maxAge = maxAgeMinutes * 60 * 1000;
    const now = Date.now();

    return now - verifiedAt.getTime() < maxAge;
  }

  /**
   * Get the count of remaining recovery codes
   * @param {Object|Array} recoveryData - Recovery codes data (structured or legacy format)
   * @returns {number}
   */
  static getRemainingRecoveryCodesCount(recoveryData) {
    if (!recoveryData) {
      return 0;
    }

    // Handle new structured format
    if (recoveryData.version) {
      return recoveryData.codes.filter((code) => !code.used).length;
    }

    // Handle legacy array format
    if (Array.isArray(recoveryData)) {
      return recoveryData.filter((code) => code !== null && code !== "").length;
    }

    return 0;
  }

  /**
   * Encrypt a TOTP secret using AES-256-GCM
   * @param {string} secret - Plain text secret
   * @returns {{encrypted: string, iv: string}}
   * @private
   */
  static _encryptSecret(secret) {
    const encryptionKey = process.env.TOTP_ENCRYPTION_KEY;
    if (!encryptionKey || encryptionKey.length !== 64) {
      throw new Error(
        "TOTP_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)"
      );
    }

    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(
      "aes-256-gcm",
      Buffer.from(encryptionKey, "hex"),
      iv
    );

    let encrypted = cipher.update(secret, "utf8", "hex");
    encrypted += cipher.final("hex");
    const authTag = cipher.getAuthTag().toString("hex");

    return {
      encrypted: encrypted + ":" + authTag,
      iv: iv.toString("hex"),
    };
  }

  /**
   * Decrypt a TOTP secret using AES-256-GCM
   * @param {string} encryptedData - Encrypted data with auth tag
   * @param {string} iv - Initialization vector (hex)
   * @returns {string} - Decrypted secret
   * @private
   */
  static _decryptSecret(encryptedData, iv) {
    const encryptionKey = process.env.TOTP_ENCRYPTION_KEY;
    if (!encryptionKey || encryptionKey.length !== 64) {
      throw new Error(
        "TOTP_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)"
      );
    }

    const [ciphertext, authTag] = encryptedData.split(":");
    const decipher = crypto.createDecipheriv(
      "aes-256-gcm",
      Buffer.from(encryptionKey, "hex"),
      Buffer.from(iv, "hex")
    );
    decipher.setAuthTag(Buffer.from(authTag, "hex"));

    let decrypted = decipher.update(ciphertext, "hex", "utf8");
    decrypted += decipher.final("utf8");
    return decrypted;
  }

  /**
   * Check if email OTP attempts are locked
   * @param {number} attempts - Current attempt count
   * @returns {boolean}
   */
  static isEmailOtpLocked(attempts) {
    return attempts >= MAX_EMAIL_OTP_ATTEMPTS;
  }
}

module.exports = TwoFactorService;
