"""Discover provider adapters from the bundled and Okstra-user locations."""
from __future__ import annotations

import json
from collections.abc import Iterable, Mapping
from functools import lru_cache
from pathlib import Path

from okstra_project.dirs import okstra_home

from ..domain.provider import ProviderSpec, UnknownProviderError
from ..json_boundary import JsonBoundaryError, load_owned_object
from .factory_loader import FactoryLoadError, load_relative_factory


class ProviderAdapterContractError(ValueError):
    """Raised when a discovered provider adapter violates its contract."""


class ProviderRegistry:
    """Resolved provider specifications, indexed by their normalized id."""

    def __init__(self, providers: Mapping[str, ProviderSpec]) -> None:
        self._providers = dict(providers)

    @classmethod
    def from_roots(cls, roots: Iterable[Path]) -> "ProviderRegistry":
        providers: dict[str, ProviderSpec] = {}
        for manifest_path, provider_id, factory_ref in _manifest_entries(roots):
            spec = _load_provider_spec(manifest_path, provider_id, factory_ref)
            providers[provider_id] = spec
        return cls(providers)

    def resolve(self, provider_id: str) -> ProviderSpec:
        normalized = provider_id.strip().lower()
        try:
            return self._providers[normalized]
        except KeyError as exc:
            allowed = ", ".join(self._providers)
            raise UnknownProviderError(
                f"unknown provider {provider_id!r}. Allowed values: {allowed}"
            ) from exc

    def ids(self, role: str | None = None) -> tuple[str, ...]:
        if role is None:
            return tuple(self._providers)
        return tuple(
            provider_id
            for provider_id, spec in self._providers.items()
            if spec.supports_role(role)
        )

    @property
    def providers(self) -> Mapping[str, ProviderSpec]:
        return dict(self._providers)


def default_provider_registry() -> ProviderRegistry:
    """Load bundled providers plus only adapters owned by the Okstra user home."""
    bundled_root = Path(__file__).resolve().parents[1] / "adapters" / "providers"
    user_root = okstra_home() / "adapters" / "providers"
    if user_root.is_symlink():
        raise ProviderAdapterContractError(
            f"provider discovery root is symbolic link: {user_root}"
        )
    return _registry_for_roots(bundled_root.resolve(), user_root.resolve())


@lru_cache(maxsize=None)
def _registry_for_roots(
    bundled_root: Path,
    user_root: Path,
) -> ProviderRegistry:
    discovered = ProviderRegistry.from_roots((bundled_root, user_root)).providers
    bundled_order = ("claude", "antigravity", "codex", "grok", "kimi")
    ordered = {
        provider_id: discovered[provider_id]
        for provider_id in bundled_order
        if provider_id in discovered
    }
    ordered.update(
        (provider_id, discovered[provider_id])
        for provider_id in sorted(discovered)
        if provider_id not in ordered
    )
    return ProviderRegistry(ordered)


def _manifest_entries(roots: Iterable[Path]) -> list[tuple[Path, str, str]]:
    entries: list[tuple[Path, str, str]] = []
    provider_paths: dict[str, Path] = {}
    for manifest_path in _manifest_paths(roots):
        provider_id, factory_ref = _read_manifest(manifest_path)
        if provider_id in provider_paths:
            raise ProviderAdapterContractError(
                f"duplicate provider id {provider_id!r}"
            )
        provider_paths[provider_id] = manifest_path
        entries.append((manifest_path, provider_id, factory_ref))
    return entries


def _manifest_paths(roots: Iterable[Path]) -> tuple[Path, ...]:
    manifests: list[Path] = []
    for root in roots:
        if root.is_symlink():
            raise ProviderAdapterContractError(
                f"provider discovery root is symbolic link: {root}"
            )
        if not root.is_dir():
            continue
        resolved_root = root.resolve()
        for manifest_path in sorted(root.glob("*/manifest.json")):
            resolved_manifest = manifest_path.resolve()
            if resolved_root not in resolved_manifest.parents:
                raise ProviderAdapterContractError(
                    f"provider adapter escapes discovery root: {manifest_path}"
                )
            manifests.append(resolved_manifest)
    return tuple(manifests)


def _read_manifest(manifest_path: Path) -> tuple[str, str]:
    try:
        raw = load_owned_object(manifest_path, artifact="provider adapter manifest")
    except JsonBoundaryError as exc:
        raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}") from exc
    if not isinstance(raw, dict) or raw.get("schemaVersion") != 1:
        raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}")
    provider_id = raw.get("id")
    factory_ref = raw.get("factory")
    if not isinstance(provider_id, str) or not provider_id.strip():
        raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}")
    if not isinstance(factory_ref, str) or not factory_ref:
        raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}")
    return provider_id.strip().lower(), factory_ref


def _load_provider_spec(
    manifest_path: Path, provider_id: str, factory_ref: str,
) -> ProviderSpec:
    try:
        spec = load_relative_factory(manifest_path, factory_ref)()
    except FactoryLoadError as exc:
        raise ProviderAdapterContractError(str(exc)) from exc
    except Exception as exc:
        raise ProviderAdapterContractError(
            f"provider factory failed for {provider_id!r}"
        ) from exc
    if not isinstance(spec, ProviderSpec) or spec.provider != provider_id:
        raise ProviderAdapterContractError(
            f"provider factory returned invalid provider {provider_id!r}"
        )
    return spec
