"""Remote scanner base class for Tru-Guard."""

import asyncio
import os
import threading
from typing import Any

import httpx

from trusys.scanners.base import BaseScanner
from trusys.types import ScanContext, ScanResult, ScanType
from trusys.utils.logging import Timer

# Shared sync HTTP client with connection pooling for low-latency requests
# Thread-safe singleton pattern
_sync_http_client: httpx.Client | None = None
_sync_client_lock = threading.Lock()
_async_http_clients: dict[int, httpx.AsyncClient] = {}
_async_client_lock = threading.Lock()


def _get_sync_http_client() -> httpx.Client:
    """Get or create the shared sync HTTP client with connection pooling."""
    global _sync_http_client
    if _sync_http_client is None or _sync_http_client.is_closed:
        with _sync_client_lock:
            # Double-check after acquiring lock
            if _sync_http_client is None or _sync_http_client.is_closed:
                _sync_http_client = httpx.Client(
                    timeout=httpx.Timeout(30.0, connect=5.0),
                    limits=httpx.Limits(
                        max_keepalive_connections=20,
                        max_connections=100,
                        keepalive_expiry=30.0,
                    ),
                )
    return _sync_http_client


def close_http_client() -> None:
    """Close the shared HTTP client. Call during application shutdown."""
    global _sync_http_client
    if _sync_http_client is not None and not _sync_http_client.is_closed:
        _sync_http_client.close()
        _sync_http_client = None


def _get_async_http_client() -> httpx.AsyncClient:
    """Get or create a shared async HTTP client for the current event loop."""
    loop = asyncio.get_running_loop()
    loop_id = id(loop)
    client = _async_http_clients.get(loop_id)
    if client is None or client.is_closed:
        with _async_client_lock:
            client = _async_http_clients.get(loop_id)
            if client is None or client.is_closed:
                client = httpx.AsyncClient(
                    timeout=httpx.Timeout(30.0, connect=5.0),
                    limits=httpx.Limits(
                        max_keepalive_connections=20,
                        max_connections=100,
                        keepalive_expiry=30.0,
                    ),
                )
                _async_http_clients[loop_id] = client
    return client


async def close_async_http_clients() -> None:
    """Close all shared async HTTP clients. Call during application shutdown."""
    if not _async_http_clients:
        return
    clients = list(_async_http_clients.values())
    _async_http_clients.clear()
    for client in clients:
        if not client.is_closed:
            await client.aclose()


class BaseRemoteScanner(BaseScanner):
    """Base class for scanners that execute remotely via API.

    Remote scanners are executed on a remote server via API calls.
    Multiple remote scanners can be batched into a single API call
    for efficiency.

    Attributes:
        is_remote: Always True for remote scanners
        scanner_type: The type identifier for this scanner (e.g., "content-safety")
    """

    is_remote: bool = True
    scanner_type: str

    def __init__(
        self,
        config: dict[str, Any] | None = None,
        on_fail: str | None = None,
        enabled: bool | None = None,
    ) -> None:
        """Initialize the remote scanner.

        Args:
            config: JSON configuration dictionary with scanner-specific parameters
            on_fail: Action to take when scan fails
            enabled: Whether this scanner is enabled
        """
        super().__init__(config=config, on_fail=on_fail, enabled=enabled)
        # Ensure scanner_type is set by subclass
        if not hasattr(self, "scanner_type") or not self.scanner_type:
            raise ValueError(
                f"{self.__class__.__name__} must define a 'scanner_type' class attribute"
            )

    def scan(self, content: dict[str, Any], context: ScanContext) -> ScanResult:
        """Perform remote scan via API.

        Note: This method should not be called directly for remote scanners.
        Remote scanners are batched and executed together via the execution runner.

        Args:
            content: JSON dictionary containing content to scan
            context: The scan context

        Returns:
            ScanResult with the scan outcome

        Raises:
            NotImplementedError: Remote scanners must be executed via batch API
        """
        raise NotImplementedError(
            "Remote scanners must be executed via batch API call. "
            "Use ExecutionRunner which handles batching automatically."
        )

    async def scan_async(self, content: dict[str, Any], context: ScanContext) -> ScanResult:
        """Perform remote scan via API (async).

        Note: This method should not be called directly for remote scanners.
        Remote scanners are batched and executed together via the execution runner.

        Args:
            content: JSON dictionary containing content to scan
            context: The scan context

        Returns:
            ScanResult with the scan outcome

        Raises:
            NotImplementedError: Remote scanners must be executed via batch API
        """
        raise NotImplementedError(
            "Remote scanners must be executed via batch API call. "
            "Use ExecutionRunner which handles batching automatically."
        )

    @classmethod
    def execute_batch(
        cls,
        scanners: list["BaseRemoteScanner"],
        content: dict[str, Any],
        context: ScanContext,
        api_key: str | None = None,
        application_id: str | None = None,
    ) -> tuple[list[ScanResult], float]:
        """Execute multiple remote scanners in a single API call.

        This is a synchronous method that uses connection pooling for low latency.

        Args:
            scanners: List of remote scanner instances to execute
            content: Content dictionary to scan
            context: Scan context
            api_key: API key for authentication (optional, may be in config)
            application_id: Application ID (optional, required by some APIs).
                Falls back to TRUSYS_APPLICATION_ID env var if not provided.

        Returns:
            Tuple of (list of ScanResult objects, one per scanner; API call latency in ms).

        Raises:
            ValueError: If scanners list is empty or contains non-remote scanners
            httpx.HTTPError: If API call fails
        """
        if not scanners:
            return ([], 0.0)

        # Validate all scanners are remote
        for scanner in scanners:
            if not isinstance(scanner, BaseRemoteScanner) or not scanner.is_remote:
                raise ValueError(
                    f"All scanners must be remote scanners. "
                    f"Found non-remote scanner: {scanner.__class__.__name__}"
                )

        # Get API host from environment variable or use default
        api_host = os.getenv("TRUSYS_GUARDRAILS_HOST", "https://guard-api.trusys.ai")

        # Construct API URL for batch remote scanner execution
        api_url = f"{api_host.rstrip('/')}/execute"

        # Resolve application_id (required by some remote APIs)
        if application_id is None:
            application_id = os.getenv("TRUSYS_APPLICATION_ID", "")

        # Prepare batch request payload
        batch_payload = {
            "application_id": application_id,
            "scanners": [
                {
                    "scanner_type": scanner.scanner_type,
                    "config": scanner._config,
                }
                for scanner in scanners
            ],
            "scan_context": context.scan_type.value,  # "input" or "output"
            "content": content,
        }

        # Get API key from first scanner's config or parameter
        if not api_key:
            # Try to get from environment or scanner config
            api_key = os.getenv("TRUSYS_API_KEY", "")

        # Make sync POST request using shared httpx client with connection pooling.
        # Measure only the remote API call latency (request + response).
        headers = {
            "x-api-key": api_key if api_key else "",
            "Content-Type": "application/json",
        }
        client = _get_sync_http_client()
        with Timer() as api_timer:
            response = client.post(api_url, json=batch_payload, headers=headers)
        api_call_ms = api_timer.elapsed_ms
        try:
            response.raise_for_status()
            batch_response = response.json()
        except httpx.HTTPError as e:
            # If API call fails, return failed results for all scanners (with API call latency)
            from trusys.types import Severity, Violation
            # Include response body for 4xx/5xx to help debug (e.g. 422 validation errors)
            error_detail = str(e)
            resp = getattr(e, "response", None)
            if resp is not None:
                try:
                    body = resp.json()
                    error_detail = f"{e}. Response: {body}"
                except Exception:
                    if resp.text:
                        error_detail = f"{e}. Response body: {resp.text[:500]}"
            error_results = [
                ScanResult(
                    passed=False,
                    scanner_name=scanner.name,
                    scan_type=context.scan_type,
                    violations=[
                        Violation(
                            rule="remote_api_error",
                            severity=Severity.HIGH,
                            message=f"Remote scanner API call failed: {error_detail}",
                        )
                    ],
                    execution_time_ms=api_call_ms / len(scanners) if scanners else 0.0,
                    metadata={
                        "error": error_detail,
                        "scanner_type": scanner.scanner_type,
                        "assertion": {"type": scanner.scanner_type, "output": ""},
                        "score": 0.0,
                        "reason": f"Remote scanner API call failed: {error_detail}",
                        "latency": api_call_ms / len(scanners) if scanners else 0.0,
                        "action": {"type": scanner.on_fail.value if scanner.on_fail else "ignore"},
                    },
                )
                for scanner in scanners
            ]
            return (error_results, api_call_ms)

        results = cls._parse_batch_response(batch_response, scanners, context.scan_type)
        return (results, api_call_ms)

    @classmethod
    async def execute_batch_async(
        cls,
        scanners: list["BaseRemoteScanner"],
        content: dict[str, Any],
        context: ScanContext,
        api_key: str | None = None,
        application_id: str | None = None,
    ) -> tuple[list[ScanResult], float]:
        """Execute multiple remote scanners in a single async API call.

        Returns:
            Tuple of (list of ScanResult objects, one per scanner; API call latency in ms).
        """
        if not scanners:
            return ([], 0.0)

        # Validate all scanners are remote
        for scanner in scanners:
            if not isinstance(scanner, BaseRemoteScanner) or not scanner.is_remote:
                raise ValueError(
                    f"All scanners must be remote scanners. "
                    f"Found non-remote scanner: {scanner.__class__.__name__}"
                )

        api_host = os.getenv("TRUSYS_GUARDRAILS_HOST", "https://guard-api.trusys.ai")
        api_url = f"{api_host.rstrip('/')}/execute"

        if application_id is None:
            application_id = os.getenv("TRUSYS_APPLICATION_ID", "")

        batch_payload = {
            "application_id": application_id,
            "scanners": [
                {
                    "scanner_type": scanner.scanner_type,
                    "config": scanner._config,
                }
                for scanner in scanners
            ],
            "scan_context": context.scan_type.value,
            "content": content,
        }

        if not api_key:
            api_key = os.getenv("TRUSYS_API_KEY", "")

        headers = {
            "x-api-key": api_key if api_key else "",
            "Content-Type": "application/json",
        }
        client = _get_async_http_client()
        with Timer() as api_timer:
            response = await client.post(api_url, json=batch_payload, headers=headers)
        api_call_ms = api_timer.elapsed_ms
        try:
            response.raise_for_status()
            batch_response = response.json()
        except httpx.HTTPError as e:
            from trusys.types import Severity, Violation
            error_detail = str(e)
            resp = getattr(e, "response", None)
            if resp is not None:
                try:
                    body = resp.json()
                    error_detail = f"{e}. Response: {body}"
                except Exception:
                    if resp.text:
                        error_detail = f"{e}. Response body: {resp.text[:500]}"
            error_results = [
                ScanResult(
                    passed=False,
                    scanner_name=scanner.name,
                    scan_type=context.scan_type,
                    violations=[
                        Violation(
                            rule="remote_api_error",
                            severity=Severity.HIGH,
                            message=f"Remote scanner API call failed: {error_detail}",
                        )
                    ],
                    execution_time_ms=api_call_ms / len(scanners) if scanners else 0.0,
                    metadata={
                        "error": error_detail,
                        "scanner_type": scanner.scanner_type,
                        "assertion": {"type": scanner.scanner_type, "output": ""},
                        "score": 0.0,
                        "reason": f"Remote scanner API call failed: {error_detail}",
                        "latency": api_call_ms / len(scanners) if scanners else 0.0,
                        "action": {"type": scanner.on_fail.value if scanner.on_fail else "ignore"},
                    },
                )
                for scanner in scanners
            ]
            return (error_results, api_call_ms)

        results = cls._parse_batch_response(batch_response, scanners, context.scan_type)
        return (results, api_call_ms)

    @classmethod
    def _parse_batch_response(
        cls,
        batch_response: dict[str, Any],
        scanners: list["BaseRemoteScanner"],
        scan_type: ScanType,
    ) -> list[ScanResult]:
        """Parse a batch API response into ScanResult objects."""
        if not batch_response.get("success") or "data" not in batch_response:
            from trusys.types import Severity, Violation
            return [
                ScanResult(
                    passed=False,
                    scanner_name=scanner.name,
                    scan_type=scan_type,
                    violations=[
                        Violation(
                            rule="remote_api_format_error",
                            severity=Severity.HIGH,
                            message="Invalid API response format: missing 'success' or 'data' field",
                        )
                    ],
                    metadata={
                        "error": "Invalid API response format",
                        "scanner_type": scanner.scanner_type,
                        "assertion": {"type": scanner.scanner_type, "output": ""},
                        "score": 0.0,
                        "reason": "Invalid API response format: missing 'success' or 'data' field",
                        "latency": 0.0,
                        "action": {"type": scanner.on_fail.value if scanner.on_fail else "ignore"},
                    },
                )
                for scanner in scanners
            ]

        response_results = batch_response["data"].get("results", [])
        results: list[ScanResult] = []

        if len(response_results) != len(scanners):
            for i, scanner in enumerate(scanners):
                if i < len(response_results):
                    result_data = response_results[i]
                else:
                    result_data = {
                        "pass": False,
                        "score": 0.0,
                        "reason": "No result returned from remote API",
                        "assertion": {
                            "type": scanner.scanner_type,
                            "output": "",
                        },
                    }
                result = cls._parse_api_result(result_data, scanner, scan_type)
                results.append(result)
        else:
            for scanner, result_data in zip(scanners, response_results):
                result = cls._parse_api_result(result_data, scanner, scan_type)
                results.append(result)

        return results

    @classmethod
    def _parse_api_result(
        cls,
        result_data: dict[str, Any],
        scanner: "BaseRemoteScanner",
        scan_type: ScanType,
    ) -> ScanResult:
        """Parse API response result into ScanResult.

        Args:
            result_data: Result data from API response in new format:
                       { "pass": bool, "score": float, "reason": str, "assertion": {
                           "type": str, "output": {...}, "failures": [str, ...]  # optional
                       } }
            scanner: The scanner instance this result belongs to
            scan_type: The scan type

        Returns:
            ScanResult object. When "failures" is present, one violation per failure text
            is created with metadata["matched_text"] so actions (mask, filter, encrypt) can replace.
        """
        from trusys.types import Severity, Violation

        # Extract pass status from new format
        passed = result_data.get("pass", True)

        # Create violations: use failures array from assertion as matched_text (like local guardrails)
        # so subsequent actions (mask, filter, encrypt) can replace by content
        violations: list[Violation] = []
        assertion_data = result_data.get("assertion", {})
        failures = assertion_data.get("failures") if isinstance(assertion_data, dict) else None
        if not passed and failures and isinstance(failures, list):
            reason = result_data.get("reason", "Remote scanner check failed")
            for failure_text in failures:
                if failure_text is not None and isinstance(failure_text, str):
                    violations.append(
                        Violation(
                            rule="remote_scanner_check",
                            severity=Severity.HIGH,
                            message=reason,
                            metadata={"matched_text": failure_text},
                        )
                    )
        if not passed and not violations and "reason" in result_data:
            violations.append(
                Violation(
                    rule="remote_scanner_check",
                    severity=Severity.HIGH,
                    message=result_data.get("reason", "Remote scanner check failed"),
                )
            )

        # Build metadata in new format for componentResults
        metadata = result_data.get("metadata", {}).copy()
        
        # Store assertion from API response (assertion_data already read for failures)
        assertion = assertion_data if isinstance(assertion_data, dict) else {}
        if assertion:
            # Output as string with matched_text='...' format like local guardrails when we have failures
            if failures and isinstance(failures, list):
                output_parts = [
                    f"matched_text': '{ft}'"
                    for ft in failures
                    if ft is not None and isinstance(ft, str)
                ]
                output = "; ".join(output_parts) if output_parts else ""
            else:
                output = assertion.get("output")
                if isinstance(output, str):
                    pass  # keep as string
                elif output is not None and not isinstance(output, str):
                    import json
                    output = json.dumps(output)  # stringify for consistent saved format
            
            metadata["assertion"] = {
                "type": assertion.get("type", scanner.scanner_type),
                "output": output if output is not None else "",
            }
        else:
            # Fallback: create assertion from scanner type
            metadata["assertion"] = {
                "type": scanner.scanner_type,
                "output": "",
            }
        
        # Store other fields from new format
        metadata["score"] = result_data.get("score", 1.0 if passed else 0.0)
        metadata["reason"] = result_data.get("reason", "")
        api_latency_ms = result_data.get("latency", 0.0)
        metadata["latency"] = api_latency_ms
        metadata["action"] = {
            "type": scanner.on_fail.value if scanner.on_fail else "ignore"
        }
        if "tokensUsed" in result_data:
            metadata["tokensUsed"] = result_data["tokensUsed"]

        return ScanResult(
            passed=passed,
            scanner_name=scanner.name,
            scan_type=scan_type,
            violations=violations,
            suggested_fix=result_data.get("suggested_fix"),
            metadata=metadata,
            execution_time_ms=api_latency_ms
        )
