"""Gateway configuration management.

Handles per-agent config resolution: registry lookup, binary validation,
environment building, and auth method selection.

Bridges the three config layers:
- AgentRegistration (static, per-agent-type) → how to launch
- AgentConfig (per-session, from backend) → what constraints to apply
- Runtime discovery (ACP responses) → what the agent actually supports

Reference: MDS-88
"""

from __future__ import annotations

import os
import shutil
import logging
from typing import Any, Dict, List, Optional

from .models import (
    AgentCapabilities,
    AgentConfig,
    AgentDescriptor,
    AgentRegistration,
    CredentialSource,
    ModelInfo,
    PromptCapabilities,
    AGENT_REGISTRY,
)

logger = logging.getLogger(__name__)


class AgentNotFoundError(Exception):
    """Raised when an agent_id is not in the registry."""
    pass


class AgentBinaryNotFoundError(Exception):
    """Raised when the agent binary is not installed on the host."""
    pass


def get_registration(agent_id: str) -> AgentRegistration:
    """Look up agent registration from the registry.

    Raises AgentNotFoundError if not registered.
    """
    reg = AGENT_REGISTRY.get(agent_id)
    if not reg:
        available = ", ".join(AGENT_REGISTRY.keys())
        raise AgentNotFoundError(
            f"Agent '{agent_id}' not registered. Available: {available}"
        )
    return reg


def validate_binary(reg: AgentRegistration) -> str:
    """Check that the agent binary is installed. Returns full path.

    Raises AgentBinaryNotFoundError if not found in PATH.
    """
    path = shutil.which(reg.executable)
    if not path:
        raise AgentBinaryNotFoundError(
            f"Binary '{reg.executable}' for agent '{reg.agent_id}' not found in PATH"
        )
    return path


def build_env(reg: AgentRegistration, session_config: AgentConfig) -> Dict[str, str]:
    """Build the environment dict for spawning the agent subprocess.

    Applies per-agent credential sandboxing:
    - If pass_full_env: pass entire os.environ (Goose needs this)
    - Otherwise: copy os.environ and strip keys from strip_env_keys
    - Then inject session credentials from AgentConfig
    """
    if reg.pass_full_env:
        env = dict(os.environ)
    else:
        env = dict(os.environ)
        for key in reg.strip_env_keys:
            env.pop(key, None)

    # Inject session-level credentials
    for key, secret in session_config.credentials.items():
        env[key] = secret.get_secret_value()

    return env


def build_launch_command(
    reg: AgentRegistration,
    session_config: AgentConfig,
) -> List[str]:
    """Build the full command to spawn the agent.

    Starts with the registration's executable + launch_args,
    then appends any session-level overrides (model, etc.)
    that the agent supports via CLI flags.
    """
    cmd = [reg.executable] + list(reg.launch_args)

    # Model override — some agents accept --model flag
    # Others use env vars (GOOSE_MODEL) or ACP set_session_model
    # For now, model is handled via ACP set_session_model after session creation
    # CLI-level model injection can be added per-agent as needed

    return cmd


def resolve_auth_method(
    reg: AgentRegistration,
    acp_auth_methods: List[Dict[str, Any]],
) -> Optional[str]:
    """Select the best auth method from ACP init response.

    Uses the registration's preferred auth_method_id if available
    in the agent's advertised methods. Falls back to first oauth/login
    method, then first available.

    Returns None if no auth methods or auth not required.
    """
    if not acp_auth_methods:
        return None

    if not reg.auth_required and reg.credential_source != CredentialSource.ACP_AUTH:
        return None

    def _get_id(am: Any) -> str:
        if isinstance(am, dict):
            return str(am.get("id", ""))
        return str(getattr(am, "id", ""))

    # Try preferred method
    if reg.auth_method_id:
        for am in acp_auth_methods:
            if _get_id(am) == reg.auth_method_id:
                return reg.auth_method_id

    # Fall back to oauth/login pattern
    for am in acp_auth_methods:
        am_id = _get_id(am)
        if "oauth" in am_id or "login" in am_id:
            return am_id

    # Last resort: first available
    return _get_id(acp_auth_methods[0])


def list_available_agents() -> Dict[str, Dict[str, Any]]:
    """List all registered agents with their install status.

    Returns dict of agent_id → {registration, installed, binary_path}.
    """
    result = {}
    for agent_id, reg in AGENT_REGISTRY.items():
        path = shutil.which(reg.executable)
        result[agent_id] = {
            "registration": reg,
            "installed": path is not None,
            "binary_path": path,
        }
    return result


# ---------------------------------------------------------------------------
# Dynamic capabilities — populated from ACP runtime responses
# ---------------------------------------------------------------------------

def build_descriptor_from_init(
    reg: AgentRegistration,
    init_result: Any,
) -> AgentDescriptor:
    """Build AgentDescriptor from ACP initialize response.

    Extracts agentInfo and agentCapabilities from the SDK's InitializeResponse.
    Falls back to registration defaults for missing fields.
    """
    descriptor = AgentDescriptor(
        agent_id=reg.agent_id,
        executable=reg.executable,
    )

    # agentInfo — some agents don't return this (Cursor, Goose)
    info = getattr(init_result, "agent_info", None)
    if info:
        descriptor.name = getattr(info, "name", "") or ""
        descriptor.version = getattr(info, "version", "") or ""

    # agentCapabilities
    caps = getattr(init_result, "agent_capabilities", None)
    if caps:
        pc = getattr(caps, "prompt_capabilities", None)
        descriptor.capabilities = AgentCapabilities(
            load_session=getattr(caps, "load_session", False) or False,
            prompt_capabilities=PromptCapabilities(
                image=getattr(pc, "image", False) if pc else False,
                audio=getattr(pc, "audio", False) if pc else False,
                embedded_context=getattr(pc, "embedded_context", False) if pc else False,
            ),
        )

    return descriptor


def update_descriptor_from_session(
    descriptor: AgentDescriptor,
    session_result: Any,
) -> AgentDescriptor:
    """Update AgentDescriptor with models from ACP session/new response.

    Extracts available models and current model from the session response.
    """
    models_info = getattr(session_result, "models", None)
    if not models_info:
        return descriptor

    available = getattr(models_info, "available_models", None) or []
    current_id = getattr(models_info, "current_model_id", None)

    model_list = []
    for m in available:
        model_id = getattr(m, "model_id", "") or ""
        name = getattr(m, "name", model_id) or model_id
        model_list.append(ModelInfo(
            id=model_id,
            name=name,
            default=(model_id == current_id),
        ))

    descriptor.available_models = model_list
    descriptor.current_model = current_id
    return descriptor
