"""Bootstrap Amplification (BA).

Uses a causal loop as an amplifier for weak signals. The loop iterates,
and at each pass the signal influences itself. With the right resonance
condition, the signal amplifies to a stable fixed point.

This is NOT circular reasoning -- it is a self-referential computation that
finds a fixed point larger than the input. Analogy: controlled acoustic
feedback (microphone + speaker).

Mathematical formulation:
    x_{n+1} = f(x_n, g(x_{n+1}))  <- self-referential, solved via fixed-point

Applications: weak signal detection, data amplification, novelty discovery.
"""

from __future__ import annotations

from typing import Any

import numpy as np
from numpy.typing import NDArray


class BootstrapAmplifier:
    """A causal loop that amplifies weak input signals.

    The loop kernel transforms the signal each cycle. A retrocausal
    modulation matrix feeds the previous iteration's output back as
    a proxy for the 'future' state. Chrono-entropic damping prevents
    unbounded growth.

    Args:
        signal_dim: Dimensionality of input/output signals.
        amplification_target: Desired amplification factor.
        stability_coefficient: Maximum allowed spectral radius for the loop kernel.
    """

    def __init__(
        self,
        signal_dim: int,
        amplification_target: float = 10.0,
        stability_coefficient: float = 0.95,
    ) -> None:
        self.signal_dim = signal_dim
        self.amplification_target = amplification_target
        self.stability_coefficient = stability_coefficient

        self.loop_kernel = np.eye(signal_dim) * stability_coefficient
        self.loop_kernel += np.random.randn(signal_dim, signal_dim) * 0.01

        self.retro_matrix = np.random.randn(signal_dim, signal_dim) * 0.1
        self.amplification_history: list[float] = []

    def _clamp_kernel_eigenvalues(self) -> None:
        """Ensure spectral radius stays within stability bound."""
        eigvals = np.linalg.eigvals(self.loop_kernel)
        max_eigval = float(np.max(np.abs(eigvals)))
        if max_eigval > self.stability_coefficient:
            self.loop_kernel *= self.stability_coefficient / max_eigval

    def learn_resonance(
        self, training_signals: NDArray[np.floating], n_epochs: int = 100
    ) -> None:
        """Learn a loop kernel that achieves target amplification.

        Trains the amplifier as a self-referential learning system via
        numerical gradient descent on the kernel parameters.

        Args:
            training_signals: Array of shape (n_signals, signal_dim).
            n_epochs: Number of training epochs.
        """
        lr = 0.001

        for _epoch in range(n_epochs):
            for signal in training_signals:
                amplified, _ = self.amplify(signal, max_iterations=50)

                signal_norm = float(np.linalg.norm(signal)) + 1e-10
                achieved_amp = float(np.linalg.norm(amplified)) / signal_norm
                loss = (achieved_amp - self.amplification_target) ** 2

                eps = 1e-4
                limit = min(self.signal_dim, 5)
                for i in range(limit):
                    for j in range(limit):
                        self.loop_kernel[i, j] += eps
                        amp_p, _ = self.amplify(signal, max_iterations=30)
                        achieved_p = float(np.linalg.norm(amp_p)) / signal_norm
                        loss_p = (achieved_p - self.amplification_target) ** 2
                        self.loop_kernel[i, j] -= eps

                        grad = (loss_p - loss) / eps
                        self.loop_kernel[i, j] -= lr * grad

                self._clamp_kernel_eigenvalues()

    def amplify(
        self,
        weak_signal: NDArray[np.floating],
        max_iterations: int = 200,
        convergence_threshold: float = 1e-6,
    ) -> tuple[NDArray[np.floating], dict[str, Any]]:
        """Amplify a weak signal using causal loop resonance.

        The signal enters the loop and self-consistently amplifies until
        it reaches a stable fixed point.

        Args:
            weak_signal: Input signal vector.
            max_iterations: Maximum loop iterations.
            convergence_threshold: Stop when state change is below this.

        Returns:
            Tuple of (amplified_signal, stats_dict).
        """
        current_signal = weak_signal.copy()
        prev_signal = np.zeros_like(weak_signal)
        input_norm = float(np.linalg.norm(weak_signal)) + 1e-10
        target_amp_norm = self.amplification_target * input_norm

        amplification_per_iter: list[float] = []
        delta = float("inf")

        for iteration in range(max_iterations):
            forward_output = self.loop_kernel @ current_signal
            retro_signal = self.retro_matrix @ prev_signal
            new_signal = forward_output + 0.3 * retro_signal

            current_amp = float(np.linalg.norm(new_signal))
            if current_amp < target_amp_norm and current_amp > 1e-10:
                new_signal = new_signal * min(1.05, target_amp_norm / current_amp)
            elif current_amp > target_amp_norm:
                new_signal = new_signal * (target_amp_norm / current_amp)

            delta = float(np.linalg.norm(new_signal - current_signal))
            amplification_per_iter.append(
                float(np.linalg.norm(new_signal)) / input_norm
            )

            prev_signal = current_signal.copy()
            current_signal = new_signal

            if delta < convergence_threshold:
                break

        self.amplification_history.extend(amplification_per_iter)

        return current_signal, {
            "amplification_factor": float(np.linalg.norm(current_signal)) / input_norm,
            "iterations": iteration + 1,
            "converged": delta < convergence_threshold,
            "amplification_history": amplification_per_iter,
        }
