"""Fetch seller offers from Allegro and eBay APIs, normalize to Offer."""

from __future__ import annotations

import logging
import re
import time
import xml.etree.ElementTree as ET
from collections.abc import Iterator

import httpx

from agent_samochodowy.compliance.ebay_oauth import (
    EbayTokenStore,
    get_valid_access_token,
)
from agent_samochodowy.config import Settings
from agent_samochodowy.models import Offer

logger = logging.getLogger(__name__)

EBAY_TRADING_URL = "https://api.ebay.com/ws/api.dll"
EBAY_SITE_ID = "212"  # Poland
EBAY_COMPAT_LEVEL = "967"

# ---------------------------------------------------------------------------
# Allegro (unchanged)
# ---------------------------------------------------------------------------

ALLEGRO_BASE = "https://api.allegro.pl"


def fetch_allegro_offers(settings: Settings) -> list[Offer]:
    """Fetch all active offers from the seller's Allegro account."""
    if not settings.allegro_oauth_token:
        logger.warning("Allegro OAuth token not set, skipping")
        return []

    headers = {
        "Authorization": f"Bearer {settings.allegro_oauth_token}",
        "Accept": "application/vnd.allegro.public.v1+json",
    }
    offers: list[Offer] = []
    offset = 0
    limit = 100

    with httpx.Client(base_url=ALLEGRO_BASE, headers=headers, timeout=30) as client:
        while True:
            resp = client.get(
                "/sale/offers",
                params={"offset": offset, "limit": limit},
            )
            resp.raise_for_status()
            data = resp.json()

            for item in data.get("offers", []):
                offer = _allegro_to_offer(item)
                if offer:
                    offers.append(offer)

            total = data.get("totalCount", 0)
            offset += limit
            if offset >= total:
                break
            logger.info("Allegro: fetched %d / %d offers", offset, total)

    logger.info("Allegro: total %d offers loaded", len(offers))
    return offers


def _allegro_to_offer(item: dict) -> Offer | None:
    """Map an Allegro sale/offers item to Offer."""
    try:
        offer_id = str(item["id"])
        title = item.get("name", "")
        price_data = item.get("sellingMode", {}).get("price", {})
        price = float(price_data["amount"]) if price_data.get("amount") else None
        currency = price_data.get("currency", "PLN")
        stock = item.get("stock", {}).get("available", 1)

        brand, model, category, engine_code = None, None, None, None
        oe_numbers: list[str] = []

        for param in item.get("parameters", []):
            name_lower = (param.get("name") or "").lower()
            values = param.get("valuesLabels", param.get("values", []))
            val = values[0] if values else ""

            if name_lower in ("marka", "brand", "producent"):
                brand = val
            elif name_lower in ("model", "model pojazdu"):
                model = val
            elif name_lower in ("numer katalogowy", "numer oe", "oe", "mpn"):
                oe_numbers.extend(_parse_oe_string(val))

        title_oes = _extract_oe_from_title(title)
        oe_numbers.extend(title_oes)

        if not brand:
            brand = _extract_brand_from_title(title)
        if not model:
            model = _extract_model_from_title(title, brand)
        if not category:
            category = _extract_category_from_title(title)

        is_part = _is_car_part(title, category)

        return Offer(
            offer_id=f"allegro_{offer_id}",
            platform="allegro",
            url=f"https://allegro.pl/oferta/{offer_id}",
            title=title,
            price=price,
            currency=currency,
            qty=stock,
            brand=brand,
            model=model,
            category=category,
            engine_code=engine_code,
            oe_numbers=oe_numbers,
            is_part=is_part,
        )
    except (KeyError, ValueError) as e:
        logger.warning("Failed to parse Allegro offer: %s", e)
        return None


# ---------------------------------------------------------------------------
# eBay — Trading API (XML)
# ---------------------------------------------------------------------------

_NS = {"e": "urn:ebay:apis:eBLBaseComponents"}


def _resolve_ebay_token(settings: Settings) -> str:
    """Get a valid eBay access token: prefer token store with auto-refresh, fall back to .env."""
    token_store = EbayTokenStore(getattr(settings, "ebay_token_path", "") or
                                 "/var/lib/agent-samochodowy/ebay_tokens.json")
    if token_store.refresh_token:
        return get_valid_access_token(
            settings.ebay_client_id,
            settings.ebay_client_secret,
            token_store,
        )
    # Fall back to legacy static token from .env
    if settings.ebay_oauth_token:
        return settings.ebay_oauth_token
    return ""


def fetch_ebay_offers(settings: Settings) -> list[Offer]:
    """Fetch all active listings via Trading API and enrich with ItemSpecifics."""
    token = _resolve_ebay_token(settings)
    if not token:
        logger.warning("eBay OAuth token not available, skipping")
        return []

    listing_items = list(_get_all_active_listings(settings))
    logger.info("eBay: %d active listings found, fetching item details...", len(listing_items))

    offers: list[Offer] = []
    errors = 0
    for i, basic in enumerate(listing_items):
        try:
            offer = _fetch_and_map_item(basic, settings)
            if offer:
                offers.append(offer)
        except Exception:
            errors += 1
            logger.exception("eBay: failed to process item %s", basic["item_id"])

        if (i + 1) % 500 == 0:
            logger.info("eBay: processed %d / %d items (%d errors)", i + 1, len(listing_items), errors)

        # Throttle: ~10 req/s to stay well within Trading API limits
        if (i + 1) % 10 == 0:
            time.sleep(1.0)

    logger.info("eBay: total %d offers loaded", len(offers))
    return offers


_IAF_EXPIRED_CODES = {"21917053", "931"}  # IAF token expired / invalid


def _trading_call(
    call_name: str,
    xml_body: str,
    settings: Settings,
    timeout: int = 60,
) -> ET.Element:
    """Execute a Trading API call with automatic token refresh on auth failure."""
    token = _resolve_ebay_token(settings)

    for attempt in range(2):
        headers = {
            "X-EBAY-API-SITEID": EBAY_SITE_ID,
            "X-EBAY-API-COMPATIBILITY-LEVEL": EBAY_COMPAT_LEVEL,
            "X-EBAY-API-CALL-NAME": call_name,
            "X-EBAY-API-IAF-TOKEN": token,
            "Content-Type": "text/xml",
        }
        resp = httpx.post(EBAY_TRADING_URL, content=xml_body, headers=headers, timeout=timeout)
        resp.raise_for_status()
        root = ET.fromstring(resp.text)

        ack = root.findtext("e:Ack", "", _NS)
        if ack in ("Success", "Warning") or attempt == 1:
            return root

        # Check if it's an auth error we can retry after refresh
        err_code = root.findtext(".//e:Errors/e:ErrorCode", "", _NS)
        if err_code not in _IAF_EXPIRED_CODES:
            return root  # Non-auth error, return as-is

        logger.info("eBay: token expired (error %s), attempting refresh...", err_code)
        token_store = EbayTokenStore(
            getattr(settings, "ebay_token_path", "") or
            "/var/lib/agent-samochodowy/ebay_tokens.json"
        )
        if not token_store.refresh_token:
            logger.warning("No refresh token available, cannot auto-refresh")
            return root

        try:
            token = get_valid_access_token(
                settings.ebay_client_id,
                settings.ebay_client_secret,
                token_store,
            )
        except Exception:
            logger.exception("Token refresh failed")
            return root

    return root  # type: ignore[possibly-undefined]


def _get_all_active_listings(settings: Settings) -> Iterator[dict]:
    """Page through GetMyeBaySelling to collect all active item basics."""
    page = 1
    per_page = 200
    total_pages = 1  # will be updated from first response

    while page <= total_pages:
        xml_body = f"""<?xml version="1.0" encoding="utf-8"?>
<GetMyeBaySellingRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <ActiveList>
    <Pagination>
      <EntriesPerPage>{per_page}</EntriesPerPage>
      <PageNumber>{page}</PageNumber>
    </Pagination>
  </ActiveList>
</GetMyeBaySellingRequest>"""

        root = _trading_call("GetMyeBaySelling", xml_body, settings, timeout=60)
        ack = root.findtext("e:Ack", "", _NS)
        if ack not in ("Success", "Warning"):
            err = root.findtext(".//e:Errors/e:LongMessage", "unknown error", _NS)
            logger.error("eBay GetMyeBaySelling page %d failed: %s", page, err)
            break

        if page == 1:
            tp = root.findtext(
                ".//e:ActiveList/e:PaginationResult/e:TotalNumberOfPages", "1", _NS
            )
            te = root.findtext(
                ".//e:ActiveList/e:PaginationResult/e:TotalNumberOfEntries", "0", _NS
            )
            total_pages = int(tp)
            logger.info("eBay: total active listings = %s, pages = %s", te, tp)

        for item in root.findall(".//e:ActiveList/e:ItemArray/e:Item", _NS):
            item_id = item.findtext("e:ItemID", "", _NS)
            title = item.findtext("e:Title", "", _NS)
            price_el = item.find(".//e:SellingStatus/e:CurrentPrice", _NS)
            price = float(price_el.text) if price_el is not None else None
            currency = price_el.get("currencyID", "PLN") if price_el is not None else "PLN"
            qty = int(item.findtext("e:QuantityAvailable", "1", _NS))
            url = item.findtext(".//e:ListingDetails/e:ViewItemURL", "", _NS)

            yield {
                "item_id": item_id,
                "title": title,
                "price": price,
                "currency": currency,
                "qty": qty,
                "url": url,
            }

        page += 1
        if page <= total_pages:
            time.sleep(0.5)


def _fetch_and_map_item(basic: dict, settings: Settings) -> Offer | None:
    """Call GetItem for ItemSpecifics, then map to Offer."""
    xml_body = f"""<?xml version="1.0" encoding="utf-8"?>
<GetItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <ItemID>{basic['item_id']}</ItemID>
  <DetailLevel>ReturnAll</DetailLevel>
  <IncludeItemSpecifics>true</IncludeItemSpecifics>
</GetItemRequest>"""

    root = _trading_call("GetItem", xml_body, settings, timeout=30)
    ack = root.findtext("e:Ack", "", _NS)
    if ack not in ("Success", "Warning"):
        err_code = root.findtext(".//e:Errors/e:ErrorCode", "", _NS)
        if err_code == "518":
            logger.warning("eBay: GetItem API call limit reached, remaining items will be skipped")
        return None

    item = root.find(".//e:Item", _NS)
    if item is None:
        return None

    # --- ItemSpecifics ---
    specs: dict[str, list[str]] = {}
    for nv in item.findall(".//e:ItemSpecifics/e:NameValueList", _NS):
        name = nv.findtext("e:Name", "", _NS)
        values = [v.text for v in nv.findall("e:Value", _NS) if v.text]
        specs[name] = values

    # OE numbers from specs (deduplicated)
    oe_numbers: list[str] = []
    _SKIP_VALUES = {"nie dotyczy", "n/a", "does not apply", "-", ""}
    for key in specs:
        if any(tok in key.lower() for tok in ("referencyjny oe", "oe/oem", "numer części oe")):
            for v in specs[key]:
                if v.lower().strip() not in _SKIP_VALUES:
                    oe_numbers.extend(_parse_oe_string(v))

    # MPN (only if it looks like a real number)
    for v in specs.get("MPN", []):
        if v.lower().strip() not in _SKIP_VALUES:
            oe_numbers.extend(_parse_oe_string(v))

    # Fallback: extract from title only if specs had no OE
    title = basic["title"]
    if not oe_numbers:
        title_oes = _extract_oe_from_title(title)
        oe_numbers.extend(title_oes)

    # Deduplicate OE list (normalized); require at least one digit
    seen_oe: set[str] = set()
    unique_oe: list[str] = []
    for oe in oe_numbers:
        norm = re.sub(r"[\s.\-/]", "", oe).upper()
        if norm and norm not in seen_oe and len(norm) >= 4 and re.search(r"\d", norm):
            seen_oe.add(norm)
            unique_oe.append(norm)

    # Brand + model from title (NOT from "Producent" spec which is part manufacturer)
    brand = _extract_brand_from_title(title)
    model = _extract_model_from_title(title, brand)
    category = _extract_category_from_title(title)
    is_part = _is_car_part(title, category)

    url = basic["url"] or f"https://www.ebay.pl/itm/{basic['item_id']}"

    return Offer(
        offer_id=f"ebay_{basic['item_id']}",
        platform="ebay",
        url=url,
        title=title,
        price=basic["price"],
        currency=basic["currency"],
        qty=basic["qty"],
        brand=brand,
        model=model,
        category=category,
        engine_code=None,
        oe_numbers=unique_oe,
        is_part=is_part,
    )


# ---------------------------------------------------------------------------
# Helpers — shared by eBay and Allegro
# ---------------------------------------------------------------------------

def _parse_oe_string(text: str) -> list[str]:
    """Split a string that may contain multiple OE numbers (comma/space separated)."""
    parts = re.split(r"[,;/|]+", text)
    results = []
    for part in parts:
        cleaned = part.strip()
        if cleaned and len(cleaned) >= 4:
            results.append(cleaned)
    return results


# OE-like tokens: standalone space-separated words that look like part numbers.
# Must contain at least one digit and one letter, or be purely numeric 6+ chars.
_OE_TITLE_RE = re.compile(
    r"(?<!\w)"  # not preceded by word char
    r"("
    r"[A-Z]{1,4}\d[\dA-Z.\-]{3,14}"  # letter-prefix + digit: A0001802701, FRE044
    r"|"
    r"\d[\dA-Z.\-]{1,4}[A-Z][\dA-Z.\-]{0,10}"  # digit-start with letter: 4757205000 won't match, but 51.06500-6408 will via dots
    r"|"
    r"\d{6,15}"  # pure numeric 6+: 20744939, 5010422381
    r"|"
    r"\d{2,5}[.\-]\d{3,6}[.\-]?\d{0,6}"  # dotted numeric: 81.47101-6136
    r")"
    r"(?!\w)",  # not followed by word char
    re.IGNORECASE,
)


def _extract_oe_from_title(title: str) -> list[str]:
    """Extract OE-like numbers from a title string.

    Must be 6+ chars after stripping separators and contain at least one digit.
    """
    results = []
    for m in _OE_TITLE_RE.finditer(title.upper()):
        token = m.group(1)
        cleaned = re.sub(r"[\s.\-/]", "", token)
        if len(cleaned) >= 6 and re.search(r"\d", cleaned):
            results.append(token)
    return results


# ---------------------------------------------------------------------------
# Brand extraction from title
# ---------------------------------------------------------------------------

_CAR_BRANDS = [
    # Trucks
    "VOLVO", "SCANIA", "MAN", "DAF", "MERCEDES", "IVECO", "RENAULT",
    "JELCZ", "STAR", "LIAZ", "TATRA", "URSUS", "KAMAZ",
    # Cars
    "OPEL", "BMW", "AUDI", "VOLKSWAGEN", "VW", "FIAT", "FORD",
    "PEUGEOT", "CITROEN", "TOYOTA", "SKODA", "SEAT", "HYUNDAI", "KIA",
    "NISSAN", "HONDA", "MAZDA", "MITSUBISHI", "SUBARU", "SUZUKI",
    "LANCIA", "ALFA ROMEO", "PORSCHE", "SAAB", "ROVER", "JAGUAR",
    "LAND ROVER", "JEEP", "CHRYSLER", "DODGE", "CHEVROLET",
]

_BRAND_RE = re.compile(
    r"\b(" + "|".join(re.escape(b) for b in sorted(_CAR_BRANDS, key=len, reverse=True)) + r")\b",
    re.IGNORECASE,
)


def _extract_brand_from_title(title: str) -> str | None:
    m = _BRAND_RE.search(title)
    return m.group(1).upper() if m else None


# ---------------------------------------------------------------------------
# Model extraction from title (brand-aware)
# ---------------------------------------------------------------------------

# brand → list of known model patterns (longest first to avoid partial matches)
_BRAND_MODELS: dict[str, list[str]] = {
    "VOLVO": [
        "FH16", "FH13", "FH12", "FM13", "FM12", "FM10", "FM7", "FM9",
        "FH", "FM", "FL", "FE", "FMX", "F16", "F12", "F10", "F7", "F6",
        "NL12", "NL10", "N12", "N10", "N7",
        "B12", "B10", "B7",
        "780", "760", "740",
        "XC90", "XC70", "XC60", "XC40", "V70", "V60", "V50", "V40",
        "S90", "S80", "S70", "S60", "S40",
        "240", "340", "360", "440", "460", "480",
        "P1800", "P544", "AMAZON",
        "BERTONE",
    ],
    "SCANIA": [
        # Full model codes (must come before series letters to match first)
        "R500", "R580", "R620", "R730", "R450", "R410",
        "R142", "R143", "R112", "R113",
        "G400", "G450", "G490",
        "P280", "P310", "P340", "P380", "P400", "P410",
        "S500", "S520", "S580",
        "T142", "T143", "T112", "T113",
        "L94", "K113", "K114",
        # Numeric generations/models (common in eBay titles)
        "164", "144", "143", "142", "141",
        "124", "114", "113", "112", "111", "93", "94", "92",
        # Series letters (single-char, matched only after brand context)
        "R", "P", "G", "S", "T", "L", "K",
        # Generation number
        "4", "3", "2",
    ],
    "MAN": [
        "TGA", "TGX", "TGS", "TGL", "TGM", "TGE",
        "F2000", "F90", "F08", "L2000", "M2000",
        "E2000", "LE",
        "SG", "SL", "NL",
    ],
    "DAF": [
        "XF105", "XF106", "XF95", "XF530", "XF480",
        "CF85", "CF75", "CF65",
        "LF55", "LF45", "LF250",
        "95", "85", "75", "65", "55", "45",
    ],
    "MERCEDES": [
        "ACTROS", "ATEGO", "AXOR", "AROCS", "ECONIC", "ANTOS", "UNIMOG",
        "SPRINTER", "VITO", "VARIO",
        "SK", "MK", "NG",
        "W124", "W126", "W140", "W201", "W202", "W203", "W210", "W211", "W220",
    ],
    "IVECO": [
        "STRALIS", "EUROSTAR", "EUROTECH", "EUROCARGO", "EUROTRAKKER",
        "TRAKKER", "S-WAY", "DAILY",
    ],
    "RENAULT": [
        "MAGNUM", "PREMIUM", "MIDLUM", "KERAX", "MAXITY",
        "MASTER", "TRAFIC", "KANGOO",
        "T460", "T480", "C460",
    ],
    "BMW": [
        "E9", "E21", "E28", "E30", "E34", "E36", "E38", "E39",
        "E46", "E53", "E60", "E61", "E65", "E70", "E81", "E82",
        "E87", "E88", "E90", "E91", "E92",
        "F01", "F02", "F10", "F11", "F20", "F30", "F31",
        "X1", "X3", "X5",
        "2002",
    ],
    "OPEL": [
        "ASTRA", "CORSA", "VECTRA", "ZAFIRA", "OMEGA", "INSIGNIA",
        "MERIVA", "KADETT", "MOVANO", "VIVARO",
    ],
    "FIAT": [
        "DUCATO", "DOBLO", "SCUDO", "PUNTO", "PANDA", "TIPO",
        "BRAVO", "STILO", "MULTIPLA", "500", "FULLBACK",
    ],
    "FORD": [
        "TRANSIT", "MONDEO", "FOCUS", "FIESTA", "GALAXY",
        "RANGER", "ESCORT", "SIERRA", "SCORPIO", "MAVERICK",
    ],
    "LANCIA": ["FULVIA", "DELTA", "THEMA", "BETA", "DEDRA", "KAPPA"],
}


def _extract_model_from_title(title: str, brand: str | None) -> str | None:
    """Try to find a known model name after the brand in the title.

    Single-letter models (like Scania "R", "P", "G") are only matched when
    they appear directly after the brand name to avoid false positives.
    """
    if not brand:
        return None
    models = _BRAND_MODELS.get(brand.upper())
    if not models:
        return None
    title_upper = title.upper()
    for model in models:
        if len(model) == 1:
            # Single-char series: require it right after brand (e.g. "SCANIA R ")
            if re.search(re.escape(brand.upper()) + r"\s+" + re.escape(model) + r"\b", title_upper):
                return model
        else:
            if re.search(r"\b" + re.escape(model) + r"\b", title_upper):
                return model
    return None


# ---------------------------------------------------------------------------
# Category extraction from title (PL + EN keywords)
# ---------------------------------------------------------------------------

_PART_CATEGORIES = [
    # PL multi-word (must come before single-word to match first)
    "pompa wody", "pompa paliwa", "pompa wspomagania", "pompa oleju",
    "filtr oleju", "filtr powietrza", "filtr paliwa", "filtr kabinowy",
    "klocki hamulcowe", "tarcze hamulcowe",
    "pasek rozrządu", "pasek klinowy",
    "skrzynia biegów", "wał korbowy", "wał kardanowy",
    "most napędowy",
    "load sensing valve", "relay valve", "brake valve", "foot brake valve",
    # PL single-word
    "pompa", "filtr", "alternator", "rozrusznik", "turbo", "turbina",
    "klocki", "tarcze", "amortyzator", "sprzęgło", "chłodnica",
    "głowica", "wał", "tłok", "cylinder", "zawór", "zaworek",
    "uszczelka", "pasek", "łożysko", "wahacz", "zwrotnica",
    "siłownik", "sprężarka", "kompresor", "licznik",
    "zderzak", "lampa", "reflektor", "lusterko", "drzwi",
    "zacisk", "bęben", "piasta", "przekaźnik", "przełącznik",
    "uchwyt", "poduszka", "tulejka", "sworzeń", "drążek",
    "korek", "nakrętka", "śruba",
    "osuszacz", "katalizator", "szyba", "przewód", "wąż",
    "wtryskiwacz", "separator", "tłumik",
    # Multi-word PL (must also be in multi-word section above to get priority)
    "zawór osuszacza", "podstawa osuszacza",
    # EN (for bilingual titles)
    "pump", "filter", "alternator", "starter", "turbo",
    "brake", "clutch", "radiator", "piston", "valve",
    "cylinder", "gasket", "bearing", "caliper", "handle",
    "switch", "relay", "cap", "mounting", "support",
    "sensor", "compressor",
    "air dryer", "lufttrockner",
    "catalytic converter", "katalysator", "partikelfilter",
    "windscreen", "windshield",
    "hose", "schlauch",
    "pulley", "riemenscheibe",
    "injector", "nozzle",
    "silencer", "exhaust", "auspuff",
    "separator",
    "hub", "radnabe",
]

_CATEGORY_RE = re.compile(
    r"\b(" + "|".join(re.escape(k) for k in sorted(_PART_CATEGORIES, key=len, reverse=True)) + r")\b",
    re.IGNORECASE,
)


def _extract_category_from_title(title: str) -> str | None:
    m = _CATEGORY_RE.search(title)
    return m.group(1).lower() if m else None


_NOT_PARTS = re.compile(
    r"\b(gra planszowa|puzzle|zabawka|koszulka|t-shirt|kubek|brelok)\b",
    re.IGNORECASE,
)


def _is_car_part(title: str, category: str | None) -> bool:
    """Heuristic: detect non-part items (merchandise, games, etc.)."""
    if _NOT_PARTS.search(title):
        return False
    return True
