"""Import card data from external sources (Offer Optimist YAML, CFPB, etc.)."""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path

import httpx
import yaml
from openpyxl import load_workbook
from sqlalchemy import and_
from sqlalchemy.orm import Session

from luca_assistant.db.models import Card, CardBenefit, CardOffer

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Mapping constants
# ---------------------------------------------------------------------------

ISSUER_MAP: dict[str, str] = {
    "AMERICAN_EXPRESS": "Amex",
    "CHASE": "Chase",
    "CAPITAL_ONE": "Capital One",
    "CITI": "Citi",
    "BANK_OF_AMERICA": "Bank of America",
    "BARCLAYS": "Barclays",
    "BREX": "Brex",
    "COMENITY": "Comenity",
}

NETWORK_MAP: dict[str, str] = {
    "VISA": "Visa",
    "MASTERCARD": "Mastercard",
    "AMERICAN_EXPRESS": "Amex",
}

# Maps the Offer Optimist `currency` field to a display name for bonus_amount.
CURRENCY_DISPLAY: dict[str, str] = {
    # Issuer transferable currencies
    "CHASE": "UR",
    "AMERICAN_EXPRESS": "MR",
    "CAPITAL_ONE": "miles",
    "CITI": "TYP",
    "BARCLAYS": "points",
    "BREX": "points",
    # Airline co-brands
    "DELTA": "SkyMiles",
    "UNITED": "miles",
    "SOUTHWEST": "Rapid Rewards",
    "JETBLUE": "TrueBlue points",
    "HAWAIIAN": "HawaiianMiles",
    "FRONTIER": "miles",
    "ALASKA": "miles",
    "AMERICAN": "AAdvantage miles",
    "SPIRIT": "points",
    "FLYING_BLUE": "Flying Blue miles",
    "EMIRATES": "Skywards miles",
    "LUFTHANSA": "miles",
    "AVIOS": "Avios",
    "AEROPLAN": "Aeroplan points",
    "BREEZE": "points",
    "CARNIVAL": "points",
    # Hotel co-brands
    "HILTON": "Hilton points",
    "MARRIOTT": "Marriott Bonvoy points",
    "IHG": "IHG points",
    "HYATT": "World of Hyatt points",
    "WYNDHAM": "Wyndham points",
    # Cash
    "USD": "cashback",
    # Fallback handled in code
}

# Cents-per-point valuations (conservative) for bonus_value_usd calculation.
POINT_VALUATION_CPP: dict[str, float] = {
    # Transferable
    "CHASE": 1.25,
    "AMERICAN_EXPRESS": 1.2,
    "CAPITAL_ONE": 1.0,
    "CITI": 1.0,
    "BARCLAYS": 1.0,
    "BREX": 1.0,
    # Airline
    "DELTA": 1.2,
    "UNITED": 1.2,
    "SOUTHWEST": 1.4,
    "JETBLUE": 1.3,
    "HAWAIIAN": 1.0,
    "FRONTIER": 0.8,
    "ALASKA": 1.5,
    "AMERICAN": 1.2,
    "SPIRIT": 1.0,
    "FLYING_BLUE": 1.2,
    "EMIRATES": 1.0,
    "LUFTHANSA": 1.0,
    "AVIOS": 1.2,
    "AEROPLAN": 1.2,
    "BREEZE": 1.0,
    "CARNIVAL": 1.0,
    # Hotel
    "HILTON": 0.5,
    "MARRIOTT": 0.7,
    "IHG": 0.5,
    "HYATT": 1.7,
    "WYNDHAM": 0.7,
    # Cash
    "USD": 100.0,
}

# (substring in card name, product_family value)
PRODUCT_FAMILY_RULES: list[tuple[str, str]] = [
    ("Sapphire", "Sapphire"),
    ("Freedom", "Freedom"),
    ("Ink Business", "Ink"),
    ("United", "United"),
    ("IHG", "IHG"),
    ("Southwest", "Southwest"),
    ("Delta", "Delta"),
    ("Hilton", "Hilton"),
    ("Marriott", "Marriott"),
    ("Hyatt", "Hyatt"),
    ("Platinum", "Platinum"),
    ("Gold Card", "Gold"),
    ("Blue Cash", "Blue Cash"),
    ("Venture", "Venture"),
    ("Savor", "Savor"),
    ("AAdvantage", "AAdvantage"),
    ("Strata", "Strata"),
    ("JetBlue", "JetBlue"),
    ("Hawaiian", "Hawaiian"),
    ("Wyndham", "Wyndham"),
    ("Alaska", "Alaska"),
]

OFFER_OPTIMIST_URL = (
    "https://raw.githubusercontent.com/andenacitelli/"
    "credit-card-bonuses-api/main/exports/data.yaml"
)

CACHE_DIR = Path(__file__).resolve().parent.parent.parent.parent / "data" / "cache"
CACHE_MAX_AGE_SECONDS = 24 * 60 * 60  # 24 hours


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------


@dataclass
class ImportResult:
    cards_created: int = 0
    cards_updated: int = 0
    offers_created: int = 0
    benefits_created: int = 0
    skipped_discontinued: int = 0
    errors: list[str] = field(default_factory=list)

    @property
    def total_cards(self) -> int:
        return self.cards_created + self.cards_updated


# ---------------------------------------------------------------------------
# Importer
# ---------------------------------------------------------------------------


class OfferOptimistImporter:
    """Download and import card data from the Offer Optimist community dataset."""

    def __init__(self, url: str = OFFER_OPTIMIST_URL, cache_dir: Path = CACHE_DIR) -> None:
        self.url = url
        self.cache_dir = cache_dir
        self.cache_path = cache_dir / "offer_optimist.yaml"

    # -- download ----------------------------------------------------------

    def download(self, *, force: bool = False) -> Path:
        """Fetch YAML to cache. Returns path. Skips if fresh cache exists."""
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        if not force and self.cache_path.exists():
            age = time.time() - self.cache_path.stat().st_mtime
            if age < CACHE_MAX_AGE_SECONDS:
                logger.info("Using cached YAML (%.0f min old)", age / 60)
                return self.cache_path

        logger.info("Downloading Offer Optimist YAML from %s", self.url)
        resp = httpx.get(self.url, timeout=30, follow_redirects=True)
        resp.raise_for_status()
        self.cache_path.write_bytes(resp.content)
        logger.info("Saved %d bytes to %s", len(resp.content), self.cache_path)
        return self.cache_path

    def get_cache_path(self) -> Path:
        """Return cache path (may or may not exist)."""
        return self.cache_path

    # -- parse -------------------------------------------------------------

    def parse(self, path: Path) -> list[dict]:
        """Load YAML and normalize each entry to a flat dict for import."""
        raw = yaml.safe_load(path.read_text(encoding="utf-8"))
        if not isinstance(raw, list):
            raise ValueError(f"Expected YAML list, got {type(raw).__name__}")

        results: list[dict] = []
        for entry in raw:
            try:
                results.append(self._normalize(entry))
            except Exception as exc:
                logger.warning("Skipping entry %s: %s", entry.get("name", "?"), exc)
        return results

    def _normalize(self, entry: dict) -> dict:
        issuer = entry["issuer"]
        currency = entry.get("currency", issuer)
        bank = ISSUER_MAP.get(issuer, issuer)
        name = entry["name"]
        network_raw = entry.get("network", issuer)

        # Build the best current offer (first in the offers list)
        offers_raw = entry.get("offers") or []
        current_offer = self._parse_offer(offers_raw[0], currency) if offers_raw else None

        # Historical offers for is_highest_known comparison
        historical_raw = entry.get("historicalOffers") or []
        historical_max = 0
        for ho in historical_raw:
            parsed = self._parse_offer(ho, currency)
            if parsed and parsed["amount"] > historical_max:
                historical_max = parsed["amount"]

        # Credits → CardBenefit rows
        credits_raw = entry.get("credits") or []
        credits_parsed = [self._parse_credit(c, currency) for c in credits_raw]

        return {
            "bank": bank,
            "name": name,
            "full_name": f"{bank} {name}",
            "network": NETWORK_MAP.get(network_raw, network_raw),
            "annual_fee": int(entry.get("annualFee", 0)),
            "annual_fee_waived_first_year": bool(entry.get("isAnnualFeeWaived", False)),
            "product_family": self._infer_product_family(name),
            "is_business": bool(entry.get("isBusiness", False)),
            "is_active": not bool(entry.get("discontinued", False)),
            "discontinued": bool(entry.get("discontinued", False)),
            "currency": currency,
            "current_offer": current_offer,
            "historical_max_amount": historical_max,
            "credits": credits_parsed,
            "source_url": entry.get("url"),
        }

    def _parse_offer(self, offer: dict, currency: str) -> dict | None:
        amounts = offer.get("amount") or []
        if not amounts:
            return None
        # Sum all point amounts (some offers have points + FNC combined)
        total_amount = sum(a.get("amount", 0) for a in amounts)
        if total_amount == 0:
            return None

        currency_name = CURRENCY_DISPLAY.get(currency, "points")
        cpp = POINT_VALUATION_CPP.get(currency, 1.0)
        bonus_value_usd = int(total_amount * cpp / 100)

        spend = offer.get("spend", 0)
        days = offer.get("days", 90)

        return {
            "amount": total_amount,
            "bonus_amount": f"{total_amount:,} {currency_name}",
            "bonus_value_usd": bonus_value_usd,
            "spend_requirement": spend,
            "spend_timeframe_months": max(1, days // 30),
        }

    def _parse_credit(self, credit: dict, currency: str) -> dict:
        description = credit.get("description", "")
        value = credit.get("value", 0)
        currency_name = CURRENCY_DISPLAY.get(currency, "points")
        return {
            "category": description.lower() if description else "other",
            "multiplier": 1.0,
            "points_type": currency_name,
            "annual_credit": int(value) if value else None,
            "credit_category": description,
            "notes": description,
        }

    def _infer_product_family(self, name: str) -> str | None:
        for pattern, family in PRODUCT_FAMILY_RULES:
            if pattern.lower() in name.lower():
                return family
        return None

    # -- import into DB ----------------------------------------------------

    def import_cards(self, session: Session, data: list[dict]) -> ImportResult:
        """Upsert parsed card data into the database. Returns import stats."""
        result = ImportResult()

        for entry in data:
            if entry.get("discontinued"):
                result.skipped_discontinued += 1
                continue

            try:
                self._upsert_card(session, entry, result)
            except Exception as exc:
                msg = f"Error importing {entry.get('name', '?')}: {exc}"
                logger.warning(msg)
                result.errors.append(msg)

        session.flush()
        return result

    def _upsert_card(self, session: Session, entry: dict, result: ImportResult) -> None:
        bank = entry["bank"]
        name = entry["name"]

        card = (
            session.query(Card)
            .filter(and_(Card.bank == bank, Card.name == name))
            .first()
        )

        if card:
            # Update existing card
            card.full_name = entry["full_name"]
            card.network = entry["network"]
            card.annual_fee = entry["annual_fee"]
            card.annual_fee_waived_first_year = entry["annual_fee_waived_first_year"]
            card.is_business = entry["is_business"]
            card.is_active = entry["is_active"]
            if entry["product_family"]:
                card.product_family = entry["product_family"]
            result.cards_updated += 1
        else:
            card = Card(
                bank=bank,
                name=name,
                full_name=entry["full_name"],
                network=entry["network"],
                annual_fee=entry["annual_fee"],
                annual_fee_waived_first_year=entry["annual_fee_waived_first_year"],
                product_family=entry["product_family"],
                is_business=entry["is_business"],
                is_active=entry["is_active"],
            )
            session.add(card)
            session.flush()  # get card.id
            result.cards_created += 1

        # Upsert current offer
        offer_data = entry.get("current_offer")
        if offer_data:
            self._upsert_offer(session, card, offer_data, entry, result)

        # Add credits as benefits (skip if already exists)
        for credit in entry.get("credits") or []:
            self._upsert_credit_benefit(session, card, credit, result)

    def _upsert_offer(
        self, session: Session, card: Card, offer_data: dict, entry: dict, result: ImportResult
    ) -> None:
        # Check if the current offer already matches — skip if identical
        current = next((o for o in card.offers if o.is_current), None)
        if current and (
            current.bonus_amount == offer_data["bonus_amount"]
            and current.bonus_value_usd == offer_data["bonus_value_usd"]
            and current.spend_requirement == offer_data["spend_requirement"]
            and current.spend_timeframe_months == offer_data["spend_timeframe_months"]
        ):
            # Identical offer already exists — just update last_seen
            current.last_seen = date.today()
            return

        # Mark old offers as not current
        for old_offer in card.offers:
            if old_offer.is_current:
                old_offer.is_current = False

        historical_max = entry.get("historical_max_amount", 0)
        is_highest = offer_data["amount"] >= historical_max if historical_max else True

        session.add(
            CardOffer(
                card_id=card.id,
                bonus_amount=offer_data["bonus_amount"],
                bonus_value_usd=offer_data["bonus_value_usd"],
                spend_requirement=offer_data["spend_requirement"],
                spend_timeframe_months=offer_data["spend_timeframe_months"],
                source_url=entry.get("source_url"),
                is_current=True,
                is_highest_known=is_highest,
            )
        )
        result.offers_created += 1

    def _upsert_credit_benefit(
        self, session: Session, card: Card, credit: dict, result: ImportResult
    ) -> None:
        category = credit["category"]
        # Skip if a benefit with same category already exists (don't overwrite curated data)
        existing = (
            session.query(CardBenefit)
            .filter(and_(CardBenefit.card_id == card.id, CardBenefit.category == category))
            .first()
        )
        if existing:
            return

        session.add(
            CardBenefit(
                card_id=card.id,
                category=category,
                multiplier=credit["multiplier"],
                points_type=credit["points_type"],
                annual_credit=credit.get("annual_credit"),
                credit_category=credit.get("credit_category"),
                notes=credit.get("notes"),
            )
        )
        result.benefits_created += 1


# ---------------------------------------------------------------------------
# CFPB TCCP Survey Importer
# ---------------------------------------------------------------------------

CFPB_TCCP_URL = (
    "https://files.consumerfinance.gov/f/documents/cfpb_tccp-data_2025-06-30.xlsx"
)

# Map CFPB institution names → our bank names (partial matches, case-insensitive)
CFPB_BANK_MAP: dict[str, str] = {
    "american express": "Amex",
    "jpmorgan chase": "Chase",
    "chase bank": "Chase",
    "capital one": "Capital One",
    "citibank": "Citi",
    "citigroup": "Citi",
    "bank of america": "Bank of America",
    "barclays": "Barclays",
    "discover": "Discover",
    "wells fargo": "Wells Fargo",
    "u.s. bank": "US Bank",
    "us bank": "US Bank",
    "usaa": "USAA",
    "pnc": "PNC",
    "td bank": "TD Bank",
    "synchrony": "Synchrony",
    "navy federal": "Navy Federal",
    "pentagon federal": "PenFed",
}


@dataclass
class CFPBImportResult:
    cards_enriched: int = 0
    cards_not_matched: int = 0
    total_rows: int = 0
    errors: list[str] = field(default_factory=list)


class CFPBImporter:
    """Download and import CFPB TCCP survey data to enrich existing cards."""

    def __init__(self, url: str = CFPB_TCCP_URL, cache_dir: Path = CACHE_DIR) -> None:
        self.url = url
        self.cache_dir = cache_dir
        self.cache_path = cache_dir / "cfpb_tccp.xlsx"

    def download(self, *, force: bool = False) -> Path:
        """Fetch XLSX to cache. Returns path."""
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        if not force and self.cache_path.exists():
            age = time.time() - self.cache_path.stat().st_mtime
            if age < CACHE_MAX_AGE_SECONDS:
                logger.info("Using cached CFPB XLSX (%.0f min old)", age / 60)
                return self.cache_path

        logger.info("Downloading CFPB TCCP XLSX from %s", self.url)
        resp = httpx.get(self.url, timeout=60, follow_redirects=True)
        resp.raise_for_status()
        self.cache_path.write_bytes(resp.content)
        logger.info("Saved %d bytes to %s", len(resp.content), self.cache_path)
        return self.cache_path

    def get_cache_path(self) -> Path:
        return self.cache_path

    def parse(self, path: Path) -> list[dict]:
        """Load XLSX and normalize each row to a dict."""
        wb = load_workbook(path, read_only=True, data_only=True)
        ws = wb.active
        if ws is None:
            raise ValueError("No active worksheet in CFPB XLSX")

        rows = list(ws.iter_rows(values_only=True))
        if not rows:
            raise ValueError("Empty CFPB XLSX")

        # First row is headers
        headers = [str(h).strip() if h else f"col_{i}" for i, h in enumerate(rows[0])]
        results: list[dict] = []

        for row in rows[1:]:
            raw = dict(zip(headers, row))
            try:
                parsed = self._normalize(raw)
                if parsed:
                    results.append(parsed)
            except Exception as exc:
                logger.warning("Skipping CFPB row %s: %s", raw.get("Card Name", "?"), exc)

        wb.close()
        return results

    def _normalize(self, raw: dict) -> dict | None:
        institution = str(raw.get("Institution Name", "")).strip()
        card_name = str(raw.get("Card Name", "")).strip()
        if not institution or not card_name:
            return None

        bank = self._map_bank(institution)

        # Parse numeric fields safely
        def _float(key: str) -> float | None:
            v = raw.get(key)
            if v is None or v == "" or v == "N/A":
                return None
            try:
                return float(v)
            except (ValueError, TypeError):
                return None

        def _int(key: str) -> int | None:
            v = raw.get(key)
            if v is None or v == "" or v == "N/A":
                return None
            try:
                return int(float(v))
            except (ValueError, TypeError):
                return None

        return {
            "institution": institution,
            "bank": bank,
            "card_name": card_name,
            "annual_fee": _int("Annual Fee"),
            "purchase_apr_min": _float("Purchase APR min"),
            "purchase_apr_max": _float("Purchase APR max"),
            "purchase_apr_median": _float("Purchase APR median"),
            "intro_apr_min": _float("Intro APR min"),
            "intro_apr_max": _float("Intro APR max"),
            "intro_apr_months": _int("Median Length of Introductory APR"),
            "foreign_transaction_fee_pct": _float("Foreign Transaction Fee (%)"),
            "late_fee": _int("Late Fee ($)"),
            "grace_period_days": _int("Grace Period"),
            "is_secured": str(raw.get("Secured Card", "")).strip().upper() == "YES",
            "targeted_credit_tiers": str(raw.get("Targeted Credit Tiers", "")).strip() or None,
            "raw": {
                k: str(v).strip() if v is not None else None
                for k, v in raw.items()
                if v is not None and str(v).strip()
            },
        }

    def _map_bank(self, institution: str) -> str:
        inst_lower = institution.lower()
        for pattern, bank_name in CFPB_BANK_MAP.items():
            if pattern in inst_lower:
                return bank_name
        return institution

    def import_cards(self, session: Session, data: list[dict]) -> CFPBImportResult:
        """Match CFPB rows to existing cards and enrich with fee/APR data."""
        result = CFPBImportResult(total_rows=len(data))

        for entry in data:
            try:
                matched = self._enrich_card(session, entry)
                if matched:
                    result.cards_enriched += 1
                else:
                    result.cards_not_matched += 1
            except Exception as exc:
                msg = f"Error enriching {entry.get('card_name', '?')}: {exc}"
                logger.warning(msg)
                result.errors.append(msg)

        session.flush()
        return result

    def _enrich_card(self, session: Session, entry: dict) -> bool:
        """Find a matching card and update CFPB fields. Returns True if matched."""
        bank = entry["bank"]
        cfpb_name = entry["card_name"]

        # Try exact bank + fuzzy name match
        card = (
            session.query(Card)
            .filter(
                Card.bank == bank,
                Card.name.ilike(f"%{cfpb_name}%"),
            )
            .first()
        )

        # Try with full_name
        if not card:
            card = (
                session.query(Card)
                .filter(
                    Card.bank == bank,
                    Card.full_name.ilike(f"%{cfpb_name}%"),
                )
                .first()
            )

        # Try reverse: CFPB name contains our card name
        if not card:
            cards_for_bank = session.query(Card).filter(Card.bank == bank).all()
            for c in cards_for_bank:
                if c.name.lower() in cfpb_name.lower():
                    card = c
                    break

        if not card:
            return False

        # Enrich with CFPB data
        if entry["purchase_apr_min"] is not None:
            card.purchase_apr_min = entry["purchase_apr_min"]
        if entry["purchase_apr_max"] is not None:
            card.purchase_apr_max = entry["purchase_apr_max"]
        if entry["foreign_transaction_fee_pct"] is not None:
            card.foreign_transaction_fee_pct = entry["foreign_transaction_fee_pct"]
        if entry["late_fee"] is not None:
            card.late_fee = entry["late_fee"]
        if entry["grace_period_days"] is not None:
            card.grace_period_days = entry["grace_period_days"]
        card.is_secured = entry["is_secured"]

        # Store full CFPB row as JSON for anything we didn't extract
        card.cfpb_data = entry["raw"]

        return True
