"""Condition evaluation for Tasks — the If / Else node's rules, a Loop node's
"while" rules, and every transition's own condition (app/crud/tasks.py).

A rule is {driver_field, operator, value}: driver_field is a variable name
or dotted path ("item.amount", "record.status"); value is compared loosely —
"5" equals 5, "true" equals True — since values typed into the designer are
always text. A value may itself reference a variable as "{{limit}}".
"""

import re
from typing import Any

from app.crud.params import get_path

UNARY = {"is_empty", "is_not_empty", "is_true", "is_false"}
_TEMPLATE = re.compile(r"^\s*\{\{\s*([\w.\[\]]+)\s*\}\}\s*$")


def _number(value: Any) -> float | None:
    if isinstance(value, bool):
        return None
    if isinstance(value, (int, float)):
        return float(value)
    try:
        return float(str(value).strip())
    except (TypeError, ValueError):
        return None


def _truthy(value: Any) -> bool:
    if isinstance(value, str):
        return value.strip().lower() in {"true", "yes", "y", "1", "on"}
    return bool(value)


def _empty(value: Any) -> bool:
    return value is None or (isinstance(value, (str, list, dict, tuple)) and len(value) == 0) or (isinstance(value, str) and not value.strip())


def _equal(a: Any, b: Any) -> bool:
    na, nb = _number(a), _number(b)
    if na is not None and nb is not None:
        return na == nb
    if isinstance(a, bool) or isinstance(b, bool):
        return _truthy(a) == _truthy(b)
    if a is None or b is None:
        return a is None and (b is None or b == "")
    return str(a) == str(b)


def _as_list(value: Any) -> list:
    if isinstance(value, list):
        return value
    if isinstance(value, str):
        return [v.strip() for v in value.split(",")]
    return [value]


def resolve_value(value: Any, context: dict) -> Any:
    """A rule's comparison value: "{{path}}" means another variable."""
    if isinstance(value, str) and (m := _TEMPLATE.match(value)):
        return get_path(context, m.group(1))
    return value


def compare(operator: str, actual: Any, expected: Any) -> bool:
    if operator == "is_empty":
        return _empty(actual)
    if operator == "is_not_empty":
        return not _empty(actual)
    if operator == "is_true":
        return _truthy(actual)
    if operator == "is_false":
        return not _truthy(actual)
    if operator == "equals":
        return _equal(actual, expected)
    if operator == "not_equals":
        return not _equal(actual, expected)
    if operator in ("gt", "gte", "lt", "lte"):
        na, nb = _number(actual), _number(expected)
        a, b = (na, nb) if na is not None and nb is not None else (None if actual is None else str(actual), str(expected))
        if a is None:
            return False
        return {"gt": a > b, "gte": a >= b, "lt": a < b, "lte": a <= b}[operator]
    if operator in ("in", "not_in"):
        found = any(_equal(actual, e) for e in _as_list(expected))
        return found if operator == "in" else not found
    if operator in ("contains", "not_contains"):
        if isinstance(actual, (list, tuple)):
            found = any(_equal(item, expected) for item in actual)
        elif isinstance(actual, dict):
            found = str(expected) in actual
        else:
            found = actual is not None and str(expected).lower() in str(actual).lower()
        return found if operator == "contains" else not found
    if operator == "starts_with":
        return actual is not None and str(actual).lower().startswith(str(expected).lower())
    if operator == "ends_with":
        return actual is not None and str(actual).lower().endswith(str(expected).lower())
    return False


def condition_passes(condition: dict | None, context: dict) -> bool:
    if not condition:
        return True
    actual = get_path(context, condition.get("driver_field") or "")
    return compare(condition.get("operator") or "equals", actual, resolve_value(condition.get("value"), context))


def rules_pass(rules: list[dict], match: str, context: dict) -> bool:
    """No rules = true. match "any" = at least one rule; otherwise all."""
    if not rules:
        return True
    results = (condition_passes(r, context) for r in rules)
    return any(results) if match == "any" else all(results)


def describe(rules: list[dict], match: str) -> str:
    parts = []
    for r in rules:
        op = (r.get("operator") or "equals").replace("_", " ")
        parts.append(f"{r.get('driver_field') or '?'} {op}" + ("" if r.get("operator") in UNARY else f" {r.get('value')!s}"))
    return f" {'or' if match == 'any' else 'and'} ".join(parts) or "no rules"
