"""
Abstract base class for AutoPIL catalog connectors.

Every catalog integration (Unity Catalog, Collibra, Alation, Purview, ...)
subclasses CatalogConnector and implements three methods:

  test_connection()         — verify credentials before saving
  fetch_classifications()   — pull sensitivity metadata from the catalog
  write_enforcement_event() — push enforcement events back for closed-loop lineage
"""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, ClassVar

if TYPE_CHECKING:
    from autopil.models import AuditEvent, CatalogClassification, CatalogConnection

logger = logging.getLogger(__name__)

# Canonical sensitivity label mapping used as the default for all connectors.
# Connectors may override map_label_to_sensitivity() to handle catalog-specific terms.
_DEFAULT_LABEL_MAP: dict[str, str] = {
    "public":        "low",
    "open":          "low",
    "internal":      "medium",
    "general":       "medium",
    "confidential":  "high",
    "sensitive":     "high",
    "pii":           "high",
    "phi":           "restricted",
    "financial":     "high",
    "restricted":    "restricted",
    "secret":        "restricted",
    "top_secret":    "restricted",
    "highly_confidential": "restricted",
}


class CatalogConnector(ABC):
    """
    Abstract base for all AutoPIL catalog connectors.

    Subclasses must set `catalog_type` as a ClassVar string — this is the
    dispatch key used by CatalogRegistry.get_connector().

    Usage:
        class UnityCatalogConnector(CatalogConnector):
            catalog_type = "unity_catalog"
            ...
    """

    catalog_type: ClassVar[str]  # must be set on every concrete subclass

    def __init__(self, connection: CatalogConnection) -> None:
        self.connection = connection

    @abstractmethod
    def test_connection(self) -> tuple[bool, str]:
        """
        Verify credentials are valid and the catalog is reachable.

        Returns:
            (True, "")          — connection OK
            (False, message)    — failed; message describes the error

        Called synchronously before a new CatalogConnection is persisted.
        Should complete quickly (< 5 s) — it runs on the request thread.
        """

    @abstractmethod
    def fetch_classifications(
        self,
        asset_fqns: list[str] | None = None,
    ) -> list[CatalogClassification]:
        """
        Pull sensitivity classifications from the catalog.

        Args:
            asset_fqns: If provided, fetch only these assets (on-demand lookup).
                        If None, fetch the full catalog (used by the background scheduler).

        Returns:
            List of CatalogClassification dataclass instances, ready to upsert.

        Failures should raise an exception so the scheduler can record them.
        """

    @abstractmethod
    def write_enforcement_event(
        self,
        event: AuditEvent,
        classification: CatalogClassification | None,
    ) -> bool:
        """
        Write an AutoPIL enforcement decision back to the catalog for audit lineage.

        This is best-effort — callers log failures but never surface them.

        Returns:
            True  — write succeeded
            False — catalog does not support write-back, or the call failed
        """

    def map_label_to_sensitivity(self, raw_label: str) -> str:
        """
        Map a catalog-native label string to an AutoPIL SensitivityLevel value.

        Override in subclasses to handle catalog-specific label vocabularies.
        Returns one of: low | medium | high | restricted
        Default: medium (safe fallback — never silently downgrades to low).
        """
        normalised = raw_label.lower().strip().replace(" ", "_").replace("-", "_")
        return _DEFAULT_LABEL_MAP.get(normalised, "medium")
