"""
AlignableSeries - A pandas Series with timeframe metadata and alignment support.

Provides the `.aligned()` method for MTF (multi-timeframe) computed properties,
eliminating the need to pass timeframe twice.

Usage:
    @computed(timeframe='1D')
    def htf_sma(self, df):
        return df['close'].rolling(50).mean()

    # In plot():
    aligned_sma = self.htf_sma.aligned()  # No need to pass timeframe!
"""

from __future__ import annotations
import pandas as pd
from typing import TYPE_CHECKING, Optional, Any

if TYPE_CHECKING:
    from .strategy import Strategy
    from .indicator import Indicator


class AlignableSeries(pd.Series):
    """
    A pandas Series with timeframe metadata and .aligned() method.

    Extends pd.Series to support automatic alignment of higher-timeframe
    data to the base timeframe without requiring explicit timeframe parameter.

    Attributes:
        _timeframe: The timeframe this series was computed on (e.g., '1D', '4h')
        _owner: Reference to the Strategy/Indicator instance for align() access

    Example:
        @computed(timeframe='1D')
        def daily_sma(self, df):
            return df['close'].rolling(50).mean()

        def plot(self, df):
            # Old way: self.align(self.daily_sma, self.timeframe)
            # New way:
            sma = self.daily_sma.aligned()
            return [Line(y=sma, color='#3b82f6')]
    """

    # Tell pandas which attributes to preserve during operations
    _metadata = ['_timeframe', '_owner']

    def __init__(
        self,
        data: Any = None,
        index: Any = None,
        dtype: Any = None,
        name: Any = None,
        copy: bool = False,
        timeframe: Optional[str] = None,
        owner: Any = None,
        **kwargs
    ):
        """
        Initialize AlignableSeries.

        Args:
            data: Array-like, Iterable, dict, or scalar value for Series data
            index: Index for the Series
            dtype: Data type for the Series
            name: Name of the Series
            copy: Whether to copy input data
            timeframe: The timeframe this series was computed on
            owner: Reference to Strategy/Indicator instance
            **kwargs: Additional arguments passed to pd.Series
        """
        super().__init__(data=data, index=index, dtype=dtype, name=name, copy=copy, **kwargs)
        object.__setattr__(self, '_timeframe', timeframe)
        object.__setattr__(self, '_owner', owner)

    @property
    def _constructor(self):
        """Return a callable that creates new AlignableSeries preserving metadata."""
        def _alignable_series_constructor(*args, **kwargs):
            # Preserve our custom attributes
            kwargs.setdefault('timeframe', getattr(self, '_timeframe', None))
            kwargs.setdefault('owner', getattr(self, '_owner', None))
            return AlignableSeries(*args, **kwargs)
        return _alignable_series_constructor

    @property
    def _constructor_expanddim(self):
        """Return DataFrame constructor for operations that expand dimensions."""
        return pd.DataFrame

    def aligned(self) -> pd.Series:
        """
        Align this HTF series to the base timeframe.

        Maps each base bar to the corresponding HTF value that was
        active at that time (fill-forward alignment).

        Returns:
            pd.Series: Series aligned to base timeframe length

        Raises:
            RuntimeError: If called without owner reference or on non-MTF series

        Example:
            @computed(timeframe='1D')
            def daily_rsi(self, df):
                return ta.momentum.rsi(df['close'], window=14)

            def plot(self, df):
                # Aligns daily RSI to current chart timeframe
                rsi = self.daily_rsi.aligned()
                return [Line(y=rsi, color='#8b5cf6')]
        """
        timeframe = getattr(self, '_timeframe', None)
        owner = getattr(self, '_owner', None)

        if timeframe is None:
            # Not an MTF series, return as regular Series
            return pd.Series(self.values, index=self.index)

        if owner is None:
            raise RuntimeError(
                "Cannot call .aligned() without owner reference. "
                "This usually means the series was created outside of @computed decorator."
            )

        # Use the owner's align method
        return owner.align(self, timeframe)

    def __repr__(self) -> str:
        """String representation showing timeframe metadata."""
        base_repr = super().__repr__()
        timeframe = getattr(self, '_timeframe', None)
        if timeframe:
            return f"{base_repr}\nTimeframe: {timeframe}"
        return base_repr


def wrap_series(
    series: pd.Series,
    timeframe: Optional[str] = None,
    owner: Any = None
) -> AlignableSeries:
    """
    Wrap a pandas Series in an AlignableSeries with metadata.

    Utility function for the @computed decorator to wrap results.

    Args:
        series: The pandas Series to wrap
        timeframe: The timeframe this series was computed on
        owner: Reference to the Strategy/Indicator instance

    Returns:
        AlignableSeries with metadata attached
    """
    if isinstance(series, AlignableSeries):
        # Already wrapped, just update metadata if needed
        if timeframe is not None:
            object.__setattr__(series, '_timeframe', timeframe)
        if owner is not None:
            object.__setattr__(series, '_owner', owner)
        return series

    return AlignableSeries(
        series.values,
        index=series.index,
        name=series.name,
        timeframe=timeframe,
        owner=owner
    )


class IndicatorSeries(AlignableSeries):
    """
    AlignableSeries with indicator plot helpers.

    Extends AlignableSeries to provide .panel() and .plot_components() methods
    that delegate to the underlying indicator instance's plot().

    Compatible with all pd.Series operations (crossover, .iloc, arithmetic).

    Attributes:
        _indicator_instance: Reference to the Indicator that produced this series
    """

    _metadata = ['_timeframe', '_owner', '_indicator_instance']

    def __init__(
        self,
        data: Any = None,
        index: Any = None,
        dtype: Any = None,
        name: Any = None,
        copy: bool = False,
        timeframe: Optional[str] = None,
        owner: Any = None,
        indicator_instance: Any = None,
        **kwargs
    ):
        super().__init__(
            data=data, index=index, dtype=dtype, name=name, copy=copy,
            timeframe=timeframe, owner=owner, **kwargs
        )
        object.__setattr__(self, '_indicator_instance', indicator_instance)

    @property
    def _constructor(self):
        """Return a callable that creates new IndicatorSeries preserving metadata."""
        def _indicator_series_constructor(*args, **kwargs):
            kwargs.setdefault('timeframe', getattr(self, '_timeframe', None))
            kwargs.setdefault('owner', getattr(self, '_owner', None))
            kwargs.setdefault('indicator_instance', getattr(self, '_indicator_instance', None))
            return IndicatorSeries(*args, **kwargs)
        return _indicator_series_constructor

    def panel(self, df=None) -> 'Panel':
        """
        Create a Panel from the indicator's plot() output.

        Args:
            df: DataFrame to plot with. If None, uses owner's _current_df.

        Returns:
            Panel with the indicator's plot components.
        """
        from .plot import Panel

        ind = getattr(self, '_indicator_instance', None)
        if ind is None:
            raise RuntimeError("Cannot call .panel() without indicator instance reference.")

        owner = getattr(self, '_owner', None)
        plot_df = df if df is not None else (owner._current_df if owner else None)
        if plot_df is None:
            raise RuntimeError("No DataFrame available for .panel(). Pass df explicitly.")

        return Panel(
            name=ind._indicator_display_name,
            components=ind.plot(plot_df),
        )

    def plot_components(self, df=None) -> list:
        """
        Get raw plot components for overlay use.

        Args:
            df: DataFrame to plot with. If None, uses owner's _current_df.

        Returns:
            List of PlotComponent objects from the indicator's plot().
        """
        ind = getattr(self, '_indicator_instance', None)
        if ind is None:
            raise RuntimeError("Cannot call .plot_components() without indicator instance reference.")

        owner = getattr(self, '_owner', None)
        plot_df = df if df is not None else (owner._current_df if owner else None)
        if plot_df is None:
            raise RuntimeError("No DataFrame available for .plot_components(). Pass df explicitly.")

        return ind.plot(plot_df)


def wrap_indicator_series(
    series: pd.Series,
    timeframe: Optional[str] = None,
    owner: Any = None,
    indicator_instance: Any = None,
) -> IndicatorSeries:
    """
    Wrap a pandas Series in an IndicatorSeries with metadata.

    Args:
        series: The pandas Series to wrap
        timeframe: The timeframe this series was computed on
        owner: Reference to the Strategy instance
        indicator_instance: Reference to the Indicator that produced this series

    Returns:
        IndicatorSeries with metadata attached
    """
    return IndicatorSeries(
        series.values,
        index=series.index,
        name=series.name,
        timeframe=timeframe,
        owner=owner,
        indicator_instance=indicator_instance,
    )


__all__ = ['AlignableSeries', 'wrap_series', 'IndicatorSeries', 'wrap_indicator_series']
