"""Independent H3/H4 metrics reconstructed from immutable raw logs."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np


@dataclass(frozen=True)
class EnergyAuditResult:
    energy_before: np.ndarray
    energy_preclip: np.ndarray
    energy_after: np.ndarray
    floor_deficit: np.ndarray
    projected_shadow_energy: np.ndarray
    candidate_shadow_energy: np.ndarray
    candidate_power: np.ndarray
    accepted_power: np.ndarray
    max_floor_deficit: float
    projected_floor_deficit: float
    candidate_floor_deficit: float
    delta_B: float
    projection_distortion: float
    released_energy: float
    preclip_log_max_error: float | None
    downstream_modification_max: float

    @property
    def passed_floor_gate(self) -> bool:
        return self.max_floor_deficit <= 1e-12


def _sample_matrix(value, name: str) -> np.ndarray:
    array = np.asarray(value, dtype=float)
    if array.ndim != 2 or array.shape[0] == 0 or array.shape[1] == 0:
        raise ValueError(f"{name} must be a non-empty 2-D array")
    if not np.all(np.isfinite(array)):
        raise ValueError(f"{name} must contain only finite values")
    return array


def audit_haptic_energy(
    *,
    tau_candidate: np.ndarray,
    tau_projected: np.ndarray,
    tau_accepted: np.ndarray,
    qd_master: np.ndarray,
    dt: float | np.ndarray,
    energy_initial: float,
    energy_min: float,
    energy_max: float,
    epsilon_tau: float = 1e-12,
    logged_preclip: np.ndarray | None = None,
) -> EnergyAuditResult:
    """Recompute the H4 budget and transparency endpoints.

    The deterministic budget gate uses ``tau_accepted`` because this is the
    actual actuator-port increment.  The same-run shadow uses the logged
    pre-projection candidate and never feeds the counterfactual back into the
    simulated trajectory.
    """
    candidate = _sample_matrix(tau_candidate, "tau_candidate")
    projected = _sample_matrix(tau_projected, "tau_projected")
    accepted = _sample_matrix(tau_accepted, "tau_accepted")
    velocity = _sample_matrix(qd_master, "qd_master")
    if not (
        candidate.shape == projected.shape == accepted.shape == velocity.shape
    ):
        raise ValueError("all torque and velocity arrays must have equal shapes")
    count = candidate.shape[0]
    delta = np.broadcast_to(np.asarray(dt, dtype=float), (count,)).copy()
    if np.any(delta <= 0.0) or not np.all(np.isfinite(delta)):
        raise ValueError("dt must contain finite positive values")
    if not (
        np.isfinite(energy_initial)
        and np.isfinite(energy_min)
        and np.isfinite(energy_max)
        and energy_min <= energy_initial <= energy_max
    ):
        raise ValueError("energy values must satisfy min <= initial <= max")
    if epsilon_tau <= 0.0:
        raise ValueError("epsilon_tau must be positive")

    candidate_power = np.einsum("ij,ij->i", candidate, velocity)
    accepted_power = np.einsum("ij,ij->i", accepted, velocity)
    energy_before = np.empty(count, dtype=float)
    energy_preclip = np.empty(count, dtype=float)
    energy_after = np.empty(count, dtype=float)
    floor_deficit = np.empty(count, dtype=float)
    energy = float(energy_initial)
    for index in range(count):
        energy_before[index] = energy
        preclip = energy - accepted_power[index] * delta[index]
        energy_preclip[index] = preclip
        floor_deficit[index] = max(0.0, energy_min - preclip)
        energy = float(np.clip(preclip, energy_min, energy_max))
        energy_after[index] = energy

    candidate_shadow = np.empty(count + 1, dtype=float)
    projected_shadow = np.empty(count + 1, dtype=float)
    candidate_shadow[0] = energy_initial
    projected_shadow[0] = energy_initial
    for index in range(count):
        candidate_shadow[index + 1] = min(
            energy_max,
            candidate_shadow[index] - candidate_power[index] * delta[index],
        )
        projected_shadow[index + 1] = min(
            energy_max,
            projected_shadow[index] - accepted_power[index] * delta[index],
        )
    candidate_B = float(
        np.max(np.maximum(0.0, energy_min - candidate_shadow))
    )
    projected_B = float(
        np.max(np.maximum(0.0, energy_min - projected_shadow))
    )
    numerator = float(
        np.sum(np.linalg.norm(accepted - candidate, axis=1) * delta)
    )
    denominator = float(
        np.sum(np.linalg.norm(candidate, axis=1) * delta) + epsilon_tau
    )

    preclip_error = None
    if logged_preclip is not None:
        logged = np.asarray(logged_preclip, dtype=float).reshape(-1)
        if logged.shape != (count,) or not np.all(np.isfinite(logged)):
            raise ValueError("logged_preclip must be finite with one value per sample")
        preclip_error = float(np.max(np.abs(logged - energy_preclip)))

    return EnergyAuditResult(
        energy_before=energy_before,
        energy_preclip=energy_preclip,
        energy_after=energy_after,
        floor_deficit=floor_deficit,
        projected_shadow_energy=projected_shadow,
        candidate_shadow_energy=candidate_shadow,
        candidate_power=candidate_power,
        accepted_power=accepted_power,
        max_floor_deficit=float(np.max(floor_deficit)),
        projected_floor_deficit=projected_B,
        candidate_floor_deficit=candidate_B,
        delta_B=candidate_B - projected_B,
        projection_distortion=numerator / denominator,
        released_energy=float(np.sum(accepted_power * delta)),
        preclip_log_max_error=preclip_error,
        downstream_modification_max=float(
            np.max(np.linalg.norm(accepted - projected, axis=1))
        ),
    )
