import os
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple

from zoneinfo import ZoneInfo

IST = ZoneInfo("Asia/Kolkata")

WATCHLIST_COLLECTION = os.getenv("INTRADAY_WATCHLIST_COLLECTION", "intraday_watchlist_items")

# Meta keys
_ET_LAST_SYNC_KEY = os.getenv("INTRADAY_ET_LAST_SYNC_META_KEY", "intraday_watchlist_et_last_sync_utc")
_SEEDED_KEY_PREFIX = os.getenv("INTRADAY_SEEDED_META_PREFIX", "intraday_watchlist_seeded")

# Sync cadence for ET -> watchlist list reconciliation
ET_SYNC_MINUTES = int(os.getenv("INTRADAY_ET_SYNC_MINUTES", "30"))

# Sources
SOURCE_EARLY = "EARLY_MOVERS"
SOURCE_ET = "ET_MOVERS"
SOURCE_MANUAL = "MANUAL"


def ist_date_str(now_utc: Optional[datetime] = None) -> str:
    now = now_utc or datetime.utcnow()
    return now.replace(tzinfo=ZoneInfo("UTC")).astimezone(IST).date().isoformat()


def ensure_indexes(db) -> None:
    try:
        db[WATCHLIST_COLLECTION].create_index(
            [("ist_date", 1), ("symbol", 1)],
            unique=True,
            name="ux_intraday_watchlist_ist_date_symbol",
            background=True,
        )
    except Exception:
        pass

    try:
        db[WATCHLIST_COLLECTION].create_index(
            [("ist_date", 1), ("sources", 1)],
            name="ix_intraday_watchlist_ist_date_sources",
            background=True,
        )
    except Exception:
        pass


def _norm_symbols(symbols: Iterable[Any]) -> List[str]:
    out: List[str] = []
    for s in symbols or []:
        if not isinstance(s, str):
            continue
        sym = s.strip().upper()
        if sym:
            out.append(sym)
    # preserve order, drop dups
    return list(dict.fromkeys(out))


def upsert_symbols(
    db,
    *,
    ist_date: str,
    symbols: Iterable[str],
    source: str,
    meta_by_symbol: Optional[Dict[str, Dict[str, Any]]] = None,
) -> int:
    syms = _norm_symbols(symbols)
    if not syms:
        return 0

    now = datetime.utcnow()
    updated = 0
    for sym in syms:
        meta = (meta_by_symbol or {}).get(sym) if isinstance(meta_by_symbol, dict) else None
        update: Dict[str, Any] = {
            "$set": {"ist_date": ist_date, "symbol": sym, "updated_at": now},
            "$addToSet": {"sources": str(source or "").strip().upper()},
            "$setOnInsert": {"created_at": now},
        }
        if isinstance(meta, dict) and meta:
            update["$set"][f"source_meta.{str(source or '').strip().upper()}"] = meta

        try:
            db[WATCHLIST_COLLECTION].update_one({"ist_date": ist_date, "symbol": sym}, update, upsert=True)
            updated += 1
        except Exception:
            continue

    return updated


def _pull_source(db, *, ist_date: str, symbols: Set[str], source: str) -> Tuple[int, int]:
    """Remove a source tag from items not in `symbols`.

    Returns: (touched_count, deleted_count)
    """

    src = str(source or "").strip().upper()
    if not src:
        return 0, 0

    q = {"ist_date": ist_date, "sources": src}
    if symbols:
        q["symbol"] = {"$nin": sorted(list(symbols))}

    try:
        res = db[WATCHLIST_COLLECTION].update_many(
            q,
            {"$pull": {"sources": src}, "$unset": {f"source_meta.{src}": ""}, "$set": {"updated_at": datetime.utcnow()}},
        )
        touched = int(res.modified_count or 0)
    except Exception:
        touched = 0

    # Clean up: delete docs that have no sources left
    try:
        delres = db[WATCHLIST_COLLECTION].delete_many({"ist_date": ist_date, "sources": {"$size": 0}})
        deleted = int(delres.deleted_count or 0)
    except Exception:
        deleted = 0

    return touched, deleted


def list_symbols(db, *, ist_date: str, sources_any: Optional[List[str]] = None) -> List[str]:
    q: Dict[str, Any] = {"ist_date": ist_date}
    if sources_any:
        srcs = [str(s or "").strip().upper() for s in sources_any if str(s or "").strip()]
        if srcs:
            q["sources"] = {"$in": srcs}

    cur = db[WATCHLIST_COLLECTION].find(q, {"_id": 0, "symbol": 1}).sort([("symbol", 1)])
    out: List[str] = []
    for d in cur:
        sym = (d.get("symbol") or "").strip().upper()
        if sym:
            out.append(sym)
    return out


def get_symbol_sources_map(db, *, ist_date: str, symbols: Optional[List[str]] = None) -> Dict[str, List[str]]:
    q: Dict[str, Any] = {"ist_date": ist_date}
    if symbols:
        q["symbol"] = {"$in": _norm_symbols(symbols)}

    cur = db[WATCHLIST_COLLECTION].find(q, {"_id": 0, "symbol": 1, "sources": 1})
    out: Dict[str, List[str]] = {}
    for d in cur:
        sym = (d.get("symbol") or "").strip().upper()
        if not sym:
            continue
        srcs = d.get("sources")
        if isinstance(srcs, list):
            out[sym] = [str(s).strip().upper() for s in srcs if isinstance(s, str) and s.strip()]
        else:
            out[sym] = []
    return out


def maybe_seed_from_early_movers_snapshot(db, *, ist_date: str) -> Dict[str, Any]:
    """Seed today's watchlist from the latest early movers snapshot.

    - One-time per `ist_date` (tracked in system_meta)
    - No removals: early movers list is fixed for the trading day.
    """

    meta_key = f"{_SEEDED_KEY_PREFIX}:{ist_date}"
    already = db["system_meta"].find_one({"key": meta_key})
    if already and str(already.get("value") or "").strip() == "1":
        return {"seeded": False, "reason": "already_seeded"}

    snap = db[os.getenv("EARLY_MOVERS_SNAPSHOT_COLLECTION", "early_movers_snapshots")].find_one(
        {}, sort=[("date", -1)]
    )
    if not snap:
        return {"seeded": False, "reason": "no_snapshot"}

    top = snap.get("top") if isinstance(snap, dict) else None
    top = top if isinstance(top, dict) else {}

    bullish = top.get("bullish") if isinstance(top.get("bullish"), list) else []
    bearish = top.get("bearish") if isinstance(top.get("bearish"), list) else []

    symbols: List[str] = []
    meta: Dict[str, Dict[str, Any]] = {}

    for it in bullish:
        if not isinstance(it, dict):
            continue
        sym = (it.get("symbol") or "").strip().upper()
        if not sym:
            continue
        symbols.append(sym)
        meta[sym] = {
            "bias": "BULLISH",
            "final_score": it.get("final_score"),
            "algo_score": it.get("algo_score"),
            "score": it.get("score"),
            "key_level": it.get("key_level"),
            "distance_to_level_pct": it.get("distance_to_level_pct"),
            "risk_flags": it.get("risk_flags"),
        }

    for it in bearish:
        if not isinstance(it, dict):
            continue
        sym = (it.get("symbol") or "").strip().upper()
        if not sym:
            continue
        symbols.append(sym)
        meta[sym] = {
            "bias": "BEARISH",
            "final_score": it.get("final_score"),
            "algo_score": it.get("algo_score"),
            "score": it.get("score"),
            "key_level": it.get("key_level"),
            "distance_to_level_pct": it.get("distance_to_level_pct"),
            "risk_flags": it.get("risk_flags"),
        }

    inserted = upsert_symbols(db, ist_date=ist_date, symbols=symbols, source=SOURCE_EARLY, meta_by_symbol=meta)

    # mark seeded
    db["system_meta"].update_one(
        {"key": meta_key},
        {"$set": {"key": meta_key, "value": "1", "updated_at": datetime.utcnow(), "snapshot_date": snap.get("date")}},
        upsert=True,
    )

    return {"seeded": True, "count": inserted, "snapshot_date": snap.get("date")}


def maybe_sync_from_et_movers(db, *, ist_date: str, symbols: List[str]) -> Dict[str, Any]:
    """Reconcile today's watchlist ET symbols to exactly `symbols`.

    - Adds missing symbols with source=ET_MOVERS
    - Removes ET source tag from symbols no longer present
    """

    now = datetime.utcnow()
    last = db["system_meta"].find_one({"key": _ET_LAST_SYNC_KEY}) or {}
    last_v = last.get("value")
    last_dt: Optional[datetime] = None
    if isinstance(last_v, str) and last_v.strip():
        try:
            last_dt = datetime.fromisoformat(last_v)
        except Exception:
            last_dt = None
    if isinstance(last_dt, datetime):
        age_min = (now - last_dt).total_seconds() / 60.0
        if age_min < max(1, int(ET_SYNC_MINUTES)):
            return {"synced": False, "reason": "cooldown", "age_minutes": round(age_min, 2)}

    syms = _norm_symbols(symbols)
    keep = set(syms)

    added = upsert_symbols(db, ist_date=ist_date, symbols=syms, source=SOURCE_ET)
    touched, deleted = _pull_source(db, ist_date=ist_date, symbols=keep, source=SOURCE_ET)

    db["system_meta"].update_one(
        {"key": _ET_LAST_SYNC_KEY},
        {"$set": {"key": _ET_LAST_SYNC_KEY, "value": now.isoformat(), "updated_at": now, "count": len(syms)}},
        upsert=True,
    )

    return {"synced": True, "added": added, "removed_source": touched, "deleted": deleted, "count": len(syms)}


def remove_source_for_symbols(db, *, ist_date: str, symbols: Iterable[str], source: str) -> Dict[str, Any]:
    """Remove `source` tag for the given symbols on the given IST date."""

    src = str(source or "").strip().upper()
    syms = _norm_symbols(symbols)
    if not src or not syms:
        return {"ok": True, "removed_source": 0, "deleted": 0}

    try:
        res = db[WATCHLIST_COLLECTION].update_many(
            {"ist_date": ist_date, "symbol": {"$in": syms}},
            {"$pull": {"sources": src}, "$unset": {f"source_meta.{src}": ""}, "$set": {"updated_at": datetime.utcnow()}},
        )
        removed = int(res.modified_count or 0)
    except Exception:
        removed = 0

    try:
        delres = db[WATCHLIST_COLLECTION].delete_many({"ist_date": ist_date, "sources": {"$size": 0}})
        deleted = int(delres.deleted_count or 0)
    except Exception:
        deleted = 0

    return {"ok": True, "removed_source": removed, "deleted": deleted}


def clear_source_for_date(db, *, ist_date: str, source: str) -> Dict[str, Any]:
    """Remove `source` from ALL items for that IST date."""
    src = str(source or "").strip().upper()
    if not src:
        return {"ok": True, "removed_source": 0, "deleted": 0}
    try:
        res = db[WATCHLIST_COLLECTION].update_many(
            {"ist_date": ist_date, "sources": src},
            {"$pull": {"sources": src}, "$unset": {f"source_meta.{src}": ""}, "$set": {"updated_at": datetime.utcnow()}},
        )
        removed = int(res.modified_count or 0)
    except Exception:
        removed = 0
    try:
        delres = db[WATCHLIST_COLLECTION].delete_many({"ist_date": ist_date, "sources": {"$size": 0}})
        deleted = int(delres.deleted_count or 0)
    except Exception:
        deleted = 0
    return {"ok": True, "removed_source": removed, "deleted": deleted}
