"""CI failure webhook handler.

Handles ``check_run.completed`` and ``check_suite.completed`` events where
``conclusion == "failure"``.  On failure, creates a high-priority ``fix`` task
so a peon-loop agent can investigate and resolve the issue.

The handler is registered automatically when this module is imported.  Import
it from ``github_webhooks/__init__.py`` (already done) so it loads at startup.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime
from typing import Any

from peon_mcp.db import get_pool
from peon_mcp.github_webhooks.dispatcher import register_handler

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Failure classification
# ---------------------------------------------------------------------------

#: Patterns that suggest lint/format issues.
_LINT_FORMAT_PATTERNS = (
    "would reformat",
    "reformatted",
    "ruff check",
    "ruff format",
    "black --check",
    "oh no! black",
    "e501",    # line too long
    "w291",    # trailing whitespace
    "w293",    # whitespace before comment
    "f401",    # unused import (ruff can --fix)
    "i001",    # isort (ruff can --fix)
    "lint error",
    "formatting",
)

#: Patterns that suggest type-checking failures.
_TYPE_ERROR_PATTERNS = (
    "error: incompatible type",
    "cannot be assigned to type",
    "mypy",
    "pyright",
    "type error",
    "typeerror",
)

#: Patterns that suggest test failures.
_TEST_FAILURE_PATTERNS = (
    "failed",
    "assertionerror",
    "assert ",
    "pytest",
    "test failure",
    "test_",
    "traceback (most recent",
)


def classify_failure(logs: str) -> dict[str, Any]:
    """Classify a CI failure from log text.

    Returns a dict with keys:
        - ``type``: one of ``"lint_format"``, ``"type_error"``,
          ``"test_failure"``, ``"unknown"``
        - ``fixable``: ``True`` when it's likely auto-fixable
        - ``summary``: human-readable one-liner
        - ``log_excerpt``: first 1500 chars of logs for the task description
    """
    lower = logs.lower()
    excerpt = logs[:1500].strip() if logs else "(no logs retrieved)"

    if any(p in lower for p in _LINT_FORMAT_PATTERNS):
        return {
            "type": "lint_format",
            "fixable": True,
            "summary": "Linting / formatting failure detected.",
            "log_excerpt": excerpt,
        }

    if any(p in lower for p in _TYPE_ERROR_PATTERNS):
        return {
            "type": "type_error",
            "fixable": False,
            "summary": "Type-checking failure detected.",
            "log_excerpt": excerpt,
        }

    if any(p in lower for p in _TEST_FAILURE_PATTERNS):
        return {
            "type": "test_failure",
            "fixable": False,
            "summary": "Test failure detected.",
            "log_excerpt": excerpt,
        }

    return {
        "type": "unknown",
        "fixable": False,
        "summary": "CI failure — could not determine cause from logs.",
        "log_excerpt": excerpt,
    }


# ---------------------------------------------------------------------------
# Task description builders
# ---------------------------------------------------------------------------


def _build_ci_fix_description(
    failure_info: dict[str, Any],
    repo: str,
    pr_number: int | None,
    pr_url: str,
    branch: str,
    head_sha: str,
    check_name: str,
) -> str:
    """Build a task description with CI failure context."""
    lines = [
        f"CI checks failed on `{repo}`.",
        "",
        f"**Check name**: `{check_name}`" if check_name else "",
        f"**Failure type**: {failure_info['summary']}",
        f"**Classification**: `{failure_info['type']}`",
        f"**Branch**: `{branch}`" if branch else "",
        f"**Commit**: `{head_sha[:12]}`" if head_sha else "",
    ]
    if pr_number:
        lines.append(f"**PR**: [#{pr_number}]({pr_url})")
    lines = [l for l in lines if l]  # drop empty lines from missing fields

    lines += [
        "",
        "## Instructions",
        "",
        f"1. Fetch the failed CI logs: `gh run list --repo {repo} --commit {head_sha} --json databaseId,conclusion`",
        f"   Then: `gh run view <run_id> --repo {repo} --log-failed`",
    ]

    if pr_number and branch:
        lines.append(f"2. Check out the PR branch: `{branch}`")
    elif branch:
        lines.append(f"2. Check out branch: `{branch}`")

    failure_type = failure_info["type"]
    if failure_type == "lint_format":
        lines += [
            "3. Run `ruff format .` and `ruff check --fix .` to auto-fix lint/format issues",
            "4. Review the changes and commit",
            "5. Push to the existing branch — do NOT create a new PR",
            "6. Verify CI passes after your fix",
        ]
    elif failure_type == "test_failure":
        lines += [
            "3. Run the test suite locally to reproduce the failure",
            "4. Identify the root cause from the log excerpt below",
            "5. Fix the failing test(s) or the code they exercise",
            "6. Push to the existing branch — do NOT create a new PR",
            "7. Verify CI passes after your fix",
        ]
    elif failure_type == "type_error":
        lines += [
            "3. Run the type checker locally to reproduce the errors",
            "4. Fix the type errors identified in the log excerpt below",
            "5. Push to the existing branch — do NOT create a new PR",
            "6. Verify CI passes after your fix",
        ]
    else:
        lines += [
            "3. Read the log excerpt below and investigate the failure",
            "4. Reproduce the issue locally if possible",
            "5. Apply a targeted fix",
            "6. Push to the existing branch — do NOT create a new PR",
            "7. Verify CI passes after your fix",
        ]

    excerpt = failure_info.get("log_excerpt", "")
    if excerpt and excerpt != "(no logs retrieved)":
        lines += [
            "",
            "## CI Log Excerpt",
            "",
            "```",
            excerpt[:2000],
            "```",
        ]

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


async def _find_project_for_repo(
    conn,
    repo_full_name: str,
) -> str | None:
    """Find a project_id whose github_url matches the repository."""
    rows = await conn.fetch(
        "SELECT id, github_url FROM projects WHERE github_url != ''"
    )
    for row in rows:
        url = (row["github_url"] or "").rstrip("/").removesuffix(".git")
        if url.endswith(repo_full_name):
            return row["id"]
    return None


async def _find_task_for_pr(
    conn,
    pr_url: str,
) -> dict[str, Any] | None:
    """Find the most recent non-terminal task linked to a PR URL."""
    rows = await conn.fetch(
        """SELECT id, project_id, feature_id, branch, pr_url
           FROM tasks
           WHERE pr_url = $1
             AND status NOT IN ('done', 'cancelled')
           ORDER BY updated_at DESC
           LIMIT 1""",
        pr_url,
    )
    if rows:
        return dict(rows[0])
    return None


async def _ci_fix_task_exists(
    conn,
    pr_url: str,
    head_sha: str,
) -> bool:
    """Check if a pending CI fix task already exists for this PR+SHA."""
    rows = await conn.fetch(
        """SELECT 1 FROM tasks
           WHERE pr_url = $1
             AND task_type = 'fix'
             AND title ILIKE '%CI%'
             AND description ILIKE $2
             AND status NOT IN ('done', 'cancelled')
           LIMIT 1""",
        pr_url, f"%{head_sha[:12]}%",
    )
    return bool(rows)


# ---------------------------------------------------------------------------
# Core handler
# ---------------------------------------------------------------------------


async def _handle_ci_failure(
    delivery_id: str,
    payload: dict[str, Any],
    *,
    event_type: str,
) -> None:
    """Create a fix task for a CI failure event.

    Shared between ``check_run`` and ``check_suite`` handlers.
    """
    repo_info = payload.get("repository", {})
    repo: str = repo_info.get("full_name", "")

    if not repo:
        logger.debug("ci_failure: skipping event with no repository info")
        return

    # Extract failure details from the event payload.
    if event_type == "check_run":
        obj = payload.get("check_run", {})
    else:
        obj = payload.get("check_suite", {})

    head_sha: str = obj.get("head_sha", "")
    check_name: str = obj.get("name", "") if event_type == "check_run" else ""
    pull_requests: list[dict] = obj.get("pull_requests", [])

    pr_number: int | None = None
    pr_url: str = ""
    branch: str = ""
    if pull_requests:
        first_pr = pull_requests[0]
        pr_number = first_pr.get("number")
        branch = (first_pr.get("head") or {}).get("ref", "")
        pr_url = first_pr.get("html_url") or first_pr.get("url", "")
        # The pull_requests array in check events often has API URLs, not html.
        # Build html_url from repo if needed.
        if pr_number and not pr_url.startswith("https://github.com"):
            pr_url = f"https://github.com/{repo}/pull/{pr_number}"

    # Also try to get the branch from head_branch (check_suite)
    if not branch:
        branch = obj.get("head_branch", "")

    logger.info(
        "ci_failure: handling %s failure delivery=%s repo=%s sha=%s pr=%s branch=%s",
        event_type, delivery_id, repo, head_sha[:8] if head_sha else "?",
        pr_number, branch,
    )

    # We need a PR to attach the fix task to — skip branch-only pushes
    # that aren't associated with a PR.
    if not pr_number:
        logger.info(
            "ci_failure: delivery %s — no PR associated with %s failure on %s; "
            "skipping task creation",
            delivery_id, event_type, repo,
        )
        return

    # Classify the failure from the output text (check_run has output.text).
    output_text = ""
    if event_type == "check_run":
        output_text = (obj.get("output") or {}).get("text") or ""
        if not output_text:
            output_text = (obj.get("output") or {}).get("summary") or ""
    failure_info = classify_failure(output_text)

    logger.info(
        "ci_failure: classified as type=%s fixable=%s",
        failure_info["type"], failure_info["fixable"],
    )

    try:
        pool = get_pool()
        async with pool.acquire() as conn:
            # Find project by matching github_url to repo.
            project_id = await _find_project_for_repo(conn, repo)
            if not project_id:
                logger.info(
                    "ci_failure: delivery %s — no project found for repo=%s; "
                    "skipping task creation",
                    delivery_id, repo,
                )
                return

            # Find the original task linked to this PR.
            original_task = await _find_task_for_pr(conn, pr_url)
            feature_id = original_task["feature_id"] if original_task else None

            # Don't create duplicate fix tasks for the same PR+SHA.
            if await _ci_fix_task_exists(conn, pr_url, head_sha):
                logger.info(
                    "ci_failure: delivery %s — fix task already exists for "
                    "%s on sha %s; skipping",
                    delivery_id, pr_url, head_sha[:8],
                )
                return

            failure_type_label = {
                "lint_format": "lint/format",
                "type_error": "type check",
                "test_failure": "test",
                "unknown": "CI",
            }.get(failure_info["type"], "CI")

            title = f"Fix CI {failure_type_label} failure on PR #{pr_number}"
            if check_name:
                title += f" ({check_name})"
            if len(title) > 200:
                title = title[:197] + "..."

            description = _build_ci_fix_description(
                failure_info, repo, pr_number, pr_url, branch, head_sha, check_name,
            )
            now = datetime.now(UTC)

            new_task_id = await conn.fetchval(
                """INSERT INTO tasks
                       (project_id, feature_id, title, description, priority,
                        status, branch, pr_url, task_type, created_at, updated_at)
                   VALUES ($1, $2, $3, $4, 'high', 'todo', $5, $6, 'fix', $7, $8)
                   RETURNING id""",
                project_id,
                feature_id,
                title,
                description,
                branch,
                pr_url,
                now,
                now,
            )

            logger.info(
                "ci_failure: created fix task #%d for project=%s — %s",
                new_task_id, project_id, title,
            )

    except Exception:
        logger.exception(
            "ci_failure: delivery %s — error creating fix task for %s",
            delivery_id, pr_url,
        )


# ---------------------------------------------------------------------------
# Dispatcher-registered handlers
# ---------------------------------------------------------------------------


@register_handler("check_run")
async def handle_check_run(delivery_id: str, payload: dict[str, Any]) -> None:
    """Handle ``check_run`` webhook events.

    Only processes events where ``action == "completed"`` and
    ``check_run.conclusion == "failure"``.
    """
    action = payload.get("action", "")
    if action != "completed":
        return

    check_run = payload.get("check_run", {})
    if check_run.get("conclusion") != "failure":
        return

    await _handle_ci_failure(delivery_id, payload, event_type="check_run")


@register_handler("check_suite")
async def handle_check_suite(delivery_id: str, payload: dict[str, Any]) -> None:
    """Handle ``check_suite`` webhook events.

    Only processes events where ``action == "completed"`` and
    ``check_suite.conclusion == "failure"``.
    """
    action = payload.get("action", "")
    if action != "completed":
        return

    check_suite = payload.get("check_suite", {})
    if check_suite.get("conclusion") != "failure":
        return

    await _handle_ci_failure(delivery_id, payload, event_type="check_suite")
