# pylint: disable=import-outside-toplevel, broad-exception-caught
"""Generic Temporal activity for executing any processor via factory."""
import logging
import threading
import time
from typing import Any, Dict, Optional
from temporalio import activity
from mini_videorag.processors.factory import ProcessorFactory
from mini_videorag.core.processor import ProcessorResult

logger = logging.getLogger(__name__)


class ProcessorActivity:
    """
    Generic Temporal activity wrapper for any processor.

    Executes processors via ProcessorFactory, preserving lazy loading
    and plugin architecture. Handles serialization of ProcessorResult
    for Temporal's JSON-based data model.

    Example:
        factory = ProcessorFactory()
        activity = ProcessorActivity(factory)

        # Executed by Temporal worker
        result = activity.run(
            processor_name="transcription",
            video_path="s3://bucket/video.mp4",
            video_id="video_123"
        )
    """

    def __init__(self, factory: ProcessorFactory):
        """
        Initialize activity with processor factory.

        Args:
            factory: ProcessorFactory for creating processors
        """
        self.factory = factory
        self.logger = logging.getLogger(__name__)

    @activity.defn(name="process_video")
    def run(
        self,
        processor_name: str,
        video_path: str,
        video_id: str,
        processor_kwargs: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Execute a processor on a video.

        Supports multiple video path types:
        - Local filesystem: /path/to/video.mp4
        - HTTP(S) URLs: https://example.com/video.mp4
        - S3/MinIO paths: s3://bucket/video.mp4

        Args:
            processor_name: Name of processor to run (e.g., "transcription", "ocr")
            video_path: Path, URL, or S3 URI to video file
            video_id: Unique video identifier
            processor_kwargs: Additional arguments passed to processor.process()

        Returns:
            Dict representation of ProcessorResult (JSON-serializable)

        Raises:
            ValueError: If processor_name is unknown or video_path is invalid
            FileNotFoundError: If local video file doesn't exist
            Exception: If processor execution fails
        """
        if processor_kwargs is None:
            processor_kwargs = {}

        activity.heartbeat(f"Starting {processor_name}")

        self.logger.info(f"Activity processing video_id={video_id} with processor={processor_name}")

        from mini_videorag.utils.path_resolver import resolve_video_path

        self.logger.debug(f"Resolving video path: {video_path}")
        resolved_path = resolve_video_path(video_path)
        self.logger.info(f"Resolved video path: {video_path} → {resolved_path}")

        stop_heartbeat = threading.Event()
        heartbeat_thread = threading.Thread(
            target=self._send_heartbeats, args=(processor_name, stop_heartbeat), daemon=True
        )
        heartbeat_thread.start()

        try:
            self.logger.info(f"Creating processor: {processor_name}")
            start_create = time.time()
            processor = self.factory.create(processor_name)
            create_duration = time.time() - start_create
            self.logger.info(f"Processor {processor_name} created in {create_duration:.2f}s")

            import tempfile

            temp_frames_dir = None
            if processor_name == "frame_extraction" and "frames_dir" not in processor_kwargs:
                temp_frames_dir = tempfile.mkdtemp(prefix=f"frames_{video_id}_")
                processor_kwargs["frames_dir"] = temp_frames_dir
                self.logger.info(f"Created temporary frames directory: {temp_frames_dir}")

            self.logger.info(f"Starting {processor_name}.process() with video_id={video_id}")
            start_process = time.time()
            result: ProcessorResult = processor.process(resolved_path, video_id, **processor_kwargs)
            process_duration = time.time() - start_process
            self.logger.info(
                f"Processor {processor_name}.process() completed in {process_duration:.2f}s "
                f"(success={result.success})"
            )
        finally:
            stop_heartbeat.set()
            heartbeat_thread.join(timeout=1.0)

        if temp_frames_dir:
            self.logger.info(
                f"Preserved frames directory for downstream processors: {temp_frames_dir}"
            )

        activity.heartbeat(f"Completed {processor_name}")

        result_dict = self._serialize_result(result)

        return result_dict

    def _serialize_result(self, result: ProcessorResult) -> Dict[str, Any]:
        """
        Convert ProcessorResult to JSON-serializable dict.

        Handles non-serializable objects in all fields (data, metadata, segments)
        by converting numpy types to Python native types.

        Args:
            result: ProcessorResult from processor execution

        Returns:
            Dictionary representation safe for Temporal serialization
        """
        result_dict = result.model_dump()

        result_dict["data"] = self._sanitize_metadata(result.data)
        result_dict["metadata"] = self._sanitize_metadata(result.metadata)

        result_dict["segments"] = [
            self._sanitize_metadata(segment) if isinstance(segment, dict) else segment
            for segment in result_dict.get("segments", [])
        ]

        return result_dict

    def _sanitize_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
        """
        Remove non-serializable objects from metadata.

        Temporal requires all data to be JSON-serializable. This removes
        objects like numpy arrays, PIL images, etc.

        Args:
            metadata: Raw metadata from processor

        Returns:
            Sanitized metadata with only JSON-serializable values
        """
        import numpy as np

        clean = {}
        for key, value in metadata.items():
            if isinstance(value, np.ndarray):
                self.logger.debug(f"Skipping non-serializable metadata key: {key} (numpy.ndarray)")
                continue
            if hasattr(value, "__module__") and "PIL" in value.__module__:
                self.logger.debug(f"Skipping non-serializable metadata key: {key} (PIL.Image)")
                continue
            if isinstance(value, dict):
                clean[key] = self._sanitize_metadata(value)
            elif isinstance(value, (list, tuple)):
                clean[key] = [
                    self._sanitize_value(v) for v in value if not isinstance(v, np.ndarray)
                ]
            else:
                clean[key] = self._sanitize_value(value)

        return clean

    def _sanitize_value(self, value: Any) -> Any:
        """
        Sanitize a single value for JSON serialization.

        Converts numpy types to Python native types and validates JSON serializability.

        Args:
            value: Value to sanitize

        Returns:
            Sanitized value or None if not serializable
        """
        import numpy as np

        if isinstance(value, (np.integer, np.int64, np.int32, np.int16, np.int8)):
            return int(value)
        if isinstance(value, (np.floating, np.float64, np.float32, np.float16)):
            return float(value)
        if isinstance(value, np.bool_):
            return bool(value)
        if isinstance(value, np.str_):
            return str(value)

        if isinstance(value, dict):
            return self._sanitize_metadata(value)

        try:
            import json

            json.dumps(value)
            return value
        except (TypeError, ValueError):
            self.logger.debug(f"Skipping non-serializable value: {type(value)}")
            return None

    def _send_heartbeats(self, processor_name: str, stop_event: threading.Event):
        """
        Send periodic heartbeats during long-running processor execution.

        This prevents Temporal heartbeat timeout errors for processors that
        take a long time to complete (e.g., transcription, OCR).

        Args:
            processor_name: Name of processor being executed
            stop_event: Event to signal when to stop sending heartbeats
        """
        heartbeat_interval = 30
        iteration = 0
        successful_heartbeats = 0

        self.logger.info(f"Heartbeat thread started for {processor_name}")

        stop_event.wait(5)

        while not stop_event.is_set():
            iteration += 1
            elapsed = iteration * heartbeat_interval
            try:
                activity.heartbeat(f"Processing {processor_name} ({elapsed}s elapsed)")
                successful_heartbeats += 1
                self.logger.info(
                    f"❤️ Heartbeat sent for {processor_name} (iteration {iteration}, {elapsed}s elapsed)"
                )
            except Exception as e:
                if "Cancelled" in str(e) or "CancelledError" in str(e):
                    self.logger.warning(
                        f"Activity {processor_name} cancelled, stopping heartbeats: {e}"
                    )
                    break
                self.logger.debug(f"Heartbeat failed for {processor_name} (will retry): {e}")

            stop_event.wait(heartbeat_interval)

        self.logger.info(
            f"Heartbeat thread stopped for {processor_name} (sent {successful_heartbeats} successful heartbeats)"
        )
