"""
AIPTX GitLab Key Validator
==========================

Validates GitLab Personal Access Tokens and Project Access Tokens.

Supports:
- Personal Access Tokens (glpat-)
- Project Access Tokens
- Group Access Tokens
- Deploy Tokens
"""

from __future__ import annotations

import re
import time
import logging
from typing import Any

import httpx

from ..base import (
    APIKeyInfo,
    ValidationResult,
    BaseKeyValidator,
    KeyType,
    RiskLevel,
)

logger = logging.getLogger(__name__)


class GitLabKeyValidator(BaseKeyValidator):
    """
    Validate GitLab tokens and enumerate permissions.

    Uses GitLab REST API to validate tokens and discover:
    - User identity
    - Project access
    - Group membership
    - Available scopes
    """

    # Default to gitlab.com, but can be overridden for self-hosted
    API_BASE = "https://gitlab.com/api/v4"

    def get_key_type(self) -> KeyType:
        return KeyType.GITLAB

    def get_key_patterns(self) -> dict[str, re.Pattern]:
        return {
            "gitlab_pat": re.compile(r'glpat-[A-Za-z0-9_-]{20,}'),
            "gitlab_runner": re.compile(r'glrt-[A-Za-z0-9_-]{20,}'),
            "gitlab_pipeline": re.compile(r'glptt-[A-Za-z0-9_-]{20,}'),
            # Legacy patterns
            "gitlab_personal_token": re.compile(r'[A-Za-z0-9_-]{20}'),
        }

    async def validate(self, key_info: APIKeyInfo) -> ValidationResult:
        """
        Validate GitLab token against API.

        Calls GET /user to validate and retrieve identity.
        """
        start_time = time.time()

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                headers = {
                    "PRIVATE-TOKEN": key_info.key_value,
                    "Accept": "application/json",
                }

                # Get user info
                response = await client.get(
                    f"{self.API_BASE}/user",
                    headers=headers,
                )

                duration_ms = (time.time() - start_time) * 1000

                if response.status_code == 200:
                    user_data = response.json()

                    # Build identity info
                    identity_info = {
                        "user_id": user_data.get("id"),
                        "username": user_data.get("username"),
                        "name": user_data.get("name"),
                        "email": user_data.get("email"),
                        "state": user_data.get("state"),
                        "is_admin": user_data.get("is_admin", False),
                        "created_at": user_data.get("created_at"),
                        "two_factor_enabled": user_data.get("two_factor_enabled", False),
                    }

                    # Enumerate projects and groups
                    projects, groups, scopes = await self._enumerate_access(
                        client, headers
                    )

                    # Build permissions from scopes
                    permissions = self._scopes_to_permissions(scopes)

                    # Build resources list
                    resources = []
                    resources.extend([f"project:{p}" for p in projects[:20]])
                    resources.extend([f"group:{g}" for g in groups])

                    result = ValidationResult(
                        key_info=key_info,
                        is_valid=True,
                        validation_method="GET /user",
                        validation_endpoint=f"{self.API_BASE}/user",
                        identity_info=identity_info,
                        scopes=scopes,
                        permissions=permissions,
                        resources_accessible=resources,
                        raw_response=user_data,
                        validated_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                        validation_duration_ms=duration_ms,
                    )

                    # Assess risk
                    result.risk_level = self._assess_gitlab_risk(
                        scopes, projects, groups, identity_info
                    )
                    result.exploitation_potential = self._get_exploitation_potential(result)
                    result.attack_vectors = self._get_attack_vectors(scopes, projects, groups)

                    return result

                elif response.status_code == 401:
                    return self._create_error_result(
                        key_info,
                        error="Invalid or expired token",
                        error_code="401_UNAUTHORIZED",
                        duration_ms=duration_ms,
                    )
                elif response.status_code == 403:
                    return self._create_error_result(
                        key_info,
                        error="Token lacks required permissions",
                        error_code="403_FORBIDDEN",
                        duration_ms=duration_ms,
                    )
                else:
                    return self._create_error_result(
                        key_info,
                        error=f"Unexpected status: {response.status_code}",
                        error_code=str(response.status_code),
                        duration_ms=duration_ms,
                    )

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            return self._create_error_result(
                key_info,
                error="Request timeout",
                error_code="TIMEOUT",
                duration_ms=duration_ms,
            )
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(f"GitLab validation error: {e}")
            return self._create_error_result(
                key_info,
                error=str(e),
                error_code="ERROR",
                duration_ms=duration_ms,
            )

    async def _enumerate_access(
        self,
        client: httpx.AsyncClient,
        headers: dict,
    ) -> tuple[list[str], list[str], list[str]]:
        """Enumerate projects, groups, and infer scopes."""
        projects = []
        groups = []
        scopes = []

        try:
            # Get projects (first page)
            response = await client.get(
                f"{self.API_BASE}/projects",
                headers=headers,
                params={"membership": "true", "per_page": 100},
            )
            if response.status_code == 200:
                project_data = response.json()
                projects = [p["path_with_namespace"] for p in project_data]
                scopes.append("read_api")

                # Check if we have write access to any project
                for p in project_data[:3]:
                    if p.get("permissions", {}).get("project_access", {}).get("access_level", 0) >= 30:
                        scopes.append("write_repository")
                        break

            # Get groups
            response = await client.get(
                f"{self.API_BASE}/groups",
                headers=headers,
                params={"per_page": 50},
            )
            if response.status_code == 200:
                group_data = response.json()
                groups = [g["full_path"] for g in group_data]

            # Try to get personal access tokens info (only works for own tokens)
            response = await client.get(
                f"{self.API_BASE}/personal_access_tokens/self",
                headers=headers,
            )
            if response.status_code == 200:
                token_data = response.json()
                token_scopes = token_data.get("scopes", [])
                scopes = list(set(scopes + token_scopes))

            # Infer additional scopes from what we can access
            # Check if we can read user emails
            response = await client.get(
                f"{self.API_BASE}/user/emails",
                headers=headers,
            )
            if response.status_code == 200:
                scopes.append("read_user")

            # Check if we can access registry
            if projects:
                response = await client.get(
                    f"{self.API_BASE}/projects/{projects[0].replace('/', '%2F')}/registry/repositories",
                    headers=headers,
                )
                if response.status_code == 200:
                    scopes.append("read_registry")

        except Exception as e:
            logger.debug(f"GitLab enumeration error: {e}")

        return projects, groups, list(set(scopes))

    def _scopes_to_permissions(self, scopes: list[str]) -> list[str]:
        """Convert GitLab scopes to permission descriptions."""
        scope_descriptions = {
            "api": "Full API access",
            "read_api": "Read-only API access",
            "read_user": "Read user information",
            "read_repository": "Read repository content",
            "write_repository": "Write to repository",
            "read_registry": "Read container registry",
            "write_registry": "Write container registry",
            "sudo": "Admin impersonation",
            "admin_mode": "Admin mode access",
            "create_runner": "Create CI/CD runners",
            "k8s_proxy": "Kubernetes proxy access",
            "ai_features": "AI features access",
        }

        permissions = []
        for scope in scopes:
            desc = scope_descriptions.get(scope, f"{scope} scope")
            permissions.append(desc)

        return permissions

    def _assess_gitlab_risk(
        self,
        scopes: list[str],
        projects: list[str],
        groups: list[str],
        identity: dict,
    ) -> RiskLevel:
        """Assess risk level based on token capabilities."""
        # Critical: Admin or sudo access
        if identity.get("is_admin") or "sudo" in scopes or "admin_mode" in scopes:
            return RiskLevel.CRITICAL

        # High: Full API access or write repository
        if "api" in scopes or "write_repository" in scopes:
            return RiskLevel.HIGH

        # Medium: Read access with multiple projects
        if len(projects) > 5 or groups:
            return RiskLevel.MEDIUM

        # Low: Read-only access
        if projects or scopes:
            return RiskLevel.LOW

        return RiskLevel.INFO

    def _get_exploitation_potential(self, result: ValidationResult) -> str:
        """Get exploitation potential description."""
        if result.risk_level == RiskLevel.CRITICAL:
            return "Critical - Admin access or sudo privileges"
        elif result.risk_level == RiskLevel.HIGH:
            return "High - Can push code to repositories"
        elif result.risk_level == RiskLevel.MEDIUM:
            return "Medium - Access to multiple projects and groups"
        elif result.risk_level == RiskLevel.LOW:
            return "Low - Read-only access to repositories"
        return "Minimal - Token has very limited permissions"

    def _get_attack_vectors(
        self,
        scopes: list[str],
        projects: list[str],
        groups: list[str],
    ) -> list[str]:
        """Identify potential attack vectors."""
        vectors = []

        if "api" in scopes or "write_repository" in scopes:
            vectors.append("Push malicious code to repositories")
            vectors.append("Modify CI/CD pipelines for supply chain attack")

        if projects:
            vectors.append("Clone private repositories")
            vectors.append("Access CI/CD secrets in pipeline configs")

        if "read_registry" in scopes or "write_registry" in scopes:
            vectors.append("Pull/push container images")
            vectors.append("Inject malicious container images")

        if "create_runner" in scopes:
            vectors.append("Create malicious CI/CD runners")

        if groups:
            vectors.append("Enumerate group members and projects")

        if "sudo" in scopes:
            vectors.append("Impersonate any user (admin)")
            vectors.append("Access all repositories as admin")

        return vectors
