from __future__ import annotations

import os
from datetime import datetime, timedelta
from typing import Any, Dict, Optional

import pandas as pd
import requests
from dotenv import load_dotenv

from indicator_lib import (
    ema,
    atr_percent,
    rvol,
    detect_trend_close_vs_ema,
    detect_trend_ema_cross,
    detect_intraday_momentum,
)


class TrendFeatures(dict):
    """
    Container für Trend-Features.
    Ermöglicht Zugriff sowohl über features["key"] als auch features.key.
    Fehlende Attribute liefern None statt Fehler.
    """

    def __getattr__(self, item: str) -> Any:
        return self.get(item, None)

    def __setattr__(self, key: str, value: Any) -> None:
        self[key] = value


load_dotenv()
POLYGON_KEY = os.getenv("POLYGON_API_KEY")
BASE_URL = "https://api.polygon.io/v2/aggs/ticker"


def fetch_polygon_ohlc(symbol: str, multiplier: int, timespan: str, days_back: int) -> Optional[pd.DataFrame]:
    """
    Holt OHLCV-Daten von Polygon über die Aggregates-API.
    timespan: 'day', 'hour', 'minute'
    days_back: wie viele Kalendertage rückwärts wir abdecken wollen.
    """
    if not POLYGON_KEY:
        raise RuntimeError("POLYGON_API_KEY nicht in .env gesetzt")

    end_date = datetime.utcnow().date()
    start_date = end_date - timedelta(days=days_back)

    url = (
        f"{BASE_URL}/{symbol}/range/{multiplier}/{timespan}/"
        f"{start_date.isoformat()}/{end_date.isoformat()}"
        f"?adjusted=true&sort=asc&limit=5000&apiKey={POLYGON_KEY}"
    )

    try:
        resp = requests.get(url, timeout=10)
        data = resp.json()
        if resp.status_code != 200 or data.get("resultsCount", 0) == 0:
            print(f"⚠️ Polygon keine Daten für {symbol} ({timespan}): {data.get('error', resp.status_code)}")
            return None

        rows = data.get("results", [])
        if not rows:
            print(f"⚠️ Polygon leere Ergebnismenge für {symbol} ({timespan})")
            return None

        df = pd.DataFrame(rows)
        df.rename(
            columns={"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume", "t": "timestamp"},
            inplace=True,
        )
        df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
        return df

    except Exception as e:
        print(f"⚠️ Fehler beim Polygon-Call {symbol} ({timespan}): {e}")
        return None


def build_dummy_trend_features(symbol: str) -> TrendFeatures:
    """
    Fallback, falls Polygon nicht erreichbar ist.
    Alle erwarteten Felder werden gesetzt, damit der Fullscan nicht crasht.
    """
    return TrendFeatures(
        {
            "symbol": symbol,
            # einfache Gesamt-Trendfelder
            "trend": "sideways",
            "trend_label": "sideways",
            "trend_bias": "sideways",
            # Bias / Trend pro Timeframe (so wie Fullscan sie evtl. nennt)
            "bias_d1": "sideways",
            "bias_h1": "sideways",
            "bias_m15": "sideways",
            "d1_trend": "NEUTRAL",
            "h1_trend": "NEUTRAL",
            "m15_trend": "FLAT",
            # detailliertere Namen
            "trend_D1": "NEUTRAL",
            "trend_H1": "NEUTRAL",
            "trend_M15": "FLAT",
            "ema200_D1": None,
            "atr_percent": None,
            "rvol": None,
            # Zusatzfelder, die fullscan_20.py erwartet
            "setup_type": "UNKNOWN",
            "intraday_trend_m15": "FLAT",
            "counter_trend": False,
            "vwap_relation": "unknown",
            "atrp": None,
            # NEU: Kurs & Session-Change (Fallback = 0)
            "last_price": 0.0,
            "pct_session": 0.0,
        }
    )


def build_trend_features_from_polygon(
    symbol: str,
    scan_config: Optional[Dict[str, Any]] = None,
    market_bias: Optional[str] = None,
    **kwargs: Any,
) -> TrendFeatures:
    """
    Hauptfunktion, die vom Fullscan benutzt wird.
    Baut echte Trend-Features aus Polygon-Daten:
    - D1: EMA200, ATR%, RVOL, Bias LONG/SHORT/NEUTRAL
    - H1: Trend über EMA20 vs. EMA50 (UP/DOWN/NEUTRAL)
    - M15: Intraday-Momentum (UP/DOWN/FLAT)
    - M1: letzter Kurs + Session-Performance seit Tages-Open
    """
    try:
        # --- D1: Tagesdaten für Bias, ATR%, RVOL ---
        df_d1 = fetch_polygon_ohlc(symbol, multiplier=1, timespan="day", days_back=260)
        if df_d1 is None or df_d1.empty:
            raise ValueError("Keine D1-Daten")

        df_d1["ema200"] = ema(df_d1["close"], 200)
        df_d1["atr_pct"] = atr_percent(df_d1, period=14)

        trend_d1_raw = detect_trend_close_vs_ema(df_d1["close"], df_d1["ema200"])
        atr_p = float(round(df_d1["atr_pct"].iloc[-1], 2))
        rv = rvol(df_d1, lookback=20)
        ema200_last = float(round(df_d1["ema200"].iloc[-1], 2))

        # auf einfaches Label mappen, das der Fullscan für die Statistik nutzt
        if trend_d1_raw == "LONG":
            trend_simple = "long"
        elif trend_d1_raw == "SHORT":
            trend_simple = "short"
        else:
            trend_simple = "sideways"

        # --- H1: Stunden-Trend über EMA20 vs. EMA50 ---
        df_h1 = fetch_polygon_ohlc(symbol, multiplier=1, timespan="hour", days_back=5)
        if df_h1 is not None and not df_h1.empty:
            df_h1["ema20"] = ema(df_h1["close"], 20)
            df_h1["ema50"] = ema(df_h1["close"], 50)
            trend_h1 = detect_trend_ema_cross(df_h1["ema20"], df_h1["ema50"])
        else:
            trend_h1 = "NEUTRAL"

        # --- M15: Intraday-Momentum ---
        df_m15 = fetch_polygon_ohlc(symbol, multiplier=15, timespan="minute", days_back=2)
        if df_m15 is not None and not df_m15.empty:
            trend_m15 = detect_intraday_momentum(df_m15["close"], lookback=5)
        else:
            trend_m15 = "FLAT"

        # --- M1: Live-Kurs und Session-Veränderung ---
        last_price: float = 0.0
        pct_session: float = 0.0

        df_m1 = fetch_polygon_ohlc(symbol, multiplier=1, timespan="minute", days_back=1)
        if df_m1 is not None and not df_m1.empty:
            # wir arbeiten immer nur mit dem letzten Handelstag
            df_m1["date"] = df_m1["timestamp"].dt.date
            last_day = df_m1["date"].iloc[-1]
            df_today = df_m1[df_m1["date"] == last_day]

            if not df_today.empty:
                session_open = float(df_today["open"].iloc[0])
                last_price = float(df_today["close"].iloc[-1])
                if session_open > 0:
                    pct_session = (last_price - session_open) / session_open * 100.0

        # Setup-Typ / Zusatzinfos (erstmal Platzhalter)
        setup_type = "TREND" if trend_d1_raw in ("LONG", "SHORT") else "RANGE"
        counter_trend = False
        vwap_relation = "unknown"

        return TrendFeatures(
            {
                "symbol": symbol,
                # Gesamt-Trend
                "trend": trend_simple,          # 'long', 'short', 'sideways'
                "trend_label": trend_simple,
                "trend_bias": trend_simple,
                # Bias / Trend pro Timeframe (verschiedene Namensvarianten)
                "bias_d1": trend_d1_raw,
                "bias_h1": trend_h1,
                "bias_m15": trend_m15,
                "d1_trend": trend_d1_raw,
                "h1_trend": trend_h1,
                "m15_trend": trend_m15,
                # „offizielle“ Felder
                "trend_D1": trend_d1_raw,
                "trend_H1": trend_h1,
                "trend_M15": trend_m15,
                "ema200_D1": ema200_last,
                "atr_percent": atr_p,
                "atrp": atr_p,
                "rvol": float(round(rv, 2)) if rv is not None else None,
                # Zusatzfelder für Fullscan
                "setup_type": setup_type,
                "intraday_trend_m15": trend_m15,
                "counter_trend": counter_trend,
                "vwap_relation": vwap_relation,
                # NEU: Kurs & Session-Change
                "last_price": float(round(last_price, 2)),
                "pct_session": float(round(pct_session, 2)),
            }
        )

    except Exception as e:
        # WICHTIG: Fallback, damit der Fullscan nicht abstürzt
        print(f"❌ Trend-Fallback für {symbol}: {e}")
        return build_dummy_trend_features(symbol)


def score_from_features(
    features: TrendFeatures,
    scan_config: Optional[Dict[str, Any]] = None,
    market_bias: Optional[str] = None,
    **kwargs: Any,
) -> float:
    """
    Berechnet einen einfachen Score (0–100) aus den Features.
    scan_config und market_bias werden optional benutzt, damit der Fullscan
    flexibel bleiben kann.
    """

    # Basiswerte aus Features
    trend_simple = features.get("trend", "sideways")      # 'long', 'short', 'sideways'
    trend_d1 = features.get("trend_D1", "NEUTRAL")       # 'LONG', 'SHORT', 'NEUTRAL'
    trend_h1 = features.get("trend_H1", "NEUTRAL")       # 'UP', 'DOWN', 'NEUTRAL'
    trend_m15 = features.get("trend_M15", "FLAT")        # 'UP', 'DOWN', 'FLAT'
    atr_p = features.get("atr_percent")
    rv = features.get("rvol")

    # optionale Gewichte aus scan_config
    def cfg(name: str, default: float) -> float:
        if not scan_config:
            return default
        try:
            return float(scan_config.get(name, default))
        except Exception:
            return default

    w_trend = cfg("weight_trend", 1.0)
    w_atr = cfg("weight_atr", 1.0)
    w_rvol = cfg("weight_rvol", 1.0)
    w_intraday = cfg("weight_intraday", 1.0)
    w_bias = cfg("weight_market_bias", 1.0)

    score = 40.0  # Basis

    # 1) D1-Trend
    if trend_d1 == "LONG" or trend_d1 == "SHORT":
        score += 15.0 * w_trend
    elif trend_d1 == "NEUTRAL":
        score += 5.0 * w_trend

    # 2) ATR% – bevorzugt 2–8 %
    if isinstance(atr_p, (int, float)):
        if 2.0 <= atr_p <= 8.0:
            score += 20.0 * w_atr
        elif 1.0 <= atr_p < 2.0 or 8.0 < atr_p <= 12.0:
            score += 10.0 * w_atr

    # 3) RVOL – bevorzugt >= 1.3
    if isinstance(rv, (int, float)):
        if rv >= 2.0:
            score += 20.0 * w_rvol
        elif rv >= 1.3:
            score += 12.0 * w_rvol
        elif rv >= 1.0:
            score += 5.0 * w_rvol

    # 4) Intraday (H1/M15)
    if trend_h1 in ("UP", "DOWN"):
        score += 8.0 * w_intraday
    if trend_m15 in ("UP", "DOWN"):
        score += 6.0 * w_intraday
    if (trend_h1 == "UP" and trend_m15 == "UP") or (trend_h1 == "DOWN" and trend_m15 == "DOWN"):
        score += 6.0 * w_intraday

    # 5) Markt-Bias (optional)
    if market_bias in ("long", "short"):
        if trend_simple == market_bias:
            score += 10.0 * w_bias
        elif (market_bias == "long" and trend_simple == "short") or (
            market_bias == "short" and trend_simple == "long"
        ):
            score -= 10.0 * w_bias

    # Score begrenzen
    if score < 0:
        score = 0.0
    if score > 100:
        score = 100.0

    return float(round(score, 2))
