from __future__ import annotations

import hashlib
import subprocess
from collections.abc import Iterable, Iterator
from dataclasses import dataclass, replace
from typing import Any, Protocol

from account_registry import Account, AccountRegistry, AccountRegistryKind, AuthorityMode
from authority_client import (
    AuthorityConfigurationError,
    gateway_health_snapshot,
    resolve_authority_endpoint,
)
from device_auth import DeviceAuthPrompt, DeviceAuthSession
from device_auth_operation import DeviceAuthOperation, FlowEvent
from health_client import AccountHealthClient, AccountSnapshot, HealthStatus
from health_store import HealthSnapshotStore


class LifecyclePrompt(Protocol):
    @property
    def url(self) -> str: ...

    @property
    def raw_text(self) -> str: ...


class LifecycleSession(Protocol):
    @property
    def process(self) -> subprocess.Popen[Any]: ...


@dataclass(frozen=True)
class LifecycleEvent:
    kind: str
    account: Account | None = None
    message: str | None = None
    error: Exception | None = None
    prompt: DeviceAuthPrompt | LifecyclePrompt | None = None
    session: DeviceAuthSession | LifecycleSession | None = None
    collision: Account | None = None
    existing_accounts: list[tuple[str, str]] | None = None


class AccountLifecycle(Protocol):
    def add(
        self,
        alias: str,
        *,
        login_hint: str | None = None,
        slug: str | None = None,
    ) -> Iterator[LifecycleEvent]: ...

    def reauthenticate(self, account: Account) -> Iterator[LifecycleEvent]: ...

    def submit_code(self, session: LifecycleSession, code: str) -> None: ...


class AccountHealthProvider(Protocol):
    def fetch(self, account: Account, timeout_secs: float = 10.0) -> AccountSnapshot: ...


@dataclass(frozen=True)
class ProviderServices:
    tool: AccountRegistryKind
    registry: AccountRegistry
    lifecycle: AccountLifecycle
    health: AccountHealthProvider


class CodexLifecycleAdapter:
    def __init__(self, operation: DeviceAuthOperation) -> None:
        self._operation = operation

    def add(
        self,
        alias: str,
        *,
        login_hint: str | None = None,
        slug: str | None = None,
    ) -> Iterator[LifecycleEvent]:
        del login_hint
        for event in self._operation.add(alias, slug=slug):
            yield self._translate_event(event)

    def reauthenticate(self, account: Account) -> Iterator[LifecycleEvent]:
        for event in self._operation.repair(account):
            yield self._translate_event(event)

    @staticmethod
    def _translate_event(event: FlowEvent) -> LifecycleEvent:
        return LifecycleEvent(
            kind=event.kind,
            account=event.account,
            message=event.message,
            error=event.error,
            prompt=event.prompt,
            session=event.session,
            collision=event.collision,
            existing_accounts=event.existing_accounts,
        )


class CodexHealthAdapter:
    def __init__(self, client: AccountHealthClient) -> None:
        self._client = client

    def fetch(self, account: Account, timeout_secs: float = 10.0) -> AccountSnapshot:
        return self._client.fetch(
            account.account_home, timeout_secs=timeout_secs, allow_warmup=True
        )


class AuthorityHealthAdapter:
    def __init__(self, registry: AccountRegistry) -> None:
        self._base_dir = registry.base_dir
        self._store = HealthSnapshotStore(
            registry.base_dir / "gateway_health_cache.json", stale_after_s=15 * 60
        )

    def _cache_key(self, account: Account) -> str:
        binding = account.authority_binding
        if binding is None:
            raise ValueError("authority health requires a binding")
        try:
            endpoint_identity = resolve_authority_endpoint(
                self._base_dir, binding
            ).origin
        except AuthorityConfigurationError:
            endpoint_identity = "unresolved"
        try:
            grant_info = binding.proxy_grant_ref.lstat()
            grant_identity = ":".join(
                str(value)
                for value in (
                    binding.proxy_grant_ref,
                    grant_info.st_dev,
                    grant_info.st_ino,
                    grant_info.st_size,
                    grant_info.st_mtime_ns,
                )
            )
        except OSError:
            grant_identity = f"{binding.proxy_grant_ref}:missing"
        value = "\0".join(
            (
                account.tray_key,
                binding.authority_name,
                endpoint_identity,
                binding.route_id,
                binding.provider,
                grant_identity,
            )
        )
        return hashlib.sha256(value.encode("utf-8")).hexdigest()

    def fetch(self, account: Account, timeout_secs: float = 10.0) -> AccountSnapshot:
        snapshot = gateway_health_snapshot(
            self._base_dir, account, timeout_secs=timeout_secs
        )
        cache_key = self._cache_key(account)
        if snapshot.status != HealthStatus.UNKNOWN:
            cached = self._store.read()
            cached[cache_key] = snapshot
            self._store.write(cached)
            return snapshot
        previous = self._store.read().get(cache_key)
        if previous is None:
            return snapshot
        return replace(
            previous,
            status=HealthStatus.UNKNOWN,
            detail=f"{snapshot.detail}; last verified status is stale",
        )


class DarkAuthorityHealthAdapter:
    def __init__(
        self,
        native: AccountHealthProvider,
        gateway: AuthorityHealthAdapter,
    ) -> None:
        self._native = native
        self._gateway = gateway

    def fetch(self, account: Account, timeout_secs: float = 10.0) -> AccountSnapshot:
        native = self._native.fetch(account, timeout_secs=timeout_secs)
        gateway = self._gateway.fetch(account, timeout_secs=timeout_secs)
        detail = gateway.detail
        if native.detail:
            detail = f"{native.detail}; {gateway.detail}" if gateway.detail else native.detail
        return replace(native, detail=detail)


class ProviderServiceMap:
    def __init__(self, providers: Iterable[ProviderServices]) -> None:
        self._providers: dict[AccountRegistryKind, ProviderServices] = {}
        for provider in providers:
            if provider.tool in self._providers:
                raise ValueError(f"duplicate provider for tool '{provider.tool.value}'")
            self._providers[provider.tool] = provider

    def for_tool(self, tool: AccountRegistryKind | str) -> ProviderServices:
        normalized = self._normalize_tool(tool)
        try:
            return self._providers[normalized]
        except KeyError as exc:
            raise LookupError(f"missing provider for tool '{normalized.value}'") from exc

    def for_account(self, account: Account) -> ProviderServices:
        provider = self.for_tool(account.tool)
        if account.authority_binding is None:
            return provider
        gateway_health = AuthorityHealthAdapter(provider.registry)
        health: AccountHealthProvider = gateway_health
        if account.authority_binding.mode == AuthorityMode.SUBROUTER_DARK:
            health = DarkAuthorityHealthAdapter(provider.health, gateway_health)
        return ProviderServices(
            tool=provider.tool,
            registry=provider.registry,
            lifecycle=provider.lifecycle,
            health=health,
        )

    @staticmethod
    def _normalize_tool(tool: AccountRegistryKind | str) -> AccountRegistryKind:
        if isinstance(tool, AccountRegistryKind):
            return tool
        normalized = tool.strip().lower()
        try:
            return AccountRegistryKind(normalized)
        except ValueError as exc:
            raise ValueError(f"unknown provider tool '{tool}'") from exc
