"""Motore segnali: squeeze detection + ML confirmation su dati live."""
from __future__ import annotations

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler


def keltner_ratio(close: np.ndarray, high: np.ndarray, low: np.ndarray, window: int = 14) -> np.ndarray:
    n = len(close)
    result = np.full(n, np.nan)
    for i in range(window, n):
        wc = close[i - window : i]
        wh = high[i - window : i]
        wl = low[i - window : i]
        ma = np.mean(wc)
        bb_std = np.std(wc)
        tr = np.maximum(wh - wl, np.maximum(np.abs(wh - np.roll(wc, 1)), np.abs(wl - np.roll(wc, 1))))
        atr = np.mean(tr[1:])
        kc_r = (ma + 1.5 * atr) - (ma - 1.5 * atr)
        bb_r = (ma + 2 * bb_std) - (ma - 2 * bb_std)
        if kc_r > 0:
            result[i] = bb_r / kc_r
    return result


def build_features(df: pd.DataFrame, i: int, squeeze_duration: int, squeeze_avg_vol: float, kcr_val: float) -> np.ndarray | None:
    if i < 100 or i >= len(df):
        return None

    o = df["open"].values
    h = df["high"].values
    l = df["low"].values
    c = df["close"].values
    v = df["volume"].values

    feats = []
    for w in [12, 24, 48]:
        if i < w:
            feats.extend([0] * 12)
            continue

        win_c = c[i - w : i]
        win_o = o[i - w : i]
        win_h = h[i - w : i]
        win_l = l[i - w : i]
        win_v = v[i - w : i]

        mn, mx = win_l.min(), max(win_h.max(), win_c.max())
        rng = mx - mn if mx - mn > 0 else 1e-10
        total = win_h - win_l
        total = np.where(total == 0, 1e-10, total)
        body = np.abs(win_c - win_o) / total
        direction = np.sign(win_c - win_o)
        log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
        rets = np.diff(log_c)
        v_mean = np.mean(win_v)

        feats.extend([
            np.mean(rets) if len(rets) > 0 else 0,
            np.std(rets) if len(rets) > 0 else 0,
            np.sum(rets) if len(rets) > 0 else 0,
            float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
            float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
            np.mean(body),
            np.std(body),
            np.mean(direction),
            np.mean(direction[-min(3, w):]),
            (win_c[-1] - mn) / rng,
            win_v[-1] / v_mean if v_mean > 0 else 1,
            np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
        ])

    feats.extend([
        squeeze_duration,
        squeeze_duration / (24 * 4),
        kcr_val,
        v[i - 1] / squeeze_avg_vol if squeeze_avg_vol > 0 else 1,
        np.mean(v[max(0, i - 3) : i]) / squeeze_avg_vol if squeeze_avg_vol > 0 else 1,
    ])

    h48 = np.max(h[max(0, i - 48) : i])
    l48 = np.min(l[max(0, i - 48) : i])
    r48 = h48 - l48
    feats.append((c[i - 1] - l48) / r48 if r48 > 0 else 0.5)

    tr = np.maximum(h[i - 14 : i] - l[i - 14 : i],
                    np.maximum(np.abs(h[i - 14 : i] - np.roll(c[i - 14 : i], 1)),
                               np.abs(l[i - 14 : i] - np.roll(c[i - 14 : i], 1))))
    atr = np.mean(tr[1:])
    feats.append(atr / c[i - 1] if c[i - 1] > 0 else 0)

    first_ret = (c[i - 1] - c[i - 2]) / c[i - 2] if i >= 2 and c[i - 2] > 0 else 0
    feats.append(first_ret)

    return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)


class SignalEngine:
    """Rileva squeeze e genera segnali ML in real-time."""

    def __init__(self, bb_w: int = 14, sq_thr: float = 0.8, ml_thr: float = 0.70, min_squeeze_bars: int = 5):
        self.bb_w = bb_w
        self.sq_thr = sq_thr
        self.ml_thr = ml_thr
        self.min_squeeze_bars = min_squeeze_bars

        self.model: GradientBoostingClassifier | None = None
        self.scaler: StandardScaler | None = None
        self.in_squeeze = False
        self.squeeze_start_idx = 0
        self.trained = False

    def _new_model(self) -> GradientBoostingClassifier:
        return GradientBoostingClassifier(
            n_estimators=150, max_depth=4, min_samples_leaf=10,
            learning_rate=0.05, subsample=0.8, random_state=42,
        )

    def _validate_oos(self, X: np.ndarray, y: np.ndarray, test_frac: float = 0.2) -> dict:
        """Split temporale (no shuffle) per stimare la performance out-of-sample.

        Allena su training iniziale e valuta sull'ultimo `test_frac` dei campioni.
        Oltre all'accuratezza OOS, riporta la precisione sui soli segnali con
        confidenza >= ml_thr — cioè i trade che la strategia aprirebbe davvero.
        """
        n_test = int(len(X) * test_frac)
        n_train = len(X) - n_test
        if n_train < 30 or n_test < 5:
            return {"oos_warning": "test set troppo piccolo", "oos_test_samples": n_test}

        scaler = StandardScaler()
        X_tr = scaler.fit_transform(X[:n_train])
        X_te = scaler.transform(X[n_train:])
        y_tr, y_te = y[:n_train], y[n_train:]

        model = self._new_model()
        model.fit(X_tr, y_tr)

        up_idx = list(model.classes_).index(1)
        p_up = model.predict_proba(X_te)[:, up_idx]
        test_acc = float(np.mean((p_up >= 0.5).astype(int) == y_te) * 100)
        oos_train_acc = float(np.mean(model.predict(X_tr) == y_tr) * 100)

        long_sig = p_up >= self.ml_thr
        short_sig = p_up <= (1 - self.ml_thr)
        n_sig = int((long_sig | short_sig).sum())
        if n_sig > 0:
            correct = int(((long_sig & (y_te == 1)) | (short_sig & (y_te == 0))).sum())
            sig_prec = round(correct / n_sig * 100, 1)
        else:
            sig_prec = None

        return {
            "oos_train_accuracy": round(oos_train_acc, 1),
            "oos_test_accuracy": round(test_acc, 1),
            "oos_test_samples": n_test,
            "oos_signals": n_sig,
            "oos_signal_precision": sig_prec,
        }

    def train(self, df: pd.DataFrame, lookahead: int = 3) -> dict:
        """Addestra il modello su dati storici."""
        close = df["close"].values
        high = df["high"].values
        low = df["low"].values
        volume = df["volume"].values
        n = len(df)

        kcr = keltner_ratio(close, high, low, self.bb_w)

        X_all, y_all = [], []
        in_sq = False
        sq_start = 0

        for i in range(self.bb_w + 1, n - lookahead):
            if np.isnan(kcr[i]):
                continue
            is_sq = kcr[i] < self.sq_thr
            if is_sq and not in_sq:
                in_sq = True
                sq_start = i
            elif not is_sq and in_sq:
                in_sq = False
                duration = i - sq_start
                if duration < self.min_squeeze_bars:
                    continue

                avg_vol = np.mean(volume[sq_start:i])
                feats = build_features(df, i, duration, avg_vol, kcr[i])
                if feats is None:
                    continue

                actual = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
                X_all.append(feats)
                y_all.append(1 if actual > 0 else 0)

        if len(X_all) < 30:
            return {"error": "not enough training samples", "samples": len(X_all)}

        X = np.array(X_all)
        y = np.array(y_all)

        oos = self._validate_oos(X, y)

        self.scaler = StandardScaler()
        X_s = self.scaler.fit_transform(X)

        self.model = self._new_model()
        self.model.fit(X_s, y)
        self.trained = True

        preds = self.model.predict(X_s)
        train_acc = float(np.mean(preds == y) * 100)

        return {
            "samples": len(X),
            "up_ratio": round(float(np.mean(y) * 100), 1),
            "train_accuracy": round(train_acc, 1),
            **oos,
        }

    def check_signal(self, df: pd.DataFrame) -> dict | None:
        """Controlla se c'è un segnale sulle ultime candele.
        Ritorna dict con direzione e probabilità, oppure None.
        """
        if not self.trained:
            return None

        close = df["close"].values
        high = df["high"].values
        low = df["low"].values
        volume = df["volume"].values
        n = len(df)

        kcr = keltner_ratio(close, high, low, self.bb_w)

        if n < self.bb_w + 10:
            return None

        last_kcr = kcr[-1]
        prev_kcr = kcr[-2] if n > 1 else np.nan

        if np.isnan(last_kcr) or np.isnan(prev_kcr):
            return None

        was_squeeze = prev_kcr < self.sq_thr
        is_released = last_kcr >= self.sq_thr

        if not (was_squeeze and is_released):
            self.in_squeeze = prev_kcr < self.sq_thr
            if self.in_squeeze and not hasattr(self, '_sq_start_tracking'):
                self._sq_start_tracking = n - 1
            if not self.in_squeeze:
                self._sq_start_tracking = None
            return None

        sq_start = getattr(self, '_sq_start_tracking', n - 10)
        if sq_start is None:
            sq_start = n - 10
        duration = (n - 1) - sq_start
        if duration < self.min_squeeze_bars:
            self._sq_start_tracking = None
            return None

        avg_vol = np.mean(volume[max(0, sq_start) : n - 1])
        feats = build_features(df, n - 1, duration, avg_vol, last_kcr)
        self._sq_start_tracking = None

        if feats is None:
            return None

        feats_s = self.scaler.transform(feats.reshape(1, -1))
        proba = self.model.predict_proba(feats_s)[0]
        up_idx = list(self.model.classes_).index(1)
        p_up = proba[up_idx]

        if p_up >= self.ml_thr:
            return {"direction": "buy", "probability": p_up, "squeeze_duration": duration}
        elif p_up <= (1 - self.ml_thr):
            return {"direction": "sell", "probability": 1 - p_up, "squeeze_duration": duration}

        return None
