"""API provider registry for kv.

Maps provider names to base URLs, auth patterns, and required secret keys.
Used by the kv agent daemon to inject credentials into HTTP requests
without exposing them to the AI agent.
"""


# Auth types:
#   "bearer"  → Authorization: Bearer <key>
#   "x-api-key" → x-api-key: <key>  (Anthropic's pattern)
#   "query"   → ?key=<key> appended to URL
#   "basic"   → Authorization: Basic base64(user:key)

PROVIDERS = {
    "openai": {
        "base_url": "https://api.openai.com",
        "secret_name": "OPENAI_API_KEY",
        "auth_type": "bearer",
        "default_headers": {
            "Content-Type": "application/json",
        },
    },
    "anthropic": {
        "base_url": "https://api.anthropic.com",
        "secret_name": "ANTHROPIC_API_KEY",
        "auth_type": "x-api-key",
        "default_headers": {
            "Content-Type": "application/json",
            "anthropic-version": "2023-06-01",
        },
    },
    "google": {
        "base_url": "https://generativelanguage.googleapis.com",
        "secret_name": "GOOGLE_API_KEY",
        "auth_type": "query",
        "auth_query_param": "key",
        "default_headers": {
            "Content-Type": "application/json",
        },
    },
    "github": {
        "base_url": "https://api.github.com",
        "secret_name": "GITHUB_TOKEN",
        "auth_type": "bearer",
        "default_headers": {
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
        },
    },
    "google-cloud": {
        "base_url": "https://www.googleapis.com",
        "secret_name": "GOOGLE_CLOUD_API_KEY",
        "auth_type": "query",
        "auth_query_param": "key",
        "default_headers": {
            "Content-Type": "application/json",
        },
    },
}


def load_custom_providers(project_root):
    """Load custom providers from .secrets/config.json and merge into registry.

    Called at daemon/MCP startup. Custom providers override built-ins if
    they share a name. Format in config.json:

    {
      "providers": {
        "stripe": {
          "base_url": "https://api.stripe.com",
          "secret_name": "STRIPE_SECRET_KEY",
          "auth_type": "bearer",
          "default_headers": {"Content-Type": "application/json"}
        }
      }
    }

    auth_type must be one of: bearer, x-api-key, query, basic, header.
    For "header": set "auth_header_name" (e.g., "X-Custom-Auth").
    For "query": set "auth_query_param" (e.g., "key").
    """
    try:
        from .config import load_config
        config = load_config(project_root)
        custom = config.get("providers", {})
        if not isinstance(custom, dict):
            return 0

        valid_auth = {"bearer", "x-api-key", "query", "basic", "header"}
        loaded = 0
        for name, prov in custom.items():
            if not isinstance(prov, dict):
                continue
            if "base_url" not in prov or "secret_name" not in prov:
                continue
            if prov.get("auth_type", "bearer") not in valid_auth:
                continue
            # Merge: custom overrides built-in
            PROVIDERS[name] = {
                "base_url": prov["base_url"],
                "secret_name": prov["secret_name"],
                "auth_type": prov.get("auth_type", "bearer"),
                "default_headers": prov.get("default_headers", {"Content-Type": "application/json"}),
                **{k: v for k, v in prov.items()
                   if k not in ("base_url", "secret_name", "auth_type", "default_headers")},
            }
            loaded += 1
        return loaded
    except Exception:
        return 0


def get_provider(name):
    """Get provider config by name. Returns None if unknown."""
    return PROVIDERS.get(name)


def list_providers():
    """List all registered provider names."""
    return sorted(PROVIDERS.keys())


def build_auth(provider_config, secret_value):
    """Build auth headers and query params for a provider.

    Returns (headers_dict, query_params_dict).
    """
    headers = dict(provider_config.get("default_headers", {}))
    query_params = {}

    auth_type = provider_config["auth_type"]

    if auth_type == "bearer":
        headers["Authorization"] = f"Bearer {secret_value}"
    elif auth_type == "x-api-key":
        headers["x-api-key"] = secret_value
    elif auth_type == "query":
        param_name = provider_config.get("auth_query_param", "key")
        query_params[param_name] = secret_value
    elif auth_type == "basic":
        import base64
        encoded = base64.b64encode(f":{secret_value}".encode()).decode()
        headers["Authorization"] = f"Basic {encoded}"
    elif auth_type == "header":
        header_name = provider_config.get("auth_header_name", "Authorization")
        headers[header_name] = secret_value

    return headers, query_params


def build_url(provider_config, path, query_params=None):
    """Build the full URL for an API call.

    Combines base_url + path + any query parameters.
    """
    url = provider_config["base_url"].rstrip("/") + "/" + path.lstrip("/")

    if query_params:
        from urllib.parse import urlencode
        separator = "&" if "?" in url else "?"
        url = url + separator + urlencode(query_params)

    return url


def _check_url_safety(url):
    """Block requests to private/local networks and non-HTTPS targets.

    Prevents SSRF: a malicious custom provider config can't exfiltrate
    secrets to internal services or localhost.

    Checks:
      1. Scheme must be HTTPS (credentials travel over TLS only)
      2. Hostname must not be localhost/loopback
      3. Resolved IP must not be in private ranges
      4. DNS rebinding defense: resolve before connecting, check the IP

    Returns (safe, reason).
    """
    import ipaddress
    import socket
    from urllib.parse import urlparse

    parsed = urlparse(url)

    # 1. HTTPS only (allow HTTP only for built-in providers via explicit bypass)
    if parsed.scheme != "https":
        return False, f"non-HTTPS URL blocked: {parsed.scheme}://"

    # 2. Hostname checks
    hostname = parsed.hostname or ""
    if not hostname:
        return False, "empty hostname"

    blocked_hosts = {"localhost", "localhost.", "0.0.0.0", "[::]", "[::1]"}
    if hostname.lower() in blocked_hosts:
        return False, f"localhost blocked: {hostname}"

    # 3. Resolve and check IP
    try:
        infos = socket.getaddrinfo(hostname, parsed.port or 443,
                                   socket.AF_UNSPEC, socket.SOCK_STREAM)
        for family, _, _, _, sockaddr in infos:
            ip_str = sockaddr[0]
            try:
                ip = ipaddress.ip_address(ip_str)
                if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
                    return False, f"private/reserved IP blocked: {ip_str} ({hostname})"
            except ValueError:
                continue
    except socket.gaierror:
        return False, f"DNS resolution failed: {hostname}"

    return True, ""


# Built-in provider base_urls that are known-safe (skip SSRF check)
_BUILTIN_URLS = {p["base_url"] for p in PROVIDERS.values()}


def make_api_call(provider_name, secret_value, path, method="POST",
                  body=None, extra_headers=None, secrets_for_redact=None,
                  timeout=120):
    """Make an authenticated API call through a provider.

    This is the shared HTTP logic used by both the agent daemon and the
    MCP server direct mode. The secret value is used for auth but never
    returned in the response.

    SSRF protection: custom provider URLs are validated against private
    IP ranges and must use HTTPS. Built-in providers are trusted.

    Args:
        provider_name: registered provider name
        secret_value: the API key/token value
        path: API endpoint path
        method: HTTP method
        body: request body (dict, list, or str)
        extra_headers: additional headers
        secrets_for_redact: dict of {name: value} for response redaction
        timeout: request timeout in seconds

    Returns dict with either:
        {"status": int, "body": parsed_json_or_str}
        {"error": str, "status": int (optional), "body": str (optional)}
    """
    import json
    import ssl
    import urllib.request
    import urllib.error
    from .sandbox import redact

    provider = get_provider(provider_name)
    if not provider:
        available = ", ".join(list_providers())
        return {"error": f"unknown provider: {provider_name}. available: {available}"}

    # Build auth
    auth_headers, auth_params = build_auth(provider, secret_value)
    url = build_url(provider, path, auth_params)

    # SSRF check — skip for built-in providers (trusted URLs)
    if provider.get("base_url") not in _BUILTIN_URLS:
        safe, reason = _check_url_safety(url)
        if not safe:
            return {"error": f"SSRF blocked: {reason}"}

    # Merge headers — strip dangerous headers that could override auth or redirect
    _blocked_headers = {"authorization", "x-api-key", "host", "cookie", "proxy-authorization"}
    safe_extra = {k: v for k, v in (extra_headers or {}).items()
                  if k.lower() not in _blocked_headers}
    headers = {**auth_headers, **safe_extra}

    # Encode body
    body_bytes = None
    if body is not None:
        if isinstance(body, (dict, list)):
            body_bytes = json.dumps(body).encode("utf-8")
        elif isinstance(body, str):
            body_bytes = body.encode("utf-8")

    redact_secrets = secrets_for_redact or {}

    try:
        req = urllib.request.Request(url, data=body_bytes, headers=headers,
                                     method=method.upper())
        ctx = ssl.create_default_context()

        with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
            resp_body = resp.read().decode("utf-8")
            resp_body = redact(resp_body, redact_secrets)

            try:
                return {"status": resp.status, "body": json.loads(resp_body)}
            except json.JSONDecodeError:
                return {"status": resp.status, "body": resp_body}

    except urllib.error.HTTPError as e:
        error_body = ""
        try:
            error_body = e.read().decode("utf-8")
            error_body = redact(error_body, redact_secrets)
        except Exception:
            pass
        return {"error": f"HTTP {e.code}: {e.reason}", "status": e.code,
                "body": error_body}

    except urllib.error.URLError as e:
        return {"error": f"connection error: {e.reason}"}

    except TimeoutError:
        return {"error": f"timeout ({timeout}s)"}

    except Exception as e:
        return {"error": f"{type(e).__name__}: {str(e)}"}
