"""GitHub repository handler."""

import asyncio
import base64
import os
from datetime import datetime
from typing import Any, Self

from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

from code_reviewer.errors import AuthenticationFailed, InvalidResponse, NetworkError

from ..._cli import CLIClient
from ..._http import HttpClient, PaginationHelper
from ..._transport import APIClient
from ...logging import PlatformLogger
from ..gitlab.schema import GitLabReviewComment
from ..handler import RepositoryHandler
from ..schema import (
    BasicReviewOutput,
    DiscussionItem,
    DiscussionItemType,
    DiscussionNote,
    MergeRequest,
)
from .schema import GitHubPullRequest, GitHubReviewComment

# Module-level platform logger for GitHub adaptor
logger = PlatformLogger("github")


class GitHubRepositoryHandler(RepositoryHandler):
    """GitHub repository handler."""

    def __init__(self, api_url: str, client: APIClient):
        """
        Initialize GitHub repository handler.

        Args:
            api_url: GitHub API base URL (e.g., "https://api.github.com")
            client: API client (HttpClient or CLIClient)
        """
        self.api_url = api_url
        self.http = client

    @classmethod
    def from_token(cls, api_url: str, token: str) -> Self:
        """Create handler with HTTP token auth."""
        client = HttpClient(token=token, platform="GitHub", auth_header="Authorization")
        return cls(api_url, client)

    @classmethod
    def from_cli(cls, api_url: str = "https://api.github.com") -> Self:
        """Create handler using gh CLI for auth."""
        client = CLIClient(binary="gh", api_url=api_url)
        return cls(api_url, client)

    @classmethod
    def from_env(cls) -> Self:
        """
        Create GitHub repository handler from environment variables.

        Requires GITHUB_API_TOKEN or GITHUB_TOKEN environment variable.
        Optionally uses GITHUB_API_URL (defaults to https://api.github.com).

        Returns:
            GitHubRepositoryHandler instance

        Raises:
            AuthenticationFailed: If required environment variables are missing
        """
        api_url = os.getenv("GITHUB_API_URL", "https://api.github.com")
        token = os.getenv("GITHUB_API_TOKEN") or os.getenv("GITHUB_TOKEN")

        if not token:
            raise AuthenticationFailed(
                "Missing GITHUB_API_TOKEN or GITHUB_TOKEN environment variable"
            )

        return cls.from_token(api_url, token)

    def _headers(self) -> dict[str, str]:
        """Get common request headers (non-auth)."""
        return {
            "User-Agent": "Reviewate",
            "Accept": "application/vnd.github.v3+json",
        }

    @retry(
        retry=retry_if_exception_type((NetworkError, InvalidResponse)),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=10, min=1, max=100),
        reraise=True,
    )
    async def fetch_merge_request(self, repo: str, merge_id: str) -> MergeRequest:
        """Fetch GitHub pull request information.

        Retries on network errors with exponential backoff: 1s, 10s, 100s (max 3 attempts).
        """
        logger.info(f"Fetching GitHub PR {repo}#{merge_id}")
        # Fetch PR details
        response = await self.http.get(
            f"{self.api_url}/repos/{repo}/pulls/{merge_id}",
            headers=self._headers(),
        )
        pr_data = response.json()
        github_pr = GitHubPullRequest.model_validate(pr_data)

        # Fetch raw diff using GitHub API with Accept header for diff format
        diff_headers = self._headers()
        diff_headers["Accept"] = "application/vnd.github.v3.diff"

        diff_response = await self.http.get(
            f"{self.api_url}/repos/{repo}/pulls/{merge_id}",
            headers=diff_headers,
        )

        # Convert to platform-agnostic MergeRequest
        return MergeRequest(
            id=str(github_pr.number),
            repo=repo,
            title=github_pr.title,
            description=github_pr.body or "",
            source_branch=github_pr.head["ref"],
            target_branch=github_pr.base["ref"],
            head_sha=github_pr.head["sha"],
            diffs=diff_response.text,
        )

    async def _fetch_comments_page(
        self, repo: str, merge_id: str, page: int
    ) -> list[dict[str, Any]]:
        """Fetch a single page of issue comments."""
        response = await self.http.get(
            f"{self.api_url}/repos/{repo}/issues/{merge_id}/comments",
            headers=self._headers(),
            params={"page": page, "per_page": 100},
        )
        return response.json()

    async def _fetch_review_comments_page(
        self, repo: str, merge_id: str, page: int
    ) -> list[dict[str, Any]]:
        """Fetch a single page of review comments."""
        response = await self.http.get(
            f"{self.api_url}/repos/{repo}/pulls/{merge_id}/comments",
            headers=self._headers(),
            params={"page": page, "per_page": 100},
        )
        return response.json()

    def _parse_discussions(
        self,
        comments: list[dict[str, Any]],
        review_comments: list[dict[str, Any]],
    ) -> list[DiscussionItem]:
        """Parse raw API responses into platform-agnostic DiscussionItems."""
        items: list[DiscussionItem] = []

        # Process issue comments (filter out bot comments)
        for comment in comments:
            body = comment.get("body")
            updated_at = comment.get("updated_at")
            user = comment.get("user", {})

            # Filter out bots
            if user.get("type") == "Bot":
                continue

            if body and updated_at:
                try:
                    dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
                    items.append(
                        DiscussionItem(
                            type=DiscussionItemType.COMMENT,
                            body=body,
                            updated_at=dt,
                        )
                    )
                except ValueError:
                    continue

        # Process review comments (these are inline comments, treat as threads)
        for comment in review_comments:
            body = comment.get("body")
            updated_at = comment.get("updated_at")
            user = comment.get("user", {})

            # Filter out bots
            if user.get("type") == "Bot":
                continue

            if body and updated_at:
                try:
                    dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
                    # For GitHub, review comments are individual items
                    # In a more complete implementation, we'd group by thread
                    items.append(
                        DiscussionItem(
                            type=DiscussionItemType.THREAD,
                            notes=[DiscussionNote(body=body, updated_at=dt)],
                            updated_at=dt,
                        )
                    )
                except ValueError:
                    continue

        # Sort by updated_at descending
        items.sort(key=lambda x: x.get_updated_at(), reverse=True)

        return items

    @retry(
        retry=retry_if_exception_type((NetworkError, InvalidResponse)),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=10, min=1, max=100),
        reraise=True,
    )
    async def fetch_discussions(self, repo: str, merge_id: str) -> list[DiscussionItem]:
        """Fetch all discussions for a GitHub pull request.

        Retries on network errors with exponential backoff: 1s, 10s, 100s (max 3 attempts).
        """
        logger.info(f"Fetching discussions for GitHub PR {repo}#{merge_id}")
        # Fetch all pages using pagination helper
        all_comments = await PaginationHelper.fetch_all_pages(
            self._fetch_comments_page, repo, merge_id
        )
        all_review_comments = await PaginationHelper.fetch_all_pages(
            self._fetch_review_comments_page, repo, merge_id
        )

        # Parse all discussions
        return self._parse_discussions(all_comments, all_review_comments)

    def _parse_datetime(self, dt_str: str) -> datetime:
        """Parse ISO datetime string."""
        try:
            return datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
        except ValueError as e:
            raise InvalidResponse(f"Invalid date format: {e}") from e

    async def post_comment(self, repo: str, merge_id: str, content: str) -> None:
        """Post a comment on a GitHub pull request."""
        await self.http.post(
            f"{self.api_url}/repos/{repo}/issues/{merge_id}/comments",
            headers=self._headers(),
            json={"body": content},
        )

    async def post_review(
        self,
        merge_request: MergeRequest,
        reviews: list[GitHubReviewComment | GitLabReviewComment | BasicReviewOutput],
    ) -> tuple[int, int]:
        """Post review comments on a GitHub pull request.

        Posts individual inline comments on the pull request.
        GitHub API: POST /repos/{owner}/{repo}/pulls/{pull_number}/comments

        Note: This method is resilient to failures. If a comment fails to post
        (e.g., due to invalid line numbers from LLM), it logs the error and continues.

        Args:
            merge_request: The merge request being reviewed
            reviews: List of review comments to post
        """
        # Extract required data from merge_request
        repo = merge_request.repo
        merge_id = merge_request.id
        commit_id = merge_request.head_sha

        logger.info(f"Posting {len(reviews)} review comments to GitHub PR {repo}#{merge_id}")

        # Track success/failure counts
        successful = 0
        failed = 0

        # Post each review comment individually
        for idx, review in enumerate(reviews, 1):
            # Check if this is a GitHubReviewComment with path data
            path = getattr(review, "path", None)
            if path is None:
                endpoint_url = f"{self.api_url}/repos/{repo}/issues/{merge_id}/comments"

                # If no path and line are specified, post a general comment
                comment_data: dict[str, str | int | None] = {
                    "body": review.body,
                }

            else:
                endpoint_url = f"{self.api_url}/repos/{repo}/pulls/{merge_id}/comments"
                # Otherwise, post an inline comment
                side = getattr(review, "side", None)
                line = getattr(review, "line", None)
                comment_data = {
                    "body": review.body,
                    "commit_id": commit_id,
                    "path": path,
                    "side": side.upper() if side else "RIGHT",
                }
                if line is not None:
                    comment_data["line"] = line
                else:
                    # File-level comment when no line number is available
                    comment_data["subject_type"] = "file"

            try:
                await self.http.post(
                    endpoint_url,
                    headers=self._headers(),
                    json=comment_data,
                )
                successful += 1
                logger.info(f"Successfully posted comment {idx}/{len(reviews)}")
                logger.debug(f"Successfully posted comment {idx}/{len(reviews)}")

                # Small delay between comments to avoid GitHub rate limiting
                # ("was submitted too quickly" error)
                if idx < len(reviews):
                    await asyncio.sleep(0.5)
            except Exception as e:
                # Log error but don't fail the entire review
                # This is especially important when LLMs get line numbers wrong
                failed += 1
                logger.warning(
                    f"Failed to post comment {idx}/{len(reviews)}: {e}\n"
                    f"Comment data: {comment_data}"
                )

        # Log final summary
        if failed > 0:
            logger.warning(
                f"Posted {successful}/{len(reviews)} comments successfully, "
                f"{failed} failed (likely due to invalid line numbers)"
            )
        else:
            logger.info(f"Successfully posted all {successful} comments")

        return successful, failed

    def platform_name(self) -> str:
        """Get platform name."""
        return "GitHub"

    @staticmethod
    def output_format() -> str:
        """Get GitHub-specific output format instructions."""
        return """<output_format>
Required Output Format:
You must return a JSON object with a "reviews" array containing inline code review comments.

Structure:
{
  "reviews": [
    {
      "path": "file/path/in/repo",
      "body": "Your detailed comment in Markdown",
      "line": 42,
      "side": "RIGHT",
    }
  ]
}

Field Requirements:
- "path": Relative file path from the diff header (e.g., "src/main.rs"). REQUIRED.
- "body": Your detailed comment in Markdown format
- "line": Line number — read it directly from the diff (added lines: "      N   :+", deleted lines: "N         :-"). REQUIRED, never null.
- "side": "RIGHT" for added/modified lines (+), "LEFT" for deleted lines (-)

Note: Commit SHA will be added automatically by the system.
</output_format>"""

    @retry(
        retry=retry_if_exception_type((NetworkError, InvalidResponse)),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=10, min=1, max=100),
        reraise=True,
    )
    async def fetch_archive(self, repo: str, ref: str) -> bytes:
        """Download repository archive as a ZIP file.

        GitHub API returns a redirect to the archive download URL.
        The httpx client is configured to follow redirects.

        Args:
            repo: Repository identifier (e.g., "owner/repo")
            ref: Git reference (branch, tag, or commit SHA)

        Returns:
            ZIP file content as bytes
        """
        logger.info(f"Fetching archive for {repo} at ref {ref}")
        response = await self.http.get(
            f"{self.api_url}/repos/{repo}/zipball/{ref}",
            headers=self._headers(),
            timeout=120,
        )
        response.raise_for_status()
        return response.content

    async def fetch_file(self, repo: str, ref: str, path: str) -> str | None:
        """Fetch a file from the repository.

        GitHub API returns file content as base64 encoded string.
        Returns None if file not found (404).

        Args:
            repo: Repository identifier (e.g., "owner/repo")
            ref: Git reference (branch, tag, or commit SHA)
            path: Path to the file in the repository

        Returns:
            File content as string, or None if file not found
        """
        logger.info(f"Fetching file {path} from {repo} at ref {ref}")
        try:
            response = await self.http.get(
                f"{self.api_url}/repos/{repo}/contents/{path}",
                headers=self._headers(),
                params={"ref": ref},
            )
            data = response.json()
            # GitHub returns base64 encoded content
            content = data.get("content", "")
            if content:
                # Remove newlines that GitHub adds in base64 content
                return base64.b64decode(content.replace("\n", "")).decode("utf-8")
            return None
        except Exception as e:
            # File not found or other error
            logger.debug(f"Failed to fetch file {path}: {e}")
            return None

    async def close(self) -> None:
        """Close the API client."""
        await self.http.close()
