"""Main TruGuard class - entry point for the library."""

import logging
import os
import threading
import time
from typing import Any, Literal

import httpx

from trusys.core.config import GuardConfig
from trusys.exceptions import ConfigurationError, ScanFailedError
from trusys.execution.runner import ExecutionRunner
from trusys.scanners.base import BaseScanner
from trusys.scanners.registry import ScannerRegistry
from trusys.types import (
    AggregatedResult,
    ConflictStrategy,
    ExecutionMode,
    ScanContext,
    ScanType,
)
from trusys.results.formatter import ResultFormatter
from trusys.results.uploader import ResultUploader
from trusys.utils.helpers import merge_metadata
from trusys.utils.logging import get_logger


class _GuardDescriptor:
    """Descriptor that lazily creates an empty TruGuard when accessed before init()."""

    def __init__(self, guard_type: ScanType, storage_attr: str) -> None:
        self.guard_type = guard_type
        self.storage_attr = storage_attr
        self._owner: type | None = None

    def __get__(self, obj: Any, owner: type) -> "TruGuard":
        if obj is not None:
            return self.__get__(None, type(obj))
        inst = getattr(owner, self.storage_attr, None)
        if inst is None:
            inst = owner(type=self.guard_type)
            setattr(owner, self.storage_attr, inst)
        return inst

    def __set__(self, obj: Any, value: "TruGuard") -> None:
        owner = obj if obj is not None else self._owner
        if owner is not None:
            setattr(owner, self.storage_attr, value)

    def __set_name__(self, owner: type, name: str) -> None:
        self._owner = owner


class TruGuard:
    """Main entry point for Tru-Guard scanning.

    TruGuard manages a collection of scanners and orchestrates their
    execution to scan content for various types of violations.

    Example:
        ```python
        from trusys import TruGuard
        from trusys.scanners import ContentSafety, ToxicityCheck

        # Create guard for input scanning
        input_guard = TruGuard(type="input")
        input_guard.add_scanners([
            ContentSafety(threshold=0.9, on_fail="fix")
        ])

        result = input_guard.scan(content={"prompt": "user input"})

        # Create guard for output scanning
        output_guard = TruGuard(type="output")
        output_guard.add_scanners([
            ToxicityCheck(model="advanced", on_fail="filter")
        ])

        result = output_guard.scan(content={"prompt": "user input", "response": "llm output"})
        ```

    Example with API initialization:
        ```python
        from trusys import TruGuard

        # Option 1: Initialize with explicit parameters
        TruGuard.init(
            application_id="1234567890",
            api_key="your-api-key",
            batch_interval=60.0,  # Upload every 60 seconds
            batch_count=100,      # Or when 100 results are queued
        )

        # Option 2: Initialize using environment variables
        # Set TRUSYS_APPLICATION_ID, TRUSYS_API_KEY, TRUSYS_BATCH_INTERVAL, TRUSYS_BATCH_COUNT
        TruGuard.init()

        # Option 3: Mixed approach (parameter takes precedence)
        TruGuard.init(application_id="1234567890")  # api_key from TRUSYS_API_KEY

        # Use the pre-configured guards
        result = TruGuard.input_guard.scan(content={"prompt": "user input"})
        result = TruGuard.output_guard.scan(content={"prompt": "user input", "response": "llm output"})

        # Results are automatically batched and uploaded to TRUSYS backend
        # Flush remaining results on shutdown (automatic via atexit)
        ```
    """

    # Class-level fields for API-initialized guards (lazy: empty guards until init() runs)
    _input_guard_instance: "TruGuard | None" = None
    _output_guard_instance: "TruGuard | None" = None
    input_guard = _GuardDescriptor(ScanType.INPUT, "_input_guard_instance")
    output_guard = _GuardDescriptor(ScanType.OUTPUT, "_output_guard_instance")
    _api_key: str | None = None  # Store API key for remote scanners
    _application_id: str | None = None  # Store application ID
    _application_version: str | None = None  # Store application version
    _environment: str | None = None  # Store deployment environment
    _metadata: dict[str, Any] | None = None  # Store additional metadata
    _uploader: "ResultUploader | None" = None  # Result uploader for batch uploads
    _debug_print: bool = False  # Whether to print results to console (adds latency)
    _api_host: str | None = None  # API host for config refresh
    _config_refresh_interval: float = 300.0  # Seconds between config refreshes (5 min)
    _config_refresh_stop_event: "threading.Event | None" = None
    _config_refresh_thread: "threading.Thread | None" = None

    @classmethod
    def init(
        cls,
        application_id: str | None = None,
        api_key: str | None = None,
        application_version: str | None = None,
        environment: str | None = None,
        metadata: dict[str, Any] | None = None,
        batch_interval: float | None = None,
        batch_count: int | None = None,
        debug_print: bool | None = None,
    ) -> None:
        """Initialize TruGuard with configuration from API.

        Fetches guardrail configuration from the API and sets up input_guard
        and output_guard instances with the configured scanners.

        Args:
            application_id: Application ID for the guardrail configuration.
                If not provided, will be read from TRUSYS_APPLICATION_ID environment variable.
            api_key: API key for authentication.
                If not provided, will be read from TRUSYS_API_KEY environment variable.
            application_version: Optional application version.
                If not provided, will be read from TRUSYS_APPLICATION_VERSION environment variable.
            environment: Optional deployment environment (e.g. "dev", "staging", "prod").
                If not provided, will be read from TRUSYS_ENVIRONMENT environment variable.
            metadata: Optional dictionary of additional metadata to attach to results.
            batch_interval: Time interval in seconds between batch uploads (default: 60).
                If not provided, will be read from TRUSYS_BATCH_INTERVAL environment variable.
            batch_count: Maximum number of results to batch before uploading (default: 100).
                If not provided, will be read from TRUSYS_BATCH_COUNT environment variable.
            debug_print: Whether to print results to console (default: False).
                If not provided, will be read from TRUSYS_DEBUG_PRINT environment variable.
                Set to True only for debugging as it adds latency.

        On failure (missing config, API error, auth error), logs an error and returns
        without raising, so host applications are not blocked from loading.
        """
        logger = get_logger()
        # Get application_id from parameter or environment variable
        if application_id is None:
            application_id = os.getenv("TRUSYS_APPLICATION_ID")
        if application_id is None:
            logger.error(
                "TruGuard init failed: application_id is required. "
                "Provide it as a parameter or set TRUSYS_APPLICATION_ID environment variable."
            )
            logger.info("TruGuard initialization aborted.")
            return

        # Get api_key from parameter or environment variable
        if api_key is None:
            api_key = os.getenv("TRUSYS_API_KEY")
        if api_key is None:
            logger.error(
                "TruGuard init failed: api_key is required. "
                "Provide it as a parameter or set TRUSYS_API_KEY environment variable."
            )
            logger.info("TruGuard initialization aborted.")
            return
        
        # Get optional application version and environment
        if application_version is None:
            application_version = os.getenv("TRUSYS_APPLICATION_VERSION")
        if environment is None:
            environment = os.getenv("TRUSYS_ENVIRONMENT")

        # Get batch configuration from parameters or environment variables
        if batch_interval is None:
            batch_interval_str = os.getenv("TRUSYS_BATCH_INTERVAL")
            batch_interval = float(batch_interval_str) if batch_interval_str else 60.0
        if batch_count is None:
            batch_count_str = os.getenv("TRUSYS_BATCH_COUNT")
            batch_count = int(batch_count_str) if batch_count_str else 100

        # Get debug_print flag from parameter or environment variable (default: False)
        if debug_print is None:
            debug_print_str = os.getenv("TRUSYS_DEBUG_PRINT", "").lower()
            debug_print = debug_print_str in ("true", "1", "yes")
        cls._debug_print = debug_print
        # When debug_print is True, enable DEBUG-level logs (scan started/completed, etc.)
        get_logger().set_level(logging.DEBUG if debug_print else logging.INFO)

        # Get API host from environment variable or use default
        api_host = os.getenv("TRUSYS_API_HOST", "https://backend.trusys.ai")
        
        # Construct API URL
        api_url = f"{api_host.rstrip('/')}/api/guardrails/application-config?applicationId={application_id}"

        # Make GET request to fetch configuration
        try:
            headers = {
                "x-api-key": f"{api_key}",
                "Content-Type": "application/json",
            }
            response = httpx.get(api_url, headers=headers, timeout=10.0)
            response.raise_for_status()
            config_data = response.json()
        except httpx.HTTPError as e:
            logger.error(f"TruGuard init failed: failed to fetch guardrail configuration from API: {e}")
            logger.info("TruGuard initialization aborted.")
            return
        except ValueError as e:
            logger.error(f"TruGuard init failed: invalid JSON response from API: {e}")
            logger.info("TruGuard initialization aborted.")
            return

        # Authenticate with guardrails host (GET /authenticate with x-api-key and x-application-id)
        guardrails_host = os.getenv("TRUSYS_GUARDRAILS_HOST", "https://guard-api.trusys.ai")
        auth_url = f"{guardrails_host.rstrip('/')}/authenticate"
        try:
            auth_headers = {
                "x-api-key": api_key,
                "x-application-id": application_id,
            }
            auth_response = httpx.get(auth_url, headers=auth_headers, timeout=10.0)
            auth_response.raise_for_status()
            auth_data = auth_response.json()
            if not auth_data.get("success"):
                logger.error(
                    "TruGuard init failed: guardrails authentication failed - response did not return success"
                )
                logger.info("TruGuard initialization aborted.")
                return
        except httpx.HTTPError as e:
            logger.error(f"TruGuard init failed: failed to authenticate with guardrails: {e}")
            logger.info("TruGuard initialization aborted.")
            return
        except ValueError as e:
            logger.error(f"TruGuard init failed: invalid JSON response from authenticate endpoint: {e}")
            logger.info("TruGuard initialization aborted.")
            return

        # Store API key, application ID and related metadata for remote scanners and formatting
        cls._api_key = api_key
        cls._application_id = application_id
        cls._application_version = application_version
        cls._environment = environment
        cls._metadata = metadata
        cls._api_host = api_host

        # Stop existing uploader if re-initializing
        if cls._uploader is not None:
            cls._uploader.stop()

        # Stop existing config refresh thread if re-initializing
        if cls._config_refresh_stop_event is not None and cls._config_refresh_thread is not None:
            cls._config_refresh_stop_event.set()
            cls._config_refresh_thread.join(timeout=2.0)
            cls._config_refresh_stop_event = None
            cls._config_refresh_thread = None

        # Initialize result uploader for batch uploads
        cls._uploader = ResultUploader(
            api_host=api_host,
            api_key=api_key,
            batch_interval=batch_interval,
            batch_count=batch_count,
        )

        # Parse configuration and create guards
        input_validators = config_data.get("input_validators", [])
        output_validators = config_data.get("output_validators", [])

        # Create input guard
        input_guard = cls(type=ScanType.INPUT)
        input_scanners = cls._create_scanners_from_validators(input_validators)
        if input_scanners:
            input_guard.add_scanners(input_scanners)
        cls.input_guard = input_guard

        # Create output guard
        output_guard = cls(type=ScanType.OUTPUT)
        output_scanners = cls._create_scanners_from_validators(output_validators)
        if output_scanners:
            output_guard.add_scanners(output_scanners)
        cls.output_guard = output_guard

        # Start background thread to refresh guardrail config every 5 minutes
        cls._config_refresh_stop_event = threading.Event()
        cls._config_refresh_thread = threading.Thread(
            target=cls._config_refresh_loop,
            daemon=True,
            name="TruGuardConfigRefresh",
        )
        cls._config_refresh_thread.start()
        get_logger().debug("Guardrail config refresh thread started (interval=300s)")
        logger.info("TruGuard initialized successfully.")

    @classmethod
    def _fetch_guardrail_config(cls) -> dict[str, Any] | None:
        """Fetch guardrail configuration from the API.

        Returns:
            Config dict with input_validators and output_validators, or None on failure.
        """
        if cls._api_host is None or cls._api_key is None or cls._application_id is None:
            return None
        api_url = (
            f"{cls._api_host.rstrip('/')}/api/guardrails/application-config"
            f"?applicationId={cls._application_id}"
        )
        try:
            headers = {
                "x-api-key": cls._api_key,
                "Content-Type": "application/json",
            }
            response = httpx.get(api_url, headers=headers, timeout=10.0)
            response.raise_for_status()
            return response.json()
        except (httpx.HTTPError, ValueError) as e:
            get_logger().warning(
                "Failed to refresh guardrail config from API: %s. Keeping current scanners.",
                e,
            )
            return None

    @classmethod
    def _refresh_guardrail_config(cls) -> None:
        """Fetch latest config and update input/output guards' scanners."""
        config_data = cls._fetch_guardrail_config()
        if config_data is None:
            return
        input_validators = config_data.get("input_validators", [])
        output_validators = config_data.get("output_validators", [])
        input_scanners = cls._create_scanners_from_validators(input_validators)
        output_scanners = cls._create_scanners_from_validators(output_validators)
        # Replace scanner lists atomically so in-flight scans are unaffected
        cls.input_guard._scanners = input_scanners
        cls.output_guard._scanners = output_scanners
        get_logger().debug(
            f"Guardrail config refreshed: input_scanners={len(input_scanners)}, output_scanners={len(output_scanners)}"
        )

    @classmethod
    def _config_refresh_loop(cls) -> None:
        """Background loop that refreshes guardrail config every 5 minutes."""
        logger = get_logger()
        while cls._config_refresh_stop_event is not None and not cls._config_refresh_stop_event.is_set():
            # Wait 5 minutes or until stop is requested
            if cls._config_refresh_stop_event.wait(timeout=cls._config_refresh_interval):
                break
            try:
                cls._refresh_guardrail_config()
            except Exception as e:
                logger.warning(f"Error during guardrail config refresh: {e}")

    @classmethod
    def _create_scanners_from_validators(
        cls, validators: list[dict[str, Any]]
    ) -> list[BaseScanner]:
        """Create scanner instances from validator configurations.

        Uses the scanner registry to look up scanner classes by validator type,
        allowing for dynamic scanner registration without if/else chains.

        Args:
            validators: List of validator configuration dictionaries

        Returns:
            List of scanner instances

        Raises:
            ConfigurationError: If validator type is unknown or configuration is invalid
        """
        scanners: list[BaseScanner] = []
        logger = get_logger()

        for validator in validators:
            validator_type = validator.get("type")
            validator_config = validator.get("config", {})
            on_fail = validator.get("on_fail", "ignore")

            if not validator_type:
                logger.warning("Validator missing 'type' field. Skipping.")
                continue

            # Look up scanner class by validator type from registry
            scanner_class = ScannerRegistry.get_scanner_by_validator_type(validator_type)

            if scanner_class is None:
                logger.warning(
                    f"Unknown validator type '{validator_type}'. "
                    f"No scanner registered for this type. Skipping."
                )
                continue

            try:
                # Create scanner instance with the validator config
                scanner = scanner_class(config=validator_config, on_fail=on_fail)
                scanners.append(scanner)
            except Exception as e:
                logger.error(
                    f"Failed to create scanner for validator type '{validator_type}': {e}"
                )
                # Continue with other validators even if one fails
                continue

        return scanners

    def __init__(
        self,
        type: ScanType | Literal["input", "output"] = ScanType.INPUT,
        config: GuardConfig | None = None,
    ) -> None:
        """Initialize TruGuard.

        Args:
            type: The scan type this guard instance will handle ("input" or "output")
            config: Optional configuration. If not provided, defaults are used.

        Raises:
            ConfigurationError: If type is invalid
        """
        # Convert string type to enum if needed
        if isinstance(type, str):
            type = ScanType(type)

        if type not in (ScanType.INPUT, ScanType.OUTPUT):
            raise ConfigurationError(
                f"TruGuard type must be 'input' or 'output', got '{type.value}'"
            )

        self._type = type
        self._config = config or GuardConfig()
        self._scanners: list[BaseScanner] = []
        self._runner = ExecutionRunner(self._config)
        self._application_id: str | None = None  # Instance-level application ID

        # Configure logger based on config
        logger = get_logger()
        if not self._config.enable_logging:
            logger.disable()

    @property
    def type(self) -> ScanType:
        """Get the scan type this guard handles."""
        return self._type

    @property
    def config(self) -> GuardConfig:
        """Get the current configuration."""
        return self._config

    @property
    def scanners(self) -> list[BaseScanner]:
        """Get the list of registered scanners."""
        return self._scanners.copy()

    def configure(
        self,
        execution_mode: ExecutionMode | str | None = None,
        conflict_strategy: ConflictStrategy | str | None = None,
        timeout: float | None = None,
        early_exit: bool | None = None,
        enable_logging: bool | None = None,
        max_parallel_scanners: int | None = None,
        **kwargs: Any,
    ) -> "TruGuard":
        """Configure the TruGuard instance.

        Args:
            execution_mode: Execution mode (parallel, sequential, pipeline)
            conflict_strategy: Strategy for resolving action conflicts
            timeout: Per-scanner timeout in seconds
            early_exit: Stop on first failure (for pipeline mode)
            enable_logging: Enable/disable logging
            max_parallel_scanners: Maximum concurrent scanners
            **kwargs: Additional configuration options

        Returns:
            Self for method chaining
        """
        updates: dict[str, Any] = {}

        if execution_mode is not None:
            if isinstance(execution_mode, str):
                execution_mode = ExecutionMode(execution_mode)
            updates["execution_mode"] = execution_mode

        if conflict_strategy is not None:
            if isinstance(conflict_strategy, str):
                conflict_strategy = ConflictStrategy(conflict_strategy)
            updates["conflict_strategy"] = conflict_strategy

        if timeout is not None:
            if timeout <= 0:
                raise ConfigurationError("Timeout must be positive", "timeout")
            updates["timeout"] = timeout

        if early_exit is not None:
            updates["early_exit"] = early_exit

        if enable_logging is not None:
            updates["enable_logging"] = enable_logging
            logger = get_logger()
            if enable_logging:
                logger.enable()
            else:
                logger.disable()

        if max_parallel_scanners is not None:
            if max_parallel_scanners <= 0:
                raise ConfigurationError(
                    "max_parallel_scanners must be positive",
                    "max_parallel_scanners",
                )
            updates["max_parallel_scanners"] = max_parallel_scanners

        updates.update(kwargs)

        if updates:
            self._config = self._config.merge(**updates)
            self._runner.update_config(self._config)

        return self

    def add_scanners(self, scanners: list[BaseScanner]) -> "TruGuard":
        """Add multiple scanners to the guard.

        The scanners will only be added if they support the guard's scan type
        (INPUT or OUTPUT as specified during initialization).

        Args:
            scanners: List of scanner instances to add

        Returns:
            Self for method chaining

        Raises:
            ConfigurationError: If any scanner doesn't support the guard's scan type
        """
        for scanner in scanners:
            if not isinstance(scanner, BaseScanner):
                raise ConfigurationError(
                    f"Scanner must be a BaseScanner instance, got {type(scanner).__name__}"
                )

            if not scanner.supports_scan_type(self._type):
                raise ConfigurationError(
                    f"Scanner '{scanner.name}' does not support {self._type.value} scanning. "
                    f"Supported scan types: {[st.value for st in scanner.scan_types]}. "
                    f"Guard is configured for {self._type.value} scanning."
                )

            if scanner not in self._scanners:
                self._scanners.append(scanner)
        return self

    def remove_scanner(self, scanner_name: str) -> "TruGuard":
        """Remove a scanner by name.

        Args:
            scanner_name: Name of the scanner to remove

        Returns:
            Self for method chaining
        """
        self._scanners = [s for s in self._scanners if s.name != scanner_name]
        return self

    def clear_scanners(self) -> "TruGuard":
        """Remove all scanners.

        Returns:
            Self for method chaining
        """
        self._scanners.clear()
        return self

    def scan(
        self,
        content: dict[str, Any],
        metadata: dict[str, Any] | None = None,
    ) -> AggregatedResult:
        """Scan content synchronously.

        Args:
            content: JSON dictionary containing content to scan.
                   Should contain 'prompt' for input guards or 'response' for output guards.
                   Both 'prompt' and 'response' can be included and will be available to scanners.
            metadata: Optional metadata for this scan (e.g. conversation_id, session_id).
                   Merged with metadata from TruGuard.init(); scan-level keys override init keys.

        Returns:
            Aggregated result from all scanners

        Raises:
            ConfigurationError: If no scanners are configured or required parameters are missing
            ScanFailedError: If a scanner fails and action is FAIL
        """
        merged_metadata = merge_metadata(
            TruGuard._metadata or {},
            metadata or {},
        )
        if not self._scanners:
            raise ConfigurationError(
                f"No validators are configured for {self._type.value} scanning. "
                "Ensure TruGuard.init() was called successfully with valid credentials."
            )

        # Extract prompt and response from content dict
        prompt = content.get("prompt")
        response = content.get("response")

        # Validate required parameters based on guard type
        if self._type == ScanType.INPUT:
            if not prompt:
                raise ConfigurationError(
                    "'prompt' is required in content dict for input scanning",
                    "prompt",
                )
            scan_context = ScanContext(
                scan_type=ScanType.INPUT,
                prompt=prompt,
                response=response,
            )
        else:  # OUTPUT
            if not response:
                raise ConfigurationError(
                    "'response' is required in content dict for output scanning",
                    "response",
                )
            scan_context = ScanContext(
                scan_type=ScanType.OUTPUT,
                prompt=prompt,
                response=response,
            )

        try:
            aggregated_result = self._runner.run(self._scanners, scan_context)
        except ScanFailedError as e:
            # Save failed result so it is uploaded even if caller does not catch and app exits
            if getattr(e, "aggregated_result", None) is not None and TruGuard._uploader is not None:
                app_id = self._application_id or TruGuard._application_id
                TruGuard._uploader.queue_aggregated_result(
                    e.aggregated_result,
                    scan_context,
                    application_id=app_id,
                    application_version=TruGuard._application_version,
                    environment=TruGuard._environment,
                    metadata=merged_metadata,
                )
                TruGuard._uploader.flush()
            raise

        app_id = self._application_id or TruGuard._application_id
        formatted_result = None
        if TruGuard._debug_print:
            formatted_result = ResultFormatter.format_for_backend(
                aggregated_result,
                scan_context,
                application_id=app_id,
                application_version=TruGuard._application_version,
                environment=TruGuard._environment,
                metadata=merged_metadata,
            )

        # Print for debugging (disabled by default for performance)
        if TruGuard._debug_print and formatted_result is not None:
            ResultFormatter.print_formatted_result(
                aggregated_result,
                scan_context,
                application_id=app_id,
                application_version=TruGuard._application_version,
                environment=TruGuard._environment,
                metadata=merged_metadata,
            )

        # Queue result for batch upload if uploader is initialized
        if TruGuard._uploader is not None:
            if formatted_result is not None:
                TruGuard._uploader.queue_result(formatted_result)
            else:
                TruGuard._uploader.queue_aggregated_result(
                    aggregated_result,
                    scan_context,
                    application_id=app_id,
                    application_version=TruGuard._application_version,
                    environment=TruGuard._environment,
                    metadata=merged_metadata,
                )
        
        return aggregated_result

    async def scan_async(
        self,
        content: dict[str, Any],
        metadata: dict[str, Any] | None = None,
    ) -> AggregatedResult:
        """Scan content asynchronously.

        Args:
            content: JSON dictionary containing content to scan.
                   Should contain 'prompt' for input guards or 'response' for output guards.
                   Both 'prompt' and 'response' can be included and will be available to scanners.
            metadata: Optional metadata for this scan (e.g. conversation_id, session_id).
                   Merged with metadata from TruGuard.init(); scan-level keys override init keys.

        Returns:
            Aggregated result from all scanners

        Raises:
            ConfigurationError: If no scanners are configured or required parameters are missing
            ScanFailedError: If a scanner fails and action is FAIL
        """
        merged_metadata = merge_metadata(
            TruGuard._metadata or {},
            metadata or {},
        )
        if not self._scanners:
            raise ConfigurationError(
                f"No validators are configured for {self._type.value} scanning. "
                "Ensure TruGuard.init() was called successfully with valid credentials."
            )

        # Extract prompt and response from content dict
        prompt = content.get("prompt")
        response = content.get("response")

        # Validate required parameters based on guard type
        if self._type == ScanType.INPUT:
            if not prompt:
                raise ConfigurationError(
                    "'prompt' is required in content dict for input scanning",
                    "prompt",
                )
            scan_context = ScanContext(
                scan_type=ScanType.INPUT,
                prompt=prompt,
                response=response,
            )
        else:  # OUTPUT
            if not response:
                raise ConfigurationError(
                    "'response' is required in content dict for output scanning",
                    "response",
                )
            scan_context = ScanContext(
                scan_type=ScanType.OUTPUT,
                prompt=prompt,
                response=response,
            )

        try:
            aggregated_result = await self._runner.run_async(self._scanners, scan_context)
        except ScanFailedError as e:
            # Save failed result so it is uploaded even if caller does not catch and app exits
            if getattr(e, "aggregated_result", None) is not None and TruGuard._uploader is not None:
                app_id = self._application_id or TruGuard._application_id
                TruGuard._uploader.queue_aggregated_result(
                    e.aggregated_result,
                    scan_context,
                    application_id=app_id,
                    application_version=TruGuard._application_version,
                    environment=TruGuard._environment,
                    metadata=merged_metadata,
                )
                TruGuard._uploader.flush()
            raise

        app_id = self._application_id or TruGuard._application_id
        formatted_result = None
        if TruGuard._debug_print:
            formatted_result = ResultFormatter.format_for_backend(
                aggregated_result,
                scan_context,
                application_id=app_id,
                application_version=TruGuard._application_version,
                environment=TruGuard._environment,
                metadata=merged_metadata,
            )

        # Print for debugging (disabled by default for performance)
        if TruGuard._debug_print and formatted_result is not None:
            ResultFormatter.print_formatted_result(
                aggregated_result,
                scan_context,
                application_id=app_id,
                application_version=TruGuard._application_version,
                environment=TruGuard._environment,
                metadata=merged_metadata,
            )

        # Queue result for batch upload if uploader is initialized
        if TruGuard._uploader is not None:
            if formatted_result is not None:
                TruGuard._uploader.queue_result(formatted_result)
            else:
                TruGuard._uploader.queue_aggregated_result(
                    aggregated_result,
                    scan_context,
                    application_id=app_id,
                    application_version=TruGuard._application_version,
                    environment=TruGuard._environment,
                    metadata=merged_metadata,
                )
        
        return aggregated_result

    def get_scanner(self, name: str) -> BaseScanner | None:
        """Get a scanner by name.

        Args:
            name: Name of the scanner

        Returns:
            Scanner instance or None if not found
        """
        for scanner in self._scanners:
            if scanner.name == name:
                return scanner
        return None

    @classmethod
    def flush_results(cls) -> None:
        """Flush all queued results to the backend immediately.

        This method can be called manually to ensure all pending results
        are uploaded before the application exits or at any other time.
        """
        if cls._uploader is not None:
            cls._uploader.flush()

    @classmethod
    def get_queued_results_count(cls) -> int:
        """Get the number of results currently queued for upload.

        Returns:
            Number of results in the upload queue
        """
        if cls._uploader is not None:
            return cls._uploader.get_queue_size()
        return 0

    def __repr__(self) -> str:
        """String representation."""
        return (
            f"TruGuard(type={self._type.value}, "
            f"scanners={len(self._scanners)}, "
            f"mode={self._config.execution_mode.value})"
        )
