"""
Abstract Indicator interface.

Provides the base class for all technical indicators and derived data transformations.
Indicators compose into DAGs, processing data incrementally as updates arrive.
"""

from abc import ABC, abstractmethod
from typing import Dict, List, Optional

from .schema import (
    ComputeContext,
    ComputeResult,
    IndicatorMetadata,
    InputSchema,
    OutputSchema,
)


class Indicator(ABC):
    """
    Abstract base class for all indicators.

    Indicators are composable transformation nodes that:
    - Declare input schema (columns they need)
    - Declare output schema (columns they produce)
    - Compute outputs synchronously from inputs
    - Support incremental updates (process only what changed)
    - Provide rich metadata for AI agent discovery

    Indicators are stateless at the instance level - all state is managed
    by the pipeline execution engine. This allows the same indicator class
    to be reused with different parameters.
    """

    def __init__(self, instance_name: str, **params):
        """
        Initialize an indicator instance.

        Args:
            instance_name: Unique name for this instance (used for output column prefixing)
            **params: Configuration parameters (validated against metadata.parameters)
        """
        self.instance_name = instance_name
        self.params = params
        self._validate_params()

    @classmethod
    @abstractmethod
    def get_metadata(cls) -> IndicatorMetadata:
        """
        Get metadata for this indicator class.

        Called by the registry for AI agent discovery and documentation.
        Should return comprehensive information about the indicator's purpose,
        parameters, and use cases.

        Returns:
            IndicatorMetadata describing this indicator class
        """
        pass

    @classmethod
    @abstractmethod
    def get_input_schema(cls) -> InputSchema:
        """
        Get the input schema required by this indicator.

        Declares what columns must be present in the input data.
        The pipeline will match this against available data sources.

        Returns:
            InputSchema describing required and optional input columns
        """
        pass

    @classmethod
    @abstractmethod
    def get_output_schema(cls, **params) -> OutputSchema:
        """
        Get the output schema produced by this indicator.

        Output column names will be automatically prefixed with the instance name
        by the pipeline engine.

        Args:
            **params: Configuration parameters (may affect output schema)

        Returns:
            OutputSchema describing the columns this indicator produces
        """
        pass

    @abstractmethod
    def compute(self, context: ComputeContext) -> ComputeResult:
        """
        Compute indicator values from input data.

        This method is called synchronously by the pipeline engine whenever
        input data changes. Implementations should:

        1. Extract needed columns from context.data
        2. Perform calculations
        3. Return results with proper time alignment

        For incremental updates (context.is_incremental == True):
        - context.data contains only new/updated rows
        - Implementations MAY optimize by computing only these rows
        - OR implementations MAY recompute everything (simpler but slower)

        Args:
            context: Input data and update metadata

        Returns:
            ComputeResult with calculated indicator values

        Raises:
            ValueError: If input data doesn't match expected schema
        """
        pass

    def _validate_params(self) -> None:
        """
        Validate that provided parameters match the metadata specification.

        Raises:
            ValueError: If required parameters are missing or invalid
        """
        metadata = self.get_metadata()

        # Check for required parameters
        for param_def in metadata.parameters:
            if param_def.required and param_def.name not in self.params:
                raise ValueError(
                    f"Indicator '{metadata.name}' requires parameter '{param_def.name}'"
                )

        # Validate parameter types and ranges
        for name, value in self.params.items():
            # Find parameter definition
            param_def = next(
                (p for p in metadata.parameters if p.name == name),
                None
            )

            if param_def is None:
                raise ValueError(
                    f"Unknown parameter '{name}' for indicator '{metadata.name}'"
                )

            # Type checking
            if param_def.type == "int" and not isinstance(value, int):
                raise ValueError(
                    f"Parameter '{name}' must be int, got {type(value).__name__}"
                )
            elif param_def.type == "float" and not isinstance(value, (int, float)):
                raise ValueError(
                    f"Parameter '{name}' must be float, got {type(value).__name__}"
                )
            elif param_def.type == "bool" and not isinstance(value, bool):
                raise ValueError(
                    f"Parameter '{name}' must be bool, got {type(value).__name__}"
                )
            elif param_def.type == "string" and not isinstance(value, str):
                raise ValueError(
                    f"Parameter '{name}' must be string, got {type(value).__name__}"
                )

            # Range checking for numeric types
            if param_def.type in ("int", "float"):
                if param_def.min_value is not None and value < param_def.min_value:
                    raise ValueError(
                        f"Parameter '{name}' must be >= {param_def.min_value}, got {value}"
                    )
                if param_def.max_value is not None and value > param_def.max_value:
                    raise ValueError(
                        f"Parameter '{name}' must be <= {param_def.max_value}, got {value}"
                    )

    def get_output_columns(self) -> List[str]:
        """
        Get the output column names with instance name prefix.

        Returns:
            List of prefixed output column names
        """
        output_schema = self.get_output_schema(**self.params)
        prefixed = output_schema.with_prefix(self.instance_name)
        return [col.name for col in prefixed.columns if col.name != output_schema.time_column]

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(instance_name='{self.instance_name}', params={self.params})"


class DataSourceAdapter:
    """
    Adapter to make a DataSource look like an Indicator for pipeline composition.

    This allows DataSources to be inputs to indicators in a unified way.
    """

    def __init__(self, datasource_id: str, symbol: str, resolution: str):
        """
        Create a DataSource adapter.

        Args:
            datasource_id: Identifier for the datasource (e.g., 'ccxt', 'demo')
            symbol: Symbol to query (e.g., 'BTC/USD')
            resolution: Time resolution (e.g., '1', '5', '1D')
        """
        self.datasource_id = datasource_id
        self.symbol = symbol
        self.resolution = resolution
        self.instance_name = f"ds_{datasource_id}_{symbol}_{resolution}".replace("/", "_").replace(":", "_")

    def get_output_columns(self) -> List[str]:
        """
        Get the columns provided by this datasource.

        Note: This requires runtime resolution - the pipeline engine
        will need to query the actual DataSource to get the schema.

        Returns:
            List of column names (placeholder - needs runtime resolution)
        """
        # This will be resolved at runtime by the pipeline engine
        return []

    def __repr__(self) -> str:
        return f"DataSourceAdapter(datasource='{self.datasource_id}', symbol='{self.symbol}', resolution='{self.resolution}')"
