"""
Predicate helpers: translate tuple filters to ``pyarrow.compute.Expression`` for
dataset pushdown, and apply equivalent logic in pandas for CSV / Parquet fallbacks.

Tuple format: ``(column_name, operator, value)``.

Supported operators:
  ``==``, ``!=``, ``>``, ``>=``, ``<``, ``<=``,
  ``in`` (value is a non-string sequence),
  ``not_in`` (value is a non-string sequence).
"""

from __future__ import annotations

from typing import Any, List, Optional, Sequence, Tuple

import pandas as pd
import pyarrow as pa
import pyarrow.compute as pc

from snapparquet.schema import norm_col

FilterTuple = Tuple[str, str, Any]
FilterSpec = Sequence[FilterTuple]

# Defensive limits for untrusted filter specs (column identifiers).
_MAX_FILTER_COLUMN_LEN = 512


def _validate_filter_column_identifier(raw: str) -> str:
    """Normalize and reject pathological column names (length, control characters)."""
    name = norm_col(str(raw))
    if not name:
        raise ValueError("Filter column name must be non-empty.")
    if len(name) > _MAX_FILTER_COLUMN_LEN:
        raise ValueError(
            "Filter column name exceeds maximum length ({}).".format(_MAX_FILTER_COLUMN_LEN)
        )
    if any(ord(c) < 32 for c in name):
        raise ValueError("Filter column name contains disallowed control characters.")
    return name


def build_filter(filters: Optional[FilterSpec]) -> Any:
    """
    Convert *filters* into one AND-combined ``pyarrow.compute.Expression``.

    Returns ``None`` if *filters* is ``None`` or empty.

    Raises
    ------
    ValueError
        Invalid tuple shape, unknown operator, or unsafe / invalid column name.
    """
    if not filters:
        return None

    parts: List[Any] = []
    for raw in filters:
        if not isinstance(raw, (tuple, list)) or len(raw) != 3:
            raise ValueError(
                "Each filter must be a 3-tuple (column, operator, value), got {!r}".format(
                    raw
                )
            )
        col, op, val = raw[0], raw[1], raw[2]
        name = _validate_filter_column_identifier(str(col))
        op_s = str(op).strip().lower()

        fld = pc.field(name)

        if op_s == "==":
            parts.append(fld == val)
        elif op_s == "!=":
            parts.append(fld != val)
        elif op_s == ">":
            parts.append(fld > val)
        elif op_s == ">=":
            parts.append(fld >= val)
        elif op_s == "<":
            parts.append(fld < val)
        elif op_s == "<=":
            parts.append(fld <= val)
        elif op_s == "in":
            if val is None or not hasattr(val, "__iter__") or isinstance(val, (str, bytes)):
                raise ValueError(
                    '"in" filter value must be a non-string sequence, got {!r}'.format(
                        type(val).__name__
                    )
                )
            parts.append(pc.is_in(fld, pa.array(list(val))))
        elif op_s == "not_in":
            if val is None or not hasattr(val, "__iter__") or isinstance(val, (str, bytes)):
                raise ValueError(
                    '"not_in" filter value must be a non-string sequence, got {!r}'.format(
                        type(val).__name__
                    )
                )
            parts.append(~pc.is_in(fld, pa.array(list(val))))
        else:
            raise ValueError(
                "Unsupported filter operator {!r} (column {!r})".format(op, name)
            )

    if len(parts) == 1:
        return parts[0]

    out: Any = parts[0]
    for p in parts[1:]:
        out = pc.and_(out, p)
    return out


def apply_filters_pandas(df: pd.DataFrame, filters: Optional[FilterSpec]) -> pd.DataFrame:
    """
    Apply the same semantics as :func:`build_filter` on an in-memory DataFrame.

    Raises
    ------
    ValueError
        Unknown operator, missing referenced column, or invalid filter row.
    """
    if df is None or len(df) == 0 or not filters:
        return df

    missing: List[str] = []
    for raw in filters:
        if not isinstance(raw, (tuple, list)) or len(raw) != 3:
            raise ValueError(
                "Each filter must be a 3-tuple (column, operator, value), got {!r}".format(
                    raw
                )
            )
        name = _validate_filter_column_identifier(str(raw[0]))
        if name not in df.columns:
            missing.append(name)

    if missing:
        raise ValueError(
            "Filter references unknown column(s): {} (available: {})".format(
                ", ".join(sorted(set(missing))),
                ", ".join(sorted(df.columns.astype(str).tolist())[:50])
                + ("…" if len(df.columns) > 50 else ""),
            )
        )

    out = df
    for raw in filters:
        col, op, val = raw[0], raw[1], raw[2]
        name = _validate_filter_column_identifier(str(col))
        op_s = str(op).strip().lower()
        s = out[name]

        if op_s == "==":
            out = out[s == val]
        elif op_s == "!=":
            out = out[s != val]
        elif op_s == ">":
            out = out[pd.to_numeric(s, errors="coerce") > val]
        elif op_s == ">=":
            out = out[pd.to_numeric(s, errors="coerce") >= val]
        elif op_s == "<":
            out = out[pd.to_numeric(s, errors="coerce") < val]
        elif op_s == "<=":
            out = out[pd.to_numeric(s, errors="coerce") <= val]
        elif op_s == "in":
            if val is None or not hasattr(val, "__iter__") or isinstance(val, (str, bytes)):
                raise ValueError(
                    '"in" value must be a non-string sequence, got {!r}'.format(type(val).__name__)
                )
            out = out[s.isin(list(val))]
        elif op_s == "not_in":
            if val is None or not hasattr(val, "__iter__") or isinstance(val, (str, bytes)):
                raise ValueError(
                    '"not_in" value must be a non-string sequence, got {!r}'.format(
                        type(val).__name__
                    )
                )
            out = out[~s.isin(list(val))]
        else:
            raise ValueError("Unsupported filter operator {!r}".format(op_s))

    return out
