"""Historical FX lookup for converting foreign funds to IDR.

Uses Frankfurter (https://www.frankfurter.app) — free, no API key, ECB historical
rates. Returns the rate for the requested date (Frankfurter falls back to the most
recent prior business day automatically).
"""
import logging
from decimal import Decimal, InvalidOperation

import requests

logger = logging.getLogger(__name__)

# Upstream is a third party inside our own request path, so keep the ceiling
# low — a slow FX provider must not dominate our response time. A miss degrades
# to "rate unavailable", which callers already handle.
HTTP_TIMEOUT_SECONDS = 2

# In-process memo: {(currency, iso_date): Decimal}. Only *resolved* historical
# rates land here — those are immutable, so keeping them for the process
# lifetime is safe. Failures are deliberately never memoized so they retry.
# The durable, cross-process cache is the ExchangeRate table; this is just the
# hot path in front of it.
_CACHE: dict[tuple[str, str], Decimal] = {}


def _cache_get(currency: str, iso: str) -> Decimal | None:
    """Resolved rate from the memo, else the ExchangeRate table, else None.

    A stored row whose rate is NULL records a past failure; that reads as a miss
    so it gets retried rather than being treated as permanently unavailable.
    """
    key = (currency, iso)
    cached = _CACHE.get(key)
    if cached is not None:
        return cached

    from .models import ExchangeRate
    rate = (
        ExchangeRate.objects.filter(currency=currency, date=iso, rate__isnull=False)
        .values_list("rate", flat=True)
        .first()
    )
    if rate is not None:
        _CACHE[key] = rate
    return rate


def _cache_put(currency: str, iso: str, rate: Decimal | None) -> None:
    from .models import ExchangeRate
    # Only memoize successes — a failed lookup must stay retryable.
    if rate is not None:
        _CACHE[(currency, iso)] = rate
    # Historical rates are immutable, so an existing row is never stale. Failures
    # are still recorded (rate=NULL) to keep a trail of what couldn't resolve.
    ExchangeRate.objects.update_or_create(
        currency=currency, date=iso, defaults={"rate": rate},
    )


def _fetch_remote(currency: str, iso: str) -> Decimal | None:
    url = f"https://api.frankfurter.app/{iso}?from={currency}&to=IDR"
    try:
        resp = requests.get(url, timeout=HTTP_TIMEOUT_SECONDS)
        resp.raise_for_status()
        raw = resp.json().get("rates", {}).get("IDR")
        if raw is not None:
            return Decimal(str(raw))
    except (requests.RequestException, ValueError, InvalidOperation, KeyError) as exc:
        logger.warning("FX lookup failed for %s on %s: %s", currency, iso, exc)
    return None


def fetch_idr_rate(currency: str, date) -> Decimal | None:
    """IDR per 1 unit of `currency` on `date` (a date object). None on failure.

    Resolves from the request-local memo, then the ExchangeRate table, and only
    then from the upstream API. `currency == "IDR"` short-circuits to 1.
    """
    currency = (currency or "").upper()
    if currency == "IDR":
        return Decimal("1")
    if not currency or date is None:
        return None

    iso = date.isoformat()
    cached = _cache_get(currency, iso)
    if cached is not None:
        return cached

    rate = _fetch_remote(currency, iso)
    _cache_put(currency, iso, rate)
    return rate


def prefetch_idr_rates(pairs) -> None:
    """Warm the cache for an iterable of (currency, date) pairs.

    Resolves every distinct pair up front — one DB query for what's already
    stored, then one upstream call per genuinely unknown pair. Callers that
    serialize many rows should call this first so per-row `fetch_idr_rate`
    lookups all hit the memo instead of issuing their own round-trips.
    """
    from .models import ExchangeRate

    wanted = {
        ((c or "").upper(), d.isoformat())
        for c, d in pairs
        if c and (c or "").upper() != "IDR" and d is not None
    }
    wanted -= _CACHE.keys()
    if not wanted:
        return

    # One query covers every pair; filtering per pair would defeat the purpose.
    currencies = {c for c, _ in wanted}
    dates = {iso for _, iso in wanted}
    stored = ExchangeRate.objects.filter(
        currency__in=currencies, date__in=dates,
    ).values_list("currency", "date", "rate")
    for currency, date, rate in stored:
        key = (currency, date.isoformat())
        if key in wanted and rate is not None:
            _CACHE[key] = rate

    for currency, iso in sorted(wanted - _CACHE.keys()):
        _cache_put(currency, iso, _fetch_remote(currency, iso))


def clear_rate_memo() -> None:
    """Drop the request-local memo. The ExchangeRate table is untouched."""
    _CACHE.clear()


def convert_between(value, from_ccy, to_ccy, fund=None):
    """Convert `value` from `from_ccy` to `to_ccy` at today's rate, pivoting
    through IDR. Both legs use today's rate for a consistent conversion.

    `fund` is accepted for call-site compatibility but is not used for the rate.
    Returns a Decimal, or None if a needed rate can't be resolved.
    """
    from django.utils import timezone

    from_ccy = (from_ccy or "").upper()
    to_ccy = (to_ccy or "").upper()
    if not from_ccy or not to_ccy or value is None:
        return None
    if from_ccy == to_ccy:
        return Decimal(value)

    today = timezone.now().date()
    from_rate = fetch_idr_rate(from_ccy, today)   # IDR per 1 from_ccy, today
    to_rate = fetch_idr_rate(to_ccy, today)       # IDR per 1 to_ccy, today
    if from_rate is None or to_rate in (None, Decimal("0")):
        return None
    return (Decimal(value) * from_rate) / to_rate


def convert_on_date(value, from_ccy, to_ccy, on_date):
    """Convert `value` from `from_ccy` to `to_ccy` using the rate on `on_date`,
    pivoting through IDR (both legs use the same historical date).

    Returns a Decimal, or None if a needed rate can't be resolved.
    """
    from_ccy = (from_ccy or "").upper()
    to_ccy = (to_ccy or "").upper()
    if not from_ccy or not to_ccy or value is None or on_date is None:
        return None
    if from_ccy == to_ccy:
        return Decimal(value)

    from_rate = fetch_idr_rate(from_ccy, on_date)   # IDR per 1 from_ccy
    to_rate = fetch_idr_rate(to_ccy, on_date)        # IDR per 1 to_ccy
    if from_rate is None or to_rate in (None, Decimal("0")):
        return None
    return (Decimal(value) * from_rate) / to_rate


def convert_fund(fund, save=True):
    """Fill fund.exchange_rate + fund.amount_idr. IDR is 1:1; foreign uses the rate
    on received_date (→ agreement_date → today). Returns True if values changed."""
    from django.utils import timezone

    if fund.currency and fund.currency.upper() == "IDR":
        rate = Decimal("1")
    else:
        ref_date = fund.received_date or fund.agreement_date or timezone.now().date()
        rate = fetch_idr_rate(fund.currency, ref_date)

    new_idr = (fund.amount * rate) if rate is not None else None
    if fund.exchange_rate == rate and fund.amount_idr == new_idr:
        return False
    fund.exchange_rate = rate
    fund.amount_idr = new_idr
    if save:
        fund.save(update_fields=["exchange_rate", "amount_idr", "updated_at"])
    return True
