from __future__ import annotations

import logging
from typing import Any, Callable


logger = logging.getLogger(__name__)


# Canonical freshness thresholds — these MUST match bin/update-intel.py
# check_freshness() so the web dashboard and the CLI tell the same story.
# (KEV >48h, NVD >24h, intel feeds >72h.)
_KEV_THRESHOLD_HOURS = 48.0
_NVD_THRESHOLD_HOURS = 24.0
_FEED_THRESHOLD_HOURS = 72.0


# Lazy import to avoid circular dependency at module level
def _get_staleness_tracker() -> Any:
    try:
        from lib.intel_status import assess  # intel staleness assessment

        return _IntelStatusAdapter(assess)
    except ImportError as exc:
        logger.error("lib.intel_status not available: %s", exc)
        raise


def _gather_real_feeds() -> list[dict[str, Any]]:
    """Read true per-feed freshness from the same sources the CLI reads.

    Mirrors bin/update-intel.py check_freshness(): KEV cache age, NVD CVE
    count (empty == stale), and intel-feed last_updated timestamps. Each feed
    is returned as a dict with its real age in hours and the threshold beyond
    which it is considered stale. Missing/unreadable sources are reported as
    not-present (which the adapter treats as stale) rather than silently ok.
    """
    import time
    from datetime import datetime, timezone

    feeds: list[dict[str, Any]] = []

    # --- CISA KEV catalog (>48h stale) ---------------------------------
    try:
        from threat_intel import get_threat_intel

        ti = get_threat_intel()
        kev_stats = ti.get_kev_stats()
        if kev_stats.get("status") == "loaded":
            feeds.append({
                "name": "kev",
                "present": True,
                "age_hours": float(kev_stats.get("cache_age_hours", 0.0)),
                "threshold_hours": _KEV_THRESHOLD_HOURS,
            })
        else:
            feeds.append({
                "name": "kev", "present": False,
                "age_hours": None, "threshold_hours": _KEV_THRESHOLD_HOURS,
            })
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("KEV freshness unavailable: %s", exc)
        feeds.append({
            "name": "kev", "present": False,
            "age_hours": None, "threshold_hours": _KEV_THRESHOLD_HOURS,
        })

    # --- NVD CVE cache (empty == stale) --------------------------------
    try:
        from vuln_database import get_vuln_database

        vdb = get_vuln_database()
        stats = vdb.get_statistics()
        cached = int(stats.get("cached_cves", 0))
        present = cached > 0
        age_hours: float | None = None
        if present and vdb.db_path.exists():
            age_hours = (time.time() - vdb.db_path.stat().st_mtime) / 3600.0
        feeds.append({
            "name": "nvd",
            "present": present,
            "age_hours": age_hours,
            "threshold_hours": _NVD_THRESHOLD_HOURS,
        })
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("NVD freshness unavailable: %s", exc)
        feeds.append({
            "name": "nvd", "present": False,
            "age_hours": None, "threshold_hours": _NVD_THRESHOLD_HOURS,
        })

    # --- Multi-source intel feeds (>72h stale) -------------------------
    try:
        from intel_feeds import IntelFeedManager

        mgr = IntelFeedManager()
        meta = mgr.get_stats().get("_meta", {})
        if not meta:
            feeds.append({
                "name": "intel_feeds", "present": False,
                "age_hours": None, "threshold_hours": _FEED_THRESHOLD_HOURS,
            })
        for feed_name, info in sorted(meta.items()):
            last = info.get("last_updated", "never")
            if not last or last == "never":
                feeds.append({
                    "name": feed_name, "present": False,
                    "age_hours": None, "threshold_hours": _FEED_THRESHOLD_HOURS,
                })
                continue
            try:
                dt = datetime.fromisoformat(last)
                age = (datetime.now(timezone.utc) - dt).total_seconds() / 3600.0
                feeds.append({
                    "name": feed_name, "present": True,
                    "age_hours": age, "threshold_hours": _FEED_THRESHOLD_HOURS,
                })
            except Exception:
                feeds.append({
                    "name": feed_name, "present": False,
                    "age_hours": None, "threshold_hours": _FEED_THRESHOLD_HOURS,
                })
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("Intel-feed freshness unavailable: %s", exc)
        feeds.append({
            "name": "intel_feeds", "present": False,
            "age_hours": None, "threshold_hours": _FEED_THRESHOLD_HOURS,
        })

    return feeds


class _IntelStatusAdapter:
    """Adapts real intel freshness into the tracker interface used by _dispatch.

    The previous implementation hardcoded {'status': 'ok'} and empty lists, so
    the dashboard always rendered GREEN regardless of true data age. This now
    reads the real per-feed ages (KEV/NVD/feeds) and classifies each against the
    same thresholds the CLI uses (bin/update-intel.py --check), then reuses
    lib.intel_status.assess to attach a human-readable day-bucket staleness
    label per feed.
    """

    def __init__(
        self,
        assess_fn: Any,
        gather_feeds: Callable[[], list[dict[str, Any]]] | None = None,
    ) -> None:
        self._assess = assess_fn
        self._gather_feeds = gather_feeds or _gather_real_feeds

    def _evaluate(self) -> list[dict[str, Any]]:
        """Return the normalized status for every known feed."""
        items: list[dict[str, Any]] = []
        for feed in self._gather_feeds():
            name = feed["name"]
            present = bool(feed.get("present"))
            age_hours = feed.get("age_hours")
            threshold = float(feed.get("threshold_hours", _FEED_THRESHOLD_HOURS))

            if not present or age_hours is None:
                # Missing data is stale, full stop — never report ok for it.
                items.append({
                    "name": name,
                    "present": False,
                    "stale": True,
                    "age_hours": None,
                    "age_days": None,
                    "threshold_hours": threshold,
                    "staleness": "missing",
                    "reason": "data source not present",
                })
                continue

            age_hours = float(age_hours)
            stale = age_hours > threshold
            # Reuse lib.intel_status.assess for the day-bucket label.
            try:
                verdict = self._assess(name, max(0, int(age_hours // 24)))
                label = verdict["staleness"]
                age_days = verdict["age_days"]
            except Exception:
                label = "stale" if stale else "fresh"
                age_days = int(age_hours // 24)

            items.append({
                "name": name,
                "present": True,
                "stale": stale,
                "age_hours": round(age_hours, 1),
                "age_days": age_days,
                "threshold_hours": threshold,
                "staleness": label,
                "reason": (
                    f"{age_hours:.0f}h old (threshold {threshold:.0f}h)"
                    if stale else "within threshold"
                ),
            })
        return items

    def get_staleness_summary(self) -> dict[str, Any]:
        items = self._evaluate()
        stale_items = [i for i in items if i["stale"]]
        return {
            "status": "stale" if stale_items else "ok",
            "total": len(items),
            "stale_count": len(stale_items),
            "stale_feeds": [i["name"] for i in stale_items],
            "feeds": items,
        }

    def list_items(self) -> list[Any]:
        return self._evaluate()

    def get_item(self, item_id: str) -> dict[str, Any] | None:
        for item in self._evaluate():
            if item["name"] == item_id:
                return item
        return None

    def create_item(self) -> dict[str, Any]:
        return {"created": True}

    def update_item(self, item_id: str) -> dict[str, Any] | None:
        return None

    def delete_item(self, item_id: str) -> bool:
        return False

    def trigger_refresh(self) -> dict[str, Any]:
        return {"refreshed": True}

    def get_stale_items(self) -> list[Any]:
        return [i for i in self._evaluate() if i["stale"]]


def handle(method: str, path: str) -> dict[str, dict[str, Any]]:
    """Route intel management API requests.

    Args:
        method: HTTP method (GET, POST, PUT, DELETE, PATCH).
        path: URL path string, e.g. '/intel/items' or '/intel/items/42'.

    Returns:
    """
    if not method or not isinstance(method, str):
        return {"status": {"code": 400, "error": "method must be a non-empty string"}}
    if not path or not isinstance(path, str):
        return {"status": {"code": 400, "error": "path must be a non-empty string"}}

    method = method.upper().strip()
    path = path.strip()

    allowed_methods = {"GET", "POST", "PUT", "DELETE", "PATCH"}
    if method not in allowed_methods:
        return {"status": {"code": 405, "error": f"method {method!r} not allowed"}}

    try:
        tracker = _get_staleness_tracker()
    except Exception as exc:
        logger.exception("Failed to initialise staleness tracker")
        return {"status": {"code": 503, "error": f"intel-staleness-tracker unavailable: {exc}"}}

    # Route dispatch
    try:
        return _dispatch(tracker, method, path)
    except Exception as exc:
        logger.exception("Unhandled error dispatching %s %s", method, path)
        return {"status": {"code": 500, "error": str(exc)}}


def _dispatch(tracker: Any, method: str, path: str) -> dict[str, dict[str, Any]]:
    """Internal dispatcher; raises on unexpected errors."""
    segments = [s for s in path.split("/") if s]

    # /intel/staleness  — GET staleness summary
    if method == "GET" and segments == ["intel", "staleness"]:
        data = tracker.get_staleness_summary()
        return {"status": {"code": 200, "data": data}}

    # /intel/items  — list items or create
    if segments[:1] == ["intel"] and segments[1:2] == ["items"]:
        item_id: str | None = segments[2] if len(segments) > 2 else None

        if method == "GET" and item_id is None:
            data = tracker.list_items()
            return {"status": {"code": 200, "data": data}}

        if method == "GET" and item_id is not None:
            data = tracker.get_item(item_id)
            if data is None:
                return {"status": {"code": 404, "error": f"item {item_id!r} not found"}}
            return {"status": {"code": 200, "data": data}}

        if method == "POST" and item_id is None:
            # Creation payload handled by caller; tracker.create_item() uses its own state.
            data = tracker.create_item()
            return {"status": {"code": 201, "data": data}}

        if method in {"PUT", "PATCH"} and item_id is not None:
            data = tracker.update_item(item_id)
            if data is None:
                return {"status": {"code": 404, "error": f"item {item_id!r} not found"}}
            return {"status": {"code": 200, "data": data}}

        if method == "DELETE" and item_id is not None:
            removed = tracker.delete_item(item_id)
            if not removed:
                return {"status": {"code": 404, "error": f"item {item_id!r} not found"}}
            return {"status": {"code": 204, "data": None}}

    # /intel/refresh  — POST triggers a refresh
    if method == "POST" and segments == ["intel", "refresh"]:
        data = tracker.trigger_refresh()
        return {"status": {"code": 202, "data": data}}

    # /intel/stale  — GET list of stale items
    if method == "GET" and segments == ["intel", "stale"]:
        data = tracker.get_stale_items()
        return {"status": {"code": 200, "data": data}}

    return {"status": {"code": 404, "error": f"no route for {method} {path!r}"}}
