from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
import os
from typing import Any, Dict, List, Optional, Tuple
from zoneinfo import ZoneInfo

from app.v1.utils.confidence import confidence_rank, normalize_confidence
from app.v1.services.intraday_watchlist import ist_date_str

IST = ZoneInfo("Asia/Kolkata")

ALERTS_MIN_SCORE = float(os.getenv("ALERTS_MIN_SCORE", "85"))

# Materialized alerts collection populated by background loops.
INTRADAY_ALERTS_COLLECTION = os.getenv("INTRADAY_ALERTS_COLLECTION", "intraday_alerts")


def _env_float(name: str, default: float) -> float:
    raw = os.getenv(name)
    if raw is None or str(raw).strip() == "":
        return float(default)
    try:
        v = float(str(raw).strip())
        if v != v or v in (float("inf"), float("-inf")):
            return float(default)
        return float(v)
    except Exception:
        return float(default)


def _safe_float(v: Any) -> Optional[float]:
    try:
        if v is None or isinstance(v, bool):
            return None
        return float(v)
    except Exception:
        return None


def _norm_symbol(v: Any) -> str:
    return (str(v or "").strip().upper())


def _confidence_score(v: Any) -> int:
    return confidence_rank(v)


def _normalize_source(v: Any) -> str:
    s = str(v or "").strip().upper()
    return s


def _normalize_decision(v: Any) -> str:
    d = str(v or "").strip().upper()
    # Normalize a few aliases.
    if d == "LONG":
        return "BUY"
    if d == "SHORT":
        return "SELL"
    return d


def _default_alert_settings() -> Dict[str, Any]:
    # Defaults: allow all common sources and BUY/SELL signals.
    return {
        "min_score": _env_float("ALERTS_MIN_SCORE", 85.0),
        "sources": ["MANUAL", "GAINER", "LOSER", "EARLY_MOVERS"],
        "decisions": ["BUY", "SELL"],
    }


def get_user_alert_settings(db, *, user_id: str) -> Dict[str, Any]:
    doc = db["user_settings"].find_one({"user_id": str(user_id)}, {"_id": 0}) or {}
    alerts = doc.get("alerts") if isinstance(doc.get("alerts"), dict) else {}

    defaults = _default_alert_settings()
    min_score = _safe_float(alerts.get("min_score"))
    if min_score is None:
        min_score = _safe_float(defaults.get("min_score"))
    if min_score is None:
        min_score = 85.0

    sources = alerts.get("sources")
    if not isinstance(sources, list) or not sources:
        sources = defaults.get("sources")
    sources = [
        _normalize_source(s)
        for s in (sources or [])
        if _normalize_source(s) in {"MANUAL", "GAINER", "LOSER", "EARLY_MOVERS"}
    ]
    if not sources:
        sources = list(defaults.get("sources") or [])

    decisions = alerts.get("decisions")
    if not isinstance(decisions, list) or not decisions:
        decisions = defaults.get("decisions")
    decisions = [_normalize_decision(d) for d in (decisions or []) if _normalize_decision(d) in {"BUY", "SELL"}]
    if not decisions:
        decisions = list(defaults.get("decisions") or [])

    return {"min_score": float(min_score), "sources": sources, "decisions": decisions}


def update_user_alert_settings(db, *, user_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
    current = get_user_alert_settings(db, user_id=str(user_id))
    incoming = settings if isinstance(settings, dict) else {}

    min_score = _safe_float(incoming.get("min_score"))
    if min_score is None:
        min_score = current.get("min_score")
    min_score = float(min_score) if min_score is not None else 85.0
    min_score = max(0.0, min(100.0, float(min_score)))

    sources_in = incoming.get("sources")
    sources = current.get("sources")
    if isinstance(sources_in, list) and sources_in:
        sources = [_normalize_source(s) for s in sources_in]
        sources = [s for s in sources if s in {"MANUAL", "GAINER", "LOSER", "EARLY_MOVERS"}]
    if not sources:
        sources = current.get("sources")

    decisions_in = incoming.get("decisions")
    decisions = current.get("decisions")
    if isinstance(decisions_in, list) and decisions_in:
        decisions = [_normalize_decision(d) for d in decisions_in]
        decisions = [d for d in decisions if d in {"BUY", "SELL"}]
    if not decisions:
        decisions = current.get("decisions")

    merged = {"min_score": float(min_score), "sources": sources, "decisions": decisions}
    db["user_settings"].update_one(
        {"user_id": str(user_id)},
        {"$set": {"user_id": str(user_id), "alerts": merged, "updated_at": datetime.utcnow()}},
        upsert=True,
    )
    return merged


def upsert_intraday_alert_from_snapshot(
    db,
    *,
    snapshot_id: str,
    stock_id: str,
    symbol: str,
    source: str,
    analysis: Dict[str, Any],
    market_data: Optional[Dict[str, Any]] = None,
    snapshot_timestamp: Optional[datetime] = None,
) -> Optional[Dict[str, Any]]:
    """Persist a single materialized alert row (upsert by day+symbol).

    This is intentionally global (not per-user). Per-user filtering happens at read-time.
    """

    if not snapshot_id or not stock_id or not symbol or not isinstance(analysis, dict):
        return None

    decision = _normalize_decision(analysis.get("decision") or analysis.get("action"))
    if decision not in {"BUY", "SELL"}:
        return None

    score = _safe_float(analysis.get("score"))
    # If no score, skip materialization (cannot filter by percent).
    if score is None:
        return None

    symbol = _norm_symbol(symbol)
    src = _normalize_source(source)
    if src not in {"MANUAL", "GAINER", "LOSER", "EARLY_MOVERS"}:
        # Keep unknown sources out of the materialized list by default.
        return None

    now = snapshot_timestamp if isinstance(snapshot_timestamp, datetime) else datetime.utcnow()
    ist_date = ist_date_str(now)

    def _as_float(v: Any) -> Optional[float]:
        try:
            if v is None or isinstance(v, bool):
                return None
            f = float(v)
            if f != f or f in (float("inf"), float("-inf")):
                return None
            return float(f)
        except Exception:
            return None

    def _as_zone(v: Any) -> Optional[Dict[str, float]]:
        if not isinstance(v, dict):
            return None
        low = _as_float(v.get("low") if "low" in v else v.get("lower"))
        high = _as_float(v.get("high") if "high" in v else v.get("upper"))
        if low is None or high is None:
            return None
        if low <= 0 or high <= 0:
            return None
        if high < low:
            low, high = high, low
        return {"low": float(low), "high": float(high)}

    def _as_targets(v: Any) -> Optional[List[float]]:
        if isinstance(v, list):
            out: List[float] = []
            for x in v:
                f = _as_float(x)
                if f is not None and f > 0:
                    out.append(float(f))
            return out[:2] if out else None
        if isinstance(v, dict):
            t1 = _as_float(v.get("target_1") or v.get("t1") or v.get("target1") or v.get("target"))
            t2 = _as_float(v.get("target_2") or v.get("t2") or v.get("target2"))
            out2: List[float] = []
            if t1 is not None and t1 > 0:
                out2.append(float(t1))
            if t2 is not None and t2 > 0:
                out2.append(float(t2))
            return out2[:2] if out2 else None
        return None

    def _indicator_confirmations(a: Dict[str, Any], md: Optional[Dict[str, Any]]) -> Optional[Dict[str, int]]:
        """Compute a compact X/Y confirmations summary for the UI.

        This is intentionally simple and defensive: only counts checks we can evaluate.
        """

        if not isinstance(a, dict):
            return None

        d = str(a.get("decision") or "").strip().upper()
        if d not in {"BUY", "SELL"}:
            return None

        ind = {}
        if isinstance(md, dict):
            ind = (md.get("indicators") or {}).get("15minute") or (md.get("indicators") or {}).get("5minute") or {}
        if not isinstance(ind, dict):
            ind = {}

        ema9 = _as_float(ind.get("ema9"))
        ema21 = _as_float(ind.get("ema21"))
        vwap = _as_float(ind.get("vwap"))
        close = _as_float(ind.get("close"))
        rsi = _as_float(ind.get("rsi"))
        macd_hist = _as_float(ind.get("macd_hist"))

        checks: List[Optional[bool]] = []

        trend = str(a.get("trend") or "").strip().upper()
        if trend in {"BULLISH", "BEARISH"}:
            checks.append(trend == ("BULLISH" if d == "BUY" else "BEARISH"))

        if ema9 is not None and ema21 is not None:
            checks.append(ema9 > ema21 if d == "BUY" else ema9 < ema21)

        if close is not None and vwap is not None:
            checks.append(close > vwap if d == "BUY" else close < vwap)

        if rsi is not None:
            checks.append(rsi >= 55.0 if d == "BUY" else rsi <= 45.0)

        if macd_hist is not None:
            checks.append(macd_hist > 0.0 if d == "BUY" else macd_hist < 0.0)

        fired = a.get("entry_trigger_fired")
        if isinstance(fired, bool):
            checks.append(bool(fired))

        evaluated = [c for c in checks if isinstance(c, bool)]
        if not evaluated:
            return None

        confirmed = sum(1 for c in evaluated if c)
        return {"confirmed": int(confirmed), "total": int(len(evaluated))}

    doc: Dict[str, Any] = {
        "ist_date": ist_date,
        "symbol": symbol,
        "stock_id": str(stock_id),
        "source": src,
        "decision": decision,
        "confidence": normalize_confidence(
            analysis.get("confidence"),
            decision_probability=analysis.get("decision_probability"),
            score=analysis.get("score"),
        ),
        "score": float(score),
        "snapshot_id": str(snapshot_id),
        "snapshot_ts": now,
        "updated_at": datetime.utcnow(),
    }

    # --- UI enrichment (optional) ---
    # Keep this list tight: it powers the Alerts + Dashboard views without needing a heavy join.
    try:
        doc["decision_probability"] = _as_float(analysis.get("decision_probability"))
        doc["confidence_pct"] = doc.get("decision_probability")
        doc["trend"] = str(analysis.get("trend") or "").strip().upper() or None
        doc["setup"] = str(analysis.get("setup") or "").strip().upper() or None
        doc["trend_label"] = str(analysis.get("trend_label") or "").strip() or None

        # For UI deep-links (kite chart)
        tok = _as_float(analysis.get("instrument_token"))
        if tok is None and isinstance(market_data, dict):
            tok = _as_float(market_data.get("instrument_token"))
        if tok is not None:
            try:
                doc["instrument_token"] = int(tok)
            except Exception:
                doc["instrument_token"] = tok

        # Trade levels (prefer deterministic EntryEngine outputs if present)
        doc["entry_zone"] = _as_zone(analysis.get("entry_zone") or analysis.get("entry_engine_entry_zone"))
        doc["exec_sl"] = _as_float(analysis.get("exec_sl") or analysis.get("entry_engine_exec_sl"))
        doc["exec_targets"] = (
            _as_targets(analysis.get("exec_targets") or analysis.get("entry_engine_exec_targets"))
            or _as_targets(analysis.get("targets"))
        )

        # Price / change (best-effort from analysis + quote)
        current_price = _as_float(analysis.get("current_price"))
        quote = market_data.get("quote") if isinstance(market_data, dict) else None
        if not isinstance(quote, dict):
            quote = {}
        last_price = _as_float(quote.get("last_price") or quote.get("ltp"))
        if current_price is None:
            current_price = last_price
        if current_price is not None:
            doc["current_price"] = float(current_price)

        ohlc = quote.get("ohlc") if isinstance(quote.get("ohlc"), dict) else {}
        prev_close = _as_float(ohlc.get("close") or quote.get("previous_close"))
        if prev_close is not None and current_price is not None and prev_close > 0:
            chg = float(current_price) - float(prev_close)
            doc["change"] = chg
            doc["change_pct"] = (chg / float(prev_close)) * 100.0

        doc["signal_state"] = str(analysis.get("signal_state") or analysis.get("entry_engine_signal_state") or "").strip().upper() or None
        confs = _indicator_confirmations(analysis, market_data)
        if confs:
            doc["indicators_confirmed"] = confs
    except Exception:
        # Never break materialization
        pass

    try:
        col = db[INTRADAY_ALERTS_COLLECTION]
        col.create_index([("ist_date", 1), ("symbol", 1)], name="ix_intraday_alerts_day_symbol")
        col.update_one(
            {"ist_date": ist_date, "symbol": symbol},
            {"$set": doc, "$setOnInsert": {"created_at": datetime.utcnow()}},
            upsert=True,
        )
        return doc
    except Exception:
        # Never break the background loop.
        return None


def get_materialized_top_signals_for_user(
    db,
    *,
    current_user: Dict[str, Any],
    limit: int = 10,
    ist_day: Optional[str] = None,
) -> Dict[str, Any]:
    user_id = str((current_user or {}).get("_id"))
    settings = get_user_alert_settings(db, user_id=user_id)

    day = (ist_day or "").strip() or ist_date_str()
    min_score = float(settings.get("min_score") or 0.0)
    allowed_sources = set(settings.get("sources") or [])
    allowed_decisions = set(settings.get("decisions") or [])

    q: Dict[str, Any] = {
        "ist_date": day,
        "score": {"$gte": float(min_score)},
    }
    if allowed_sources:
        q["source"] = {"$in": list(allowed_sources)}
    if allowed_decisions:
        q["decision"] = {"$in": list(allowed_decisions)}

    cur = (
        db[INTRADAY_ALERTS_COLLECTION]
        .find(q, {"_id": 0})
        .sort([("score", -1), ("snapshot_ts", -1)])
        .limit(int(limit))
    )

    rows = list(cur)
    return {
        "status": "ok",
        "day": day,
        "settings": settings,
        "counts": {"top_signals": len(rows)},
        "top_signals": rows,
    }


def _dt_to_ist_hm(ts: Optional[datetime]) -> Optional[str]:
    if not isinstance(ts, datetime):
        return None
    dt = ts
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=ZoneInfo("UTC"))
    return dt.astimezone(IST).strftime("%H:%M IST")


def _extract_from_analysis(symbol: str, stock: Dict[str, Any], snap: Dict[str, Any]) -> Dict[str, Any]:
    analysis = snap.get("analysis") or {}
    if not isinstance(analysis, dict):
        analysis = {}

    decision = (analysis.get("decision") or analysis.get("action") or "HOLD")
    confidence = normalize_confidence(
        (analysis.get("confidence") or analysis.get("confidence_level")),
        decision_probability=analysis.get("decision_probability"),
        score=analysis.get("score"),
    )

    score = _safe_float(analysis.get("score"))

    targets = analysis.get("targets")
    if isinstance(targets, dict):
        entry_price = analysis.get("entry_price") or targets.get("entry")
    else:
        entry_price = analysis.get("entry_price")

    ts = snap.get("timestamp")
    if isinstance(ts, str):
        try:
            ts = datetime.fromisoformat(ts.replace("Z", "+00:00"))
        except Exception:
            ts = None

    md = snap.get("market_data")
    quote = md.get("quote") if isinstance(md, dict) else None
    if not isinstance(quote, dict):
        quote = {}

    current_price = (
        quote.get("last_price")
        or analysis.get("current_price")
        or analysis.get("ltp")
        or (analysis.get("price") if isinstance(analysis.get("price"), (int, float)) else None)
    )

    out: Dict[str, Any] = {
        "analysis_id": analysis.get("analysis_id") or str(snap.get("_id")),
        "stock_id": stock.get("stock_id"),
        "symbol": symbol,
        "name": stock.get("name"),
        "exchange": stock.get("exchange") or "NSE",
        "instrument_token": stock.get("instrument_token"),
        "decision": str(decision).upper() if decision is not None else "HOLD",
        "confidence": confidence,
        "score": score,
        "entry_price": entry_price,
        "stop_loss": analysis.get("stop_loss"),
        "targets": targets,
        "timestamp": ts.isoformat() if isinstance(ts, datetime) else None,
        "time_ist": _dt_to_ist_hm(ts) if isinstance(ts, datetime) else None,
        "current_price": current_price,
    }

    # Convenience fields for UI
    dec = out.get("decision")
    out["signal_label"] = "Entry Signal" if dec in ("BUY", "SELL") else "Monitor"

    return out


def _what_changed(prev: Optional[Dict[str, Any]], curr: Dict[str, Any]) -> str:
    if not prev:
        return "New"

    parts: List[str] = []
    if prev.get("decision") != curr.get("decision"):
        parts.append(f"decision {prev.get('decision')}→{curr.get('decision')}")
    if prev.get("confidence") != curr.get("confidence"):
        parts.append(f"confidence {prev.get('confidence')}→{curr.get('confidence')}")

    for key in ("entry_price", "stop_loss"):
        if prev.get(key) != curr.get(key) and curr.get(key) is not None:
            parts.append(f"{key} updated")

    return "; ".join(parts) if parts else "-"


@dataclass
class Universe:
    stock_ids: List[str]
    source: str


def _portfolio_stock_ids(db, user_id: str, account_id: Optional[str]) -> List[str]:
    query: Dict[str, Any] = {
        "user_id": user_id,
        "status": "ACTIVE",
    }
    if account_id:
        query["$or"] = [
            {"account_id": account_id},
            {"account_id": {"$exists": False}},
            {"account_id": ""},
            {"account_id": None},
        ]

    cur = db["user_portfolio_items"].find(query, {"stock_id": 1})
    out: List[str] = []
    for row in cur:
        sid = row.get("stock_id")
        if sid and sid not in out:
            out.append(sid)
    return out


def _et_movers_stock_ids(db, limit: int) -> List[str]:
    cur = db["live_movers"].find({}, {"stock_id": 1}).sort([("rank", 1), ("last_updated", -1)]).limit(limit)
    out: List[str] = []
    for row in cur:
        sid = row.get("stock_id")
        if sid and sid not in out:
            out.append(sid)
    return out


def resolve_universe_for_top_signals(
    db,
    current_user: Dict[str, Any],
    limit_movers: int = 20,
) -> Universe:
    user_id = str(current_user.get("_id"))
    account_id = (current_user.get("account_id") or "")
    account_id = str(account_id).strip() or None

    is_super_admin = int(current_user.get("role") or 0) == 1

    portfolio_ids = _portfolio_stock_ids(db, user_id, account_id)
    if not is_super_admin:
        return Universe(stock_ids=portfolio_ids, source="portfolio")

    mover_ids = _et_movers_stock_ids(db, limit=limit_movers)
    merged: List[str] = []
    for sid in portfolio_ids + mover_ids:
        if sid and sid not in merged:
            merged.append(sid)

    return Universe(stock_ids=merged, source="portfolio+et_movers")


def get_top_signals_service(
    db,
    current_user: Dict[str, Any],
    limit: int = 10,
) -> Dict[str, Any]:
    uni = resolve_universe_for_top_signals(db, current_user)
    if not uni.stock_ids:
        return {
            "status": "ok",
            "source": uni.source,
            "counts": {"top_signals": 0},
            "top_signals": [],
        }

    stocks = list(
        db["stocks"].find(
            {"stock_id": {"$in": uni.stock_ids}},
            {"_id": 0, "stock_id": 1, "symbol": 1, "exchange": 1, "instrument_token": 1, "name": 1},
        )
    )
    by_id = {s.get("stock_id"): s for s in stocks if s.get("stock_id")}

    live_rows = list(
        db["live_movers"].find(
            {"stock_id": {"$in": uni.stock_ids}},
            {"_id": 0, "stock_id": 1, "rank": 1, "mover_type": 1, "last_updated": 1},
        )
    )
    live_by_id = {r.get("stock_id"): r for r in live_rows if r.get("stock_id")}

    candidates: List[Dict[str, Any]] = []

    for sid in uni.stock_ids:
        stock = by_id.get(sid)
        if not stock:
            continue
        symbol = _norm_symbol(stock.get("symbol"))
        if not symbol:
            continue

        snaps = list(db["stock_analysis_snapshots"].find({"stock_id": sid}).sort([("timestamp", -1)]).limit(2))
        if not snaps:
            continue

        curr = _extract_from_analysis(symbol, stock, snaps[0])
        prev = _extract_from_analysis(symbol, stock, snaps[1]) if len(snaps) > 1 else None
        curr["what_changed"] = _what_changed(prev, curr)

        live = live_by_id.get(sid) or {}
        curr["rank"] = live.get("rank")
        curr["mover_type"] = live.get("mover_type")

        decision = str(curr.get("decision") or "HOLD").upper()
        conf = _confidence_score(curr.get("confidence"))
        score = curr.get("score")

        score_ok = isinstance(score, (int, float)) and float(score) >= float(ALERTS_MIN_SCORE)

        # "Best" signals: BUY/SELL with VERY_STRONG interest (score>=threshold).
        if decision in ("BUY", "SELL") and score_ok:
            candidates.append(curr)

    def sort_key(row: Dict[str, Any]) -> Tuple[int, int, str]:
        conf_val = _confidence_score(row.get("confidence"))
        rank = row.get("rank")
        rank_val = int(rank) if isinstance(rank, int) else 9999
        ts = row.get("timestamp") or ""
        return (-conf_val, rank_val, ts)

    candidates.sort(key=sort_key)

    top = candidates[:limit]
    return {
        "status": "ok",
        "source": uni.source,
        "counts": {"top_signals": len(top)},
        "top_signals": top,
    }
