# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Azure DevOps REST API client.

Provides async HTTP client for interacting with Azure DevOps APIs:
- Build Status API (posting PR check statuses)
- Pull Request Threads API (posting comments)
- Git API (fetching PR diffs)

Example:
    auth = AzureDevOpsAuth(org_url, pat)
    async with AzureDevOpsAPI(auth) as api:
        await api.post_status(repo_id, pr_id, sha, "pending", "Evaluating...")
        diff = await api.fetch_pr_diff(repo_id, pr_id)
        thread_id = await api.create_thread(repo_id, pr_id, "Results...")
"""

from __future__ import annotations

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

import httpx

from mergeguide.azure_devops.auth import AzureDevOpsAuth

logger = logging.getLogger(__name__)

# Azure DevOps API version
API_VERSION = "7.1"

# Status state mapping
STATUS_STATES = {
    "pending": "pending",
    "success": "succeeded",
    "succeeded": "succeeded",
    "failure": "failed",
    "failed": "failed",
    "error": "error",
}


class AzureDevOpsAPI:
    """Async HTTP client for Azure DevOps REST API.

    Provides methods for:
    - Posting build statuses to PRs
    - Creating and updating comment threads
    - Fetching PR diffs and metadata

    The client uses httpx for async HTTP requests and handles
    authentication via the provided AzureDevOpsAuth instance.

    Attributes:
        auth: Authentication handler with org URL and PAT.
        client: Underlying httpx.AsyncClient instance.

    Example:
        >>> auth = AzureDevOpsAuth("https://dev.azure.com/myorg", "pat123")
        >>> async with AzureDevOpsAPI(auth) as api:
        ...     await api.post_status(repo_id, 123, "abc123", "pending", "Checking...")
    """

    # Context name for MergeGuide status checks
    CONTEXT_NAME = "MergeGuide"
    CONTEXT_GENRE = "mergeguide/policy-check"

    def __init__(self, auth: AzureDevOpsAuth) -> None:
        """Initialize the API client.

        Args:
            auth: Authentication handler with credentials.
        """
        self.auth = auth
        self._client: httpx.AsyncClient | None = None

    @property
    def client(self) -> httpx.AsyncClient:
        """Get or create the HTTP client."""
        if self._client is None:
            self._client = httpx.AsyncClient(
                headers={
                    **self.auth.get_auth_headers(),
                    "Content-Type": "application/json",
                },
                timeout=30.0,
            )
        return self._client

    async def close(self) -> None:
        """Close the HTTP client."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    async def __aenter__(self) -> AzureDevOpsAPI:
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Async context manager exit."""
        await self.close()

    def _build_url(self, project: str, path: str) -> str:
        """Build API URL with version parameter.

        Args:
            project: Project name or ID.
            path: API path (without leading slash).

        Returns:
            Full URL with api-version parameter.
        """
        base = self.auth.get_api_base_url(project)
        separator = "&" if "?" in path else "?"
        return f"{base}/{path}{separator}api-version={API_VERSION}"

    async def post_status(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
        commit_sha: str,
        state: str,
        description: str,
        target_url: str | None = None,
    ) -> dict[str, Any]:
        """Post a build status to a pull request.

        Posts to the PR statuses API to show check status in the PR UI.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.
            commit_sha: Commit SHA to post status for.
            state: Status state (pending, success, failure, error).
            description: Status description text.
            target_url: Optional URL for "Details" link.

        Returns:
            API response as dictionary.

        Raises:
            httpx.HTTPStatusError: If API request fails.
        """
        # Map state to Azure DevOps status state
        ado_state = STATUS_STATES.get(state.lower(), "pending")

        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}/statuses",
        )

        payload: dict[str, Any] = {
            "state": ado_state,
            "description": description[:140],  # Max 140 chars
            "context": {
                "name": self.CONTEXT_NAME,
                "genre": self.CONTEXT_GENRE,
            },
        }

        if target_url:
            payload["targetUrl"] = target_url

        logger.debug(f"Posting status to PR #{pr_id}: {ado_state} - {description}")

        response = await self.client.post(url, json=payload)
        response.raise_for_status()
        return response.json()

    async def create_thread(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
        content: str,
        status: str = "active",
    ) -> int:
        """Create a new comment thread on a pull request.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.
            content: Markdown content for the comment.
            status: Thread status (active, fixed, closed, etc.).

        Returns:
            Created thread ID.

        Raises:
            httpx.HTTPStatusError: If API request fails.
        """
        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}/threads",
        )

        payload = {
            "comments": [
                {
                    "parentCommentId": 0,
                    "content": content,
                    "commentType": "text",
                }
            ],
            "status": status,
        }

        logger.debug(f"Creating thread on PR #{pr_id}")

        response = await self.client.post(url, json=payload)
        response.raise_for_status()
        data = response.json()
        return data["id"]

    async def update_thread_comment(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
        thread_id: int,
        comment_id: int,
        content: str,
    ) -> dict[str, Any]:
        """Update an existing comment in a thread.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.
            thread_id: Thread ID containing the comment.
            comment_id: Comment ID to update.
            content: New Markdown content.

        Returns:
            Updated comment data.

        Raises:
            httpx.HTTPStatusError: If API request fails.
        """
        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}/threads/{thread_id}/comments/{comment_id}",
        )

        payload = {"content": content}

        logger.debug(f"Updating comment {comment_id} in thread {thread_id}")

        response = await self.client.patch(url, json=payload)
        response.raise_for_status()
        return response.json()

    async def find_bot_thread(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
        marker: str = "<!-- mergeguide-policy-check -->",
    ) -> tuple[int, int] | None:
        """Find existing MergeGuide comment thread.

        Searches PR threads for one containing the marker comment.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.
            marker: HTML comment marker to identify our threads.

        Returns:
            Tuple of (thread_id, comment_id) if found, None otherwise.
        """
        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}/threads",
        )

        response = await self.client.get(url)
        response.raise_for_status()
        data = response.json()

        for thread in data.get("value", []):
            for comment in thread.get("comments", []):
                content = comment.get("content", "")
                if marker in content:
                    return thread["id"], comment["id"]

        return None

    async def fetch_pr_diff(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
    ) -> str:
        """Fetch the unified diff for a pull request.

        Uses the PR iterations API to get the diff between
        source and target branches.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.

        Returns:
            Unified diff as string.

        Raises:
            httpx.HTTPStatusError: If API request fails.
        """
        # First, get the PR to find the source/target refs
        pr_url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}",
        )

        pr_response = await self.client.get(pr_url)
        pr_response.raise_for_status()
        pr_data = pr_response.json()

        # Get diff between target and source using the diffs API
        source_version = pr_data.get("lastMergeSourceCommit", {}).get("commitId", "")
        target_version = pr_data.get("lastMergeTargetCommit", {}).get("commitId", "")

        if not source_version or not target_version:
            # Fallback: use branch refs
            source_ref = pr_data.get("sourceRefName", "").replace("refs/heads/", "")
            target_ref = pr_data.get("targetRefName", "").replace("refs/heads/", "")
            source_version = source_ref
            target_version = target_ref

        # Use the diffs API
        diff_url = self._build_url(
            project,
            f"git/repositories/{repo_id}/diffs/commits"
            f"?baseVersion={quote(target_version, safe='')}&targetVersion={quote(source_version, safe='')}"
            f"&diffCommonCommit=true",
        )

        diff_response = await self.client.get(diff_url)
        diff_response.raise_for_status()
        diff_data = diff_response.json()

        # Build unified diff from changes
        return self._build_unified_diff(diff_data, project, repo_id, source_version)

    async def _build_unified_diff(
        self,
        diff_data: dict[str, Any],
        project: str,
        repo_id: str,
        commit_sha: str,
    ) -> str:
        """Build unified diff format from Azure DevOps diff response.

        Args:
            diff_data: Diff API response data.
            project: Project name.
            repo_id: Repository GUID.
            commit_sha: Commit SHA for file content.

        Returns:
            Unified diff string.
        """
        changes = diff_data.get("changes", [])
        if not changes:
            return ""

        diff_lines = []

        for change in changes:
            item = change.get("item", {})
            path = item.get("path", "")
            change_type = change.get("changeType", "")

            if not path or item.get("isFolder"):
                continue

            # Skip binary files
            if item.get("contentMetadata", {}).get("encoding") == "binary":
                continue

            # Fetch file content for the new version
            if change_type in ("add", "edit"):
                try:
                    content = await self._fetch_file_content(project, repo_id, path, commit_sha)
                    # Generate a simple diff showing all lines as added
                    diff_lines.append(f"diff --git a{path} b{path}")
                    diff_lines.append(f"--- a{path}")
                    diff_lines.append(f"+++ b{path}")

                    lines = content.split("\n")
                    diff_lines.append(f"@@ -0,0 +1,{len(lines)} @@")
                    for line in lines:
                        diff_lines.append(f"+{line}")
                except Exception as e:
                    logger.warning(f"Failed to fetch content for {path}: {e}")

        return "\n".join(diff_lines)

    async def _fetch_file_content(
        self,
        project: str,
        repo_id: str,
        path: str,
        commit_sha: str,
    ) -> str:
        """Fetch file content at a specific commit.

        Args:
            project: Project name.
            repo_id: Repository GUID.
            path: File path in repository.
            commit_sha: Commit SHA or version.

        Returns:
            File content as string.
        """
        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/items"
            f"?path={quote(path, safe='/')}&versionType=commit&version={quote(commit_sha, safe='')}",
        )

        response = await self.client.get(url)
        response.raise_for_status()
        return response.text

    async def get_pr(
        self,
        project: str,
        repo_id: str,
        pr_id: int,
    ) -> dict[str, Any]:
        """Get pull request details.

        Args:
            project: Project name or ID.
            repo_id: Repository GUID.
            pr_id: Pull request ID.

        Returns:
            PR data dictionary.
        """
        url = self._build_url(
            project,
            f"git/repositories/{repo_id}/pullRequests/{pr_id}",
        )

        response = await self.client.get(url)
        response.raise_for_status()
        return response.json()

    async def test_connection(self) -> bool:
        """Test the API connection and authentication.

        Attempts to fetch the list of projects to verify credentials.

        Returns:
            True if connection successful, False otherwise.
        """
        try:
            url = f"{self.auth.org_url}/_apis/projects?api-version={API_VERSION}"
            response = await self.client.get(url)
            response.raise_for_status()
            return True
        except Exception as e:
            logger.warning(f"Connection test failed: {e}")
            return False
