"""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:
    from hipr.constraints import Ge, Le
    from hipr 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
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Literal, get_args, get_origin

if TYPE_CHECKING:
  from collections.abc import Mapping

from annotated_types import (
  Ge as AtGe,
  Gt as AtGt,
  Le as AtLe,
  Lt as AtLt,
  MaxLen as AtMaxLen,
  MinLen as AtMinLen,
  MultipleOf as AtMultipleOf,
)


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

  pass


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


def validate_constraint_conflicts(
  constraints: Mapping[str, object],
  param_name: str,
  func_name: str | None = None,
) -> None:
  """Validate that constraints don't conflict with each other.

  Args:
    constraints: Dictionary of constraints
    param_name: Name of the parameter (for error messages)
    func_name: Optional name of the function/class (for richer error messages)

  Raises:
    ValueError: If constraints are contradictory
  """
  # Check numeric bound conflicts
  ge_val = constraints.get("ge")
  gt_val = constraints.get("gt")
  le_val = constraints.get("le")
  lt_val = constraints.get("lt")

  # Determine effective lower and upper bounds
  lower_bound: float | int | None = None
  lower_inclusive = True
  if ge_val is not None and isinstance(ge_val, (int, float)):
    lower_bound = ge_val
    lower_inclusive = True
  if (
    gt_val is not None
    and isinstance(gt_val, (int, float))
    and (lower_bound is None or gt_val >= lower_bound)
  ):
    lower_bound = gt_val
    lower_inclusive = False

  upper_bound: float | int | None = None
  upper_inclusive = True
  if le_val is not None and isinstance(le_val, (int, float)):
    upper_bound = le_val
    upper_inclusive = True
  if (
    lt_val is not None
    and isinstance(lt_val, (int, float))
    and (upper_bound is None or lt_val <= upper_bound)
  ):
    upper_bound = lt_val
    upper_inclusive = False

  context = f"In function '{func_name}', parameter" if func_name else "Parameter"

  # Check if bounds are impossible
  if lower_bound is not None and upper_bound is not None:
    if lower_bound > upper_bound:
      msg = (
        f"{context} '{param_name}' has contradictory constraints: "
        f"lower bound ({lower_bound}) is greater than upper bound ({upper_bound})"
      )
      raise ValueError(msg)
    if lower_bound == upper_bound and not (lower_inclusive and upper_inclusive):
      msg = (
        f"{context} '{param_name}' has contradictory constraints: "
        f"no value can satisfy both strict and non-strict bounds at {lower_bound}"
      )
      raise ValueError(msg)

  # Check length constraint conflicts
  min_len = constraints.get("min_length")
  max_len = constraints.get("max_length")

  if (
    min_len is not None
    and max_len is not None
    and isinstance(min_len, int)
    and isinstance(max_len, int)
    and min_len > max_len
  ):
    msg = (
      f"{context} '{param_name}' has contradictory constraints: "
      f"min_length ({min_len}) is greater than max_length ({max_len})"
    )
    raise ValueError(msg)


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:
      age: Hyper[int, Ge[0]] = 25  # age must be >= 0
  """

  def __class_getitem__(cls, value: object) -> AtGe:
    """Support Ge[2] syntax."""
    if not isinstance(value, (int, float)):
      msg = f"Ge constraint requires a numeric value, got {type(value).__name__}"
      raise TypeError(msg)
    return AtGe(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:
      percentage: Hyper[float, Ge[0.0], Le[100.0]] = 50.0
  """

  def __class_getitem__(cls, value: object) -> AtLe:
    """Support Le[100] syntax."""
    if not isinstance(value, (int, float)):
      msg = f"Le constraint requires a numeric value, got {type(value).__name__}"
      raise TypeError(msg)
    return AtLe(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:
      epsilon: Hyper[float, Gt[0.0]] = 1e-9  # epsilon must be > 0
  """

  def __class_getitem__(cls, value: object) -> AtGt:
    """Support Gt[0] syntax."""
    if not isinstance(value, (int, float)):
      msg = f"Gt constraint requires a numeric value, got {type(value).__name__}"
      raise TypeError(msg)
    return AtGt(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:
      probability: Hyper[float, Ge[0.0], Lt[1.0]] = 0.5
  """

  def __class_getitem__(cls, value: object) -> AtLt:
    """Support Lt[10] syntax."""
    if not isinstance(value, (int, float)):
      msg = f"Lt constraint requires a numeric value, got {type(value).__name__}"
      raise TypeError(msg)
    return AtLt(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:
      name: Hyper[str, MinLen[1]] = "default"
  """

  def __class_getitem__(cls, value: object) -> AtMinLen:
    """Support MinLen[5] syntax."""
    if not isinstance(value, int):
      msg = f"MinLen constraint requires an integer, got {type(value).__name__}"
      raise TypeError(msg)
    if value < 0:
      msg = f"MinLen constraint requires a non-negative integer, got {value}"
      raise ValueError(msg)
    return AtMinLen(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:
      name: Hyper[str, MinLen[1], MaxLen[100]] = "default"
  """

  def __class_getitem__(cls, value: object) -> AtMaxLen:
    """Support MaxLen[100] syntax."""
    if not isinstance(value, int):
      msg = f"MaxLen constraint requires an integer, got {type(value).__name__}"
      raise TypeError(msg)
    if value < 0:
      msg = f"MaxLen constraint requires a non-negative integer, got {value}"
      raise ValueError(msg)
    return AtMaxLen(value)


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:
      code: Hyper[str, Pattern[r'^[A-Z]{3}$']] = "USD"

  Note: This constraint uses a tuple format ("pattern", regex) rather than
  an annotated_types object because annotated_types doesn't have a direct
  Pattern equivalent. Pydantic understands the "pattern" field constraint
  directly. This approach is consistent with Pydantic v2's field validation.
  """

  def __class_getitem__(cls, regex: object) -> tuple[str, str]:
    """Support Pattern[r'^[a-z]+$'] syntax.

    Returns a tuple for Pydantic field validation.

    Raises:
      TypeError: If regex is not a string
      InvalidPatternError: If regex pattern is invalid
    """
    # Handle Literal["pattern"]
    if get_origin(regex) is Literal:
      args = get_args(regex)
      if len(args) == 1 and isinstance(args[0], str):
        regex = args[0]

    if not isinstance(regex, str):
      msg = f"Pattern constraint requires a string, got {type(regex).__name__}"
      raise TypeError(msg)

    # Validate regex pattern
    try:
      re.compile(regex)
    except re.error as e:
      msg = f"Invalid regex pattern '{regex}': {e}"
      raise InvalidPatternError(msg) from e

    return ("pattern", regex)


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:
      batch_size: Hyper[int, Ge[1], MultipleOf[8]] = 32
  """

  def __class_getitem__(cls, value: object) -> AtMultipleOf:
    """Support MultipleOf[5] syntax."""
    if not isinstance(value, (int, float)):
      type_name = type(value).__name__
      msg = f"MultipleOf constraint requires a numeric value, got {type_name}"
      raise TypeError(msg)
    if value <= 0:
      msg = f"MultipleOf constraint requires a positive value, got {value}"
      raise ValueError(msg)
    return AtMultipleOf(value)
