"""Deterministic comparison operators used by the rule engine.

Every operator is a pure function of (field value, rule value, rule value2).
No AI, no network, no randomness.
"""

from __future__ import annotations

import re
from collections.abc import Iterable
from typing import Any

from app.models.enums import RuleOperator


class RuleEvaluationError(ValueError):
    """Raised when a rule is structurally invalid (bad regex, missing bound, ...)."""


def _as_text(value: Any, case_sensitive: bool) -> str:
    """Flatten any field value to a searchable string."""
    if value is None:
        return ""
    if isinstance(value, bool):
        text = "true" if value else "false"
    elif isinstance(value, (list, tuple, set)):
        text = " ".join(str(item) for item in value if item is not None)
    else:
        text = str(value)
    text = text.strip()
    return text if case_sensitive else text.lower()


def _as_number(value: Any) -> float | None:
    """Coerce a field or rule value to a number, tolerating things like '10-100' -> 10."""
    if value is None or isinstance(value, bool):
        return None
    if isinstance(value, (int, float)):
        return float(value)
    match = re.search(r"-?\d+(?:\.\d+)?", str(value))
    return float(match.group()) if match else None


def _iter_parts(value: Any) -> Iterable[str]:
    """Yield each element of a list field, or the single scalar value."""
    if isinstance(value, (list, tuple, set)):
        for item in value:
            if item is not None:
                yield str(item)
    elif value is not None:
        yield str(value)


def _is_empty(value: Any) -> bool:
    if value is None:
        return True
    if isinstance(value, str):
        return not value.strip()
    if isinstance(value, (list, tuple, set, dict)):
        return len(value) == 0
    return False


def apply_operator(
    operator: str,
    field_value: Any,
    rule_value: Any = None,
    rule_value2: Any = None,
    case_sensitive: bool = False,
) -> bool:
    """Return True when ``field_value`` satisfies ``operator`` against the rule values."""
    op = operator.strip() if isinstance(operator, str) else operator

    if op == RuleOperator.IS_EMPTY:
        return _is_empty(field_value)
    if op == RuleOperator.IS_NOT_EMPTY:
        return not _is_empty(field_value)

    if op in (RuleOperator.GREATER_THAN, RuleOperator.LESS_THAN, RuleOperator.BETWEEN):
        left = _as_number(field_value)
        if left is None:
            return False
        if op == RuleOperator.BETWEEN:
            low, high = _as_number(rule_value), _as_number(rule_value2)
            if low is None or high is None:
                raise RuleEvaluationError("'between' requires both value and value2")
            if low > high:
                low, high = high, low
            return low <= left <= high
        right = _as_number(rule_value)
        if right is None:
            raise RuleEvaluationError(f"'{op}' requires a numeric value")
        return left > right if op == RuleOperator.GREATER_THAN else left < right

    needle = _as_text(rule_value, case_sensitive)

    if op == RuleOperator.REGEX:
        if not needle:
            raise RuleEvaluationError("'regex' requires a pattern")
        flags = 0 if case_sensitive else re.IGNORECASE
        try:
            pattern = re.compile(str(rule_value), flags)
        except re.error as exc:  # invalid pattern authored in the UI
            raise RuleEvaluationError(f"invalid regular expression: {exc}") from exc
        return any(pattern.search(part) is not None for part in _iter_parts(field_value))

    haystack = _as_text(field_value, case_sensitive)

    if op == RuleOperator.CONTAINS:
        return bool(needle) and needle in haystack
    if op == RuleOperator.NOT_CONTAINS:
        return not needle or needle not in haystack
    if op == RuleOperator.EQUALS:
        parts = [_as_text(part, case_sensitive) for part in _iter_parts(field_value)]
        return haystack == needle or needle in parts
    if op == RuleOperator.NOT_EQUALS:
        parts = [_as_text(part, case_sensitive) for part in _iter_parts(field_value)]
        return haystack != needle and needle not in parts
    if op == RuleOperator.STARTS_WITH:
        return bool(needle) and haystack.startswith(needle)
    if op == RuleOperator.ENDS_WITH:
        return bool(needle) and haystack.endswith(needle)

    raise RuleEvaluationError(f"unsupported operator: {operator}")


OPERATOR_LABELS: dict[str, str] = {
    RuleOperator.CONTAINS: "contains",
    RuleOperator.NOT_CONTAINS: "does not contain",
    RuleOperator.EQUALS: "equals",
    RuleOperator.NOT_EQUALS: "not equals",
    RuleOperator.STARTS_WITH: "starts with",
    RuleOperator.ENDS_WITH: "ends with",
    RuleOperator.REGEX: "matches regex",
    RuleOperator.GREATER_THAN: "greater than",
    RuleOperator.LESS_THAN: "less than",
    RuleOperator.BETWEEN: "between",
    RuleOperator.IS_EMPTY: "is empty",
    RuleOperator.IS_NOT_EMPTY: "is not empty",
}
