// src/services/otpService.js
const bcrypt = require('bcrypt');
const db = require('../db');
const { markActiveOtp } = require('../middleware/rateLimitMiddleware');
// === SECURITY HARDENING: FIELD-LEVEL ENCRYPTION ===
const { encryptPhoneNumber } = require('../utils/fieldEncryption');

// === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND ===
// OTP validity changed from 10 minutes to 2 minutes (120 seconds)
const OTP_TTL_SECONDS = parseInt(process.env.OTP_TTL_SECONDS || '120', 10); // 2 minutes
const OTP_EXPIRY_MS = OTP_TTL_SECONDS * 1000;
const MAX_OTP_ATTEMPTS = Number(process.env.OTP_VERIFY_MAX_ATTEMPTS || process.env.OTP_MAX_ATTEMPTS || 5);

// === SECURITY HARDENING: TIMING ATTACK PROTECTION ===
// Pre-computed dummy hash for constant-time comparison when OTP not found
// This ensures bcrypt.compare() always executes with similar timing
// Generated once at module load to avoid performance impact
let dummyOtpHash = null;
async function getDummyOtpHash() {
  if (!dummyOtpHash) {
    // Generate a dummy hash that will never match any real OTP
    const dummyCode = 'DUMMY_OTP_' + Math.random().toString(36).substring(2, 15) + Date.now();
    dummyOtpHash = await bcrypt.hash(dummyCode, 10);
  }
  return dummyOtpHash;
}

/**
 * Extract country code from phone number (e.g., +91 from +919876543210)
 * @param {string} phoneNumber - Phone number in E.164 format
 * @returns {string} Country code (default: '+91')
 */
function extractCountryCode(phoneNumber) {
  if (!phoneNumber || !phoneNumber.startsWith('+')) {
    return '+91'; // Default to India
  }
  // Extract country code (typically 1-3 digits after +)
  const match = phoneNumber.match(/^\+(\d{1,3})/);
  return match ? `+${match[1]}` : '+91';
}

/**
 * Generate a random 6-digit OTP code
 */
function generateOtpCode() {
  return Math.floor(100000 + Math.random() * 900000).toString();
}

/**
 * Create and store an OTP for a phone number
 * @param {string} phoneNumber - The phone number to send OTP to
 * @returns {Promise<{code: string}>} - The generated OTP code
 */
async function createOtp(phoneNumber) {
  const code = generateOtpCode();
  const expiresAt = new Date(Date.now() + OTP_EXPIRY_MS);
  const otpHash = await bcrypt.hash(code, 10);

  // === SECURITY HARDENING: FIELD-LEVEL ENCRYPTION ===
  // Encrypt phone number before storing
  const encryptedPhone = encryptPhoneNumber(phoneNumber);
  const countryCode = extractCountryCode(phoneNumber);

  // === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND ===
  // Mark existing OTPs as deleted for this phone number
  // Note: We search by encrypted phone to handle both encrypted and plaintext (backward compatibility)
  await db.query(
    `UPDATE otp_requests 
     SET deleted = TRUE 
     WHERE (phone_number = $1 OR phone_number = $2) AND deleted = FALSE`,
    [encryptedPhone, phoneNumber] // Try both encrypted and plaintext for backward compatibility
  );

  // Insert new OTP with encrypted phone number (using otp_requests table from final_db.sql)
  await db.query(
    `INSERT INTO otp_requests (phone_number, country_code, otp_hash, expires_at, attempt_count, deleted)
     VALUES ($1, $2, $3, $4, 0, FALSE)`,
    [encryptedPhone, countryCode, otpHash, expiresAt]
  );

  // === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND ===
  // Mark OTP as active in Redis/memory store to prevent resend within 2 minutes
  try {
    await markActiveOtp(phoneNumber, OTP_TTL_SECONDS);
  } catch (err) {
    console.error('Failed to mark active OTP in rate limiter:', err);
    // Don't fail OTP creation if rate limiter fails
  }

  return { code };
}

/**
 * Verify an OTP code for a phone number
 * @param {string} phoneNumber - The phone number
 * @param {string} code - The OTP code to verify
 * @returns {Promise<{ok: boolean}>} - Whether the OTP is valid
 * 
 * === SECURITY HARDENING: TIMING ATTACK PROTECTION ===
 * This function uses constant-time execution to prevent timing-based attacks:
 * - Always performs bcrypt.compare() even for expired/max attempts cases
 * - Uses dummy hash for "not found" case to maintain constant time
 * - All code paths take similar execution time regardless of outcome
 */
async function verifyOtp(phoneNumber, code) {
  // === SECURITY HARDENING: FIELD-LEVEL ENCRYPTION ===
  // Encrypt phone number for search (handles both encrypted and plaintext for backward compatibility)
  const encryptedPhone = encryptPhoneNumber(phoneNumber);
  
  const result = await db.query(
    `SELECT id, otp_hash, expires_at, attempt_count, phone_number, consumed_at
     FROM otp_requests
     WHERE (phone_number = $1 OR phone_number = $2)
       AND deleted = FALSE
       AND consumed_at IS NULL
     ORDER BY created_at DESC
     LIMIT 1`,
    [encryptedPhone, phoneNumber] // Try both encrypted and plaintext for backward compatibility
  );

  // === SECURITY HARDENING: TIMING ATTACK PROTECTION ===
  // Always perform bcrypt.compare() to maintain constant execution time
  // Use a dummy hash if OTP not found to prevent timing leaks
  let otpRecord = result.rows[0];
  let isNotFound = false;
  let isExpired = false;
  let isMaxAttempts = false;
  let hashToCompare = null;
  
  if (!otpRecord) {
    // OTP not found - use dummy hash to maintain constant time
    // This ensures bcrypt.compare() always executes with similar timing
    isNotFound = true;
    hashToCompare = await getDummyOtpHash();
    otpRecord = {
      id: null,
      otp_hash: hashToCompare, // Dummy hash for constant-time comparison
      expires_at: new Date(Date.now() + 1000), // Future date
      attempt_count: 0,
    };
  } else {
    hashToCompare = otpRecord.otp_hash;
    // Check if expired (but don't return early - continue to bcrypt.compare)
    isExpired = new Date() > new Date(otpRecord.expires_at);
    
    // Check if max attempts exceeded (but don't return early - continue to bcrypt.compare)
    isMaxAttempts = otpRecord.attempt_count >= MAX_OTP_ATTEMPTS;
  }

  // === SECURITY HARDENING: TIMING ATTACK PROTECTION ===
  // Always perform bcrypt.compare() regardless of expiration/attempts status
  // This ensures all code paths take similar time
  // Even if we know the OTP is expired or max attempts exceeded, we still compare
  // to prevent attackers from distinguishing between different failure modes
  const matches = await bcrypt.compare(code, hashToCompare);

  // === SECURITY HARDENING: TIMING ATTACK PROTECTION ===
  // Determine the actual result after constant-time comparison
  // Priority: not_found > expired > max_attempts > invalid > valid
  if (isNotFound) {
    // OTP not found - return generic error
    return { ok: false, reason: 'not_found' };
  }

  if (isExpired) {
    // OTP expired - mark as consumed (but we already did bcrypt.compare for constant time)
    await db.query(
      'UPDATE otp_requests SET consumed_at = NOW() WHERE id = $1',
      [otpRecord.id]
    );
    return { ok: false, reason: 'expired' };
  }

  if (isMaxAttempts) {
    // Max attempts exceeded - mark as consumed (but we already did bcrypt.compare for constant time)
    await db.query(
      'UPDATE otp_requests SET consumed_at = NOW() WHERE id = $1',
      [otpRecord.id]
    );
    return { ok: false, reason: 'max_attempts' };
  }

  // Check if code matches (only if not expired and not max attempts)
  if (!matches) {
    // === ADDED FOR OTP ATTEMPT LIMIT ===
    // Increment attempt count
    await db.query(
      'UPDATE otp_requests SET attempt_count = attempt_count + 1 WHERE id = $1',
      [otpRecord.id]
    );
    return { ok: false, reason: 'invalid' };
  }

  // === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND ===
  // Mark OTP as consumed once verified to prevent reuse
  // Using consumed_at instead of deleting (matches final_db.sql schema)
  await db.query(
    'UPDATE otp_requests SET consumed_at = NOW() WHERE id = $1',
    [otpRecord.id]
  );

  return { ok: true };
}

/**
 * Clean up expired OTPs (can be called periodically)
 * Marks expired OTPs as consumed instead of deleting (matches final_db.sql schema)
 */
async function cleanupExpiredOtps() {
  await db.query(
    `UPDATE otp_requests 
     SET consumed_at = NOW() 
     WHERE expires_at < NOW() 
       AND consumed_at IS NULL 
       AND deleted = FALSE`
  );
}

module.exports = {
  createOtp,
  verifyOtp,
  cleanupExpiredOtps,
};
