"""Best-effort GitCode PR comments for non-blocking CI gate warnings."""

from __future__ import annotations

import json
import logging
import os
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final, Protocol

if TYPE_CHECKING:
    from collections.abc import Callable

    from scripts.helpers._config import Config
    from scripts.helpers.ci_gate.models import GateError
    from scripts.helpers.common.ast_utils import ShadowWarning

logger = logging.getLogger(__name__)

_GITCODE_API_BASE: Final = "https://api.atomgit.com/api/v5"
_UNSCOPED_MARKER: Final = "<!-- msmodeling-ci-gate:unscoped-python -->"
_ALL_EXEMPT_MARKER: Final = "<!-- msmodeling-ci-gate:all-exempt-tests -->"
_EXEMPTION_DRIFT_MARKER: Final = "<!-- msmodeling-ci-gate:exemption-drift -->"
_SHADOWED_DEFS_MARKER: Final = "<!-- msmodeling-ci-gate:shadowed-defs -->"
_HTTP_TIMEOUT_SEC: Final = 10
_MAX_LISTED_PATHS: Final = 20
_PR_COMMENTS_PER_PAGE: Final = 100


@dataclass(frozen=True, slots=True)
class GitCodeCommentConfig:
    owner: str
    repo: str
    pr_number: int
    pat_token: str


class _PathMarkerPostFn(Protocol):
    def __call__(self, paths: tuple[str, ...], *, config: GitCodeCommentConfig) -> None: ...


_GITCODE_ENV_HINT: Final = "set GITCODE_OWNER, GITCODE_REPO, GITCODE_PR_NUMBER, GITCODE_PAT"


def load_gitcode_comment_config(
    cfg: Config | None = None,
) -> GitCodeCommentConfig | None:
    """Load GitCode comment credentials from Config or env; return None when incomplete."""
    if cfg is not None:
        owner = cfg.gitcode_owner.strip()
        repo = cfg.gitcode_repo.strip()
        pr_number = cfg.gitcode_pr_number
        pat_token = cfg.gitcode_pat.strip()
    else:
        owner = os.environ.get("GITCODE_OWNER", "").strip()
        repo = os.environ.get("GITCODE_REPO", "").strip()
        pr_raw = os.environ.get("GITCODE_PR_NUMBER", "").strip()
        pat_token = os.environ.get("GITCODE_PAT", "").strip()
        pr_number = None
        if pr_raw:
            try:
                pr_number = int(pr_raw)
            except ValueError:
                logger.warning("GITCODE_PR_NUMBER must be an integer, got %r", pr_raw)
                return None
    if not owner or not repo or pr_number is None or not pat_token:
        return None
    return GitCodeCommentConfig(owner=owner, repo=repo, pr_number=pr_number, pat_token=pat_token)


def build_unscoped_python_comment_body(paths: tuple[str, ...]) -> str:
    """Render a concise PR comment body for unscoped Python file changes."""
    lines = [
        _UNSCOPED_MARKER,
        "**CI gate (info):** Python file(s) outside configured source roots are not blocking, but please review scope:",
    ]
    lines.extend(f"- `{path}`" for path in paths[:_MAX_LISTED_PATHS])
    remaining = len(paths) - _MAX_LISTED_PATHS
    if remaining > 0:
        lines.append(f"- ... and {remaining} more")
    return "\n".join(lines)


def _build_headers(pat_token: str) -> dict[str, str]:
    return {
        "Accept": "application/json, text/plain, */*",
        "Authorization": f"Bearer {pat_token}",
        "Content-Type": "application/json",
        "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        "Connection": "keep-alive",
        "Origin": "https://gitcode.com",
        "Referer": "https://gitcode.com/",
    }


def _request_json(
    method: str,
    url: str,
    *,
    headers: dict[str, str],
    payload: dict[str, object] | None = None,
) -> object:
    data = json.dumps(payload).encode() if payload is not None else None
    request = urllib.request.Request(url, data=data, headers=headers, method=method)
    with urllib.request.urlopen(request, timeout=_HTTP_TIMEOUT_SEC) as response:
        body = response.read().decode("utf-8")
        if not body:
            return {}
        return json.loads(body)


def _list_pr_comments(config: GitCodeCommentConfig) -> list[dict[str, object]]:
    comments: list[dict[str, object]] = []
    page = 1
    while True:
        query = urllib.parse.urlencode(
            {
                "comment_type": "pr_comment",
                "direction": "asc",
                "page": str(page),
                "per_page": str(_PR_COMMENTS_PER_PAGE),
            }
        )
        url = f"{_GITCODE_API_BASE}/repos/{config.owner}/{config.repo}/pulls/{config.pr_number}/comments?{query}"
        raw = _request_json("GET", url, headers=_build_headers(config.pat_token))
        if not isinstance(raw, list):
            break
        page_comments = [item for item in raw if isinstance(item, dict)]
        comments.extend(page_comments)
        if len(page_comments) < _PR_COMMENTS_PER_PAGE:
            break
        page += 1
    return comments


def build_all_exempt_tests_comment_body(paths: tuple[str, ...]) -> str:
    """Render a concise PR comment body for changed test files with only exempt nodes."""
    lines = [
        _ALL_EXEMPT_MARKER,
        "**CI gate (info):** Changed test file(s) contain only exempt test node(s); no pytest was scheduled:",
    ]
    lines.extend(f"- `{path}`" for path in paths[:_MAX_LISTED_PATHS])
    remaining = len(paths) - _MAX_LISTED_PATHS
    if remaining > 0:
        lines.append(f"- ... and {remaining} more")
    return "\n".join(lines)


def build_exemption_drift_comment_body(errors: tuple[GateError, ...]) -> str:
    """Render a PR comment body for stale gate_policy exemptions."""
    lines = [
        _EXEMPTION_DRIFT_MARKER,
        "**CI gate (blocking):** `tests/.ci/gate_policy.yaml` exemption(s) reference deleted or renamed paths:",
    ]
    for err in errors[:_MAX_LISTED_PATHS]:
        label = f"{err.path}::{err.symbol}" if err.symbol else err.path
        detail = f" — {err.detail}" if err.detail else ""
        lines.append(f"- `{label}`{detail}")
    remaining = len(errors) - _MAX_LISTED_PATHS
    if remaining > 0:
        lines.append(f"- ... and {remaining} more")
    return "\n".join(lines)


def build_shadowed_defs_comment_body(warnings: tuple[ShadowWarning, ...]) -> str:
    """Render a PR comment body for shadowed duplicate definitions."""
    lines = [
        _SHADOWED_DEFS_MARKER,
        "**CI gate (info):** Duplicate function definitions detected; last definition wins for coverage mapping:",
    ]
    lines.extend(
        f"- `{warning.file}:{warning.line}` `{warning.name}` shadowed by line {warning.shadowed_by_line}"
        for warning in warnings[:_MAX_LISTED_PATHS]
    )
    remaining = len(warnings) - _MAX_LISTED_PATHS
    if remaining > 0:
        lines.append(f"- ... and {remaining} more")
    return "\n".join(lines)


def _find_marker_comment_id(comments: list[dict[str, object]], marker: str) -> int | None:
    for comment in comments:
        body = comment.get("body")
        if isinstance(body, str) and marker in body:
            note_id = comment.get("id")
            if isinstance(note_id, int):
                return note_id
    return None


def _create_pr_comment(config: GitCodeCommentConfig, body: str) -> None:
    url = f"{_GITCODE_API_BASE}/repos/{config.owner}/{config.repo}/pulls/{config.pr_number}/comments"
    _request_json("POST", url, headers=_build_headers(config.pat_token), payload={"body": body})


def _update_pr_comment(config: GitCodeCommentConfig, note_id: int, body: str) -> None:
    url = f"{_GITCODE_API_BASE}/repos/{config.owner}/{config.repo}/pulls/comments/{note_id}"
    _request_json("PATCH", url, headers=_build_headers(config.pat_token), payload={"body": body})


def post_marker_pr_comment(
    paths: tuple[str, ...],
    *,
    config: GitCodeCommentConfig,
    marker: str,
    body: str,
    action_label: str,
) -> None:
    """Create or update a fixed-marker PR comment. Raises on HTTP/API failure."""
    if not paths:
        return
    comments = _list_pr_comments(config)
    note_id = _find_marker_comment_id(comments, marker)
    if note_id is None:
        _create_pr_comment(config, body)
        logger.info("GitCode PR comment created for %s (%d file(s))", action_label, len(paths))
        return
    _update_pr_comment(config, note_id, body)
    logger.info("GitCode PR comment updated for %s (%d file(s))", action_label, len(paths))


def post_unscoped_python_comment(paths: tuple[str, ...], *, config: GitCodeCommentConfig) -> None:
    """Create or update the unscoped-Python PR comment. Raises on HTTP/API failure."""
    post_marker_pr_comment(
        paths,
        config=config,
        marker=_UNSCOPED_MARKER,
        body=build_unscoped_python_comment_body(paths),
        action_label="unscoped Python",
    )


def post_all_exempt_tests_comment(paths: tuple[str, ...], *, config: GitCodeCommentConfig) -> None:
    """Create or update the all-exempt-tests PR comment. Raises on HTTP/API failure."""
    post_marker_pr_comment(
        paths,
        config=config,
        marker=_ALL_EXEMPT_MARKER,
        body=build_all_exempt_tests_comment_body(paths),
        action_label="all-exempt test files",
    )


def post_exemption_drift_comment(errors: tuple[GateError, ...], *, config: GitCodeCommentConfig) -> None:
    """Create or update the exemption-drift PR comment. Raises on HTTP/API failure."""
    paths = tuple(sorted({err.path for err in errors}))
    post_marker_pr_comment(
        paths,
        config=config,
        marker=_EXEMPTION_DRIFT_MARKER,
        body=build_exemption_drift_comment_body(errors),
        action_label="exemption drift",
    )


def post_shadowed_defs_comment(warnings: tuple[ShadowWarning, ...], *, config: GitCodeCommentConfig) -> None:
    """Create or update the shadowed-defs PR comment. Raises on HTTP/API failure."""
    paths = tuple(sorted({warning.file for warning in warnings}))
    post_marker_pr_comment(
        paths,
        config=config,
        marker=_SHADOWED_DEFS_MARKER,
        body=build_shadowed_defs_comment_body(warnings),
        action_label="shadowed definitions",
    )


def _log_gitcode_comment_not_posted(env_hint: str) -> None:
    """GitCode credentials absent; policy warnings are logged by the caller."""
    logger.debug("GitCode PR comment not posted (%s)", env_hint)


def _try_post_comment(post: Callable[[], None]) -> None:
    try:
        post()
    except (
        OSError,
        urllib.error.URLError,
        json.JSONDecodeError,
        UnicodeDecodeError,
    ) as exc:
        logger.warning("GitCode PR comment failed (non-blocking): %s", exc)
    except ValueError:
        logger.exception("GitCode PR comment logic error (non-blocking)")


def _maybe_post_marker_comment(
    paths: tuple[str, ...],
    *,
    cfg: Config | None,
    env_hint: str,
    post_fn: _PathMarkerPostFn,
) -> None:
    if not paths:
        return
    config = load_gitcode_comment_config(cfg)
    if config is None:
        _log_gitcode_comment_not_posted(env_hint)
        return
    _try_post_comment(lambda: post_fn(paths, config=config))


def maybe_post_unscoped_python_comment(paths: tuple[str, ...], *, cfg: Config | None = None) -> None:
    """Best-effort PR comment for unscoped Python changes; never raises."""
    _maybe_post_marker_comment(
        paths,
        cfg=cfg,
        env_hint=_GITCODE_ENV_HINT,
        post_fn=post_unscoped_python_comment,
    )


def maybe_post_all_exempt_tests_comment(paths: tuple[str, ...], *, cfg: Config | None = None) -> None:
    """Best-effort PR comment for all-exempt changed test files; never raises."""
    _maybe_post_marker_comment(
        paths,
        cfg=cfg,
        env_hint=_GITCODE_ENV_HINT,
        post_fn=post_all_exempt_tests_comment,
    )


def maybe_post_exemption_drift_comment(errors: tuple[GateError, ...], *, cfg: Config | None = None) -> None:
    """Best-effort PR comment for stale exemptions; never raises."""
    if not errors:
        return
    config = load_gitcode_comment_config(cfg)
    if config is None:
        _log_gitcode_comment_not_posted(_GITCODE_ENV_HINT)
        return
    _try_post_comment(lambda: post_exemption_drift_comment(errors, config=config))


def maybe_post_shadowed_defs_comment(warnings: tuple[ShadowWarning, ...], *, cfg: Config | None = None) -> None:
    """Best-effort PR comment for shadowed duplicate defs; never raises."""
    if not warnings:
        return
    config = load_gitcode_comment_config(cfg)
    if config is None:
        _log_gitcode_comment_not_posted(_GITCODE_ENV_HINT)
        return
    _try_post_comment(lambda: post_shadowed_defs_comment(warnings, config=config))