"""
Ferguson's tableau rules from Definition 9.

This module implements the exact tableau rules as specified in
Ferguson (2021) "Tableaux and Restricted Quantification for
Systems Related to Weak Kleene Logic."
"""

from dataclasses import dataclass
from typing import Callable, Optional

from .formula import (
    CompoundFormula,
    Constant,
    RestrictedExistentialFormula,
    RestrictedUniversalFormula,
)
from .signs import SignedFormula, e, f, m, n, t


@dataclass
class FergusonRule:
    """A tableau rule as specified in Ferguson's Definition 9."""

    name: str
    premise: SignedFormula
    conclusions: list[list[SignedFormula]]  # List of branches, each with formulas
    instantiation_constant: Optional[str] = (
        None  # For tracking universal instantiations
    )

    def is_branching(self) -> bool:
        """Check if this rule creates branches."""
        return len(self.conclusions) > 1


def get_negation_rule(signed_formula: SignedFormula) -> Optional[FergusonRule]:
    """Get negation rule per Ferguson Definition 9.

    v : ~φ
    ------
    ~v : φ

    Where ~t = f, ~f = t, ~e = e
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not (isinstance(formula, CompoundFormula) and formula.connective == "~"):
        return None

    subformula = formula.subformulas[0]

    if sign == t:
        # t : ~φ → f : φ
        return FergusonRule(
            name="t-negation",
            premise=signed_formula,
            conclusions=[[SignedFormula(f, subformula)]],
        )
    elif sign == f:
        # f : ~φ → t : φ
        return FergusonRule(
            name="f-negation",
            premise=signed_formula,
            conclusions=[[SignedFormula(t, subformula)]],
        )
    elif sign == e:
        # e : ~φ → e : φ
        return FergusonRule(
            name="e-negation",
            premise=signed_formula,
            conclusions=[[SignedFormula(e, subformula)]],
        )
    elif sign == m:
        # m : ~φ means both t : ~φ and f : ~φ are possible
        # t : ~φ → f : φ and f : ~φ → t : φ
        # So m : ~φ → (f : φ) + (t : φ)
        return FergusonRule(
            name="m-negation",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, subformula)],  # From t : ~φ
                [SignedFormula(t, subformula)],  # From f : ~φ
            ],
        )
    elif sign == n:
        # n : ~φ means both f : ~φ and e : ~φ are possible
        # f : ~φ → t : φ and e : ~φ → e : φ
        # So n : ~φ → (t : φ) + (e : φ)
        return FergusonRule(
            name="n-negation",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, subformula)],  # From f : ~φ
                [SignedFormula(e, subformula)],  # From e : ~φ
            ],
        )

    return None


def get_conjunction_rule(signed_formula: SignedFormula) -> Optional[FergusonRule]:
    """Get conjunction rule per Ferguson Definition 9.

    v : φ ∧ ψ
    ---------
    + {v₀ : φ ○ v₁ : ψ} for all v₀, v₁ where v₀ ∧ v₁ = v
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not (isinstance(formula, CompoundFormula) and formula.connective == "&"):
        return None

    left = formula.subformulas[0]
    right = formula.subformulas[1]

    if sign == t:
        # t : φ ∧ ψ → t : φ ○ t : ψ (only t ∧ t = t)
        return FergusonRule(
            name="t-conjunction",
            premise=signed_formula,
            conclusions=[[SignedFormula(t, left), SignedFormula(t, right)]],
        )
    elif sign == f:
        # f : φ ∧ ψ → branches for all ways to get f
        # Per Ferguson Definition 9: f:(φ ∧ ψ) → f:φ | f:ψ | (e:φ, e:ψ)
        return FergusonRule(
            name="f-conjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, left)],  # f ∧ _ = f
                [SignedFormula(f, right)],  # _ ∧ f = f
                [
                    SignedFormula(e, left),
                    SignedFormula(e, right),
                ],  # e ∧ e = e (both must be e)
            ],
        )
    elif sign == e:
        # e : φ ∧ ψ → e appears if either operand is e
        # (e : φ) + (e : ψ)
        return FergusonRule(
            name="e-conjunction",
            premise=signed_formula,
            conclusions=[[SignedFormula(e, left)], [SignedFormula(e, right)]],
        )
    elif sign == m:
        # m : φ ∧ ψ means both t and f are possible
        # For t: need t : φ ○ t : ψ
        # For f: need branches as in f case above
        # This is complex - Ferguson's paper shows this as branching
        return FergusonRule(
            name="m-conjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, left), SignedFormula(t, right)],  # For t result
                [SignedFormula(f, left)],  # For f result
                [SignedFormula(f, right)],  # For f result
            ],
        )
    elif sign == n:
        # n : φ ∧ ψ means both f and e are possible
        # Per Ferguson Definition 9: n:(φ ∧ ψ) → f:φ | f:ψ | (e:φ, e:ψ)
        # This combines the f and e cases
        return FergusonRule(
            name="n-conjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, left)],  # f case
                [SignedFormula(f, right)],  # f case
                [
                    SignedFormula(e, left),
                    SignedFormula(e, right),
                ],  # e case (both must be e)
            ],
        )

    return None


def get_disjunction_rule(signed_formula: SignedFormula) -> Optional[FergusonRule]:
    """Get disjunction rule per Ferguson Definition 9.

    v : φ ∨ ψ
    ---------
    + {v₀ : φ ○ v₁ : ψ} for all v₀, v₁ where v₀ ∨ v₁ = v
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not (isinstance(formula, CompoundFormula) and formula.connective == "|"):
        return None

    left = formula.subformulas[0]
    right = formula.subformulas[1]

    if sign == t:
        # t : φ ∨ ψ → branches for all ways to get t
        # Per Ferguson Definition 9: t:φ | t:ψ | (e:φ, e:ψ)
        # The (e,e) branch is included for completeness even though it contradicts t:(φ∨ψ)
        # This ensures we explore all valuations systematically
        return FergusonRule(
            name="t-disjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, left)],  # t ∨ _ = t
                [SignedFormula(t, right)],  # _ ∨ t = t
                [
                    SignedFormula(e, left),
                    SignedFormula(e, right),
                ],  # e ∨ e = e (will close)
            ],
        )
    elif sign == f:
        # f : φ ∨ ψ → f : φ ○ f : ψ (only f ∨ f = f)
        return FergusonRule(
            name="f-disjunction",
            premise=signed_formula,
            conclusions=[[SignedFormula(f, left), SignedFormula(f, right)]],
        )
    elif sign == e:
        # e : φ ∨ ψ → e appears if either operand is e
        # (e : φ) + (e : ψ)
        return FergusonRule(
            name="e-disjunction",
            premise=signed_formula,
            conclusions=[[SignedFormula(e, left)], [SignedFormula(e, right)]],
        )
    elif sign == m:
        # m : φ ∨ ψ means both t and f are possible
        return FergusonRule(
            name="m-disjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, left)],  # For t result
                [SignedFormula(t, right)],  # For t result
                [SignedFormula(f, left), SignedFormula(f, right)],  # For f result
            ],
        )
    elif sign == n:
        # n : φ ∨ ψ means both f and e are possible
        return FergusonRule(
            name="n-disjunction",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, left), SignedFormula(f, right)],  # For f result
                [SignedFormula(e, left)],  # For e result
                [SignedFormula(e, right)],  # For e result
            ],
        )

    return None


def get_implication_rule(signed_formula: SignedFormula) -> Optional[FergusonRule]:
    """Get implication rule per Ferguson Definition 9.

    Note: φ → ψ is treated as ~φ ∨ ψ in weak Kleene logic.
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not (isinstance(formula, CompoundFormula) and formula.connective == "->"):
        return None

    antecedent = formula.subformulas[0]
    consequent = formula.subformulas[1]

    # φ → ψ ≡ ~φ ∨ ψ
    # So we need to consider truth tables for this combination

    if sign == t:
        # t : φ → ψ means ~φ ∨ ψ = t
        # Per Ferguson Definition 9: f:φ | t:ψ | (e:φ, e:ψ)
        # The (e,e) branch is included for completeness even though e→e = e
        # This branch will close due to contradiction with t:(φ→ψ)
        return FergusonRule(
            name="t-implication",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, antecedent)],  # φ = f makes ~φ = t
                [SignedFormula(t, consequent)],  # ψ = t
                [
                    SignedFormula(e, antecedent),
                    SignedFormula(e, consequent),
                ],  # e→e = e (will close)
            ],
        )
    elif sign == f:
        # f : φ → ψ means ~φ ∨ ψ = f
        # This happens only when: ~φ = f (i.e., φ = t) and ψ = f
        return FergusonRule(
            name="f-implication",
            premise=signed_formula,
            conclusions=[[SignedFormula(t, antecedent), SignedFormula(f, consequent)]],
        )
    elif sign == e:
        # e : φ → ψ means ~φ ∨ ψ = e
        # In weak Kleene, this happens when either operand of ∨ is e
        # So either ~φ = e (i.e., φ = e) or ψ = e
        return FergusonRule(
            name="e-implication",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(e, antecedent)],  # φ = e makes ~φ = e
                [SignedFormula(e, consequent)],  # ψ = e
            ],
        )
    elif sign == m:
        # m : φ → ψ means both t and f are possible for ~φ ∨ ψ
        return FergusonRule(
            name="m-implication",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, antecedent)],  # For t result
                [SignedFormula(t, consequent)],  # For t result
                [
                    SignedFormula(t, antecedent),
                    SignedFormula(f, consequent),
                ],  # For f result
            ],
        )
    elif sign == n:
        # n : φ → ψ means both f and e are possible for ~φ ∨ ψ
        return FergusonRule(
            name="n-implication",
            premise=signed_formula,
            conclusions=[
                [
                    SignedFormula(t, antecedent),
                    SignedFormula(f, consequent),
                ],  # For f result
                [SignedFormula(e, antecedent)],  # For e result
                [SignedFormula(e, consequent)],  # For e result
            ],
        )

    return None


def get_restricted_existential_rule(
    signed_formula: SignedFormula,
    fresh_constant: Constant,
    existing_constant: Optional[Constant] = None,
) -> Optional[FergusonRule]:
    """Get restricted existential quantifier rule per Ferguson Definition 9.

    From the paper:
    t : [∃φ(x)]ψ(x) → t : φ(c) ○ t : ψ(c)
    f : [∃φ(x)]ψ(x) → m : φ(c) ○ m : ψ(c) ○ (n : φ(a) + n : ψ(a))
    e : [∃φ(x)]ψ(x) → e : φ(a) + e : ψ(a)
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not isinstance(formula, RestrictedExistentialFormula):
        return None

    # Get the restriction and matrix with variable substituted
    restriction_fresh = formula.restriction.substitute_term(
        {formula.var.name: fresh_constant}
    )
    matrix_fresh = formula.matrix.substitute_term({formula.var.name: fresh_constant})

    if sign == t:
        # t : [∃φ(x)]ψ(x) → t : φ(c) ○ t : ψ(c)
        return FergusonRule(
            name="t-restricted-exists",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, restriction_fresh), SignedFormula(t, matrix_fresh)]
            ],
        )
    elif sign == f:
        # f : [∃φ(x)]ψ(x) → complex branching with m and n
        # Need another constant for the n branches
        if existing_constant:
            restriction_existing = formula.restriction.substitute_term(
                {formula.var.name: existing_constant}
            )
            matrix_existing = formula.matrix.substitute_term(
                {formula.var.name: existing_constant}
            )

            # f : [∃xφ(x)]ψ(x) → m : φ(c) ○ m : ψ(c) ○ (n : φ(a) + n : ψ(a))
            # ○ means "and" (same branch), + means "or" (different branches)
            return FergusonRule(
                name="f-restricted-exists",
                premise=signed_formula,
                conclusions=[
                    # Branch 1: m : φ(c) ○ m : ψ(c) ○ n : φ(a)
                    [
                        SignedFormula(m, restriction_fresh),
                        SignedFormula(m, matrix_fresh),
                        SignedFormula(n, restriction_existing),
                    ],
                    # Branch 2: m : φ(c) ○ m : ψ(c) ○ n : ψ(a)
                    [
                        SignedFormula(m, restriction_fresh),
                        SignedFormula(m, matrix_fresh),
                        SignedFormula(n, matrix_existing),
                    ],
                ],
            )
        else:
            # No existing constant available - need to generate a second fresh constant
            # for the n branches to fully implement Ferguson's rule
            # f : [∃xφ(x)]ψ(x) → m : φ(c) ○ m : ψ(c) ○ (n : φ(c') + n : ψ(c'))
            # Generate a second fresh constant (c' in Ferguson's notation)
            # This is an "arbitrary" constant for the n branches
            second_fresh = Constant(f"{fresh_constant.name}_arb")
            restriction_second = formula.restriction.substitute_term(
                {formula.var.name: second_fresh}
            )
            matrix_second = formula.matrix.substitute_term(
                {formula.var.name: second_fresh}
            )

            return FergusonRule(
                name="f-restricted-exists",
                premise=signed_formula,
                conclusions=[
                    # Branch 1: m : φ(c) ○ m : ψ(c) ○ n : φ(c')
                    [
                        SignedFormula(m, restriction_fresh),
                        SignedFormula(m, matrix_fresh),
                        SignedFormula(n, restriction_second),
                    ],
                    # Branch 2: m : φ(c) ○ m : ψ(c) ○ n : ψ(c')
                    [
                        SignedFormula(m, restriction_fresh),
                        SignedFormula(m, matrix_fresh),
                        SignedFormula(n, matrix_second),
                    ],
                ],
            )
    elif sign == e:
        # e : [∃φ(x)]ψ(x) → e : φ(a) + e : ψ(a)
        return FergusonRule(
            name="e-restricted-exists",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(e, restriction_fresh)],
                [SignedFormula(e, matrix_fresh)],
            ],
        )
    elif sign == m:
        # m branches to both t and f cases
        # Need second constant for the f case
        second_fresh = Constant(f"{fresh_constant.name}_arb")
        restriction_second = formula.restriction.substitute_term(
            {formula.var.name: second_fresh}
        )
        matrix_second = formula.matrix.substitute_term({formula.var.name: second_fresh})

        return FergusonRule(
            name="m-restricted-exists",
            premise=signed_formula,
            conclusions=[
                # t case: t : φ(c) ○ t : ψ(c)
                [SignedFormula(t, restriction_fresh), SignedFormula(t, matrix_fresh)],
                # f case: m : φ(c) ○ m : ψ(c) ○ n : φ(c')
                [
                    SignedFormula(m, restriction_fresh),
                    SignedFormula(m, matrix_fresh),
                    SignedFormula(n, restriction_second),
                ],
                # f case: m : φ(c) ○ m : ψ(c) ○ n : ψ(c')
                [
                    SignedFormula(m, restriction_fresh),
                    SignedFormula(m, matrix_fresh),
                    SignedFormula(n, matrix_second),
                ],
            ],
        )
    elif sign == n:
        # n branches to both f and e cases
        # Need second constant for the f case
        second_fresh = Constant(f"{fresh_constant.name}_arb")
        restriction_second = formula.restriction.substitute_term(
            {formula.var.name: second_fresh}
        )
        matrix_second = formula.matrix.substitute_term({formula.var.name: second_fresh})

        return FergusonRule(
            name="n-restricted-exists",
            premise=signed_formula,
            conclusions=[
                # f case: m : φ(c) ○ m : ψ(c) ○ n : φ(c')
                [
                    SignedFormula(m, restriction_fresh),
                    SignedFormula(m, matrix_fresh),
                    SignedFormula(n, restriction_second),
                ],
                # f case: m : φ(c) ○ m : ψ(c) ○ n : ψ(c')
                [
                    SignedFormula(m, restriction_fresh),
                    SignedFormula(m, matrix_fresh),
                    SignedFormula(n, matrix_second),
                ],
                # e case: e : φ(c) + e : ψ(c)
                [SignedFormula(e, restriction_fresh)],
                [SignedFormula(e, matrix_fresh)],
            ],
        )

    return None


def get_restricted_universal_rule(
    signed_formula: SignedFormula,
    instantiation_constant: Constant,
    additional_fresh_constant: Optional[Constant] = None,
) -> Optional[FergusonRule]:
    """Get restricted universal quantifier rule per Ferguson Definition 9.

    From the paper:
    t : [∀φ(x)]ψ(x) → m : φ(c) ○ m : ψ(c) ○ (n : φ(c') + t : ψ(c'))
    f : [∀φ(x)]ψ(x) → t : φ(c) ○ f : ψ(c)
    e : [∀φ(x)]ψ(x) → e : φ(a) + e : ψ(a)

    Note: The paper's notation is complex here. We simplify to the core behavior.
    """
    sign = signed_formula.sign
    formula = signed_formula.formula

    if not isinstance(formula, RestrictedUniversalFormula):
        return None

    # Substitute the variable with the constant
    restriction_inst = formula.restriction.substitute_term(
        {formula.var.name: instantiation_constant}
    )
    matrix_inst = formula.matrix.substitute_term(
        {formula.var.name: instantiation_constant}
    )

    if sign == t:
        # t : [∀φ(x)]ψ(x) means: for all x, if φ(x) then ψ(x)
        # This is equivalent to: for all x, ¬φ(x) ∨ ψ(x)
        # So we branch: either ¬φ(c) or ψ(c)
        return FergusonRule(
            name="t-restricted-forall",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(f, restriction_inst)],  # ¬φ(c) case
                [SignedFormula(t, matrix_inst)],  # ψ(c) case
            ],
            instantiation_constant=instantiation_constant.name,
        )
    elif sign == f:
        # f : [∀φ(x)]ψ(x) → t : φ(c) ○ f : ψ(c)
        # There exists a counterexample
        return FergusonRule(
            name="f-restricted-forall",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(t, restriction_inst), SignedFormula(f, matrix_inst)]
            ],
            instantiation_constant=instantiation_constant.name,
        )
    elif sign == e:
        # e : [∀φ(x)]ψ(x) → e : φ(a) + e : ψ(a)
        return FergusonRule(
            name="e-restricted-forall",
            premise=signed_formula,
            conclusions=[
                [SignedFormula(e, restriction_inst)],
                [SignedFormula(e, matrix_inst)],
            ],
            instantiation_constant=instantiation_constant.name,
        )
    elif sign == m:
        # m branches to both t and f cases
        return FergusonRule(
            name="m-restricted-forall",
            premise=signed_formula,
            conclusions=[
                # t case
                [SignedFormula(f, restriction_inst)],
                [SignedFormula(t, matrix_inst)],
                # f case
                [SignedFormula(t, restriction_inst), SignedFormula(f, matrix_inst)],
            ],
            instantiation_constant=instantiation_constant.name,
        )
    elif sign == n:
        # n branches to both f and e cases
        conclusions = [
            # f case with given constant
            [SignedFormula(t, restriction_inst), SignedFormula(f, matrix_inst)],
            # e cases with given constant
            [SignedFormula(e, restriction_inst)],
            [SignedFormula(e, matrix_inst)],
        ]

        # If we have an additional fresh constant, add branches for it too
        if additional_fresh_constant:
            fresh_restriction = formula.restriction.substitute_term(
                {formula.var.name: additional_fresh_constant}
            )
            fresh_matrix = formula.matrix.substitute_term(
                {formula.var.name: additional_fresh_constant}
            )
            conclusions.extend(
                [
                    # f case with fresh constant
                    [
                        SignedFormula(t, fresh_restriction),
                        SignedFormula(f, fresh_matrix),
                    ],
                    # e cases with fresh constant
                    [SignedFormula(e, fresh_restriction)],
                    [SignedFormula(e, fresh_matrix)],
                ]
            )

        return FergusonRule(
            name="n-restricted-forall",
            premise=signed_formula,
            conclusions=conclusions,
            instantiation_constant=instantiation_constant.name,
        )

    return None


def _get_compound_rule(signed_formula: SignedFormula) -> Optional[FergusonRule]:
    """Get rule for compound formulas."""
    formula = signed_formula.formula
    if not isinstance(formula, CompoundFormula):
        return None

    connective_rules = {
        "~": get_negation_rule,
        "&": get_conjunction_rule,
        "|": get_disjunction_rule,
        "->": get_implication_rule,
    }

    rule_func = connective_rules.get(formula.connective)
    return rule_func(signed_formula) if rule_func else None


def _get_universal_constant(
    existing_constants: Optional[list[str]],
    used_constants: Optional[set[str]],
    fresh_constant_generator: Callable[[], Constant],
) -> Optional[Constant]:
    """Get the appropriate constant for universal instantiation."""
    if existing_constants and used_constants is not None:
        # Find first unused constant
        for const_name in existing_constants:
            if const_name not in used_constants:
                return Constant(const_name)
        # All existing constants have been used
        return None
    elif existing_constants:
        # No tracking, use first constant
        return Constant(existing_constants[0])
    else:
        # No existing constants, generate fresh one
        return fresh_constant_generator()


def _get_universal_constant_for_falsification(
    formula: RestrictedUniversalFormula,
    existing_constants: Optional[list[str]],
    used_constants: Optional[set[str]],
    fresh_constant_generator: Callable[[], Constant],
    branch_formulas: set[SignedFormula],
) -> Optional[Constant]:
    """
    Get a constant for falsifying a universal quantifier.

    This function checks if using an existing constant would immediately
    lead to a contradiction (e.g., if we already know B(c) is true from
    an existential witness, we shouldn't use c to try to make B(c) false).
    """
    # Always generate fresh constant for now to avoid infinite loop
    # TODO: Fix the logic to properly check for contradictions
    return fresh_constant_generator()


def get_applicable_rule(
    signed_formula: SignedFormula,
    fresh_constant_generator,  # type: Callable[[], Constant]
    existing_constants: Optional[list[str]] = None,
    used_constants: Optional[set[str]] = None,
) -> Optional[FergusonRule]:
    """Get the applicable Ferguson rule for a signed formula."""
    formula = signed_formula.formula
    sign = signed_formula.sign

    # Try compound formula rules
    if isinstance(formula, CompoundFormula):
        return _get_compound_rule(signed_formula)

    # Handle existential quantifier
    if isinstance(formula, RestrictedExistentialFormula):
        fresh_const = fresh_constant_generator()
        existing_const = None
        if existing_constants and len(existing_constants) > 0:
            existing_const = Constant(existing_constants[0])
        return get_restricted_existential_rule(
            signed_formula, fresh_const, existing_const
        )

    # Handle universal quantifier
    if isinstance(formula, RestrictedUniversalFormula):
        # For f-case, we need special handling to avoid the original bug
        # but also prevent infinite constant generation
        if signed_formula.sign == f:
            # Check if we've already used any constants for this formula
            if used_constants and len(used_constants) > 0:
                # We've already generated a counterexample, no need for more
                return None

            # First f-case instantiation - always use fresh to avoid the bug
            # where existential witnesses are incorrectly reused
            const = fresh_constant_generator()
        elif signed_formula.sign == n:
            # n-sign needs to find counterexamples (false or undefined cases)
            # Special handling: on first application, try BOTH existing and fresh constants

            if used_constants and len(used_constants) > 0:
                # Already applied once, don't apply again to prevent infinite loop
                return None

            if existing_constants:
                # Use first existing constant and also generate a fresh one
                const = Constant(existing_constants[0])
                # Generate fresh constant for additional branches
                fresh = fresh_constant_generator()
                return get_restricted_universal_rule(signed_formula, const, fresh)
            else:
                # No existing constants, just use fresh
                const = fresh_constant_generator()
        else:
            # For other cases (t, e, m), use standard logic
            maybe_const = _get_universal_constant(
                existing_constants, used_constants, fresh_constant_generator
            )
            if maybe_const is None:
                return None
            const = maybe_const
        return get_restricted_universal_rule(signed_formula, const)

    # Handle meta-signs on atomic formulas
    # CRITICAL: m and n signs must be expanded even for atomic formulas!
    if sign == m:
        # m:φ branches to (t:φ) | (f:φ)
        return FergusonRule(
            name="m-atomic",
            premise=signed_formula,
            conclusions=[[SignedFormula(t, formula)], [SignedFormula(f, formula)]],
        )
    elif sign == n:
        # n:φ branches to (f:φ) | (e:φ)
        return FergusonRule(
            name="n-atomic",
            premise=signed_formula,
            conclusions=[[SignedFormula(f, formula)], [SignedFormula(e, formula)]],
        )

    # No rule for t, f, e on atomic formulas
    return None
