"""Trend detection algorithms using candle data."""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Tuple


class TrendDirection(Enum):
    """Possible trend directions."""
    UPTREND = "uptrend"
    DOWNTREND = "downtrend"
    NEUTRAL = "neutral"


@dataclass
class TrendResult:
    """Result of trend analysis."""
    direction: TrendDirection
    strength: float  # 0.0 to 1.0
    slope: float  # Linear regression slope
    confidence: float  # 0.0 to 1.0
    sma_short: float  # Short-term SMA value
    sma_long: float  # Long-term SMA value
    rsi: Optional[float] = None  # RSI value if computed


@dataclass
class Candle:
    """Normalized candle data."""
    timestamp: int
    open: float
    high: float
    low: float
    close: float
    volume: float

    @classmethod
    def from_dict(cls, data: dict) -> "Candle":
        """Create Candle from database row dict."""
        return cls(
            timestamp=int(data["timestamp"]),
            open=float(data["Open"]),
            high=float(data["High"]),
            low=float(data["Low"]),
            close=float(data["Close"]),
            volume=float(data["Volume"]),
        )


def _sma(values: List[float], period: int) -> List[float]:
    """Compute Simple Moving Average."""
    if len(values) < period:
        return []
    sma_values = []
    for i in range(period - 1, len(values)):
        window = values[i - period + 1 : i + 1]
        sma_values.append(sum(window) / period)
    return sma_values


def _ema(values: List[float], period: int) -> List[float]:
    """Compute Exponential Moving Average."""
    if len(values) < period:
        return []
    multiplier = 2.0 / (period + 1)
    ema_values = [values[0]]
    for val in values[1:]:
        ema_values.append((val - ema_values[-1]) * multiplier + ema_values[-1])
    return ema_values


def _rsi(closes: List[float], period: int = 14) -> Optional[float]:
    """Compute Relative Strength Index."""
    if len(closes) < period + 1:
        return None
    deltas = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
    gains = [max(d, 0.0) for d in deltas]
    losses = [abs(min(d, 0.0)) for d in deltas]
    avg_gain = sum(gains[-period:]) / period
    avg_loss = sum(losses[-period:]) / period
    if avg_loss == 0:
        return 100.0
    rs = avg_gain / avg_loss
    return 100.0 - (100.0 / (1.0 + rs))


def _linear_regression_slope(values: List[float]) -> float:
    """Compute slope of linear regression through values."""
    n = len(values)
    if n < 2:
        return 0.0
    x_mean = (n - 1) / 2.0
    y_mean = sum(values) / n
    numerator = sum((i - x_mean) * (v - y_mean) for i, v in enumerate(values))
    denominator = sum((i - x_mean) ** 2 for i in range(n))
    if denominator == 0:
        return 0.0
    return numerator / denominator


def detect_trend(candles: List[Candle],
                 short_period: int = 8,
                 long_period: int = 21,
                 use_rsi: bool = True) -> TrendResult:
    """
    Detect trend direction based on moving averages and momentum.

    Uses last N candles (recommended: 24) to determine trend direction
    by comparing short and long period SMAs and analyzing momentum.

    Args:
        candles: List of candles (oldest to newest)
        short_period: SMA period for short-term trend
        long_period: SMA period for long-term trend
        use_rsi: Whether to include RSI in analysis

    Returns:
        TrendResult with direction, strength, slope, and confidence
    """
    if len(candles) < long_period + 1:
        return TrendResult(
            direction=TrendDirection.NEUTRAL,
            strength=0.0,
            slope=0.0,
            confidence=0.0,
            sma_short=candles[-1].close if candles else 0.0,
            sma_long=candles[-1].close if candles else 0.0,
        )

    closes = [c.close for c in candles]
    highs = [c.high for c in candles]
    lows = [c.low for c in candles]

    # Compute SMAs
    sma_short_values = _sma(closes, short_period)
    sma_long_values = _sma(closes, long_period)

    sma_short = sma_short_values[-1] if sma_short_values else closes[-1]
    sma_long = sma_long_values[-1] if sma_long_values else closes[-1]

    # Linear regression slope on closes
    slope = _linear_regression_slope(closes)

    # Trend direction based on SMA crossover
    if sma_short > sma_long:
        raw_direction = TrendDirection.UPTREND
        raw_strength = (sma_short - sma_long) / sma_long if sma_long != 0 else 0.0
    elif sma_short < sma_long:
        raw_direction = TrendDirection.DOWNTREND
        raw_strength = (sma_long - sma_short) / sma_long if sma_long != 0 else 0.0
    else:
        raw_direction = TrendDirection.NEUTRAL
        raw_strength = 0.0

    # Confirm with slope
    slope_confirms = (slope > 0 and raw_direction == TrendDirection.UPTREND) or \
                     (slope < 0 and raw_direction == TrendDirection.DOWNTREND)

    # RSI confirmation
    rsi_value = _rsi(closes) if use_rsi else None
    rsi_confirms = True
    if rsi_value is not None and raw_direction == TrendDirection.UPTREND:
        rsi_confirms = rsi_value > 40  # Not oversold
    elif rsi_value is not None and raw_direction == TrendDirection.DOWNTREND:
        rsi_confirms = rsi_value < 60  # Not overbought

    # Confidence: 0-1 based on strength, slope confirmation, and RSI
    strength_clamped = min(raw_strength * 10, 1.0)  # Scale and clamp
    confidence = strength_clamped
    if slope_confirms:
        confidence = min(confidence + 0.2, 1.0)
    if rsi_confirms:
        confidence = min(confidence + 0.15, 1.0)

    # Final direction: if no confirmation, weaken
    if not slope_confirms and not rsi_confirms:
        final_direction = TrendDirection.NEUTRAL
        confidence *= 0.5
    elif not slope_confirms or not rsi_confirms:
        confidence *= 0.8
        final_direction = raw_direction
    else:
        final_direction = raw_direction

    return TrendResult(
        direction=final_direction,
        strength=min(raw_strength, 1.0),
        slope=slope,
        confidence=round(confidence, 4),
        sma_short=round(sma_short, 8),
        sma_long=round(sma_long, 8),
        rsi=round(rsi_value, 2) if rsi_value is not None else None,
    )


def detect_trend_reversal(candles: List[Candle], period: int = 14) -> Optional[str]:
    """
    Detect potential trend reversal signals.

    Args:
        candles: List of candles (oldest to newest)
        period: Lookback period

    Returns:
        Reversal signal string or None
    """
    if len(candles) < period + 2:
        return None

    recent = candles[-period:]
    closes = [c.close for c in recent]

    rsi = _rsi(closes, min(period - 1, len(closes) - 1))
    if rsi is None:
        return None

    last_candle = candles[-1]
    prev_candle = candles[-2]

    # Bullish reversal: RSI oversold + close > open
    if rsi < 30 and last_candle.close > last_candle.open:
        return "bullish_reversal"

    # Bearish reversal: RSI overbought + close < open
    if rsi > 70 and last_candle.close < last_candle.open:
        return "bearish_reversal"

    return None


@dataclass
class Level:
    """A price level with metadata for strength assessment."""
    price: float
    touches: int
    first_ts: int
    last_ts: int
    is_recent: bool = field(default=False)


def _cluster_levels(candidates: List[Tuple[float, int]],
                    avg_price: float) -> List[Level]:
    """
    Cluster nearby price levels and return merged Level objects.

    Groups levels within 0.5% of each other.  A cluster with more
    touches is considered "stronger".

    Args:
        candidates: List of (price, timestamp) tuples, chronologically ordered
        avg_price: Average price for computing relative distance threshold

    Returns:
        List of Level objects sorted by touches descending
    """
    if not candidates:
        return []

    relative_threshold = avg_price * 0.005  # 0.5% zone
    min_threshold = avg_price * 0.001       # floor: 0.1%

    clusters: List[Dict] = []  # [{prices:[], timestamps:[], avg, touches}]

    for price, ts in candidates:
        merged = False
        for cluster in clusters:
            cluster_avg = sum(cluster["prices"]) / len(cluster["prices"])
            if abs(price - cluster_avg) < max(relative_threshold, min_threshold):
                cluster["prices"].append(price)
                cluster["timestamps"].append(ts)
                merged = True
                break
        if not merged:
            clusters.append({"prices": [price], "timestamps": [ts]})

    newest_ts = candidates[-1][1] if candidates else 0

    levels = []
    for cluster in clusters:
        prices = cluster["prices"]
        timestamps = cluster["timestamps"]
        levels.append(Level(
            price=round(sum(prices) / len(prices), 5),
            touches=len(prices),
            first_ts=min(timestamps),
            last_ts=max(timestamps),
            is_recent=(max(timestamps) >= newest_ts - 86400),
        ))

    levels.sort(key=lambda l: l.touches, reverse=True)
    return levels


def detect_support_resistance(candles: List[Candle],
                               lookback: int = 24,
                               min_touches: int = 2) -> Tuple[List[Level], List[Level]]:
    """
    Detect support and resistance levels from recent candles.

    Finds local price extrema, clusters them by proximity, and returns
    only levels with sufficient touches to be considered "strong".

    Args:
        candles: List of candles sorted by timestamp ascending
        lookback: Number of recent candles to analyze
        min_touches: Minimum touches in a cluster to include the level

    Returns:
        Tuple of (support_levels, resistance_levels) — each element is
        a list of Level dataclass objects with price, touches, and timestamps.
    """
    if len(candles) < lookback:
        lookback = len(candles)

    if lookback == 0:
        return [], []

    recent = candles[-lookback:]
    lows = [(c.low, c.timestamp) for c in recent]
    highs = [(c.high, c.timestamp) for c in recent]
    avg_price = sum(c.close for c in recent) / len(recent)

    support_candidates: List[Tuple[float, int]] = []
    resistance_candidates: List[Tuple[float, int]] = []

    for i in range(1, len(lows) - 1):
        if lows[i][0] <= lows[i - 1][0] and lows[i][0] <= lows[i + 1][0]:
            support_candidates.append(lows[i])

    for i in range(1, len(highs) - 1):
        if highs[i][0] >= highs[i - 1][0] and highs[i][0] >= highs[i + 1][0]:
            resistance_candidates.append(highs[i])

    supports = _cluster_levels(support_candidates, avg_price)
    resistances = _cluster_levels(resistance_candidates, avg_price)

    supports = [s for s in supports if s.touches >= min_touches]
    resistances = [r for r in resistances if r.touches >= min_touches]

    return supports, resistances