"""
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, Union
from typing_extensions import TypedDict
import pandas as pd
import numpy as np

from .params import InputParam, ParamValue, SelectOption, ConfigParam
from .plot import PlotComponent, extract_plot_data_with_panels
from .series import wrap_series

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


# =============================================================================
# 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:
    """
    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)

    def __get__(self, obj, objtype=None):
        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',
]
