"""Formula function registry.

Registry of 56 built-in functions exposed to the formula evaluator,
spanning Conditional, Math, Logical, String, Array, Date/Time, and
Constant categories. The ``FUNCTIONS`` dict is passed to ``simpleeval``
as the allowed call surface.
"""
import math
from datetime import date, datetime, time, timedelta
from typing import Any, Dict


def _parse_date(val: Any) -> Any:
    """Convert string/date/datetime to a naive datetime object.

    Strips tzinfo when present so arithmetic between mixed aware/naive
    inputs (date literals in the formula vs. tz-aware values on records)
    doesn't crash.
    """
    if isinstance(val, str):
        d = datetime.fromisoformat(val)
        if d.tzinfo is not None:
            d = d.replace(tzinfo=None)
        return d
    if isinstance(val, datetime):
        if val.tzinfo is not None:
            return val.replace(tzinfo=None)
        return val
    if isinstance(val, date):
        return datetime.combine(val, time())
    return val


def _IF(condition, true_val, false_val=None):
    return true_val if condition else false_val


def _SWITCH(*args):
    for i in range(0, len(args) - 1, 2):
        if args[i]:
            return args[i + 1]
    return args[-1] if len(args) % 2 == 1 else None


def _XOR(*args):
    if len(args) == 2:
        return args[0] ^ args[1]
    result = args[0]
    for arg in args[1:]:
        result = result ^ arg
    return result


def _DATEADD(date_val, amount, unit):
    base = _parse_date(date_val)
    if unit == "days":
        return base + timedelta(days=amount)
    if unit == "hours":
        return base + timedelta(hours=amount)
    if unit == "minutes":
        return base + timedelta(minutes=amount)
    raise ValueError(f"Unsupported unit: {unit}")


def _DATEDIFF(date1, date2, unit="days"):
    d1 = _parse_date(date1)
    d2 = _parse_date(date2)
    delta = d1 - d2
    if unit == "days":
        return delta.days
    if unit == "seconds":
        return delta.total_seconds()
    if unit == "minutes":
        return delta.total_seconds() / 60
    if unit == "hours":
        return delta.total_seconds() / 3600
    raise ValueError(f"Unsupported unit: {unit}")


def _WORKDAY(date_val, no_of_days):
    current = _parse_date(date_val)
    if no_of_days == 0:
        return current.date() if isinstance(current, datetime) else current
    direction = 1 if no_of_days > 0 else -1
    remaining = abs(no_of_days)
    while remaining > 0:
        current = current + timedelta(days=direction)
        if current.weekday() < 5:
            remaining -= 1
    return current


def _WORKDAYS(date1, date2):
    d1 = _parse_date(date1)
    d2 = _parse_date(date2)
    if d1 == d2:
        return 0
    start, end = (d1, d2) if d1 < d2 else (d2, d1)
    workdays = 0
    current = start
    while current < end:
        if current.weekday() < 5:
            workdays += 1
        current += timedelta(days=1)
    return workdays if d1 <= d2 else -workdays


def _DAYS(date1, date2):
    d1 = _parse_date(date1)
    d2 = _parse_date(date2)
    if d1 == d2:
        return 0
    start, end = (d1, d2) if d1 < d2 else (d2, d1)
    days = 0
    current = start
    while current < end:
        days += 1
        current += timedelta(days=1)
    return days


def _HOURS_DIFF(end_time, start_time):
    if isinstance(end_time, str):
        end_time = datetime.strptime(end_time, "%H:%M")
    if isinstance(start_time, str):
        start_time = datetime.strptime(start_time, "%H:%M")
    return (end_time - start_time).total_seconds() / 3600


def _MINUTES_DIFF(end_time, start_time):
    if isinstance(end_time, str):
        end_time = datetime.strptime(end_time, "%H:%M")
    if isinstance(start_time, str):
        start_time = datetime.strptime(start_time, "%H:%M")
    return (end_time - start_time).total_seconds() / 60


def _ADD_MINUTES(time_val, minutes):
    if isinstance(time_val, str):
        time_val = datetime.strptime(time_val, "%H:%M")
    return time_val + timedelta(minutes=minutes)


def _SUBTRACT_MINUTES(time_val, minutes):
    if isinstance(time_val, str):
        time_val = datetime.strptime(time_val, "%H:%M")
    return time_val - timedelta(minutes=minutes)


def _APPEND(array, *values, unique=False):
    if array is None:
        array = []
    elif not isinstance(array, list):
        array = [array]
    result = array.copy()
    for value in values:
        if value is None:
            continue
        if unique:
            if value not in result:
                result.append(value)
        else:
            result.append(value)
    return result


def _EXTEND(*arrays):
    result = []
    for arr in arrays:
        if isinstance(arr, list):
            result.extend(arr)
        elif arr is not None:
            result.append(arr)
    return result


def _REMOVE(array, *values):
    if not isinstance(array, list):
        return []
    result = array.copy()
    for value in values:
        while value in result:
            result.remove(value)
    return result


def _TEXT(value, format_string):
    try:
        if isinstance(value, (int, float)):
            return format(value, format_string)
        if isinstance(value, datetime):
            return value.strftime(format_string)
        if isinstance(value, date):
            return value.strftime(format_string)
        return str(value)
    except (ValueError, TypeError):
        return str(value)


FUNCTIONS: Dict[str, Any] = {
    # Conditional
    "IF": _IF,
    "SWITCH": _SWITCH,
    # Math
    "SUM": lambda *a: sum(a),
    "COUNT": lambda *a: len(a),
    "AVG": lambda *a: (sum(a) / len(a)) if a else None,
    "MIN": lambda *a: min(a),
    "MAX": lambda *a: max(a),
    "ROUND": lambda v, n=0: round(v, n),
    "ABS": lambda v: abs(v),
    "DIVIDE": lambda a, b: a / b,
    "MULTIPLY": lambda a, b: a * b,
    "MINUS": lambda a, b: a - b,
    "MOD": lambda a, b: a % b,
    "POWER": lambda a, b: a ** b,
    "SQRT": lambda v: v ** 0.5,
    "LOG": lambda n, base=10: math.log(n, base),
    # Logical
    "AND": lambda *a: all(a),
    "OR": lambda *a: any(a),
    "NOT": lambda a: not a,
    "XOR": _XOR,
    "EXACT": lambda a, b: a == b,
    # String
    "CONCATENATE": lambda *a: "".join(str(x) for x in a),
    "UPPER": lambda s: str(s).upper(),
    "LOWER": lambda s: str(s).lower(),
    "LEN": lambda s: len(str(s)),
    "LEFT": lambda t, n: str(t)[:n],
    "RIGHT": lambda t, n: str(t)[-n:],
    "MID": lambda t, s, l: str(t)[s - 1: s - 1 + l],
    "TRIM": lambda t: str(t).strip(),
    "REPLACE": lambda t, s, l, new: str(t)[:s] + str(new) + str(t)[s + l:],
    "REPT": lambda t, n: str(t) * n,
    "SEARCH": lambda t, find, start=0: str(t).find(str(find), start),
    "SUBSTITUTE": lambda t, old, new: str(t).replace(str(old), str(new)),
    "TEXT": _TEXT,
    "IN": lambda v, lst: v in lst,
    "STR": lambda v: str(v),
    "VALUE": lambda t, base=10: int(str(t), base),
    # Array
    "APPEND": _APPEND,
    "EXTEND": _EXTEND,
    "REMOVE": _REMOVE,
    # Date/Time
    "TODAY": lambda: date.today(),
    "NOW": lambda: datetime.now(),
    "DATE": lambda y, m, d: datetime(y, m, d),
    "DATEVALUE": lambda d: _parse_date(d).toordinal(),
    "DATEADD": _DATEADD,
    "DATEDIFF": _DATEDIFF,
    "ADD_DAYS": lambda d, n: _parse_date(d) + timedelta(days=n),
    "SUBTRACT_DAYS": lambda d, n: _parse_date(d) - timedelta(days=n),
    "WORKDAY": _WORKDAY,
    "WORKDAYS": _WORKDAYS,
    "DAYS": _DAYS,
    "MONTH": lambda d: _parse_date(d).month,
    "YEAR": lambda d: _parse_date(d).year,
    "DAY": lambda d: _parse_date(d).day,
    "HOUR": lambda d: _parse_date(d).hour,
    "MINUTE": lambda d: _parse_date(d).minute,
    "SECOND": lambda d: _parse_date(d).second,
    "HOURS_DIFF": _HOURS_DIFF,
    "MINUTES_DIFF": _MINUTES_DIFF,
    "ADD_MINUTES": _ADD_MINUTES,
    "SUBTRACT_MINUTES": _SUBTRACT_MINUTES,
    "FORMAT_DATE": lambda d, fmt: _parse_date(d).strftime(fmt),
    "IOSWEEKNUM": lambda d: _parse_date(d).isocalendar()[1],
    "WEEKNUM": lambda d: int(_parse_date(d).strftime("%U")) + 1,
    # Constants
    "PI": lambda: math.pi,
    "TRUE": lambda: True,
    "FALSE": lambda: False,
}
