"""Built-in filters for Kida templates.

Filters transform values in template expressions using the pipe syntax:
`{{ value | filter }}` or `{{ value | filter(arg1, arg2) }}`

Constants for sort key tuples:
SORT_KEY_NONE: (1, 0, 0) - Used for None/empty values (sorts last)
SORT_KEY_NUMERIC: (0, 0, value) - Used for numeric values
SORT_KEY_STRING: (0, 1, value) - Used for string values

Categories:
**String Manipulation**:
    - `capitalize`, `lower`, `upper`, `title`: Case conversion
    - `trim`/`strip`: Remove whitespace
    - `truncate`: Shorten with ellipsis
    - `replace`, `striptags`: Text transformation
    - `center`, `indent`, `wordwrap`: Formatting

**HTML/Security**:
    - `escape`/`e`: HTML entity encoding (auto-applied with autoescape)
    - `safe`: Mark content as trusted HTML (skip escaping)
    - `striptags`: Remove HTML tags

**Collections**:
    - `first`, `last`: Get endpoints
    - `length`/`count`: Item count
    - `sort`: Sort sequence (with `attribute=` for objects)
    - `reverse`: Reverse order
    - `unique`: Remove duplicates
    - `batch`, `slice`: Group items
    - `map`, `select`, `reject`: Functional operations
    - `selectattr`, `rejectattr`: Filter by attribute
    - `groupby`: Group by attribute
    - `join`: Concatenate with separator

**Numbers**:
    - `abs`, `round`, `int`, `float`: Math operations
    - `filesizeformat`: Human-readable file sizes

**Type Conversion**:
    - `string`, `int`, `float`, `list`: Type coercion
    - `tojson`: JSON serialization (auto-escaped)

**Debugging**:
    - `debug`: Print variable info to stderr
    - `pprint`: Pretty-print value

**Validation**:
    - `default`/`d`: Fallback for None/undefined
    - `require`: Raise error if None

None-Resilient Behavior:
Filters handle None gracefully (like Hugo):
- `{{ none | default('N/A') }}` -> `'N/A'`
- `{{ none | length }}` -> `0`
- `{{ none | first }}` -> `None`

Custom Filters:
    >>> env.add_filter('double', lambda x: x * 2)
    >>> env.add_filter('money', lambda x, currency='$': f'{currency}{x:,.2f}')

Or with decorator:
    >>> @env.filter()
    ... def reverse_words(s):
    ...     return ' '.join(s.split()[::-1])

"""

from __future__ import annotations

import json
import random as random_module
import re
import textwrap
from collections import deque
from collections.abc import Callable
from datetime import datetime
from itertools import groupby, islice
from pprint import pformat
from typing import Any
from urllib.parse import quote

from kida.environment.exceptions import TemplateRuntimeError
from kida.utils.html import (
    Markup,
    html_escape_filter,
    strip_tags,
    xmlattr,
)

# Sort key tuple constants for clarity
SORT_KEY_NONE = (1, 0, 0)  # None/empty values sort last


def _make_sort_key_numeric(value: int | float) -> tuple[int, int, int | float]:
    """Create sort key for numeric value."""
    return (0, 0, value)


def _make_sort_key_string(value: str) -> tuple[int, int, str]:
    """Create sort key for string value."""
    return (0, 1, value)


def _filter_abs(value: Any) -> Any:
    """Return absolute value."""
    return abs(value)


def _filter_capitalize(value: str) -> str:
    """Capitalize first character."""
    return str(value).capitalize()


def _filter_default(value: Any, default: Any = "", boolean: bool = False) -> Any:
    """Return default if value is undefined or falsy.

    With None-resilient handling, empty string is treated as missing (like None).
    This matches Hugo behavior where nil access returns empty string.

    """
    if boolean:
        return value or default
    # Treat both None and "" as missing (None-resilient compatibility)
    return default if value is None or value == "" else value


def _filter_escape(value: Any) -> Markup:
    """HTML-escape the value.

    Returns a Markup object so the result won't be escaped again by autoescape.
    Uses optimized html_escape_filter from utils.html module.

    """
    return html_escape_filter(value)


def _filter_first(value: Any) -> Any:
    """Return first item of sequence."""
    if value is None:
        return None
    try:
        return next(iter(value), None)
    except (TypeError, ValueError):
        return None


def _filter_int(value: Any, default: int = 0, strict: bool = False) -> int:
    """Convert to integer.

    Args:
        value: Value to convert to integer.
        default: Default value to return if conversion fails (default: 0).
        strict: If True, raise TemplateRuntimeError on conversion failure
            instead of returning default (default: False).

    Returns:
        Integer value, or default if conversion fails and strict=False.

    Raises:
        TemplateRuntimeError: If strict=True and conversion fails.

    Examples:
            >>> _filter_int("42")
        42
            >>> _filter_int("not a number")
        0
            >>> _filter_int("not a number", strict=True)
        TemplateRuntimeError: Cannot convert str to int: 'not a number'

    """
    try:
        return int(value)
    except (ValueError, TypeError) as e:
        if strict:
            raise TemplateRuntimeError(
                f"Cannot convert {type(value).__name__} to int: {value!r}",
                suggestion="Use | default(0) | int for optional conversion, or ensure value is numeric",
            ) from e
        return default


def _filter_join(value: Any, separator: str = "") -> str:
    """Join sequence with separator."""
    if value is None:
        return ""
    try:
        return separator.join(str(x) for x in value)
    except (TypeError, ValueError):
        return str(value)


def _filter_last(value: Any) -> Any:
    """Return last item of sequence.

    Uses deque(maxlen=1) for O(1) space on iterators; avoids materializing
    entire sequence.
    """
    if value is None:
        return None
    try:
        it = iter(value)
        dq = deque(it, maxlen=1)
        return dq[0] if dq else None
    except (TypeError, ValueError):
        return None


def _filter_length(value: Any) -> int:
    """Return length of sequence."""
    if value is None:
        return 0
    try:
        return len(value)
    except (TypeError, ValueError):
        return 0


def _filter_list(value: Any) -> list[Any]:
    """Convert to list."""
    return list(value)


def _filter_lower(value: str) -> str:
    """Convert to lowercase."""
    return str(value).lower()


def _filter_replace(value: str, old: str, new: str, count: int = -1) -> str:
    """Replace occurrences."""
    return str(value).replace(old, new, count if count > 0 else -1)


def _filter_reverse(value: Any) -> list[Any] | str:
    """Reverse sequence."""
    try:
        return list(reversed(value))
    except TypeError:
        return str(value)[::-1]


def _filter_safe(value: Any, reason: str | None = None) -> Markup:
    """Mark value as safe (no HTML escaping).

    Args:
        value: Content to mark as safe for raw HTML output.
        reason: Optional documentation of why this content is trusted.
            Purely for code review and audit purposes - not used at runtime.

    Example:
        {{ content | safe }}
        {{ user_html | safe(reason="sanitized by bleach library") }}
        {{ cms_block | safe(reason="trusted CMS output, admin-only") }}

    """
    return Markup(str(value))


def _filter_sort(
    value: Any,
    reverse: bool = False,
    case_sensitive: bool = False,
    attribute: str | None = None,
) -> list[Any]:
    """Sort sequence with improved error handling for None values.

    When sorting fails due to None comparisons, provides detailed error
    showing which items have None values for the sort attribute.

    """
    from kida.environment.exceptions import NoneComparisonError

    if not value:
        return []

    items = list(value)

    # Handle multi-attribute sorting (e.g., "weight,title")
    attributes = attribute.split(",") if attribute else []

    def key_func(item: Any) -> tuple[Any, ...]:
        """Generate sort key with None-safe handling.

        Strategy: Use (is_none, sort_value) tuples where:
        - is_none=0 for real values (sort first)
        - is_none=1 for None/empty values (sort last)
        - sort_value is normalized for consistent comparison

        With None-resilient handling, both None and "" are treated as missing.
        """
        if not attributes:
            val = item
            if val is None or val == "":
                return SORT_KEY_NONE  # None/empty sorts last
            if isinstance(val, (int, float)):
                return _make_sort_key_numeric(val)  # Numbers
            val_str = str(val)
            if not case_sensitive:
                val_str = val_str.lower()
            return _make_sort_key_string(val_str)  # Strings

        # Build tuple of values for multi-attribute sort
        values: list[tuple[int, int, int | float | str]] = []
        for attr in attributes:
            attr = attr.strip()
            val = _filter_attr(item, attr)

            # Defensive: Handle None and "" (None-resilient) first
            if val is None or val == "":
                # None/empty always sorts last
                values.append(SORT_KEY_NONE)
            elif isinstance(val, (int, float)):
                # Numbers: type 0 means numeric
                values.append(_make_sort_key_numeric(val))
            else:
                # Everything else as string: type 1 means string
                # Convert to string safely (handles edge cases)
                try:
                    val_str = str(val)
                    if not case_sensitive:
                        val_str = val_str.lower()
                    values.append(_make_sort_key_string(val_str))
                except (TypeError, ValueError):
                    # Fallback for unstringable values (shouldn't happen, but be defensive)
                    values.append((1, 0, ""))

        return tuple(values)

    try:
        return sorted(items, reverse=reverse, key=key_func)
    except TypeError as e:
        # Provide detailed error about which items have None/empty values
        error_str = str(e)
        if "NoneType" in error_str or "not supported between" in error_str:
            # Find items with None/empty values for the attribute(s)
            none_items = []
            for idx, item in enumerate(items):
                if attributes:
                    for attr in attributes:
                        attr = attr.strip()
                        val = _filter_attr(item, attr)
                        if val is None or val == "":
                            # Get a representative label for the item
                            item_label = (
                                getattr(item, "title", None)
                                or getattr(item, "name", None)
                                or f"item[{idx}]"
                            )
                            none_items.append(f"  - {item_label}: {attr} = None/empty")
                            break
                else:
                    if item is None or item == "":
                        none_items.append(f"  - item[{idx}] = None/empty")

            attr_str = attribute or "value"
            error_msg = f"Sort failed: cannot compare None values when sorting by '{attr_str}'"
            if none_items:
                error_msg += "\n\nItems with None/empty values:\n" + "\n".join(none_items[:10])
                if len(none_items) > 10:
                    error_msg += f"\n  ... and {len(none_items) - 10} more"
            error_msg += "\n\nSuggestion: Use | default(fallback) on the attribute, or filter out None values first"

            raise NoneComparisonError(
                None,
                None,
                attribute=attribute,
                expression=f"| sort(attribute='{attribute}')" if attribute else "| sort",
            ) from e
        raise


def _filter_string(value: Any) -> str:
    """Convert to string."""
    return str(value)


def _filter_title(value: str) -> str:
    """Title case."""
    return str(value).title()


def _filter_trim(value: str, chars: str | None = None) -> str:
    """Strip whitespace or specified characters.

    Args:
        value: String to trim
        chars: Optional characters to strip (default: whitespace)

    """
    return str(value).strip(chars)


def _filter_truncate(
    value: str,
    length: int = 255,
    killwords: bool = False,
    end: str = "...",
    leeway: int | None = None,
) -> str:
    """Truncate string to specified length.

    Args:
        value: String to truncate
        length: Maximum length including end marker
        killwords: If False (default), truncate at word boundary; if True, cut mid-word
        end: String to append when truncated (default: "...")
        leeway: Allow slightly longer strings before truncating (Jinja2 compat, ignored)

    Returns:
        Truncated string with end marker if truncated

    """
    value = str(value)
    if len(value) <= length:
        return value

    # Calculate available space for content
    available = length - len(end)
    if available <= 0:
        return end[:length] if length > 0 else ""

    if killwords:
        # Cut mid-word
        return value[:available] + end
    else:
        # Try to break at word boundary
        truncated = value[:available]
        # Find last space
        last_space = truncated.rfind(" ")
        if last_space > 0:
            truncated = truncated[:last_space]
        return truncated.rstrip() + end


def _filter_upper(value: str) -> str:
    """Convert to uppercase."""
    return str(value).upper()


def _filter_tojson(value: Any, indent: int | None = None) -> Markup:
    """Convert value to JSON string (marked safe to prevent escaping)."""
    return Markup(json.dumps(value, indent=indent, default=str))


def _filter_batch(value: Any, linecount: int, fill_with: Any = None) -> list[list[Any]]:
    """Batch items into groups of linecount."""
    result: list[list[Any]] = []
    batch: list[Any] = []
    for item in value:
        batch.append(item)
        if len(batch) >= linecount:
            result.append(batch)
            batch = []
    if batch:
        if fill_with is not None:
            while len(batch) < linecount:
                batch.append(fill_with)
        result.append(batch)
    return result


def _filter_slice(value: Any, slices: int, fill_with: Any = None) -> list[list[Any]]:
    """Slice items into number of groups."""
    result: list[list[Any]] = [[] for _ in range(slices)]
    for idx, item in enumerate(value):
        result[idx % slices].append(item)
    return result


def _filter_take(value: Any, count: int) -> list[Any]:
    """Take the first N items from a sequence.

    Kida-native filter for readable pipeline operations.

    Example:
        {{ items |> take(5) }}
        {{ posts |> sort(attribute='date', reverse=true) |> take(3) }}

    Args:
        value: Sequence to take from
        count: Number of items to take

    Returns:
        List of first N items (or fewer if sequence is shorter)

    """
    if value is None:
        return []
    try:
        return list(islice(value, count))
    except (TypeError, ValueError):
        return []


def _filter_skip(value: Any, count: int) -> list[Any]:
    """Skip the first N items from a sequence.

    Kida-native filter for readable pipeline operations.

    Example:
        {{ items |> skip(5) }}
        {{ posts |> skip(10) |> take(10) }}  # pagination

    Args:
        value: Sequence to skip from
        count: Number of items to skip

    Returns:
        List of remaining items after skipping N

    """
    if value is None:
        return []
    try:
        return list(islice(value, count, None))
    except (TypeError, ValueError):
        return []


def _filter_compact(value: Any, *, truthy: bool = True) -> list[Any]:
    """Remove None values (and optionally all falsy values) from a sequence.

    Enables declarative list building with conditional items, replacing
    imperative {% do %} patterns.

    Example:
        {# Declarative conditional list building #}
        {% let badges = [
                'async' if member.is_async,
                'deprecated' if member.is_deprecated,
                'abstract' if member.is_abstract,
        ] | compact %}

        {# Remove only None (keep empty strings, 0, False) #}
        {{ [0, None, '', False, 'value'] | compact(truthy=false) }}
        → [0, '', False, 'value']

        {# Remove all falsy values (default) #}
        {{ [0, None, '', False, 'value'] | compact }}
        → ['value']

    Args:
        value: Sequence to compact
        truthy: If True (default), remove all falsy values.
                If False, remove only None values.

    Returns:
        List with None/falsy values removed.

    """
    if value is None:
        return []
    try:
        if truthy:
            return [v for v in value if v]
        else:
            return [v for v in value if v is not None]
    except (TypeError, ValueError):
        return []


def _filter_map(
    value: Any,
    *args: Any,
    attribute: str | None = None,
) -> list[Any]:
    """Map an attribute or method from a sequence."""
    if value is None:
        return []
    try:
        if attribute:
            return [_filter_attr(item, attribute) for item in value]
        if args:
            method_name = args[0]
            return [getattr(item, method_name)() for item in value]
        return list(value)
    except (TypeError, ValueError):
        return []


def _filter_selectattr(value: Any, attr: str, *args: Any) -> list[Any]:
    """Select items where attribute passes test."""
    from kida.environment.tests import _apply_test

    result = []
    for item in value:
        val = getattr(item, attr, None)
        if args:
            test_name = args[0]
            test_args = args[1:] if len(args) > 1 else ()
            if _apply_test(val, test_name, *test_args):
                result.append(item)
        elif val:
            result.append(item)
    return result


def _filter_rejectattr(value: Any, attr: str, *args: Any) -> list[Any]:
    """Reject items where attribute passes test."""
    from kida.environment.tests import _apply_test

    result = []
    for item in value:
        val = getattr(item, attr, None)
        if args:
            test_name = args[0]
            test_args = args[1:] if len(args) > 1 else ()
            if not _apply_test(val, test_name, *test_args):
                result.append(item)
        elif not val:
            result.append(item)
    return result


def _filter_select(value: Any, test_name: str | None = None, *args: Any) -> list[Any]:
    """Select items that pass a test."""
    from kida.environment.tests import _apply_test

    if test_name is None:
        return [item for item in value if item]
    return [item for item in value if _apply_test(item, test_name, *args)]


def _filter_reject(value: Any, test_name: str | None = None, *args: Any) -> list[Any]:
    """Reject items that pass a test."""
    from kida.environment.tests import _apply_test

    if test_name is None:
        return [item for item in value if not item]
    return [item for item in value if not _apply_test(item, test_name, *args)]


def _filter_groupby(value: Any, attribute: str) -> list[dict[str, Any]]:
    """Group items by attribute with None-safe sorting.

    Items with None/empty values for the attribute are grouped together
    and sorted last.

    """

    def get_key(item: Any) -> Any:
        # Support dict-style access for dict items
        if isinstance(item, dict):
            return item.get(attribute)
        return getattr(item, attribute, None)

    def sort_key(item: Any) -> tuple[Any, ...]:
        """None-safe sort key: (is_none, value_for_comparison)."""
        val = get_key(item)
        if val is None or val == "":
            # None/empty sorts last, use empty string for grouping key stability
            return (1, "")
        if isinstance(val, (int, float)):
            return (0, val)
        # Convert to string for consistent comparison
        return (0, str(val).lower())

    sorted_items = sorted(value, key=sort_key)
    return [
        {"grouper": key, "list": list(group)} for key, group in groupby(sorted_items, key=get_key)
    ]


def _filter_striptags(value: str) -> str:
    """Strip HTML tags."""
    return strip_tags(value)


def _filter_wordwrap(value: str, width: int = 79, break_long_words: bool = True) -> str:
    """Wrap text at width."""
    return textwrap.fill(str(value), width=width, break_long_words=break_long_words)


def _filter_indent(value: str, width: int = 4, first: bool = False) -> str:
    """Indent text lines."""
    lines = str(value).splitlines(True)
    indent = " " * width
    if not first:
        return lines[0] + "".join(indent + line for line in lines[1:])
    return "".join(indent + line for line in lines)


def _filter_urlencode(value: str) -> str:
    """URL-encode a string."""
    return quote(str(value), safe="")


def _filter_pprint(value: Any) -> str:
    """Pretty-print a value."""
    return pformat(value)


def _filter_xmlattr(value: dict[str, Any]) -> Markup:
    """Convert dict to XML attributes.

    Returns Markup to prevent double-escaping when autoescape is enabled.

    """
    return xmlattr(value)


def _filter_unique(
    value: Any, case_sensitive: bool = False, attribute: str | None = None
) -> list[Any]:
    """Return unique items."""
    seen: set[Any] = set()
    result = []
    for item in value:
        val = getattr(item, attribute, None) if attribute else item
        if not case_sensitive and isinstance(val, str):
            val = val.lower()
        if val not in seen:
            seen.add(val)
            result.append(item)
    return result


def _filter_min(value: Any, attribute: str | None = None) -> Any:
    """Return minimum value."""
    if attribute:
        return min(value, key=lambda x: getattr(x, attribute, None) or 0)
    return min(value)


def _filter_max(value: Any, attribute: str | None = None) -> Any:
    """Return maximum value."""
    if attribute:
        return max(value, key=lambda x: getattr(x, attribute, None) or 0)
    return max(value)


def _filter_sum(value: Any, attribute: str | None = None, start: int = 0) -> Any:
    """Return sum of values."""
    if attribute:
        return sum((getattr(x, attribute, 0) for x in value), start)
    return sum(value, start)


def _filter_attr(value: Any, name: str) -> Any:
    """Get attribute from object or dictionary key.

    Returns "" for None/missing values (None-resilient, like Hugo).

    """
    if value is None:
        return ""
    # Try dictionary access first (for dict items)
    if isinstance(value, dict):
        val = value.get(name)
        return "" if val is None else val
    # Then try attribute access (for objects)
    try:
        val = getattr(value, name, None)
        return "" if val is None else val
    except (AttributeError, TypeError):
        return ""


def _filter_get(value: Any, key: str, default: Any = None) -> Any:
    """Safe dictionary/object access that avoids Python method name conflicts.

    When accessing dict keys like 'items', 'keys', 'values', or 'get', using
    dotted access (e.g., ``schema.items``) returns the method, not the key value.
    This filter provides clean syntax for safe key access.

    Examples:
        {{ user | get('name') }}              # Get 'name' key
        {{ config | get('timeout', 30) }}     # With default value
        {{ schema | get('items') }}           # Safe access to 'items' key
        {{ data | get('keys') }}              # Safe access to 'keys' key

    Args:
        value: Dict, object, or None to access
        key: Key or attribute name to access
        default: Value to return if key doesn't exist (default: None)

    Returns:
        value[key] if exists, else default

    Note:
        This avoids conflicts with Python's built-in dict method names
        (items, keys, values, get) that would otherwise shadow key access.

    """
    if value is None:
        return default

    # Dict access (handles method name conflicts)
    if isinstance(value, dict):
        return value.get(key, default)

    # Object attribute access
    return getattr(value, key, default)


def _filter_format(value: str, *args: Any, **kwargs: Any) -> str:
    """Format string with args/kwargs."""
    return str(value).format(*args, **kwargs)


def _filter_center(value: str, width: int = 80) -> str:
    """Center string in width."""
    return str(value).center(width)


def _filter_round(value: Any, precision: int = 0, method: str = "common") -> float:
    """Round a number to a given precision."""
    if method == "ceil":
        import math

        return float(math.ceil(float(value) * (10**precision)) / (10**precision))
    elif method == "floor":
        import math

        return float(math.floor(float(value) * (10**precision)) / (10**precision))
    else:
        return round(float(value), precision)


def _filter_format_number(value: Any, decimal_places: int = 0) -> str:
    """Format a number with thousands separators.

    Example:
        {{ 1234567 | format_number }} → "1,234,567"
        {{ 1234.567 | format_number(2) }} → "1,234.57"

    """
    try:
        num = float(value)
        if decimal_places > 0:
            return f"{num:,.{decimal_places}f}"
        else:
            return f"{int(num):,}"
    except (ValueError, TypeError):
        return str(value)


def _filter_commas(value: Any) -> str:
    """Format a number with commas as thousands separators.

    Alias for format_number without decimal places.

    Example:
        {{ 1234567 | commas }} → "1,234,567"

    """
    return _filter_format_number(value, 0)


def _filter_classes(value: Any) -> str:
    """Join a list of CSS class names, dropping falsy values.

    Simplifies conditional class building in templates:

    Example:
        {{ ['card', 'active' if is_active, 'done' if todo.done] | classes }}
        → "card active"   (when is_active=True, todo.done=False)

    Handles:
    - None values (dropped)
    - Empty strings (dropped)
    - False / 0 (dropped)
    - Nested lists (flattened one level)

    """
    if value is None:
        return ""
    parts: list[str] = []
    try:
        items = list(value)
    except TypeError:
        return str(value)
    for item in items:
        if isinstance(item, (list, tuple)):
            parts.extend(str(sub) for sub in item if sub)
        elif item:
            parts.append(str(item))
    return " ".join(parts)


def _filter_decimal(value: Any, places: int = 2) -> str:
    """Format a number with a fixed number of decimal places.

    Example:
        {{ 3.1 | decimal }}     → "3.10"
        {{ 3.1 | decimal(1) }}  → "3.1"
        {{ 1234 | decimal(2) }} → "1234.00"

    """
    try:
        num = float(value)
        return f"{num:.{places}f}"
    except (ValueError, TypeError):
        return str(value)


def _filter_dictsort(
    value: dict[str, Any],
    case_sensitive: bool = False,
    by: str = "key",
    reverse: bool = False,
) -> list[tuple[str, Any]]:
    """Sort a dict and return list of (key, value) pairs."""
    if by == "value":

        def sort_key(item: tuple[str, Any]) -> Any:
            val = item[1]
            if not case_sensitive and isinstance(val, str):
                return val.lower()
            return val

    else:

        def sort_key(item: tuple[str, Any]) -> Any:
            val = item[0]
            if not case_sensitive and isinstance(val, str):
                return val.lower()
            return val

    return sorted(value.items(), key=sort_key, reverse=reverse)


def _filter_wordcount(value: str) -> int:
    """Count words in a string."""
    return len(str(value).split())


def _filter_float(value: Any, default: float = 0.0, strict: bool = False) -> float:
    """Convert value to float.

    Args:
        value: Value to convert to float.
        default: Default value to return if conversion fails (default: 0.0).
        strict: If True, raise TemplateRuntimeError on conversion failure
            instead of returning default (default: False).

    Returns:
        Float value, or default if conversion fails and strict=False.

    Raises:
        TemplateRuntimeError: If strict=True and conversion fails.

    Examples:
            >>> _filter_float("3.14")
        3.14
            >>> _filter_float("not a number")
        0.0
            >>> _filter_float("not a number", strict=True)
        TemplateRuntimeError: Cannot convert str to float: 'not a number'

    """
    try:
        return float(value)
    except (ValueError, TypeError) as e:
        if strict:
            raise TemplateRuntimeError(
                f"Cannot convert {type(value).__name__} to float: {value!r}",
                suggestion="Use | default(0.0) | float for optional conversion, or ensure value is numeric",
            ) from e
        return default


def _filter_filesizeformat(value: int | float, binary: bool = False) -> str:
    """Format a file size as human-readable."""
    bytes_val = float(value)
    base = 1024 if binary else 1000
    prefixes = [
        ("KiB" if binary else "kB", base),
        ("MiB" if binary else "MB", base**2),
        ("GiB" if binary else "GB", base**3),
        ("TiB" if binary else "TB", base**4),
    ]

    if bytes_val < base:
        return f"{int(bytes_val)} Bytes"

    for prefix, divisor in prefixes:
        if bytes_val < divisor * base:
            return f"{bytes_val / divisor:.1f} {prefix}"

    # Fallback to TB
    return f"{bytes_val / (base**4):.1f} {'TiB' if binary else 'TB'}"


def _filter_require(value: Any, message: str | None = None, field_name: str | None = None) -> Any:
    """Require a value to be non-None, raising a clear error if it is.

    Usage:
        {{ user.name | require('User name is required') }}
        {{ config.api_key | require(field_name='api_key') }}

    Args:
        value: The value to check
        message: Custom error message if value is None
        field_name: Field name for the default error message

    Returns:
        The value if not None

    Raises:
        RequiredValueError: If value is None

    """
    from kida.environment.exceptions import RequiredValueError

    if value is None:
        raise RequiredValueError(
            field_name=field_name or "value",
            message=message,
        )
    return value


def _filter_random(value: Any) -> Any:
    """Return a random item from the sequence.

    Warning: This filter is impure (non-deterministic).

    Args:
        value: A sequence to pick from.

    Returns:
        A random element from the sequence.

    """
    seq = list(value)
    if not seq:
        return None
    return random_module.choice(seq)


def _filter_shuffle(value: Any) -> list[Any]:
    """Return a shuffled copy of the sequence.

    Warning: This filter is impure (non-deterministic).

    Args:
        value: A sequence to shuffle.

    Returns:
        A new list with elements in random order.

    """
    result = list(value)
    random_module.shuffle(result)
    return result


def _filter_debug(value: Any, label: str | None = None, max_items: int = 5) -> Any:
    """Debug filter that prints variable info to stderr and returns the value unchanged.

    Usage:
        {{ posts | debug }}                    -> Shows type and length
        {{ posts | debug('my posts') }}        -> Shows with custom label
        {{ posts | debug(max_items=10) }}      -> Show more items

    Args:
        value: The value to inspect
        label: Optional label for the output
        max_items: Maximum number of items to show for sequences

    Returns:
        The value unchanged (for use in filter chains)

    Output example:
        DEBUG [my posts]: <list[5]>
          [0] Page(title='Getting Started', weight=10)
          [1] Page(title='Installation', weight=None)  <-- None!
              ...

    """
    import sys

    type_name = type(value).__name__
    label_str = f"[{label}]" if label else ""

    # Build output
    lines = []

    if value is None:
        lines.append(f"DEBUG {label_str}: None")
    elif isinstance(value, (list, tuple)):
        lines.append(f"DEBUG {label_str}: <{type_name}[{len(value)}]>")
        for idx, item in enumerate(value[:max_items]):
            item_repr = _debug_repr(item)
            # Flag None values prominently
            none_warning = ""
            if hasattr(item, "__dict__"):
                none_attrs = [
                    k for k, v in vars(item).items() if v is None and not k.startswith("_")
                ]
                if none_attrs:
                    none_warning = f"  <-- None: {', '.join(none_attrs[:3])}"
            lines.append(f"  [{idx}] {item_repr}{none_warning}")
        if len(value) > max_items:
            lines.append(f"  ... ({len(value) - max_items} more items)")
    elif isinstance(value, dict):
        lines.append(f"DEBUG {label_str}: <{type_name}[{len(value)} keys]>")
        for k, v in list(value.items())[:max_items]:
            v_repr = _debug_repr(v)
            none_warning = " <-- None!" if v is None else ""
            lines.append(f"  {k!r}: {v_repr}{none_warning}")
        if len(value) > max_items:
            lines.append(f"  ... ({len(value) - max_items} more keys)")
    elif hasattr(value, "__dict__"):
        # Object with attributes
        attrs = {k: v for k, v in vars(value).items() if not k.startswith("_")}
        lines.append(f"DEBUG {label_str}: <{type_name}>")
        for k, v in list(attrs.items())[:max_items]:
            v_repr = _debug_repr(v)
            none_warning = " <-- None!" if v is None else ""
            lines.append(f"  .{k} = {v_repr}{none_warning}")
        if len(attrs) > max_items:
            lines.append(f"  ... ({len(attrs) - max_items} more attributes)")
    else:
        lines.append(f"DEBUG {label_str}: {_debug_repr(value)} ({type_name})")

    # Print to stderr
    print("\n".join(lines), file=sys.stderr)

    # Return value unchanged for chaining
    return value


def _debug_repr(value: Any, max_len: int = 60) -> str:
    """Create a compact repr for debug output."""
    if value is None:
        return "None"

    type_name = type(value).__name__

    # Special handling for common types
    if hasattr(value, "title"):
        title = getattr(value, "title", None)
        weight = getattr(
            value,
            "weight",
            getattr(value, "metadata", {}).get("weight") if hasattr(value, "metadata") else None,
        )
        if title is not None:
            if weight is not None:
                return f"{type_name}(title={title!r}, weight={weight})"
            return f"{type_name}(title={title!r})"

    # Truncate long reprs
    r = repr(value)
    if len(r) > max_len:
        return r[: max_len - 3] + "..."
    return r


def _filter_date(value: Any, format: str = "%Y-%m-%d") -> str:
    """Format datetime, date, or epoch timestamp with strftime.

    Example:
        {{ dt | date }} → "2025-02-13"
        {{ dt | date("%b %d, %Y") }} → "Feb 13, 2025"
        {{ none | date }} → ""
    """
    if value is None:
        return ""
    if hasattr(value, "strftime"):
        return value.strftime(format)
    if isinstance(value, (int, float)):
        return datetime.fromtimestamp(value).strftime(format)
    return ""


def _filter_slug(value: Any) -> str:
    """Convert text to URL-safe slug (lowercase, hyphens, ASCII-only).

    Example:
        {{ "Hello World" | slug }} → "hello-world"
        {{ "  foo  bar  " | slug }} → "foo-bar"
        {{ none | slug }} → ""
    """
    if value is None:
        return ""
    s = str(value).lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    return s.strip("-")


def _filter_pluralize(value: Any, suffix: str = "s") -> str:
    """Django-style pluralization suffix.

    Example:
        {{ 1 | pluralize }} → ""
        {{ 2 | pluralize }} → "s"
        {{ 1 | pluralize("y,ies") }} → "y"
        {{ 2 | pluralize("y,ies") }} → "ies"
    """
    if value is None:
        return suffix
    n = int(value) if not isinstance(value, int) else value
    if n == 1:
        if "," in suffix:
            return suffix.split(",")[0].strip()
        return ""
    if "," in suffix:
        return suffix.split(",")[1].strip()
    return suffix


# Default filters - comprehensive set matching Jinja2
DEFAULT_FILTERS: dict[str, Callable[..., Any]] = {
    # Basic transformations
    "abs": _filter_abs,
    "capitalize": _filter_capitalize,
    "center": _filter_center,
    "d": _filter_default,
    "date": _filter_date,
    "default": _filter_default,
    "e": _filter_escape,
    "escape": _filter_escape,
    "first": _filter_first,
    "format": _filter_format,
    "indent": _filter_indent,
    "int": _filter_int,
    "join": _filter_join,
    "last": _filter_last,
    "length": _filter_length,
    "list": _filter_list,
    "lower": _filter_lower,
    "pluralize": _filter_pluralize,
    "pprint": _filter_pprint,
    "replace": _filter_replace,
    "reverse": _filter_reverse,
    "safe": _filter_safe,
    "sort": _filter_sort,
    "string": _filter_string,
    "striptags": _filter_striptags,
    "title": _filter_title,
    "trim": _filter_trim,
    "truncate": _filter_truncate,
    "upper": _filter_upper,
    "urlencode": _filter_urlencode,
    "wordwrap": _filter_wordwrap,
    "xmlattr": _filter_xmlattr,
    # Serialization
    "tojson": _filter_tojson,
    # Collections
    "attr": _filter_attr,
    "batch": _filter_batch,
    "groupby": _filter_groupby,
    "map": _filter_map,
    "max": _filter_max,
    "min": _filter_min,
    "reject": _filter_reject,
    "rejectattr": _filter_rejectattr,
    "select": _filter_select,
    "selectattr": _filter_selectattr,
    "skip": _filter_skip,
    "slice": _filter_slice,
    "slug": _filter_slug,
    "sum": _filter_sum,
    "take": _filter_take,
    "unique": _filter_unique,
    "classes": _filter_classes,
    "compact": _filter_compact,
    # Additional filters
    "count": _filter_length,  # alias
    "decimal": _filter_decimal,
    "dictsort": _filter_dictsort,
    "filesizeformat": _filter_filesizeformat,
    "float": _filter_float,
    "round": _filter_round,
    "strip": _filter_trim,  # alias
    "wordcount": _filter_wordcount,
    "format_number": _filter_format_number,
    "commas": _filter_commas,
    # Debugging and validation filters
    "require": _filter_require,
    "debug": _filter_debug,
    # Safe access filter (avoids Python method name conflicts)
    "get": _filter_get,
    # Randomization filters (impure - non-deterministic)
    "random": _filter_random,
    "shuffle": _filter_shuffle,
}
