# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""
GitLab API Client

Provides an async HTTP client for interacting with the GitLab REST API.
Handles merge request operations including:
- Posting pipeline/commit status
- Creating and updating MR notes (comments)
- Fetching MR diffs and changes

Supports both gitlab.com and self-hosted GitLab instances.
"""

from __future__ import annotations

import logging
from typing import Any, Literal
from urllib.parse import quote

import httpx

from ..http.retry import request_with_retry
from .auth import GitLabAuth

logger = logging.getLogger(__name__)

# Valid states for commit/pipeline status
StatusState = Literal["pending", "running", "success", "failed", "canceled"]

# Map MergeGuide status to GitLab status
MERGEGUIDE_TO_GITLAB_STATUS = {
    "pending": "pending",
    "success": "success",
    "failure": "failed",
    "error": "failed",
}


class GitLabAPI:
    """
    Async GitLab API client for merge request operations.

    Provides methods for interacting with GitLab's REST API for MR-related
    workflows: commit status, notes (comments), and diff retrieval.

    Supports:
    - GitLab.com (default)
    - Self-hosted GitLab instances
    - Project-level and user-level access tokens

    Example:
        >>> auth = GitLabAuth(access_token="glpat-xxxx")
        >>> api = GitLabAPI(auth)
        >>>
        >>> try:
        ...     await api.post_commit_status(
        ...         project_id=123,
        ...         sha="abc123",
        ...         state="success",
        ...         description="All checks passed",
        ...     )
        ... finally:
        ...     await api.close()
    """

    def __init__(
        self,
        auth: GitLabAuth,
        gitlab_url: str = "https://gitlab.com",
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """
        Initialize the GitLab API client.

        Args:
            auth: GitLabAuth instance for authentication.
            gitlab_url: Base URL for GitLab instance.
            http_client: Optional pre-configured httpx.AsyncClient.
        """
        self.auth = auth
        self.gitlab_url = gitlab_url.rstrip("/")
        self.api_url = f"{self.gitlab_url}/api/v4"
        self._http_client = http_client
        self._owns_client = http_client is None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or lazily create the HTTP client."""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(timeout=30.0)
            self._owns_client = True
        return self._http_client

    async def _get_headers(self) -> dict[str, str]:
        """Build request headers with authentication."""
        token = await self.auth.get_token()
        return self.auth.get_auth_headers(token)

    async def _request(
        self,
        method: str,
        url: str,
        **kwargs,
    ) -> httpx.Response:
        """Make an HTTP request with retry on 429 rate limits."""
        client = await self._get_client()
        headers = await self._get_headers()
        merged_headers = {**headers, **kwargs.pop("headers", {})}
        return await request_with_retry(client, method, url, headers=merged_headers, **kwargs)

    async def close(self) -> None:
        """Close the HTTP client if this instance owns it."""
        if self._owns_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    # =========================================================================
    # Commit Status API
    # =========================================================================

    async def post_commit_status(
        self,
        project_id: int | str,
        sha: str,
        state: StatusState,
        description: str,
        name: str = "MergeGuide Policy Check",
        target_url: str | None = None,
        coverage: float | None = None,
        pipeline_id: int | None = None,
        ref: str | None = None,
    ) -> dict[str, Any]:
        """
        Post a commit status to GitLab.

        This creates an external status check visible on commits and MRs.

        Args:
            project_id: Project ID or URL-encoded path.
            sha: Commit SHA to post status for.
            state: Status state (pending, running, success, failed, canceled).
            description: Human-readable status description.
            name: Name of the status check (context).
            target_url: URL to link from the status.
            coverage: Code coverage percentage (0-100).
            pipeline_id: Pipeline ID to associate status with.
            ref: Git ref (branch/tag) the commit is on.

        Returns:
            GitLab API response with created status.
        """
        # URL-encode project path if it's a string
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id

        url = f"{self.api_url}/projects/{project_ref}/statuses/{sha}"

        payload: dict[str, Any] = {
            "state": state,
            "description": description[:255],  # GitLab limit
            "name": name,
        }

        if target_url:
            payload["target_url"] = target_url
        if coverage is not None:
            payload["coverage"] = coverage
        if pipeline_id:
            payload["pipeline_id"] = pipeline_id
        if ref:
            payload["ref"] = ref

        response = await self._request("POST", url, json=payload)
        response.raise_for_status()

        logger.info(
            "Posted GitLab commit status: project=%s sha=%s state=%s",
            project_id,
            sha[:8],
            state,
        )

        return response.json()

    # =========================================================================
    # Merge Request API
    # =========================================================================

    async def get_merge_request(
        self,
        project_id: int | str,
        mr_iid: int,
    ) -> dict[str, Any]:
        """
        Get merge request details.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID (IID).

        Returns:
            Merge request details.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}"

        response = await self._request("GET", url)
        response.raise_for_status()

        return response.json()

    async def get_merge_request_changes(
        self,
        project_id: int | str,
        mr_iid: int,
    ) -> dict[str, Any]:
        """
        Get merge request changes (diff).

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.

        Returns:
            MR changes including diff content.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}/changes"

        response = await self._request("GET", url)
        response.raise_for_status()

        return response.json()

    async def fetch_mr_diff(
        self,
        project_id: int | str,
        mr_iid: int,
    ) -> str:
        """
        Fetch the raw diff for a merge request.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.

        Returns:
            Raw unified diff as a string.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}/diffs"

        response = await self._request("GET", url, headers={"Accept": "text/plain"})
        response.raise_for_status()

        # GitLab returns JSON array of diff objects
        # We need to reconstruct the unified diff
        diffs = response.json()

        unified_diff_parts = []
        for diff in diffs:
            old_path = diff.get("old_path", "")
            new_path = diff.get("new_path", "")
            diff_content = diff.get("diff", "")

            # Build unified diff header
            header = f"diff --git a/{old_path} b/{new_path}\n"
            if diff.get("new_file"):
                header += "new file mode 100644\n"
            elif diff.get("deleted_file"):
                header += "deleted file mode 100644\n"
            elif diff.get("renamed_file"):
                header += f"rename from {old_path}\nrename to {new_path}\n"

            header += f"--- a/{old_path}\n+++ b/{new_path}\n"

            unified_diff_parts.append(header + diff_content)

        return "\n".join(unified_diff_parts)

    # =========================================================================
    # Notes (Comments) API
    # =========================================================================

    async def create_mr_note(
        self,
        project_id: int | str,
        mr_iid: int,
        body: str,
    ) -> dict[str, Any]:
        """
        Create a note (comment) on a merge request.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.
            body: Comment body (Markdown supported).

        Returns:
            Created note details.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}/notes"

        response = await self._request("POST", url, json={"body": body})
        response.raise_for_status()

        logger.info(
            "Created GitLab MR note: project=%s mr=%d",
            project_id,
            mr_iid,
        )

        return response.json()

    async def update_mr_note(
        self,
        project_id: int | str,
        mr_iid: int,
        note_id: int,
        body: str,
    ) -> dict[str, Any]:
        """
        Update an existing note on a merge request.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.
            note_id: Note ID to update.
            body: New comment body.

        Returns:
            Updated note details.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}/notes/{note_id}"

        response = await self._request("PUT", url, json={"body": body})
        response.raise_for_status()

        return response.json()

    async def list_mr_notes(
        self,
        project_id: int | str,
        mr_iid: int,
        per_page: int = 100,
    ) -> list[dict[str, Any]]:
        """
        List all notes on a merge request.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.
            per_page: Number of notes per page.

        Returns:
            List of notes.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        base_url = f"{self.api_url}/projects/{project_ref}/merge_requests/{mr_iid}/notes"

        all_notes: list[dict[str, Any]] = []
        page = 1
        max_pages = 100

        while page <= max_pages:
            response = await self._request(
                "GET",
                base_url,
                params={"per_page": per_page, "page": page},
            )
            response.raise_for_status()

            notes = response.json()
            if not notes:
                break
            all_notes.extend(notes)

            # Check for next page via header
            next_page = response.headers.get("x-next-page")
            if not next_page:
                break
            page += 1

        return all_notes

    async def find_bot_note(
        self,
        project_id: int | str,
        mr_iid: int,
        marker: str,
    ) -> dict[str, Any] | None:
        """
        Find a bot-created note by marker string.

        Args:
            project_id: Project ID or URL-encoded path.
            mr_iid: Merge request internal ID.
            marker: Unique marker string to identify bot comments.

        Returns:
            Note dict if found, None otherwise.
        """
        notes = await self.list_mr_notes(project_id, mr_iid)

        for note in notes:
            if marker in note.get("body", ""):
                return note

        return None

    # =========================================================================
    # Project API
    # =========================================================================

    async def get_project(self, project_id: int | str) -> dict[str, Any]:
        """
        Get project details.

        Args:
            project_id: Project ID or URL-encoded path.

        Returns:
            Project details.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}"

        response = await self._request("GET", url)
        response.raise_for_status()

        return response.json()

    async def list_project_hooks(
        self,
        project_id: int | str,
    ) -> list[dict[str, Any]]:
        """
        List webhooks configured for a project.

        Args:
            project_id: Project ID or URL-encoded path.

        Returns:
            List of webhook configurations.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        url = f"{self.api_url}/projects/{project_ref}/hooks"

        response = await self._request("GET", url)
        response.raise_for_status()

        return response.json()

    async def create_project_hook(
        self,
        project_id: int | str,
        url: str,
        secret_token: str,
        merge_requests_events: bool = True,
        push_events: bool = False,
        enable_ssl_verification: bool = True,
    ) -> dict[str, Any]:
        """
        Create a webhook for a project.

        Args:
            project_id: Project ID or URL-encoded path.
            url: Webhook URL.
            secret_token: Secret for webhook signature.
            merge_requests_events: Trigger on MR events.
            push_events: Trigger on push events.
            enable_ssl_verification: Verify SSL certificates.

        Returns:
            Created webhook details.
        """
        project_ref = quote(str(project_id), safe="") if isinstance(project_id, str) else project_id
        api_url = f"{self.api_url}/projects/{project_ref}/hooks"

        response = await self._request(
            "POST",
            api_url,
            json={
                "url": url,
                "token": secret_token,
                "merge_requests_events": merge_requests_events,
                "push_events": push_events,
                "enable_ssl_verification": enable_ssl_verification,
            },
        )
        response.raise_for_status()

        return response.json()
