"""Pipeline state management."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from cascade_fm.core.types import PaneStatus
from cascade_fm.operations.base import Operation, OperationStatus
from cascade_fm.operations.registry import create_operation


@dataclass
class Pane:
    """A pane in the pipeline with its operation instance."""

    id: str
    operation: Operation
    status: PaneStatus = PaneStatus.CONFIGURED

    @property
    def output_files(self) -> list[Path]:
        """Get output files from operation."""
        return self.operation.output_files

    @property
    def error(self) -> str | None:
        """Get error from operation."""
        return self.operation.error


class Pipeline:
    """Pipeline coordinator.

    Manages the DAG of panes and propagates changes through the pipeline.
    Operations are reactive and execute themselves when inputs/params change.
    """

    def __init__(self):
        """Initialize pipeline."""
        self.panes: list[Pane] = []

        # Optional callback for UI integration
        self.on_pane_updated: Callable[[str], None] | None = None

    def add_pane(self, operation_name: str, params: dict[str, Any] | None = None) -> str:
        """Add a new pane to the pipeline.

        Args:
            operation_name: Operation name (e.g., "filter_extension").
            params: Initial operation parameters (optional).

        Returns:
            The pane_id of the newly created pane.
        """
        pane_id = f"pane_{len(self.panes)}"

        # Create operation with callback
        operation = create_operation(
            operation_name,
            on_output_changed=lambda files: self._on_pane_output_changed(pane_id, files),
        )

        # Create pane
        pane = Pane(id=pane_id, operation=operation)
        self.panes.append(pane)

        # Set initial params if provided
        if params:
            operation.set_params(params)

        # Link to previous pane's output
        if len(self.panes) > 1:
            prev_pane = self.panes[-2]
            operation.input_files = list(prev_pane.output_files)

        return pane_id

    def _on_pane_output_changed(self, pane_id: str, files: list[Path]) -> None:
        """Called when an operation completes.

        This propagates changes downstream automatically.

        Args:
            pane_id: ID of the pane that changed.
            files: New output files from the operation.
        """
        pane = self.get_pane(pane_id)
        if pane.operation.status == OperationStatus.EXECUTING:
            pane.status = PaneStatus.EXECUTING
        elif pane.operation.status == OperationStatus.ERROR:
            pane.status = PaneStatus.ERROR
        elif pane.operation.status == OperationStatus.DONE:
            pane.status = PaneStatus.DONE

        # Notify UI
        if self.on_pane_updated:
            self.on_pane_updated(pane_id)

        # Propagate to next pane (reactive cascade) only after completion
        if pane.status not in {PaneStatus.DONE, PaneStatus.ERROR}:
            return

        next_pane = self.get_next_pane(pane_id)
        if next_pane:
            next_pane.operation.set_input_files(files)

    def update_browser_files(self, files: list[Path]) -> None:
        """Update browser pane (pane_0) with selected files.

        Called when user selects files in the file browser.
        Automatically propagates to first operation pane.

        Args:
            files: List of selected file paths.
        """
        # If first pane is a normal operation (no dedicated browser pane yet),
        # treat these files as external input and propagate directly.
        if self.panes and self.panes[0].operation.name != "browse_files":
            self.panes[0].operation.set_input_files(files)
            return

        # Ensure browser pane exists (special case - no actual operation)
        if not self.panes:
            browser_op = self._create_browser_operation(
                lambda files: self._on_pane_output_changed("pane_0", files)
            )
            browser_pane = Pane(id="pane_0", operation=browser_op)
            browser_pane.status = PaneStatus.DONE
            self.panes.insert(0, browser_pane)

        # Update browser output files
        self.panes[0].operation.output_files = files
        self.panes[0].status = PaneStatus.DONE

        # Notify UI about browser pane update
        if self.on_pane_updated:
            self.on_pane_updated("pane_0")

        # Trigger cascade if there are downstream panes
        if len(self.panes) > 1:
            self.panes[1].operation.set_input_files(files)

    @staticmethod
    def _create_browser_operation(on_output_changed):
        """Create the special pass-through browser operation."""
        from cascade_fm.operations.base import Operation, OperationStatus

        class BrowserOperation(Operation):
            def __init__(self, callback):
                super().__init__(callback)
                self.status = OperationStatus.DONE

            @property
            def name(self) -> str:
                return "browse_files"

            @property
            def label(self) -> str:
                return "Browse Files"

            @property
            def description(self) -> str:
                return "File browser"

            def _execute_impl(self, files, **params):
                return files

        return BrowserOperation(on_output_changed)

    def switch_operation_at_bar(self, bar_index: int, operation_name: str) -> str:
        """Switch operation at a given bar position, clearing downstream.

        Args:
            bar_index: Index of the operation bar (0-based).
            operation_name: New operation name.

        Returns:
            The pane_id of the newly created pane.
        """
        target_pane_index = bar_index + 1  # Browser is pane_0
        target_pane_id = f"pane_{target_pane_index}"

        # Check if pane exists at this position
        try:
            self.get_pane(target_pane_id)
            # Remove this pane and all downstream
            self.remove_pane(target_pane_id)
        except ValueError:
            pass  # Pane doesn't exist, just add new one

        # Add new pane with this operation
        return self.add_pane(operation_name=operation_name)

    def set_pane_params(self, pane_id: str, params: dict[str, Any]) -> None:
        """Set operation parameters for a pane.

        The operation will auto-execute if it has input files.

        Args:
            pane_id: Pane identifier.
            params: Operation parameters.
        """
        pane = self.get_pane(pane_id)
        pane.operation.set_params(params)

    def remove_pane(self, pane_id: str) -> None:
        """Remove pane and all downstream panes.

        Args:
            pane_id: Pane to remove.
        """
        idx = self._get_pane_index(pane_id)
        # Remove this pane and all to the right
        self.panes = self.panes[:idx]

    def get_pane(self, pane_id: str) -> Pane:
        """Get pane by ID.

        Args:
            pane_id: Pane identifier.

        Returns:
            Pane for the given ID.

        Raises:
            ValueError: If pane not found.
        """
        for pane in self.panes:
            if pane.id == pane_id:
                return pane
        raise ValueError(f"Pane not found: {pane_id}")

    def get_previous_pane(self, pane_id: str) -> Pane | None:
        """Get the pane immediately before this one.

        Args:
            pane_id: Current pane ID.

        Returns:
            Previous Pane or None if this is the first pane.
        """
        idx = self._get_pane_index(pane_id)
        if idx > 0:
            return self.panes[idx - 1]
        return None

    def get_next_pane(self, pane_id: str) -> Pane | None:
        """Get the pane immediately after this one.

        Args:
            pane_id: Current pane ID.

        Returns:
            Next Pane or None if this is the last pane.
        """
        idx = self._get_pane_index(pane_id)
        if idx < len(self.panes) - 1:
            return self.panes[idx + 1]
        return None

    def _get_pane_index(self, pane_id: str) -> int:
        """Get pane index by ID.

        Args:
            pane_id: Pane identifier.

        Returns:
            Index of the pane.

        Raises:
            ValueError: If pane not found.
        """
        for i, pane in enumerate(self.panes):
            if pane.id == pane_id:
                return i
        raise ValueError(f"Pane not found: {pane_id}")
