# Copyright (C) 2026 GoodData Corporation

"""Small GitHub API client helpers.

Goals:
- Non-interactive, API-only (no `gh` subprocess).
- Strict output size caps to avoid flooding the client.
- Token read from env (`GH_TOKEN` preferred; `GITHUB_TOKEN` fallback).
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Any, cast

import httpx

GITHUB_API_BASE_URL = "https://api.github.com"


@dataclass(frozen=True)
class GithubAuth:
    token: str


class GithubApiError(RuntimeError):
    def __init__(self, message: str, *, status_code: int | None = None) -> None:
        super().__init__(message)
        self.status_code = status_code


def _get_github_token() -> str:
    token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
    if not token:
        raise GithubApiError(
            "Missing GitHub token. Set GH_TOKEN (preferred) or GITHUB_TOKEN in the MCP server environment.",
            status_code=None,
        )
    return token


def get_default_auth() -> GithubAuth:
    return GithubAuth(token=_get_github_token())


def _headers(auth: GithubAuth) -> dict[str, str]:
    # Keep headers minimal; never echo tokens back to caller.
    return {
        "Authorization": f"Bearer {auth.token}",
        "Accept": "application/vnd.github+json",
        "User-Agent": "aida-mcp",
        "X-GitHub-Api-Version": "2022-11-28",
    }


async def github_get_json(
    path: str,
    *,
    auth: GithubAuth | None = None,
    params: dict[str, str | int] | None = None,
) -> object:
    """GET JSON from GitHub REST API (api.github.com)."""
    if not path.startswith("/"):
        raise ValueError("path must start with '/'")
    a = auth or get_default_auth()
    async with httpx.AsyncClient(base_url=GITHUB_API_BASE_URL, timeout=30.0) as client:
        resp = await client.get(path, headers=_headers(a), params=params)
    if resp.status_code >= 400:
        raise GithubApiError(f"GitHub API GET {path} failed: HTTP {resp.status_code}", status_code=resp.status_code)
    return resp.json()


async def github_post_json(
    path: str,
    *,
    auth: GithubAuth | None = None,
    json_body: dict[str, Any] | None = None,
) -> object:
    """POST JSON to GitHub REST API (api.github.com)."""
    if not path.startswith("/"):
        raise ValueError("path must start with '/'")
    a = auth or get_default_auth()
    async with httpx.AsyncClient(base_url=GITHUB_API_BASE_URL, timeout=30.0) as client:
        resp = await client.post(path, headers=_headers(a), json=json_body or {})
    if resp.status_code >= 400:
        raise GithubApiError(f"GitHub API POST {path} failed: HTTP {resp.status_code}", status_code=resp.status_code)
    return resp.json()


async def github_post_graphql(
    *,
    query: str,
    variables: dict[str, Any],
    auth: GithubAuth | None = None,
) -> object:
    """Call GitHub GraphQL API (api.github.com/graphql)."""
    a = auth or get_default_auth()
    async with httpx.AsyncClient(base_url=GITHUB_API_BASE_URL, timeout=30.0) as client:
        resp = await client.post("/graphql", headers=_headers(a), json={"query": query, "variables": variables})
    if resp.status_code >= 400:
        raise GithubApiError(
            f"GitHub GraphQL failed: HTTP {resp.status_code}",
            status_code=resp.status_code,
        )
    data_obj: object = resp.json()
    if isinstance(data_obj, dict):
        data = cast(dict[str, object], data_obj)
        errors = data.get("errors")
        if errors:
            raise GithubApiError(f"GitHub GraphQL returned errors: {errors!r}", status_code=resp.status_code)
        return data
    return data_obj


async def github_get_redirect_location(
    path: str,
    *,
    auth: GithubAuth | None = None,
) -> str:
    """GET a GitHub API endpoint that responds with a redirect and return its Location.

    Used for Actions job logs endpoint which redirects to a blob URL.
    """
    if not path.startswith("/"):
        raise ValueError("path must start with '/'")
    a = auth or get_default_auth()
    async with httpx.AsyncClient(base_url=GITHUB_API_BASE_URL, timeout=30.0, follow_redirects=False) as client:
        resp = await client.get(path, headers=_headers(a))
    if resp.status_code in (301, 302, 303, 307, 308):
        loc = resp.headers.get("Location")
        if not loc:
            raise GithubApiError("GitHub API redirect missing Location header.", status_code=resp.status_code)
        return loc
    if resp.status_code >= 400:
        raise GithubApiError(f"GitHub API GET {path} failed: HTTP {resp.status_code}", status_code=resp.status_code)
    raise GithubApiError(
        f"GitHub API GET {path} did not redirect (HTTP {resp.status_code}).", status_code=resp.status_code
    )


async def fetch_url_tail(
    url: str,
    *,
    max_bytes: int,
) -> str:
    """Fetch the tail of a URL using HTTP Range (best-effort).

    This avoids downloading large logs. If Range isn't supported, falls back to full download capped to max_bytes.
    """
    headers = {"Range": f"bytes=-{max_bytes}"}
    async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
        resp = await client.get(url, headers=headers)
        if resp.status_code in (200, 206):
            content = resp.text
            if len(content.encode("utf-8")) > max_bytes:
                # As a last resort, hard-cap by characters (approx).
                return content[-max_bytes:]
            return content
        # Fallback (unexpected status): best-effort capped fetch
        resp2 = await client.get(url)
        if resp2.status_code >= 400:
            raise GithubApiError(f"Failed to fetch log tail: HTTP {resp2.status_code}", status_code=resp2.status_code)
        text = resp2.text
        return text[-max_bytes:]
