/**
 * Geo-Currency Service
 * Detects user's country from IP and maps to currency/payment gateway.
 * Provides currency conversion using exchange rates from CurrencyConfig.
 */

import { getModels } from '../models';

/**
 * Region pricing rules — the single source of truth for "which currency and
 * which gateway does this buyer get".
 *
 *   India (IN)  → INR (₹)   via Razorpay
 *   UAE   (AE)  → AED (د.إ) via Stripe
 *   Everywhere else → USD ($) via Stripe
 *
 * Both the displayed price and the amount actually charged resolve through
 * here, so they can never disagree.
 */
export interface RegionRule {
  currency: string;
  symbol: string;
  name: string;
  gateway: 'stripe' | 'razorpay';
  /** Used only when CurrencyConfig has no row for this currency. */
  fallbackRateToUSD: number;
}

const COUNTRY_RULES: Record<string, RegionRule> = {
  IN: { currency: 'INR', symbol: '₹', name: 'Indian Rupee', gateway: 'razorpay', fallbackRateToUSD: 0.012 },
  AE: { currency: 'AED', symbol: 'د.إ', name: 'UAE Dirham', gateway: 'stripe', fallbackRateToUSD: 0.27 },
};

/** Applied to every country not listed in COUNTRY_RULES. */
const DEFAULT_RULE: RegionRule = {
  currency: 'USD', symbol: '$', name: 'US Dollar', gateway: 'stripe', fallbackRateToUSD: 1,
};

/** Currency each gateway can actually process in this deployment. */
const GATEWAY_CURRENCIES: Record<'stripe' | 'razorpay', string[]> = {
  stripe: ['USD', 'AED', 'EUR', 'GBP'],
  razorpay: ['INR'],
};

/** True when `gateway` can charge in `currency` — guards cross-gateway fallbacks. */
export function gatewaySupportsCurrency(gateway: 'stripe' | 'razorpay', currency: string): boolean {
  return GATEWAY_CURRENCIES[gateway]?.includes(currency.toUpperCase()) ?? false;
}

/** Last-resort USD rate for a currency when CurrencyConfig has no row for it. */
function fallbackRateForCurrency(currencyCode: string): number {
  const code = currencyCode.toUpperCase();
  if (code === DEFAULT_RULE.currency) return DEFAULT_RULE.fallbackRateToUSD;
  const rule = Object.values(COUNTRY_RULES).find(r => r.currency === code);
  return rule?.fallbackRateToUSD ?? 0;
}

/** The currency/gateway rule for a country code, never null. */
export function getRegionRule(countryCode?: string | null): RegionRule {
  const code = typeof countryCode === 'string' ? countryCode.trim().toUpperCase() : '';
  return COUNTRY_RULES[code] || DEFAULT_RULE;
}

/**
 * Make sure the three currencies the pricing rules reference exist in
 * CurrencyConfig. Previously only the /prices route seeded them, so any other
 * entry point (checkout, renew, change plan) silently fell back to USD on a
 * fresh database.
 */
let seedPromise: Promise<void> | null = null;

export async function ensureCurrencyConfigSeed(): Promise<void> {
  // Memoised: /prices converts five cycles per package, and each conversion
  // would otherwise re-run countDocuments().
  if (seedPromise) return seedPromise;

  seedPromise = (async () => {
    try {
      const { CurrencyConfig } = getModels();
      const count = await CurrencyConfig.countDocuments();
      if (count > 0) return;

      await CurrencyConfig.insertMany([
        { code: 'USD', name: 'US Dollar', symbol: '$', paymentGateway: 'stripe', isActive: true, isDefault: true, exchangeRateToUSD: 1 },
        { code: 'INR', name: 'Indian Rupee', symbol: '₹', paymentGateway: 'razorpay', isActive: true, isDefault: false, exchangeRateToUSD: 0.012 },
        { code: 'AED', name: 'UAE Dirham', symbol: 'د.إ', paymentGateway: 'stripe', isActive: true, isDefault: false, exchangeRateToUSD: 0.27 },
      ]);
      console.log('[CurrencyConfig] Seeded default currencies (USD / INR / AED)');
    } catch (error) {
      // Don't cache a failure — the DB may just not be connected yet.
      seedPromise = null;
      console.warn('[CurrencyConfig] Seeding failed:', error);
    }
  })();

  return seedPromise;
}

/**
 * True for loopback / RFC1918 / link-local addresses — i.e. any address that
 * tells us nothing about the user's real region. Behind the Next.js `rewrites()`
 * proxy every request looks like this, which is why a client-supplied country
 * hint is needed (see resolveRequestCountry).
 */
export function isPrivateOrLoopbackIp(ip: string): boolean {
  if (!ip) return true;
  const normalized = ip.replace(/^::ffff:/, '').trim();
  return (
    normalized === '127.0.0.1' ||
    normalized === '::1' ||
    normalized === 'localhost' ||
    normalized.startsWith('10.') ||
    normalized.startsWith('192.168.') ||
    normalized.startsWith('169.254.') ||
    /^172\.(1[6-9]|2\d|3[01])\./.test(normalized) ||
    normalized.startsWith('fc') ||
    normalized.startsWith('fd')
  );
}

/**
 * Resolve the effective country for a request.
 *
 * Server-side IP detection is authoritative whenever the request carries a real
 * public address AND the lookup actually succeeded. The client hint is only
 * consulted when the server genuinely cannot tell: a private/loopback address
 * (behind the app proxy) or a failed lookup.
 *
 * The distinction matters: `lookupCountryFromIp` returns null — not 'US' — when
 * it fails, so a real US detection (e.g. a buyer on a US VPN) is no longer
 * mistaken for "detection failed" and overridden by a stale client hint.
 */
export async function resolveRequestCountry(
  ip: string,
  clientHint?: string | null
): Promise<string> {
  const hint = typeof clientHint === 'string' && /^[A-Za-z]{2}$/.test(clientHint)
    ? clientHint.toUpperCase()
    : null;

  if (!isPrivateOrLoopbackIp(ip)) {
    const detected = await lookupCountryFromIp(ip);
    if (detected) {
      console.log(`[GeoCurrency] Country from public IP ${ip}: ${detected} (hint was ${hint ?? 'none'})`);
      return detected;
    }
    console.warn(`[GeoCurrency] GeoIP lookup failed for ${ip} — using client hint ${hint ?? 'none'}`);
  }

  return hint ?? 'US';
}

/**
 * Look up the country for a public IP.
 * Returns null when the address carries no region info or the lookup fails —
 * callers must not confuse that with a genuine 'US' result.
 */
export async function lookupCountryFromIp(ip: string): Promise<string | null> {
  if (isPrivateOrLoopbackIp(ip)) {
    return null;
  }

  try {
    // Use ip-api.com free tier (45 requests per minute)
    const response = await fetch(`http://ip-api.com/json/${ip}?fields=countryCode`);
    if (response.ok) {
      const data = await response.json() as { countryCode?: string };
      if (typeof data.countryCode === 'string' && /^[A-Za-z]{2}$/.test(data.countryCode)) {
        return data.countryCode.toUpperCase();
      }
    }
  } catch (error) {
    console.warn('GeoIP lookup failed:', error);
  }

  return null;
}

/**
 * @deprecated Use lookupCountryFromIp — it reports failure as null instead of
 * masquerading as a US detection. Kept for any external callers.
 */
export async function detectCountryFromIp(ip: string): Promise<string> {
  return (await lookupCountryFromIp(ip)) ?? 'US';
}

/**
 * Resolve the currency + gateway a country is priced and charged in.
 *
 * The country → currency → gateway decision comes from COUNTRY_RULES, so it is
 * identical on every code path. CurrencyConfig only supplies the presentation
 * details (name, symbol) and the exchange rate, and may override the gateway —
 * but only with one that can actually process the currency, so a mis-set admin
 * row can never route an AED charge to Razorpay.
 *
 * Never returns null: on a DB failure the hardcoded rule is used.
 */
export async function getCurrencyForCountry(countryCode: string): Promise<{
  code: string;
  name: string;
  symbol: string;
  gateway: 'stripe' | 'razorpay';
  exchangeRateToUSD: number;
}> {
  const rule = getRegionRule(countryCode);

  try {
    await ensureCurrencyConfigSeed();
    const { CurrencyConfig } = getModels();
    const currency = await CurrencyConfig.findOne({ code: rule.currency, isActive: true }).lean() as any;

    if (currency) {
      const configuredGateway = currency.paymentGateway as 'stripe' | 'razorpay' | undefined;
      return {
        code: rule.currency,
        name: currency.name || rule.name,
        symbol: currency.symbol || rule.symbol,
        // Honour the Currency Rules module, but only when the configured
        // gateway can charge in this currency.
        gateway: configuredGateway && gatewaySupportsCurrency(configuredGateway, rule.currency)
          ? configuredGateway
          : rule.gateway,
        exchangeRateToUSD: typeof currency.exchangeRateToUSD === 'number'
          ? currency.exchangeRateToUSD
          : rule.fallbackRateToUSD,
      };
    }
  } catch (error) {
    console.warn('[CurrencyService] Failed to load currency config, using region rule:', error);
  }

  return {
    code: rule.currency,
    name: rule.name,
    symbol: rule.symbol,
    gateway: rule.gateway,
    exchangeRateToUSD: rule.fallbackRateToUSD,
  };
}

/**
 * Get the payment gateway for a given currency code.
 */
export function getGatewayForCurrency(currencyCode: string): 'stripe' | 'razorpay' {
  switch (currencyCode.toUpperCase()) {
    case 'INR': return 'razorpay';
    default: return 'stripe'; // USD, AED, and all others use Stripe
  }
}

/**
 * Convert an amount from one currency to another using exchange rates from CurrencyConfig.
 *
 * Exchange rates in CurrencyConfig represent: 1 unit of currency = X USD
 *   e.g., INR exchangeRateToUSD = 0.012 means 1 INR = 0.012 USD
 *   e.g., AED exchangeRateToUSD = 0.27  means 1 AED = 0.27 USD
 *   e.g., USD exchangeRateToUSD = 1       means 1 USD = 1 USD
 *
 * Conversion formula:
 *   targetAmount = sourceAmount * (sourceRateToUSD / targetRateToUSD)
 *
 * Examples:
 *   $29 USD → INR: 29 * (1 / 0.012) = 2416.67 INR
 *   ₹1000 INR → USD: 1000 * (0.012 / 1) = 12 USD
 *   ₹1000 INR → AED: 1000 * (0.012 / 0.27) = 44.44 AED
 */
export async function convertCurrency(
  amount: number,
  fromCurrency: string,
  toCurrency: string
): Promise<{ amount: number; exchangeRate: number }> {
  // Same currency — no conversion needed
  if (fromCurrency === toCurrency) {
    return { amount, exchangeRate: 1 };
  }

  try {
    await ensureCurrencyConfigSeed();
    const { CurrencyConfig } = getModels();
    const currencies = await CurrencyConfig.find({ isActive: true }).lean();

    const fromConfig = currencies.find((c: any) => c.code === fromCurrency);
    const toConfig = currencies.find((c: any) => c.code === toCurrency);

    // Fall back to the hardcoded region rates rather than skipping conversion —
    // returning the raw number under a different currency symbol would show a
    // ₹29 price where $29 was meant.
    const fromRate = fromConfig?.exchangeRateToUSD ?? fallbackRateForCurrency(fromCurrency);
    const toRate = toConfig?.exchangeRateToUSD ?? fallbackRateForCurrency(toCurrency);

    if (!fromConfig || !toConfig) {
      console.warn(`[CurrencyService] Currency config not found for ${fromCurrency} or ${toCurrency} — using fallback rates ${fromRate}/${toRate}`);
    }

    if (!fromRate || !toRate) {
      return { amount, exchangeRate: 1 };
    }

    // Convert: targetAmount = sourceAmount * (fromRate / toRate)
    const exchangeRate = fromRate / toRate;
    const convertedAmount = Math.round(amount * exchangeRate * 100) / 100;

    console.log(`[CurrencyService] Converted ${amount} ${fromCurrency} → ${convertedAmount} ${toCurrency} (rate: ${fromRate}/${toRate} = ${exchangeRate.toFixed(4)})`);

    return { amount: convertedAmount, exchangeRate };
  } catch (error) {
    console.warn('[CurrencyService] Currency conversion failed, using original amount:', error);
    return { amount, exchangeRate: 1 };
  }
}

/**
 * Get all active currencies with their exchange rates.
 * Used by the frontend to display prices in the user's local currency.
 */
export async function getAllCurrencyRates(): Promise<Array<{
  code: string;
  name: string;
  symbol: string;
  exchangeRateToUSD: number;
  gateway: string;
}>> {
  try {
    const { CurrencyConfig } = getModels();
    const currencies = await CurrencyConfig.find({ isActive: true }).lean();
    return currencies.map((c: any) => ({
      code: c.code,
      name: c.name,
      symbol: c.symbol,
      exchangeRateToUSD: c.exchangeRateToUSD,
      gateway: c.paymentGateway,
    }));
  } catch (error) {
    console.warn('[CurrencyService] Failed to fetch currency rates:', error);
    return [];
  }
}