"""Simulation validation utilities for scientific computing.

This module provides helper functions for validating simulation constraints:
conservation laws, numerical stability, convergence, physical bounds, and
CFL conditions.

Author: Generated by AgentBible
"""

from typing import Optional, Union

import numpy as np
from numpy.typing import NDArray


def check_conservation(
    quantity_history: NDArray[np.floating],
    rtol: float = 1e-6,
    atol: float = 1e-10,
    name: Optional[str] = None,
) -> None:
    """Validate that a conserved quantity remains constant.

    Args:
        quantity_history: Time series of conserved quantity values.
        rtol: Relative tolerance for conservation check.
        atol: Absolute tolerance for conservation check.
        name: Optional name for error messages (e.g., "energy", "mass").

    Raises:
        ValueError: If conservation is violated beyond tolerance.

    Example:
        >>> energies = [100.0, 99.99, 100.01, 100.0]
        >>> check_conservation(energies, rtol=1e-3)  # Passes
    """
    label = f" '{name}'" if name else ""
    quantity_history = np.asarray(quantity_history)

    if len(quantity_history) < 2:
        return  # Nothing to check

    initial = quantity_history[0]
    if np.abs(initial) < atol:
        # Use absolute tolerance for near-zero values
        max_deviation = np.max(np.abs(quantity_history - initial))
        if max_deviation > atol:
            raise ValueError(
                f"Conservation{label} violated: max deviation = {max_deviation:.2e} "
                f"(atol: {atol:.2e})"
            )
    else:
        # Use relative tolerance
        relative_change = np.abs((quantity_history - initial) / initial)
        max_rel_change = np.max(relative_change)
        if max_rel_change > rtol:
            raise ValueError(
                f"Conservation{label} violated: max relative change = {max_rel_change:.2e} "
                f"(rtol: {rtol:.2e})"
            )


def check_stability(
    data: NDArray,
    max_value: float = 1e30,
    name: Optional[str] = None,
) -> None:
    """Validate numerical stability (no NaN, Inf, or blowup).

    Args:
        data: Array to check for stability.
        max_value: Maximum allowed absolute value.
        name: Optional name for error messages.

    Raises:
        ValueError: If data contains NaN, Inf, or exceeds max_value.
    """
    label = f" '{name}'" if name else ""

    nan_count = np.sum(np.isnan(data))
    if nan_count > 0:
        raise ValueError(
            f"Numerical instability{label}: {nan_count} NaN values detected"
        )

    inf_count = np.sum(np.isinf(data))
    if inf_count > 0:
        raise ValueError(
            f"Numerical instability{label}: {inf_count} Inf values detected"
        )

    max_abs = np.max(np.abs(data))
    if max_abs > max_value:
        raise ValueError(
            f"Numerical blowup{label}: max|value| = {max_abs:.2e} exceeds {max_value:.2e}"
        )


def check_convergence(
    residuals: NDArray[np.floating],
    tol: float = 1e-10,
    name: Optional[str] = None,
) -> None:
    """Validate that iterative solution has converged.

    Args:
        residuals: Array of residual values (should decrease).
        tol: Tolerance for convergence.
        name: Optional name for error messages.

    Raises:
        ValueError: If final residual exceeds tolerance.
    """
    label = f" '{name}'" if name else ""
    residuals = np.asarray(residuals)

    if len(residuals) == 0:
        raise ValueError(f"No residuals{label} provided for convergence check")

    final_residual = residuals[-1]
    if final_residual > tol:
        raise ValueError(
            f"Solution{label} did not converge: final residual = {final_residual:.2e} "
            f"(tol: {tol:.2e})"
        )


def check_physical_bounds(
    data: NDArray[np.floating],
    min_val: Optional[float] = None,
    max_val: Optional[float] = None,
    name: Optional[str] = None,
) -> None:
    """Validate that values fall within physical bounds.

    Args:
        data: Array of values to check.
        min_val: Minimum allowed value (e.g., 0 for density).
        max_val: Maximum allowed value.
        name: Optional name for error messages (e.g., "density", "temperature").

    Raises:
        ValueError: If any values are outside bounds.
    """
    label = f" '{name}'" if name else ""

    if min_val is not None:
        actual_min = np.min(data)
        if actual_min < min_val:
            n_violations = np.sum(data < min_val)
            raise ValueError(
                f"Physical bound{label} violated: {n_violations} values below "
                f"minimum {min_val} (min found: {actual_min:.4e})"
            )

    if max_val is not None:
        actual_max = np.max(data)
        if actual_max > max_val:
            n_violations = np.sum(data > max_val)
            raise ValueError(
                f"Physical bound{label} violated: {n_violations} values above "
                f"maximum {max_val} (max found: {actual_max:.4e})"
            )


def check_cfl_condition(
    dt: float,
    dx: float,
    velocity: Union[float, NDArray[np.floating]],
    cfl_max: float = 1.0,
    name: Optional[str] = None,
) -> float:
    """Validate CFL (Courant-Friedrichs-Lewy) condition for stability.

    CFL number = max(|velocity|) * dt / dx

    Args:
        dt: Time step.
        dx: Spatial step (minimum if non-uniform).
        velocity: Velocity or array of velocities.
        cfl_max: Maximum allowed CFL number (usually <= 1).
        name: Optional name for error messages.

    Returns:
        The computed CFL number.

    Raises:
        ValueError: If CFL condition is violated.
    """
    label = f" '{name}'" if name else ""

    max_velocity = np.max(np.abs(velocity))
    cfl = max_velocity * dt / dx

    if cfl > cfl_max:
        raise ValueError(
            f"CFL condition{label} violated: CFL = {cfl:.4f} > {cfl_max}. "
            f"Reduce dt to <= {cfl_max * dx / max_velocity:.4e}"
        )

    return float(cfl)


def check_time_step(
    dt: float,
    t_final: float,
    min_dt: float = 1e-15,
    name: Optional[str] = None,
) -> None:
    """Validate time step parameters.

    Args:
        dt: Time step size.
        t_final: Final simulation time.
        min_dt: Minimum allowed time step.
        name: Optional name for error messages.

    Raises:
        ValueError: If time step parameters are invalid.
    """
    label = f" '{name}'" if name else ""

    if dt <= 0:
        raise ValueError(f"Time step{label} must be positive, got {dt}")

    if dt < min_dt:
        raise ValueError(
            f"Time step{label} {dt:.2e} below minimum {min_dt:.2e}. "
            "This may indicate numerical issues."
        )

    if dt > t_final:
        raise ValueError(f"Time step{label} {dt} exceeds final time {t_final}")

    n_steps = int(np.ceil(t_final / dt))
    if n_steps > 1e9:
        raise ValueError(
            f"Time step{label} would require {n_steps:.2e} steps. "
            "Consider using a larger dt or adaptive stepping."
        )


def check_grid_spacing(
    grid: NDArray[np.floating],
    min_ratio: float = 0.1,
    max_ratio: float = 10.0,
    name: Optional[str] = None,
) -> None:
    """Validate grid spacing regularity.

    Args:
        grid: 1D array of grid point positions.
        min_ratio: Minimum allowed ratio between adjacent spacings.
        max_ratio: Maximum allowed ratio between adjacent spacings.
        name: Optional name for error messages.

    Raises:
        ValueError: If grid spacing varies too much.
    """
    label = f" '{name}'" if name else ""
    grid = np.asarray(grid)

    if len(grid) < 3:
        return  # Not enough points to check ratio

    spacings = np.diff(grid)

    if np.any(spacings <= 0):
        raise ValueError(f"Grid{label} must be strictly increasing")

    ratios = spacings[1:] / spacings[:-1]
    min_r = np.min(ratios)
    max_r = np.max(ratios)

    if min_r < min_ratio:
        raise ValueError(
            f"Grid{label} spacing ratio {min_r:.4f} below minimum {min_ratio}. "
            "Grid is too irregular."
        )

    if max_r > max_ratio:
        raise ValueError(
            f"Grid{label} spacing ratio {max_r:.4f} above maximum {max_ratio}. "
            "Grid is too irregular."
        )
