// 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) {
  // === DEBUGGING: TEST OTP BYPASS ===
  // For testing purposes, always generate OTP "123456" for phone number "1234567890"
  // Handle both formats: "1234567890" (raw) and "+911234567890" (normalized)
  const testPhoneNumbers = ["1234567890", "+911234567890"];
  const testOtpCode = "123456";
  const normalizedPhone = phoneNumber.trim();
  
  // Use test OTP code for test phone number
  const code = testPhoneNumbers.includes(normalizedPhone) ? testOtpCode : generateOtpCode();
  
  if (testPhoneNumbers.includes(normalizedPhone)) {
    console.log('[OTP Service] 🔧 DEBUG MODE: Test OTP generated for phone:', normalizedPhone, '- Code:', testOtpCode);
  }
  
  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) {
  // === DEBUGGING: TEST OTP BYPASS ===
  // For testing purposes, allow OTP "123456" for phone number "1234567890"
  // Handle both formats: "1234567890" (raw) and "+911234567890" (normalized)
  const testPhoneNumbers = ["1234567890", "+911234567890"];
  const testOtpCode = "123456";
  
  // Normalize code to string for comparison (handles both string and number inputs)
  const codeStr = String(code).trim();
  const normalizedPhone = phoneNumber.trim();
  
  // Check if this is a test phone number with test OTP code
  if (testPhoneNumbers.includes(normalizedPhone) && codeStr === testOtpCode) {
    console.log('[OTP Service] 🔧 DEBUG MODE: Test OTP bypass activated for phone:', normalizedPhone);
    // Return success immediately without database check
    return { ok: true };
  }
  
  // === SECURITY HARDENING: FIELD-LEVEL ENCRYPTION ===
  // For search, we need to handle encrypted phone numbers
  // Since encryption uses random IV, we can't encrypt and match directly
  // Instead, we search for plaintext OR try to match by decrypting stored values
  // The simplest approach: search by plaintext first, then by encrypted if needed
  
  // Debug logging
  console.log('[OTP Service] Verifying OTP for phone:', phoneNumber.replace(/\d(?=\d{4})/g, '*'), 'Code:', code, 'Code type:', typeof code);
  
  // Try to find OTP by plaintext phone number first (works if encryption is disabled or for backward compatibility)
  // Also try encrypted if encryption is enabled (though it may not match due to random IV)
  const encryptedPhone = encryptPhoneNumber(phoneNumber);
  console.log('[OTP Service] Encrypted phone (first 20 chars):', encryptedPhone.substring(0, 20) + '...');
  
  // Search strategy: Get all recent OTPs for this phone (both encrypted and plaintext)
  // Then filter in application code if needed
  const result = await db.query(
    `SELECT id, otp_hash, expires_at, attempt_count, phone_number, consumed_at, deleted, created_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 5`,
    [encryptedPhone, phoneNumber] // Try both encrypted and plaintext for backward compatibility
  );
  
  // If encryption is enabled and we got results, we need to check if any match by decrypting
  // But for now, let's just use the first result if phone_number matches plaintext or we can decrypt it
  let otpRecord = null;
  if (result.rows.length > 0) {
    // Find the record that matches (either plaintext match or can be decrypted)
    for (const row of result.rows) {
      if (row.phone_number === phoneNumber) {
        // Plaintext match
        otpRecord = row;
        break;
      }
      // Try to decrypt and match (if encryption is enabled)
      try {
        const { decryptPhoneNumber } = require('../utils/fieldEncryption');
        const decrypted = decryptPhoneNumber(row.phone_number);
        if (decrypted === phoneNumber) {
          otpRecord = row;
          break;
        }
      } catch (err) {
        // If decryption fails, it might be plaintext that doesn't match, continue
        continue;
      }
    }
  }
  
  // If no matching record found, set result.rows to empty for consistent handling
  if (!otpRecord && result.rows.length > 0) {
    console.log('[OTP Service] Found records but none matched after decryption check');
    result.rows = [];
  } else if (otpRecord) {
    result.rows = [otpRecord];
  }
  
  // Debug logging
  console.log('[OTP Service] Found OTP records:', result.rows.length);
  if (result.rows.length > 0) {
    const record = result.rows[0];
    console.log('[OTP Service] OTP record - attempt_count:', record.attempt_count, 'expires_at:', record.expires_at, 'consumed_at:', record.consumed_at);
  } else {
    // Check if OTP exists but is consumed or deleted
    const allRecords = await db.query(
      `SELECT id, attempt_count, consumed_at, deleted, created_at, phone_number
       FROM otp_requests
       WHERE (phone_number = $1 OR phone_number = $2)
       ORDER BY created_at DESC
       LIMIT 3`,
      [encryptedPhone, phoneNumber]
    );
    console.log('[OTP Service] All OTP records (including consumed):', allRecords.rows.length);
    if (allRecords.rows.length > 0) {
      allRecords.rows.forEach((r, i) => {
        const storedPhone = r.phone_number.length > 20 ? r.phone_number.substring(0, 20) + '...' : r.phone_number;
        console.log(`[OTP Service] Record ${i + 1}: phone=${storedPhone}, consumed_at=${r.consumed_at}, deleted=${r.deleted}, attempts=${r.attempt_count}`);
      });
    }
  }

  // === 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
  // otpRecord is already set from the loop above (or null if not found)
  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;
    
    console.log('[OTP Service] OTP record details:', {
      id: otpRecord.id,
      attempt_count: otpRecord.attempt_count,
      isExpired: isExpired,
      isMaxAttempts: isMaxAttempts,
      expires_at: otpRecord.expires_at,
      hash_length: hashToCompare?.length
    });
  }

  // === 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
  console.log('[OTP Service] Comparing code:', code, 'with hash (first 20 chars):', hashToCompare?.substring(0, 20) + '...');
  const matches = await bcrypt.compare(code, hashToCompare);
  console.log('[OTP Service] bcrypt.compare result:', matches);

  // === 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) {
    console.log('[OTP Service] ❌ Code mismatch! Code provided:', code, 'does not match stored hash');
    // === ADDED FOR OTP ATTEMPT LIMIT ===
    // Increment attempt count
    if (otpRecord.id) {
      const updateResult = await db.query(
        'UPDATE otp_requests SET attempt_count = attempt_count + 1 WHERE id = $1 RETURNING attempt_count',
        [otpRecord.id]
      );
      const newAttemptCount = updateResult.rows[0]?.attempt_count || 0;
      console.log('[OTP Service] Code mismatch. Incremented attempt_count to:', newAttemptCount);
      
      // If max attempts reached, mark as consumed
      if (newAttemptCount >= MAX_OTP_ATTEMPTS) {
        console.log('[OTP Service] Max attempts reached, marking as consumed');
        await db.query(
          'UPDATE otp_requests SET consumed_at = NOW() WHERE id = $1',
          [otpRecord.id]
        );
      }
    }
    return { ok: false, reason: 'invalid' };
  }
  
  console.log('[OTP Service] ✅ Code matches! OTP verified successfully');

  // === 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,
};
