"""Shared HTTP client for communicating with the graph8 backend API."""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

import httpx

CREDENTIALS_PATH = Path.home() / ".g8" / "credentials.json"


def load_stored_credentials() -> tuple[str, str, str]:
    """Load credentials from ~/.g8/credentials.json.

    Returns (api_key, api_url, auth_method).
    auth_method is "oauth" or "api_key" (or empty if no credentials).
    """
    if not CREDENTIALS_PATH.exists():
        return "", "", ""
    try:
        data = json.loads(CREDENTIALS_PATH.read_text())
        return (
            data.get("api_key", ""),
            data.get("api_url", ""),
            data.get("auth_method", "api_key"),
        )
    except (json.JSONDecodeError, OSError):
        return "", "", ""


def save_credentials(api_key: str, api_url: str = "", *, auth_method: str = "api_key", email: str = "") -> None:
    """Save credentials to ~/.g8/credentials.json."""
    CREDENTIALS_PATH.parent.mkdir(parents=True, exist_ok=True)
    data: dict[str, str] = {"api_key": api_key, "api_url": api_url, "auth_method": auth_method}
    if email:
        data["email"] = email
    CREDENTIALS_PATH.write_text(json.dumps(data, indent=2))
    CREDENTIALS_PATH.chmod(0o600)


def remove_credentials() -> bool:
    """Remove stored credentials. Returns True if file existed."""
    if CREDENTIALS_PATH.exists():
        CREDENTIALS_PATH.unlink()
        return True
    return False


class G8ClientError(Exception):
    """Raised when a graph8 API call fails."""

    def __init__(self, status_code: int, message: str):
        self.status_code = status_code
        super().__init__(message)


class G8Client:
    """Thin wrapper around httpx for graph8 Developer API calls.

    Key resolution order:
    1. contextvars (set by auth middleware in remote/HTTP mode)
    2. G8_API_KEY env var (used in local/stdio mode)
    3. ~/.g8/credentials.json (stored by `g8 login`)
    """

    def __init__(self, *, require_key: bool = True) -> None:
        self._env_api_key = os.environ.get("G8_API_KEY", "")
        self._stored_key, stored_url, self._auth_method = "", "", ""
        if not self._env_api_key:
            self._stored_key, stored_url, self._auth_method = load_stored_credentials()

        env_url = os.environ.get("G8_API_URL", "")
        self.base_url = (env_url or stored_url or "http://localhost:8000").rstrip("/")

        if require_key and not self._env_api_key and not self._stored_key:
            raise G8ClientError(
                401,
                "No API key found. Run `g8 login` or set G8_API_KEY.\n"
                "  Get a key at https://app.graph8.com/settings/api",
            )

    @property
    def _api_key(self) -> str:
        from g8_mcp_server.auth import get_api_key
        return get_api_key() or self._env_api_key or self._stored_key

    @property
    def _api_key_source(self) -> str:
        """Return where the active API key comes from (for `g8 whoami`)."""
        from g8_mcp_server.auth import get_api_key
        if get_api_key():
            return "MCP remote (OAuth)"
        if self._env_api_key:
            return "G8_API_KEY environment variable"
        if self._stored_key:
            return f"~/.g8/credentials.json"
        return "none"

    @property
    def _headers(self) -> dict[str, str]:
        key = self._api_key
        if not key:
            raise G8ClientError(401, "No API key — run `g8 login` or set G8_API_KEY")
        return {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}

    async def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        async with httpx.AsyncClient(base_url=self.base_url, headers=self._headers, timeout=60) as client:
            resp = await client.get(f"/api/v1{path}", params=_strip_none(params))
            return _handle_response(resp)

    async def post(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
        async with httpx.AsyncClient(base_url=self.base_url, headers=self._headers, timeout=120) as client:
            resp = await client.post(f"/api/v1{path}", json=body)
            return _handle_response(resp)

    async def patch(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
        async with httpx.AsyncClient(base_url=self.base_url, headers=self._headers, timeout=60) as client:
            resp = await client.patch(f"/api/v1{path}", json=body)
            return _handle_response(resp)

    async def delete(self, path: str) -> dict[str, Any]:
        async with httpx.AsyncClient(base_url=self.base_url, headers=self._headers, timeout=30) as client:
            resp = await client.delete(f"/api/v1{path}")
            return _handle_response(resp)


def _strip_none(params: dict[str, Any] | None) -> dict[str, Any] | None:
    if params is None:
        return None
    return {k: v for k, v in params.items() if v is not None}


def _handle_response(resp: httpx.Response) -> dict[str, Any]:
    if resp.status_code == 401:
        raise G8ClientError(401, "Invalid API key. Check G8_API_KEY or generate a new one at https://app.graph8.com/settings/api-keys")
    if resp.status_code == 403:
        raise G8ClientError(403, "Access denied. Your API key may not have permission for this operation.")
    if resp.status_code == 404:
        raise G8ClientError(404, "Resource not found. Check the ID and try again.")
    if resp.status_code == 429:
        raise G8ClientError(429, "Rate limit exceeded. Wait a moment and try again.")
    if resp.status_code >= 400:
        try:
            body = resp.json()
            msg = body.get("message") or body.get("detail") or body.get("error") or resp.text
        except Exception:
            msg = resp.text
        raise G8ClientError(resp.status_code, f"API error ({resp.status_code}): {msg}")
    try:
        return resp.json()
    except Exception:
        return {"data": resp.text}


def format_result(data: Any) -> str:
    """Format API response data as readable JSON for the agent."""
    if isinstance(data, dict) and "data" in data:
        return json.dumps(data["data"], indent=2, default=str)
    return json.dumps(data, indent=2, default=str)
