"""Auto-merge handler for GitHub pull_request_review and check_suite events.

Registers handlers on the module-level ``dispatcher`` singleton.  Import this
module to activate the handlers — typically done in ``github_webhooks/__init__.py``.

Configuration is read from environment variables at call time so that changes
take effect without a restart:

``PEON_AUTO_MERGE_ENABLED``
    Set to ``"true"``, ``"1"``, or ``"yes"`` to enable auto-merge.
    Default: disabled.

``PEON_AUTO_MERGE_MIN_APPROVALS``
    Minimum number of approving reviews required before merging.
    Default: ``1``.

``PEON_AUTO_MERGE_METHOD``
    Merge strategy passed to ``gh pr merge``.
    One of ``merge``, ``squash``, or ``rebase``.
    Default: ``merge``.
"""

from __future__ import annotations

import asyncio
import logging
import os
from dataclasses import dataclass, field
from typing import Any

from peon_mcp.github_webhooks.dispatcher import dispatcher

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Internal state tracking
# ---------------------------------------------------------------------------


@dataclass
class _PRMergeState:
    """Tracks approval / check-suite state for a single open PR."""

    pr_number: int
    repo_full_name: str  # "owner/repo"
    pr_url: str          # HTML URL, e.g. https://github.com/owner/repo/pull/42
    head_sha: str = ""

    # Sets of reviewer logins
    approved_by: set[str] = field(default_factory=set)
    changes_requested_by: set[str] = field(default_factory=set)

    all_checks_passed: bool = False


class AutoMergeHandler:
    """Processes PR review and check-suite events to drive auto-merge.

    State is kept in memory; it is lost on process restart.  That is acceptable
    because GitHub will re-send events on reconnect and the handler will
    re-evaluate criteria on the next incoming event.
    """

    def __init__(self) -> None:
        # pr_url → _PRMergeState
        self._pr_states: dict[str, _PRMergeState] = {}
        # head_sha → list of pr_url (for look-ups from check_suite events)
        self._sha_to_prs: dict[str, list[str]] = {}

    # ------------------------------------------------------------------
    # Configuration helpers (read env at call-time)
    # ------------------------------------------------------------------

    @property
    def enabled(self) -> bool:
        return os.environ.get("PEON_AUTO_MERGE_ENABLED", "").lower() in ("1", "true", "yes")

    @property
    def min_approvals(self) -> int:
        try:
            return int(os.environ.get("PEON_AUTO_MERGE_MIN_APPROVALS", "1"))
        except ValueError:
            return 1

    @property
    def merge_method(self) -> str:
        method = os.environ.get("PEON_AUTO_MERGE_METHOD", "merge").lower()
        return method if method in ("merge", "squash", "rebase") else "merge"

    # ------------------------------------------------------------------
    # State helpers
    # ------------------------------------------------------------------

    def _get_or_create_state(
        self, pr_url: str, pr_number: int, repo_full_name: str
    ) -> _PRMergeState:
        if pr_url not in self._pr_states:
            self._pr_states[pr_url] = _PRMergeState(
                pr_number=pr_number,
                repo_full_name=repo_full_name,
                pr_url=pr_url,
            )
        return self._pr_states[pr_url]

    def _track_sha(self, head_sha: str, pr_url: str) -> None:
        """Register the SHA → PR URL mapping used by check_suite look-ups."""
        if head_sha and pr_url not in self._sha_to_prs.get(head_sha, []):
            self._sha_to_prs.setdefault(head_sha, []).append(pr_url)

    # ------------------------------------------------------------------
    # Event handlers
    # ------------------------------------------------------------------

    async def handle_pull_request_review(
        self, delivery_id: str, payload: dict[str, Any]
    ) -> None:
        """Handle ``pull_request_review`` events from GitHub."""
        if not self.enabled:
            return

        if payload.get("action") != "submitted":
            return

        review = payload.get("review", {})
        review_state = review.get("state", "").lower()
        reviewer = review.get("user", {}).get("login", "")

        pr = payload.get("pull_request", {})
        pr_number = pr.get("number")
        pr_url = pr.get("html_url", "")
        head_sha = pr.get("head", {}).get("sha", "")
        repo_full_name = payload.get("repository", {}).get("full_name", "")

        if not pr_number or not pr_url:
            logger.warning("auto_merge: pull_request_review missing PR info; skipping")
            return

        state = self._get_or_create_state(pr_url, pr_number, repo_full_name)
        if head_sha:
            state.head_sha = head_sha
            self._track_sha(head_sha, pr_url)

        if review_state == "approved":
            state.approved_by.add(reviewer)
            state.changes_requested_by.discard(reviewer)
            logger.info(
                "auto_merge: PR #%s approved by %s (%d total approvals)",
                pr_number,
                reviewer,
                len(state.approved_by),
            )
        elif review_state == "changes_requested":
            state.changes_requested_by.add(reviewer)
            state.approved_by.discard(reviewer)
            logger.info(
                "auto_merge: PR #%s has changes_requested from %s — blocked",
                pr_number,
                reviewer,
            )
        elif review_state == "dismissed":
            state.approved_by.discard(reviewer)
            state.changes_requested_by.discard(reviewer)

        await self._evaluate_and_merge(state)

    async def handle_check_suite(
        self, delivery_id: str, payload: dict[str, Any]
    ) -> None:
        """Handle ``check_suite`` events from GitHub."""
        if not self.enabled:
            return

        if payload.get("action") != "completed":
            return

        check_suite = payload.get("check_suite", {})
        conclusion = check_suite.get("conclusion", "")
        head_sha = check_suite.get("head_sha", "")
        repo_full_name = payload.get("repository", {}).get("full_name", "")

        if conclusion != "success":
            logger.info(
                "auto_merge: check_suite completed with %r for sha=%s — not merging",
                conclusion,
                head_sha,
            )
            return

        logger.info("auto_merge: check_suite SUCCESS for sha=%s", head_sha)

        # First, try to find already-tracked PRs for this SHA.
        known_pr_urls = list(self._sha_to_prs.get(head_sha, []))

        # Also inspect the pull_requests list embedded in the check_suite payload.
        for pr_info in check_suite.get("pull_requests", []):
            pr_number = pr_info.get("number")
            if not pr_number or not repo_full_name:
                continue
            html_pr_url = f"https://github.com/{repo_full_name}/pull/{pr_number}"
            state = self._get_or_create_state(html_pr_url, pr_number, repo_full_name)
            state.head_sha = head_sha
            self._track_sha(head_sha, html_pr_url)
            if html_pr_url not in known_pr_urls:
                known_pr_urls.append(html_pr_url)

        for pr_url in known_pr_urls:
            state = self._pr_states.get(pr_url)
            if state is None:
                continue
            state.all_checks_passed = True
            await self._evaluate_and_merge(state)

    # ------------------------------------------------------------------
    # Merge-criteria evaluation
    # ------------------------------------------------------------------

    def _meets_merge_criteria(self, state: _PRMergeState) -> bool:
        """Return True when the PR satisfies all configured merge gates."""
        if state.changes_requested_by:
            return False
        if len(state.approved_by) < self.min_approvals:
            return False
        if not state.all_checks_passed:
            return False
        return True

    async def _all_feature_tasks_complete(self, pr_url: str) -> tuple[bool, str]:
        """Check if all tasks in a feature linked to this PR are complete.

        Returns:
            (ok, reason) — ok is True if no feature is linked or all tasks are
            in a terminal state.  reason describes what's blocking if ok is False.
        """
        from peon_mcp.db import get_pool

        try:
            pool = get_pool()
            async with pool.acquire() as conn:
                # Find features linked to this PR URL
                features = await conn.fetch(
                    "SELECT id, name FROM features WHERE pr_url = $1",
                    pr_url,
                )
                if not features:
                    return True, ""

                for feature in features:
                    feature_id = feature["id"]
                    feature_name = feature["name"]
                    incomplete = await conn.fetch(
                        """SELECT id, title, status FROM tasks
                           WHERE feature_id = $1
                             AND status NOT IN ('done', 'cancelled')""",
                        feature_id,
                    )
                    if incomplete:
                        details = ", ".join(
                            f"#{r['id']} ({r['title']!r}: {r['status']})"
                            for r in incomplete
                        )
                        return False, (
                            f"Feature #{feature_id} ({feature_name!r}) has "
                            f"{len(incomplete)} incomplete task(s): {details}"
                        )
        except Exception:
            logger.exception(
                "auto_merge: failed to check feature tasks for PR %s — "
                "allowing merge to avoid permanent block",
                pr_url,
            )
        return True, ""

    async def _evaluate_and_merge(self, state: _PRMergeState) -> None:
        """Merge the PR if all criteria are met, then clean up state."""
        if not self._meets_merge_criteria(state):
            return

        # Gate: if PR is linked to a feature, all child tasks must be complete
        tasks_ok, block_reason = await self._all_feature_tasks_complete(state.pr_url)
        if not tasks_ok:
            logger.warning(
                "auto_merge: PR #%s in %s meets GitHub criteria but BLOCKED — %s",
                state.pr_number,
                state.repo_full_name,
                block_reason,
            )
            return

        logger.info(
            "auto_merge: PR #%s in %s meets all criteria "
            "(approvals=%d, checks=green, no blocks, tasks complete) — merging via gh CLI",
            state.pr_number,
            state.repo_full_name,
            len(state.approved_by),
        )

        success = await self._merge_pr(state)
        if success:
            # Remove from tracking to prevent a duplicate merge attempt.
            self._pr_states.pop(state.pr_url, None)
            await self._update_linked_tasks(state.pr_url)

    # ------------------------------------------------------------------
    # Re-evaluation trigger (called from task routes)
    # ------------------------------------------------------------------

    async def re_evaluate_for_pr(self, pr_url: str) -> None:
        """Re-evaluate merge criteria for a tracked PR.

        Called when an external event (e.g. all feature tasks completing)
        may have unblocked a previously-blocked auto-merge.  No-ops if the
        PR is not being tracked or criteria are still not met.
        """
        state = self._pr_states.get(pr_url)
        if state is None:
            return
        logger.info(
            "auto_merge: re-evaluating PR #%s after task completion",
            state.pr_number,
        )
        await self._evaluate_and_merge(state)

    # ------------------------------------------------------------------
    # GitHub merge via gh CLI
    # ------------------------------------------------------------------

    async def _merge_pr(self, state: _PRMergeState) -> bool:
        """Invoke ``gh pr merge`` to merge the PR.  Returns True on success."""
        method_flag = f"--{self.merge_method}"
        try:
            proc = await asyncio.create_subprocess_exec(
                "gh",
                "pr",
                "merge",
                str(state.pr_number),
                method_flag,
                "--repo",
                state.repo_full_name,
                "--delete-branch",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await proc.communicate()

            if proc.returncode != 0:
                logger.error(
                    "auto_merge: failed to merge PR #%s in %s: %s",
                    state.pr_number,
                    state.repo_full_name,
                    stderr.decode().strip(),
                )
                return False

            logger.info(
                "auto_merge: PR #%s in %s merged successfully (%s)",
                state.pr_number,
                state.repo_full_name,
                self.merge_method,
            )
            return True

        except FileNotFoundError:
            logger.error(
                "auto_merge: gh CLI not found — cannot merge PR #%s. "
                "Install the GitHub CLI: https://cli.github.com/",
                state.pr_number,
            )
            return False
        except Exception:
            logger.exception(
                "auto_merge: unexpected error merging PR #%s in %s",
                state.pr_number,
                state.repo_full_name,
            )
            return False

    # ------------------------------------------------------------------
    # Linked-task update
    # ------------------------------------------------------------------

    async def _update_linked_tasks(self, pr_url: str) -> None:
        """Mark Peon tasks linked to the merged PR as ready-to-deploy.

        Tasks are linked either via their ``pr_url`` field directly, or via a
        feature that has the matching ``pr_url``.
        """
        from peon_mcp.db import get_pool

        try:
            pool = get_pool()
            async with pool.acquire() as conn:
                # Tasks linked via a feature's PR URL.
                features = await conn.fetch(
                    "SELECT id FROM features WHERE pr_url = $1",
                    pr_url,
                )

                for feature in features:
                    feature_id = feature["id"]
                    tasks = await conn.fetch(
                        """SELECT id FROM tasks
                           WHERE feature_id = $1
                             AND status NOT IN ('done', 'cancelled', 'ready-to-deploy')""",
                        feature_id,
                    )
                    for task in tasks:
                        task_id = task["id"]
                        await conn.execute(
                            """UPDATE tasks
                               SET status = 'ready-to-deploy', updated_at = CURRENT_TIMESTAMP
                               WHERE id = $1""",
                            task_id,
                        )
                        logger.info(
                            "auto_merge: marked task #%d as ready-to-deploy (PR merged via feature)",
                            task_id,
                        )

                # Tasks linked directly by their own pr_url.
                tasks = await conn.fetch(
                    """SELECT id FROM tasks
                       WHERE pr_url = $1
                         AND status NOT IN ('done', 'cancelled', 'ready-to-deploy')""",
                    pr_url,
                )
                for task in tasks:
                    task_id = task["id"]
                    await conn.execute(
                        """UPDATE tasks
                           SET status = 'ready-to-deploy', updated_at = CURRENT_TIMESTAMP
                           WHERE id = $1""",
                        task_id,
                    )
                    logger.info(
                        "auto_merge: marked task #%d as ready-to-deploy (PR merged, direct link)",
                        task_id,
                    )

        except Exception:
            logger.exception(
                "auto_merge: failed to update linked tasks for PR %s", pr_url
            )


# ---------------------------------------------------------------------------
# Module-level singleton and dispatcher registration
# ---------------------------------------------------------------------------

auto_merge_handler = AutoMergeHandler()


@dispatcher.register_handler("pull_request_review")
async def _on_pull_request_review(delivery_id: str, payload: dict[str, Any]) -> None:
    await auto_merge_handler.handle_pull_request_review(delivery_id, payload)


@dispatcher.register_handler("check_suite")
async def _on_check_suite(delivery_id: str, payload: dict[str, Any]) -> None:
    await auto_merge_handler.handle_check_suite(delivery_id, payload)
