"""Guard class — the primary entry point for the meshulash-guard SDK.

Usage:
    from meshulash_guard import Guard

    guard = Guard(api_key="sk-xxx", tenant_id="meshulash-3lqgn")
    result = guard.scan_input(text, scanners=[pii_scanner])
    result2 = guard.scan_output(llm_response, scanners=[pii_scanner])
    restored = guard.deanonymize(result2.processed_text)
"""

from __future__ import annotations

import logging

import requests as _requests

from ._http import _build_session
from .exceptions import AuthError, ConnectionError, ServerError, ValidationError
from .models import ScanResult

logger = logging.getLogger(__name__)


class Guard:
    """HTTP client wrapper for the Meshulash security engine.

    Initializes by building a requests.Session with auth headers and validating
    the API key via a lightweight health endpoint. Maintains a session-scoped
    placeholder vault for deanonymization.

    Args:
        api_key: Meshulash API key. Passed as ``X-Api-Key`` header.
        tenant_id: Tenant identifier. Passed as ``X-Tenant-Id`` header.
            Also used by the GCP load balancer to route requests to the
            correct tenant's Cloud Run instance.
        server_url: Override the default cloud server URL. Use for on-prem
            deployments. Defaults to ``https://app.meshulash.ai``.
        timeout: Request timeout in seconds. Defaults to 60 (NER + TC models
            can be slow on large text).

    Raises:
        AuthError: API key or tenant ID is invalid.
        ConnectionError: Could not reach the server.
        ServerError: Server returned 5xx during initialization.
    """

    DEFAULT_BASE_URL = "https://app.meshulash.ai"
    DEFAULT_TIMEOUT = 60

    def __init__(
        self,
        api_key: str,
        tenant_id: str,
        server_url: str | None = None,
        timeout: int = DEFAULT_TIMEOUT,
    ) -> None:
        self._base_url = (server_url or self.DEFAULT_BASE_URL).rstrip("/")
        self._timeout = timeout
        self._session = _build_session(api_key=api_key, tenant_id=tenant_id)
        # Session-scoped placeholder vault for deanonymize()
        self._vault: dict[str, str] = {}
        # Fail fast on invalid credentials
        self._validate_credentials()

    def _validate_credentials(self) -> None:
        """Validate API key via lightweight GET to /api/security/health/auth.

        Raises:
            AuthError: Server returns 401.
            ServerError: Server returns 5xx.
            ConnectionError: Cannot reach the server.
        """
        url = f"{self._base_url}/api/security/health/auth"
        try:
            resp = self._session.get(url, timeout=self._timeout)
        except _requests.exceptions.ConnectionError as exc:
            raise ConnectionError(
                f"Cannot reach server: {self._base_url}"
            ) from exc
        except _requests.exceptions.Timeout as exc:
            raise ConnectionError(
                f"Timed out connecting to server: {self._base_url}"
            ) from exc

        if resp.status_code == 401:
            raise AuthError("Invalid API key or tenant ID")
        if resp.status_code >= 500:
            raise ServerError(
                f"Server unavailable during initialization (HTTP {resp.status_code})",
                resp.status_code,
            )

    def _scan(self, text: str, scanners: list) -> ScanResult:
        """Send text + scanners to server, accumulate placeholders, return result.

        Builds guardline_specs from scanners, POSTs to /api/security/scan,
        handles all error cases, parses the response, accumulates placeholders
        into the session vault, and returns the ScanResult.

        Args:
            text: The text to scan.
            scanners: List of scanner instances (BaseScanner subclasses).

        Returns:
            ScanResult: Parsed scan result with status, processed_text,
                placeholders, per-scanner breakdown, and risk categories.

        Raises:
            AuthError: API key was rejected.
            ValidationError: Request payload failed server-side validation.
            ServerError: Server returned 5xx.
            ConnectionError: Could not reach the server or request timed out.
        """
        guardline_specs: dict[str, dict] = {}
        for i, scanner in enumerate(scanners):
            prefix = f"sdk_{scanner.__class__.__name__.lower()}_{i}"
            if hasattr(scanner, "to_guardline_specs"):
                multi_specs = scanner.to_guardline_specs()
                for key, spec in multi_specs.items():
                    guardline_specs[f"{prefix}_{key}"] = spec
            else:
                guardline_specs[prefix] = scanner.to_guardline_spec()

        url = f"{self._base_url}/api/security/scan"
        payload = {"text": text, "guardline_specs": guardline_specs}

        try:
            resp = self._session.post(
                url,
                json=payload,  # sets Content-Type: application/json per-request
                timeout=self._timeout,
            )
        except _requests.exceptions.ConnectionError as exc:
            raise ConnectionError("Cannot connect to server") from exc
        except _requests.exceptions.Timeout as exc:
            raise ConnectionError(
                f"Request timed out after {self._timeout}s"
            ) from exc

        if resp.status_code == 401:
            raise AuthError("API key rejected by server")
        if resp.status_code == 422:
            raise ValidationError(f"Invalid request: {resp.text}")
        if resp.status_code >= 500:
            raise ServerError(
                f"Server error {resp.status_code}", resp.status_code
            )

        resp.raise_for_status()

        result = ScanResult.model_validate(resp.json())
        # Accumulate placeholders into session vault for deanonymize()
        self._vault.update(result.placeholders)
        return result

    def scan_input(self, text: str, scanners: list) -> ScanResult:
        """Scan input text through the given scanners.

        Translates each scanner's configuration into a guardline_spec and
        POSTs to /api/security/scan. Placeholders from the result are
        accumulated into the session vault for deanonymize().

        Args:
            text: The text to scan (e.g., user prompt before sending to LLM).
            scanners: List of scanner instances (BaseScanner subclasses).
                Each scanner must implement to_guardline_spec(). Scanners
                that also implement to_guardline_specs() (e.g., PIIScanner
                with per-label action overrides) will produce multiple
                guardline entries, one per action group.

        Returns:
            ScanResult: Parsed scan result with status, processed_text,
                placeholders, per-scanner breakdown, and risk categories.

        Raises:
            AuthError: API key was rejected.
            ValidationError: Request payload failed server-side validation.
            ServerError: Server returned 5xx.
            ConnectionError: Could not reach the server or request timed out.

        Delegates to _scan() for the actual HTTP call and vault accumulation.
        """
        return self._scan(text, scanners)

    def scan_output(self, text: str, scanners: list) -> ScanResult:
        """Scan LLM output text through the given scanners.

        Calls the same /api/security/scan endpoint as scan_input().
        Placeholders from the result are accumulated into the session vault
        for later deanonymize() calls.

        Args:
            text: The text to scan (e.g., LLM response before returning to user).
            scanners: List of scanner instances (BaseScanner subclasses).

        Returns:
            ScanResult: Parsed scan result with status, processed_text,
                placeholders, per-scanner breakdown, and risk categories.

        Raises:
            AuthError: API key was rejected.
            ValidationError: Request payload failed server-side validation.
            ServerError: Server returned 5xx.
            ConnectionError: Could not reach the server or request timed out.
        """
        return self._scan(text, scanners)

    def deanonymize(self, text: str) -> str:
        """Replace placeholders in text with original values from session vault.

        Uses exact-match semantics: if the LLM modifies a placeholder (e.g.,
        removes characters or changes case), that placeholder will not be
        restored. All occurrences of each known placeholder are replaced.

        Uses the accumulated vault of placeholder->original mappings from all
        scan_input() and scan_output() calls in this session. No server
        round-trip required.

        Args:
            text: Text containing placeholder tokens (e.g., LLM response after
                scanning input with Action.REPLACE).

        Returns:
            str: Text with all known placeholders replaced by original values.
        """
        if not self._vault:
            logger.debug(
                "deanonymize called but vault is empty -- no placeholders to restore"
            )
            return text

        result = text
        # Safe: original PII values cannot contain [LABEL-HASH] patterns
        for placeholder, original in sorted(
            self._vault.items(), key=lambda x: len(x[0]), reverse=True
        ):
            result = result.replace(placeholder, original)

        if result == text:
            logger.debug(
                "deanonymize found no known placeholders in the provided text"
            )
        return result

    def clear_cache(self) -> None:
        """Reset the session placeholder vault.

        Call this to discard accumulated placeholder mappings and start a
        fresh session.
        """
        self._vault.clear()
