# pylint: disable=import-outside-toplevel,broad-exception-caught,too-many-positional-arguments,too-many-locals
"""Simple processor pipeline for running multiple video processors."""

import logging
from typing import Dict, List, Optional, TYPE_CHECKING
from dataclasses import dataclass, field
import os
from collections import deque
from openai import OpenAI

from mini_videorag.core.processor import ProcessorResult
from mini_videorag.core.exceptions import (
    ProcessorError,
    VideoLoadError,
    ModelInitError,
    InsufficientDataError,
    ProcessingError,
    PipelineError,
)
from mini_videorag.types import WorkflowStepConfig
from mini_videorag.utils.logger import log_processor_result
from pprint import pformat

if TYPE_CHECKING:
    from mini_videorag.processors.factory import ProcessorFactory
    from mini_videorag.utils.output_manager import OutputManager

logger = logging.getLogger(__name__)


@dataclass
class PipelineResult:
    """Aggregated results from running multiple processors."""

    video_id: str
    results: Dict[str, ProcessorResult] = field(default_factory=dict)

    def __getitem__(self, key: str) -> ProcessorResult:
        return self.results[key]

    def __repr__(self) -> str:
        return (
            f"PipelineResult(video_id={self.video_id}, results={pformat(self.results, indent=2)})"
        )

    def successful(self) -> Dict[str, ProcessorResult]:
        """Return only successful processor results."""
        return {k: v for k, v in self.results.items() if v.success}

    def failed(self) -> Dict[str, ProcessorResult]:
        """Return only failed processor results."""
        return {k: v for k, v in self.results.items() if not v.success}


class ProcessorPipeline:
    """
    Simple pipeline to run multiple video processors.

    Always uses ProcessorFactory to create processors.

    Supports DAG-based execution with dependencies and conditions.

    Example:
        # Create factory with config
        config = {
            "transcription": {"provider": "openai_whisper"},
            "ocr": {"provider": "easyocr", "languages": ["en", "fr"]}
        }
        factory = ProcessorFactory(config)

        # Create pipeline with factory
        pipeline = ProcessorPipeline(processor_factory=factory)
        results = pipeline.run_all("video.mp4", "video_123")

        # Or run specific processors
        results = pipeline.run(["transcription", "ocr"], "video.mp4", "video_123")

        # Or run with workflow (DAG)
        workflow = {
            "transcription": {"depends_on": []},
            "ocr": {"depends_on": []},
            "content_safety": {
                "depends_on": [],
                "condition": lambda results: results.get("ocr", {}).get("success", False)
            }
        }
        pipeline = ProcessorPipeline(processor_factory=factory, workflow=workflow)
        results = pipeline.run_all("video.mp4", "video_123")
    """

    _ERROR_MESSAGES = {
        VideoLoadError: "video load error",
        ModelInitError: "model initialization error",
        InsufficientDataError: "insufficient data",
        ProcessingError: "processing error",
        ProcessorError: "processor error",
        PipelineError: "unexpected error",
    }

    def __init__(
        self,
        processor_factory: Optional["ProcessorFactory"] = None,
        openai_client: Optional[OpenAI] = None,
        ocr_languages: List[str] = None,
        yolo_model: str = "yolo11n.pt",
        workflow: Optional[Dict[str, WorkflowStepConfig]] = None,
        output_manager: Optional["OutputManager"] = None,
    ):
        """
        Initialize pipeline with ProcessorFactory.

        Args:
            processor_factory: ProcessorFactory to use for creating processors.
                             If None, creates a default factory with basic config.
            openai_client: OpenAI client (injected into factory config for transcription)
            ocr_languages: Languages for OCR (used in default factory config)
            yolo_model: YOLO model path (used in default factory config)
            workflow: Optional DAG workflow definition with dependencies and conditions.
                     Format: {"processor_name": WorkflowStepConfig(...)}
            output_manager: Optional OutputManager for saving results to disk.
        """
        if processor_factory is None:
            from mini_videorag.processors.factory import ProcessorFactory

            config = {
                "ocr": {"provider": "easyocr", "languages": ocr_languages or ["en"]},
                "person_detection": {"provider": "yolo", "model_path": yolo_model},
                "object_detection": {"provider": "yolo", "model_path": yolo_model},
                "music_detection": {"provider": "shazam"},
            }

            if openai_client:
                config["transcription"] = {
                    "provider": "openai_whisper",
                    "openai_client": openai_client,
                }

            processor_factory = ProcessorFactory(config)

        self.processor_factory = processor_factory
        self._processors = {}
        self.workflow = workflow or {}
        self.output_manager = output_manager

    def get_processor(self, name: str):
        """Get or create a processor by name (lazy loading)."""
        if name not in self._processors:
            self._processors[name] = self.processor_factory.create(name)
        return self._processors[name]

    @property
    def available_processors(self) -> List[str]:
        """List of available processor names."""
        return self.processor_factory.get_available_processor_types()

    def _resolve_execution_order(self, processors: List[str]) -> List[str]:
        """
        Resolve execution order based on workflow dependencies (topological sort).

        Args:
            processors: List of processor names to run

        Returns:
            Ordered list of processors respecting dependencies

        Raises:
            PipelineError: If circular dependency detected
        """
        if not self.workflow:
            return processors

        graph = {}
        reverse_graph = {}
        in_degree = {}

        for proc in processors:
            workflow_config = self.workflow.get(proc)
            if workflow_config:
                deps = [d for d in workflow_config.depends_on if d in processors]
            else:
                deps = []
            graph[proc] = deps
            in_degree[proc] = len(deps)
            reverse_graph[proc] = []

        for proc, deps in graph.items():
            for dep in deps:
                reverse_graph[dep].append(proc)

        queue = deque([proc for proc in processors if in_degree[proc] == 0])
        ordered = []

        while queue:
            current = queue.popleft()
            ordered.append(current)

            for dependent in reverse_graph[current]:
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    queue.append(dependent)

        if len(ordered) != len(processors):
            unprocessed = [p for p in processors if p not in ordered]
            logger.warning(
                f"Circular dependency detected in workflow. "
                f"Processors involved: {unprocessed}. Using original order."
            )
            return processors

        return ordered

    def _should_run_processor(self, name: str, results: Dict[str, ProcessorResult]) -> bool:
        """
        Check if processor should run based on workflow conditions.

        Args:
            name: Processor name
            results: Current results from completed processors

        Returns:
            True if processor should run, False otherwise
        """
        if not self.workflow or name not in self.workflow:
            return True

        workflow_config = self.workflow[name]

        for dep in workflow_config.depends_on:
            if dep not in results or not results[dep].success:
                logger.info(f"Skipping {name}: dependency {dep} not satisfied")
                return False

        if workflow_config.condition and callable(workflow_config.condition):
            try:
                if not workflow_config.condition(results):
                    logger.info(f"Skipping {name}: condition not met")
                    return False
            except PipelineError as e:
                logger.error(f"Condition check failed for {name}: {e}", exc_info=True)
                return False

        return True

    def _handle_processor_error(
        self, error: Exception, processor_name: str, video_id: str, output_dir: Optional[str] = None
    ) -> ProcessorResult:
        """
        Handle processor exceptions in a unified way.

        Args:
            error: The exception that occurred
            processor_name: Name of the processor that failed
            video_id: Video identifier
            output_dir: Optional output directory for saving failed result

        Returns:
            ProcessorResult with error information
        """
        error_type = type(error).__name__
        error_message = self._ERROR_MESSAGES.get(type(error), "unexpected error")

        logger.error(
            f"Processor {processor_name} failed - {error_message}: {error}",
            exc_info=True,
        )

        proc_result = ProcessorResult(
            processor_name=processor_name,
            video_id=video_id,
            success=False,
            error=f"{error_type}: {error}",
        )

        if self.output_manager and output_dir:
            self.output_manager.save_processor_result(processor_name, proc_result, output_dir)

        return proc_result

    def run(
        self, processors: List[str], video_path: str, video_id: str, **kwargs
    ) -> PipelineResult:
        """
        Run specific processors on a video.

        If workflow is configured, processors run in dependency order with conditions.

        Args:
            processors: List of processor names to run
            video_path: Path to video file
            video_id: Video identifier
            **kwargs: Additional args passed to each processor

        Returns:
            PipelineResult with all processor outputs
        """
        result = PipelineResult(video_id=video_id)

        output_dir = None
        if self.output_manager:
            output_dir = self.output_manager.create_output_directory(video_id)

        ordered_processors = self._resolve_execution_order(processors)

        if "frame_extraction" in ordered_processors and "frames_dir" not in kwargs:
            if output_dir:
                frames_dir = os.path.join(output_dir, "frames")
                os.makedirs(frames_dir, exist_ok=True)
            else:
                import tempfile

                frames_dir = tempfile.mkdtemp(prefix=f"frames_{video_id}_")
            kwargs["frames_dir"] = frames_dir
            logger.info(f"Auto-created frames_dir: {frames_dir}")

        for name in ordered_processors:
            if not self._should_run_processor(name, result.results):
                logger.info(f"Skipping processor: {name}")
                result.results[name] = ProcessorResult(
                    processor_name=name,
                    video_id=video_id,
                    success=False,
                    error="Skipped: workflow condition not met",
                )
                continue

            try:
                logger.info(f"Running processor: {name}")
                processor = self.get_processor(name)

                processor_kwargs = kwargs.copy()
                if self.workflow and name in self.workflow:
                    workflow_config = self.workflow[name]
                    for dep in workflow_config.depends_on:
                        if dep in result.results:
                            processor_kwargs[f"{dep}_result"] = result.results[dep]

                            if dep == "frame_extraction" and result.results[dep].success:
                                dep_data = result.results[dep].data
                                if "frames_dir" in dep_data:
                                    processor_kwargs["frames_dir"] = dep_data["frames_dir"]

                proc_result = processor.process(video_path, video_id, **processor_kwargs)
                result.results[name] = proc_result
                log_processor_result(logger, proc_result)

                if self.output_manager and output_dir:
                    self.output_manager.save_processor_result(name, proc_result, output_dir)
            except (
                VideoLoadError,
                ModelInitError,
                InsufficientDataError,
                ProcessingError,
                ProcessorError,
                PipelineError,
            ) as e:
                proc_result = self._handle_processor_error(e, name, video_id, output_dir)
                result.results[name] = proc_result
            except Exception as e:
                logger.critical(
                    f"Unexpected error in processor {name}: {type(e).__name__}", exc_info=True
                )
                proc_result = self._handle_processor_error(
                    ProcessorError(f"Unexpected: {e}"), name, video_id, output_dir
                )
                result.results[name] = proc_result

        if self.output_manager and output_dir:
            self.output_manager.save_pipeline_result(result, output_dir)
            self.output_manager.generate_markdown_report(result, output_dir)

            outputs_config = self.output_manager.config.get("outputs", {})
            debug_viz_enabled = outputs_config.get("debug_visualizations", False)

            if debug_viz_enabled:
                detection_processors = [
                    "person_detection",
                    "object_detection",
                    "brand_detection",
                ]
                has_detections = any(
                    name in result.results and result.results[name].success
                    for name in detection_processors
                )

                if has_detections and "frames_dir" in kwargs:
                    from mini_videorag.utils.debug_visualizer import DebugVisualizer

                    try:
                        viz_stats = DebugVisualizer.visualize_detections(
                            frames_dir=kwargs["frames_dir"],
                            results=result.results,
                            output_dir=output_dir,
                        )
                        logger.info(f"Debug visualizations created: {viz_stats}")
                    except Exception as e:
                        logger.warning(f"Failed to create debug visualizations: {e}")

        return result

    def run_all(self, video_path: str, video_id: str, **kwargs) -> PipelineResult:
        """
        Run all available processors on a video.

        If workflow is configured, only runs processors defined in workflow.
        """
        if self.workflow:
            processors = list(self.workflow.keys())
        else:
            processors = self.available_processors
        return self.run(processors, video_path, video_id, **kwargs)
