"""
symbol_filters.py

Shared filtering rules used by BOTH scanners (premarket_scanner.py's
universe pull, and top_stocks.py's replacement search) so a symbol
that's excluded in the morning can't sneak back in as a same-day
replacement candidate.

Three filter categories, all configurable via config.json -> universe:

1. ETFs / funds / trusts / notes  (exclude_etfs_and_funds)
2. Leveraged / inverse ("multiplier") products, e.g. 2x/3x bull/bear,
   ultra, daily-target products  (exclude_leveraged_inverse)
3. Company names that are too syllable-heavy to be a clean, liquid,
   easily-recognized single-purpose equity ticker  (max_name_syllables)

Alpaca's Asset objects (asset_class=US_EQUITY) include ETFs and ETNs —
US_EQUITY alone does NOT separate common stock from funds. Filtering
must be done on the asset's `name` field since there's no clean
is_etf boolean on the SDK object.
"""

import re
from config_loader import get_config
from logger_setup import get_logger

log = get_logger("symbol_filters")

_FUND_KEYWORDS = [
    "etf", "etn", "trust", " fund", "index fund", "index trust",
    "proshares", "direxion", "ishares", "spdr", "vaneck", "invesco",
    "global x", "wisdomtree", "ark ", "vanguard", "graniteshares",
    "simplify ", "defiance ", "roundhill", "tuttle", "yieldmax",
    "exchange traded", "unit investment trust", "depositary shares",
    "notes due", "exchange-traded",
]

_LEVERAGED_INVERSE_KEYWORDS = [
    "2x", "3x", "-1x", "ultra", "ultrashort", "ultra short",
    "bull ", "bear ", "leveraged", "inverse", "daily target",
    "daily bull", "daily bear", "3x shares", "2x shares",
    "double", "triple",
]

_LEGAL_SUFFIXES = [
    r"\binc\.?\b", r"\bincorporated\b", r"\bcorp\.?\b", r"\bcorporation\b",
    r"\bco\.?\b", r"\bcompany\b", r"\bltd\.?\b", r"\blimited\b",
    r"\bholdings?\b", r"\bgroup\b", r"\bplc\b", r"\bllc\b", r"\bl\.p\.?\b",
    r"\bclass a\b", r"\bclass b\b", r"\bclass c\b", r"\bordinary shares?\b",
    r"\bcommon stock\b", r"\bsa\b", r"\bag\b", r"\bnv\b",
]

_VOWEL_GROUPS = re.compile(r"[aeiouy]+")


def is_etf_or_fund(name: str) -> bool:
    if not name:
        return False
    n = name.lower()
    return any(kw in n for kw in _FUND_KEYWORDS)


def is_leveraged_or_inverse(name: str) -> bool:
    if not name:
        return False
    n = name.lower()
    return any(kw in n for kw in _LEVERAGED_INVERSE_KEYWORDS)


def _strip_legal_suffixes(name: str) -> str:
    n = name.lower()
    for pattern in _LEGAL_SUFFIXES:
        n = re.sub(pattern, " ", n)
    return n


def count_syllables(name: str) -> int:
    """
    Rough English syllable-count heuristic: strips legal suffixes
    (Inc, Corp, Class A, etc. shouldn't count against the company's
    "real" name), then counts vowel-groups per word, with a silent
    trailing-e correction. Good enough to flag genuinely long,
    multi-syllable company names — not intended to be linguistically
    perfect.
    """
    if not name:
        return 0
    cleaned = _strip_legal_suffixes(name)
    words = re.findall(r"[a-zA-Z]+", cleaned)
    total = 0
    for word in words:
        w = word.lower()
        groups = _VOWEL_GROUPS.findall(w)
        count = len(groups)
        if w.endswith("e") and not w.endswith("le") and count > 1:
            count -= 1
        total += max(count, 1)
    return total


def passes_symbol_filters(symbol: str, name: str) -> tuple[bool, str]:
    """
    Returns (passes, reason_if_rejected).
    Applies all three configured filters. Called by both scanners.
    """
    cfg = get_config()["universe"]

    if cfg.get("exclude_etfs_and_funds", True) and is_etf_or_fund(name):
        return False, "ETF/fund/trust"

    if cfg.get("exclude_leveraged_inverse", True) and is_leveraged_or_inverse(name):
        return False, "leveraged/inverse (multiplier) product"

    max_syllables = cfg.get("max_name_syllables")
    if max_syllables:
        syl = count_syllables(name)
        if syl > max_syllables:
            return False, f"name too complex ({syl} syllables > {max_syllables})"

    return True, ""


def filter_symbol_list(assets, log_rejections: bool = True) -> list:
    """
    assets: iterable of Alpaca Asset objects (must have .symbol and .name)
    Returns the list of symbols that pass all filters.
    """
    survivors = []
    for a in assets:
        symbol = getattr(a, "symbol", None)
        name = getattr(a, "name", "") or ""
        if not symbol:
            continue
        ok, reason = passes_symbol_filters(symbol, name)
        if ok:
            survivors.append(symbol)
        elif log_rejections:
            log.debug(f"[FILTER] {symbol} rejected: {reason}")
    return survivors
