"""
phi.consistency — Layer 4: 6 consistency rules → ADMIT / QUARANTINE

Each equation is checked against 6 rules.  All must pass for the
equation to be ADMITted.  The results are embedded in the DNA as
G (pass) / T (fail) bases for allele-specific PCR filtering.

Dependencies: phi.ast_parse (for tree-based checks).
"""

from __future__ import annotations

import ast
import re
from typing import Dict, Optional

from .ast_parse import parse_to_ast

# ── Well-known math function names (considered "defined references") ─────

KNOWN_MATH_NAMES = frozenset({
    "sin", "cos", "tan", "log", "ln", "exp", "sqrt", "abs", "sum",
    "min", "max", "floor", "ceil", "round", "sign", "arg", "norm",
    "det", "trace", "tr", "diag", "vec", "mat", "rank", "dim",
    "arccos", "arcsin", "arctan", "sinh", "cosh", "tanh",
    "Re", "Im", "conj", "adj", "trans", "id",
})

# ── The 6 rules ──────────────────────────────────────────────────────────

CONSISTENCY_RULES = [
    "balanced_parens",       # Rule 1: parentheses must be balanced
    "valid_operator_order",  # Rule 2: no illegal consecutive operators
    "valid_variable_name",   # Rule 3: identifiers start with letter/underscore
    "no_empty_expression",   # Rule 4: equation is non-empty
    "single_expression",     # Rule 5: parseable as single AST expression
    "defined_reference",     # Rule 6: all names are known or short vars
]

RULE_ORDER = list(CONSISTENCY_RULES)


def check_consistency(equation: str) -> Dict[str, bool]:
    """Check all 6 consistency rules.

    Returns dict of rule_name → True/False.
    All True = ADMIT; any False = QUARANTINE.
    """
    result: Dict[str, bool] = {}

    # ── Rule 1: Balanced parentheses ──
    depth = 0
    for c in equation:
        if c == "(":
            depth += 1
        elif c == ")":
            depth -= 1
        if depth < 0:
            break
    result["balanced_parens"] = depth == 0

    # ── Rule 2: No illegal consecutive operators ──
    ops = set("+-*/^=<>!")
    valid_pairs = frozenset({"<=", ">=", "==", "!=", "**"})
    bad = False
    for i in range(len(equation) - 1):
        pair = equation[i:i+2]
        if equation[i] in ops and equation[i + 1] in ops:
            if pair not in valid_pairs:
                bad = True
                break
    result["valid_operator_order"] = not bad

    # ── Rule 3: Identifiers start with letter or underscore ──
    tokens = re.findall(r"[A-Za-z_]\w*|\d+|[^\w\s]", equation)
    bad_vars = any(
        re.match(r"^\d", t) and not re.match(r"^\d+$", t)
        for t in tokens
    )
    result["valid_variable_name"] = not bad_vars

    # ── Rule 4: Non-empty ──
    result["no_empty_expression"] = len(equation.strip()) > 0

    # ── Rule 5: Parseable as single expression ──
    tree = parse_to_ast(equation)
    result["single_expression"] = tree is not None

    # ── Rule 6: All names are known or short ──
    result["defined_reference"] = True
    if tree is not None:
        for node in ast.walk(tree):
            if isinstance(node, ast.Name):
                name = node.id
                allowed = (
                    len(name) <= 2
                    or name.startswith("_")
                    or "_" in name
                    or name in KNOWN_MATH_NAMES
                )
                if not allowed:
                    result["defined_reference"] = False

    return result
