"""
Price action / market structure analysis.

Covers:
- Swing high/low detection
- Market structure labeling: Higher High, Higher Low, Lower High, Lower Low
- Break of Structure (BOS) and Change of Character (CHoCH)
- Support / Resistance levels (from swing clusters)
- Supply / Demand zones (from strong impulsive moves)
- Liquidity sweeps (wick beyond a prior swing that closes back inside)
- Fair Value Gaps (3-candle imbalance)
- Simple trend continuation / reversal read

All functions take an OHLCV DataFrame (open, high, low, close, volume) and
return either a DataFrame of flags or a small structured dict/list —
whatever is most useful for the confidence engine to consume.
"""

import numpy as np
import pandas as pd
from config import settings as cfg


# ---------------------------------------------------------------------------
# Swing points
# ---------------------------------------------------------------------------

def find_swings(df: pd.DataFrame, lookback: int = None) -> pd.DataFrame:
    """Marks swing highs/lows: a bar is a swing high if its high is the max
    within [i-lookback, i+lookback], swing low is the analogous min."""
    lookback = lookback or cfg.SWING_LOOKBACK
    high, low = df["high"], df["low"]

    swing_high = pd.Series(False, index=df.index)
    swing_low = pd.Series(False, index=df.index)

    for i in range(lookback, len(df) - lookback):
        window_high = high.iloc[i - lookback: i + lookback + 1]
        window_low = low.iloc[i - lookback: i + lookback + 1]
        if high.iloc[i] == window_high.max() and (window_high == high.iloc[i]).sum() == 1:
            swing_high.iloc[i] = True
        if low.iloc[i] == window_low.min() and (window_low == low.iloc[i]).sum() == 1:
            swing_low.iloc[i] = True

    return pd.DataFrame({"swing_high": swing_high, "swing_low": swing_low})


# ---------------------------------------------------------------------------
# Market structure: HH / HL / LH / LL, BOS, CHoCH
# ---------------------------------------------------------------------------

def market_structure(df: pd.DataFrame, lookback: int = None) -> dict:
    """
    Walks confirmed swing points in order and classifies each as HH/HL/LH/LL,
    then derives the current structural trend and flags BOS / CHoCH events.

    Returns:
        {
          "swings": [{"index": ts, "price": float, "type": "high"/"low",
                       "label": "HH"/"HL"/"LH"/"LL"}, ...],
          "trend": "uptrend" | "downtrend" | "ranging",
          "last_bos": {...} or None,
          "last_choch": {...} or None,
        }
    """
    swings_df = find_swings(df, lookback)
    points = []
    for ts, row in swings_df.iterrows():
        if row["swing_high"]:
            points.append({"index": ts, "price": df.loc[ts, "high"], "type": "high"})
        elif row["swing_low"]:
            points.append({"index": ts, "price": df.loc[ts, "low"], "type": "low"})
    points.sort(key=lambda p: p["index"])

    labeled = []
    last_high, last_low = None, None
    trend = "ranging"
    last_bos, last_choch = None, None

    for p in points:
        if p["type"] == "high":
            if last_high is not None:
                label = "HH" if p["price"] > last_high else "LH"
                if label == "LH" and trend == "uptrend":
                    last_choch = {"index": p["index"], "price": p["price"], "from": "uptrend", "to": "downtrend"}
                    trend = "downtrend"
                elif label == "HH" and trend in ("downtrend", "ranging"):
                    last_bos = {"index": p["index"], "price": p["price"], "direction": "bullish"}
                    trend = "uptrend"
                elif label == "HH":
                    last_bos = {"index": p["index"], "price": p["price"], "direction": "bullish"}
            else:
                label = "H"
            last_high = p["price"]
        else:
            if last_low is not None:
                label = "HL" if p["price"] > last_low else "LL"
                if label == "HL" and trend in ("downtrend", "ranging"):
                    last_bos = {"index": p["index"], "price": p["price"], "direction": "bullish"}
                    trend = "uptrend"
                elif label == "LL" and trend == "uptrend":
                    last_choch = {"index": p["index"], "price": p["price"], "from": "uptrend", "to": "downtrend"}
                    trend = "downtrend"
                elif label == "LL":
                    last_bos = {"index": p["index"], "price": p["price"], "direction": "bearish"}
            else:
                label = "L"
            last_low = p["price"]

        labeled.append({**p, "label": label})

    return {
        "swings": labeled,
        "trend": trend,
        "last_bos": last_bos,
        "last_choch": last_choch,
    }


# ---------------------------------------------------------------------------
# Support / Resistance from swing clustering
# ---------------------------------------------------------------------------

def support_resistance(df: pd.DataFrame, lookback: int = None, tolerance_pct: float = 0.15) -> dict:
    swings_df = find_swings(df, lookback)
    highs = df.loc[swings_df["swing_high"], "high"].tolist()
    lows = df.loc[swings_df["swing_low"], "low"].tolist()

    def cluster(levels):
        levels = sorted(levels)
        clusters = []
        for lvl in levels:
            if clusters and abs(lvl - clusters[-1]["price"]) / clusters[-1]["price"] * 100 <= tolerance_pct:
                clusters[-1]["touches"] += 1
                clusters[-1]["price"] = (clusters[-1]["price"] + lvl) / 2
            else:
                clusters.append({"price": lvl, "touches": 1})
        return clusters

    return {"resistance": cluster(highs), "support": cluster(lows)}


# ---------------------------------------------------------------------------
# Supply / Demand zones (impulsive move origin)
# ---------------------------------------------------------------------------

def supply_demand_zones(df: pd.DataFrame, impulse_atr_mult: float = 1.5, atr_period: int = 14) -> list:
    from core.indicators import atr as _atr
    atr_series = _atr(df, atr_period)
    zones = []
    for i in range(1, len(df)):
        body = abs(df["close"].iloc[i] - df["open"].iloc[i])
        if pd.isna(atr_series.iloc[i]) or atr_series.iloc[i] == 0:
            continue
        if body >= impulse_atr_mult * atr_series.iloc[i]:
            origin = df.iloc[i - 1]
            zone_type = "demand" if df["close"].iloc[i] > df["open"].iloc[i] else "supply"
            zones.append({
                "index": df.index[i - 1],
                "type": zone_type,
                "zone_low": float(min(origin["open"], origin["close"])),
                "zone_high": float(max(origin["open"], origin["close"])),
            })
    return zones


# ---------------------------------------------------------------------------
# Liquidity sweeps
# ---------------------------------------------------------------------------

def liquidity_sweeps(df: pd.DataFrame, lookback: int = None) -> list:
    """A sweep = price wicks beyond a prior confirmed swing high/low but
    closes back inside — classic stop-hunt / liquidity grab."""
    lookback = lookback or cfg.SWING_LOOKBACK
    swings_df = find_swings(df, lookback)
    sweeps = []

    swing_highs = df.loc[swings_df["swing_high"], "high"]
    swing_lows = df.loc[swings_df["swing_low"], "low"]

    for i in range(len(df)):
        ts = df.index[i]
        prior_highs = swing_highs[swing_highs.index < ts]
        prior_lows = swing_lows[swing_lows.index < ts]

        if not prior_highs.empty:
            level = prior_highs.iloc[-1]
            if df["high"].iloc[i] > level and df["close"].iloc[i] < level:
                sweeps.append({"index": ts, "type": "sell_side_sweep_of_highs", "level": float(level)})

        if not prior_lows.empty:
            level = prior_lows.iloc[-1]
            if df["low"].iloc[i] < level and df["close"].iloc[i] > level:
                sweeps.append({"index": ts, "type": "buy_side_sweep_of_lows", "level": float(level)})

    return sweeps


# ---------------------------------------------------------------------------
# Fair Value Gaps (3-candle imbalance)
# ---------------------------------------------------------------------------

def fair_value_gaps(df: pd.DataFrame) -> list:
    fvgs = []
    for i in range(2, len(df)):
        c1, c3 = df.iloc[i - 2], df.iloc[i]
        if c3["low"] > c1["high"]:
            fvgs.append({"index": df.index[i], "type": "bullish_fvg", "low": float(c1["high"]), "high": float(c3["low"])})
        elif c3["high"] < c1["low"]:
            fvgs.append({"index": df.index[i], "type": "bearish_fvg", "low": float(c3["high"]), "high": float(c1["low"])})
    return fvgs


# ---------------------------------------------------------------------------
# Trend continuation vs reversal (simple structural read)
# ---------------------------------------------------------------------------

def trend_bias(df: pd.DataFrame) -> str:
    structure = market_structure(df)
    if structure["last_choch"] is not None:
        recent_choch_idx = structure["last_choch"]["index"]
        recent_bos_idx = structure["last_bos"]["index"] if structure["last_bos"] else None
        if recent_bos_idx is None or recent_choch_idx > recent_bos_idx:
            return "reversal"
    if structure["trend"] in ("uptrend", "downtrend"):
        return "continuation"
    return "ranging"


def analyze(df: pd.DataFrame) -> dict:
    """One-call summary used by the confidence engine."""
    structure = market_structure(df)
    return {
        "trend": structure["trend"],
        "last_bos": structure["last_bos"],
        "last_choch": structure["last_choch"],
        "bias": trend_bias(df),
        "support_resistance": support_resistance(df),
        "zones": supply_demand_zones(df),
        "liquidity_sweeps": liquidity_sweeps(df)[-5:],  # most recent 5 only
        "fvgs": fair_value_gaps(df)[-5:],
    }
