"""Laplace mechanism for differential-privacy-based unlearning.

Moved from sage.libs.privacy.unlearning.algorithms — concrete implementation
belongs in sage-privacy (L3 research).
"""

from __future__ import annotations

import numpy as np

from sage_privacy.dp_unlearning.base_mechanism import BasePrivacyMechanism


class LaplaceMechanism(BasePrivacyMechanism):
    """Laplace mechanism for pure ε-differential privacy.

    The mechanism adds noise sampled from Lap(Δf / ε) to achieve ε-DP.

    Args:
        epsilon: Privacy parameter (smaller → stronger privacy).
        sensitivity: L1 sensitivity of the query (default: 1.0).
        clip_bound: If set, noise is clipped to ``[-clip_bound, clip_bound]``.

    Example:
        >>> mech = LaplaceMechanism(epsilon=1.0)
        >>> noisy_value = 42.0 + mech.compute_noise()
    """

    def __init__(
        self,
        epsilon: float,
        sensitivity: float = 1.0,
        clip_bound: float | None = None,
    ) -> None:
        super().__init__(
            epsilon=epsilon,
            delta=None,  # Pure ε-DP — no (ε,δ) failure probability
            sensitivity=sensitivity,
            name="Laplace",
        )
        self.clip_bound = clip_bound

    def compute_noise(
        self,
        sensitivity: float | None = None,
        epsilon: float | None = None,
        delta: float | None = None,  # Ignored — Laplace is pure DP
    ) -> float:
        """Generate Laplace noise: Lap(Δf / ε).

        Args:
            sensitivity: Override instance sensitivity.
            epsilon: Override instance epsilon.
            delta: Unused (kept for protocol compatibility).

        Returns:
            A single noise value drawn from Lap(scale).
        """
        sens = sensitivity if sensitivity is not None else self.sensitivity
        eps = epsilon if epsilon is not None else self.epsilon

        scale = sens / eps
        noise = float(np.random.laplace(0, scale))

        if self.clip_bound is not None:
            noise = float(np.clip(noise, -self.clip_bound, self.clip_bound))

        return noise

    def privacy_cost(self) -> tuple[float, float]:
        """Return the privacy cost (ε, 0) — Laplace satisfies pure ε-DP."""
        return (self.epsilon, 0.0)
