"""Constraint marker classes for Pydantic Field validation.

This module provides type-safe constraint markers that can be used in type annotations
with the Hyper type system. Each constraint class uses __class_getitem__ to create
runtime constraint objects (from annotated_types) while appearing as valid type
expressions to type checkers.

Example:
  ```python
  from nonfig.constraints import Ge, Le
  from nonfig import Hyper

  def my_function(
    period: Hyper[int, Ge[2], Le[100]] = 14,
    threshold: Hyper[float, Ge[0.0], Le[1.0]] = 0.5,
  ) -> float:
    return period * threshold
  ```
"""

# pyright: reportUnknownMemberType=false

import re
from typing import Any, Literal, cast, get_args, get_origin

from annotated_types import (
  Ge as _Ge,
  Gt as _Gt,
  Le as _Le,
  Lt as _Lt,
  MaxLen as _MaxLen,
  MinLen as _MinLen,
  MultipleOf as _MultipleOf,
)

__all__ = [
  "Ge",
  "Gt",
  "InvalidPatternError",
  "Le",
  "Lt",
  "MaxLen",
  "MinLen",
  "MultipleOf",
  "Pattern",
  "PatternConstraint",
  "validate_constraint_conflicts",
]


class InvalidPatternError(ValueError):
  """Raised when a regex pattern is invalid."""

  pass


def _check_numeric(value: Any, name: str) -> None:
  """Validate that a constraint value is numeric."""
  if not isinstance(value, int | float):
    raise TypeError(f"{name} requires a numeric value, got {type(value).__name__}")


def _check_int(value: Any, name: str) -> None:
  """Validate that a constraint value is an integer."""
  if not isinstance(value, int):
    raise TypeError(f"{name} requires an integer value, got {type(value).__name__}")


def _check_non_negative_int(value: Any, name: str) -> None:
  """Validate that a constraint value is a non-negative integer."""
  _check_int(value, name)
  if value < 0:
    raise ValueError(f"{name} requires a non-negative integer, got {value}")


class Ge:
  """Greater than or equal constraint marker (for numbers).

  Usage: Ge[value] in type annotations creates a constraint that the
  runtime value must be >= the specified value.

  Example:
    ```python
    age: Hyper[int, Ge[0]] = 25  # age must be >= 0
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _Ge:
    """Support Ge[2] syntax."""
    _check_numeric(value, "Ge")
    return _Ge(value)


class Gt:
  """Greater than constraint marker (for numbers).

  Usage: Gt[value] in type annotations creates a constraint that the
  runtime value must be > the specified value (strict inequality).

  Example:
    ```python
    epsilon: Hyper[float, Gt[0.0]] = 1e-9  # epsilon must be > 0
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _Gt:
    """Support Gt[0] syntax."""
    _check_numeric(value, "Gt")
    return _Gt(value)


class Le:
  """Less than or equal constraint marker (for numbers).

  Usage: Le[value] in type annotations creates a constraint that the
  runtime value must be <= the specified value.

  Example:
    ```python
    percentage: Hyper[float, Ge[0.0], Le[100.0]] = 50.0
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _Le:
    """Support Le[100] syntax."""
    _check_numeric(value, "Le")
    return _Le(value)


class Lt:
  """Less than constraint marker (for numbers).

  Usage: Lt[value] in type annotations creates a constraint that the
  runtime value must be < the specified value (strict inequality).

  Example:
    ```python
    probability: Hyper[float, Ge[0.0], Lt[1.0]] = 0.5
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _Lt:
    """Support Lt[10] syntax."""
    _check_numeric(value, "Lt")
    return _Lt(value)


class MinLen:
  """Minimum length constraint marker (for strings, lists, etc.).

  Usage: MinLen[value] in type annotations creates a constraint that the
  runtime value's length must be >= the specified value.

  Example:
    ```python
    name: Hyper[str, MinLen[1]] = "default"
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _MinLen:
    """Support MinLen[5] syntax."""
    _check_non_negative_int(value, "MinLen")
    return _MinLen(value)


class MaxLen:
  """Maximum length constraint marker (for strings, lists, etc.).

  Usage: MaxLen[value] in type annotations creates a constraint that the
  runtime value's length must be <= the specified value.

  Example:
    ```python
    name: Hyper[str, MinLen[1], MaxLen[100]] = "default"
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _MaxLen:
    """Support MaxLen[100] syntax."""
    _check_non_negative_int(value, "MaxLen")
    return _MaxLen(value)


class MultipleOf:
  """Multiple of constraint marker (for numbers).

  Usage: MultipleOf[value] in type annotations creates a constraint that the
  runtime value must be a multiple of the specified value.

  Example:
    ```python
    batch_size: Hyper[int, Ge[1], MultipleOf[8]] = 32
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, value: Any) -> _MultipleOf:
    """Support MultipleOf[5] syntax."""
    _check_numeric(value, "MultipleOf")
    if value <= 0:
      raise ValueError(f"MultipleOf requires a positive value, got {value}")
    return _MultipleOf(value)


class PatternConstraint:
  """Represents a compiled regex pattern constraint."""

  __slots__ = ("compiled",)

  def __init__(self, compiled: re.Pattern[str]) -> None:
    self.compiled = compiled

  @property
  def pattern(self) -> str:
    """Return the original pattern string."""
    return self.compiled.pattern


class Pattern:
  """Regex pattern constraint marker (for strings).

  Usage: Pattern[regex] in type annotations creates a constraint that the
  runtime string value must match the specified regex pattern.

  Example:
    ```python
    code: Hyper[str, Pattern[r"^[A-Z]{3}$"]] = "USD"
    ```
  """

  __slots__ = ()

  def __class_getitem__(cls, pattern: Any) -> PatternConstraint:
    """Support Pattern[r'^[a-z]+$'] syntax."""
    # Handle Literal types at runtime (e.g., Pattern[Literal["..."]])
    # Needed because annotations like Hyper[str, Pattern[Literal["..."]]] pass
    # the Literal type object to __class_getitem__ at runtime
    if get_origin(pattern) is Literal:
      args = get_args(pattern)
      if args and isinstance(args[0], str):
        pattern = args[0]

    if not isinstance(pattern, str):
      raise TypeError(f"Pattern expects a string, got {type(pattern).__name__}")

    # Validate and compile regex at decoration time
    try:
      # Explicit cast to Any needed to avoid "unnecessary cast" in CI (where type is correct)
      # while handling "Unknown" return type in local env (Python 3.13)
      compiled: re.Pattern[str] = cast("Any", re.compile(pattern))
    except re.error as e:
      raise InvalidPatternError(f"Invalid regex pattern: {e}") from e
    return PatternConstraint(compiled)


def _validate_numeric_bounds(
  resolved: dict[str, Any], param_name: str, context: str
) -> None:
  """Check for logical conflicts in numeric bounds."""
  lower_bound = None
  upper_bound = None
  lower_exclusive = False
  upper_exclusive = False

  if "ge" in resolved:
    lower_bound = resolved["ge"]
    lower_exclusive = False
  if "gt" in resolved:
    gt_val = resolved["gt"]
    if lower_bound is not None:
      if gt_val >= lower_bound:
        lower_bound = gt_val
        lower_exclusive = True
    else:
      lower_bound = gt_val
      lower_exclusive = True

  if "le" in resolved:
    upper_bound = resolved["le"]
    upper_exclusive = False
  if "lt" in resolved:
    lt_val = resolved["lt"]
    if upper_bound is not None:
      if lt_val <= upper_bound:
        upper_bound = lt_val
        upper_exclusive = True
    else:
      upper_bound = lt_val
      upper_exclusive = True

  if lower_bound is not None and upper_bound is not None:
    if lower_bound > upper_bound:
      raise ValueError(
        f"Conflicting constraints for '{param_name}'{context}: lower bound ({lower_bound}) > upper bound ({upper_bound})"
      )
    if lower_bound == upper_bound and (lower_exclusive or upper_exclusive):
      raise ValueError(
        f"Conflicting constraints for '{param_name}'{context}: bounds equal at {lower_bound} but one is exclusive"
      )


def _validate_length_bounds(
  resolved: dict[str, Any], param_name: str, context: str
) -> None:
  """Check for logical conflicts in length bounds."""
  if (
    "min_length" in resolved
    and "max_length" in resolved
    and resolved["min_length"] > resolved["max_length"]
  ):
    raise ValueError(
      f"Conflicting constraints for '{param_name}'{context}: min_length ({resolved['min_length']}) > max_length ({resolved['max_length']})"
    )


def validate_constraint_conflicts(
  constraints: dict[str, Any],
  param_name: str,
  func_name: str | None = None,
) -> dict[str, Any]:
  """
  Resolve multiple constraints of the same type and validate for conflicts.

  Args:
    constraints: Dictionary of constraint names to values or lists of values.
    param_name: Name of the parameter (for error messages).
    func_name: Optional function/class name (for richer error messages).

  Returns:
    Dictionary of resolved single-value constraints compatible with Pydantic Field.

  Raises:
    ValueError: If impossible constraints are detected.
  """
  # Context for error messages
  context = f" (in '{func_name}')" if func_name else ""

  # 1. Resolve same-type constraints (e.g., Ge[5], Ge[10] -> Ge[10])
  resolved: dict[str, Any] = {}
  for key, val in constraints.items():
    if isinstance(val, list):
      if not val:
        continue
      if key in {"ge", "gt", "min_length"}:
        resolved[key] = max(cast("list[Any]", val))
      elif key in {"le", "lt", "max_length"}:
        resolved[key] = min(cast("list[Any]", val))
      elif key == "multiple_of":
        resolved[key] = val[-1]
      else:
        resolved[key] = val[-1]
    else:
      resolved[key] = val

  # 2. Check numeric bound conflicts
  _validate_numeric_bounds(resolved, param_name, context)

  # 3. Check length bound conflicts
  _validate_length_bounds(resolved, param_name, context)

  return resolved
