"""
Informatica Intelligent Data Management Cloud (IDMC) connector for AutoPIL.

Reads asset sensitivity classifications from the Cloud Data Governance and
Catalog (CDGC) REST API and maps them to AutoPIL CatalogClassification records.

Install:
    pip install autopil[catalog-informatica]

Required credentials (passed as 'credentials' dict when registering connection):
    client_id:     IICS OAuth2 client ID
    client_secret: IICS OAuth2 client secret
    org_id:        Informatica organization / pod ID

Optional credentials:
    pod_url:    Custom pod base URL (default: https://dm-us.informaticacloud.com)
    page_size:  Number of assets to fetch per page (default: 200)

Sensitivity mapping:
    Informatica CDGC sensitivity levels mapped to AutoPIL scale:
        "Highly Restricted" / "Restricted"  → restricted
        "Confidential" / "Sensitive"        → high
        "Internal" / "Internal Use"         → medium
        "Public"                            → low

Write-back:
    Enforcement events are written as custom attribute patches on the CDGC asset.
    Best-effort and fail-open.
"""

from __future__ import annotations

import json
import logging
import os
import secrets
from datetime import datetime, timedelta
from typing import TYPE_CHECKING

try:
    import requests
except ImportError as exc:
    raise ImportError(
        "requests is required for the Informatica connector. "
        "Install with: pip install autopil[catalog-informatica]"
    ) from exc

from autopil.catalog.base import CatalogConnector
from autopil.catalog.registry import CatalogRegistry
from autopil.models import CatalogClassification, CatalogConnection

if TYPE_CHECKING:
    from autopil.models import AuditEvent

logger = logging.getLogger(__name__)

_DEFAULT_POD_URL = "https://dm-us.informaticacloud.com"
_LOGIN_PATH = "/ma/api/v2/user/login"
_CDGC_SEARCH_PATH = "/ccgf-search/api/v1/search"
_CDGC_ASSET_PATH = "/ccgf-assets/api/v1/assets"

_SENSITIVITY_MAP: dict[str, str] = {
    "highly restricted": "restricted",
    "restricted": "restricted",
    "confidential": "high",
    "sensitive": "high",
    "internal use only": "medium",
    "internal use": "medium",
    "internal": "medium",
    "public": "low",
}


def _creds(connection: CatalogConnection) -> dict:
    raw = connection.credentials_enc
    enc_key = os.environ.get("AUTOPIL_CATALOG_ENCRYPTION_KEY")
    if enc_key:
        try:
            from cryptography.fernet import Fernet
            return json.loads(Fernet(enc_key.encode()).decrypt(raw.encode()))
        except Exception:
            pass
    return json.loads(raw)


class InformaticaConnector(CatalogConnector):
    """
    AutoPIL connector for Informatica IDMC / CDGC.

    Uses the IICS REST API for OAuth2 login and the CDGC Search API to
    enumerate data assets with sensitivity classifications.
    """

    catalog_type = "informatica"

    def _pod_url(self) -> str:
        creds = _creds(self.connection)
        return creds.get("pod_url", self.connection.base_url.rstrip("/") or _DEFAULT_POD_URL)

    def _get_access_token(self) -> str:
        """Obtain a session token via IICS login API."""
        creds = _creds(self.connection)
        resp = requests.post(
            f"{_DEFAULT_POD_URL}{_LOGIN_PATH}",
            json={
                "username": creds["client_id"],
                "password": creds["client_secret"],
            },
            timeout=15,
        )
        resp.raise_for_status()
        data = resp.json()
        # IICS login returns sessionId and products[].baseApiUrl for the pod
        session_id = data.get("userInfo", {}).get("sessionId") or data.get("sessionId", "")
        pod_url = data.get("products", [{}])[0].get("baseApiUrl", "")
        return session_id, pod_url

    def _session(self) -> tuple[requests.Session, str]:
        """Return (session with auth headers, pod_base_url)."""
        session_id, pod_url = self._get_access_token()
        s = requests.Session()
        s.headers.update({
            "INFA-SESSION-ID": session_id,
            "Accept": "application/json",
            "Content-Type": "application/json",
        })
        return s, pod_url.rstrip("/") if pod_url else self._pod_url()

    def test_connection(self) -> tuple[bool, str]:
        try:
            creds = _creds(self.connection)
            for key in ("client_id", "client_secret"):
                if key not in creds:
                    return False, f"Missing required credential: '{key}'"
            s, pod_url = self._session()
            # Probe CDGC with a minimal search
            resp = s.post(
                f"{pod_url}{_CDGC_SEARCH_PATH}",
                json={"offset": 0, "limit": 1},
                timeout=10,
            )
            resp.raise_for_status()
            return True, ""
        except ImportError as exc:
            return False, str(exc)
        except Exception as exc:
            return False, f"Informatica connection failed: {exc}"

    def fetch_classifications(
        self,
        asset_fqns: list[str] | None = None,
    ) -> list[CatalogClassification]:
        creds = _creds(self.connection)
        page_size = int(creds.get("page_size", 200))
        now = datetime.utcnow()

        try:
            s, pod_url = self._session()
        except Exception as exc:
            logger.error("Informatica auth failed during fetch: %s", exc)
            return []

        classifications: list[CatalogClassification] = []
        offset = 0

        while True:
            body: dict = {
                "offset": offset,
                "limit": page_size,
                "facets": [{"field": "sensitivityClassification"}],
            }
            if asset_fqns:
                body["query"] = " OR ".join(f'"{fqn}"' for fqn in asset_fqns)

            try:
                resp = s.post(f"{pod_url}{_CDGC_SEARCH_PATH}", json=body, timeout=30)
                resp.raise_for_status()
                data = resp.json()
            except Exception as exc:
                logger.warning("Informatica search failed (offset=%d): %s", offset, exc)
                break

            assets = data.get("hits", data.get("results", []))
            if not assets:
                break

            for asset in assets:
                cls = self._asset_to_classification(asset, now)
                if cls:
                    classifications.append(cls)

            total = data.get("total", data.get("totalCount", 0))
            offset += len(assets)
            if offset >= total or len(assets) < page_size:
                break

        logger.info("Informatica fetch complete: classifications=%d", len(classifications))
        return classifications

    def write_enforcement_event(
        self,
        event: AuditEvent,
        classification: CatalogClassification | None,
    ) -> bool:
        """
        Write enforcement event back to Informatica CDGC as a custom attribute patch.
        Best-effort and fail-open.
        """
        if classification is None:
            return False
        try:
            s, pod_url = self._session()
            asset_id = classification.asset_fqn  # CDGC may use FQN as ID
            payload = {
                "customAttributes": {
                    "autopil_last_event_id": event.event_id,
                    "autopil_last_decision": event.decision.value,
                    "autopil_agent_role": event.agent_role,
                    "autopil_policy_name": event.policy_name,
                    "autopil_timestamp": event.timestamp.isoformat(),
                }
            }
            resp = s.patch(
                f"{pod_url}{_CDGC_ASSET_PATH}/{asset_id}",
                json=payload,
                timeout=10,
            )
            return resp.status_code in (200, 204)
        except Exception as exc:
            logger.debug("Informatica write-back failed: %s", exc)
            return False

    def map_label_to_sensitivity(self, raw_label: str) -> str:
        normalized = raw_label.strip().lower()
        return _SENSITIVITY_MAP.get(normalized, super().map_label_to_sensitivity(raw_label))

    # ── internal helpers ──────────────────────────────────────────────────────

    def _asset_to_classification(
        self, asset: dict, now: datetime
    ) -> CatalogClassification | None:
        """Convert a CDGC search hit to a CatalogClassification."""
        try:
            fqn = (
                asset.get("qualifiedName")
                or asset.get("name")
                or asset.get("id", "")
            )
            if not fqn:
                return None

            raw_label = (
                asset.get("sensitivityClassification")
                or asset.get("sensitivity")
                or asset.get("dataClassification")
                or "unclassified"
            )
            sensitivity = self.map_label_to_sensitivity(raw_label)

            data_classes: list[str] = []
            for dc_field in ("dataClass", "dataClasses", "informationTypes"):
                val = asset.get(dc_field)
                if isinstance(val, list):
                    data_classes.extend(str(v) for v in val if v)
                elif isinstance(val, str) and val:
                    data_classes.append(val)

            asset_type = self._map_asset_type(asset.get("type") or asset.get("assetType", "table"))

            return CatalogClassification(
                classification_id="cls_" + secrets.token_hex(8),
                connection_id=self.connection.connection_id,
                tenant_id=self.connection.tenant_id,
                asset_fqn=fqn,
                asset_type=asset_type,
                sensitivity_label=sensitivity,
                raw_label=raw_label,
                data_classes=data_classes,
                is_certified=asset.get("isCertified", False),
                trust_score=None,
                catalog_policy_ids=asset.get("policyIds", []),
                fetched_at=now,
                expires_at=now + timedelta(minutes=self.connection.sync_interval_minutes),
            )
        except Exception as exc:
            logger.debug("Skipping Informatica asset (parse error): %s", exc)
            return None

    def _map_asset_type(self, raw: str) -> str:
        raw = (raw or "").lower()
        if "column" in raw:
            return "column"
        if "schema" in raw:
            return "schema"
        return "table"


# Register on import
CatalogRegistry.register(InformaticaConnector)
