#!/usr/bin/env python3
# Datei: /kleiner/jobs/run_live_scan_v1.py

import os
import sys
import json
from collections import Counter
from datetime import datetime, timezone

# ----------------------------
# Pfade & Python-Import-Setup
# ----------------------------

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(SCRIPT_DIR)  # ==> /home/suprafl1/kleiner

DATA_DIR = os.path.join(BASE_DIR, "data")
LIVE_SCAN_DIR = os.path.join(DATA_DIR, "live_scan")

MARKET_FILE = os.path.join(DATA_DIR, "market", "market.json")
DAYTRADER_20_FILE = os.path.join(LIVE_SCAN_DIR, "daytrader_20.json")
SIGNALS_FILE = os.path.join(LIVE_SCAN_DIR, "signals.json")

# Parent-Verzeichnis für Import von live_scan_engine.py
if BASE_DIR not in sys.path:
    sys.path.insert(0, BASE_DIR)

# scripts/ ins PYTHONPATH, damit signal_lifecycle.py importierbar ist
SCRIPTS_DIR = os.path.join(BASE_DIR, "scripts")
if SCRIPTS_DIR not in sys.path:
    sys.path.insert(0, SCRIPTS_DIR)

from signal_lifecycle import update_signal_store

from live_scan_engine import (  # type: ignore
    MarketContext,
    TickerSnapshot,
    run_live_scan,
    apply_basic_filters,
    detect_setup,
    compute_score,
    compute_levels,
)

# ----------------------------
# Hilfsfunktionen
# ----------------------------

def to_float(val, default=0.0) -> float:
    try:
        if val is None:
            return default
        return float(val)
    except (TypeError, ValueError):
        return default


def to_int(val, default=0) -> int:
    try:
        if val is None:
            return default
        return int(val)
    except (TypeError, ValueError):
        return default


def load_market_context() -> MarketContext:
    """
    Liest /kleiner/data/market/market.json und baut daraus einen MarketContext.
    Fällt defensiv auf UNKNOWN zurück, falls etwas fehlt.
    Erwartete Struktur in etwa:
    {
      "symbol": "SPY",
      "market_trend": "Aufwärts" | "Abwärts" | "Seitwärts" | "Unbekannt",
      "mtf": { "D1": "...", "H1": "...", "M15": "..." },
      ...
    }
    """
    mode = "UNKNOWN"
    label = "Unbekannt"
    spy_trend = "unknown"
    qqq_trend = "unknown"

    try:
        with open(MARKET_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"[WARN] market.json nicht gefunden unter {MARKET_FILE}, MarketContext=UNKNOWN")
        return MarketContext(mode=mode, label=label, spy_trend=spy_trend, qqq_trend=qqq_trend)
    except json.JSONDecodeError as e:
        print(f"[WARN] market.json JSON-Fehler: {e}, MarketContext=UNKNOWN")
        return MarketContext(mode=mode, label=label, spy_trend=spy_trend, qqq_trend=qqq_trend)

    raw_trend = str(data.get("market_trend", "Unbekannt"))
    label = raw_trend

    lt = raw_trend.lower()
    if "auf" in lt or "up" in lt:
        mode = "TREND_LONG"
    elif "ab" in lt or "down" in lt:
        mode = "TREND_SHORT"
    elif "seit" in lt or "range" in lt:
        mode = "RANGE"
    else:
        mode = "UNKNOWN"

    mtf = data.get("mtf") or {}
    # hier nur informative Felder – für die Engine selbst ist das nicht kritisch
    spy_trend = str(mtf.get("D1", "unknown"))
    # qqq_trend lassen wir erstmal unknown (kann später aus separatem QQQ-File kommen)

    print(f"[INFO] MarketContext: mode={mode}, label={label}, spy_trend={spy_trend}")
    return MarketContext(mode=mode, label=label, spy_trend=spy_trend, qqq_trend=qqq_trend)


def build_mtf_trend(raw: dict) -> dict:
    """
    Baut ein mtf_trend-Objekt für TickerSnapshot.
    Unterstützt mehrere mögliche Feldnamen, z.B.:
      - bereits vorhandenes 'mtf_trend'
      - einzelne Felder 'trend_D1', 'trend_H1', 'trend_M15'
    """
    mtf = raw.get("mtf_trend")
    if isinstance(mtf, dict):
        return mtf

    return {
        "D1": str(raw.get("trend_D1", "unknown")),
        "H1": str(raw.get("trend_H1", "unknown")),
        "M15": str(raw.get("trend_M15", "unknown")),
    }


def snapshot_from_raw(raw: dict) -> TickerSnapshot:
    """
    Wandelt einen Eintrag aus daytrader_20.json in einen TickerSnapshot um.
    Arbeitet robust mit alternativen Feldnamen.
    """
    ticker = str(raw.get("ticker") or raw.get("symbol") or "UNKNOWN").upper()

    price = to_float(raw.get("price") or raw.get("last_price") or raw.get("close"))
    prev_close = to_float(raw.get("prev_close") or raw.get("yesterday_close") or price)
    open_price = to_float(raw.get("open_price") or raw.get("open") or price)

    day_high = to_float(raw.get("day_high") or raw.get("high") or price)
    day_low = to_float(raw.get("day_low") or raw.get("low") or price)

    lookback_high = to_float(raw.get("lookback_high") or raw.get("range_high") or day_high)
    lookback_low = to_float(raw.get("lookback_low") or raw.get("range_low") or day_low)

    avg_vol_20 = to_float(raw.get("avg_vol_20") or raw.get("avg_volume_20") or raw.get("avg_vol") or 0)
    today_vol = to_float(raw.get("today_vol") or raw.get("volume") or 0)

    atr_pct = to_float(raw.get("atr_pct") or raw.get("atr_percent") or raw.get("atrp") or 0)

    ema20 = to_float(raw.get("ema20") or raw.get("ema_20") or price)
    ema50 = to_float(raw.get("ema50") or raw.get("ema_50") or price)
    ema200 = to_float(raw.get("ema200") or raw.get("ema_200") or price)

    vwap = to_float(raw.get("vwap") or raw.get("VWAP") or price)

    mtf_trend = build_mtf_trend(raw)
    data_quality = str(raw.get("data_quality", "ok"))

    body_pct = raw.get("curr_candle_body_pct")
    upper_wick_pct = raw.get("curr_candle_upper_wick_pct")
    lower_wick_pct = raw.get("curr_candle_lower_wick_pct")

    return TickerSnapshot(
        ticker=ticker,
        price=price,
        prev_close=prev_close,
        open_price=open_price,
        day_high=day_high,
        day_low=day_low,
        lookback_high=lookback_high,
        lookback_low=lookback_low,
        avg_vol_20=avg_vol_20,
        today_vol=today_vol,
        atr_pct=atr_pct,
        ema20=ema20,
        ema50=ema50,
        ema200=ema200,
        vwap=vwap,
        mtf_trend=mtf_trend,
        data_quality=data_quality,
        curr_candle_body_pct=to_float(body_pct) if body_pct is not None else None,
        curr_candle_upper_wick_pct=to_float(upper_wick_pct) if upper_wick_pct is not None else None,
        curr_candle_lower_wick_pct=to_float(lower_wick_pct) if lower_wick_pct is not None else None,
    )


def load_daytrader_snapshots() -> list[TickerSnapshot]:
    """
    Lädt /kleiner/data/live_scan/daytrader_20.json und
    gibt eine Liste von TickerSnapshot zurück.

    Unterstützte Formate:

    1) Liste von Objekten (aktuelles Format):
       [
         { ... }, { ... }, ...
       ]

    2) Objekt mit 'tickers'-Array:
       {
         "generated_at": "...",
         "universe": "DAYTRADER_20",
         "tickers": [ { ... }, { ... }, ... ]
       }
    """
    try:
        with open(DAYTRADER_20_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"[ERROR] daytrader_20.json nicht gefunden unter {DAYTRADER_20_FILE}")
        return []
    except json.JSONDecodeError as e:
        print(f"[ERROR] JSON-Fehler in daytrader_20.json: {e}")
        return []

    # Fall A: Datei ist bereits eine Liste von Tickern
    if isinstance(data, list):
        raw_tickers = data

    # Fall B: Datei ist ein Dict mit 'tickers'-Array
    elif isinstance(data, dict):
        raw_tickers = data.get("tickers") or data.get("symbols") or data.get("data") or []
    else:
        print("[ERROR] daytrader_20.json hat ein unbekanntes Format (weder Liste noch Objekt)")
        return []

    if not isinstance(raw_tickers, list):
        print("[ERROR] daytrader_20.json enthält kein gültiges Ticker-Array")
        return []

    snapshots: list[TickerSnapshot] = []
    for raw in raw_tickers:
        try:
            snap = snapshot_from_raw(raw)
            snapshots.append(snap)
        except Exception as e:
            # robust: ein kaputter Eintrag soll den Run nicht abbrechen
            try:
                ident = raw.get("ticker") or raw.get("symbol")
            except Exception:
                ident = "UNKNOWN"
            print(f"[WARN] Konnte Ticker-Eintrag nicht parsen ({ident}): {e}")
            continue

    print(f"[INFO] Geladene Snapshots: {len(snapshots)}")
    return snapshots


def debug_filters(
    snapshots: list[TickerSnapshot],
    market_ctx: MarketContext,
    min_score: float,
    min_crv_tp1: float,
) -> None:
    """
    Zeigt im Terminal an, wo die meisten Titel rausfliegen:
    - Basisfilter
    - kein Setup
    - Score zu tief
    - CRV zu tief
    """
    basic_fail = Counter()
    no_setup = 0
    score_too_low = 0
    crv_too_low = 0
    passed_all = 0

    for snap in snapshots:
        passed, reason, info = apply_basic_filters(snap, market_ctx)
        if not passed:
            basic_fail[reason or "Unbekannter Grund"] += 1
            continue

        setup = detect_setup(snap, market_ctx)
        if setup is None:
            no_setup += 1
            continue

        direction, setup_type, setup_meta = setup
        info.update(setup_meta)

        score, breakdown = compute_score(
            snap, market_ctx, direction, setup_type, info
        )
        if score < min_score:
            score_too_low += 1
            continue

        levels = compute_levels(snap, direction)
        if levels["crv_tp1"] < min_crv_tp1:
            crv_too_low += 1
            continue

        passed_all += 1

    print("---------- Filter-Diagnose ----------")
    print(f"[DEBUG] Gesamt-Snapshots: {len(snapshots)}")
    if basic_fail:
        print("[DEBUG] Basisfilter nicht bestanden:")
        for reason, count in basic_fail.items():
            print(f"        {count:>3} × {reason}")
    else:
        print("[DEBUG] Alle Titel bestehen die Basisfilter.")

    print(f"[DEBUG] Basis ok, aber kein Setup gefunden: {no_setup}")
    print(f"[DEBUG] Setup vorhanden, aber Score < {min_score}: {score_too_low}")
    print(f"[DEBUG] Score ok, aber CRV(TP1) < {min_crv_tp1}: {crv_too_low}")
    print(f"[DEBUG] Potenzielle Signale (alle Kriterien erfüllt): {passed_all}")
    print("---------- Ende Filter-Diagnose ----------")


def write_signals_file(result: dict) -> None:
    os.makedirs(LIVE_SCAN_DIR, exist_ok=True)
    tmp_file = SIGNALS_FILE + ".tmp"

    with open(tmp_file, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    os.replace(tmp_file, SIGNALS_FILE)
    print(f"[INFO] signals.json geschrieben: {SIGNALS_FILE}")


# ----------------------------
# Main
# ----------------------------

def main() -> None:
    print("========== Live-Scan v1 – run_live_scan_v1 ==========")
    print(f"[INFO] Startzeit (UTC): {datetime.now(timezone.utc).isoformat()}")

    market_ctx = load_market_context()
    snapshots = load_daytrader_snapshots()

    if not snapshots:
        print("[WARN] Keine Snapshots geladen – abbrechen.")
        return

    # Konfiguration (später aus filter_config.json)
    min_score = 0.0
    premium_score = 70.0
    min_crv_tp1 = 0.0
    max_long_per_run = 3
    max_short_per_run = 3

    # Diagnose-Ausgabe vor dem eigentlichen Scan
    debug_filters(
        snapshots=snapshots,
        market_ctx=market_ctx,
        min_score=min_score,
        min_crv_tp1=min_crv_tp1,
    )

    # Eigentlicher Signal-Scan
    result = run_live_scan(
        snapshots=snapshots,
        market_ctx=market_ctx,
        min_score=min_score,
        premium_score=premium_score,
        min_crv_tp1=min_crv_tp1,
        max_long_per_run=max_long_per_run,
        max_short_per_run=max_short_per_run,
    )

    # ----------------------------
    # Schritt 4: Active Signals + Intraday History
    # ----------------------------
    signals_raw = None
    for k in ("signals", "selected_signals", "live_signals"):
        v = result.get(k)
        if isinstance(v, list):
            signals_raw = v
            break
    signals_raw = signals_raw or []

    # normalize auf unser Store-Format
    signals_for_store = []
    for s in signals_raw:
        if not isinstance(s, dict):
            continue

        ticker = (s.get("ticker") or s.get("symbol") or "").upper()
        direction = (s.get("direction") or s.get("side") or s.get("dir") or "").upper()

        if direction in ("BUY",):
            direction = "LONG"
        if direction in ("SELL",):
            direction = "SHORT"

        if not ticker or direction not in ("LONG", "SHORT"):
            continue

        signals_for_store.append({
            "ticker": ticker,
            "direction": direction,
            "entry": s.get("entry") or s.get("entry_price") or s.get("price"),
            "stop": s.get("stop") or s.get("sl") or s.get("stop_loss"),
            "tp1": s.get("tp1") or s.get("take_profit_1") or s.get("tp_1"),
            "tp2": s.get("tp2") or s.get("take_profit_2") or s.get("tp_2"),
            "score": s.get("score") or s.get("win_chance"),
            "crv": s.get("crv") or s.get("rr"),
            "comment": s.get("comment") or s.get("setup_type") or "",
        })

    lifecycle_res = update_signal_store(signals_for_store)
    print("[INFO] lifecycle:", lifecycle_res)

    write_signals_file(result)

    stats = result.get("stats", {})
    print(
        "[INFO] Scan fertig – checked={checked}, candidates={candidates_after_filters}, "
        "signals={signals_selected} (long={long_signals}, short={short_signals})".format(
            checked=stats.get("checked"),
            candidates_after_filters=stats.get("candidates_after_filters"),
            signals_selected=stats.get("signals_selected"),
            long_signals=stats.get("long_signals"),
            short_signals=stats.get("short_signals"),
        )
    )


if __name__ == "__main__":
    main()
