#!/usr/bin/env python3
import json
from pathlib import Path
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional

# Projekt-Root = eine Ebene über /backend
# (im Pflichtenheft nennen wir das /kleiner)
BASE_DIR = Path(__file__).resolve().parent.parent

DATA_DIR = BASE_DIR / "data"
MARKET_FILE = DATA_DIR / "market" / "market.json"
# hier werden später die 20 Daytrader-Aktien als Snapshot liegen
TICKER_FILE = DATA_DIR / "live_scan" / "daytrader_20.json"
SIGNALS_FILE = DATA_DIR / "live_scan" / "signals.json"
STATE_FILE = DATA_DIR / "live_scan" / "state.json"

# --- Konfiguration (wie besprochen) ---

MIN_SCORE = 85.0       # Signal-Schwelle
APLUS_SCORE = 93.0     # A+ Signal
MIN_CRV_TP1 = 1.5      # Mindest-CRV auf TP1
ENTRY_VALID_MIN = 45   # Entry-Gültigkeit in Minuten
COOLDOWN_MIN = 30      # Cooldown pro Ticker
MAX_SIGNALS_PER_RUN_LONG = 3
MAX_SIGNALS_PER_RUN_SHORT = 3

# Basisfilter (Standard v1 = Variante B)
PRICE_MIN = 5
PRICE_MAX = 500
AVG_VOL_MIN = 1_000_000
RVOL_MIN = 1.3
ATR_PCT_MIN = 2.0
ATR_PCT_MAX = 8.0


def load_json(path: Path, default):
    if not path.exists():
        return default
    try:
        with path.open("r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return default


def save_json(path: Path, data: Any):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


# ---------------------------------------------------------
# 1) Marktmodus einlesen
# ---------------------------------------------------------

def get_market_mode() -> Dict[str, Any]:
    market = load_json(MARKET_FILE, {})
    # Erwartet: z. B. market["market_mode"] = "Trendtag Long"
    if not market:
        return {"market_trend": "Unbekannt", "market_mode": "Neutral"}
    if "market_mode" not in market:
        market["market_mode"] = "Neutral"
    return market


# ---------------------------------------------------------
# 2) Ticker-Snapshots einlesen (20 Daytrader-Aktien)
# ---------------------------------------------------------

def load_ticker_snapshots() -> List[Dict[str, Any]]:
    """
    Erwartete Felder pro Ticker (später an dein echtes JSON angepasst):

    {
      "ticker": "NVDA",
      "price": 125.4,
      "avg_vol20": 32000000,
      "rvol": 1.8,
      "atr_pct": 4.2,
      "trend_d1": "LONG",
      "trend_h1": "LONG",
      "trend_m15": "LONG",
      "vs_ema200": "oberhalb",  # oder "unterhalb"
      "gap_pct": 3.4,
      "data_quality": "ok"
    }
    """
    data = load_json(TICKER_FILE, [])
    if not isinstance(data, list):
        return []
    return data


# ---------------------------------------------------------
# 3) Basisfilter
# ---------------------------------------------------------

def passes_basis_filters(t: Dict[str, Any], market_mode: Dict[str, Any]) -> bool:
    # Datenqualität
    if str(t.get("data_quality", "")).lower() == "warnung":
        return False

    price = float(t.get("price", 0))
    if not (PRICE_MIN <= price <= PRICE_MAX):
        return False

    avg_vol20 = float(t.get("avg_vol20", 0))
    if avg_vol20 < AVG_VOL_MIN:
        return False

    rvol = float(t.get("rvol", 0))
    if rvol < RVOL_MIN:
        return False

    atr_pct = float(t.get("atr_pct", 0))
    if not (ATR_PCT_MIN <= atr_pct <= ATR_PCT_MAX):
        return False

    # Trend-Kopplung Aktie ↔ Markt
    vs_ema200 = t.get("vs_ema200", "unbekannt")  # "oberhalb" / "unterhalb"
    mode = market_mode.get("market_mode", "Neutral")

    if mode == "Trendtag Long" and vs_ema200 != "oberhalb":
        return False
    if mode == "Trendtag Short" and vs_ema200 != "unterhalb":
        return False

    # Gaps erst mal nur im Score berücksichtigt
    return True


# ---------------------------------------------------------
# 4) Setup-Erkennung (Stub)
# ---------------------------------------------------------

def detect_setup(t: Dict[str, Any],
                 market_mode: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """
    TODO: hier kommen Breakout-/Pullback-Regeln rein, wenn
    wir dein Intraday-JSON-Format definiert haben.
    Aktuell: kein Setup -> kein Signal.
    """
    return None


# ---------------------------------------------------------
# 5) Score & CRV
# ---------------------------------------------------------

def calculate_score(t: Dict[str, Any],
                    setup: Dict[str, Any],
                    market_mode: Dict[str, Any]) -> float:
    score = 0.0

    mode = market_mode.get("market_mode", "Neutral")
    if mode in ("Trendtag Long", "Trendtag Short"):
        score += 20

    rvol = float(t.get("rvol", 1.0))
    if rvol >= 2.0:
        score += 25
    elif rvol >= 1.5:
        score += 18
    elif rvol >= 1.3:
        score += 12

    atr_pct = float(t.get("atr_pct", 0.0))
    if ATR_PCT_MIN <= atr_pct <= ATR_PCT_MAX:
        score += 20

    gap_pct = abs(float(t.get("gap_pct", 0.0)))
    if gap_pct > 10:
        score -= 10
    elif gap_pct > 5:
        score -= 5

    if setup.get("setup_type") == "Breakout":
        score += 20
    elif setup.get("setup_type") == "Pullback":
        score += 15

    return max(0.0, min(100.0, score))


def calculate_crv_tp1(entry: float, stop: float, tp1: float) -> float:
    risk = abs(entry - stop)
    if risk <= 0:
        return 0.0
    reward = abs(tp1 - entry)
    return reward / risk


# ---------------------------------------------------------
# 6) Cooldown & Limits
# ---------------------------------------------------------

def load_state() -> Dict[str, Any]:
    state = load_json(STATE_FILE, {})
    if "per_ticker" not in state:
        state["per_ticker"] = {}
    if "run_stats" not in state:
        state["run_stats"] = {
            "date": datetime.now().date().isoformat(),
            "total_signals_today": 0
        }
    return state


def update_state_for_signal(state: Dict[str, Any],
                            ticker: str,
                            now: datetime):
    per_ticker = state.setdefault("per_ticker", {})
    info = per_ticker.setdefault(ticker, {})
    info["last_signal_at"] = now.isoformat()
    info["cooldown_until"] = (now + timedelta(minutes=COOLDOWN_MIN)).isoformat()
    info["signals_today"] = int(info.get("signals_today", 0)) + 1

    today = datetime.now().date().isoformat()
    if state["run_stats"].get("date") != today:
        state["run_stats"]["date"] = today
        state["run_stats"]["total_signals_today"] = 0
    state["run_stats"]["total_signals_today"] = int(
        state["run_stats"].get("total_signals_today", 0)
    ) + 1


def is_on_cooldown(state: Dict[str, Any],
                   ticker: str,
                   now: datetime) -> bool:
    t_info = state.get("per_ticker", {}).get(ticker)
    if not t_info:
        return False
    cooldown_until = t_info.get("cooldown_until")
    if not cooldown_until:
        return False
    try:
        dt = datetime.fromisoformat(cooldown_until)
        return now < dt
    except Exception:
        return False


# ---------------------------------------------------------
# 7) Hauptlauf
# ---------------------------------------------------------

def run_live_scan():
    now = datetime.now(timezone.utc)

    market_mode = get_market_mode()
    tickers = load_ticker_snapshots()
    state = load_state()

    long_signals: List[Dict[str, Any]] = []
    short_signals: List[Dict[str, Any]] = []

    for t in tickers:
        ticker = t.get("ticker")
        if not ticker:
            continue

        if is_on_cooldown(state, ticker, now):
            continue

        if not passes_basis_filters(t, market_mode):
            continue

        setup = detect_setup(t, market_mode)
        if setup is None:
            continue

        entry = float(setup["entry"])
        stop = float(setup["stop_loss"])
        tp1 = float(setup["tp1"])
        tp2 = float(setup["tp2"])

        crv_tp1 = calculate_crv_tp1(entry, stop, tp1)
        if crv_tp1 < MIN_CRV_TP1:
            continue

        score = calculate_score(t, setup, market_mode)
        if score < MIN_SCORE:
            continue

        direction = setup["direction"]  # "LONG" oder "SHORT"
        signal = {
            "ticker": ticker,
            "direction": direction,
            "setup_type": setup.get("setup_type"),
            "entry": entry,
            "stop_loss": stop,
            "tp1": tp1,
            "tp2": tp2,
            "score": round(score, 1),
            "crv_tp1": round(crv_tp1, 2),
            "market_mode": market_mode.get("market_mode", "Unbekannt"),
            "created_at": now.isoformat(),
            "valid_until": (now + timedelta(minutes=ENTRY_VALID_MIN)).isoformat(),
            "quality_tag": "A+" if score >= APLUS_SCORE else "Normal"
        }

        if direction == "LONG":
            long_signals.append(signal)
        else:
            short_signals.append(signal)

        update_state_for_signal(state, ticker, now)

    long_signals = sorted(long_signals, key=lambda s: s["score"], reverse=True)[:MAX_SIGNALS_PER_RUN_LONG]
    short_signals = sorted(short_signals, key=lambda s: s["score"], reverse=True)[:MAX_SIGNALS_PER_RUN_SHORT]

    output = {
        "last_run": now.isoformat(),
        "market_mode": market_mode.get("market_mode", "Unbekannt"),
        "signals": long_signals + short_signals
    }

    save_json(SIGNALS_FILE, output)
    save_json(STATE_FILE, state)


if __name__ == "__main__":
    run_live_scan()
