"""
Indicator Base Class and Decorator

Provides the foundation for building technical indicators with:
- @indicator decorator for metadata and auto-registration
- Indicator base class with plot() method
- Automatic parameter handling via param descriptors

Usage:
    from sixtysix_core import Indicator, indicator, param, Line

    @indicator(
        name='sma',
        display_name='Simple Moving Average',
        type='overlay',
        category='Moving Averages'
    )
    class SMA:
        period = param.number(default=20, min=1, max=500, step=1)
        color = param.color(default='#2962ff')

        def plot(self, df):
            sma = df['close'].rolling(self.period).mean()
            return [Line(y=sma, color=self.color, line_width=2)]
"""

from __future__ import annotations
from dataclasses import dataclass, field, fields
from typing import Any, Callable, Dict, List, Literal, Optional, Set, Type, TypedDict, TypeVar, Union, overload
import pandas as pd
import numpy as np

from typing import TYPE_CHECKING
from .params import InputParam, ParamValue, SelectOption, ConfigParam
from .plot import PlotComponent, extract_plot_data_with_panels

if TYPE_CHECKING:
    from .series import IndicatorSeries

# Capture builtin type() before it gets shadowed by indicator(type=...) parameter
_make_type = type

# TypeVar for preserving decorated class type through @indicator
_IndicatorT = TypeVar('_IndicatorT')


# =============================================================================
# Types (from former types.py)
# =============================================================================

# Type for values that can appear in style dictionaries
StyleValue = Union[str, int, float, bool]


@dataclass
class RenderComponent:
    """
    Defines how a component of an indicator should be rendered.

    Basic Primitives:
    - line: Continuous line from y values
    - scatter: Points with shape option (circle, triangle_up, triangle_down, square, diamond)
    - bar: Vertical bars with optional data-driven colors
    - hbar: Horizontal bars (for volume profile, etc.)
    - hline: Horizontal line at fixed y value
    - fill: Fill between two y arrays (supports data-driven colors)
    - segment: Line segment from (x0,y0) to (x1,y1)
    - marker: Buy/sell markers (convenience type for strategies)

    Position Modes (for hbar):
    - 'right_edge': Bars are anchored to the right edge of data
    - 'fixed': Bars stay at their original position (default)
    - 'left_edge': Bars are anchored to the left edge of data
    """
    type: str
    data_fields: Dict[str, str]
    style: Dict[str, StyleValue]
    legend: Optional[str] = None
    position_mode: Optional[str] = None


@dataclass
class IndicatorConfig:
    """Configuration for an indicator"""
    name: str
    display_name: str
    type: str  # 'overlay' or 'oscillator'
    category: str = 'Other'
    render_components: List[RenderComponent] = field(default_factory=list)
    y_range: Optional[Dict[str, float]] = None
    configurable_params: List[ConfigParam] = field(default_factory=list)


# =============================================================================
# Result Classes (from former results.py)
# =============================================================================

@dataclass
class IndicatorResult:
    """Base class for indicator results."""

    def at(self, idx: int) -> 'IndicatorResult':
        """Extract values at a specific index, returning a new result with scalars."""
        values = {}
        for f in fields(self):
            value = getattr(self, f.name)
            if isinstance(value, pd.Series):
                values[f.name] = value.iloc[idx] if idx < len(value) else float('nan')
            else:
                values[f.name] = value
        return self.__class__(**values)


# =============================================================================
# Indicator Info Types
# =============================================================================

class IndicatorInfoDict(TypedDict):
    """Type for indicator info response"""
    name: str
    display_name: str
    type: Literal['overlay', 'oscillator']
    category: str


class ConfigParamDict(TypedDict, total=False):
    """Type for configurable parameter in API response"""
    name: str
    label: str
    type: Literal['number', 'color', 'boolean', 'select']
    value: ParamValue
    min_value: Optional[float]
    max_value: Optional[float]
    step: Optional[float]
    options: Optional[List[SelectOption]]


class IndicatorResponseDict(TypedDict, total=False):
    """Type for indicator API response"""
    name: str
    type: Literal['overlay', 'oscillator']
    display_name: str
    data: List[Dict[str, Union[int, float, str, bool, None]]]
    render_components: List[Dict[str, Union[str, Dict[str, str], Dict[str, Union[str, int, float, bool, None]], None]]]
    configurable_params: List[ConfigParamDict]
    y_range: Optional[Dict[str, float]]


# Registry for auto-registration
INDICATOR_REGISTRY: Dict[str, Type["Indicator"]] = {}


def indicator(
    name: str,
    display_name: str,
    type: str = 'overlay',
    category: str = 'Other',
    y_range: Optional[Dict[str, float]] = None
) -> Callable[[Type[_IndicatorT]], Type[_IndicatorT]]:
    """
    Class decorator for defining indicators.

    Args:
        name: Unique identifier for the indicator (e.g., 'sma', 'rsi')
        display_name: Human-readable name for UI display
        type: 'overlay' (on price chart) or 'oscillator' (separate pane)
        category: Category for grouping in UI
        y_range: Optional fixed y-axis range (e.g., {'min': 0, 'max': 100} for RSI)

    The decorator automatically injects Indicator as a base class if the class
    doesn't already inherit from it, so explicit inheritance is optional:

        @indicator(name='sma', display_name='SMA', type='overlay')
        class SMA:              # decorator injects Indicator base
            ...

        @indicator(name='sma', display_name='SMA', type='overlay')
        class SMA(Indicator):   # also works (backward compatible)
            ...
    """
    def decorator(cls: type) -> type:
        # Inject Indicator base class if not already present
        if not issubclass(cls, Indicator):
            new_bases = (Indicator,) if cls.__bases__ == (object,) else (*cls.__bases__, Indicator)
            ns = {k: v for k, v in cls.__dict__.items() if k not in ('__dict__', '__weakref__')}
            cls = _make_type(cls.__name__, new_bases, ns)

        # Store metadata on class
        cls._indicator_name = name
        cls._indicator_display_name = display_name
        cls._indicator_type = type
        cls._indicator_category = category
        cls._indicator_y_range = y_range

        # Auto-register
        INDICATOR_REGISTRY[name] = cls

        return cls

    return decorator


class Indicator:
    """
    Base class for all technical indicators.

    Subclasses must implement:
        plot(df) -> List[PlotComponent]: Compute and return visualization

    Parameters are defined using param descriptors:
        period = param.number(default=20, min=1, max=500)
        color = param.color(default='#2962ff')

    Multi-Timeframe Support:
        Indicators can access higher timeframe data using the @computed decorator.

        htf = param.timeframe(default='1D', label='Higher Timeframe')

        @computed(timeframe=htf)
        def htf_sma(self, df):
            return df['close'].rolling(self.period).mean()

    Example:
        @indicator(name='sma', display_name='SMA', type='overlay')
        class SMA:  # @indicator injects Indicator base automatically
            period = param.number(default=20, min=1, max=500)
            color = param.color(default='#2962ff')

            def plot(self, df):
                sma = df['close'].rolling(self.period).mean()
                return [Line(y=sma, color=self.color, line_width=2)]
    """

    # These are set by the @indicator decorator
    _indicator_name: str = ''
    _indicator_display_name: str = ''
    _indicator_type: str = 'overlay'
    _indicator_category: str = 'Other'
    _indicator_y_range: Optional[Dict[str, float]] = None

    # MTF data (injected by charting service before rendering)
    _mtf_data: Dict[str, pd.DataFrame] = None
    _current_df: pd.DataFrame = None
    _mtf_bar_indices: Dict[str, np.ndarray] = None

    def __init__(self, **kwargs):
        """
        Initialize indicator with optional parameter overrides.

        Parameters can be passed as keyword arguments to override defaults.
        """
        for attr_name in dir(self.__class__):
            attr = getattr(self.__class__, attr_name, None)
            if isinstance(attr, InputParam):
                value = kwargs.get(attr_name, attr.default)
                setattr(self, attr_name, value)

    @classmethod
    def _get_input_params(cls) -> List[InputParam]:
        """Get all InputParam descriptors from the class"""
        params = []
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name, None)
            if isinstance(attr, InputParam):
                params.append(attr)
        return params

    def compute(self, df: pd.DataFrame) -> Union[pd.Series, IndicatorResult, Any]:
        """
        Compute raw indicator values for use in strategies.

        Override in subclasses to return:
        - pd.Series for single-value indicators (RSI, SMA, EMA)
        - IndicatorResult subclass for multi-value indicators (MACD, BBands)

        Args:
            df: DataFrame with OHLCV data

        Returns:
            pd.Series or IndicatorResult with computed values

        Example:
            def compute(self, df):
                return ta.momentum.rsi(df['close'], window=self.period)
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} must implement compute(df) to be used in strategies"
        )

    def plot(self, df: pd.DataFrame) -> List[PlotComponent]:
        """
        Define the indicator's visualization. Required for all indicators.

        Compute indicator values and return plot components.

        Args:
            df: DataFrame with OHLCV data (timestamp, open, high, low, close, volume)

        Returns:
            List of PlotComponent objects (Line, Scatter, Bar, etc.)

        Example:
            def plot(self, df):
                sma = df['close'].rolling(self.period).mean()
                return [Line(y=sma, color=self.color, line_width=2)]
        """
        raise NotImplementedError("Subclasses must implement plot(df)")

    @property
    def config(self) -> IndicatorConfig:
        """Build IndicatorConfig from class metadata"""
        configurable_params = []
        for p in self._get_input_params():
            config_param = p.to_config_param()
            config_param.value = getattr(self, p.name)
            configurable_params.append(config_param)

        # Generate display name with params
        display_name = self._indicator_display_name
        if '(' not in display_name:
            params = self._get_input_params()
            primary_params = [p for p in params if p.param_type == 'number' and p.name in ('period', 'fast', 'k_period')]
            if primary_params:
                param_values = ', '.join(str(getattr(self, p.name)) for p in primary_params)
                display_name = f"{display_name} ({param_values})"

        return IndicatorConfig(
            name=self._indicator_name,
            display_name=display_name,
            type=self._indicator_type,
            category=self._indicator_category,
            render_components=[],
            y_range=self._indicator_y_range,
            configurable_params=configurable_params
        )

    def get_info(self) -> IndicatorInfoDict:
        """Get indicator metadata without calculating data"""
        return {
            'name': self._indicator_name,
            'display_name': self._indicator_display_name,
            'type': self._indicator_type,  # type: ignore[typeddict-item]
            'category': self._indicator_category
        }

    # =========================================================================
    # Multi-Timeframe (MTF) Support
    # =========================================================================

    def get_instance_timeframes(self) -> Set[str]:
        """
        Get all timeframes required by this instance.

        Scans @computed decorators for timeframe declarations.

        Returns:
            Set of timeframe strings needed for this instance
        """
        timeframes: Set[str] = set()
        for name in dir(self.__class__):
            try:
                attr = getattr(self.__class__, name, None)
                if isinstance(attr, property) and hasattr(attr.fget, '_timeframe'):
                    tf_spec = attr.fget._timeframe
                    if tf_spec:
                        if isinstance(tf_spec, InputParam):
                            # It's a param - resolve value from instance
                            tf = getattr(self, tf_spec.name)
                        else:
                            tf = tf_spec
                        if tf:
                            timeframes.add(tf)
            except Exception:
                continue
        return timeframes

    def get_mtf_limits(self) -> Dict[str, int]:
        """
        Get MTF candle limits for each required timeframe.

        Returns:
            Dict mapping timeframe to limit (e.g., {'1D': 1000})
        """
        limits: Dict[str, int] = {}
        for name in dir(self.__class__):
            try:
                attr = getattr(self.__class__, name, None)
                if isinstance(attr, property) and hasattr(attr.fget, '_timeframe'):
                    tf_spec = attr.fget._timeframe
                    mtf_limit = getattr(attr.fget, '_mtf_limit', 1000)
                    if tf_spec:
                        if isinstance(tf_spec, InputParam):
                            tf = getattr(self, tf_spec.name)
                        else:
                            tf = tf_spec
                        if tf:
                            # Use max limit if multiple @computed use same timeframe
                            limits[tf] = max(limits.get(tf, 0), mtf_limit)
            except Exception:
                continue
        return limits

    def _get_mtf_bar_index(self, timeframe: str) -> int:
        """Find which MTF bar corresponds to the current context."""
        if self._mtf_bar_indices is not None and timeframe in self._mtf_bar_indices:
            # For indicators, we typically render the full series
            # Return last bar index by default
            return len(self._mtf_bar_indices[timeframe]) - 1

        if self._mtf_data is None or timeframe not in self._mtf_data:
            return -1

        # Return last bar
        return len(self._mtf_data[timeframe]) - 1

    def _precompute_mtf_indices(self, base_df: pd.DataFrame) -> None:
        """Map each base bar to its corresponding MTF bar index (-1 if before MTF coverage)."""
        if self._mtf_data is None:
            return

        self._mtf_bar_indices = {}
        base_ts = base_df['timestamp'].values

        for tf, mtf_df in self._mtf_data.items():
            mtf_ts = mtf_df['timestamp'].values
            indices = np.searchsorted(mtf_ts, base_ts, side='right') - 1
            # Mark bars before MTF start as -1, clamp to last bar for bars after
            self._mtf_bar_indices[tf] = np.clip(indices, -1, len(mtf_ts) - 1)

    def align(self, series: pd.Series, timeframe: str) -> pd.Series:
        """
        Align HTF series to base timeframe using fill-forward.

        Prefer using .aligned() method on @computed properties instead:
            self.htf_sma.aligned()

        Args:
            series: HTF indicator values
            timeframe: Which MTF timeframe this series is from

        Returns NaN for bars outside MTF coverage.
        """
        if series is None or len(series) == 0:
            return pd.Series(dtype=float)

        indices = self._mtf_bar_indices.get(timeframe) if self._mtf_bar_indices else None
        if indices is not None:
            safe_idx = np.clip(indices, 0, len(series) - 1)
            values = np.where(indices >= 0, series.iloc[safe_idx].values, np.nan)
            return pd.Series(values)

        return pd.Series([series.iloc[-1]] * len(self._current_df))

    def to_dict(self, df: pd.DataFrame) -> IndicatorResponseDict:
        """Convert indicator to dictionary format for API response"""
        # Set _current_df so @computed properties work in plot()
        self._current_df = df
        plot_result = self.plot(df)
        timestamps = [int(t) for t in df['timestamp'].tolist()] if 'timestamp' in df.columns else []

        # Use new function that handles both List[PlotComponent] and PlotResult
        plot_data = extract_plot_data_with_panels(plot_result, timestamps)

        configurable_params = []
        for p in self.config.configurable_params:
            param_dict = {
                'name': p.name,
                'label': p.label,
                'type': p.type,
                'value': p.value
            }
            if p.min_value is not None:
                param_dict['min_value'] = p.min_value
            if p.max_value is not None:
                param_dict['max_value'] = p.max_value
            if p.step is not None:
                param_dict['step'] = p.step
            if p.options is not None:
                param_dict['options'] = p.options
            configurable_params.append(param_dict)

        response = {
            'name': self.config.name,
            'type': self.config.type,
            'display_name': self.config.display_name,
            'data': plot_data['overlay_data'],
            'render_components': plot_data['overlay_components'],
            'panels': plot_data['panels'],
            'configurable_params': configurable_params
        }

        if self.config.y_range:
            response['y_range'] = self.config.y_range

        return response


def get_indicator(name: str, **kwargs) -> Indicator:
    """
    Get an indicator instance by name.

    Args:
        name: Name of the indicator
        **kwargs: Parameter overrides

    Returns:
        Indicator instance
    """
    if name not in INDICATOR_REGISTRY:
        raise ValueError(f"Unknown indicator: {name}")

    cls = INDICATOR_REGISTRY[name]
    return cls(**kwargs)


def get_available_indicators() -> Dict[str, type]:
    """Get all registered indicators"""
    return INDICATOR_REGISTRY.copy()


def get_indicator_info(name: str) -> IndicatorInfoDict:
    """Get indicator info without instantiating"""
    if name not in INDICATOR_REGISTRY:
        raise ValueError(f"Unknown indicator: {name}")

    cls = INDICATOR_REGISTRY[name]
    return {
        'name': cls._indicator_name,
        'display_name': cls._indicator_display_name,
        'type': cls._indicator_type,  # type: ignore[typeddict-item]
        'category': cls._indicator_category
    }


def get_all_indicators() -> list:
    """Get info for all registered indicators."""
    result = []
    for name, cls in INDICATOR_REGISTRY.items():
        result.append({
            'name': cls._indicator_name,
            'display_name': cls._indicator_display_name,
            'type': cls._indicator_type,
            'category': cls._indicator_category
        })
    return result


# =============================================================================
# use_indicator() Descriptor
# =============================================================================

class IndicatorRef:
    """
    Descriptor that pulls an indicator from the registry by name.

    Behaves like @computed: cached, auto-sliced in on_bar context, full in plot context.
    Returns IndicatorSeries with .panel() and .plot_components() helpers.

    Usage:
        class MyStrategy(Strategy):
            sma_period = param.number(default=200)
            sma = use_indicator('sma', period=sma_period)
            rsi = use_indicator('rsi')

            def on_bar(self, df):
                if self.rsi.iloc[-1] < 30:
                    return self.buy()
    """

    def __init__(self, name: str, timeframe=None, **param_mapping):
        self.indicator_name = name
        self.timeframe = timeframe
        self.param_mapping = param_mapping
        self.attr_name: Optional[str] = None

    def __set_name__(self, owner, name):
        self.attr_name = name

    def _resolve_indicator(self, obj) -> 'Indicator':
        """Lazy-instantiate the indicator, cached on the strategy instance."""
        cache_key = f'_ui_ind_{self.attr_name}'
        if cache_key in obj.__dict__:
            return obj.__dict__[cache_key]

        kwargs = {}
        for k, v in self.param_mapping.items():
            if isinstance(v, InputParam):
                kwargs[k] = getattr(obj, v.name)
            else:
                kwargs[k] = v

        ind = get_indicator(self.indicator_name, **kwargs)
        obj.__dict__[cache_key] = ind
        return ind

    @staticmethod
    def _slice_indicator_result(result: 'IndicatorResult', end_idx: int) -> 'IndicatorResult':
        """Slice all Series fields in an IndicatorResult to end_idx."""
        from dataclasses import fields as dc_fields
        kwargs = {}
        for f in dc_fields(result):
            val = getattr(result, f.name)
            if isinstance(val, pd.Series):
                kwargs[f.name] = val.iloc[:end_idx]
            else:
                kwargs[f.name] = val
        return type(result)(**kwargs)

    @overload
    def __get__(self, obj: None, objtype: type) -> 'IndicatorRef': ...
    @overload
    def __get__(self, obj: Any, objtype: type) -> 'IndicatorSeries': ...

    def __get__(self, obj: Any, objtype: Any = None) -> Any:
        if obj is None:
            return self

        from .series import wrap_indicator_series

        ind = self._resolve_indicator(obj)

        # Resolve timeframe
        tf = self.timeframe
        if isinstance(tf, InputParam):
            tf = getattr(obj, tf.name)

        cache_attr = f'_ui_{self.attr_name}'

        # --- MTF path ---
        if tf:
            mtf_data = getattr(obj, '_mtf_data', None)
            if not mtf_data or tf not in mtf_data:
                return pd.Series(dtype=float)
            full_df = mtf_data[tf]
            if full_df is None or len(full_df) == 0:
                return pd.Series(dtype=float)

            # Get current MTF bar index for slicing
            mtf_idx = -1
            ctx = getattr(obj, '_context', None)
            if ctx is not None:
                mtf_idx = obj._get_mtf_bar_index(tf)
                if mtf_idx >= len(full_df):
                    mtf_idx = len(full_df) - 1

            cache_key = (len(full_df), tf)
            cache = obj.__dict__.get(cache_attr)
            if cache is not None and cache[0] == cache_key:
                full_result = cache[1]
            else:
                full_result = ind.compute(full_df)
                obj.__dict__[cache_attr] = (cache_key, full_result)

            # Multi-value IndicatorResult
            if isinstance(full_result, IndicatorResult):
                if mtf_idx >= 0:
                    return self._slice_indicator_result(full_result, mtf_idx)
                return full_result

            if mtf_idx >= 0:
                sliced = full_result.iloc[:mtf_idx]
                return wrap_indicator_series(sliced, timeframe=tf, owner=obj, indicator_instance=ind)
            return wrap_indicator_series(full_result, timeframe=tf, owner=obj, indicator_instance=ind)

        # --- Non-MTF path ---
        current_df = getattr(obj, '_current_df', None)
        if current_df is None:
            raise RuntimeError("Strategy._current_df must be set.")
        if len(current_df) == 0:
            return pd.Series(dtype=float)

        cache_key = (len(current_df), None)
        cache = obj.__dict__.get(cache_attr)
        if cache is not None and cache[0] == cache_key:
            full_result = cache[1]
        else:
            full_result = ind.compute(current_df)
            obj.__dict__[cache_attr] = (cache_key, full_result)

        # Multi-value IndicatorResult
        if isinstance(full_result, IndicatorResult):
            ctx = getattr(obj, '_context', None)
            if ctx is not None and not getattr(obj, '_is_computing', False):
                return self._slice_indicator_result(full_result, ctx.bar_index + 1)
            return full_result

        # Auto-slice if in on_bar context
        ctx = getattr(obj, '_context', None)
        if ctx is not None and not getattr(obj, '_is_computing', False):
            bar_idx = ctx.bar_index
            sliced = full_result.iloc[:bar_idx + 1]
            return wrap_indicator_series(sliced, owner=obj, indicator_instance=ind)

        # Full series (plot context)
        return wrap_indicator_series(full_result, owner=obj, indicator_instance=ind)


def use_indicator(name: str, *, timeframe=None, **kwargs) -> IndicatorRef:
    """
    Declare a strategy attribute backed by a registered indicator.

    Returns an IndicatorSeries (pd.Series subclass) with .panel() and
    .plot_components() helpers. Supports MTF via timeframe parameter.

    Args:
        name: Indicator registry name (e.g., 'rsi', 'sma', 'ema')
        timeframe: Optional timeframe for MTF (e.g., '1D') or InputParam
        **kwargs: Parameter overrides passed to the indicator constructor.
                  Values can be literals or InputParam references for dynamic binding.

    Usage:
        sma_period = param.number(default=200)
        sma = use_indicator('sma', period=sma_period)   # overlay
        rsi = use_indicator('rsi')                       # oscillator, default period=14
        daily_rsi = use_indicator('rsi', period=14, timeframe='1D')  # MTF

        def on_bar(self, df):
            if self.rsi.iloc[-1] < 30:
                return self.buy()

        def plot(self, df):
            return PlotResult(
                overlay=[*self.sma.plot_components(df)],
                panels=[self.rsi.panel(df)],
            )
    """
    return IndicatorRef(name, timeframe=timeframe, **kwargs)


__all__ = [
    # Types (from former types.py)
    'StyleValue',
    'RenderComponent',
    'IndicatorConfig',
    # Result base class (from former results.py)
    'IndicatorResult',
    # Indicator types
    'IndicatorInfoDict',
    'ConfigParamDict',
    'IndicatorResponseDict',
    # Registry
    'INDICATOR_REGISTRY',
    # Decorators
    'indicator',
    # Base class
    'Indicator',
    # Descriptor
    'IndicatorRef',
    'use_indicator',
    # Helper functions
    'get_indicator',
    'get_available_indicators',
    'get_indicator_info',
    'get_all_indicators',
]
