# pylint: disable=too-many-locals,too-many-positional-arguments
"""Temporal workflow for video processing pipeline with DAG dependencies."""
import logging
from collections import deque
from datetime import timedelta
from typing import Dict, List, Any, Optional

from temporalio import workflow
from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError

from mini_videorag.core.processor import ProcessorResult
from mini_videorag.types.temporal import WorkflowProgress, WorkflowResult

logger = logging.getLogger(__name__)


@workflow.defn
class VideoPipelineWorkflow:
    """
    Temporal workflow for executing video processors in DAG order.

    Mirrors the execution logic from ProcessorPipeline but runs as
    a distributed Temporal workflow
    """

    def __init__(self) -> None:
        """Initialize workflow state."""
        self.total_processors = 0
        self.completed_processors = 0
        self.current_processor = ""
        self.results: Dict[str, ProcessorResult] = {}

    @workflow.run
    async def run(
        self,
        video_path: str,
        video_id: str,
        workflow_config: Dict[str, Dict[str, Any]],
        processor_kwargs: Optional[Dict[str, Any]] = None,
        temporal_config: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Execute video processing pipeline with DAG dependencies.

        Args:
            video_path: Path or URL to video file
            video_id: Unique video identifier
            workflow_config: Workflow configuration mapping processor names to config dicts
                            Format: {"processor_name": {"depends_on": [...], "enabled": bool}}
            processor_kwargs: Additional kwargs passed to all processors

        Returns:
            Dictionary mapping processor names to results
        """
        processor_kwargs = processor_kwargs or {}

        workflow_steps = workflow_config

        enabled_processors = [
            name for name, config in workflow_steps.items() if config.get("enabled", True)
        ]

        execution_order = self._resolve_execution_order(enabled_processors, workflow_steps)

        self.total_processors = len(execution_order)
        workflow.logger.info(
            f"Starting pipeline for video_id={video_id} with {self.total_processors} processors"
        )

        for processor_name in execution_order:
            if not self._dependencies_satisfied(processor_name, workflow_steps, self.results):
                workflow.logger.info(f"Skipping {processor_name}: dependencies not satisfied")
                self.results[processor_name] = ProcessorResult(
                    success=False,
                    error="Dependencies not satisfied",
                    processor_name=processor_name,
                    video_id=video_id,
                )
                continue

            self.current_processor = processor_name
            workflow.logger.info(f"Executing processor: {processor_name}")

            try:
                proc_kwargs = processor_kwargs.copy()

                workflow_config_obj = workflow_steps[processor_name]
                for dep in workflow_config_obj.get("depends_on", []):
                    if dep in self.results:
                        proc_kwargs[f"{dep}_result"] = self.results[dep]
                        if dep == "frame_extraction" and self.results[dep].get("success"):
                            frames_dir = self.results[dep].get("data", {}).get("frames_dir")
                            if frames_dir:
                                proc_kwargs["frames_dir"] = frames_dir
                                workflow.logger.info(
                                    f"Passing frames_dir to {processor_name}: {frames_dir}"
                                )

                activity_config = temporal_config.get("activity", {}) if temporal_config else {}
                processor_overrides = activity_config.get("processor_overrides", {}).get(
                    processor_name, {}
                )

                start_to_close = processor_overrides.get(
                    "start_to_close_timeout_minutes",
                    activity_config.get("start_to_close_timeout_minutes", 30),
                )
                heartbeat = processor_overrides.get(
                    "heartbeat_timeout_minutes",
                    activity_config.get("heartbeat_timeout_minutes", 15),
                )

                retry_config = activity_config.get("retry", {})

                result = await workflow.execute_activity(
                    "process_video",
                    args=[processor_name, video_path, video_id, proc_kwargs],
                    activity_id=f"{processor_name}_{video_id}",
                    start_to_close_timeout=timedelta(minutes=start_to_close),
                    heartbeat_timeout=timedelta(minutes=heartbeat),
                    retry_policy=RetryPolicy(
                        initial_interval=timedelta(
                            seconds=retry_config.get("initial_interval_seconds", 10)
                        ),
                        maximum_interval=timedelta(
                            minutes=retry_config.get("maximum_interval_minutes", 5)
                        ),
                        maximum_attempts=retry_config.get("maximum_attempts", 3),
                        non_retryable_error_types=retry_config.get(
                            "non_retryable_errors",
                            [
                                "ModelInitError",
                                "VideoLoadError",
                            ],
                        ),
                    ),
                )

                self.results[processor_name] = result
                self.completed_processors += 1

                workflow.logger.info(
                    f"Completed {processor_name}: success={result.get('success', False)}"
                )

            except ActivityError as e:
                workflow.logger.error(f"Processor {processor_name} failed: {e}")
                self.results[processor_name] = ProcessorResult(
                    success=False,
                    error=str(e),
                    processor_name=processor_name,
                    video_id=video_id,
                )
                self.completed_processors += 1

        workflow.logger.info(
            f"Pipeline completed for video_id={video_id}: "
            f"{self.completed_processors}/{self.total_processors} processors executed"
        )

        return WorkflowResult(
            video_id=video_id,
            results=self.results,
            total_processors=self.total_processors,
            completed_processors=self.completed_processors,
        ).model_dump()

    @workflow.query
    def get_progress(self) -> Dict[str, Any]:
        """
        Query current workflow progress.

        Returns:
            Progress information including completed/total processors
        """
        return WorkflowProgress(
            total_processors=self.total_processors,
            completed_processors=self.completed_processors,
            current_processor=self.current_processor,
            progress_pct=(
                (self.completed_processors / self.total_processors * 100)
                if self.total_processors > 0
                else 0
            ),
            completed_processor_names=list(self.results.keys()),
        ).model_dump()

    def _resolve_execution_order(
        self,
        processors: List[str],
        workflow_steps: Dict[str, Dict[str, Any]],
    ) -> List[str]:
        """
        Resolve execution order using topological sort (same as ProcessorPipeline)
        """
        graph = {}
        reverse_graph = {}
        in_degree = {}

        for proc in processors:
            workflow_config = workflow_steps.get(proc, {})
            deps = [d for d in workflow_config.get("depends_on", []) if d in processors]
            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):
            workflow.logger.warning(f"Circular dependency detected. Using partial order: {ordered}")
            return ordered

        return ordered

    def _dependencies_satisfied(
        self,
        processor_name: str,
        workflow_steps: Dict[str, Dict[str, Any]],
        results: Dict[str, ProcessorResult],
    ) -> bool:
        """
        Check if processor dependencies are satisfied.

        Args:
            processor_name: Processor to check
            workflow_steps: Workflow configuration
            results: Results from completed processors

        Returns:
            True if all dependencies completed successfully
        """
        if processor_name not in workflow_steps:
            return True

        workflow_config = workflow_steps[processor_name]

        for dep in workflow_config.get("depends_on", []):
            if dep not in results:
                return False
            if not results[dep].get("success", False):
                return False

        return True
