"""Shared Hive configuration utilities.

Centralises reading of ~/.hive/configuration.json so that the runner
and every agent template share one implementation instead of copy-pasting
helper functions.
"""

import json
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

DEFAULT_MAX_TOKENS = 8192

# ---------------------------------------------------------------------------
# Hive home directory structure
# ---------------------------------------------------------------------------

HIVE_HOME = Path.home() / ".hive"
QUEENS_DIR = HIVE_HOME / "agents" / "queens"
COLONIES_DIR = HIVE_HOME / "colonies"
MEMORIES_DIR = HIVE_HOME / "memories"


def queen_dir(queen_name: str = "default") -> Path:
    """Return the storage directory for a named queen agent."""
    return QUEENS_DIR / queen_name


def colony_dir(colony_name: str) -> Path:
    """Return the directory for a named colony."""
    return COLONIES_DIR / colony_name


def memory_dir(scope: str, name: str | None = None) -> Path:
    """Return memory dir for a scope.

    Examples::

        memory_dir("global")                  -> ~/.hive/memories/global
        memory_dir("colonies", "my_agent")    -> ~/.hive/memories/colonies/my_agent
        memory_dir("agents/queens", "default")-> ~/.hive/memories/agents/queens/default
        memory_dir("agents", "worker_name")   -> ~/.hive/memories/agents/worker_name
    """
    base = MEMORIES_DIR / scope
    return base / name if name else base


# ---------------------------------------------------------------------------
# Low-level config file access
# ---------------------------------------------------------------------------

HIVE_CONFIG_FILE = HIVE_HOME / "configuration.json"

# Hive LLM router endpoint (Anthropic-compatible).
# litellm's Anthropic handler appends /v1/messages, so this is just the base host.
HIVE_LLM_ENDPOINT = "https://api.adenhq.com"
logger = logging.getLogger(__name__)


def get_hive_config() -> dict[str, Any]:
    """Load hive configuration from ~/.hive/configuration.json."""
    if not HIVE_CONFIG_FILE.exists():
        return {}
    try:
        with open(HIVE_CONFIG_FILE, encoding="utf-8-sig") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        logger.warning(
            "Failed to load Hive config %s: %s",
            HIVE_CONFIG_FILE,
            e,
        )
        return {}


# ---------------------------------------------------------------------------
# Credential store helpers (for BYOK keys)
# ---------------------------------------------------------------------------

# Provider name → credential store ID mapping
_PROVIDER_CRED_MAP: dict[str, str] = {
    "anthropic": "anthropic",
    "openai": "openai",
    "gemini": "gemini",
    "google": "gemini",
    "minimax": "minimax",
    "groq": "groq",
    "cerebras": "cerebras",
    "openrouter": "openrouter",
    "mistral": "mistral",
    "together": "together",
    "together_ai": "together",
    "deepseek": "deepseek",
    "kimi": "kimi",
    "hive": "hive",
}


def _get_api_key_from_credential_store(provider: str) -> str | None:
    """Look up a BYOK API key from the encrypted credential store.

    Returns None if no key is found or the credential store is unavailable.
    """
    if not os.environ.get("HIVE_CREDENTIAL_KEY"):
        return None
    cred_id = _PROVIDER_CRED_MAP.get(provider.lower())
    if not cred_id:
        return None
    try:
        from framework.credentials import CredentialStore

        store = CredentialStore.with_encrypted_storage()
        return store.get(cred_id)
    except Exception:
        return None


# ---------------------------------------------------------------------------
# Derived helpers
# ---------------------------------------------------------------------------


def get_preferred_model() -> str:
    """Return the user's preferred LLM model string (e.g. 'anthropic/claude-sonnet-4-20250514')."""
    llm = get_hive_config().get("llm", {})
    if llm.get("provider") and llm.get("model"):
        provider = str(llm["provider"])
        model = str(llm["model"]).strip()
        # OpenRouter quickstart stores raw model IDs; tolerate pasted "openrouter/<id>" too.
        if provider.lower() == "openrouter" and model.lower().startswith("openrouter/"):
            model = model[len("openrouter/") :]
        if model:
            return f"{provider}/{model}"
    return "anthropic/claude-sonnet-4-20250514"


def get_preferred_worker_model() -> str | None:
    """Return the user's preferred worker LLM model, or None if not configured.

    Reads from the ``worker_llm`` section of ~/.hive/configuration.json.
    Returns None when no worker-specific model is set, so callers can
    fall back to the default (queen) model via ``get_preferred_model()``.
    """
    worker_llm = get_hive_config().get("worker_llm", {})
    if worker_llm.get("provider") and worker_llm.get("model"):
        provider = str(worker_llm["provider"])
        model = str(worker_llm["model"]).strip()
        if provider.lower() == "openrouter" and model.lower().startswith("openrouter/"):
            model = model[len("openrouter/") :]
        if model:
            return f"{provider}/{model}"
    return None


def get_vision_fallback_model() -> str | None:
    """Return the configured vision-fallback model, or None if not configured.

    Reads from the ``vision_fallback`` section of ~/.hive/configuration.json.
    Used by the agent-loop hook that captions tool-result images when the
    main agent's model cannot accept image content (text-only LLMs).

    When this returns None the fallback chain skips the configured-subagent
    stage and proceeds straight to the generic caption rotation
    (``_describe_images_as_text``).
    """
    vision = get_hive_config().get("vision_fallback", {})
    if vision.get("provider") and vision.get("model"):
        provider = str(vision["provider"])
        model = str(vision["model"]).strip()
        if provider.lower() == "openrouter" and model.lower().startswith("openrouter/"):
            model = model[len("openrouter/") :]
        if model:
            return f"{provider}/{model}"
    return None


def get_vision_fallback_api_key() -> str | None:
    """Return the API key for the vision-fallback model.

    Resolution order: ``vision_fallback.api_key_env_var`` from the env,
    then the default ``get_api_key()``. No subscription-token branches —
    vision fallback is intended for hosted vision models (Anthropic,
    OpenAI, Google), not for the subscription-bearer providers.
    """
    vision = get_hive_config().get("vision_fallback", {})
    if not vision:
        return get_api_key()
    api_key_env_var = vision.get("api_key_env_var")
    if api_key_env_var:
        return os.environ.get(api_key_env_var)
    return get_api_key()


def get_vision_fallback_api_base() -> str | None:
    """Return the api_base for the vision-fallback model, or None."""
    vision = get_hive_config().get("vision_fallback", {})
    if not vision:
        return None
    if vision.get("api_base"):
        return vision["api_base"]
    if str(vision.get("provider", "")).lower() == "openrouter":
        return OPENROUTER_API_BASE
    return None


def get_worker_api_key() -> str | None:
    """Return the API key for the worker LLM, falling back to the default key."""
    worker_llm = get_hive_config().get("worker_llm", {})
    if not worker_llm:
        return get_api_key()

    # Worker-specific subscription / env var
    if worker_llm.get("use_claude_code_subscription"):
        try:
            from framework.loader.agent_loader import get_claude_code_token

            token = get_claude_code_token()
            if token:
                return token
        except ImportError:
            pass

    if worker_llm.get("use_codex_subscription"):
        try:
            from framework.loader.agent_loader import get_codex_token

            token = get_codex_token()
            if token:
                return token
        except ImportError:
            pass

    if worker_llm.get("use_kimi_code_subscription"):
        try:
            from framework.loader.agent_loader import get_kimi_code_token

            token = get_kimi_code_token()
            if token:
                return token
        except ImportError:
            pass

    if worker_llm.get("use_antigravity_subscription"):
        try:
            from framework.loader.agent_loader import get_antigravity_token

            token = get_antigravity_token()
            if token:
                return token
        except ImportError:
            pass

    api_key_env_var = worker_llm.get("api_key_env_var")
    if api_key_env_var:
        return os.environ.get(api_key_env_var)

    # Fall back to default key
    return get_api_key()


def get_worker_api_base() -> str | None:
    """Return the api_base for the worker LLM, falling back to the default."""
    worker_llm = get_hive_config().get("worker_llm", {})
    if not worker_llm:
        return get_api_base()

    if worker_llm.get("use_codex_subscription"):
        return "https://chatgpt.com/backend-api/codex"
    if worker_llm.get("use_kimi_code_subscription"):
        return "https://api.kimi.com/coding"
    if worker_llm.get("use_antigravity_subscription"):
        # Antigravity uses AntigravityProvider directly — no api_base needed.
        return None
    if worker_llm.get("api_base"):
        return worker_llm["api_base"]
    if str(worker_llm.get("provider", "")).lower() == "openrouter":
        return OPENROUTER_API_BASE
    return None


def get_worker_llm_extra_kwargs() -> dict[str, Any]:
    """Return extra kwargs for the worker LLM provider."""
    worker_llm = get_hive_config().get("worker_llm", {})
    if not worker_llm:
        return get_llm_extra_kwargs()

    if worker_llm.get("use_claude_code_subscription"):
        api_key = get_worker_api_key()
        if api_key:
            return {
                "extra_headers": {"authorization": f"Bearer {api_key}"},
            }
    if worker_llm.get("use_codex_subscription"):
        api_key = get_worker_api_key()
        if api_key:
            headers: dict[str, str] = {
                "Authorization": f"Bearer {api_key}",
                "User-Agent": "CodexBar",
            }
            try:
                from framework.loader.agent_loader import get_codex_account_id

                account_id = get_codex_account_id()
                if account_id:
                    headers["ChatGPT-Account-Id"] = account_id
            except ImportError:
                pass
            return {
                "extra_headers": headers,
                "store": False,
                "allowed_openai_params": ["store"],
            }
    if worker_llm.get("provider") == "ollama":
        return {"num_ctx": worker_llm.get("num_ctx", 16384)}
    return {}


def get_worker_max_tokens() -> int:
    """Return max_tokens for the worker LLM, falling back to default."""
    worker_llm = get_hive_config().get("worker_llm", {})
    if worker_llm and "max_tokens" in worker_llm:
        return worker_llm["max_tokens"]
    return get_max_tokens()


def get_worker_max_context_tokens() -> int:
    """Return max_context_tokens for the worker LLM, falling back to default."""
    worker_llm = get_hive_config().get("worker_llm", {})
    if worker_llm and "max_context_tokens" in worker_llm:
        return worker_llm["max_context_tokens"]
    return get_max_context_tokens()


def get_max_tokens() -> int:
    """Return the configured max_tokens, falling back to DEFAULT_MAX_TOKENS."""
    return get_hive_config().get("llm", {}).get("max_tokens", DEFAULT_MAX_TOKENS)


DEFAULT_MAX_CONTEXT_TOKENS = 32_000
OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"


def get_max_context_tokens() -> int:
    """Return the configured max_context_tokens, falling back to DEFAULT_MAX_CONTEXT_TOKENS."""
    return get_hive_config().get("llm", {}).get("max_context_tokens", DEFAULT_MAX_CONTEXT_TOKENS)


def get_api_keys() -> list[str] | None:
    """Return a list of API keys if ``api_keys`` is configured, else ``None``.

    This supports key-pool rotation: configure multiple keys in
    ``~/.hive/configuration.json`` under ``llm.api_keys`` and the
    :class:`~framework.llm.key_pool.KeyPool` will rotate through them.
    """
    llm = get_hive_config().get("llm", {})
    keys = llm.get("api_keys")
    if keys and isinstance(keys, list) and len(keys) > 0:
        return [k for k in keys if k]  # filter empties
    return None


def get_api_key() -> str | None:
    """Return the API key, supporting env var, Claude Code subscription, Codex, and ZAI Code.

    Priority:
    0. Explicit key pool (``api_keys`` list) -- returns first key for
       single-key callers; full pool available via :func:`get_api_keys`.
    1. Claude Code subscription (``use_claude_code_subscription: true``)
       reads the OAuth token from ``~/.claude/.credentials.json``.
    2. Codex subscription (``use_codex_subscription: true``)
       reads the OAuth token from macOS Keychain or ``~/.codex/auth.json``.
    3. Environment variable named in ``api_key_env_var``.
    """
    # If an explicit key pool is configured, use the first key.
    pool_keys = get_api_keys()
    if pool_keys:
        return pool_keys[0]

    llm = get_hive_config().get("llm", {})

    # Claude Code subscription: read OAuth token directly
    if llm.get("use_claude_code_subscription"):
        try:
            from framework.loader.agent_loader import get_claude_code_token

            token = get_claude_code_token()
            if token:
                return token
        except ImportError:
            pass

    # Codex subscription: read OAuth token from Keychain / auth.json
    if llm.get("use_codex_subscription"):
        try:
            from framework.loader.agent_loader import get_codex_token

            token = get_codex_token()
            if token:
                return token
        except ImportError:
            pass

    # Kimi Code subscription: read API key from ~/.kimi/config.toml
    if llm.get("use_kimi_code_subscription"):
        try:
            from framework.loader.agent_loader import get_kimi_code_token

            token = get_kimi_code_token()
            if token:
                return token
        except ImportError:
            pass

    # Antigravity subscription: read OAuth token from accounts JSON
    if llm.get("use_antigravity_subscription"):
        try:
            from framework.loader.agent_loader import get_antigravity_token

            token = get_antigravity_token()
            if token:
                return token
        except ImportError:
            pass

    # Standard env-var path (covers ZAI Code and all API-key providers)
    api_key_env_var = llm.get("api_key_env_var")
    if api_key_env_var:
        key = os.environ.get(api_key_env_var)
        if key:
            return key

    # Credential store fallback — BYOK keys stored via the UI
    return _get_api_key_from_credential_store(llm.get("provider", ""))


# OAuth credentials for Antigravity are fetched from the opencode-antigravity-auth project.
# This project reverse-engineered and published the public OAuth credentials
# for Google's Antigravity/Cloud Code Assist API.
# Source: https://github.com/NoeFabris/opencode-antigravity-auth
_ANTIGRAVITY_CREDENTIALS_URL = (
    "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/dev/src/constants.ts"
)
_antigravity_credentials_cache: tuple[str | None, str | None] = (None, None)


def _fetch_antigravity_credentials() -> tuple[str | None, str | None]:
    """Fetch OAuth client ID and secret from the public npm package source on GitHub."""
    global _antigravity_credentials_cache
    if _antigravity_credentials_cache[0] and _antigravity_credentials_cache[1]:
        return _antigravity_credentials_cache

    import re
    import urllib.request

    try:
        req = urllib.request.Request(_ANTIGRAVITY_CREDENTIALS_URL, headers={"User-Agent": "Hive/1.0"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            content = resp.read().decode("utf-8")
            id_match = re.search(r'ANTIGRAVITY_CLIENT_ID\s*=\s*"([^"]+)"', content)
            secret_match = re.search(r'ANTIGRAVITY_CLIENT_SECRET\s*=\s*"([^"]+)"', content)
            client_id = id_match.group(1) if id_match else None
            client_secret = secret_match.group(1) if secret_match else None
            if client_id and client_secret:
                _antigravity_credentials_cache = (client_id, client_secret)
            return client_id, client_secret
    except Exception as e:
        logger.debug("Failed to fetch Antigravity credentials from public source: %s", e)
    return None, None


def get_antigravity_client_id() -> str:
    """Return the Antigravity OAuth application client ID.

    Checked in order:
    1. ``ANTIGRAVITY_CLIENT_ID`` environment variable
    2. ``llm.antigravity_client_id`` in ~/.hive/configuration.json
    3. Fetch from public source (opencode-antigravity-auth project on GitHub)
    """
    env = os.environ.get("ANTIGRAVITY_CLIENT_ID")
    if env:
        return env
    cfg_val = get_hive_config().get("llm", {}).get("antigravity_client_id")
    if cfg_val:
        return cfg_val
    # Fetch from public source
    client_id, _ = _fetch_antigravity_credentials()
    if client_id:
        return client_id
    raise RuntimeError("Could not obtain Antigravity OAuth client ID")


def get_antigravity_client_secret() -> str | None:
    """Return the Antigravity OAuth client secret.

    Checked in order:
    1. ``ANTIGRAVITY_CLIENT_SECRET`` environment variable
    2. ``llm.antigravity_client_secret`` in ~/.hive/configuration.json
    3. Fetch from public source (opencode-antigravity-auth project on GitHub)

    Returns None when not found — token refresh will be skipped and
    the caller must use whatever access token is already available.
    """
    env = os.environ.get("ANTIGRAVITY_CLIENT_SECRET")
    if env:
        return env
    cfg_val = get_hive_config().get("llm", {}).get("antigravity_client_secret") or None
    if cfg_val:
        return cfg_val
    # Fetch from public source
    _, secret = _fetch_antigravity_credentials()
    return secret


def get_gcu_enabled() -> bool:
    """Return whether GCU (browser automation) is enabled in user config."""
    return get_hive_config().get("gcu_enabled", True)


def get_gcu_viewport_scale() -> float:
    """Return GCU viewport scale factor (0.1-1.0), default 0.8."""
    scale = get_hive_config().get("gcu_viewport_scale", 0.8)
    if isinstance(scale, (int, float)) and 0.1 <= scale <= 1.0:
        return float(scale)
    return 0.8


def get_api_base() -> str | None:
    """Return the api_base URL for OpenAI-compatible endpoints, if configured."""
    llm = get_hive_config().get("llm", {})
    if llm.get("use_codex_subscription"):
        # Codex subscription routes through the ChatGPT backend, not api.openai.com.
        return "https://chatgpt.com/backend-api/codex"
    if llm.get("use_kimi_code_subscription"):
        # Kimi Code uses an Anthropic-compatible endpoint (no /v1 suffix).
        return "https://api.kimi.com/coding"
    if llm.get("use_antigravity_subscription"):
        # Antigravity uses AntigravityProvider directly — no api_base needed.
        return None
    if llm.get("api_base"):
        return llm["api_base"]
    if str(llm.get("provider", "")).lower() == "openrouter":
        return OPENROUTER_API_BASE
    return None


def get_llm_extra_kwargs() -> dict[str, Any]:
    """Return extra kwargs for LiteLLMProvider (e.g. OAuth headers).

    When ``use_claude_code_subscription`` is enabled, returns
    ``extra_headers`` with the OAuth Bearer token so that litellm's
    built-in Anthropic OAuth handler adds the required beta headers.

    When ``use_codex_subscription`` is enabled, returns
    ``extra_headers`` with the Bearer token, ``ChatGPT-Account-Id``,
    and ``store=False`` (required by the ChatGPT backend).
    """
    llm = get_hive_config().get("llm", {})
    if llm.get("use_claude_code_subscription"):
        api_key = get_api_key()
        if api_key:
            return {
                "extra_headers": {"authorization": f"Bearer {api_key}"},
            }
    if llm.get("use_codex_subscription"):
        api_key = get_api_key()
        if api_key:
            headers: dict[str, str] = {
                "Authorization": f"Bearer {api_key}",
                "User-Agent": "CodexBar",
            }
            try:
                from framework.loader.agent_loader import get_codex_account_id

                account_id = get_codex_account_id()
                if account_id:
                    headers["ChatGPT-Account-Id"] = account_id
            except ImportError:
                pass
            return {
                "extra_headers": headers,
                "store": False,
                "allowed_openai_params": ["store"],
            }
    if llm.get("provider") == "ollama":
        # Pass num_ctx to Ollama so it doesn't silently truncate the ~9.5k Queen prompt.
        # Ollama's default num_ctx is only 2048. We set it to 16384 here so LiteLLM
        # passes it through as a provider-specific option.
        return {"num_ctx": llm.get("num_ctx", 16384)}
    return {}


# ---------------------------------------------------------------------------
# RuntimeConfig – shared across agent templates
# ---------------------------------------------------------------------------


@dataclass
class RuntimeConfig:
    """Agent runtime configuration loaded from ~/.hive/configuration.json."""

    model: str = field(default_factory=get_preferred_model)
    temperature: float = 0.7
    max_tokens: int = field(default_factory=get_max_tokens)
    max_context_tokens: int = field(default_factory=get_max_context_tokens)
    api_key: str | None = field(default_factory=get_api_key)
    api_base: str | None = field(default_factory=get_api_base)
    extra_kwargs: dict[str, Any] = field(default_factory=get_llm_extra_kwargs)
