"""Base operation interface."""

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Callable
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from cascade_fm.core.types import OperationResult


class OperationStatus(Enum):
    """Status of an operation instance."""

    IDLE = "idle"  # Not yet executed
    READY = "ready"  # Has input files and params, ready to execute
    EXECUTING = "executing"  # Currently running
    DONE = "done"  # Successfully completed
    ERROR = "error"  # Failed with error


class CommitIntent(Enum):
    """How an operation's output should be committed to disk."""

    NONE = "none"
    MATERIALIZED_OUTPUTS = "materialized_outputs"
    PLANNED_OUTPUT_NAMES = "planned_output_names"


class Operation(ABC):
    """Reactive operation that processes files.

    Operations are stateful, reactive components that:
    - Hold input files, output files, and parameters
    - Auto-execute when inputs or params change
    - Notify pipeline via callback when done
    - Support subprocess execution and caching
    """

    def __init__(self, on_output_changed: Callable[[list[Path]], None] | None = None):
        """Initialize operation.

        Args:
            on_output_changed: Callback to notify when output files change.
                               Called with list of output file paths.
        """
        self.input_files: list[Path] = []
        self.output_files: list[Path] = []
        self.params: dict[str, Any] = {}
        self.status: OperationStatus = OperationStatus.IDLE
        self.error: str | None = None
        self.progress_current: int | None = None
        self.progress_total: int | None = None
        self.progress_message: str | None = None
        self.on_output_changed = on_output_changed

    @property
    @abstractmethod
    def name(self) -> str:
        """Unique identifier for this operation."""
        pass

    @property
    @abstractmethod
    def label(self) -> str:
        """Human-readable label shown in UI."""
        pass

    @property
    @abstractmethod
    def description(self) -> str:
        """Brief description of what this operation does."""
        pass

    @property
    def accepts(self) -> list[str]:
        """MIME type patterns this operation accepts.

        Returns:
            List of MIME patterns (e.g., ["*/*"], ["image/*"], ["text/plain"])
            Default accepts all file types.
        """
        return ["*/*"]

    @property
    def produces(self) -> list[str]:
        """MIME type patterns this operation produces.

        Returns:
            List of MIME patterns for output files.
            Default produces the same types as input.
        """
        return self.accepts

    @property
    def caches_artifacts(self) -> bool:
        """Whether executor should cache this operation's output artifacts.

        Returns:
            True if outputs are materialized artifacts that should be
            written to and restored from the cache backend.
            False for pass-through/selection operations like filters.
        """
        return False

    @property
    def supports_incremental_cache(self) -> bool:
        """Whether executor may cache and execute this operation per input file.

        Returns:
            True if the operation is file-wise and can be executed independently
            for each input file while preserving correctness.
        """
        return False

    @property
    def produces_committable_output(self) -> bool:
        """Whether operation output should be committed to disk via UI commit action.

        Returns:
            True for operations that create or plan changed output artifacts,
            False for pass-through selection operations like filters.
        """
        return self.commit_intent is not CommitIntent.NONE

    @property
    def commit_intent(self) -> CommitIntent:
        """Commit intent for this operation.

        Returns:
            A typed commit intent describing how output should be written.
        """
        return CommitIntent.NONE

    def cache_load(
        self,
        cache_key: str,
        files: list[Path],
        params: dict[str, Any],
        load_artifacts: Callable[[str], OperationResult | None],
    ) -> OperationResult | None:
        """Try loading operation output from cache.

        Args:
            cache_key: Executor-prepared cache key.
            files: Current input files.
            params: Current operation parameters.
            load_artifacts: Helper callback for default artifact restore.

        Returns:
            Cached result if available, otherwise None.
        """
        if not self.caches_artifacts:
            return None
        return load_artifacts(cache_key)

    def cache_store(
        self,
        cache_key: str,
        result: OperationResult,
        files: list[Path],
        params: dict[str, Any],
        store_artifacts: Callable[[str, list[Path]], None],
    ) -> None:
        """Store operation output in cache.

        Args:
            cache_key: Executor-prepared cache key.
            result: Operation execution result.
            files: Current input files.
            params: Current operation parameters.
            store_artifacts: Helper callback for default artifact store.
        """
        if not self.caches_artifacts:
            return
        store_artifacts(cache_key, result.files)

    def set_input_files(self, files: list[Path]) -> None:
        """Set input files and auto-execute if ready.

        Called by pipeline when upstream operation completes.

        Args:
            files: List of input file paths.
        """
        self.input_files = files
        self.error = None

        # Auto-execute if we have params (or params not required)
        if self.params or self._can_execute_without_params():
            self.execute()

    def set_params(self, params: dict[str, Any]) -> None:
        """Set operation parameters and auto-execute if ready.

        Called by UI when user changes parameters.

        Args:
            params: Operation-specific parameters.
        """
        self.params = params
        self.error = None

        # Auto-execute if we have input files
        if self.input_files:
            self.execute()

    def execute(self) -> None:
        """Execute the operation.

        Validates inputs, runs the operation logic, and notifies
        pipeline of output changes via callback.
        """
        # Validate
        valid, error = self.validate_params(**self.params)
        if not valid:
            self.status = OperationStatus.ERROR
            self.error = error or "Invalid parameters"
            self._notify_output_changed()
            return

        if not self.input_files:
            self.status = OperationStatus.ERROR
            self.error = "No input files"
            self._notify_output_changed()
            return

        # Execute
        self.progress_current = None
        self.progress_total = None
        self.progress_message = None
        self.status = OperationStatus.EXECUTING
        self._notify_output_changed()
        try:
            # Call subclass implementation
            self.output_files = self._execute_impl(self.input_files, **self.params)
            self.status = OperationStatus.DONE
            self.error = None
        except Exception as e:
            self.status = OperationStatus.ERROR
            self.error = str(e)
            self.output_files = []

        # Notify pipeline
        self._notify_output_changed()

    def report_progress(
        self,
        current: int,
        total: int,
        message: str | None = None,
    ) -> None:
        """Update operation progress and notify listeners.

        Args:
            current: Number of processed items.
            total: Total number of items.
            message: Optional progress detail text.
        """
        if total <= 0:
            self.progress_current = None
            self.progress_total = None
            self.progress_message = message
            return

        clamped_current = max(0, min(current, total))
        self.progress_current = clamped_current
        self.progress_total = total
        self.progress_message = message
        self._notify_output_changed()

    def _notify_output_changed(self) -> None:
        """Notify pipeline that output has changed."""

        if self.on_output_changed:
            self.on_output_changed(self.output_files)

    def _can_execute_without_params(self) -> bool:
        """Check if operation can execute without params.

        Override in subclasses that support param-less execution.

        Returns:
            True if operation doesn't require params.
        """
        return False

    @abstractmethod
    def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
        """Execute the operation logic.

        Subclasses implement this method with the actual operation logic.
        This is called by execute() after validation.

        Args:
            files: List of input file paths.
            **params: Operation-specific parameters.

        Returns:
            List of output file paths.

        Raises:
            Exception: If operation fails.
        """
        pass

    def validate_params(self, **params: Any) -> tuple[bool, str | None]:
        """Validate operation parameters before execution.

        Args:
            **params: Parameters to validate.

        Returns:
            Tuple of (is_valid, error_message). error_message is None if valid.
        """
        return True, None
