# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Bitbucket Cloud API client for PR operations.

Provides methods for:
- Build status (commit status) updates
- PR comments (create and update)
- PR diff retrieval

Bitbucket Cloud API 2.0:
https://developer.atlassian.com/cloud/bitbucket/rest/intro/
"""

from __future__ import annotations

import logging
from typing import Any, Literal

import httpx

from ..http.retry import request_with_retry
from .auth import BitbucketAuth

logger = logging.getLogger(__name__)

# Bitbucket Cloud API base URL
BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0"

# Build status states
BuildState = Literal["SUCCESSFUL", "FAILED", "INPROGRESS", "STOPPED"]


class BitbucketAPI:
    """Async Bitbucket Cloud API client.

    Provides methods for interacting with Bitbucket for PR workflows:
    build status, comments, and diff retrieval.

    Example:
        >>> auth = BitbucketAuth(username="user", app_password="xxx")
        >>> api = BitbucketAPI(auth)
        >>>
        >>> await api.post_build_status(
        ...     workspace="myteam",
        ...     repo_slug="myrepo",
        ...     commit="abc123",
        ...     state="SUCCESSFUL",
        ...     key="mergeguide",
        ...     name="MergeGuide Policy Check",
        ...     description="All checks passed",
        ... )
    """

    def __init__(
        self,
        auth: BitbucketAuth,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """Initialize Bitbucket API client.

        Args:
            auth: BitbucketAuth instance for authentication
            http_client: Optional pre-configured httpx client
        """
        self.auth = auth
        self._http_client = http_client
        self._owns_client = http_client is None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create 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]:
        """Get request headers with authentication."""
        headers = await self.auth.get_auth_headers()
        headers["Accept"] = "application/json"
        headers["Content-Type"] = "application/json"
        return headers

    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 HTTP client if owned."""
        if self._owns_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    async def post_build_status(
        self,
        workspace: str,
        repo_slug: str,
        commit: str,
        state: BuildState,
        key: str,
        name: str,
        description: str,
        url: str | None = None,
    ) -> None:
        """Post a build status (commit status) to Bitbucket.

        Build statuses appear in PR views and can be required for merging.

        States:
        - SUCCESSFUL: Build passed
        - FAILED: Build failed
        - INPROGRESS: Build is running
        - STOPPED: Build was cancelled

        Args:
            workspace: Workspace slug (team or user)
            repo_slug: Repository slug
            commit: Full commit hash (40 characters)
            state: Build state
            key: Unique key for this status (e.g., "mergeguide")
            name: Display name for the status
            description: Status description (max 255 chars)
            url: Optional URL for details link

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        # Build status endpoint
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses/build"

        # Truncate description to Bitbucket's limit
        if len(description) > 255:
            description = description[:252] + "..."

        payload: dict[str, Any] = {
            "state": state,
            "key": key,
            "name": name,
            "description": description,
        }
        if url:
            payload["url"] = url

        response = await self._request("POST", api_url, json=payload)
        response.raise_for_status()
        logger.info(f"Posted build status '{state}' for {workspace}/{repo_slug}@{commit[:7]}")

    async def post_pr_comment(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
        content: str,
    ) -> int:
        """Post a comment on a pull request.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID
            content: Comment content (Markdown supported)

        Returns:
            Comment ID

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments"

        payload = {
            "content": {
                "raw": content,
            }
        }

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

        data = response.json()
        comment_id = data.get("id", 0)
        logger.info(f"Posted comment #{comment_id} on {workspace}/{repo_slug}!{pr_id}")
        return comment_id

    async def update_pr_comment(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
        comment_id: int,
        content: str,
    ) -> None:
        """Update an existing PR comment.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID
            comment_id: Comment ID to update
            content: New comment content

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments/{comment_id}"

        payload = {
            "content": {
                "raw": content,
            }
        }

        response = await self._request("PUT", api_url, json=payload)
        response.raise_for_status()
        logger.info(f"Updated comment #{comment_id} on {workspace}/{repo_slug}!{pr_id}")

    async def find_bot_comment(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
        marker: str = "<!-- mergeguide-policy-check -->",
    ) -> int | None:
        """Find an existing bot comment by marker.

        Searches PR comments for one containing the marker string.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID
            marker: String to search for in comments

        Returns:
            Comment ID if found, None otherwise
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments"

        # Paginate through comments (max 100 pages to prevent infinite loops)
        page_url: str | None = api_url
        max_pages = 100
        for _page in range(max_pages):
            if not page_url:
                break
            response = await self._request("GET", page_url)
            response.raise_for_status()
            data = response.json()

            for comment in data.get("values", []):
                content = comment.get("content", {}).get("raw", "")
                if marker in content:
                    return comment.get("id")

            # Check for next page
            page_url = data.get("next")

        return None

    async def get_pr_diff(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
    ) -> str:
        """Get the diff for a pull request.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID

        Returns:
            Unified diff string

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        api_url = (
            f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diff"
        )

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

    async def get_pr_files(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
    ) -> list[dict[str, Any]]:
        """Get list of files changed in a PR.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID

        Returns:
            List of file info dicts with:
            - path: File path
            - type: Change type (added, removed, modified, renamed)
            - lines_added: Number of lines added
            - lines_removed: Number of lines removed
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diffstat"

        files = []
        page_url: str | None = api_url
        max_pages = 100

        for _page in range(max_pages):
            if not page_url:
                break
            response = await self._request("GET", page_url)
            response.raise_for_status()
            data = response.json()

            for entry in data.get("values", []):
                old_path = entry.get("old", {}).get("path")
                new_path = entry.get("new", {}).get("path")

                # Determine change type
                if old_path is None:
                    change_type = "added"
                    path = new_path
                elif new_path is None:
                    change_type = "removed"
                    path = old_path
                elif old_path != new_path:
                    change_type = "renamed"
                    path = new_path
                else:
                    change_type = "modified"
                    path = new_path

                files.append(
                    {
                        "path": path,
                        "type": change_type,
                        "lines_added": entry.get("lines_added", 0),
                        "lines_removed": entry.get("lines_removed", 0),
                        "old_path": old_path,
                        "new_path": new_path,
                    }
                )

            page_url = data.get("next")

            # Safety limit
            if len(files) > 5000:
                logger.warning(f"PR {workspace}/{repo_slug}!{pr_id} has >5000 files, truncating")
                break

        return files

    async def get_pr_info(
        self,
        workspace: str,
        repo_slug: str,
        pr_id: int,
    ) -> dict[str, Any]:
        """Get pull request details.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            pr_id: Pull request ID

        Returns:
            PR info dict
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}"

        response = await self._request("GET", api_url)
        response.raise_for_status()
        return response.json()

    async def get_file_content(
        self,
        workspace: str,
        repo_slug: str,
        commit: str,
        path: str,
    ) -> str:
        """Get raw file content at a specific commit.

        Args:
            workspace: Workspace slug
            repo_slug: Repository slug
            commit: Commit hash
            path: File path

        Returns:
            File content as string

        Raises:
            httpx.HTTPStatusError: If file not found or access denied
        """
        api_url = f"{BITBUCKET_API_BASE}/repositories/{workspace}/{repo_slug}/src/{commit}/{path}"

        response = await self._request(
            "GET", api_url, headers={"Accept": "application/octet-stream"}
        )
        response.raise_for_status()
        return response.text
