"""CI errors retrieval for GitHub pull requests.

Primary signal: GitHub Checks API annotations (small, structured).
Optional signal: size-capped tail of Actions job logs (best-effort).
"""

from __future__ import annotations

from typing import cast

from aida_mcp.tools.github.ci_errors_helpers import (
    annotations_are_generic,
    default_log_dir,
    extract_error_excerpt,
    normalize_message,
    strip_ansi,
    validate_repo,
    write_log_file,
)
from aida_mcp.tools.github.ci_errors_models import (
    CiAnnotation,
    CiErrorsOptions,
    CiErrorsResponse,
    CiFailure,
)
from aida_mcp.tools.github.client import (
    GithubApiError,
    fetch_url_tail,
    github_get_json,
    github_get_redirect_location,
)


async def get_ci_errors_for_pr(
    *,
    repo: str,
    pr_number: int,
    options: CiErrorsOptions,
) -> CiErrorsResponse:
    """Return size-capped CI failures for a PR."""
    validate_repo(repo)

    pr_obj = await github_get_json(f"/repos/{repo}/pulls/{pr_number}")
    if not isinstance(pr_obj, dict):
        raise GithubApiError("Unexpected GitHub PR response shape (expected object).")
    pr = cast(dict[str, object], pr_obj)
    head_obj = pr.get("head")
    if not isinstance(head_obj, dict):
        raise GithubApiError("Unexpected GitHub PR response shape (missing head).")
    head = cast(dict[str, object], head_obj)
    head_sha_raw = head.get("sha")
    if not isinstance(head_sha_raw, str) or not head_sha_raw:
        raise GithubApiError("Could not determine PR head SHA from GitHub API response.")
    head_sha = head_sha_raw

    check_runs_obj = await github_get_json(
        f"/repos/{repo}/commits/{head_sha}/check-runs",
        params={"filter": "latest", "per_page": 100},
    )
    if not isinstance(check_runs_obj, dict):
        raise GithubApiError("Unexpected GitHub check-runs response shape (expected object).")
    check_runs_doc = cast(dict[str, object], check_runs_obj)
    check_runs_val = check_runs_doc.get("check_runs")
    if not isinstance(check_runs_val, list):
        raise GithubApiError("Unexpected GitHub check-runs response shape.")
    check_runs = cast(list[object], check_runs_val)

    failures: list[CiFailure] = []
    for cr_obj in check_runs:
        if not isinstance(cr_obj, dict):
            continue
        cr = cast(dict[str, object], cr_obj)
        conclusion = cr.get("conclusion")
        status = cr.get("status")
        if not isinstance(status, str) or status != "completed":
            continue
        if conclusion in ("success", "skipped", None):
            continue

        check_run_id = cr.get("id")
        name = cr.get("name")
        details_url = cr.get("details_url")
        if not isinstance(check_run_id, int) or not isinstance(name, str):
            continue

        # Annotations are the "errors only" signal. Keep it small.
        ann_obj = await github_get_json(
            f"/repos/{repo}/check-runs/{check_run_id}/annotations",
            params={"per_page": options.max_annotations_per_check},
        )
        annotations: list[CiAnnotation] = []
        if isinstance(ann_obj, list):
            ann_list = cast(list[object], ann_obj)
            for a_obj in ann_list:
                if not isinstance(a_obj, dict):
                    continue
                a = cast(dict[str, object], a_obj)
                if a.get("annotation_level") != "failure":
                    continue
                msg_obj = a.get("message")
                if not isinstance(msg_obj, str) or not msg_obj.strip():
                    continue
                msg = msg_obj
                start_line_obj = a.get("start_line")
                end_line_obj = a.get("end_line")
                title_obj = a.get("title")
                start_line = start_line_obj if isinstance(start_line_obj, int) else None
                end_line = end_line_obj if isinstance(end_line_obj, int) else None
                title = title_obj if isinstance(title_obj, str) else None
                annotations.append(
                    {
                        "path": str(a.get("path") or ""),
                        "start_line": start_line,
                        "end_line": end_line,
                        "title": title,
                        "message": normalize_message(msg)[:300],
                    }
                )

        log_tail: str | None = None
        log_tail_path: str | None = None
        error_excerpt: str | None = None

        should_fetch_tail = options.include_log_tails or (
            options.include_log_tails_if_generic and annotations_are_generic(annotations)
        )
        if should_fetch_tail:
            # check_run_id matches Actions job ID for GitHub Actions check-runs in this org (as observed),
            # so we can try the Actions job logs endpoint. Best-effort only.
            try:
                loc = await github_get_redirect_location(f"/repos/{repo}/actions/jobs/{check_run_id}/logs")
                raw_tail = await fetch_url_tail(loc, max_bytes=options.log_tail_max_bytes)
                tail = strip_ansi(raw_tail[-options.log_tail_max_bytes :])
                error_excerpt = extract_error_excerpt(tail)

                if options.save_log_files:
                    base_dir = options.log_dir or default_log_dir()
                    log_tail_path = write_log_file(
                        base_dir=base_dir,
                        repo=repo,
                        pr_number=pr_number,
                        check_run_id=check_run_id,
                        name=name,
                        text=tail,
                    )

                if options.include_log_tails:
                    log_tail = tail
            except Exception:
                log_tail = None

        failures.append(
            {
                "check_run_id": check_run_id,
                "name": name,
                "conclusion": conclusion if isinstance(conclusion, str) else None,
                "details_url": details_url if isinstance(details_url, str) else None,
                "annotations": annotations,
                "log_tail": log_tail,
                "log_tail_path": log_tail_path,
                "error_excerpt": error_excerpt,
            }
        )

        if len(failures) >= options.max_failed_checks:
            break

    msg: str | None = None
    if not failures:
        msg = "No completed failing check-runs found for this PR head SHA (or insufficient permissions)."

    return {
        "ok": True,
        "repo": repo,
        "pr_number": pr_number,
        "head_sha": head_sha,
        "failures": failures,
        "message": msg,
    }
