"""Result uploader for Tru-Guard - batches and uploads results to TRUSYS backend.

This module provides a non-blocking, thread-safe mechanism for batching and uploading
guardrail results to the TRUSYS backend API. Results are queued with minimal latency
and uploaded asynchronously by a background thread.
"""

import atexit
import threading
import time
from collections import deque
from dataclasses import dataclass
from typing import Any

import httpx

from trusys.results.formatter import ResultFormatter
from trusys.types import AggregatedResult, ScanContext
from trusys.utils.logging import get_logger

logger = get_logger()

@dataclass(frozen=True)
class QueuedResult:
    """Raw result data to be formatted in the upload thread."""
    aggregated_result: AggregatedResult
    context: ScanContext
    application_id: str | None
    application_version: str | None
    environment: str | None
    metadata: dict[str, Any] | None


class ResultUploader:
    """Manages batching and uploading of guardrail results to TRUSYS backend.

    The uploader uses a lock-free append pattern for minimal latency when queuing
    results. A background thread handles all HTTP operations to avoid blocking
    the main request thread.

    Upload triggers:
    - Time-based: Every `batch_interval` seconds (default: 60s)
    - Count-based: When `batch_count` results are queued (default: 100)

    Reliability features:
    - Automatic flush on application shutdown via atexit
    - Failed uploads are re-queued for retry
    - Thread-safe operations
    """

    def __init__(
        self,
        api_host: str,
        api_key: str,
        batch_interval: float = 60.0,
        batch_count: int = 100,
    ) -> None:
        """Initialize the result uploader.

        Args:
            api_host: Base URL for the TRUSYS API
            api_key: API key for authentication
            batch_interval: Time interval in seconds between batch uploads (default: 60)
            batch_count: Maximum number of results to batch before uploading (default: 100)
        """
        self.api_host = api_host.rstrip("/")
        self.api_key = api_key
        self.batch_interval = batch_interval
        self.batch_count = batch_count

        # Thread-safe queue for results using deque (thread-safe for append/popleft)
        self._queue: deque[dict[str, Any] | QueuedResult] = deque()
        # Lock only needed for batch extraction, not for single appends
        self._batch_lock = threading.Lock()
        # Condition to signal the upload thread when batch_count is reached
        self._upload_condition = threading.Condition()
        self._stop_event = threading.Event()
        self._upload_thread: threading.Thread | None = None
        self._last_upload_time = time.time()

        # Start background upload thread
        self._start_upload_thread()

        # Register flush on exit (atexit handles duplicate registrations gracefully)
        atexit.register(self.flush)

    def _start_upload_thread(self) -> None:
        """Start the background thread for periodic batch uploads."""
        if self._upload_thread is not None and self._upload_thread.is_alive():
            return

        self._stop_event.clear()
        self._upload_thread = threading.Thread(
            target=self._upload_loop, daemon=True, name="ResultUploader"
        )
        self._upload_thread.start()
        logger.debug("Result uploader thread started")

    def _upload_loop(self) -> None:
        """Background loop that periodically uploads batched results.

        This loop runs in a dedicated thread and handles all HTTP operations
        to avoid blocking the main request thread.
        """
        while not self._stop_event.is_set():
            try:
                # Wait for either:
                # 1. batch_interval timeout
                # 2. Signal from queue_result when batch_count is reached
                # 3. Stop event
                with self._upload_condition:
                    # Wait with timeout for batch_interval
                    self._upload_condition.wait(timeout=self.batch_interval)

                # Check if we should stop
                if self._stop_event.is_set():
                    break

                # Check if we should upload
                queue_size = len(self._queue)
                current_time = time.time()
                time_since_last_upload = current_time - self._last_upload_time

                should_upload = False
                if queue_size >= self.batch_count:
                    should_upload = True
                    logger.debug(
                        f"Batch count threshold reached: {queue_size} >= {self.batch_count}"
                    )
                elif queue_size > 0 and time_since_last_upload >= self.batch_interval:
                    should_upload = True
                    logger.debug(
                        f"Batch interval threshold reached: {time_since_last_upload:.1f}s >= {self.batch_interval}s"
                    )

                if should_upload:
                    self._upload_batch()

            except Exception as e:
                logger.error(f"Error in upload loop: {e}")
                # Continue loop even if there's an error

        # Final flush before exiting
        self._upload_batch()

    def queue_result(self, result: dict[str, Any]) -> None:
        """Queue a formatted result for batch upload.

        This method is designed for minimal latency - it uses deque.append()
        which is thread-safe and O(1), then signals the background thread
        without blocking.

        Args:
            result: Formatted result dictionary to queue
        """
        if not isinstance(result, dict):
            raise TypeError("queue_result expects a formatted result dictionary")
        # deque.append() is thread-safe and O(1) - no lock needed for append
        self._queue.append(result)

        # Signal the upload thread if we've reached batch_count
        # This is non-blocking - we just notify, don't wait
        if len(self._queue) >= self.batch_count:
            with self._upload_condition:
                self._upload_condition.notify()

    def queue_aggregated_result(
        self,
        aggregated_result: AggregatedResult,
        context: ScanContext,
        application_id: str | None,
        application_version: str | None,
        environment: str | None,
        metadata: dict[str, Any] | None,
    ) -> None:
        """Queue a raw result for formatting in the upload thread."""
        self._queue.append(
            QueuedResult(
                aggregated_result=aggregated_result,
                context=context,
                application_id=application_id,
                application_version=application_version,
                environment=environment,
                metadata=metadata,
            )
        )
        if len(self._queue) >= self.batch_count:
            with self._upload_condition:
                self._upload_condition.notify()

    def _upload_batch(self) -> None:
        """Upload the current batch of results to the API.

        This method is only called from the background thread, never from
        the main request thread.
        """
        # Extract batch from queue - use lock to ensure atomic batch extraction
        batch: list[dict[str, Any]] = []
        with self._batch_lock:
            if not self._queue:
                return

            # Move all queued results to batch
            # deque.popleft() is thread-safe, but we use lock to ensure
            # we get a consistent batch without interleaving
            while self._queue:
                try:
                    item = self._queue.popleft()
                except IndexError:
                    # Queue became empty (edge case with concurrent access)
                    break
                if isinstance(item, QueuedResult):
                    batch.append(
                        ResultFormatter.format_for_backend(
                            item.aggregated_result,
                            item.context,
                            application_id=item.application_id,
                            application_version=item.application_version,
                            environment=item.environment,
                            metadata=item.metadata,
                        )
                    )
                else:
                    batch.append(item)

            self._last_upload_time = time.time()

        if not batch:
            return

        # Upload to API (outside of lock to minimize lock hold time)
        try:
            self._send_to_api(batch)
            logger.debug(f"Successfully uploaded batch of {len(batch)} results")
        except Exception as e:
            logger.error(f"Failed to upload batch of {len(batch)} results: {e}")
            # Re-queue failed results (at the front to retry them first)
            # Use extendleft with reversed to maintain order
            self._queue.extendleft(reversed(batch))

    def _send_to_api(self, results: list[dict[str, Any]]) -> None:
        """Send results to the TRUSYS backend API.

        Args:
            results: List of formatted result dictionaries

        Raises:
            httpx.HTTPError: If the API request fails
        """
        api_url = f"{self.api_host.rstrip('/')}/api/guardrails/save-results"
        headers = {
            "x-api-key": f"{self.api_key}",
            "Content-Type": "application/json",
        }

        response = httpx.post(
            api_url, json=results, headers=headers, timeout=30.0
        )
        response.raise_for_status()

    def flush(self) -> None:
        """Flush all queued results immediately.

        This should be called during application shutdown to ensure
        all results are uploaded before the process exits.
        """
        logger.debug("Flushing result queue...")
        self._stop_event.set()

        # Wake up the upload thread if it's waiting
        with self._upload_condition:
            self._upload_condition.notify_all()

        # Wait for upload thread to finish current operation
        if self._upload_thread is not None and self._upload_thread.is_alive():
            self._upload_thread.join(timeout=5.0)

        # Upload any remaining results (in case thread didn't process them)
        self._upload_batch()

        logger.debug("Result queue flushed")

    def stop(self) -> None:
        """Stop the uploader and flush remaining results."""
        self.flush()

    def get_queue_size(self) -> int:
        """Get the current number of queued results.

        Returns:
            Number of results currently in the queue
        """
        # len() on deque is thread-safe and O(1)
        return len(self._queue)
