"""
Registro de estrategias PERSONALIZADAS de Leonex.

Aqui aterrizan las estrategias traducidas de PineScript. PineScript solo se
ejecuta dentro de TradingView; Leonex no puede ejecutarlo. El flujo es: pegas
el PineScript en el chat, se traduce su LOGICA (entradas + salidas) a una
funcion Python registrada aqui, y agente_custom_lab.py la evalua con el mismo
rigor que el Strategy Lab: trades discretos, walk-forward, PSR/DSR, tiers.

COMO ANADIR UNA ESTRATEGIA:
    Decora una funcion con @register(...). La funcion recibe un DataFrame
    OHLCV y devuelve una serie booleana: True en la barra donde se ENTRA.
    direction = 'long' o 'short'.
    exit_config admite dos modos:
       ATR:  {"tp": x, "sl": y, "timeout": n}      (multiplos de ATR / barras)
       PCT:  {"mode":"pct","tp_pct":3.0,"sl_pct":1.5,"eod":True}
             (TP/SL en % fijo; eod=True cierra al final del dia natural)
"""

from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent))
from agente_strategy_lab import (  # noqa: E402
    _ema, _sma, _rsi, _atr, _macd, _bollinger, _adx,
    _cross_up, _cross_down,
)

CUSTOM_STRATEGIES: list[dict] = []


def register(name: str, exit_config: dict, timeframes=("1d",),
             direction: str = "long", source: str = "manual",
             n_trials_hint: int = 1, description: str = ""):
    """Decorador para registrar una estrategia custom."""
    def deco(fn):
        CUSTOM_STRATEGIES.append({
            "name": name,
            "entry_fn": fn,
            "exit": dict(exit_config),
            "timeframes": list(timeframes),
            "direction": direction,
            "source": source,
            "n_trials_hint": max(int(n_trials_hint), 1),
            "description": description or (fn.__doc__ or "").strip(),
        })
        return fn
    return deco


# ─────────────────────────────────────────────────────────────────────────────
# Helpers para "TT PrevDay BoxStrategy V1.8" (Box Theory + velas japonesas)
# ─────────────────────────────────────────────────────────────────────────────

def _prev_day_levels(df: pd.DataFrame):
    """Para cada barra intradia devuelve (prev_high, prev_low): maximo y
    minimo del DIA NATURAL anterior. Sin request.security."""
    idx = pd.to_datetime(df.index)
    day = pd.Series(idx.normalize(), index=df.index)
    daily_high = df["high"].groupby(day).max()
    daily_low = df["low"].groupby(day).min()
    prev_high = day.map(daily_high.shift(1))
    prev_low = day.map(daily_low.shift(1))
    return prev_high, prev_low


def _box_zone_state(df: pd.DataFrame, activation_pct: float = 0.05,
                    reset_pct: float = 0.20) -> pd.Series:
    """Replica la maquina de estados del PineScript (wick-based):
        0  neutral
        1  zona superior activa (precio cerca del maximo del dia previo)
       -1  zona inferior activa (precio cerca del minimo del dia previo)
    Activa al entrar en el 5% del borde; desactiva al volver pasado el 20%.
    Resetea a 0 en cada cambio de dia natural."""
    prev_high, prev_low = _prev_day_levels(df)
    rng = prev_high - prev_low
    valid = ((prev_high > prev_low) & rng.notna()).to_numpy()
    upper95 = (prev_high - rng * activation_pct).to_numpy()
    lower05 = (prev_low + rng * activation_pct).to_numpy()
    upper20 = (prev_high - rng * reset_pct).to_numpy()
    lower20 = (prev_low + rng * reset_pct).to_numpy()
    high = df["high"].to_numpy(dtype=float)
    low = df["low"].to_numpy(dtype=float)
    close = df["close"].to_numpy(dtype=float)
    day = pd.factorize(pd.to_datetime(df.index).normalize())[0]

    n = len(df)
    state = np.zeros(n, dtype=int)
    cur = 0
    for i in range(n):
        if i > 0 and day[i] != day[i - 1]:
            cur = 0
        if valid[i]:
            if cur == 0:
                if high[i] >= upper95[i] or close[i] >= upper95[i]:
                    cur = 1
                elif low[i] <= lower05[i] or close[i] <= lower05[i]:
                    cur = -1
            elif cur == 1:
                if low[i] < upper20[i] or close[i] < upper20[i]:
                    cur = 0
            elif cur == -1:
                if high[i] > lower20[i] or close[i] > lower20[i]:
                    cur = 0
        else:
            cur = 0
        state[i] = cur
    return pd.Series(state, index=df.index)


def _is_hammer_pine(df: pd.DataFrame, min_wick_ratio: float = 2.0,
                    max_body_pct: float = 0.35) -> pd.Series:
    """Martillo segun el PineScript: cuerpo pequeno, mecha inferior larga
    (>= min_wick_ratio x cuerpo), mecha superior minima (<= 10% del rango)."""
    o, h, l, c = df["open"], df["high"], df["low"], df["close"]
    rng = h - l
    body = (c - o).abs()
    upper = h - np.maximum(c, o)
    lower = np.minimum(c, o) - l
    cond = ((rng > 0) & (body > 0)
            & (body <= max_body_pct * rng)
            & (lower >= min_wick_ratio * body)
            & (upper <= 0.10 * rng))
    return cond.fillna(False)


def _is_shooting_star_pine(df: pd.DataFrame, min_wick_ratio: float = 2.0,
                           max_body_pct: float = 0.35) -> pd.Series:
    """Estrella fugaz segun el PineScript: cuerpo pequeno, mecha superior
    larga (>= min_wick_ratio x cuerpo), mecha inferior minima."""
    o, h, l, c = df["open"], df["high"], df["low"], df["close"]
    rng = h - l
    body = (c - o).abs()
    upper = h - np.maximum(c, o)
    lower = np.minimum(c, o) - l
    cond = ((rng > 0) & (body > 0)
            & (body <= max_body_pct * rng)
            & (upper >= min_wick_ratio * body)
            & (lower <= 0.10 * rng))
    return cond.fillna(False)


# ─────────────────────────────────────────────────────────────────────────────
# Traduccion fiel de "TT PrevDay BoxStrategy V1.8 — Strategy".
#
# Logica original (secciones 7-9 del PineScript):
#   longSignal  = en zona inferior (bgState=-1)  Y  vela Hammer
#   shortSignal = en zona superior (bgState=1)   Y  vela Shooting Star
#   Salida: SL 1.5% / TP 3% sobre el precio de entrada + cierre fin de dia.
#
# Leonex evalua intradia (la estrategia usa niveles del dia previo y cierra
# al final del dia). El Custom Lab opera long Y short en % fijo con EOD.
# ─────────────────────────────────────────────────────────────────────────────

_PINE_TIMEFRAMES = ["4h", "1h", "30m", "15m", "5m"]
_PINE_EXIT = {"mode": "pct", "tp_pct": 3.0, "sl_pct": 1.5, "eod": True}


@register("box_prevday_hammer_long",
          exit_config=_PINE_EXIT, timeframes=_PINE_TIMEFRAMES,
          direction="long", source="pinescript", n_trials_hint=1,
          description="TT PrevDay BoxStrategy (lado LONG): largo cuando el "
                      "precio esta en la zona inferior del rango del dia "
                      "previo (5% sobre el minimo) y se forma un Hammer. "
                      "Salida SL 1.5% / TP 3% / cierre fin de dia.")
def box_prevday_hammer_long(df: pd.DataFrame) -> pd.Series:
    state = _box_zone_state(df)
    hammer = _is_hammer_pine(df)
    return ((state == -1) & hammer).fillna(False).astype(bool)


@register("box_prevday_star_short",
          exit_config=_PINE_EXIT, timeframes=_PINE_TIMEFRAMES,
          direction="short", source="pinescript", n_trials_hint=1,
          description="TT PrevDay BoxStrategy (lado SHORT): corto cuando el "
                      "precio esta en la zona superior del rango del dia "
                      "previo (5% bajo el maximo) y se forma una Shooting "
                      "Star. Salida SL 1.5% / TP 3% / cierre fin de dia.")
def box_prevday_star_short(df: pd.DataFrame) -> pd.Series:
    state = _box_zone_state(df)
    star = _is_shooting_star_pine(df)
    return ((state == 1) & star).fillna(False).astype(bool)
