# platform/deskclaw/adapters/loader.py
# ═══════════════════════════════════════════════════════════════
# Dynamic Adapter Loader — Runtime Discovery & Registration
#
# Scans directories for adapter definitions and registers them:
#   1. YAML files → UniversalHTTPAdapter instances
#   2. Python files → importlib dynamic import (triggers @register_adapter)
#
# Eliminates the need to modify __init__.py for every new adapter.
# ═══════════════════════════════════════════════════════════════

from __future__ import annotations

import importlib.util
import json
import logging
import os
import re
import sys
from pathlib import Path
from typing import Any

from deskclaw.adapters.http_adapter import HTTPAdapterConfig, UniversalHTTPAdapter
from deskclaw.adapters.openclaw_adapter import OpenClawAdapter, OpenClawConfig
from deskclaw.adapters.protocol import AgentCapability
from deskclaw.adapters.registry import register_adapter_instance

logger = logging.getLogger("deskclaw.adapters.loader")

# ── Constants ──

_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")

_CAPABILITY_MAP: dict[str, AgentCapability] = {
    "text_generation": AgentCapability.TEXT_GENERATION,
    "code_execution": AgentCapability.CODE_EXECUTION,
    "tool_use": AgentCapability.TOOL_USE,
    "multimodal": AgentCapability.MULTIMODAL,
    "desktop_control": AgentCapability.DESKTOP_CONTROL,
    "multi_turn": AgentCapability.MULTI_TURN,
}


# ── YAML Parsing ──


def _parse_yaml_text(text: str) -> dict[str, Any]:
    """Parse YAML (or JSON fallback) text into a dict."""
    try:
        import yaml  # type: ignore[import-untyped]

        data = yaml.safe_load(text)
    except ModuleNotFoundError:
        try:
            data = json.loads(text)
        except json.JSONDecodeError as exc:
            raise ValueError(
                f"pyyaml not installed and content is not valid JSON: {exc}"
            ) from exc

    if not isinstance(data, dict):
        raise ValueError(f"Expected top-level dict, got {type(data).__name__}")
    return data


def _resolve_env_vars(value: str) -> str:
    """Replace ``${VAR}`` tokens with environment variable values."""

    def _replacer(match: re.Match[str]) -> str:
        var = match.group(1)
        env_val = os.environ.get(var)
        if env_val is None:
            logger.warning(
                "Environment variable '${%s}' is not set — using empty string.", var
            )
            return ""
        return env_val

    return _ENV_VAR_PATTERN.sub(_replacer, value)


def _parse_capabilities(caps: list[str]) -> set[AgentCapability]:
    """Convert string capability names to AgentCapability enum values."""
    result: set[AgentCapability] = set()
    for cap_str in caps:
        cap = _CAPABILITY_MAP.get(cap_str.lower().strip())
        if cap is not None:
            result.add(cap)
        else:
            logger.warning("Unknown capability '%s' — skipping.", cap_str)
    return result


# ── Config Builder ──


def config_from_yaml(data: dict[str, Any]) -> HTTPAdapterConfig:
    """
    Build an HTTPAdapterConfig from a parsed YAML adapter definition.

    Expected structure::

        adapter:
          name: "my-bot"
          type: "http"
          url: "http://..."
          protocol: "openai"
          auth:
            type: "bearer"
            token: "${API_KEY}"
          capabilities: ["text_generation", "tool_use"]
          ...
    """
    adapter_block = data.get("adapter", data)

    name = adapter_block.get("name", "unnamed-http-adapter")
    url = _resolve_env_vars(str(adapter_block.get("url", "")))
    protocol = adapter_block.get("protocol", "openai")

    # Auth
    auth_block = adapter_block.get("auth", {})
    auth_type = auth_block.get("type", "none")
    auth_token = _resolve_env_vars(str(auth_block.get("token", "")))
    auth_header = auth_block.get("header", "Authorization")

    # Capabilities
    raw_caps = adapter_block.get("capabilities", ["text_generation"])
    capabilities = _parse_capabilities(raw_caps)

    # Timeouts
    timeouts = adapter_block.get("timeouts", {})
    connect_timeout = float(timeouts.get("connect_s", 10.0))
    read_timeout = float(timeouts.get("read_s", 300.0))

    # Custom mapping
    request_mapping = adapter_block.get("request_mapping", {})
    response_mapping = adapter_block.get("response_mapping", {})

    # Metadata
    context_window = int(adapter_block.get("context_window", 128_000))
    cost = adapter_block.get("cost_per_1k", {})
    cost_input = float(cost.get("input", 0.0))
    cost_output = float(cost.get("output", 0.0))

    # Extra headers
    extra_headers = {
        str(k): _resolve_env_vars(str(v))
        for k, v in adapter_block.get("extra_headers", {}).items()
    }

    return HTTPAdapterConfig(
        name=name,
        url=url,
        protocol=protocol,
        capabilities=capabilities,
        auth_type=auth_type,
        auth_token=auth_token,
        auth_header=auth_header,
        request_mapping=request_mapping,
        response_mapping=response_mapping,
        connect_timeout_s=connect_timeout,
        read_timeout_s=read_timeout,
        context_window=context_window,
        cost_per_1k_input=cost_input,
        cost_per_1k_output=cost_output,
        extra_headers=extra_headers,
    )


def openclaw_config_from_yaml(data: dict[str, Any]) -> tuple[str, OpenClawConfig]:
    """
    Build an OpenClawConfig from a parsed YAML adapter definition.

    Expected structure::

        adapter:
          name: "my-openclaw"
          type: "openclaw"
          target: "ws://localhost:18789"
          token: "${OPENCLAW_TOKEN}"
          agent_id: "default"
          thinking: "high"
    """
    adapter_block = data.get("adapter", data)
    name = adapter_block.get("name", "openclaw")
    target = _resolve_env_vars(str(adapter_block.get("target", "ws://localhost:18789")))
    token = _resolve_env_vars(str(adapter_block.get("token", "")))

    auth_block = adapter_block.get("auth", {})
    if not token and auth_block.get("token"):
        token = _resolve_env_vars(str(auth_block["token"]))

    agent_id = adapter_block.get("agent_id", "")
    thinking = adapter_block.get("thinking", "")
    session_key = adapter_block.get("session_key", "")

    scopes = adapter_block.get("scopes", [
        "operator.read", "operator.write", "operator.admin",
    ])

    timeouts = adapter_block.get("timeouts", {})
    request_timeout = float(timeouts.get("request_s", 300.0))
    handshake_timeout = float(timeouts.get("handshake_s", 10.0))

    context_window = int(adapter_block.get("context_window", 128_000))
    cost = adapter_block.get("cost_per_1k", {})

    config = OpenClawConfig(
        target=target,
        token=token,
        agent_id=agent_id,
        thinking=thinking,
        session_key=session_key,
        scopes=scopes,
        request_timeout_s=request_timeout,
        handshake_timeout_s=handshake_timeout,
        context_window=context_window,
        cost_per_1k_input=float(cost.get("input", 0.0)),
        cost_per_1k_output=float(cost.get("output", 0.0)),
    )
    return name, config


def config_from_cli(
    *,
    url: str,
    name: str | None = None,
    protocol: str = "openai",
    capabilities: list[str] | None = None,
    headers: list[str] | None = None,
    auth_token: str | None = None,
) -> HTTPAdapterConfig:
    """
    Build an HTTPAdapterConfig from CLI arguments.

    Args:
        url:          Target API URL.
        name:         Adapter name (auto-generated from URL if not given).
        protocol:     API protocol ("openai" | "anthropic" | "custom").
        capabilities: List of capability strings.
        headers:      List of "Key: Value" header strings.
        auth_token:   Bearer token (shorthand for --header "Authorization: Bearer ...").
    """
    adapter_name: str
    if name:
        adapter_name = name
    else:
        name_match = re.search(r"https?://([^/:]+)", url)
        adapter_name = (
            (name_match.group(1) or "http-bot").replace(".", "-")
            if name_match
            else "http-bot"
        )

    caps = _parse_capabilities(capabilities or ["text_generation"])

    extra: dict[str, str] = {}
    for hdr in headers or []:
        if ":" in hdr:
            key, value = hdr.split(":", 1)
            extra[key.strip()] = value.strip()

    auth_type = "bearer" if auth_token else "none"
    token = auth_token or ""

    return HTTPAdapterConfig(
        name=adapter_name,
        url=url,
        protocol=protocol,
        capabilities=caps,
        auth_type=auth_type,
        auth_token=token,
        extra_headers=extra,
    )


# ── Dynamic Loader ──


class AdapterLoader:
    """
    Runtime adapter discovery and registration.

    Scans a directory for:
    1. ``*.yaml`` / ``*.yml`` files → parsed into HTTPAdapterConfig → UniversalHTTPAdapter
    2. ``*.py`` files → dynamically imported (triggers @register_adapter decorators)

    Already-registered adapters are skipped (no duplicate error).
    """

    @staticmethod
    def discover_and_register(
        adapters_dir: str | Path = "plugins/adapters",
    ) -> list[str]:
        """
        Scan *adapters_dir* for adapter definitions and register them.

        Returns:
            List of adapter names that were newly registered.
        """
        adapters_dir = Path(adapters_dir).resolve()
        registered: list[str] = []

        if not adapters_dir.is_dir():
            logger.warning(
                "Adapter discovery dir does not exist: %s — no dynamic adapters loaded.",
                adapters_dir,
            )
            return registered

        yaml_files = sorted(
            list(adapters_dir.glob("*.yaml")) + list(adapters_dir.glob("*.yml"))
        )
        logger.info(
            "Scanning %s: found %d YAML file(s)", adapters_dir, len(yaml_files)
        )

        # Phase 1: YAML adapters
        for yaml_path in yaml_files:
            try:
                text = yaml_path.read_text(encoding="utf-8")
                data = _parse_yaml_text(text)
                adapter_block = data.get("adapter", data)
                adapter_type = adapter_block.get("type", "http")

                if adapter_type == "openclaw":
                    name, oc_config = openclaw_config_from_yaml(data)
                    adapter = OpenClawAdapter.from_config(oc_config)
                else:
                    config = config_from_yaml(data)
                    name = config.name
                    adapter = UniversalHTTPAdapter.from_config(config)

                if register_adapter_instance(name, adapter):
                    registered.append(name)
                    logger.info(
                        "Registered %s adapter '%s' from %s",
                        adapter_type,
                        name,
                        yaml_path,
                    )
                else:
                    logger.debug(
                        "Adapter '%s' already registered — skipping %s",
                        name,
                        yaml_path,
                    )
            except Exception as exc:
                logger.error(
                    "Failed to load adapter from %s: %s", yaml_path, exc, exc_info=True
                )

        # Phase 2: Python adapters
        for py_path in sorted(adapters_dir.glob("*.py")):
            if py_path.name.startswith("_"):
                continue
            try:
                _import_module_from_path(py_path)
                logger.info("Imported Python adapter module: %s", py_path)
            except Exception as exc:
                logger.warning("Failed to import adapter from %s: %s", py_path, exc)

        return registered


def _import_module_from_path(path: Path) -> None:
    """Dynamically import a Python module from a file path."""
    module_name = f"deskclaw_adapter_plugin_{path.stem}"
    if module_name in sys.modules:
        return  # Already imported

    spec = importlib.util.spec_from_file_location(module_name, str(path))
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot create module spec for {path}")

    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
