"""GitHub App token generation service.

This module provides functionality to generate short-lived GitHub installation
tokens using a GitHub App's private key. Tokens are stored only in memory and
expire after ~1 hour.
"""

import base64
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional, Tuple

import aiohttp
import jwt
import structlog

from rasa.model_manager import config

structlogger = structlog.get_logger()


class GitHubTokenError(Exception):
    """Exception raised when GitHub token generation fails."""

    pass


@dataclass
class GitHubToken:
    """Represents a GitHub installation access token."""

    token: str
    expires_at: datetime


class GitHubTokenService:
    """Service for generating short-lived GitHub installation tokens.

    This service uses a GitHub App's private key to generate JWT tokens,
    which are then exchanged for short-lived installation access tokens.
    All tokens are stored in memory only - no persistence.
    """

    def __init__(
        self,
        app_id: Optional[str] = None,
        private_key: Optional[str] = None,
        github_host: Optional[str] = None,
    ) -> None:
        """Initialize the GitHub token service.

        Args:
            app_id: GitHub App ID. Defaults to config value.
            private_key: GitHub App private key (PEM format). Defaults to config value.
            github_host: GitHub host (for Enterprise). Defaults to config value.
        """
        self.app_id = app_id or config.GITHUB_APP_ID
        self.private_key = private_key or self._load_private_key()
        self.github_host = github_host or config.GITHUB_HOST

        # In-memory token cache: installation_id -> GitHubToken
        self._token_cache: dict[str, GitHubToken] = {}

    def _load_private_key(self) -> Optional[str]:
        """Load private key from file path or environment variable.

        Returns:
            The private key in PEM format, or None if not configured.
        """
        # Try loading from file path first
        if config.GITHUB_APP_PRIVATE_KEY_PATH:
            try:
                with open(config.GITHUB_APP_PRIVATE_KEY_PATH, "r") as f:
                    return f.read()
            except Exception as e:
                structlogger.error(
                    "github_token.load_private_key_file_failed",
                    path=config.GITHUB_APP_PRIVATE_KEY_PATH,
                    error=str(e),
                )
                return None

        # Try base64-decoded environment variable
        if config.GITHUB_APP_PRIVATE_KEY:
            try:
                # Try to decode as base64 first
                return base64.b64decode(config.GITHUB_APP_PRIVATE_KEY).decode("utf-8")
            except Exception:
                # If not base64, assume it's raw PEM content
                return config.GITHUB_APP_PRIVATE_KEY

        return None

    def is_configured(self) -> bool:
        """Check if the GitHub App is properly configured.

        Returns:
            True if app_id and private_key are available.
        """
        return bool(self.app_id and self.private_key)

    def _create_jwt(self) -> str:
        """Create a JWT signed with the GitHub App's private key.

        Returns:
            A JWT string valid for 10 minutes.

        Raises:
            GitHubTokenError: If the service is not configured.
        """
        if not self.is_configured():
            raise GitHubTokenError(
                "GitHub App not configured. Set GITHUB_APP_ID and "
                "GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_PATH."
            )

        now = int(time.time())
        payload = {
            "iat": now - 60,  # Issued 60 seconds ago (clock skew tolerance)
            "exp": now + (10 * 60),  # Expires in 10 minutes
            "iss": self.app_id,
        }

        return jwt.encode(payload, self.private_key, algorithm="RS256")

    def _get_api_base_url(self) -> str:
        """Get the GitHub API base URL.

        Returns:
            The API base URL for the configured GitHub host.
        """
        if self.github_host == "github.com":
            return "https://api.github.com"
        return f"https://{self.github_host}/api/v3"

    async def get_installation_token(
        self, installation_id: str
    ) -> Tuple[str, datetime]:
        """Get an installation access token, using cache if valid.

        Args:
            installation_id: The GitHub App installation ID.

        Returns:
            Tuple of (token, expires_at).

        Raises:
            GitHubTokenError: If token generation fails.
        """
        # Check cache first
        cached = self._token_cache.get(installation_id)
        if cached and cached.expires_at > datetime.now(timezone.utc):
            # Add 5 minute buffer before expiry
            buffer_time = cached.expires_at.timestamp() - 5 * 60
            if time.time() < buffer_time:
                structlogger.debug(
                    "github_token.cache_hit",
                    installation_id=installation_id,
                    expires_at=cached.expires_at.isoformat(),
                )
                return cached.token, cached.expires_at

        # Generate new token
        return await self._generate_installation_token(installation_id)

    async def _generate_installation_token(
        self, installation_id: str
    ) -> Tuple[str, datetime]:
        """Generate a new installation access token from GitHub API.

        Args:
            installation_id: The GitHub App installation ID.

        Returns:
            Tuple of (token, expires_at).

        Raises:
            GitHubTokenError: If the API call fails.
        """
        jwt_token = self._create_jwt()
        api_url = (
            f"{self._get_api_base_url()}/app/installations/"
            f"{installation_id}/access_tokens"
        )

        structlogger.debug(
            "github_token.generating",
            installation_id=installation_id,
            api_url=api_url,
        )

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    api_url,
                    headers={
                        "Authorization": f"Bearer {jwt_token}",
                        "Accept": "application/vnd.github+json",
                        "X-GitHub-Api-Version": "2022-11-28",
                    },
                ) as response:
                    if response.status != 201:
                        error_body = await response.text()
                        structlogger.error(
                            "github_token.api_error",
                            installation_id=installation_id,
                            status=response.status,
                            error=error_body,
                        )
                        raise GitHubTokenError(
                            f"Failed to generate installation token: "
                            f"{response.status} - {error_body}"
                        )

                    data = await response.json()
                    token = data["token"]
                    expires_at_str = data["expires_at"]
                    expires_at = datetime.fromisoformat(
                        expires_at_str.replace("Z", "+00:00")
                    )

                    # Cache the token in memory
                    self._token_cache[installation_id] = GitHubToken(
                        token=token, expires_at=expires_at
                    )

                    structlogger.info(
                        "github_token.generated",
                        installation_id=installation_id,
                        expires_at=expires_at.isoformat(),
                    )

                    return token, expires_at

        except aiohttp.ClientError as e:
            structlogger.error(
                "github_token.network_error",
                installation_id=installation_id,
                error=str(e),
            )
            raise GitHubTokenError(f"Network error generating token: {e}")

    def clear_cache(self, installation_id: Optional[str] = None) -> None:
        """Clear cached tokens.

        Args:
            installation_id: If provided, clear only this installation's token.
                           If None, clear all cached tokens.
        """
        if installation_id:
            self._token_cache.pop(installation_id, None)
        else:
            self._token_cache.clear()

    @staticmethod
    def build_clone_url(
        owner: str,
        repo: str,
        token: str,
        github_host: str = "github.com",
    ) -> str:
        """Build an authenticated HTTPS clone URL.

        Args:
            owner: Repository owner (org or user).
            repo: Repository name.
            token: GitHub access token.
            github_host: GitHub host.

        Returns:
            Authenticated HTTPS URL for git clone.
        """
        return f"https://x-access-token:{token}@{github_host}/{owner}/{repo}.git"

    @staticmethod
    def parse_repo_url(repo_url: str) -> Tuple[str, str, str]:
        """Parse a GitHub repository URL into components.

        Args:
            repo_url: Repository URL (HTTPS format).

        Returns:
            Tuple of (host, owner, repo_name).

        Raises:
            ValueError: If URL format is invalid.
        """
        # Handle various URL formats:
        # https://github.com/owner/repo.git
        # https://github.com/owner/repo
        # git@github.com:owner/repo.git

        url = repo_url.strip()

        # Remove .git suffix
        if url.endswith(".git"):
            url = url[:-4]

        # Handle HTTPS URLs
        if url.startswith("https://"):
            parts = url[8:].split("/")
            if len(parts) >= 3:
                host = parts[0]
                owner = parts[1]
                repo = parts[2]
                return host, owner, repo

        # Handle SSH URLs
        if url.startswith("git@"):
            # git@github.com:owner/repo
            host_part, path_part = url[4:].split(":", 1)
            parts = path_part.split("/")
            if len(parts) >= 2:
                return host_part, parts[0], parts[1]

        raise ValueError(f"Cannot parse repository URL: {repo_url}")


# Module-level singleton instance
_token_service: Optional[GitHubTokenService] = None


def get_github_token_service() -> GitHubTokenService:
    """Get the singleton GitHubTokenService instance.

    Returns:
        The GitHubTokenService singleton.
    """
    global _token_service
    if _token_service is None:
        _token_service = GitHubTokenService()
    return _token_service
