"""
universe.py

Builds the tradable symbol universe and does a cheap first-pass filter
before any bar data is pulled. Ported from screener/premarket's
premarket_scanner.py (get_universe_symbols / prefilter_by_snapshot),
which itself already implements the same universe filters the
breakout_bot design doc calls for (Part II section 2) -- price band,
liquidity floor, ETF/leveraged/name-complexity exclusion. Reimplemented
here rather than imported so this project has no runtime dependency on
screener/premarket.

Deliberately does NOT apply any RVOL/spread/volatility hard gate here
-- those belong to sip_bot's own scorer.py, not this project's
philosophy (see scanner.py's module docstring: candidate_score is a
pure continuous rank, with no hard reject gates beyond universe
membership and a minimum-bars data-availability check).
"""

from config_loader import get_config
from logger_setup import get_logger
import symbol_filters

log = get_logger("universe")


def _bars_to_dicts(bars) -> list:
    out = []
    for b in bars:
        out.append({
            "t": b.timestamp, "o": float(b.open), "h": float(b.high),
            "l": float(b.low), "c": float(b.close), "v": float(b.volume),
        })
    return out


def get_universe_symbols(client) -> list:
    """
    Pulls active, tradable US equities and applies:
      1. the include/exclude_symbols lists from config
      2. ETF/fund/trust exclusion
      3. leveraged/inverse ("multiplier") product exclusion
      4. company-name syllable-count cap
    """
    cfg = get_config()["universe"]
    if cfg.get("include_only_symbols"):
        return cfg["include_only_symbols"]

    assets = client.get_tradable_assets()
    excluded = set(cfg.get("exclude_symbols", []))

    candidates = [a for a in assets if getattr(a, "tradable", False) and a.symbol not in excluded]
    before = len(candidates)
    survivors = symbol_filters.filter_symbol_list(candidates, log_rejections=True)
    log.info(f"[UNIVERSE] Symbol filters (ETF/leveraged/name-complexity) removed "
             f"{before - len(survivors)} of {before} candidates")
    return survivors


def prefilter_by_snapshot(client, symbols: list, baseline_out: dict = None,
                           prev_day_high_out: dict = None, prev_close_out: dict = None) -> list:
    """
    Cheap first pass using snapshot data to cut the universe down before
    pulling minute bars for scoring. Filters on price band and a real
    prior-day volume floor.

    baseline_out: if a dict is passed, populated with {symbol: real
        prior-day volume} from snap.previous_daily_bar.volume (the
        prior COMPLETE session -- NOT snap.daily_bar, which is today's
        session-so-far and would make an RVOL calculation compare
        today's volume to itself).

    prev_day_high_out / prev_close_out: same pattern, populated with
        {symbol: previous session's high / close} from
        snap.previous_daily_bar. prev_close is a REQUIRED input to
        scanner.py's gap_pct -- a symbol missing it there gets skipped
        entirely, unlike prev_day_high which fails open (falls back to
        the next resistance level).
    """
    cfg = get_config()["universe"]
    survivors = []

    snapshots = client.get_snapshots(symbols)
    if not snapshots:
        return survivors

    for symbol, snap in snapshots.items():
        try:
            latest_trade = snap.latest_trade
            if latest_trade is None:
                continue
            price = float(latest_trade.price)
            if not (cfg["price_min"] <= price <= cfg["price_max"]):
                continue

            prev_bar = getattr(snap, "previous_daily_bar", None)
            if prev_bar is not None and prev_bar.volume:
                prev_volume = float(prev_bar.volume)
                if prev_volume < cfg["min_avg_daily_volume"]:
                    continue
                if baseline_out is not None:
                    baseline_out[symbol] = prev_volume
            if prev_day_high_out is not None and prev_bar is not None and prev_bar.high:
                prev_day_high_out[symbol] = float(prev_bar.high)
            if prev_close_out is not None and prev_bar is not None and prev_bar.close:
                prev_close_out[symbol] = float(prev_bar.close)
            survivors.append(symbol)
        except (AttributeError, TypeError):
            continue

    return survivors
