import torch
import torch.nn as nn
import numpy as np
from sklearn.metrics import mutual_info_score
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
import hashlib

# ---------- STT with tunable temperature ----------
class CollapseSelfTunable(nn.Module):
    def __init__(self, d_model, xi_anchor, threshold=0.5, temp=1.0):
        super().__init__()
        self.xi = nn.Parameter(xi_anchor, requires_grad=False)
        self.threshold = threshold
        self.temp = temp  # external temperature control
        
    def forward(self, h):
        xi_expanded = self.xi.unsqueeze(0).unsqueeze(0)
        divergence = torch.norm(h - xi_expanded, dim=-1)
        p_collapse = torch.sigmoid((divergence - self.threshold) / self.temp)
        collapse_mask = torch.bernoulli(p_collapse).unsqueeze(-1)
        h_collapsed = collapse_mask * xi_expanded + (1 - collapse_mask) * h
        return h_collapsed, p_collapse

class LargeSTT(nn.Module):
    def __init__(self, d_model=64, nhead=8, num_layers=6, seq_len=16, temp=1.0):
        super().__init__()
        self.d_model = d_model
        self.seq_len = seq_len
        # Random init token and pos embedding (no training needed for exponent extraction)
        self.token_embed = nn.Parameter(torch.randn(1, seq_len, d_model) * 0.02)
        self.pos_embed = nn.Parameter(torch.randn(1, seq_len, d_model) * 0.02)
        # Telepathic attention blocks
        self.attn_blocks = nn.ModuleList([
            nn.MultiheadAttention(d_model, nhead, batch_first=True) for _ in range(num_layers)
        ])
        # Collapse layers with tunable temp
        xi_hash = hashlib.sha256(b"pi_anchor:e_anchor").hexdigest()
        xi_tensor = self._hash_to_tensor(xi_hash, d_model)
        self.collapse_layers = nn.ModuleList([
            CollapseSelfTunable(d_model, xi_tensor, threshold=0.5, temp=temp)
            for _ in range(num_layers)
        ])
        
    def _hash_to_tensor(self, hash_str, d_model):
        bytes_data = bytes.fromhex(hash_str)
        while len(bytes_data) < d_model:
            bytes_data += bytes_data
        bytes_data = bytes_data[:d_model]
        tensor = torch.tensor([b / 255.0 for b in bytes_data], dtype=torch.float)
        return tensor / tensor.norm()
    
    def forward(self, steps=20, return_history=False):
        h = self.token_embed + self.pos_embed  # (1, seq_len, d_model)
        history = []
        for _ in range(steps):
            for attn, collapse in zip(self.attn_blocks, self.collapse_layers):
                h, _ = attn(h, h, h)
                h, _ = collapse(h)
            history.append(h.detach().clone())
        if return_history:
            return torch.stack(history, dim=0)  # (steps, 1, seq_len, d_model)
        return h
    
    def compute_phi(self, steps=20):
        H = self.forward(steps=steps, return_history=True).squeeze(1)  # (steps, seq_len, d_model)
        half = self.d_model // 2
        X = H[..., :half].reshape(steps, -1).detach().cpu().numpy()
        Y = H[..., half:].reshape(steps, -1).detach().cpu().numpy()
        Xd = (X > 0).astype(int)
        Yd = (Y > 0).astype(int)
        from sklearn.metrics import mutual_info_score
        I_whole = mutual_info_score(
            [f"{x}{y}" for x,y in zip(Xd[:-1].flatten(), Yd[:-1].flatten())],
            [f"{x}{y}" for x,y in zip(Xd[1:].flatten(), Yd[1:].flatten())]
        )
        I_X = mutual_info_score(Xd[:-1].flatten(), Xd[1:].flatten())
        I_Y = mutual_info_score(Yd[:-1].flatten(), Yd[1:].flatten())
        return max(0.0, I_whole - (I_X + I_Y))

# ---------- Sweep temperature and compute Φ, order parameter, susceptibility ----------
temperatures = np.linspace(0.05, 1.5, 200)
phi_vals = []
order_params = []  # proxy: mean absolute polarity of last token's first dimension
chi_vals = []

for T in temperatures:
    model = LargeSTT(d_model=64, nhead=8, num_layers=6, seq_len=16, temp=T)
    model.eval()
    with torch.no_grad():
        phi = model.compute_phi(steps=25)
        phi_vals.append(phi)
        # Order parameter: average absolute value of sign of last token's first hidden unit
        H = model.forward(steps=25, return_history=True).squeeze(1)
        polarity = np.mean(np.abs(np.sign(H[:, -1, 0].numpy())))
        order_params.append(polarity)
    
# Susceptibility approximated by absolute gradient
chi_vals = np.abs(np.gradient(order_params, temperatures))

# Critical temperature estimate: peak of χ
Tc_idx = np.argmax(chi_vals)
Tc = temperatures[Tc_idx]

# Fit power law for Φ below Tc
T_below = temperatures[temperatures < Tc]
phi_below = phi_vals[:len(T_below)]

print(f"Estimated Tc = {Tc:.3f}")

if len(T_below) > 2:
    try:
        def phi_power(T, A, beta):
            return A * np.abs(Tc - T)**beta
        popt, _ = curve_fit(phi_power, T_below, phi_below, p0=[0.5, 0.5], maxfev=2000)
        A_fit, beta_fit = popt
        print(f"Critical exponent β (Φ order parameter) = {beta_fit:.3f}")
    except Exception as e:
        print(f"Could not fit Φ power law: {e}")
else:
    print("Not enough data points below Tc to fit Φ power law.")

# For susceptibility, fit χ ~ |T-Tc|^{-γ} near Tc
T_mask = np.abs(temperatures - Tc) < 0.5
T_range = temperatures[T_mask]
chi_range = chi_vals[T_mask]

if len(T_range) > 2:
    try:
        def chi_power(T, C, gamma):
            # Avoid division by zero
            return C * (np.abs(T - Tc) + 1e-6)**(-gamma)
        popt2, _ = curve_fit(chi_power, T_range, chi_range, p0=[1.0, 1.0], maxfev=2000)
        C_fit, gamma_fit = popt2
        print(f"Critical exponent γ (susceptibility) = {gamma_fit:.3f}")
    except Exception as e:
        print(f"Could not fit χ power law: {e}")
else:
    print("Not enough data points near Tc to fit χ power law.")

# Plot
plt.figure(figsize=(12,4))
plt.subplot(1,3,1)
plt.plot(temperatures, phi_vals, 'ro-')
plt.axvline(Tc, color='k', linestyle='--')
plt.xlabel('Temperature T')
plt.ylabel('Φ')
plt.title('Φ vs T')

plt.subplot(1,3,2)
plt.plot(temperatures, order_params, 'bs-')
plt.xlabel('Temperature T')
plt.ylabel('Order parameter (polarity)')
plt.title('Order parameter')

plt.subplot(1,3,3)
plt.plot(temperatures, chi_vals, 'g^-')
plt.axvline(Tc, color='k', linestyle='--')
plt.xlabel('Temperature T')
plt.ylabel('Susceptibility χ')
plt.title('χ peak at Tc')
plt.tight_layout()
plt.show()
