"""Gate 0 — local CFG syntax validator (no WQ API call).

Catches tier-locked operators, structural errors, and known timeout patterns
before a simulation quota is consumed.

Returns (True, "") on pass, (False, reason_str) on failure.
"""

from __future__ import annotations
import re

# Operators completely blocked at TUTORIAL tier
_TIER_BLOCKED_OPS: frozenset[str] = frozenset(
    {
        "exp",
        "s_log_1p",
        "pasteurize",
        "densify",
        "replace",
        "vector_neut",
        "group_vector_neut",
        "hump",
    }
)

# Neutralizations blocked at TUTORIAL tier
_BLOCKED_NEUTRALIZATIONS: frozenset[str] = frozenset(
    {
        "STATISTICAL",
        "SLOW_AND_FAST",
    }
)

# Allowed neutralizations (explicit allowlist — fail closed)
_ALLOWED_NEUTRALIZATIONS: frozenset[str] = frozenset(
    {
        "NONE",
        "MARKET",
        "INDUSTRY",
        "SECTOR",
        "SUBINDUSTRY",
    }
)

# Windows >= this threshold count against the "one long-window" limit
_LONG_WINDOW_THRESHOLD = 100

# Scientific notation pattern (e.g. 1e-9, 2.5e10) — invalid in Fast Expression
_SCI_NOTATION_RE = re.compile(r"\d+(?:\.\d+)?[eE][+-]?\d+")

# Pattern to find all numeric window arguments: ts_rank(x, 252) → "252"
_WINDOW_RE = re.compile(r",\s*(\d+)\s*[,)]")


def _count_parens(expr: str) -> tuple[int, int]:
    return expr.count("("), expr.count(")")


def _max_depth(expr: str) -> int:
    depth = max_d = 0
    for ch in expr:
        if ch == "(":
            depth += 1
            max_d = max(max_d, depth)
        elif ch == ")":
            depth -= 1
    return max_d


def _long_window_count(expr: str) -> int:
    windows = {
        int(m) for m in _WINDOW_RE.findall(expr) if int(m) >= _LONG_WINDOW_THRESHOLD
    }
    return len(windows)


def _detect_blocked_op(expr: str) -> str | None:
    expr_lower = expr.lower()
    for op in _TIER_BLOCKED_OPS:
        if f"{op}(" in expr_lower:
            return op
    return None


def _check_group_mean_arity(expr: str) -> bool:
    """group_mean must have exactly 3 comma-separated args (value, weight, group)."""
    for m in re.finditer(r"\bgroup_mean\s*\(", expr, re.IGNORECASE):
        start = m.end()
        depth = 1
        pos = start
        while pos < len(expr) and depth > 0:
            if expr[pos] == "(":
                depth += 1
            elif expr[pos] == ")":
                depth -= 1
            pos += 1
        inner = expr[start : pos - 1]
        top_commas = 0
        d = 0
        for ch in inner:
            if ch == "(":
                d += 1
            elif ch == ")":
                d -= 1
            elif ch == "," and d == 0:
                top_commas += 1
        if top_commas != 2:
            return False
    return True


def _check_bucket_range_kwarg(expr: str) -> bool:
    """bucket() must use range= not ranges= keyword."""
    if re.search(r"\bbucket\s*\(", expr, re.IGNORECASE):
        if re.search(r"\branges\s*=", expr, re.IGNORECASE):
            return False
    return True


def validate_expression(
    expr: str, neutralization: str = "SUBINDUSTRY"
) -> tuple[bool, str]:
    """Validate a WQ Fast Expression for TUTORIAL tier constraints.

    Returns (True, "") if valid, (False, reason) if invalid.
    Zero API calls — purely local checks.
    """
    if not expr or not expr.strip():
        return False, "empty_expression"

    expr = expr.strip()

    # 1. Scientific notation
    if _SCI_NOTATION_RE.search(expr):
        return False, "sci_notation_invalid (use 0.000000001 not 1e-9)"

    # 2. Balanced parentheses
    opens, closes = _count_parens(expr)
    if opens != closes:
        return False, f"unbalanced_parens (open={opens}, close={closes})"

    # 3. Max nesting depth
    depth = _max_depth(expr)
    if depth > 5:
        return False, f"max_depth_exceeded ({depth} > 5)"

    # 4. Tier-blocked operators
    blocked_op = _detect_blocked_op(expr)
    if blocked_op:
        return False, f"tier_blocked_op: {blocked_op}()"

    # 5. Neutralization tier check
    neut_upper = (neutralization or "SUBINDUSTRY").upper()
    if neut_upper in _BLOCKED_NEUTRALIZATIONS:
        return False, f"tier_blocked_neutralization: {neut_upper}"
    if neut_upper not in _ALLOWED_NEUTRALIZATIONS:
        return False, f"unknown_neutralization: {neut_upper}"

    # 6. Long-window timeout guard (at most 1 window >= 100)
    long_count = _long_window_count(expr)
    if long_count > 1:
        return (
            False,
            f"timeout_risk: {long_count} long windows (>=100) in one expression",
        )

    # 7. group_mean must be 3-arg
    if not _check_group_mean_arity(expr):
        return (
            False,
            "group_mean_arity: must have exactly 3 args (value, weight, group)",
        )

    # 8. bucket() must use range= not ranges=
    if not _check_bucket_range_kwarg(expr):
        return False, "bucket_kwarg: use range= not ranges="

    return True, ""
