import json
import os
from pathlib import Path
from urllib.parse import urljoin

import requests


class OpenAPILoadError(Exception):
    pass


def _load_text(path_or_url: str) -> str:
    if path_or_url.startswith("http://") or path_or_url.startswith("https://"):
        r = requests.get(path_or_url, timeout=20)
        r.raise_for_status()
        return r.text
    return Path(path_or_url).read_text(encoding="utf-8")


def _parse_spec(text: str, source: str) -> dict:
    # Prefer JSON; allow YAML only if PyYAML is installed.
    try:
        return json.loads(text)
    except Exception:
        pass

    try:
        import yaml  # type: ignore

        return yaml.safe_load(text)
    except Exception as e:
        raise OpenAPILoadError(
            f"OpenAPI spec must be JSON, or install PyYAML for YAML support. source={source} err={e}"
        )


def load_openapi(path_or_url: str) -> dict:
    text = _load_text(path_or_url)
    spec = _parse_spec(text, path_or_url)
    if not isinstance(spec, dict) or "paths" not in spec:
        raise OpenAPILoadError("Invalid OpenAPI spec: missing 'paths'")
    return spec


def _base_url(spec: dict) -> str:
    # OpenAPI 3: servers[0].url
    servers = spec.get("servers") or []
    if servers and isinstance(servers, list) and isinstance(servers[0], dict):
        url = servers[0].get("url")
        if url:
            return url.rstrip("/") + "/"

    # Fallback: env override (useful in CI)
    env = os.getenv("AI_SWARM_OPENAPI_BASE_URL")
    if env:
        return env.rstrip("/") + "/"

    raise OpenAPILoadError(
        "OpenAPI base URL not found (spec.servers missing). Set AI_SWARM_OPENAPI_BASE_URL to override."
    )


def build_request_from_openapi(
    spec: dict,
    *,
    path: str,
    method: str,
    headers: dict | None = None,
    path_params: dict | None = None,
    query_params: dict | None = None,
    body: dict | None = None,
) -> dict:
    """Build a swarm request dict from an OpenAPI operation.

    This is intentionally minimal and deterministic.
    Caller can provide path/query/body overrides.
    """

    method = method.lower()
    paths = spec.get("paths") or {}
    op = (paths.get(path) or {}).get(method)
    if not op:
        raise OpenAPILoadError(f"Operation not found: {method.upper()} {path}")

    base = _base_url(spec)

    # Resolve URL with path params if provided
    url_path = path
    for k, v in (path_params or {}).items():
        url_path = url_path.replace("{" + k + "}", str(v))

    url = urljoin(base, url_path.lstrip("/"))

    # Collect parameters (query only; path is assumed resolved)
    params = {}
    for p in (op.get("parameters") or []):
        if not isinstance(p, dict):
            continue
        if p.get("in") == "query":
            name = p.get("name")
            if name:
                # Do not guess values; leave empty unless user provides.
                params[name] = None

    # Apply user provided query overrides
    if query_params:
        params.update(query_params)

    # Remove Nones (requests will omit)
    params = {k: v for k, v in params.items() if v is not None}

    req_headers = {}
    if headers:
        req_headers.update(headers)

    # Default content-type if body is present
    if body is not None:
        # only set if absent
        has_ct = any(k.lower() == "content-type" for k in req_headers.keys())
        if not has_ct:
            req_headers["content-type"] = "application/json"

    return {
        "method": method.upper(),
        "url": url,
        "headers": req_headers,
        "params": params,
        "body": body or {},
    }
