"""通过 GitHub REST Contents API 读取公开/私有仓库中的文件文本或目录列表。"""

from __future__ import annotations

import base64
import re
from typing import Any
from urllib.parse import quote

import httpx


def _validate_owner_repo(owner: str, repo: str) -> tuple[str | None, str | None, str | None]:
    o = (owner or "").strip()
    r = (repo or "").strip().replace(".git", "")
    if "/" in r:
        return None, None, "repo 请只填仓库名，不要包含 owner（owner 单独传参）"
    if not o or not r:
        return None, None, "owner 与 repo 不能为空"
    if len(o) > 128 or len(r) > 128:
        return None, None, "owner/repo 过长"
    if re.search(r"[\s\\]", o) or re.search(r"[\s\\]", r):
        return None, None, "owner/repo 含非法空白或反斜杠"
    return o, r, None


def _encode_contents_path(raw: str) -> str:
    """GitHub ``/contents/{path}``：按段 URL 编码。"""
    p = (raw or "").strip().lstrip("/")
    if not p:
        return ""
    return "/".join(quote(seg, safe="") for seg in p.split("/") if seg)


async def github_repo_read(
    *,
    owner: str,
    repo: str,
    path: str = "",
    ref: str | None = None,
    api_base_url: str = "https://api.github.com",
    token: str = "",
    user_agent: str = "AICD",
    timeout_sec: float = 60.0,
    max_text_chars: int = 400_000,
    max_directory_entries: int = 200,
    block_external_network: bool = False,
) -> dict[str, Any]:
    """
    - ``path`` 为空：列出仓库根目录。
    - ``path`` 为文件：返回解码后的 UTF-8 文本（过大时用 download_url 拉取后截断）。
    - ``path`` 为目录：返回条目列表（name/path/type/size）。
    """
    if block_external_network:
        return {
            "ok": False,
            "error": "block_external_network=ON 时已禁止 github_repo_read（须访问 api.github.com / raw.githubusercontent.com）",
            "network_policy": "block_external_network",
        }
    own, rep, err = _validate_owner_repo(owner, repo)
    if err:
        return {"ok": False, "error": err}
    assert own is not None and rep is not None
    base = (api_base_url or "https://api.github.com").strip().rstrip("/")
    enc_path = _encode_contents_path(path)
    url = f"{base}/repos/{quote(own, safe='')}/{quote(rep, safe='')}/contents"
    if enc_path:
        url += f"/{enc_path}"
    params: dict[str, str] = {}
    if ref and str(ref).strip():
        params["ref"] = str(ref).strip()
    headers: dict[str, str] = {
        "Accept": "application/vnd.github+json",
        "User-Agent": user_agent.strip() or "AICD",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    tok = (token or "").strip()
    if tok:
        headers["Authorization"] = f"Bearer {tok}"
    hint = None
    try:
        async with httpx.AsyncClient(timeout=timeout_sec) as client:
            resp = await client.get(url, headers=headers, params=params or None)
        if resp.status_code == 401:
            hint = "401：私有仓库或需鉴权时在 integrations.github.token / GITHUB_TOKEN 配置 PAT"
        elif resp.status_code == 404:
            hint = "404：检查 owner/repo/path/ref 是否存在（分支名错误常见）"
        if not resp.is_success:
            return {
                "ok": False,
                "status_code": resp.status_code,
                "error": resp.text[:2000] if resp.text else f"HTTP {resp.status_code}",
                "url": url,
                "hint": hint,
                "github_token_configured": bool(tok),
            }
        data = resp.json()
        if isinstance(data, list):
            entries: list[dict[str, Any]] = []
            for item in data[:max_directory_entries]:
                if not isinstance(item, dict):
                    continue
                entries.append(
                    {
                        "name": item.get("name"),
                        "path": item.get("path"),
                        "type": item.get("type"),
                        "size": item.get("size"),
                        "sha": item.get("sha"),
                    }
                )
            return {
                "ok": True,
                "kind": "directory",
                "path": (path or "").strip("/") or "(root)",
                "entry_count": len(entries),
                "truncated_dir": len(data) > len(entries),
                "entries": entries,
                "api_url": url,
                "github_token_configured": bool(tok),
            }
        if not isinstance(data, dict):
            return {"ok": False, "error": "unexpected JSON shape"}
        typ = data.get("type")
        if typ == "dir":
            # API 有时返回单个目录对象而非目录数组（少见）；再请求子路径已由上面 list 处理
            return {"ok": False, "error": "unexpected directory object; retry with path segments"}
        if typ != "file":
            return {
                "ok": False,
                "error": f"unsupported content type: {typ!r} (symlink/submodule 请用网页或 git clone)",
            }
        text: str | None = None
        b64 = data.get("content")
        if isinstance(b64, str) and data.get("encoding") == "base64":
            try:
                raw = base64.b64decode("".join(b64.splitlines()))
                text = raw.decode("utf-8", errors="replace")
            except Exception as e:  # noqa: BLE001
                return {"ok": False, "error": f"base64 decode failed: {e}"}
        if text is None:
            dl = data.get("download_url")
            if isinstance(dl, str) and dl.startswith("http"):
                async with httpx.AsyncClient(timeout=timeout_sec) as client2:
                    r2 = await client2.get(dl, headers=headers)
                if not r2.is_success:
                    return {
                        "ok": False,
                        "error": f"download_url failed: HTTP {r2.status_code}",
                    }
                raw2 = r2.content
                text = raw2.decode("utf-8", errors="replace")
            else:
                return {"ok": False, "error": "no file content or download_url in API response"}
        trunc = False
        if len(text) > max_text_chars:
            text = text[:max_text_chars]
            trunc = True
        return {
            "ok": True,
            "kind": "file",
            "path": data.get("path"),
            "name": data.get("name"),
            "sha": data.get("sha"),
            "size": data.get("size"),
            "text": text,
            "truncated": trunc,
            "encoding_assumed": "utf-8",
            "api_url": url,
            "github_token_configured": bool(tok),
        }
    except httpx.TimeoutException:
        return {"ok": False, "error": "timeout"}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}
