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

from pymongo.collection import Collection

from app.v1.services.zerodha.client import ZerodhaClient

logger = logging.getLogger(__name__)


STOCKS_COLLECTION = os.getenv("STOCKS_COLLECTION", "stocks")
INSTRUMENTS_CACHE_COLLECTION = os.getenv("ZERODHA_INSTRUMENTS_COLLECTION", "zerodha_instruments")
SYSTEM_META_COLLECTION = os.getenv("SYSTEM_META_COLLECTION", "system_meta")


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


def _instrument_is_stock_like(inst: Dict[str, Any]) -> bool:
    """Best-effort filter: allow equities + indices; exclude obvious derivatives."""
    it = str(inst.get("instrument_type") or "").strip().upper()
    seg = str(inst.get("segment") or "").strip().upper()

    # Derivative / options types vary; exclude common ones.
    if it in {"FUT", "CE", "PE"}:
        return False

    # Equity and indices are our primary universe.
    if it in {"EQ", "INDEX"}:
        return True

    # Some instrument masters omit instrument_type or use odd values.
    # Keep spot symbols in equity segments.
    if seg.startswith("NSE") or seg.startswith("BSE"):
        return True

    return False


def refresh_stocks_master(
    *,
    db,
    zerodha_client: ZerodhaClient,
    exchanges: Optional[List[str]] = None,
    allow_cache_write: bool = True,
) -> Dict[str, Any]:
    """Refresh the `stocks` master list from Zerodha instruments.

    Non-negotiable contract:
    - Stocks are created only from Zerodha instruments.
    - One stock = one row.
    - No manual stock creation elsewhere.

    Notes:
    - This function UPSERTs into `stocks` by (symbol, exchange) and preserves existing `stock_id`.
    - If a row is newly inserted and stock_id is missing, it is set to str(_id) for backward compatibility.
    - Optionally writes a raw instruments cache into `zerodha_instruments` for legacy consumers.
    """

    exchanges = exchanges or ["NSE"]
    now = datetime.utcnow()

    stocks_coll: Collection = db[STOCKS_COLLECTION]

    inserted = 0
    updated = 0
    kept = 0
    total_instruments = 0

    for ex in exchanges:
        exu = (ex or "NSE").strip().upper()
        instruments = zerodha_client.get_instruments(exu) or []
        total_instruments += len(instruments)

        # Optional: keep a raw cache for other legacy code paths.
        if allow_cache_write:
            try:
                db[INSTRUMENTS_CACHE_COLLECTION].update_one(
                    {"type": f"{exu.lower()}_master"},
                    {
                        "$set": {
                            "type": f"{exu.lower()}_master",
                            "exchange": exu,
                            "instruments": instruments,
                            "last_updated": now,
                        },
                        "$setOnInsert": {"created_at": now},
                    },
                    upsert=True,
                )
            except Exception:
                logger.exception("Failed writing instruments cache for %s", exu)

        for inst in instruments:
            if not isinstance(inst, dict):
                continue

            if str(inst.get("exchange") or exu).strip().upper() != exu:
                # if Kite returns mixed exchanges, respect the filter.
                continue

            if not _instrument_is_stock_like(inst):
                continue

            sym = _norm_symbol(inst.get("tradingsymbol") or inst.get("symbol"))
            if not sym:
                continue

            instrument_token = inst.get("instrument_token") or inst.get("token")
            if instrument_token is None:
                continue

            patch: Dict[str, Any] = {
                "symbol": sym,
                "exchange": exu,
                "tradingsymbol": inst.get("tradingsymbol") or sym,
                "name": inst.get("name"),
                "instrument_token": instrument_token,
                "segment": inst.get("segment"),
                "series": inst.get("series"),
                "instrument_type": inst.get("instrument_type"),
                "tick_size": inst.get("tick_size"),
                "lot_size": inst.get("lot_size"),
                "last_refreshed_at": now,
                "source": "ZERODHA_INSTRUMENTS",
            }

            # Preserve user-set attributes.
            update_doc = {
                "$set": {k: v for k, v in patch.items() if v is not None},
                "$setOnInsert": {"created_at": now, "is_active": True},
            }

            res = stocks_coll.update_one({"symbol": sym, "exchange": exu}, update_doc, upsert=True)
            if getattr(res, "upserted_id", None):
                inserted += 1
                try:
                    # Back-compat: stock_id exists everywhere else.
                    stocks_coll.update_one(
                        {"_id": res.upserted_id},
                        {"$set": {"stock_id": str(res.upserted_id)}},
                    )
                except Exception:
                    logger.exception("Failed setting stock_id for %s %s", exu, sym)
            else:
                if res.modified_count:
                    updated += 1
                else:
                    kept += 1

    # Meta for observability
    try:
        db[SYSTEM_META_COLLECTION].update_one(
            {"key": "stocks_master_last_refreshed"},
            {"$set": {"key": "stocks_master_last_refreshed", "value": now, "instruments": total_instruments}},
            upsert=True,
        )
    except Exception:
        pass

    return {
        "ok": True,
        "inserted": inserted,
        "updated": updated,
        "kept": kept,
        "instruments": total_instruments,
        "timestamp": now,
        "exchanges": [str(x).strip().upper() for x in exchanges],
    }


def get_stock_by_symbol(db, symbol: str, exchange: str = "NSE") -> Optional[Dict[str, Any]]:
    sym = _norm_symbol(symbol)
    if not sym:
        return None
    ex = (exchange or "NSE").strip().upper()
    return db[STOCKS_COLLECTION].find_one({"symbol": sym, "exchange": ex})


def require_stock_by_symbol(db, symbol: str, exchange: str = "NSE") -> Dict[str, Any]:
    stock = get_stock_by_symbol(db, symbol, exchange=exchange)
    if not stock:
        raise KeyError(f"Stock not found in master list: {exchange}:{_norm_symbol(symbol)}")
    return stock
