"""Support/resistance breakout detection.

Identifies trade events where price breaks through support or resistance levels
detected by the ``features.levels`` module. These functions are used by both
``algotrading-research`` (for labeling trade events) and ``algotrading-backend``
(for live signal generation).

Bar timestamp convention
------------------------
Index = **bar open**; ``close`` is an **end-of-bar** quantity.
A breakout is detected when the current bar's close crosses a level that the
previous bar's close had not yet crossed.  Both bars must have valid level
columns.  See ``BAR_TIMESTAMP_CONVENTIONS.md`` for the full specification.
"""

from __future__ import annotations

import logging

import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)

REQUIRED_OHLCV_COLUMNS: list[str] = ["open", "high", "low", "close", "volume"]


def _validate_breakout_inputs(
    df: pd.DataFrame,
    support_col: str,
    resistance_col: str,
) -> None:
    """Validate DataFrame for breakout detection.

    Args:
        df: OHLCV DataFrame with level columns.
        support_col: Name of the closest-support column.
        resistance_col: Name of the closest-resistance column.

    Raises:
        ValueError: If required columns or index type are invalid.
    """
    if not isinstance(df.index, pd.DatetimeIndex):
        raise ValueError(
            f"❌ DataFrame index must be a DatetimeIndex.\n"
            f"   Got: {type(df.index).__name__}\n"
            f"   Fix: Ensure the DataFrame has a DatetimeIndex (bar-open convention)."
        )

    required = ["close", support_col, resistance_col]
    missing = sorted(set(required) - set(df.columns))
    if missing:
        raise ValueError(
            f"❌ Missing required columns for breakout detection: {missing}\n"
            f"   Available columns: {sorted(df.columns)}\n"
            f"   Fix: Ensure the DataFrame contains 'close' and level columns "
            f"from the feature store."
        )

    if df.empty:
        raise ValueError(
            "❌ DataFrame is empty.\n"
            "   Fix: Provide a non-empty OHLCV DataFrame with level columns."
        )


def get_sr_breakout_events(
    df: pd.DataFrame,
    support_col: str = "closest_level_10th_order_support",
    resistance_col: str = "closest_level_10th_order_resistance",
) -> pd.DatetimeIndex:
    """Detect support/resistance breakout events.

    A **resistance breakout** occurs when the current bar's close exceeds the
    resistance level while the previous bar's close was at or below it.

    A **support breakdown** occurs when the current bar's close falls below the
    support level while the previous bar's close was at or above it.

    Args:
        df: OHLCV DataFrame with a DatetimeIndex (bar-open convention) and
            support/resistance level columns produced by
            ``algotrading_core.features.levels``.
        support_col: Column name for the closest support level.
        resistance_col: Column name for the closest resistance level.

    Returns:
        DatetimeIndex of bar-open timestamps where breakouts occurred.

    Raises:
        ValueError: If the DataFrame is missing required columns or index type.
    """
    _validate_breakout_inputs(df, support_col, resistance_col)

    resistance_breakout = (df["close"] > df[resistance_col]) & (
        df["close"].shift(1) <= df[resistance_col].shift(1)
    ) & (df[resistance_col] == df[resistance_col].shift(1))

    support_breakdown = (df["close"] < df[support_col]) & (
        df["close"].shift(1) >= df[support_col].shift(1)
    ) & (df[support_col] == df[support_col].shift(1))

    breakout_events = df[resistance_breakout | support_breakdown].index

    logger.info(
        "📊 Detected %d breakout events (%d resistance, %d support)",
        len(breakout_events),
        resistance_breakout.sum(),
        support_breakdown.sum(),
    )

    return breakout_events


def get_exit_timestamps(
    trade_events: pd.DatetimeIndex,
    time_shift: pd.Timedelta = pd.Timedelta(days=1),
    trading_dates: pd.DatetimeIndex | None = None,
) -> pd.Series:
    """Compute exit timestamps (t1) for trade events.

    For each trade event, the exit timestamp is ``event + time_shift``.  When
    ``trading_dates`` is provided, exit timestamps that fall on non-trading
    days are rolled forward to the next available trading date.

    Note:
        The adjustment loop iterates over individual events (not DataFrame
        rows) because each exit may land on a different non-trading gap.
        This is event-level iteration, not a DataFrame transform.

    Args:
        trade_events: DatetimeIndex of entry timestamps (bar-open convention).
        time_shift: Holding period offset applied to each event.
        trading_dates: Optional sorted DatetimeIndex of valid trading
            timestamps.  When provided, exits are snapped forward to the
            nearest valid date.

    Returns:
        Series indexed by ``trade_events`` with exit timestamps as values.

    Raises:
        ValueError: If ``trade_events`` is empty.
    """
    if len(trade_events) == 0:
        raise ValueError(
            "❌ trade_events is empty.\n" "   Fix: Provide at least one trade event timestamp."
        )

    raw_exits = trade_events + time_shift

    if trading_dates is None:
        return pd.Series(raw_exits, index=trade_events, name="t1")

    trading_dates = trading_dates.sort_values()
    td_array = trading_dates.values

    adjusted: list[pd.Timestamp] = []
    for exit_time in raw_exits:
        exit_np = np.datetime64(exit_time)
        idx = np.searchsorted(td_array, exit_np, side="left")
        if idx < len(td_array):
            adjusted.append(pd.Timestamp(td_array[idx]))
        else:
            adjusted.append(pd.Timestamp(td_array[-1]))

    logger.debug("🔄 Adjusted %d exit timestamps to trading calendar", len(adjusted))

    return pd.Series(adjusted, index=trade_events, name="t1")
