"""Read-only AppResolver — canonical app_id → installed device entry (DSL v0.1 Phase 3a)."""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal

_CATALOG_PATH = Path(__file__).with_name("app_catalog_v0_1.json")
_AMBIGUITY_THRESHOLD = 0.6
_RESOLVE_THRESHOLD = 0.75

ResolveStatus = Literal["resolved", "ambiguous", "missing"]


@dataclass(frozen=True)
class AppResolveResult:
    status: ResolveStatus
    app_id: str
    entry: dict[str, Any] | None = None
    candidates: tuple[dict[str, Any], ...] = ()
    prompt: dict[str, Any] | None = None
    install_hint: str = ""


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


@lru_cache(maxsize=1)
def load_app_catalog() -> dict[str, Any]:
    raw = _CATALOG_PATH.read_text(encoding="utf-8")
    data = json.loads(raw)
    if not isinstance(data, dict):
        raise ValueError("app catalog must be a JSON object")
    return data


def catalog_app_ids() -> list[str]:
    catalog = load_app_catalog()
    apps = catalog.get("apps")
    if not isinstance(apps, list):
        return []
    return [str(item.get("app_id") or "") for item in apps if isinstance(item, dict) and item.get("app_id")]


def catalog_entry_for_app_id(app_id: str) -> dict[str, Any] | None:
    needle = str(app_id or "").strip().lower()
    if not needle:
        return None
    catalog = load_app_catalog()
    apps = catalog.get("apps")
    if not isinstance(apps, list):
        return None
    for item in apps:
        if isinstance(item, dict) and str(item.get("app_id") or "").lower() == needle:
            return dict(item)
    return None


def legacy_slug_to_app_id(slug: str) -> str | None:
    """Map legacy plan slugs (firefox, libreoffice, cursor) to canonical app_id."""
    cleaned = re.sub(r"[^a-z0-9]+", " ", str(slug or "").strip().lower()).strip()
    if not cleaned:
        return None

    catalog = load_app_catalog()
    apps = catalog.get("apps")
    if not isinstance(apps, list):
        return None

    for item in apps:
        if not isinstance(item, dict):
            continue
        app_id = str(item.get("app_id") or "")
        maps_from = item.get("maps_from")
        if not app_id or not isinstance(maps_from, list):
            continue
        aliases = {app_id.replace("_", " "), app_id.replace("_", "-")}
        for phrase in maps_from:
            token = str(phrase or "").strip().lower()
            if token:
                aliases.add(token)
                aliases.add(token.replace(" ", "-"))
        inventory = item.get("inventory_match")
        if isinstance(inventory, dict):
            for stem in inventory.get("desktop_stems") or []:
                aliases.add(str(stem or "").strip().lower())
            for keyword in inventory.get("name_keywords") or []:
                aliases.add(str(keyword or "").strip().lower())

        if cleaned in aliases or cleaned.replace(" ", "-") in aliases:
            return app_id
        if any(alias in cleaned or cleaned in alias for alias in aliases if len(alias) >= 4):
            return app_id

    static_map = {
        "browser": "web_browser",
        "default": "web_browser",
        "libreoffice": "document_writer",
        "writer": "document_writer",
        "cursor": "code_editor",
        "vscode": "code_editor",
        "code": "code_editor",
        "telegram": "messenger_telegram",
        "viber": "messenger_viber",
        "claude": "ai_assistant_claude",
        "gedit": "text_editor_simple",
        "kate": "text_editor_simple",
        "thunderbird": "email_client",
        "gnome-system-monitor": "system_monitor",
        "stacer": "system_monitor",
        "monitor": "system_monitor",
        "firefox": "web_browser",
        "chrome": "web_browser",
        "chromium": "web_browser",
    }
    return static_map.get(cleaned.replace(" ", "-"))


def compute_installed_apps_hash(entries: list[dict[str, Any]]) -> str:
    """SHA256 of canonical installed_apps JSON (cloud-safe subset)."""
    import hashlib

    cloud_safe: list[dict[str, Any]] = []
    for entry in entries:
        if not isinstance(entry, dict):
            continue
        cloud_safe.append(
            {
                "app_id": str(entry.get("app_id") or ""),
                "display_name": str(entry.get("display_name") or ""),
                "confidence": float(entry.get("confidence") or 0),
                "os_family": str(entry.get("os_family") or ""),
            }
        )
    cloud_safe.sort(key=lambda item: item["app_id"])
    canonical = json.dumps(cloud_safe, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def installed_app_ids_from_entries(entries: list[dict[str, Any]]) -> list[str]:
    ids = sorted(
        {
            str(entry.get("app_id") or "").strip()
            for entry in entries
            if isinstance(entry, dict) and str(entry.get("app_id") or "").strip()
        }
    )
    return ids


class AppResolver:
    """Read-only resolver: app_id → InstalledAppEntry | ambiguous | missing."""

    def __init__(
        self,
        *,
        installed_apps: list[dict[str, Any]] | None = None,
        os_family: str = "linux",
    ) -> None:
        self._installed_apps = [dict(item) for item in (installed_apps or []) if isinstance(item, dict)]
        self._os_family = str(os_family or "linux").lower()
        if self._os_family == "darwin":
            self._os_family = "macos"
        self._by_app_id: dict[str, list[dict[str, Any]]] = {}
        for entry in self._installed_apps:
            app_id = str(entry.get("app_id") or "").strip()
            if app_id:
                self._by_app_id.setdefault(app_id, []).append(entry)

    @classmethod
    def from_dcm(cls, dcm: dict[str, Any] | None) -> AppResolver:
        if not isinstance(dcm, dict):
            return cls()
        apps = dcm.get("installed_apps")
        return cls(
            installed_apps=apps if isinstance(apps, list) else None,
            os_family=str(dcm.get("os_family") or "linux"),
        )

    def resolve(self, app_id: str) -> AppResolveResult:
        needle = str(app_id or "").strip().lower()
        if not needle:
            return AppResolveResult(status="missing", app_id="", install_hint="app_id is empty")

        catalog = catalog_entry_for_app_id(needle)
        if catalog is None:
            return AppResolveResult(
                status="missing",
                app_id=needle,
                install_hint=f"Unknown catalog app_id: {needle}",
            )

        candidates = [dict(item) for item in self._by_app_id.get(needle, [])]
        if catalog.get("system_action") or catalog.get("runtime_builtin"):
            if candidates:
                best = max(candidates, key=lambda item: float(item.get("confidence") or 0))
                return AppResolveResult(status="resolved", app_id=needle, entry=best)
            synthetic = self._synthetic_entry(catalog)
            return AppResolveResult(status="resolved", app_id=needle, entry=synthetic)

        if not candidates:
            return AppResolveResult(
                status="missing",
                app_id=needle,
                install_hint=self._install_hint(catalog),
            )

        ranked = sorted(candidates, key=lambda item: float(item.get("confidence") or 0), reverse=True)
        best = ranked[0]
        best_conf = float(best.get("confidence") or 0)

        if len(ranked) >= 2:
            second_conf = float(ranked[1].get("confidence") or 0)
            if best_conf >= _AMBIGUITY_THRESHOLD and second_conf >= _AMBIGUITY_THRESHOLD:
                if abs(best_conf - second_conf) < 0.1 or best_conf < _RESOLVE_THRESHOLD:
                    return AppResolveResult(
                        status="ambiguous",
                        app_id=needle,
                        candidates=tuple(ranked[:5]),
                        prompt=self._ambiguous_prompt(needle, ranked[:5]),
                    )

        if best_conf < _RESOLVE_THRESHOLD:
            return AppResolveResult(
                status="ambiguous",
                app_id=needle,
                candidates=tuple(ranked[:5]),
                prompt=self._ambiguous_prompt(needle, ranked[:5]),
            )

        return AppResolveResult(status="resolved", app_id=needle, entry=best)

    def _synthetic_entry(self, catalog: dict[str, Any]) -> dict[str, Any]:
        app_id = str(catalog.get("app_id") or "")
        label = app_id.replace("_", " ").title()
        launch_kind = "shell" if catalog.get("system_action") else "binary"
        return {
            "app_id": app_id,
            "display_name": label,
            "version": None,
            "launch_spec": {"kind": launch_kind, "argv": []},
            "binary_path": None,
            "confidence": 1.0,
            "detected_at": _utc_now_iso(),
            "os_family": self._os_family,
            "aliases": list(catalog.get("maps_from") or []),
            "source": "priority_inject",
            "categories": [],
        }

    @staticmethod
    def _install_hint(catalog: dict[str, Any]) -> str:
        app_id = str(catalog.get("app_id") or "")
        maps_from = catalog.get("maps_from")
        examples = ""
        if isinstance(maps_from, list) and maps_from:
            examples = f" (e.g. {maps_from[0]})"
        return f"Install or enable {app_id}{examples} on this device."

    @staticmethod
    def _ambiguous_prompt(app_id: str, candidates: list[dict[str, Any]]) -> dict[str, Any]:
        labels = [str(item.get("display_name") or item.get("app_id") or "?") for item in candidates]
        if app_id == "web_browser":
            message = (
                f"На устройстве найдено несколько браузеров: {', '.join(labels)}. "
                "Какой использовать по умолчанию?"
            )
        else:
            message = (
                f"Найдено несколько приложений для «{app_id}»: {', '.join(labels)}. "
                "Какой вариант использовать?"
            )
        return {
            "kind": "app_choice",
            "app_id": app_id,
            "message": message,
            "choices": labels,
        }
