import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import numpy as np
from scipy.io.wavfile import read

# 1. Setup SmolLM2 Meta-Optimizer
model_id = "HuggingFaceTB/SmolLM2-135M-Instruct"
device = "cuda" if torch.cuda.is_available() else "cpu"

print(f"Loading {model_id}...")
tokenizer = AutoTokenizer.from_pretrained(model_id)
llm_model = AutoModelForCausalLM.from_pretrained(model_id).to(device)

# --- THE SECRET MAP (Completely hidden from the SLM) ---
anonymous_menu = {
    "A": {"label": "Strategy ALPHA", "func": "relu"},
    "B": {"label": "Strategy BETA", "func": "leaky_relu"},
    "C": {"label": "Strategy GAMMA", "func": "sigmoid"},
    "D": {"label": "Strategy DELTA", "func": "tanh"}
}

token_map = {letter: tokenizer.convert_tokens_to_ids(letter) for letter in anonymous_menu.keys()}
token_ids = list(token_map.values())
id_to_letter = {v: k for k, v in token_map.items()}

# Load training and test data
print("Loading audio wave dataset layers...")
X_train = read('../X_train.wav')[1].reshape(-1, 784)
y_train = (read('../y_train.wav')[1] * 9).astype(int)
X_test = read('../X_test.wav')[1].reshape(-1, 784)
y_test = (read('../y_test.wav')[1] * 9).astype(int)
X20 = X_test[:1000]
yt20 = y_test[:1000]

# --- Keeping the DynamicMLP logic the exact same ---
class DynamicMLP:
    def __init__(self, input_size, hidden_size, output_size, learning_rate=0.1):
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))
        self.learning_rate = learning_rate
        self.current_activation = "tanh"
        
    def activate(self, x):
        if self.current_activation == "relu": return np.maximum(0, x)
        elif self.current_activation == "leaky_relu": return np.where(x > 0, x, x * 0.01)
        elif self.current_activation == "sigmoid": return 1 / (1 + np.exp(-np.clip(x, -50, 50)))
        elif self.current_activation == "tanh": return np.tanh(x)

    def activate_derivative(self, x, act_output):
        if self.current_activation == "relu": return np.where(x > 0, 1, 0)
        elif self.current_activation == "leaky_relu": return np.where(x > 0, 1, 0.01)
        elif self.current_activation == "sigmoid": return act_output * (1 - act_output)
        elif self.current_activation == "tanh": return 1 - act_output ** 2

    def softmax(self, x):
        exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
        return exp_x / np.sum(exp_x, axis=1, keepdims=True)

    def forward(self, X):
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = self.activate(self.z1)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        return self.softmax(self.z2)

    def update(self, X, y_true):
        m = y_true.shape[0]
        y_pred = self.forward(X)
        dz2 = y_pred - y_true
        dW2 = np.dot(self.a1.T, dz2) / m
        db2 = np.sum(dz2, axis=0, keepdims=True) / m
        da1 = np.dot(dz2, self.W2.T)
        dz1 = da1 * self.activate_derivative(self.z1, self.a1)
        dW1 = np.dot(X.T, dz1) / m
        db1 = np.sum(dz1, axis=0, keepdims=True) / m
        self.W1 -= self.learning_rate * dW1
        self.b1 -= self.learning_rate * db1
        self.W2 -= self.learning_rate * dW2
        self.b2 -= self.learning_rate * db2

    def score(self, X, y_true):
        preds = np.argmax(self.forward(X), axis=1)
        return np.mean(preds == y_true)


if __name__ == "__main__":
    mlp = DynamicMLP(input_size=784, hidden_size=100, output_size=10, learning_rate=0.1)
    performance_log = {key: [] for key in anonymous_menu.keys()}
    N = 10 

    print("Running initial weight setup warm-up batches...")
    for _ in range(100):
        idx = np.random.randint(0, 60000, 100)
        X = X_train[idx]
        yt = y_train[idx]
        if _ % 20 == 0:
            print(f"Batch {_}/100 - Score: {mlp.score(X, yt):.4f}")
        mlp.update(X, np.eye(10)[yt])
    
    print(f"\nStarting ANONYMOUS data-window optimization (N = {N})...")
    
    for epoch in range(1, 4):
        print(f"\n================ META CYCLE {epoch} ================")
        
        # --- PHASE 1: ANONYMOUS EXPLORATION ---
        print("Evaluating hidden mathematical performance...")
        for letter, config in anonymous_menu.items():
            mlp.current_activation = config["func"]
            start_acc = mlp.score(X_train[:500], y_train[:500])
            
            for _ in range(N):
                idx = np.random.randint(0, 5000, 64)
                mlp.update(X_train[idx], np.eye(10)[y_train[idx]])
                
            end_acc = mlp.score(X_train[:500], y_train[:500])
            delta_acc = end_acc - start_acc
            performance_log[letter].append(delta_acc)
            print(f" -> Tested {config['label']}: Accuracy Metric Performance = {delta_acc:+.4f}")
            
        # --- PHASE 2: ANONYMOUS EXPLOITATION (With Prefix Constraint Bug Fix) ---
        prompt = (
            f"<|im_start|>system\n"
            f"You are a pure numerical analysis engine tracking statistical trajectories.\n"
            f"Analyze the performance changes from the last matrix processing window:\n"
            f"- Strategy ALPHA performance score: {performance_log['A'][-1]:+.4f}\n"
            f"- Strategy BETA performance score: {performance_log['B'][-1]:+.4f}\n"
            f"- Strategy GAMMA performance score: {performance_log['C'][-1]:+.4f}\n"
            f"- Strategy DELTA performance score: {performance_log['D'][-1]:+.4f}\n\n"
            f"Select the single character token option linked to the highest positive numerical score value:\n"
            f"A = Strategy ALPHA\n"
            f"B = Strategy BETA\n"
            f"C = Strategy GAMMA\n"
            f"D = Strategy DELTA\n"
            f"Which option letter is optimal? Output letter:<|im_end|>\n"
            f"<|im_start|>assistant\n"
        )
        
        # This function acts as a hard filter on the model's vocabulary during generation
        def restrict_to_menu_letters(batch_id, input_ids):
            return token_ids  # Only tokens corresponding to ['A', 'B', 'C', 'D'] can physically exist

        inputs = tokenizer(prompt, return_tensors="pt").to(device)
        
        with torch.no_grad():
            outputs = llm_model.generate(
                **inputs,
                max_new_tokens=1,
                prefix_allowed_tokens_fn=restrict_to_menu_letters,
                return_dict_in_generate=True,
                output_scores=True,
                temperature=0.1,
                do_sample=False
            )
            
            # Extract the actual forced single character
            chosen_token_id = outputs.sequences[0, -1].item()
            chosen_letter = id_to_letter[chosen_token_id]
            
            # Isolate forced score parameters to trace current distribution metrics
            forced_logits = outputs.scores[0][0, token_ids]
            probs = torch.softmax(forced_logits, dim=-1).cpu().numpy()
            
        # Behind the scenes, map their anonymous token choice back to real math
        selected_strategy = anonymous_menu[chosen_letter]
        mlp.current_activation = selected_strategy["func"]
        
        print("\n--- Meta Decision Metrics (Forced Structural Calibration) ---")
        print(f"A(ALPHA): {probs[0]:.2f} | B(BETA): {probs[1]:.2f} | C(GAMMA): {probs[2]:.2f} | D(DELTA): {probs[3]:.2f}")
        print(f"✨ SmolLM2 selected code {chosen_letter} ({selected_strategy['label']}).")
        print(f"   [System Execution mapping]: Operating on mathematical backend '{selected_strategy['func'].upper()}'")