"""Ecosystem inference: registry probing with Tavily+model fallback."""

import urllib.error
import urllib.request

from .. import model_caller
from .. import tavily as tavily_client

# Ordered list of (ecosystem, probe_fn) — first hit wins.
# Each probe_fn takes a package name and returns True if found.

_TIMEOUT = 5

_VALID_ECOSYSTEMS = {
    "python", "javascript", "java", "go", "dotnet",
    "perl", "php", "c", "cpp", "system", "desktop-app", "driver",
}

_MODEL_ECOSYSTEM_LIST = "\n".join(f"- {e}" for e in sorted(_VALID_ECOSYSTEMS))

# Hard-coded overrides for packages whose ecosystem the registry probes
# or model reliably get wrong.  Key = package name (lowercase).
# These are typically standalone applications distributed as binaries,
# not libraries in any standard package ecosystem.
_ECOSYSTEM_OVERRIDES: dict[str, str] = {
    "grafana":      "desktop-app",   # binary releases on grafana.com / GitHub
    "prometheus":   "desktop-app",   # binary releases on GitHub
    "elasticsearch": "desktop-app",  # binary releases on elastic.co
    "kibana":       "desktop-app",   # binary releases on elastic.co
    "consul":       "desktop-app",   # binary releases on releases.hashicorp.com
    "vault":        "desktop-app",   # binary releases on releases.hashicorp.com
    "terraform":    "desktop-app",   # binary releases on releases.hashicorp.com
    "kubectl":      "desktop-app",   # binary releases on kubernetes.io
    "helm":         "desktop-app",   # binary releases on GitHub
    "botan2":       "cpp",           # C++ crypto library, system-packaged but identity is cpp
    "bottles":      "desktop-app",   # Linux app for running Windows software via Wine
}


def _head_ok(url: str) -> bool:
    """Return True if a HEAD (or GET) request to url returns 2xx."""
    try:
        req = urllib.request.Request(
            url,
            headers={"User-Agent": "veripak/0.1"},
            method="HEAD",
        )
        with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
            return resp.status < 300
    except Exception:
        return False


def _get_ok(url: str) -> bool:
    """Return True if a GET request returns 2xx (for APIs that reject HEAD)."""
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "veripak/0.1"})
        with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
            return resp.status < 300
    except Exception:
        return False


def _probe_pypi(name: str, version: str | None = None) -> bool:
    if version:
        return _head_ok(f"https://pypi.org/pypi/{name}/{version}/json")
    return _head_ok(f"https://pypi.org/pypi/{name}/json")


def _probe_npm(name: str, version: str | None = None) -> bool:
    encoded = name.replace("/", "%2F")
    if version:
        return _head_ok(f"https://registry.npmjs.org/{encoded}/{version}")
    return _head_ok(f"https://registry.npmjs.org/{encoded}")


def _probe_nuget(name: str, version: str | None = None) -> bool:
    lower = name.lower()
    if version:
        # Flat container has a specific version directory if it exists
        return _get_ok(
            f"https://api.nuget.org/v3-flatcontainer/{lower}/{version.lower()}/{lower}.{version.lower()}.nupkg"
        )
    return _get_ok(f"https://api.nuget.org/v3-flatcontainer/{lower}/index.json")


def _probe_go(name: str, version: str | None = None) -> bool:
    encoded = name.replace("/", "%2F")
    if version:
        v = version if version.startswith("v") else f"v{version}"
        return _get_ok(f"https://proxy.golang.org/{encoded}/@v/{v}.info")
    return _get_ok(f"https://proxy.golang.org/{encoded}/@latest")


def _probe_maven(name: str, version: str | None = None) -> bool:
    """Maven search always returns 200; check numFound > 0 in the JSON body.

    Tries artifact-ID search first (exact, e.g. "poi"), then falls back to
    free-text search (handles display names like "Apache POI").
    """
    import json as _json
    import urllib.parse as _urlparse

    def _maven_search(query: str) -> bool:
        url = f"https://search.maven.org/solrsearch/select?q={query}&rows=1&wt=json"
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "veripak/0.1"})
            with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
                body = _json.loads(resp.read())
                return body.get("response", {}).get("numFound", 0) > 0
        except Exception:
            return False

    # Try artifact-ID search (works for "poi", "tomcat-catalina", etc.)
    query = f"a:{name}+AND+v:{version}" if version else f"a:{name}"
    if _maven_search(query):
        return True

    # Fallback: free-text search for display names with spaces
    # (e.g. "Apache POI", "Apache Tomcat"). Skip for single-word names
    # to avoid false positives (e.g. "botan2" matching an unrelated Java lib).
    if " " in name:
        encoded = _urlparse.quote(name)
        return _maven_search(encoded)

    return False


def _probe_cpan(name: str, version: str | None = None) -> bool:
    return _get_ok(f"https://fastapi.metacpan.org/v1/module/{name}")


def _probe_packagist(name: str, version: str | None = None) -> bool:
    # Packagist requires vendor/package format. Names without "/" are not PHP packages.
    if "/" not in name:
        return False
    return _get_ok(f"https://packagist.org/packages/{name.lower()}.json")


_REGISTRY_PROBES: list[tuple[str, object]] = [
    ("python",     _probe_pypi),
    ("javascript", _probe_npm),
    ("dotnet",     _probe_nuget),
    ("go",         _probe_go),
    ("java",       _probe_maven),
    ("perl",       _probe_cpan),
    ("php",        _probe_packagist),
]


def _infer_via_model(name: str, version: str | None = None) -> str | None:
    """Use Tavily search + model to classify a package's ecosystem."""
    query = f"{name} {version} software package" if version else f"{name} software package"
    try:
        results = tavily_client.search(query, max_results=5)
    except Exception:
        results = []

    snippets = []
    for r in results:
        title = r.get("title", "")
        content = (r.get("content") or "")[:300]
        snippets.append(f"{title}: {content}")

    search_text = "\n\n".join(snippets) if snippets else "(no search results)"

    prompt = (
        f'How is the software package "{name}" DISTRIBUTED to end users?\n\n'
        f"Choose the ecosystem based on where users obtain and install the package, "
        f"NOT the programming language it is written in.\n\n"
        f"Key distinctions:\n"
        f'- "go" means a Go library installed via `go get` (e.g. github.com/pkg/errors)\n'
        f'- "desktop-app" means a standalone binary application released on GitHub or '
        f"an official download page (e.g. Grafana, Prometheus, Vault, kubectl)\n"
        f'- "python" means a package installed via pip/PyPI\n\n'
        f"Search results:\n{search_text}\n\n"
        f"Reply with exactly one of these ecosystem names, nothing else:\n"
        f"{_MODEL_ECOSYSTEM_LIST}"
    )

    try:
        raw = model_caller.call_model(prompt).strip().lower().strip('"').strip("'")
        if raw in _VALID_ECOSYSTEMS:
            return raw
    except Exception:
        pass
    return None


def infer_ecosystem(name: str, version: str | None = None) -> str | None:
    """Infer the ecosystem for a package.

    Stage 1: probe programmatic registries (PyPI, npm, NuGet, Go, Maven,
    CPAN, Packagist) — first 2xx response wins.
    Stage 2: Tavily search + model for anything not found on a registry.

    Returns None if inference fails (caller should prompt user to supply
    --ecosystem manually).
    """
    override = _ECOSYSTEM_OVERRIDES.get(name.lower())
    if override:
        return override

    for ecosystem, probe_fn in _REGISTRY_PROBES:
        if probe_fn(name, version):
            return ecosystem

    return _infer_via_model(name, version)
