"""
Product Catalog Component

Provides access to Espressif's public product catalog API for chip/module
discovery, comparison, and procurement planning. Data is lazily fetched
and cached in-memory with a 24-hour TTL.
"""

import json
import logging
import re
import time
from typing import Any

import httpx
from fastmcp import Context, FastMCP

from ..config import ESPToolServerConfig

logger = logging.getLogger(__name__)

CATALOG_URL = "https://products.espressif.com/api/user/products?language=en"


def _parse_memory_field(value: str | int | None) -> tuple[int, str]:
    """Parse compound memory strings like '4, Quad' into (size_mb, interface).

    Returns (0, '') for empty/unparsable values. Handles bare ints from the API.
    """
    if isinstance(value, int):
        return value, ""
    if not value or value in ("N/A", "NA", "-", "—"):
        return 0, ""
    parts = [p.strip() for p in value.split(",")]
    try:
        size = int(parts[0])
    except (ValueError, IndexError):
        return 0, ""
    interface = parts[1] if len(parts) > 1 else ""
    return size, interface


def _parse_sram_kb(value: str | int | None) -> int:
    """Parse SRAM field — may be plain int or compound string."""
    if isinstance(value, int):
        return value
    if not value or value in ("N/A", "NA", "-", "—"):
        return 0
    parts = value.split(",")
    try:
        return int(parts[0].strip())
    except (ValueError, IndexError):
        return 0


def _has_capability(value: str) -> bool:
    """Check if a feature field indicates availability."""
    if not value:
        return False
    v = value.strip().lower()
    return v not in ("", "n/a", "na", "-", "—", "no", "0")


def _normalize_status(value: str) -> str:
    """Normalize status strings for consistent filtering."""
    mapping = {
        "mass production": "Mass Production",
        "nrnd": "NRND",
        "eol": "EOL",
        "replaced": "Replaced",
        "sample": "Sample",
    }
    return mapping.get(value.strip().lower(), value.strip())


def _product_summary(product: dict) -> dict[str, Any]:
    """Extract a concise summary of a product for search results."""
    flash_mb, flash_type = _parse_memory_field(product.get("flash", ""))
    psram_mb, psram_type = _parse_memory_field(product.get("psram", ""))
    return {
        "name": product.get("name", ""),
        "type": product.get("type", ""),
        "series": product.get("seriesName", ""),
        "status": _normalize_status(product.get("status", "")),
        "wifi": _has_capability(product.get("wifi", "")),
        "bluetooth": _has_capability(product.get("bluetooth", "")),
        "thread_zigbee": _has_capability(product.get("threadZigbee", "")),
        "flash_mb": flash_mb,
        "psram_mb": psram_mb,
        "sram_kb": _parse_sram_kb(product.get("sram", "")),
        "gpio": int(product.get("gpio", 0) or 0),
        "antenna": product.get("antenna", ""),
    }


def _product_detail(product: dict) -> dict[str, Any]:
    """Full formatted product record."""
    flash_mb, flash_type = _parse_memory_field(product.get("flash", ""))
    psram_mb, psram_type = _parse_memory_field(product.get("psram", ""))
    return {
        "name": product.get("name", ""),
        "name_new": product.get("nameNew", ""),
        "type": product.get("type", ""),
        "series": product.get("seriesName", ""),
        "status": _normalize_status(product.get("status", "")),
        "mpn": product.get("mpn", ""),
        "dimensions": product.get("dimensions", ""),
        "wifi": product.get("wifi", ""),
        "wifi6": product.get("wifi6", ""),
        "bluetooth": product.get("bluetooth", ""),
        "thread_zigbee": product.get("threadZigbee", ""),
        "pins": product.get("pins", ""),
        "frequency_mhz": product.get("freq", ""),
        "sram_kb": _parse_sram_kb(product.get("sram", "")),
        "rom_kb": product.get("rom", ""),
        "flash_mb": flash_mb,
        "flash_type": flash_type,
        "psram_mb": psram_mb,
        "psram_type": psram_type,
        "gpio": int(product.get("gpio", 0) or 0),
        "operating_temp": product.get("operatingTemp", ""),
        "voltage_range": product.get("voltageRange", ""),
        "antenna": product.get("antenna", ""),
        "size_type": product.get("sizeType", ""),
        "release_time": product.get("releaseTime", ""),
        "idf_supports": product.get("idfSupports", ""),
        "spq": product.get("spq", ""),
        "moq": product.get("moq", ""),
        "lead_time": product.get("leadTime", ""),
        "eccn_code": product.get("eccnCode", ""),
        "ccatsz_code": product.get("ccatszCode", ""),
        "hs_code": product.get("hsCode", ""),
        "pre_firmware": product.get("preFirmware", ""),
        "replaced_by": product.get("replacedByName", ""),
    }


def _parse_lead_weeks(lead_time: str) -> int | None:
    """Extract numeric weeks from lead time strings like '8-10 weeks'."""
    if not lead_time:
        return None
    m = re.search(r"(\d+)", lead_time)
    return int(m.group(1)) if m else None


def _parse_temp_max(temp_str: str) -> int | None:
    """Extract max operating temperature from strings like '-40~85C'."""
    if not temp_str:
        return None
    m = re.search(r"~\s*(\d+)", temp_str)
    if m:
        return int(m.group(1))
    m = re.search(r"(\d+)\s*[Cc]", temp_str)
    return int(m.group(1)) if m else None


class ProductCatalog:
    """Espressif product catalog — chip/module discovery, comparison, and procurement"""

    def __init__(self, app: FastMCP, config: ESPToolServerConfig) -> None:
        self.app = app
        self.config = config
        self._products: list[dict] = []
        self._last_fetch: float = 0
        self._cache_ttl: int = 86400  # 24 hours
        self._register_tools()
        self._register_resources()

    async def _ensure_data(self, force_refresh: bool = False) -> list[dict]:
        """Lazy-load and cache the product catalog from Espressif's API."""
        now = time.time()
        if self._products and not force_refresh and (now - self._last_fetch) < self._cache_ttl:
            return self._products

        logger.info("Fetching Espressif product catalog...")
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.get(CATALOG_URL)
            resp.raise_for_status()
            data = resp.json()

        self._products = data.get("results", [])
        self._last_fetch = time.time()
        logger.info("Cached %d products from Espressif catalog", len(self._products))
        return self._products

    def _fuzzy_find(self, products: list[dict], query: str, threshold: int = 60) -> list[dict]:
        """Fuzzy-match products by name or MPN."""
        from thefuzz import fuzz

        scored = []
        q = query.lower()
        for p in products:
            name = (p.get("name") or "").lower()
            mpn = (p.get("mpn") or "").lower()
            if name == q or mpn == q:
                scored.append((300, p))
            elif q in name or q in mpn:
                scored.append((200, p))
            elif name in q or mpn in q:
                scored.append((150, p))
            else:
                score = max(fuzz.partial_ratio(q, name), fuzz.partial_ratio(q, mpn))
                if score >= threshold:
                    scored.append((score, p))
        scored.sort(key=lambda x: x[0], reverse=True)
        return [p for _, p in scored]

    def _register_resources(self) -> None:
        """Register MCP resources."""

        @self.app.resource("esp://products/catalog")
        async def product_catalog_resource() -> str:
            """Complete Espressif product catalog — SoCs and modules with full specs"""
            products = await self._ensure_data()
            return json.dumps([_product_summary(p) for p in products])

    def _register_tools(self) -> None:
        """Register product catalog tools with FastMCP."""

        @self.app.tool("esp_product_search")
        async def product_search(
            context: Context,
            series: str | None = None,
            product_type: str | None = None,
            wifi: bool | None = None,
            bluetooth: bool | None = None,
            thread_zigbee: bool | None = None,
            min_flash_mb: int | None = None,
            min_psram_mb: int | None = None,
            min_sram_kb: int | None = None,
            min_gpio: int | None = None,
            max_temp_c: int | None = None,
            status: str | None = None,
            antenna: str | None = None,
            keyword: str | None = None,
        ) -> dict[str, Any]:
            """Search the Espressif product catalog by specs, capabilities, or keyword.

            Filter by chip family, connectivity, memory, GPIO count, temperature
            range, production status, antenna type, or free-text keyword. All
            filters are optional and combine with AND logic. Returns matching
            products with key specs.

            Args:
                series: Chip family filter, e.g. "ESP32-S3", "ESP32-C6"
                product_type: "SoC" or "Module"
                wifi: Filter to products with Wi-Fi
                bluetooth: Filter to products with Bluetooth
                thread_zigbee: Filter to products with Thread/Zigbee (802.15.4)
                min_flash_mb: Minimum flash size in MB
                min_psram_mb: Minimum PSRAM size in MB
                min_sram_kb: Minimum SRAM size in KB
                min_gpio: Minimum GPIO pin count
                max_temp_c: Required upper operating temperature (e.g. 105 for industrial)
                status: Production status — "Mass Production", "NRND", "EOL", "Sample"
                antenna: Antenna type — "PCB", "IPEX", etc.
                keyword: Fuzzy search on product name or MPN
            """
            return await self._product_search_impl(
                context,
                series=series,
                product_type=product_type,
                wifi=wifi,
                bluetooth=bluetooth,
                thread_zigbee=thread_zigbee,
                min_flash_mb=min_flash_mb,
                min_psram_mb=min_psram_mb,
                min_sram_kb=min_sram_kb,
                min_gpio=min_gpio,
                max_temp_c=max_temp_c,
                status=status,
                antenna=antenna,
                keyword=keyword,
            )

        @self.app.tool("esp_product_info")
        async def product_info(
            context: Context,
            name: str,
        ) -> dict[str, Any]:
            """Get detailed specifications for a specific Espressif product.

            Looks up a product by name or MPN (fuzzy-matched) and returns
            the full spec sheet: memory, connectivity, GPIO, temperature
            range, procurement details, ECCN/HS codes, and more.

            Args:
                name: Product name or MPN, e.g. "ESP32-S3-WROOM-1" (fuzzy-matched)
            """
            return await self._product_info_impl(context, name)

        @self.app.tool("esp_chip_compare")
        async def chip_compare(
            context: Context,
            products: list[str],
        ) -> dict[str, Any]:
            """Compare 2-4 Espressif products side-by-side.

            Produces a comparison table highlighting differences in connectivity,
            memory, GPIO, temperature range, and procurement status. Useful for
            deciding between chip families or module variants.

            Args:
                products: List of 2-4 product names to compare (fuzzy-matched)
            """
            return await self._chip_compare_impl(context, products)

        @self.app.tool("esp_product_recommend")
        async def product_recommend(
            context: Context,
            use_case: str,
            constraints: str | None = None,
            prefer_module: bool = True,
        ) -> dict[str, Any]:
            """Get chip/module recommendations for a use case.

            Describe what you're building and any constraints, and this tool
            filters the Espressif catalog to matching products ranked by fit.
            Only returns products in Mass Production by default.

            Args:
                use_case: What you're building, e.g. "battery-powered BLE sensor"
                constraints: Technical constraints, e.g. "needs PSRAM, temp to 105C"
                prefer_module: Prefer modules over bare SoCs (default: true)
            """
            return await self._product_recommend_impl(context, use_case, constraints, prefer_module)

        @self.app.tool("esp_product_availability")
        async def product_availability(
            context: Context,
            series: str | None = None,
            status: str = "Mass Production",
            max_lead_weeks: int | None = None,
            max_moq: int | None = None,
        ) -> dict[str, Any]:
            """Check product availability and procurement details.

            Filter the catalog by production status, lead time, and MOQ
            to find products you can actually source. Useful for procurement
            planning and identifying alternatives for EOL/NRND parts.

            Args:
                series: Chip family filter, e.g. "ESP32-S3"
                status: Production status filter (default: "Mass Production")
                max_lead_weeks: Maximum acceptable lead time in weeks
                max_moq: Maximum acceptable minimum order quantity
            """
            return await self._product_availability_impl(
                context, series, status, max_lead_weeks, max_moq
            )

    # ── Tool implementations ────────────────────────────────────────────

    async def _product_search_impl(
        self,
        context: Context,
        series: str | None = None,
        product_type: str | None = None,
        wifi: bool | None = None,
        bluetooth: bool | None = None,
        thread_zigbee: bool | None = None,
        min_flash_mb: int | None = None,
        min_psram_mb: int | None = None,
        min_sram_kb: int | None = None,
        min_gpio: int | None = None,
        max_temp_c: int | None = None,
        status: str | None = None,
        antenna: str | None = None,
        keyword: str | None = None,
    ) -> dict[str, Any]:
        products = await self._ensure_data()

        # Start with keyword pre-filter if provided
        if keyword:
            candidates = self._fuzzy_find(products, keyword, threshold=50)
        else:
            candidates = products

        results = []
        for p in candidates:
            if series and series.lower() not in (p.get("seriesName") or "").lower():
                continue
            if product_type and (p.get("type") or "").lower() != product_type.lower():
                continue
            if wifi is True and not _has_capability(p.get("wifi", "")):
                continue
            if wifi is False and _has_capability(p.get("wifi", "")):
                continue
            if bluetooth is True and not _has_capability(p.get("bluetooth", "")):
                continue
            if bluetooth is False and _has_capability(p.get("bluetooth", "")):
                continue
            if thread_zigbee is True and not _has_capability(p.get("threadZigbee", "")):
                continue
            if thread_zigbee is False and _has_capability(p.get("threadZigbee", "")):
                continue
            flash_mb, _ = _parse_memory_field(p.get("flash", ""))
            if min_flash_mb is not None and flash_mb < min_flash_mb:
                continue
            psram_mb, _ = _parse_memory_field(p.get("psram", ""))
            if min_psram_mb is not None and psram_mb < min_psram_mb:
                continue
            sram_kb = _parse_sram_kb(p.get("sram", ""))
            if min_sram_kb is not None and sram_kb < min_sram_kb:
                continue
            gpio = int(p.get("gpio", 0) or 0)
            if min_gpio is not None and gpio < min_gpio:
                continue
            if max_temp_c is not None:
                temp_max = _parse_temp_max(p.get("operatingTemp", ""))
                if temp_max is None or temp_max < max_temp_c:
                    continue
            if status and _normalize_status(p.get("status", "")) != _normalize_status(status):
                continue
            if antenna and antenna.lower() not in (p.get("antenna") or "").lower():
                continue

            results.append(_product_summary(p))

        return {
            "count": len(results),
            "filters_applied": {
                k: v
                for k, v in {
                    "series": series,
                    "product_type": product_type,
                    "wifi": wifi,
                    "bluetooth": bluetooth,
                    "thread_zigbee": thread_zigbee,
                    "min_flash_mb": min_flash_mb,
                    "min_psram_mb": min_psram_mb,
                    "min_sram_kb": min_sram_kb,
                    "min_gpio": min_gpio,
                    "max_temp_c": max_temp_c,
                    "status": status,
                    "antenna": antenna,
                    "keyword": keyword,
                }.items()
                if v is not None
            },
            "products": results,
        }

    async def _product_info_impl(
        self, context: Context, name: str
    ) -> dict[str, Any]:
        products = await self._ensure_data()
        matches = self._fuzzy_find(products, name, threshold=65)
        if not matches:
            return {"error": f"No product found matching '{name}'", "suggestion": "Try esp_product_search with a keyword to browse available products"}
        return _product_detail(matches[0])

    async def _chip_compare_impl(
        self, context: Context, products: list[str]
    ) -> dict[str, Any]:
        if len(products) < 2:
            return {"error": "Provide at least 2 product names to compare"}
        if len(products) > 4:
            return {"error": "Compare up to 4 products at a time"}

        all_products = await self._ensure_data()
        resolved = []
        not_found = []
        for name in products:
            matches = self._fuzzy_find(all_products, name, threshold=65)
            if matches:
                resolved.append(_product_detail(matches[0]))
            else:
                not_found.append(name)

        if not_found:
            return {"error": f"Could not find: {', '.join(not_found)}", "found": [r["name"] for r in resolved]}

        # Build comparison — highlight fields that differ
        all_keys = list(resolved[0].keys())
        differences = {}
        common = {}
        for key in all_keys:
            values = [r[key] for r in resolved]
            if len({str(v) for v in values}) > 1:
                differences[key] = {r["name"]: r[key] for r in resolved}
            else:
                common[key] = values[0]

        return {
            "products_compared": [r["name"] for r in resolved],
            "differences": differences,
            "common": common,
            "full_specs": {r["name"]: r for r in resolved},
        }

    async def _product_recommend_impl(
        self,
        context: Context,
        use_case: str,
        constraints: str | None,
        prefer_module: bool,
    ) -> dict[str, Any]:
        products = await self._ensure_data()

        # Parse use case and constraints into filter criteria
        text = f"{use_case} {constraints or ''}".lower()

        wants_wifi = any(w in text for w in ("wifi", "wi-fi", "wireless", "iot", "mqtt", "http"))
        wants_ble = any(w in text for w in ("ble", "bluetooth", "beacon"))
        wants_thread = any(w in text for w in ("thread", "zigbee", "matter", "802.15.4"))
        wants_psram = "psram" in text
        wants_industrial = any(w in text for w in ("industrial", "105", "automotive", "harsh"))

        # Extract numeric constraints from text
        min_flash = None
        m = re.search(r"(\d+)\s*mb\s*flash", text)
        if m:
            min_flash = int(m.group(1))

        min_psram = None
        m = re.search(r"(\d+)\s*mb\s*psram", text)
        if m:
            min_psram = int(m.group(1))

        candidates = []
        for p in products:
            # Only recommend actively produced parts
            if _normalize_status(p.get("status", "")) != "Mass Production":
                continue
            if prefer_module and (p.get("type") or "").lower() != "module":
                continue
            if wants_wifi and not _has_capability(p.get("wifi", "")):
                continue
            if wants_ble and not _has_capability(p.get("bluetooth", "")):
                continue
            if wants_thread and not _has_capability(p.get("threadZigbee", "")):
                continue

            flash_mb, _ = _parse_memory_field(p.get("flash", ""))
            psram_mb, _ = _parse_memory_field(p.get("psram", ""))

            if wants_psram and psram_mb == 0:
                continue
            if min_flash is not None and flash_mb < min_flash:
                continue
            if min_psram is not None and psram_mb < min_psram:
                continue
            if wants_industrial:
                temp_max = _parse_temp_max(p.get("operatingTemp", ""))
                if temp_max is None or temp_max < 105:
                    continue

            candidates.append(_product_summary(p))

        # If module preference yields nothing, fall back to including SoCs
        if not candidates and prefer_module:
            return await self._product_recommend_impl(context, use_case, constraints, False)

        return {
            "use_case": use_case,
            "constraints_parsed": {
                "wifi": wants_wifi,
                "bluetooth": wants_ble,
                "thread_zigbee": wants_thread,
                "psram_required": wants_psram,
                "industrial_temp": wants_industrial,
                "min_flash_mb": min_flash,
                "min_psram_mb": min_psram,
                "prefer_module": prefer_module,
            },
            "count": len(candidates),
            "recommendations": candidates[:15],
        }

    async def _product_availability_impl(
        self,
        context: Context,
        series: str | None,
        status: str,
        max_lead_weeks: int | None,
        max_moq: int | None,
    ) -> dict[str, Any]:
        products = await self._ensure_data()
        results = []

        for p in products:
            if series and series.lower() not in (p.get("seriesName") or "").lower():
                continue
            if _normalize_status(p.get("status", "")) != _normalize_status(status):
                continue

            lead_weeks = _parse_lead_weeks(p.get("leadTime", ""))
            if max_lead_weeks is not None and lead_weeks is not None and lead_weeks > max_lead_weeks:
                continue

            moq_val = p.get("moq", "")
            moq_int = None
            if moq_val:
                m = re.search(r"(\d+)", str(moq_val))
                if m:
                    moq_int = int(m.group(1))
            if max_moq is not None and moq_int is not None and moq_int > max_moq:
                continue

            results.append({
                **_product_summary(p),
                "lead_time": p.get("leadTime", ""),
                "moq": p.get("moq", ""),
                "spq": p.get("spq", ""),
                "mpn": p.get("mpn", ""),
                "replaced_by": p.get("replacedByName", ""),
            })

        return {
            "count": len(results),
            "filters": {
                "series": series,
                "status": status,
                "max_lead_weeks": max_lead_weeks,
                "max_moq": max_moq,
            },
            "products": results,
        }
