# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Bitbucket Cloud authentication.

Handles authentication for Bitbucket Cloud API using:
- App passwords (for user-level access)
- Workspace access tokens (for workspace-level access)
- OAuth 2.0 client credentials (for Bitbucket apps)

Bitbucket Cloud uses Basic authentication with app passwords or
Bearer tokens with OAuth 2.0 access tokens.
"""

from __future__ import annotations

import base64
import logging
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

import httpx

logger = logging.getLogger(__name__)

# Bitbucket OAuth token endpoint
BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"


class BitbucketAuthError(Exception):
    """Raised when Bitbucket authentication fails."""

    pass


@dataclass
class TokenInfo:
    """OAuth access token with expiry tracking."""

    access_token: str
    expires_at: datetime
    token_type: str = "bearer"
    scopes: list[str] | None = None

    @property
    def is_expired(self) -> bool:
        """Check if token has expired (with 5-minute buffer)."""
        return datetime.now(UTC) >= (self.expires_at - timedelta(minutes=5))


class BitbucketAuth:
    """Bitbucket Cloud authentication handler.

    Supports multiple authentication methods:

    1. App Password (Basic Auth):
       Use for simple integrations. Create at:
       https://bitbucket.org/account/settings/app-passwords/

    2. Workspace Access Token:
       Use for workspace-level automation. Create at:
       Workspace settings > Access tokens

    3. OAuth 2.0 Client Credentials:
       Use for Bitbucket apps with proper scoping.
       Register at: https://bitbucket.org/account/settings/

    Example:
        >>> # Using app password
        >>> auth = BitbucketAuth(username="myuser", app_password="xxxx")
        >>> headers = await auth.get_auth_headers()

        >>> # Using OAuth client credentials
        >>> auth = BitbucketAuth(client_id="xxx", client_secret="xxx")
        >>> headers = await auth.get_auth_headers()
    """

    def __init__(
        self,
        username: str | None = None,
        app_password: str | None = None,
        workspace_token: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
    ) -> None:
        """Initialize Bitbucket authentication.

        Provide ONE of:
        - username + app_password (Basic auth)
        - workspace_token (Bearer token)
        - client_id + client_secret (OAuth 2.0)

        Args:
            username: Bitbucket username (for app password auth)
            app_password: App password created in account settings
            workspace_token: Workspace access token
            client_id: OAuth 2.0 client ID
            client_secret: OAuth 2.0 client secret

        Raises:
            BitbucketAuthError: If no valid auth method is provided
        """
        self._username = username
        self._app_password = app_password
        self._workspace_token = workspace_token
        self._client_id = client_id
        self._client_secret = client_secret
        self._oauth_token: TokenInfo | None = None
        self._http_client: httpx.AsyncClient | None = None

        # Validate that at least one auth method is provided
        if not self._has_valid_auth():
            raise BitbucketAuthError(
                "Must provide one of: "
                "(username + app_password), workspace_token, or (client_id + client_secret)"
            )

    def _has_valid_auth(self) -> bool:
        """Check if valid authentication credentials are configured."""
        has_basic = bool(self._username and self._app_password)
        has_workspace = bool(self._workspace_token)
        has_oauth = bool(self._client_id and self._client_secret)
        return has_basic or has_workspace or has_oauth

    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)
        return self._http_client

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

    async def get_auth_headers(self) -> dict[str, str]:
        """Get authentication headers for API requests.

        Returns appropriate headers based on configured auth method:
        - Basic auth for app passwords
        - Bearer token for workspace tokens and OAuth

        Returns:
            Dict with Authorization header

        Raises:
            BitbucketAuthError: If token refresh fails
        """
        if self._workspace_token:
            return {"Authorization": f"Bearer {self._workspace_token}"}

        if self._username and self._app_password:
            credentials = f"{self._username}:{self._app_password}"
            encoded = base64.b64encode(credentials.encode()).decode()
            return {"Authorization": f"Basic {encoded}"}

        if self._client_id and self._client_secret:
            token = await self._get_oauth_token()
            return {"Authorization": f"Bearer {token}"}

        raise BitbucketAuthError("No valid authentication method configured")

    async def _get_oauth_token(self) -> str:
        """Get or refresh OAuth 2.0 access token.

        Uses client credentials grant to obtain an access token.
        Tokens are cached and automatically refreshed when expired.

        Returns:
            Valid access token string

        Raises:
            BitbucketAuthError: If token request fails
        """
        # Return cached token if still valid
        if self._oauth_token and not self._oauth_token.is_expired:
            return self._oauth_token.access_token

        # Request new token
        client = await self._get_client()

        try:
            response = await client.post(
                BITBUCKET_TOKEN_URL,
                data={"grant_type": "client_credentials"},
                auth=(self._client_id, self._client_secret),
            )
            response.raise_for_status()
            data = response.json()

            expires_in = data.get("expires_in", 3600)
            self._oauth_token = TokenInfo(
                access_token=data["access_token"],
                expires_at=datetime.now(UTC) + timedelta(seconds=expires_in),
                token_type=data.get("token_type", "bearer"),
                scopes=data.get("scopes", "").split() if data.get("scopes") else None,
            )

            logger.info("Obtained new Bitbucket OAuth token")
            return self._oauth_token.access_token

        except httpx.HTTPStatusError as e:
            raise BitbucketAuthError(f"OAuth token request failed: {e.response.status_code}") from e
        except Exception as e:
            raise BitbucketAuthError(f"OAuth token request failed: {e}") from e
