"""
Async Processing Service

Manages async request lifecycle and background processing.
"""

import logging
import re
import shutil
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from threading import Event, Thread
from typing import TYPE_CHECKING

from backend.data.async_request_db import AsyncRequestDB
from backend.web.workers.async_queue import AsyncTask, AsyncTaskQueue
from backend.web.core.rate_limiter import RateLimiter
from backend.web.services.storage_helpers import get_storage_helper

if TYPE_CHECKING:
    from backend.web.config import AsyncConfig, StorageConfig
    from backend.web.services.inference import InferenceService

logger = logging.getLogger(__name__)


@dataclass
class AsyncSubmitResult:
    """Result from async submit operation."""

    success: bool
    request_id: str | None = None
    estimated_wait_seconds: int = 0
    error: str | None = None


class AsyncProcessingService:
    """
    Manages async request lifecycle and processing.

    Coordinates between:
    - HTTP endpoints (submit/status/result)
    - Background task queue
    - Database storage
    - Rate limiting
    """

    def __init__(
        self,
        inference_service: "InferenceService",
        db: AsyncRequestDB,
        queue: AsyncTaskQueue,
        rate_limiter: RateLimiter,
        async_config: "AsyncConfig",
        storage_config: "StorageConfig",
    ) -> None:
        self._inference = inference_service
        self._db = db
        self._queue = queue
        self._rate_limiter = rate_limiter
        self._async_config = async_config
        self._storage_config = storage_config

        # Cleanup thread
        self._cleanup_stop_event = Event()
        self._cleanup_thread: Thread | None = None

    def start(self) -> None:
        """Start the async processing service."""
        # Start the task queue with our handler
        self._queue.start(self._process_task)

        # Start cleanup thread
        self._cleanup_stop_event.clear()
        self._cleanup_thread = Thread(
            target=self._cleanup_loop,
            name="async-cleanup",
            daemon=True,
        )
        self._cleanup_thread.start()
        logger.info("AsyncProcessingService started")

    def stop(self, timeout: float = 30.0) -> None:
        """Stop the async processing service."""
        # Stop cleanup thread
        self._cleanup_stop_event.set()
        if self._cleanup_thread and self._cleanup_thread.is_alive():
            self._cleanup_thread.join(timeout=5.0)

        # Stop task queue
        self._queue.stop(timeout=timeout)
        logger.info("AsyncProcessingService stopped")

    def submit_request(
        self,
        api_key: str,
        file_content: bytes,
        filename: str,
        content_type: str,
    ) -> AsyncSubmitResult:
        """
        Submit a new async processing request.

        Args:
            api_key: API key for the request
            file_content: File content as bytes
            filename: Original filename
            content_type: File content type

        Returns:
            AsyncSubmitResult with request_id and status
        """
        # Generate request ID
        request_id = str(uuid.uuid4())

        # Save file to temp storage
        file_path = self._save_upload(request_id, filename, file_content)
        file_size = len(file_content)

        try:
            # Calculate expiration
            expires_at = datetime.utcnow() + timedelta(
                days=self._async_config.result_retention_days
            )

            # Create database record
            self._db.create_request(
                api_key=api_key,
                filename=filename,
                file_size=file_size,
                content_type=content_type,
                expires_at=expires_at,
                request_id=request_id,
            )

            # Record rate limit event
            self._rate_limiter.record_request(api_key)

            # Create and queue task
            task = AsyncTask(
                request_id=request_id,
                api_key=api_key,
                file_path=file_path,
                filename=filename,
                created_at=datetime.utcnow(),
            )

            if not self._queue.submit(task):
                # Queue is full
                self._db.update_status(
                    request_id,
                    "failed",
                    error_message="Processing queue is full",
                )
                # Cleanup file
                file_path.unlink(missing_ok=True)
                return AsyncSubmitResult(
                    success=False,
                    request_id=request_id,
                    error="Processing queue is full. Please try again later.",
                )

            # Estimate wait time
            estimated_wait = self._estimate_wait()

            return AsyncSubmitResult(
                success=True,
                request_id=request_id,
                estimated_wait_seconds=estimated_wait,
            )

        except Exception as e:
            logger.error(f"Failed to submit request: {e}", exc_info=True)
            # Cleanup file on error
            file_path.unlink(missing_ok=True)
            return AsyncSubmitResult(
                success=False,
                # Return generic error message to avoid leaking implementation details
                error="Failed to process request. Please try again later.",
            )

    # Allowed file extensions whitelist
    ALLOWED_EXTENSIONS = frozenset({".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"})

    def _save_upload(
        self,
        request_id: str,
        filename: str,
        content: bytes,
    ) -> Path:
        """Save uploaded file to temp storage using StorageHelper."""
        # Extract extension from filename
        ext = Path(filename).suffix.lower()

        # Validate extension: must be alphanumeric only (e.g., .pdf, .png)
        if not ext or not re.match(r'^\.[a-z0-9]+$', ext):
            ext = ".pdf"

        # Validate against whitelist
        if ext not in self.ALLOWED_EXTENSIONS:
            ext = ".pdf"

        # Get upload directory from StorageHelper
        storage = get_storage_helper()
        upload_dir = storage.get_uploads_base_path(subfolder="async")
        if upload_dir is None:
            raise ValueError("Storage not configured for local access")

        # Build file path - request_id is a UUID so it's safe
        file_path = upload_dir / f"{request_id}{ext}"

        # Defense in depth: ensure path is within upload_dir
        if not file_path.resolve().is_relative_to(upload_dir.resolve()):
            raise ValueError("Invalid file path detected")

        file_path.write_bytes(content)

        return file_path

    def _process_task(self, task: AsyncTask) -> None:
        """
        Process a single task (called by worker thread).

        This method is called by the AsyncTaskQueue worker threads.
        """
        start_time = time.time()

        try:
            # Update status to processing
            self._db.update_status(task.request_id, "processing")

            # Ensure file exists
            if not task.file_path.exists():
                raise FileNotFoundError(f"Upload file not found: {task.file_path}")

            # Run inference based on file type
            file_ext = task.file_path.suffix.lower()
            if file_ext == ".pdf":
                result = self._inference.process_pdf(
                    task.file_path,
                    document_id=task.request_id[:8],
                )
            else:
                result = self._inference.process_image(
                    task.file_path,
                    document_id=task.request_id[:8],
                )

            # Calculate processing time
            processing_time_ms = (time.time() - start_time) * 1000

            # Prepare result for storage
            result_data = {
                "document_id": result.document_id,
                "success": result.success,
                "document_type": result.document_type,
                "fields": result.fields,
                "confidence": result.confidence,
                "detections": result.detections,
                "errors": result.errors,
            }

            # Get visualization path as string
            viz_path = None
            if result.visualization_path:
                viz_path = str(result.visualization_path.name)

            # Store result in database
            self._db.complete_request(
                request_id=task.request_id,
                document_id=result.document_id,
                result=result_data,
                processing_time_ms=processing_time_ms,
                visualization_path=viz_path,
            )

            logger.info(
                f"Task {task.request_id} completed successfully "
                f"in {processing_time_ms:.0f}ms"
            )

        except Exception as e:
            logger.error(
                f"Task {task.request_id} failed: {e}",
                exc_info=True,
            )
            self._db.update_status(
                task.request_id,
                "failed",
                error_message=str(e),
                increment_retry=True,
            )

        finally:
            # Cleanup uploaded file
            if task.file_path.exists():
                task.file_path.unlink(missing_ok=True)

    def _estimate_wait(self) -> int:
        """Estimate wait time based on queue depth."""
        queue_depth = self._queue.get_queue_depth()
        processing_count = self._queue.get_processing_count()
        total_pending = queue_depth + processing_count

        # Estimate ~5 seconds per document
        avg_processing_time = 5
        return total_pending * avg_processing_time

    def _cleanup_loop(self) -> None:
        """Background cleanup loop."""
        logger.info("Cleanup thread started")
        cleanup_interval = self._async_config.cleanup_interval_hours * 3600

        while not self._cleanup_stop_event.wait(timeout=cleanup_interval):
            try:
                self._run_cleanup()
            except Exception as e:
                logger.error(f"Cleanup failed: {e}", exc_info=True)

        logger.info("Cleanup thread stopped")

    def _run_cleanup(self) -> None:
        """Run cleanup operations."""
        logger.info("Running cleanup...")

        # Delete expired requests
        deleted_requests = self._db.delete_expired_requests()

        # Reset stale processing requests
        reset_count = self._db.reset_stale_processing_requests(
            stale_minutes=self._async_config.task_timeout_seconds // 60,
            max_retries=3,
        )

        # Cleanup old rate limit events
        deleted_events = self._db.cleanup_old_rate_limit_events(hours=1)

        # Cleanup old poll timestamps
        cleaned_polls = self._rate_limiter.cleanup_poll_timestamps()

        # Cleanup rate limiter request windows
        self._rate_limiter.cleanup_request_windows()

        # Cleanup orphaned upload files
        orphan_count = self._cleanup_orphan_files()

        logger.info(
            f"Cleanup complete: {deleted_requests} expired requests, "
            f"{reset_count} stale requests reset, "
            f"{deleted_events} rate limit events, "
            f"{cleaned_polls} poll timestamps, "
            f"{orphan_count} orphan files"
        )

    def _cleanup_orphan_files(self) -> int:
        """Clean up upload files that don't have matching requests."""
        storage = get_storage_helper()
        upload_dir = storage.get_uploads_base_path(subfolder="async")
        if upload_dir is None or not upload_dir.exists():
            return 0

        count = 0
        # Files older than 1 hour without matching request are considered orphans
        cutoff = time.time() - 3600

        for file_path in upload_dir.iterdir():
            if not file_path.is_file():
                continue

            # Check if file is old enough
            if file_path.stat().st_mtime > cutoff:
                continue

            # Extract request_id from filename
            request_id = file_path.stem

            # Check if request exists in database
            request = self._db.get_request(request_id)
            if request is None:
                file_path.unlink(missing_ok=True)
                count += 1

        return count
