"""GitLab repository handler."""

import os
from datetime import datetime
from typing import Any, Self
from urllib.parse import quote

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 ..github.schema import GitHubReviewComment
from ..handler import RepositoryHandler
from ..schema import (
    BasicReviewOutput,
    DiscussionItem,
    DiscussionItemType,
    DiscussionNote,
    MergeRequest,
)
from .schema import GitLabMergeRequest, GitLabReviewComment

logger = PlatformLogger("gitlab")


class GitLabRepositoryHandler(RepositoryHandler):
    """GitLab repository handler."""

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

        Args:
            api_url: GitLab API base URL (e.g., "https://gitlab.com/api/v4")
            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="GitLab", auth_header="PRIVATE-TOKEN")
        return cls(api_url, client)

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

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

        Requires GITLAB_API_TOKEN or GITLAB_TOKEN environment variable.
        Optionally uses GITLAB_API_URL (defaults to https://gitlab.com/api/v4).

        Returns:
            GitLabRepositoryHandler instance

        Raises:
            AuthenticationFailed: If required environment variables are missing
        """
        api_url = os.getenv("GITLAB_API_URL", "https://gitlab.com/api/v4")
        token = os.getenv("GITLAB_API_TOKEN") or os.getenv("GITLAB_TOKEN")

        if not token:
            raise AuthenticationFailed(
                "Missing GITLAB_API_TOKEN or GITLAB_TOKEN environment variable"
            )

        return cls.from_token(api_url, token)

    def _headers(self) -> dict[str, str]:
        """Get common request headers (non-auth)."""
        return {}

    @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 GitLab merge request information.

        Retries on network errors with exponential backoff: 1s, 10s, 100s (max 3 attempts).
        """
        logger.info(f"Fetching GitLab MR {repo}!{merge_id}")
        # Fetch MR details
        response = await self.http.get(
            f"{self.api_url}/projects/{quote(repo, safe='')}/merge_requests/{merge_id}",
            headers=self._headers(),
        )
        mr_data = response.json()
        gitlab_mr = GitLabMergeRequest.model_validate(mr_data)

        # Fetch raw diffs
        diff_response = await self.http.get(
            f"{self.api_url}/projects/{quote(repo, safe='')}/merge_requests/{merge_id}/diffs",
            headers=self._headers(),
            params={"unidiff": "true"},
        )
        diffs_data = diff_response.json()

        # Combine diffs into unified diff string
        raw_diff = "\n".join(diff_obj.get("diff", "") for diff_obj in diffs_data)

        # Extract base_sha from diff_refs if available
        diff_refs = mr_data.get("diff_refs", {})
        base_sha = diff_refs.get("base_sha")

        # Convert to platform-agnostic MergeRequest
        return MergeRequest(
            id=str(gitlab_mr.iid),
            repo=repo,
            title=gitlab_mr.title,
            description=gitlab_mr.description or "",
            source_branch=gitlab_mr.source_branch,
            target_branch=gitlab_mr.target_branch,
            head_sha=gitlab_mr.sha,
            base_sha=base_sha,
            diffs=raw_diff,
        )

    async def _fetch_discussions_page(
        self, repo: str, merge_id: str, page: int
    ) -> tuple[list[dict[str, Any]], bool]:
        """
        Fetch a single page of discussions.

        Returns:
            Tuple of (discussions, has_next_page)
        """
        response = await self.http.get(
            f"{self.api_url}/projects/{quote(repo, safe='')}/merge_requests/{merge_id}/discussions",
            headers=self._headers(),
            params={"page": page, "per_page": 100},
        )

        # Check for pagination in headers (GitLab uses x-next-page header)
        has_next_page = bool(response.headers.get("x-next-page", ""))

        return response.json(), has_next_page

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

        for discussion in all_discussions:
            notes = discussion.get("notes", [])

            # Filter out system notes
            human_notes = [note for note in notes if not note.get("system", False)]

            if not human_notes:
                continue

            # If only one note, treat as comment
            if len(human_notes) == 1:
                body = human_notes[0].get("body")
                updated_at = human_notes[0].get("updated_at")

                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

            else:
                # Multiple notes, treat as thread
                thread_notes: list[DiscussionNote] = []
                last_updated = None

                for note in human_notes:
                    body = note.get("body")
                    updated_at = note.get("updated_at")

                    if body and updated_at:
                        try:
                            dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
                            thread_notes.append(DiscussionNote(body=body, updated_at=dt))

                            # Track the latest update time
                            if last_updated is None or dt > last_updated:
                                last_updated = dt
                        except ValueError:
                            continue

                if thread_notes and last_updated:
                    items.append(
                        DiscussionItem(
                            type=DiscussionItemType.THREAD,
                            notes=thread_notes,
                            updated_at=last_updated,
                        )
                    )

        # 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 GitLab merge request.

        Retries on network errors with exponential backoff: 1s, 10s, 100s (max 3 attempts).
        """
        logger.info(f"Fetching discussions for GitLab MR {repo}!{merge_id}")
        # Fetch all pages using pagination helper
        all_discussions = await PaginationHelper.fetch_all_pages(
            self._fetch_discussions_page, repo, merge_id, use_header_pagination=True
        )

        # Parse all discussions
        return self._parse_discussions(all_discussions)

    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 GitLab merge request."""
        await self.http.post(
            f"{self.api_url}/projects/{quote(repo, safe='')}/merge_requests/{merge_id}/notes",
            headers=self._headers(),
            json={"body": content},
        )

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

        Creates discussion threads with inline comments on the merge request.
        GitLab API: POST /projects/{id}/merge_requests/{merge_request_iid}/discussions

        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 (SHAs will be added automatically)
        """
        # Extract required data from merge_request
        repo = merge_request.repo
        merge_id = merge_request.id
        base_sha = merge_request.base_sha or merge_request.head_sha
        head_sha = merge_request.head_sha

        logger.info(f"Posting {len(reviews)} review comments to GitLab MR {repo}!{merge_id}")

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

        # Post each review as a discussion thread
        for idx, review in enumerate(reviews, 1):
            position = None
            # Check if this is a GitLabReviewComment with position data
            new_path = getattr(review, "new_path", None)
            if new_path is not None:
                # Build position object with SHAs from merge_request
                position = {
                    "base_sha": base_sha,
                    "start_sha": base_sha,
                    "head_sha": head_sha,
                    "position_type": "text",
                    "new_path": new_path,
                    "new_line": getattr(review, "new_line", None),
                }

                # Add optional old_path and old_line if present
                old_path = getattr(review, "old_path", None)
                old_line = getattr(review, "old_line", None)
                if old_path is not None:
                    position["old_path"] = old_path
                if old_line is not None:
                    position["old_line"] = old_line

            discussion_data = {
                "body": review.body,
                "position": position,
            }

            try:
                await self.http.post(
                    f"{self.api_url}/projects/{quote(repo, safe='')}/merge_requests/{merge_id}/discussions",
                    headers=self._headers(),
                    json=discussion_data,
                )
                successful += 1
                logger.debug(f"Successfully posted comment {idx}/{len(reviews)}")
            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"Discussion data: {discussion_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 "GitLab"

    @staticmethod
    def output_format() -> str:
        """Get GitLab-specific output format instructions."""
        return """<output_format>
Required Output Format:
You must return a JSON object with a "reviews" array containing GitLab discussion threads.

Structure:
{
  "reviews": [
    {
      "body": "Your review comment in Markdown format",
      "new_path": "relative/path/to/file.rs",
      "new_line": 42,
      "old_path": "relative/path/to/old_file.rs" // optional, use dev/null if not applicable
      "old_line": null // new_line is almost always required for additions/modifications
    }
  ]
}

Field Requirements:
- "body": Your detailed comment in Markdown format
- "new_path": Relative file path from the diff header. REQUIRED.
- "new_line": Line number — read it from the diff (added lines: "      N   :+", use N). REQUIRED for added/modified lines, never null.
- "old_path": (Optional) Old path if file was renamed
- "old_line": (Optional) Line number for deleted lines (from diff: "N         :-", use N)

Note: SHA values and position metadata 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.

        GitLab API returns the archive content directly.

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

        Returns:
            ZIP file content as bytes
        """
        logger.info(f"Fetching archive for {repo} at ref {ref}")
        params = {"sha": ref} if ref and ref != "HEAD" else {}
        response = await self.http.get(
            f"{self.api_url}/projects/{quote(repo, safe='')}/repository/archive.zip",
            headers=self._headers(),
            params=params,
        )
        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.

        GitLab API returns raw file content directly.
        Returns None if file not found (404).

        Args:
            repo: Repository identifier (e.g., "group/project")
            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}/projects/{quote(repo, safe='')}/repository/files/{quote(path, safe='')}/raw",
                headers=self._headers(),
                params={"ref": ref},
            )
            return response.text
        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()
