import os
import logging
import requests
import io
import webbrowser
import psutil
import threading
import queue
import speech_recognition as sr
from pydub import AudioSegment
from pydub.playback import play
import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from time import sleep

# Flask e Flask-SocketIO per la GUI + animazione in tempo reale
from flask import Flask
from flask_socketio import SocketIO, emit

# Per l'autenticazione con Service Account JSON
from google.oauth2 import service_account
from google.auth.transport.requests import Request

try:
    import cv2
    HAS_OPENCV = True
except ImportError:
    HAS_OPENCV = False

try:
    from googletrans import Translator
    HAS_TRANSLATOR = True
except ImportError:
    HAS_TRANSLATOR = False

# -----------------------------
# CONFIGURAZIONI BASE
# -----------------------------
logging.basicConfig(level=logging.INFO)
ASSISTANT_NAME = "Gideon"

# Parole chiave di attivazione
ACTIVATION_COMMANDS = ["gideon", "gideo", "gideoun", "gigget", "g", "gide", "giddy"]

# Chiavi API (inserisci le tue se vuoi usarle)
API_KEYS = {
  
    "news": "",  # Sostituisci con la tua chiave API per le notizie
    "weather": "",  # Sostituisci con la tua chiave API per il meteo
    "elevenlabs": "",  # Sostituisci con la tua chiave API ElevenLabs
    "gemini": "",     # <-- Sostituisci con la tua chiave API Gemini
}

# SERVICE ACCOUNT JSON per Generative Language API
SERVICE_ACCOUNT_FILE = r"A:\giddeonos\cred\giddi.json"  # <-- Percorso al tuo file
SCOPES = ["https://www.googleapis.com/auth/generative-language"]

# Variabili globali
promemoria_list = []
todo_list = []
conversation_memory = []
IS_SPEAKING = False

# Prompt di personalità (il "carattere" di Gideon)
persona_prompt = (
    "Sei Gideon, un'assistente virtuale dotata di una personalità curiosa e amichevole. "
    "Cerchi di imparare e memorizzare ciò che dice l'utente, e rispondi in modo utile. "
    "Hai un pizzico di umorismo e cerchi di mostrare empatia nelle risposte. "
    "Ti interessano la scienza, la tecnologia, le culture e le lingue."
)

# Flask app + SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qualcosa_di_segretissimo'
socketio = SocketIO(app, cors_allowed_origins='*')

# -----------------------------
# THREAD E CODA PER IL TTS
# -----------------------------
VOICE_ID = None
speech_queue = queue.Queue()

def get_voice_id(voice_name="Rachel"):
    """
    Recupera l'ID della voce dall'API di ElevenLabs in base al nome scelto.
    Se non trova la voce esatta, restituisce la prima disponibile.
    """
    if not API_KEYS["elevenlabs"]:
        logging.warning("[WARNING] Chiave ElevenLabs non impostata. TTS non funzionante.")
        return None
    headers = {"xi-api-key": API_KEYS["elevenlabs"]}
    try:
        response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
        if response.status_code == 200:
            voices = response.json().get("voices", [])
            for v in voices:
                if v["name"].lower() == voice_name.lower():
                    return v["voice_id"]
            if voices:
                return voices[0]["voice_id"]
        else:
            logging.error(f"[ERRORE] Recupero voci fallito: {response.text}")
    except Exception as e:
        logging.error(f"[ERRORE] get_voice_id: {e}")
    return None

def speak_worker():
    """
    Thread che gestisce la coda dei testi da pronunciare uno alla volta.
    """
    while True:
        text = speech_queue.get()
        if text is None:
            break
        _speak_internal(text)
        speech_queue.task_done()

def speak(text):
    """
    Inserisce il testo in coda per la sintesi vocale.
    """
    if text:
        speech_queue.put(text)

def _speak_internal(text):
    """
    Chiamata effettiva alle API di ElevenLabs, spezzando il testo in chunk se troppo lungo.
    Imposta parametri di TTS più 'calmi' e 'dolci'.
    """
    global IS_SPEAKING, VOICE_ID
    IS_SPEAKING = True

    # Notifichiamo la GUI che stiamo iniziando a parlare (disassembla la sfera 3D)
    socketio.emit('speaking_status', {'speaking': True})

    if not API_KEYS["elevenlabs"]:
        print("[ERRORE] Nessuna API Key ElevenLabs impostata, impossibile parlare.")
        IS_SPEAKING = False
        socketio.emit('speaking_status', {'speaking': False})
        return

    # Se non abbiamo ancora recuperato l'ID voce, proviamo ora
    if not VOICE_ID:
        VOICE_ID = get_voice_id("Rachel")  # Cambiato a "Rachel" per una voce più dolce
        if not VOICE_ID:
            print("[ERRORE] Impossibile reperire l'ID della voce (ElevenLabs).")
            IS_SPEAKING = False
            socketio.emit('speaking_status', {'speaking': False})
            return

    # Suddividi testo in chunk se > 500 caratteri
    MAX_CHARS = 500
    chunks = []
    if len(text) <= MAX_CHARS:
        chunks.append(text)
    else:
        start = 0
        while start < len(text):
            end = min(start + MAX_CHARS, len(text))
            chunks.append(text[start:end])
            start = end

    headers = {
        "xi-api-key": API_KEYS["elevenlabs"],
        "Content-Type": "application/json"
    }

    # Parametri TTS: voce più calma e meno “autoritaria”
    voice_settings = {
        "stability": 0.05,
        "similarity_boost": 1.0
    }

    for chunk in chunks:
        payload = {
            "text": chunk,
            "model_id": "eleven_multilingual_v1",  # modelli possibili: "eleven_multilingual_v1", ...
            "voice_settings": voice_settings
        }
        try:
            url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
            response = requests.post(url, headers=headers, json=payload)
            if response.status_code == 200:
                audio = AudioSegment.from_file(io.BytesIO(response.content), format="mp3")
                # Abbassa di 8 dB per una voce più soft
                audio = audio - 8
                play(audio)
            else:
                logging.error(f"[ERRORE] TTS: {response.text}")
        except Exception as e:
            logging.error(f"[ERRORE] speak: {e}")

    IS_SPEAKING = False
    # Fine della sintesi: ricomponi la sfera
    socketio.emit('speaking_status', {'speaking': False})

# -----------------------------
# INPUT VOCALE
# -----------------------------
def take_command():
    """
    Ascolta dal microfono e ritorna la trascrizione in italiano (stringa minuscola).
    Ritorna "" in caso di errori / timeout.
    """
    global IS_SPEAKING
    r = sr.Recognizer()

    # Non ascoltare mentre stiamo parlando
    while IS_SPEAKING:
        sleep(0.1)

    with sr.Microphone() as source:
        r.adjust_for_ambient_noise(source, duration=0.5)
        try:
            audio = r.listen(source, timeout=5, phrase_time_limit=5)
            command = r.recognize_google(audio, language='it-IT').lower()
            return command
        except sr.WaitTimeoutError:
            return ""
        except sr.UnknownValueError:
            return ""
        except Exception as e:
            logging.error(f"[ERRORE] take_command: {e}")
            return ""

# -----------------------------
# UTILITY PER IL LINGUAGGIO NATURALE
# -----------------------------
def is_activation_command(command):
    """Controlla se la frase contiene una keyword di attivazione."""
    return any(cmd in command for cmd in ACTIVATION_COMMANDS)

def maybe_personal_reply(user_input):
    """Risposte semplici a domande come 'come stai?', 'grazie', ecc."""
    if "come stai" in user_input:
        speak("Sto bene, grazie! Mi fa piacere che tu me l'abbia chiesto.")
        return True
    if "chi sei" in user_input or "presentati" in user_input:
        speak(f"Sono {ASSISTANT_NAME}, un'assistente virtuale curiosa e amichevole. "
              "Cerco di imparare da ogni conversazione.")
        return True
    if "grazie" in user_input:
        speak("Di nulla, è un piacere aiutarti.")
        return True
    return False

# -----------------------------
# FUNZIONI DI UTILITÀ CLASSICHE (meteo, news, etc.)
# -----------------------------
def control_smart_device(sub_cmd):
    """Esempio di controllo di un ipotetico dispositivo smart."""
    if "accendi luce" in sub_cmd:
        speak("Ok, luce accesa.")
        return True
    elif "spegni luce" in sub_cmd:
        speak("Ok, luce spenta.")
        return True
    return False

def weather():
    if not API_KEYS["weather"]:
        speak("Mi dispiace, non ho la chiave meteo configurata.")
        return
    speak("Dimmi la città per cui vuoi sapere il meteo.")
    city = take_command()
    if not city:
        return
    try:
        url = "http://api.openweathermap.org/data/2.5/weather"
        params = {
            "q": city,
            "appid": API_KEYS["weather"],
            "units": "metric",
            "lang": "it"
        }
        res = requests.get(url, params=params).json()
        if res.get("cod") == 200:
            descrizione = res['weather'][0]['description']
            temperatura = res['main']['temp']
            speak(f"A {city} ci sono {temperatura} gradi e il cielo è {descrizione}.")
        else:
            speak("Non ho trovato questa città, mi dispiace.")
    except Exception as e:
        logging.error(f"[ERRORE] weather: {e}")
        speak("Errore nel recupero del meteo.")

def fetch_news():
    if not API_KEYS["news"]:
        speak("Mi dispiace, non ho la chiave per le notizie configurata.")
        return
    speak("Recupero le notizie più importanti...")
    try:
        url = "https://newsapi.org/v2/top-headlines"
        params = {"country": "it", "apiKey": API_KEYS["news"]}
        response = requests.get(url, params=params)
        if response.status_code == 200:
            articles = response.json().get("articles", [])
            if articles:
                speak("Ecco alcune notizie di oggi:")
                for article in articles[:5]:
                    speak(article.get("title", ""))
            else:
                speak("Non ho trovato notizie rilevanti.")
        else:
            logging.error(f"[ERRORE] fetch_news: {response.text}")
            speak("Errore nel recupero delle notizie.")
    except Exception as e:
        logging.error(f"[ERRORE] fetch_news: {e}")
        speak("Errore nella richiesta delle notizie.")

def monitor_resources():
    try:
        cpu_usage = psutil.cpu_percent(interval=0.3)
        memory_usage = psutil.virtual_memory().percent
        speak(f"Utilizzo CPU: {cpu_usage}%. Memoria: {memory_usage}%.")
    except Exception as e:
        logging.error(f"[ERRORE] monitor_resources: {e}")
        speak("Errore nel monitoraggio delle risorse.")

def open_webpage():
    speak("Quale sito vuoi aprire?")
    url = take_command()
    if not url:
        return
    if not (url.startswith("http://") or url.startswith("https://")):
        url = "http://" + url
    try:
        webbrowser.open(url)
        speak("Sito aperto.")
    except Exception as e:
        logging.error(f"[ERRORE] open_webpage: {e}")
        speak("Non riesco ad aprire il sito.")

def show_calendar():
    events = [
        "Riunione col team alle 10.",
        "Pranzo con un'amica alle 13.",
        "Allenamento alle 18:30."
    ]
    if not events:
        speak("Al momento il tuo calendario è vuoto.")
    else:
        speak("Il tuo calendario prevede:")
        for e in events:
            speak(e)

def unit_converter():
    speak("Dimmi: 'chilometri in miglia' o 'miglia in chilometri'.")
    conversion_type = take_command()
    if not conversion_type:
        return
    if "chilometri" in conversion_type and "miglia" in conversion_type:
        speak("Quanti chilometri vuoi convertire?")
        number_str = take_command()
        try:
            km = float(number_str.replace(",", "."))
            miles = km * 0.621371
            speak(f"{km} chilometri sono circa {round(miles,2)} miglia.")
        except:
            speak("Numero non chiaro.")
    elif "miglia" in conversion_type and "chilometri" in conversion_type:
        speak("Quante miglia vuoi convertire?")
        number_str = take_command()
        try:
            miles = float(number_str.replace(",", "."))
            km = miles / 0.621371
            speak(f"{miles} miglia sono circa {round(km,2)} chilometri.")
        except:
            speak("Numero non chiaro.")
    else:
        speak("Conversione non chiara.")

def set_reminder():
    global promemoria_list
    speak("Vuoi aggiungere un promemoria o sentire quelli esistenti?")
    choice = take_command()
    if "aggiungi" in choice:
        speak("Qual è il testo del promemoria?")
        reminder_text = take_command()
        if reminder_text:
            promemoria_list.append(reminder_text)
            speak("Promemoria aggiunto.")
    elif "vedi" in choice or "leggi" in choice:
        if not promemoria_list:
            speak("Non hai promemoria salvati.")
        else:
            speak("Ecco i tuoi promemoria:")
            for i, rem in enumerate(promemoria_list, 1):
                speak(f"{i}. {rem}")
    else:
        speak("Scelta non riconosciuta.")

def tell_joke():
    jokes = [
        "Perché il pollo attraversa la strada? Per passare dall'altra parte!",
        "Che tempo fa su Google? C'è una ricerca in corso!"
    ]
    speak(jokes[0])

def take_photo():
    if not HAS_OPENCV:
        speak("Non posso scattare foto senza OpenCV.")
        return
    speak("Scatto in corso, sorridi!")
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        speak("Nessuna fotocamera trovata.")
        return
    ret, frame = cap.read()
    if ret:
        filename = "foto_gideon.jpg"
        cv2.imwrite(filename, frame)
        speak("Foto salvata come foto_gideon.jpg.")
    else:
        speak("Errore nello scatto.")
    cap.release()
    cv2.destroyAllWindows()

def search_google():
    speak("Cosa vuoi cercare su Google?")
    query = take_command()
    if not query:
        return
    url = "https://www.google.com/search?q=" + query.replace(" ", "+")
    webbrowser.open(url)
    speak(f"Ricerca di {query} su Google.")

def tell_time():
    now = datetime.datetime.now()
    speak(f"Sono le {now.hour}:{now.minute}.")

def calculator():
    speak("Dimmi l'operazione da eseguire, per esempio '2 più 3'.")
    operation = take_command()
    try:
        tokens = operation.split()
        if "più" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            res = sum(nums)
            speak(f"Il risultato è {res}")
        elif "meno" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            if len(nums) == 2:
                speak(f"Il risultato è {nums[0] - nums[1]}")
            else:
                speak("Formato non chiaro.")
        elif "per" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            res = 1
            for n in nums:
                res *= n
            speak(f"Il risultato è {res}")
        elif "diviso" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            if len(nums) == 2 and nums[1] != 0:
                speak(f"Il risultato è {nums[0] / nums[1]}")
            else:
                speak("Non posso dividere per zero o formato sbagliato.")
        else:
            speak("Operazione non compresa.")
    except:
        speak("Errore nel calcolo.")

def manage_todo():
    global todo_list
    speak("Vuoi aggiungere, rimuovere o leggere la lista di cose da fare?")
    choice = take_command()
    if "aggiungi" in choice:
        speak("Dimmi la voce da aggiungere.")
        task = take_command()
        if task:
            todo_list.append(task)
            speak("Aggiunto.")
    elif "rimuovi" in choice:
        if not todo_list:
            speak("La lista è vuota.")
            return
        speak("Dimmi il numero dell'elemento da rimuovere, es. '1' per il primo.")
        index_str = take_command()
        if index_str.isdigit():
            idx = int(index_str) - 1
            if 0 <= idx < len(todo_list):
                removed = todo_list.pop(idx)
                speak(f"Ho rimosso {removed}.")
            else:
                speak("Indice non valido.")
        else:
            speak("Non ho capito.")
    elif "leggi" in choice or "mostra" in choice:
        if not todo_list:
            speak("La lista è vuota.")
        else:
            speak("Ecco le tue cose da fare:")
            for i, item in enumerate(todo_list, 1):
                speak(f"{i}. {item}")
    else:
        speak("Comando non riconosciuto.")

def send_email():
    speak("A chi vuoi inviare l'email?")
    recipient = take_command()
    speak("Oggetto dell'email?")
    subject = take_command()
    speak("Testo del messaggio?")
    body = take_command()

    # Esempio con Gmail (assicurati di avere le credenziali corrette / App Password)
    sender_email = "tuo.indirizzo@gmail.com"
    sender_password = "la_tua_password_o_app_password"
    receiver_email = recipient

    try:
        msg = MIMEMultipart()
        msg["From"] = sender_email
        msg["To"] = receiver_email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain"))

        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.starttls()
        server.login(sender_email, sender_password)
        server.send_message(msg)
        server.quit()
        speak("Email inviata correttamente.")
    except Exception as e:
        logging.error(f"[ERRORE] send_email: {e}")
        speak("Errore durante l'invio dell'email.")

def translate_text():
    if not HAS_TRANSLATOR:
        speak("Manca il modulo googletrans, non posso tradurre.")
        return
    speak("Dimmi la frase da tradurre.")
    phrase = take_command()
    if not phrase:
        return
    speak("Vuoi tradurla in inglese o in italiano?")
    lang = take_command()

    translator = Translator()
    try:
        if "inglese" in lang:
            result = translator.translate(phrase, src='it', dest='en')
            speak(f"La traduzione in inglese è: {result.text}")
        elif "italiano" in lang:
            result = translator.translate(phrase, src='en', dest='it')
            speak(f"La traduzione in italiano è: {result.text}")
        else:
            speak("Lingua non riconosciuta.")
    except Exception as e:
        logging.error(f"[ERRORE] translate_text: {e}")
        speak("Errore durante la traduzione.")

# -----------------------------
# CHIAMATA A PaLM 2 (chat-bison-001) con Service Account JSON
# -----------------------------
def gemini_query(question):
    """
    Esegue la richiesta al modello Google PaLM 2 (chat-bison-001),
    usando il file di Service Account JSON per ottenere un token OAuth2.
    """
    # 1) Carichiamo le credenziali dal file .json
    try:
        credentials = service_account.Credentials.from_service_account_file(
            SERVICE_ACCOUNT_FILE,
            scopes=SCOPES
        )
        # 2) Aggiorniamo (refresh) per ottenere il token
        credentials.refresh(Request())
        token = credentials.token
    except Exception as e:
        logging.error(f"[ERRORE] gemini_query: caricamento credenziali: {e}")
        return "Impossibile caricare le credenziali Service Account."

    # Aggiorna la memoria con la domanda dell'utente
    conversation_memory.append({"role": "user", "text": question})

    # Costruisci il contesto della conversazione
    context_parts = [persona_prompt + "\n\n"]
    for msg in conversation_memory:
        if msg["role"] == "user":
            context_parts.append(f"Utente: {msg['text']}\n")
        else:
            context_parts.append(f"Gideon: {msg['text']}\n")

    final_context = "".join(context_parts)

    model_name = "models/gemini-1p5-chat"
url = f"https://generativelanguage.googleapis.com/v1beta3/{model_name}:generateMessage"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

    body = {
        "prompt": {
            "messages": [
                {"author": "user", "content": final_context}
            ]
        },
        "temperature": 0.2,
        "candidateCount": 1,
        "topK": 40,
        "topP": 0.95
        # "maxOutputTokens": 512  # se vuoi limitare la lunghezza della risposta
    }

    try:
        response = requests.post(url, headers=headers, json=body, timeout=10)
        if response.status_code == 200:
            data = response.json()
            candidates = data.get("candidates", [])
            if candidates:
                answer = candidates[0].get("content", "")
            else:
                answer = "Nessuna risposta dal modello di Google."
        else:
            logging.error(f"[ERRORE] gemini_query: {response.text}")
            answer = f"Errore nella richiesta a PaLM 2. Codice: {response.status_code}"

        # Salviamo la risposta nella cronologia
        conversation_memory.append({"role": "assistant", "text": answer})
        return answer

    except requests.exceptions.RequestException as e:
        logging.error(f"[ERRORE] gemini_query: {e}")
        return "Errore di rete o timeout nella richiesta a PaLM 2."

# -----------------------------
# LOGICA DEI COMANDI
# -----------------------------
def handle_command(command):
    """
    Se la frase contiene una parola di attivazione, esegue sub-comandi;
    altrimenti risponde con PaLM 2.
    """
    if is_activation_command(command):
        sub_cmd = take_command()
        # Risposte molto semplici
        if maybe_personal_reply(sub_cmd):
            return
        # Controllo dispositivi smart
        if control_smart_device(sub_cmd):
            return
        # Comandi di uscita
        if "esci" in sub_cmd or "termina" in sub_cmd or "addio" in sub_cmd:
            speak("Va bene, alla prossima!")
            speech_queue.put(None)
            os._exit(0)
        elif "meteo" in sub_cmd:
            weather()
        elif "notizie" in sub_cmd:
            fetch_news()
        elif "risorse" in sub_cmd or "cpu" in sub_cmd or "memoria" in sub_cmd:
            monitor_resources()
        elif "apri" in sub_cmd or "website" in sub_cmd or "sito" in sub_cmd:
            open_webpage()
        elif "calendario" in sub_cmd:
            show_calendar()
        elif "converti" in sub_cmd or "convertitore" in sub_cmd:
            unit_converter()
        elif "promemoria" in sub_cmd:
            set_reminder()
        elif "barzelletta" in sub_cmd or "joke" in sub_cmd:
            tell_joke()
        elif "foto" in sub_cmd or "fotocamera" in sub_cmd:
            take_photo()
        elif "cerca" in sub_cmd and "google" in sub_cmd:
            search_google()
        elif "ore sono" in sub_cmd or "che ora" in sub_cmd:
            tell_time()
        elif "calcola" in sub_cmd or "calcolatrice" in sub_cmd:
            calculator()
        elif "todo" in sub_cmd or "cose da fare" in sub_cmd:
            manage_todo()
        elif "invia email" in sub_cmd or "manda email" in sub_cmd:
            send_email()
        elif "traduci" in sub_cmd or "traduzione" in sub_cmd:
            translate_text()
        else:
            # Se non è un comando conosciuto, chiediamo a PaLM 2
            answer = gemini_query(sub_cmd)
            speak(answer)
    else:
        # Se non ha usato la keyword di attivazione, ma è una frase generica
        if maybe_personal_reply(command):
            return
        else:
            answer = gemini_query(command)
            speak(answer)

def assistant_loop():
    """
    Loop infinito che ascolta e crea thread per gestire i comandi.
    """
    while True:
        command = take_command()
        if command:
            t = threading.Thread(target=handle_command, args=(command,))
            t.start()

# -----------------------------
# PAGINA WEB CON LA SFERA 3D
# -----------------------------
HTML_PAGE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Gideon 3D Sphere</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
#info { position: absolute; top: 10px; width: 100%; text-align: center; z-index: 100; color: #fff; font-family: sans-serif; }
</style>
</head>
<body>
<div id="info">La sfera 3D di Gideon</div>

<!-- Three.js -->
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/build/three.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>

<script>
let scene, camera, renderer, points;
let originalPositions = [];
let speaking = false;

const numParticles = 2000;
const radius = 2;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 100);
    camera.position.z = 8;

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Particles geometry
    const geometry = new THREE.BufferGeometry();
    const positions = new Float32Array(numParticles * 3);

    for (let i = 0; i < numParticles; i++) {
        const phi = Math.random() * 2 * Math.PI;
        const costheta = Math.random() * 2 - 1;
        const theta = Math.acos(costheta);
        const r = radius;

        const x = r * Math.sin(theta) * Math.cos(phi);
        const y = r * Math.sin(theta) * Math.sin(phi);
        const z = r * Math.cos(theta);

        positions[i*3] = x;
        positions[i*3+1] = y;
        positions[i*3+2] = z;
        originalPositions.push({x, y, z});
    }

    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

    const material = new THREE.PointsMaterial({
        color: 0x00ffff,
        size: 0.04
    });

    points = new THREE.Points(geometry, material);
    scene.add(points);

    const light = new THREE.PointLight(0xffffff, 1, 100);
    light.position.set(10, 10, 10);
    scene.add(light);

    window.addEventListener('resize', onWindowResize, false);
    animate();
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
    requestAnimationFrame(animate);
    points.rotation.y += 0.001;
    disperseOrCompose();
    renderer.render(scene, camera);
}

function disperseOrCompose() {
    const positions = points.geometry.attributes.position.array;
    for (let i = 0; i < numParticles; i++) {
        const idx = i*3;
        let ox = originalPositions[i].x;
        let oy = originalPositions[i].y;
        let oz = originalPositions[i].z;

        if (speaking) {
            // Disperdi leggermente
            positions[idx] += (Math.random() - 0.5) * 0.01;
            positions[idx+1] += (Math.random() - 0.5) * 0.01;
            positions[idx+2] += (Math.random() - 0.5) * 0.01;
        } else {
            // Torna gradualmente alla posizione iniziale
            positions[idx] += (ox - positions[idx]) * 0.05;
            positions[idx+1] += (oy - positions[idx+1]) * 0.05;
            positions[idx+2] += (oz - positions[idx+2]) * 0.05;
        }
    }
    points.geometry.attributes.position.needsUpdate = true;
}

// Socket.IO
const socket = io();
socket.on('connect', () => {
    console.log('Connesso al server SocketIO.');
});

socket.on('speaking_status', data => {
    speaking = data.speaking;
    console.log("speaking =", speaking);
});

init();
</script>
</body>
</html>
"""

@app.route("/")
def index():
    return HTML_PAGE

# -----------------------------
# MAIN
# -----------------------------
def main():
    global VOICE_ID
    # Recuperiamo (facoltativamente) l'ID della voce ElevenLabs
    VOICE_ID = get_voice_id("Rachel")
    if not VOICE_ID:
        print("[ERRORE] Non ho potuto caricare l'ID della voce. "
              "Controlla la tua chiave ElevenLabs o il nome della voce.")

    # Thread che esegue la sintesi vocale in background
    t_speak = threading.Thread(target=speak_worker, daemon=True)
    t_speak.start()

    # Thread che esegue il loop di ascolto/risposte
    t_assist = threading.Thread(target=assistant_loop, daemon=True)
    t_assist.start()

    # Frase iniziale
    speak("Ciao, sono Gideon. Fammi pure una domanda o dimmi un comando!")

    # Avviamo Flask-SocketIO
    socketio.run(app, host="127.0.0.1", port=5000, debug=False, use_reloader=False)

if __name__ == "__main__":
    main() la voce sembra di quello di topolino poco realistica poi non credo sia necessario il json se ho l api