import os
import torch
import torch.nn as nn
from torch.nn import functional as F
import customtkinter as ctk
from tkinter import filedialog, messagebox
import re
import threading
import time

MODEL_DIR = "modele"
MODEL_PATH = os.path.join(MODEL_DIR, "genius_ultimate_brain.pt")
device = 'cuda' if torch.cuda.is_available() else 'cpu'

HEBREW_CHARS = " אבגדהוזחטיכלמנסעפצקרשתםןץףך\n.,?!:-"
chars = sorted(list(set(HEBREW_CHARS)))
vocab_size = len(chars)
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }

BLOCK_SIZE = 128 
N_EMBD = 256
N_HEAD = 8
N_LAYER = 8

if not os.path.exists(MODEL_DIR):
    os.makedirs(MODEL_DIR)

def super_clean_text(text):
    text = re.sub(r'\d+', ' ', text) # הסרת מספרים
    text = re.sub(r'([.,?!])\1+', r'\1', text) # פיסוק כפול לאחד
    allowed = set(HEBREW_CHARS)
    cleaned = "".join([c for c in text if c in allowed])
    cleaned = re.sub(r' +', ' ', cleaned)
    return cleaned.strip()

class GeniusModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.token_embedding_table = nn.Embedding(vocab_size, N_EMBD)
        self.position_embedding_table = nn.Embedding(BLOCK_SIZE, N_EMBD)
        self.blocks = nn.Sequential(*[Block(N_EMBD, n_head=N_HEAD) for _ in range(N_LAYER)])
        self.ln_f = nn.LayerNorm(N_EMBD)
        self.lm_head = nn.Linear(N_EMBD, vocab_size)

    def forward(self, idx, targets=None):
        B, T = idx.shape
        x = self.token_embedding_table(idx) + self.position_embedding_table(torch.arange(T, device=device))
        x = self.ln_f(self.blocks(x))
        logits = self.lm_head(x)
        loss = F.cross_entropy(logits.view(-1, vocab_size), targets.view(-1)) if targets is not None else None
        return logits, loss

    def generate_smart(self, idx, max_new_tokens, temperature=0.8):
        self.eval()
        for _ in range(max_new_tokens):
            idx_cond = idx[:, -BLOCK_SIZE:]
            with torch.no_grad():
                logits, _ = self(idx_cond)
            
            logits = logits[:, -1, :] / temperature
            
            for char_idx in set(idx[0].tolist()):
                logits[0, char_idx] /= 1.4 

            probs = F.softmax(logits, dim=-1)
            top_probs, top_indices = torch.topk(probs, 5)
            # בחירת אות אחת מהטופ
            rel_idx = torch.multinomial(top_probs[0], 1)
            idx_next = top_indices[0, rel_idx].view(1, 1) # מוודא מימד 2D (1,1)
            
            idx = torch.cat((idx, idx_next), dim=1)
            
            if itos[idx_next.item()] == '\n': break
        return idx

class Block(nn.Module):
    def __init__(self, n_embd, n_head):
        super().__init__()
        head_size = n_embd // n_head
        self.sa = MultiHeadAttention(n_head, head_size)
        self.ffwd = nn.Sequential(nn.Linear(n_embd, 4 * n_embd), nn.ReLU(), nn.Linear(4 * n_embd, n_embd))
        self.ln1, self.ln2 = nn.LayerNorm(n_embd), nn.LayerNorm(n_embd)
    def forward(self, x):
        x = x + self.sa(self.ln1(x))
        return x + self.ffwd(self.ln2(x))

class MultiHeadAttention(nn.Module):
    def __init__(self, num_heads, head_size):
        super().__init__()
        self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
        self.proj = nn.Linear(N_EMBD, N_EMBD)
    def forward(self, x): return self.proj(torch.cat([h(x) for h in self.heads], dim=-1))

class Head(nn.Module):
    def __init__(self, head_size):
        super().__init__()
        self.key = nn.Linear(N_EMBD, head_size, bias=False)
        self.query = nn.Linear(N_EMBD, head_size, bias=False)
        self.value = nn.Linear(N_EMBD, head_size, bias=False)
        self.register_buffer('tril', torch.tril(torch.ones(BLOCK_SIZE, BLOCK_SIZE)))
    def forward(self, x):
        B,T,C = x.shape
        k, q, v = self.key(x), self.query(x), self.value(x)
        wei = q @ k.transpose(-2,-1) * C**-0.5
        wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf'))
        return F.softmax(wei, dim=-1) @ v

class GeniusApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("הגאון העברי - גרסה יציבה")
        self.geometry("1100x900")
        ctk.set_appearance_mode("dark")
        
        self.model = GeniusModel().to(device)
        if os.path.exists(MODEL_PATH):
            try: self.model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
            except: pass

        self.setup_ui()

    def setup_ui(self):
        self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self.sidebar = ctk.CTkFrame(self, width=280)
        self.sidebar.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
        
        ctk.CTkLabel(self.sidebar, text="מרכז בקרה", font=("Arial", 20, "bold")).pack(pady=20)
        ctk.CTkButton(self.sidebar, text="העלה קובץ לתיבה", command=self.upload_file, fg_color="#1f538d").pack(pady=10, padx=20)
        
        self.loss_label = ctk.CTkLabel(self.sidebar, text="Loss: 0.0000", font=("Consolas", 16), text_color="orange")
        self.loss_label.pack(pady=20)
        
        self.status_label = ctk.CTkLabel(self.sidebar, text="סטטוס: מוכן", text_color="gray")
        self.status_label.pack(pady=5)
        
        self.progress_bar = ctk.CTkProgressBar(self.sidebar)
        self.progress_bar.set(0)
        self.progress_bar.pack(pady=10, padx=20)

        self.main_frame = ctk.CTkFrame(self)
        self.main_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
        
        ctk.CTkLabel(self.main_frame, text="תיבת אימון ידנית / הדבקה:", font=("Arial", 14)).pack(pady=(15, 5), padx=20, anchor="e")
        self.train_input_box = ctk.CTkTextbox(self.main_frame, height=180, font=("Arial", 14))
        self.train_input_box.pack(fill="x", padx=20, pady=5)
        
        self.train_btn = ctk.CTkButton(self.main_frame, text="אמן מודל", height=45, fg_color="green", command=self.start_training)
        self.train_btn.pack(pady=10, padx=20, fill="x")

        self.chat_display = ctk.CTkTextbox(self.main_frame, state="disabled", font=("Arial", 16))
        self.chat_display.pack(fill="both", expand=True, padx=20, pady=10)

        self.chat_entry = ctk.CTkEntry(self.main_frame, placeholder_text="כתוב כאן...", height=50)
        self.chat_entry.pack(side="left", fill="x", expand=True, padx=(20, 5), pady=20)
        self.chat_entry.bind("<Return>", lambda e: self.send_chat())
        ctk.CTkButton(self.main_frame, text="שלח", width=120, height=50, command=self.send_chat).pack(side="right", padx=(5, 20), pady=20)

    def upload_file(self):
        path = filedialog.askopenfilename()
        if path:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                self.train_input_box.delete("1.0", "end")
                self.train_input_box.insert("end", f.read())

    def start_training(self):
        text = self.train_input_box.get("1.0", "end").strip()
        if len(text) < 10: return
        self.train_btn.configure(state="disabled")
        threading.Thread(target=self.run_training, args=(text,), daemon=True).start()

    def run_training(self, raw_text):
        clean_text = super_clean_text(raw_text)
        data = torch.tensor([stoi[c] for c in clean_text], dtype=torch.long, device=device)
        optimizer = torch.optim.AdamW(self.model.parameters(), lr=1e-4)
        self.model.train()
        steps = 400
        for i in range(steps):
            if len(data) <= BLOCK_SIZE: break
            ix = torch.randint(len(data) - BLOCK_SIZE, (16,))
            x = torch.stack([data[j:j+BLOCK_SIZE] for j in ix])
            y = torch.stack([data[j+1:j+BLOCK_SIZE+1] for j in ix])
            logits, loss = self.model(x, y)
            optimizer.zero_grad(set_to_none=True); loss.backward(); optimizer.step()
            if i % 10 == 0: self.after(0, lambda l=loss.item(), p=i/steps: self.update_ui(l, p))
        torch.save(self.model.state_dict(), MODEL_PATH)
        self.after(0, self.finish_training)

    def update_ui(self, l, p):
        self.loss_label.configure(text=f"Loss: {l:.4f}")
        self.progress_bar.set(p)

    def finish_training(self):
        self.train_btn.configure(state="normal")
        self.status_label.configure(text="האימון הושלם!", text_color="cyan")
        messagebox.showinfo("הצלחה", "המודל למד את החומר!")

    def send_chat(self):
        msg = self.chat_entry.get().strip()
        if not msg: return
        self.add_chat(f"אתה: {msg}", "white")
        self.chat_entry.delete(0, "end")
        threading.Thread(target=self.think_and_answer, args=(msg,), daemon=True).start()

    def think_and_answer(self, msg):
        self.after(0, lambda: self.status_label.configure(text="הגאון חושב...", text_color="orange"))
        # זמן חשיבה פנימי
        for i in range(1, 11):
            time.sleep(0.1)
            self.after(0, lambda v=i/10: self.progress_bar.set(v))
        
        context = torch.tensor([[stoi.get(c, stoi[' ']) for c in msg]], dtype=torch.long, device=device)
        gen = self.model.generate_smart(context, max_new_tokens=80)
        
        response = "".join([itos[i] for i in gen[0].tolist()])[len(msg):].strip().split('\n')[0]
        self.after(0, lambda: self.add_chat(f"הגאון: {response}", "#3a86ff"))
        self.after(0, lambda: self.status_label.configure(text="מוכן", text_color="gray"))
        self.after(0, lambda: self.progress_bar.set(0))

    def add_chat(self, t, color):
        self.chat_display.configure(state="normal")
        self.chat_display.insert("end", t + "\n\n")
        self.chat_display.configure(state="disabled")
        self.chat_display.see("end")

if __name__ == "__main__":
    app = GeniusApp()
    app.mainloop()