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

Handles authentication for GitLab API requests using:
- Personal Access Tokens (PAT)
- OAuth application tokens
- Project/Group access tokens

Unlike GitHub Apps, GitLab uses simpler token-based authentication.
"""

from __future__ import annotations

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

import httpx

logger = logging.getLogger(__name__)


@dataclass
class TokenInfo:
    """Information about an access token."""

    token: str
    token_type: str = "Bearer"
    expires_at: datetime | None = None
    scope: str | None = None


class GitLabAuth:
    """
    GitLab authentication handler.

    Supports multiple authentication methods:
    1. Personal Access Token (PAT) - simplest, for user-level access
    2. Project/Group Access Token - for CI/CD and automation
    3. OAuth Application - for third-party integrations

    Example:
        >>> # Using Personal Access Token
        >>> auth = GitLabAuth(access_token="glpat-xxxxxxxxxxxx")
        >>> token = await auth.get_token()

        >>> # Using OAuth (for refresh flow)
        >>> auth = GitLabAuth(
        ...     client_id="app_id",
        ...     client_secret="app_secret",
        ...     refresh_token="refresh_token",
        ...     gitlab_url="https://gitlab.com",
        ... )
        >>> token = await auth.get_token()
    """

    def __init__(
        self,
        access_token: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        refresh_token: str | None = None,
        gitlab_url: str = "https://gitlab.com",
    ) -> None:
        """
        Initialize GitLab authentication.

        Args:
            access_token: Personal access token or project/group token.
                If provided, this is used directly without OAuth flow.
            client_id: OAuth application ID (for OAuth flow).
            client_secret: OAuth application secret (for OAuth flow).
            refresh_token: OAuth refresh token (for OAuth flow).
            gitlab_url: Base URL for GitLab instance.
                Defaults to gitlab.com, but can be self-hosted URL.
        """
        self.gitlab_url = gitlab_url.rstrip("/")
        self._access_token = access_token
        self._client_id = client_id
        self._client_secret = client_secret
        self._refresh_token = refresh_token
        self._token_info: TokenInfo | None = None
        self._http_client: httpx.AsyncClient | None = 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)
        return self._http_client

    async def get_token(self) -> str:
        """
        Get a valid access token.

        For PAT/project tokens, returns the token directly.
        For OAuth, handles token refresh if needed.

        Returns:
            Valid access token string.

        Raises:
            ValueError: If no authentication method is configured.
        """
        # If using static access token
        if self._access_token:
            return self._access_token

        # If using OAuth flow
        if self._client_id and self._client_secret:
            return await self._get_oauth_token()

        raise ValueError(
            "No authentication configured. Provide either access_token or OAuth credentials."
        )

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

        Returns:
            Valid OAuth access token.
        """
        # Check if we have a valid cached token
        if self._token_info and self._token_info.expires_at:
            if datetime.utcnow() < self._token_info.expires_at - timedelta(minutes=5):
                return self._token_info.token

        # Need to refresh the token
        if not self._refresh_token:
            raise ValueError("No refresh token available for OAuth flow")

        client = await self._get_client()

        response = await client.post(
            f"{self.gitlab_url}/oauth/token",
            data={
                "grant_type": "refresh_token",
                "refresh_token": self._refresh_token,
                "client_id": self._client_id,
                "client_secret": self._client_secret,
            },
        )
        response.raise_for_status()

        data = response.json()

        # Update token info
        expires_in = data.get("expires_in", 7200)
        self._token_info = TokenInfo(
            token=data["access_token"],
            token_type=data.get("token_type", "Bearer"),
            expires_at=datetime.utcnow() + timedelta(seconds=expires_in),
            scope=data.get("scope"),
        )

        # Update refresh token if a new one was provided
        if "refresh_token" in data:
            self._refresh_token = data["refresh_token"]

        logger.debug("Refreshed GitLab OAuth token, expires in %d seconds", expires_in)

        return self._token_info.token

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

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

        Args:
            token: Access token to use.

        Returns:
            Dict of HTTP headers for authenticated requests.
        """
        return {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }


async def create_oauth_application_auth(
    client_id: str,
    client_secret: str,
    authorization_code: str,
    redirect_uri: str,
    gitlab_url: str = "https://gitlab.com",
) -> GitLabAuth:
    """
    Create a GitLabAuth instance from an OAuth authorization code.

    This is the initial OAuth flow - exchange authorization code for tokens.

    Args:
        client_id: OAuth application ID.
        client_secret: OAuth application secret.
        authorization_code: Code from OAuth callback.
        redirect_uri: Redirect URI used in authorization request.
        gitlab_url: GitLab instance URL.

    Returns:
        Configured GitLabAuth instance with valid tokens.
    """
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            f"{gitlab_url}/oauth/token",
            data={
                "grant_type": "authorization_code",
                "code": authorization_code,
                "client_id": client_id,
                "client_secret": client_secret,
                "redirect_uri": redirect_uri,
            },
        )
        response.raise_for_status()

        data = response.json()

    return GitLabAuth(
        client_id=client_id,
        client_secret=client_secret,
        refresh_token=data.get("refresh_token"),
        gitlab_url=gitlab_url,
    )
