# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""
GRC Integration Base Classes

Defines the common interfaces and types for GRC platform integrations.
"""

import logging
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any

import httpx

from ...http.retry import request_with_retry

logger = logging.getLogger(__name__)


class GRCAuthError(Exception):
    """Raised when GRC authentication fails, with credentials redacted."""

    pass


class GRCAPIError(Exception):
    """Raised when a GRC API call fails, with credentials redacted."""

    pass


_SAFE_ID_RE = re.compile(r"^[a-zA-Z0-9_\-]+$")


def validate_resource_id(value: str, name: str = "resource_id") -> str:
    """Validate that a resource ID is safe for URL path interpolation.

    Accepts alphanumeric characters, hyphens, and underscores only.
    Raises ValueError for empty or unsafe values.
    """
    if not value or not _SAFE_ID_RE.match(value):
        raise ValueError(f"Invalid {name}: must be alphanumeric/hyphens/underscores, got {value!r}")
    return value


def safe_raise_for_status(response: httpx.Response, context: str = "API call") -> None:
    """Check HTTP response status, raising a sanitized error on failure.

    Unlike response.raise_for_status(), this strips request body and
    auth headers from the exception to prevent credential leaks.
    """
    if response.is_success:
        return
    # Build a sanitized error message — never include request body
    raise GRCAPIError(f"GRC {context} failed: HTTP {response.status_code} from {response.url.host}")


class GRCPlatform(Enum):
    """Supported GRC platforms."""

    DRATA = "drata"
    VANTA = "vanta"
    HYPERPROOF = "hyperproof"


class EvidenceType(Enum):
    """Types of evidence that can be uploaded to GRC platforms."""

    FILE = "file"
    URL = "url"
    TEXT = "text"
    TEST_RESULT = "test_result"


class SyncStatus(Enum):
    """Status of a GRC sync operation."""

    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    SUCCESS = "success"
    FAILED = "failed"
    PARTIAL = "partial"


@dataclass
class GRCConfig:
    """Configuration for a GRC platform integration."""

    platform: GRCPlatform
    client_id: str
    client_secret: str
    organization_id: str
    enabled: bool = True
    auto_sync: bool = True
    sync_interval_hours: int = 24
    control_mappings: dict[str, str] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)

    # OAuth tokens (stored encrypted in production)
    access_token: str | None = None
    refresh_token: str | None = None
    token_expires_at: datetime | None = None


@dataclass
class GRCEvidence:
    """Evidence to be uploaded to a GRC platform."""

    evidence_id: str
    evidence_type: EvidenceType
    title: str
    description: str
    content: str  # Base64 encoded for files, raw for text
    file_name: str | None = None
    mime_type: str = "application/json"
    control_ids: list[str] = field(default_factory=list)
    collected_at: datetime = field(default_factory=datetime.utcnow)
    metadata: dict[str, Any] = field(default_factory=dict)

    # MergeGuide-specific fields
    evaluation_id: str | None = None
    repository: str | None = None
    policy_id: str | None = None
    policy_name: str | None = None
    passed: bool | None = None
    violation_count: int = 0


@dataclass
class GRCControlMapping:
    """Mapping between MergeGuide policies and GRC platform controls."""

    mapping_id: str
    platform: GRCPlatform
    mergeguide_policy_id: str
    mergeguide_policy_name: str
    grc_control_id: str
    grc_control_name: str
    framework: str  # e.g., "SOC 2", "ISO 27001"
    framework_control: str  # e.g., "CC6.1", "8.25"
    enabled: bool = True
    created_at: datetime = field(default_factory=datetime.utcnow)


@dataclass
class GRCSyncResult:
    """Result of a GRC sync operation."""

    sync_id: str
    platform: GRCPlatform
    organization_id: str
    status: SyncStatus
    started_at: datetime
    completed_at: datetime | None = None
    evidence_uploaded: int = 0
    evidence_failed: int = 0
    errors: list[str] = field(default_factory=list)
    details: dict[str, Any] = field(default_factory=dict)


@dataclass
class AuthToken:
    """OAuth authentication token."""

    access_token: str
    token_type: str = "Bearer"
    expires_in: int = 3600
    refresh_token: str | None = None
    scope: str | None = None
    expires_at: datetime | None = None


class GRCAdapter(ABC):
    """
    Abstract base class for GRC platform adapters.

    Each GRC platform (Drata, Vanta, Hyperproof) implements this interface
    to provide platform-specific integration logic.
    """

    def __init__(self, config: GRCConfig):
        self.config = config
        self._access_token: str | None = None
        self._token_expires_at: datetime | None = None

    async def _request_with_retry(
        self,
        client: httpx.AsyncClient,
        method: str,
        url: str,
        **kwargs,
    ) -> httpx.Response:
        """Make an HTTP request with retry on 429 rate limits."""
        return await request_with_retry(client, method, url, **kwargs)

    async def __aenter__(self):
        """Support async context manager for proper resource cleanup."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Close HTTP client on context exit."""
        await self.close()

    async def close(self):  # noqa: B027
        """Close any underlying HTTP clients. Override in subclasses."""

    @property
    @abstractmethod
    def platform(self) -> GRCPlatform:
        """Return the GRC platform this adapter supports."""
        pass

    @property
    @abstractmethod
    def base_url(self) -> str:
        """Return the base URL for the platform API."""
        pass

    @abstractmethod
    async def authenticate(self) -> AuthToken:
        """
        Authenticate with the GRC platform.

        Returns:
            AuthToken with access credentials
        """
        pass

    @abstractmethod
    async def refresh_token(self) -> AuthToken:
        """
        Refresh the authentication token.

        Returns:
            AuthToken with refreshed credentials
        """
        pass

    @abstractmethod
    async def upload_evidence(self, evidence: GRCEvidence, control_id: str) -> dict[str, Any]:
        """
        Upload evidence to a specific control.

        Args:
            evidence: The evidence to upload
            control_id: The GRC platform control ID

        Returns:
            Upload result with evidence ID and status
        """
        pass

    @abstractmethod
    async def list_controls(self, framework: str | None = None) -> list[dict]:
        """
        List available controls from the GRC platform.

        Args:
            framework: Optional framework filter (e.g., "SOC 2")

        Returns:
            List of control definitions
        """
        pass

    @abstractmethod
    async def get_control(self, control_id: str) -> dict:
        """
        Get details for a specific control.

        Args:
            control_id: The control identifier

        Returns:
            Control details
        """
        pass

    @abstractmethod
    async def update_control_status(
        self, control_id: str, status: str, evidence_ids: list[str] | None = None
    ) -> dict:
        """
        Update the status of a control.

        Args:
            control_id: The control identifier
            status: New status (platform-specific)
            evidence_ids: Optional list of evidence IDs to link

        Returns:
            Update result
        """
        pass

    async def ensure_authenticated(self) -> str:
        """
        Ensure we have a valid authentication token.

        Returns:
            Valid access token
        """
        if self._access_token and self._token_expires_at:
            # Check if token is still valid (with 5 min buffer)
            from datetime import timedelta

            if datetime.utcnow() < self._token_expires_at - timedelta(minutes=5):
                return self._access_token

        # Need to authenticate or refresh
        if self.config.refresh_token:
            token = await self.refresh_token()
        else:
            token = await self.authenticate()

        self._access_token = token.access_token
        self._token_expires_at = token.expires_at
        return self._access_token

    def transform_evidence(self, evidence: GRCEvidence) -> dict:
        """
        Transform MergeGuide evidence to platform-specific format.

        Override in subclasses for platform-specific transformations.
        """
        return {
            "evidence_id": evidence.evidence_id,
            "title": evidence.title,
            "description": evidence.description,
            "content": evidence.content,
            "file_name": evidence.file_name,
            "mime_type": evidence.mime_type,
            "collected_at": evidence.collected_at.isoformat(),
            "metadata": {
                "source": "MergeGuide",
                "evaluation_id": evidence.evaluation_id,
                "repository": evidence.repository,
                "policy_id": evidence.policy_id,
                "policy_name": evidence.policy_name,
                "passed": evidence.passed,
                "violation_count": evidence.violation_count,
                **evidence.metadata,
            },
        }
