from __future__ import annotations

import hashlib
import os
import subprocess
from pathlib import Path
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 AuthorityLifecycleAdapter:
    """Lifecycle adapter for an already authority-bound account.

    It never reads a native or authority credential. Repair is delegated to the
    local authority service identity through its supported admin CLI. Account
    creation remains an explicit migration-coordinator operation so a normal
    Add action cannot silently migrate an account.
    """

    def __init__(
        self,
        native: AccountLifecycle,
        *,
        binary: Path | None = None,
        state_dir: Path | None = None,
        command_runner: Any | None = None,
    ) -> None:
        self._native = native
        self._binary = binary or Path(
            os.environ.get(
                "OVERDECK_SUBROUTER_BIN",
                "/var/lib/overdeck/subrouter/current/bin/subrouter",
            )
        )
        self._state_dir = state_dir or Path(
            os.environ.get(
                "OVERDECK_SUBROUTER_STATE_DIR",
                "/var/lib/overdeck/subrouter/state",
            )
        )
        self._command_runner = command_runner or subprocess.run

    def add(
        self,
        alias: str,
        *,
        login_hint: str | None = None,
        slug: str | None = None,
    ) -> Iterator[LifecycleEvent]:
        # A normal provider Add is not a migration command. Keeping this native
        # prevents an unbound account from entering authority custody by accident.
        yield from self._native.add(alias, login_hint=login_hint, slug=slug)

    def reauthenticate(self, account: Account) -> Iterator[LifecycleEvent]:
        binding = account.authority_binding
        if binding is None or binding.mode == AuthorityMode.NATIVE:
            yield from self._native.reauthenticate(account)
            return
        argv = [
            "deck-sudo",
            "-u",
            "overdeck-subrouter",
            "--",
            str(self._binary),
            "authority-account",
            "repair",
            "--state-dir",
            str(self._state_dir),
            "--provider",
            account.tool,
            "--account-id",
            account.slug,
        ]
        if account.tool == "codex":
            argv.append("--device-auth")
        yield LifecycleEvent(kind="started", account=account)
        try:
            completed = self._command_runner(argv, check=False)
        except Exception as exc:
            yield LifecycleEvent(
                kind="failure",
                account=account,
                error=RuntimeError("Gateway repair failed"),
                message="Gateway repair failed",
            )
            return
        if getattr(completed, "returncode", 1) != 0:
            yield LifecycleEvent(
                kind="failure",
                account=account,
                error=RuntimeError("Gateway repair failed"),
                message="Gateway repair failed",
            )
            return
        yield LifecycleEvent(kind="success", account=account, message="Gateway repair complete")

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


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=AuthorityLifecycleAdapter(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
