"""Open-universe symbol validation via Yahoo chart meta. Any listed symbol that matches a league rule is accepted.
Results cached in data/symbols_cache.json."""
import json, os, sys, urllib.request, urllib.parse, time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
US_EX = {"NMS","NGM","NCM","NYQ","PCX","ASE","BTS","NAS","NYS"}; TSX_EX = {"TOR","VAN","NEO","CNQ"}
L1_YAHOO = {"BTC-USD","ETH-USD","SOL-USD","XRP-USD","BNB-USD","ADA-USD","AVAX-USD","TRX-USD","DOT-USD","TON11419-USD","SUI20947-USD","NEAR-USD","APT21794-USD"}
CACHE = os.path.join(ROOT, "data", "symbols_cache.json")
def market(meta):
    ex = meta.get("exchangeName"); t = meta.get("instrumentType")
    if t == "CRYPTOCURRENCY": return "CRYPTO"
    if ex in US_EX: return "US"
    if ex in TSX_EX: return "TSX"
    return None
def fetch(sym):
    u = f"https://query1.finance.yahoo.com/v8/finance/chart/{urllib.parse.quote(sym)}?range=1y&interval=1d&events=div"
    r = urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"}), timeout=20)
    return json.load(r)["chart"]["result"][0]
def trailing_yield(res):
    q = [c for c in res["indicators"]["quote"][0]["close"] if c]
    d = sum(v["amount"] for v in res.get("events", {}).get("dividends", {}).values())
    return d / q[-1] if q else 0
def check(sym, rule):
    if sym.startswith("CG:"):   # CoinGecko token (free API): tag from cached categories; no reliable price -> rejected
        sys.path.insert(0, os.path.join(ROOT, "scripts")); import fetch_coingecko as cg
        cid = sym[3:]; f = os.path.join(ROOT, "data", f"{sym}.json")
        if not os.path.exists(f) and not cg.fetch([cid]): return False, {"status": "pending_no_price"}
        t = cg.tags(cid); tag = t["tag"]
        if rule.get("any"): return True, {"tag": tag}
        return ("CG" in rule["types"] and tag in rule.get("tags", [])), {"tag": tag}
    c = json.load(open(CACHE)) if os.path.exists(CACHE) else {}
    if sym not in c or time.time() - c[sym]["t"] > 86400:
        try:
            res = fetch(sym); m = res["meta"]
            c[sym] = {"t": time.time(), "type": m.get("instrumentType"), "market": market(m), "currency": m.get("currency"), "yield": round(trailing_yield(res), 4)}
        except Exception: c[sym] = {"t": time.time(), "type": None}
        json.dump(c, open(CACHE, "w"))
    i = c[sym]
    ok = i["type"] in rule["types"] and i.get("market") in rule.get("markets", []) and i.get("yield", 0) >= rule.get("min_yield", 0)
    if ok and i["type"] == "CRYPTOCURRENCY" and rule.get("tags"):   # Yahoo crypto: L1 majors vs altcoins
        ok = ("l1" if sym in L1_YAHOO else "alt") in rule["tags"]
    return ok, i

# --- Generated universe lists (scripts/build_universe.py). Menus, not limits: data availability decides. ---
import re as _re
UNI_DIR = os.path.join(ROOT, "data", "universe"); _UNI = {}
FORMAT = _re.compile(r"^([A-Z0-9][A-Z0-9.\-^=]{0,14}|CG:[a-z0-9-]{2,80})$")
def normalize(sym):
    s = str(sym or "").strip()
    return s if s.startswith("CG:") else s.upper()
def universe_lists(sym):
    """Names of the generated lists that contain sym (empty if none or lists not built)."""
    if not _UNI and os.path.isdir(UNI_DIR):
        for f in os.listdir(UNI_DIR):
            if f.endswith(".json") and f != "index.json": _UNI[f[:-5]] = set(json.load(open(os.path.join(UNI_DIR, f)))["symbols"])
    return sorted(k for k, v in _UNI.items() if sym in v)
def validate(sym, rule, checker=None):
    """(ok, reason). Any well-formed ticker with free EOD data matching the league rule is accepted, listed or not."""
    s = normalize(sym)
    if not FORMAT.match(s): return False, f"{sym}: invalid ticker format"
    try: ok, info = (checker or check)(s, rule)
    except Exception: ok, info = False, {"type": None}
    if info.get("type") is None and not ok: return False, f"{s}: no free end-of-day price data (unpriceable)"
    if not ok: return False, f"{s}: priced, but does not fit this league's rule"
    return True, "ok" + (f" (listed in {', '.join(universe_lists(s))})" if universe_lists(s) else " (not in a generated list; accepted on data availability)")
