"""
Evaluates a computed ("formula") field's formula_expression against a
record's or submission's other field values, using the shared expression
language (app/core/expr.py) — the same one the browser uses to show the
value live while a form is being filled in (frontend/src/lib/expr.ts).
Fields are referenced as {field name} or by bare name: "{quantity} *
{unit_price}", "ROUND(SUM(items.amount) * 1.18, 2)".
"""

from typing import Any

from app.core.expr import ExprError, check_expression, evaluate


class FormulaError(Exception):
    pass


def evaluate_formula(expression: str, field_values: dict[str, Any]) -> Any:
    """The formula's value. FormulaError when it isn't a valid expression or
    can't be computed from these values (e.g. a field it uses is blank)."""
    try:
        check_expression(expression)
    except ExprError as exc:
        raise FormulaError(str(exc)) from exc
    value = evaluate(expression, field_values)
    if value is None:
        raise FormulaError("the formula has no value for these inputs")
    return value


def compute_formula_fields(field_defs: list[dict], field_values: dict[str, Any], scope: dict | None = None) -> dict[str, Any]:
    """Returns just the computed {name: value} pairs for every "formula"
    field in field_defs — callers merge this into field_values before
    persisting. A formula that fails to evaluate (e.g. a driver field is
    still blank) yields None rather than raising, since a partially-filled
    record/submission is a normal, expected state, not an error. Formulas
    are evaluated in field order, so one may use an earlier one's value."""
    values = {**(scope or {}), **field_values}
    computed: dict[str, Any] = {}
    # Line items first, so the form's own formulas can total their rows'
    # computed values (e.g. SUM(items.amount)). A row formula sees the row's
    # fields by name and the form's as parent.<name>.
    for field in field_defs:
        if field.get("type") != "subform" or not isinstance(values.get(field["name"]), list):
            continue
        sub = field.get("subform_fields") or []
        if not any(f.get("type") == "formula" for f in sub):
            continue
        rows = [
            {**row, **compute_formula_fields(sub, row, {**(scope or {}), "parent": field_values})} if isinstance(row, dict) else row
            for row in values[field["name"]]
        ]
        computed[field["name"]] = rows
        values[field["name"]] = rows
    for field in field_defs:
        if field.get("type") != "formula":
            continue
        expression = field.get("formula_expression")
        computed[field["name"]] = evaluate(expression, values) if expression else None
        values[field["name"]] = computed[field["name"]]
    return computed
