#!/usr/bin/env python
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "httpx",
# ]
# ///
"""
Auto-close issues that need MRE (Minimal Reproducible Example).

This script runs on a schedule to automatically close issues that have been
marked as "needs MRE" and haven't received activity from the issue author
within 7 days.
"""

import os
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

import httpx

MAINTAINER_AUTHOR_ASSOCIATIONS = {"OWNER", "MEMBER", "COLLABORATOR"}


@dataclass
class Issue:
    """Represents a GitHub issue."""

    number: int
    title: str
    state: str
    created_at: str
    user_id: int
    user_login: str
    body: str | None
    author_association: str


@dataclass
class Comment:
    """Represents a GitHub comment."""

    id: int
    body: str
    created_at: str
    user_id: int
    user_login: str


@dataclass
class Event:
    """Represents a GitHub issue event."""

    event: str
    created_at: str
    label_name: str | None


class GitHubClient:
    """Client for interacting with GitHub API."""

    def __init__(self, token: str, owner: str, repo: str):
        self.token = token
        self.owner = owner
        self.repo = repo
        self.headers = {
            "Authorization": f"token {token}",
            "Accept": "application/vnd.github.v3+json",
        }
        self.base_url = f"https://api.github.com/repos/{owner}/{repo}"

    def get_issues_with_label(
        self, label: str, page: int = 1, per_page: int = 100
    ) -> list[Issue]:
        """Fetch open issues with a specific label."""
        url = f"{self.base_url}/issues"
        issues = []

        with httpx.Client() as client:
            response = client.get(
                url,
                headers=self.headers,
                params={
                    "state": "open",
                    "labels": label,
                    "per_page": per_page,
                    "page": page,
                },
            )

            if response.status_code != 200:
                print(f"Error fetching issues: {response.status_code}")
                return issues

            data = response.json()
            for item in data:
                # Skip pull requests
                if "pull_request" in item:
                    continue

                issues.append(
                    Issue(
                        number=item["number"],
                        title=item["title"],
                        state=item["state"],
                        created_at=item["created_at"],
                        user_id=item["user"]["id"],
                        user_login=item["user"]["login"],
                        body=item.get("body"),
                        author_association=item.get("author_association", "NONE"),
                    )
                )

        return issues

    def get_issue_events(self, issue_number: int) -> list[Event]:
        """Fetch all events for an issue."""
        url = f"{self.base_url}/issues/{issue_number}/events"
        events = []

        with httpx.Client() as client:
            page = 1
            while True:
                response = client.get(
                    url, headers=self.headers, params={"page": page, "per_page": 100}
                )

                if response.status_code != 200:
                    break

                data = response.json()
                if not data:
                    break

                for event_data in data:
                    label_name = None
                    if event_data["event"] == "labeled" and "label" in event_data:
                        label_name = event_data["label"]["name"]

                    events.append(
                        Event(
                            event=event_data["event"],
                            created_at=event_data["created_at"],
                            label_name=label_name,
                        )
                    )

                page += 1
                if page > 10:  # Safety limit
                    break

        return events

    def get_issue_comments(self, issue_number: int) -> list[Comment]:
        """Fetch all comments for an issue."""
        url = f"{self.base_url}/issues/{issue_number}/comments"
        comments = []

        with httpx.Client() as client:
            page = 1
            while True:
                response = client.get(
                    url, headers=self.headers, params={"page": page, "per_page": 100}
                )

                if response.status_code != 200:
                    break

                data = response.json()
                if not data:
                    break

                for comment_data in data:
                    comments.append(
                        Comment(
                            id=comment_data["id"],
                            body=comment_data["body"],
                            created_at=comment_data["created_at"],
                            user_id=comment_data["user"]["id"],
                            user_login=comment_data["user"]["login"],
                        )
                    )

                page += 1
                if page > 10:  # Safety limit
                    break

        return comments

    def get_issue_timeline(self, issue_number: int) -> list[dict]:
        """Fetch timeline events for an issue (includes issue edits)."""
        url = f"{self.base_url}/issues/{issue_number}/timeline"
        timeline = []

        with httpx.Client() as client:
            page = 1
            while True:
                response = client.get(
                    url,
                    headers={
                        **self.headers,
                        "Accept": "application/vnd.github.mockingbird-preview+json",
                    },
                    params={"page": page, "per_page": 100},
                )

                if response.status_code != 200:
                    break

                data = response.json()
                if not data:
                    break

                timeline.extend(data)

                page += 1
                if page > 10:  # Safety limit
                    break

        return timeline

    def close_issue(self, issue_number: int, comment: str) -> tuple[bool, bool]:
        """Close an issue with a comment.

        Closes first, then comments — so a failed comment never leaves
        a misleading "closing" notice on a still-open issue.

        Returns (closed, commented) so the caller can log partial failures.
        """
        # Close the issue first
        issue_url = f"{self.base_url}/issues/{issue_number}"
        with httpx.Client() as client:
            response = client.patch(
                issue_url, headers=self.headers, json={"state": "closed"}
            )

            if response.status_code != 200:
                print(
                    f"Failed to close issue #{issue_number}: "
                    f"{response.status_code} {response.text}"
                )
                return False, False

        # Then add the comment
        comment_url = f"{self.base_url}/issues/{issue_number}/comments"
        with httpx.Client() as client:
            response = client.post(
                comment_url, headers=self.headers, json={"body": comment}
            )

            if response.status_code != 201:
                print(
                    f"Issue #{issue_number} was closed but comment failed: "
                    f"{response.status_code} {response.text}"
                )
                return True, False

        return True, True


def find_label_application_date(
    events: list[Event], label_name: str
) -> datetime | None:
    """Find when a specific label was applied to an issue."""
    # Look for the most recent application of this label
    for event in reversed(events):
        if event.event == "labeled" and event.label_name == label_name:
            return datetime.fromisoformat(event.created_at.replace("Z", "+00:00"))
    return None


def has_author_activity_after(
    issue: Issue,
    comments: list[Comment],
    timeline: list[dict],
    after_date: datetime,
) -> bool:
    """Check if the issue author had any activity after a specific date."""
    # Check for comments from author
    for comment in comments:
        if comment.user_id == issue.user_id:
            comment_date = datetime.fromisoformat(
                comment.created_at.replace("Z", "+00:00")
            )
            if comment_date > after_date:
                print(
                    f"Issue #{issue.number}: Author commented after label application"
                )
                return True

    # Check for issue body edits from author
    for event in timeline:
        if event.get("event") == "renamed" or event.get("event") == "edited":
            if event.get("actor", {}).get("id") == issue.user_id:
                event_date = datetime.fromisoformat(
                    event["created_at"].replace("Z", "+00:00")
                )
                if event_date > after_date:
                    print(
                        f"Issue #{issue.number}: Author edited issue after label application"
                    )
                    return True

    return False


def should_close_as_needs_mre(
    issue: Issue,
    label_date: datetime,
    comments: list[Comment],
    timeline: list[dict],
) -> bool:
    """Determine if an issue should be closed for needing an MRE."""
    if issue.author_association in MAINTAINER_AUTHOR_ASSOCIATIONS:
        print(f"Issue #{issue.number}: Skipping maintainer-authored issue")
        return False

    # Check if label is old enough (7 days)
    seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)

    if label_date > seven_days_ago:
        return False

    # Check for author activity after the label was applied
    if has_author_activity_after(issue, comments, timeline, label_date):
        return False

    return True


def main():
    """Main entry point for auto-closing needs MRE issues."""
    print("[DEBUG] Starting auto-close needs MRE script")

    # Get environment variables
    token = os.environ.get("GITHUB_TOKEN")
    if not token:
        raise ValueError("GITHUB_TOKEN environment variable is required")

    owner = os.environ.get("GITHUB_REPOSITORY_OWNER", "prefecthq")
    repo = os.environ.get("GITHUB_REPOSITORY_NAME", "fastmcp")

    print(f"[DEBUG] Repository: {owner}/{repo}")

    # Initialize client
    client = GitHubClient(token, owner, repo)

    # Get issues with "needs MRE" label
    all_issues = []
    page = 1

    while page <= 20:  # Safety limit
        issues = client.get_issues_with_label("needs MRE", page=page)
        if not issues:
            break
        all_issues.extend(issues)
        page += 1

    print(f"[DEBUG] Found {len(all_issues)} open issues with 'needs MRE' label")

    processed_count = 0
    closed_count = 0

    for issue in all_issues:
        processed_count += 1

        if processed_count % 10 == 0:
            print(f"[DEBUG] Processed {processed_count}/{len(all_issues)} issues")

        # Get events to find when label was applied
        events = client.get_issue_events(issue.number)
        label_date = find_label_application_date(events, "needs MRE")

        if not label_date:
            print(
                f"[DEBUG] Issue #{issue.number}: Could not find label application date"
            )
            continue

        print(
            f"[DEBUG] Issue #{issue.number}: Label applied on {label_date.isoformat()}"
        )

        # Get comments and timeline
        comments = client.get_issue_comments(issue.number)
        timeline = client.get_issue_timeline(issue.number)

        # Check if we should close
        if should_close_as_needs_mre(issue, label_date, comments, timeline):
            close_message = (
                "This issue is being automatically closed because we requested a minimal reproducible example (MRE) "
                "7 days ago and haven't received a response from the issue author.\n\n"
                "**If you can provide an MRE**, please add it as a comment and we'll reopen this issue. "
                "An MRE should be a complete, runnable code snippet that demonstrates the problem.\n\n"
                "**If this was closed in error**, please leave a comment explaining the situation and we'll reopen it."
            )

            closed, commented = client.close_issue(issue.number, close_message)
            if closed:
                closed_count += 1
                if commented:
                    print(f"[SUCCESS] Closed issue #{issue.number} (needs MRE)")
                else:
                    print(
                        f"[WARNING] Closed issue #{issue.number} but "
                        f"comment was not posted"
                    )
            else:
                print(f"[ERROR] Failed to close issue #{issue.number}")

    print(f"[DEBUG] Processing complete. Closed {closed_count} issues needing MRE")


if __name__ == "__main__":
    main()
