import socket
import sys
import os
import json
import time
from pathlib import Path
from typing import List, Optional
from google import genai
from google.genai import types
from dotenv import load_dotenv

load_dotenv() 

from config import *


class TF5MixerController:
    def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT):
        self.host = host
        self.port = port
        self.socket = None
        self._ensure_cache_dir()
        self._cache = self._load_cache()
    
    def _connect(self):
        """Stabilisce la connessione se non già connesso."""
        if self.socket is None:
            print('Inizializzazione socket...')
            try:
                self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.socket.settimeout(5)
                self.socket.connect((self.host, self.port))
            except socket.error as e:
                self.socket = None
                raise ConnectionError(f"Impossibile connettersi al mixer: {e}")
    
    def _disconnect(self):
        """Chiude la connessione."""
        if self.socket:
            print('Chiusura socket...')
            try:
                self.socket.close()
                print('Socket chiuso!')
            except:
                print('Errore durante chiusura socket!')
                pass
            finally:
                self.socket = None
    
    def _send_command(self, command: str) -> str:
        """Invia un comando al mixer e restituisce la risposta."""
        max_retries = 2
        
        for attempt in range(max_retries):
            try:
                print(f'Tentativo di connessione {attempt} per {command}')
                self._connect()
                self.socket.sendall((command + '\n').encode('utf-8'))
                response = self.socket.recv(4096)
                decoded = response.decode('utf-8', errors='ignore').strip()
                print(f'Risposta {decoded}')
                return decoded
            
            except socket.error as e:
                print(f'Errore di connessione dopo {max_retries} tentativi: {e}')
                self._disconnect()
                
                if attempt < max_retries - 1:
                    time.sleep(0.1)
                    continue
                else:
                    return f"Errore di connessione dopo {max_retries} tentativi: {e}"
    
    def close(self):
        """Chiude la connessione (da chiamare alla fine)."""
        self._disconnect()
    
    def __enter__(self):
        """Context manager entry."""
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()
    
    def _ensure_cache_dir(self):
        """Crea la directory di cache se non esiste."""
        CACHE_DIR.mkdir(parents=True, exist_ok=True)
    
    def _load_cache(self) -> dict:
        """Carica la cache dal file."""
        if CACHE_FILE.exists():
            try:
                with open(CACHE_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception as e:
                print(f"⚠️  Errore nel caricamento della cache: {e}")
        return {"channels": {}, "mixes": {}, "timestamp": 0}
    
    def _save_cache(self):
        """Salva la cache nel file, convertendo -inf in null."""
        try:
            # Crea una copia della cache per la serializzazione
            cache_to_save = self._prepare_cache_for_json(self._cache)
            
            with open(CACHE_FILE, 'w', encoding='utf-8') as f:
                json.dump(cache_to_save, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"⚠️  Errore nel salvataggio della cache: {e}")
    
    def _prepare_cache_for_json(self, obj):
        """Converte ricorsivamente -inf in null per la serializzazione JSON."""
        if isinstance(obj, dict):
            return {k: self._prepare_cache_for_json(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [self._prepare_cache_for_json(item) for item in obj]
        elif isinstance(obj, float):
            # Converti -inf in null per JSON
            if obj == float('-inf'):
                return None
            return obj
        else:
            return obj
    
    def _is_cache_valid(self) -> bool:
        """Verifica se la cache è ancora valida."""
        if not self._cache.get("channels"):
            return False
        cache_age = time.time() - self._cache.get("timestamp", 0)
        return cache_age < CACHE_DURATION
    
    def _normalize_level_from_cache(self, level_value):
        """Normalizza il valore del livello dalla cache (gestisce None come -inf)."""
        if level_value is None:
            return float('-inf')
        return level_value
    
    def _update_channel_cache(self, channel: int):
        """Aggiorna la cache per un singolo canale leggendo dal mixer.
        
        Args:
            channel: Numero del canale (1-40)
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return
        
        ch_idx = channel - 1
        
        # Leggi nome
        resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0")
        name = self._parse_name(resp_name)
        
        # Leggi stato ON/OFF
        resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0")
        is_on = self._parse_value(resp_on) == "1"
        
        # Leggi livello
        resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0")
        level_raw = self._parse_value(resp_level)
        try:
            level_int = int(level_raw)
            level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
        except:
            level_db = None
        
        # Leggi pan
        resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0")
        pan_raw = self._parse_value(resp_pan)
        try:
            pan_value = int(pan_raw)
        except:
            pan_value = None
        
        # Inizializza la struttura channels se non esiste
        if "channels" not in self._cache:
            self._cache["channels"] = {}
        
        # Aggiorna la cache
        self._cache["channels"][str(channel)] = {
            "channel": channel,
            "name": name,
            "on": is_on,
            "level_db": level_db,
            "pan": pan_value
        }
        
        self._save_cache()
    
    def _update_mix_cache(self, mix_number: int):
        """Aggiorna la cache per un singolo mix/aux leggendo dal mixer.
        
        Args:
            mix_number: Numero del mix (1-20)
        """
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return
        
        mix_idx = mix_number - 1
        
        # Leggi nome
        resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0")
        name = self._parse_name(resp_name)
        
        # Leggi stato ON/OFF
        resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0")
        is_on = self._parse_value(resp_on) == "1"
        
        # Leggi livello
        resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0")
        level_raw = self._parse_value(resp_level)
        try:
            level_int = int(level_raw)
            level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
        except:
            level_db = None
        
        # Inizializza la struttura mixes se non esiste
        if "mixes" not in self._cache:
            self._cache["mixes"] = {}
        
        # Aggiorna la cache
        self._cache["mixes"][str(mix_number)] = {
            "mix": mix_number,
            "name": name,
            "on": is_on,
            "level_db": level_db
        }
        
        self._save_cache()
    
    def _parse_name(self, response: str) -> str:
        """Estrae il nome tra virgolette dalla risposta."""
        try:
            start = response.find('"') + 1
            end = response.rfind('"')
            if start > 0 and end > start:
                return response[start:end]
            return "Sconosciuto"
        except:
            return "Errore"
    
    def _parse_value(self, response: str) -> str:
        """Estrae l'ultimo valore da una risposta OK."""
        parts = response.split()
        if len(parts) > 0 and parts[0] == "OK":
            return parts[-1]
        return "N/A"
    
    def refresh_cache(self) -> dict:
        """Aggiorna la cache leggendo tutti i canali e mix dal mixer.
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        print("🔄 Aggiornamento cache completo in corso...")
        channels_data = {}
        mixes_data = {}
        
        # Aggiorna canali
        for ch in range(1, TF5_INPUT_CHANNELS + 1):
            ch_idx = ch - 1
            
            # Leggi nome
            resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0")
            name = self._parse_name(resp_name)
            
            # Leggi stato ON/OFF
            resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0")
            is_on = self._parse_value(resp_on) == "1"
            
            # Leggi livello
            resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0")
            level_raw = self._parse_value(resp_level)
            try:
                level_int = int(level_raw)
                level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
            except:
                level_db = None
            
            # Leggi pan
            resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0")
            pan_raw = self._parse_value(resp_pan)
            try:
                pan_value = int(pan_raw)
            except:
                pan_value = None
            
            channels_data[str(ch)] = {
                "channel": ch,
                "name": name,
                "on": is_on,
                "level_db": level_db,
                "pan": pan_value
            }
            
            time.sleep(0.05)
        
        # Aggiorna mix
        for mix in range(1, TF5_MIX_BUSSES + 1):
            mix_idx = mix - 1
            
            # Leggi nome
            resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0")
            name = self._parse_name(resp_name)
            
            # Leggi stato ON/OFF
            resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0")
            is_on = self._parse_value(resp_on) == "1"
            
            # Leggi livello
            resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0")
            level_raw = self._parse_value(resp_level)
            try:
                level_int = int(level_raw)
                level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
            except:
                level_db = None
            
            mixes_data[str(mix)] = {
                "mix": mix,
                "name": name,
                "on": is_on,
                "level_db": level_db
            }
            
            time.sleep(0.05)
        
        self._cache = {
            "channels": channels_data,
            "mixes": mixes_data,
            "timestamp": time.time()
        }
        self._save_cache()
        
        print(f"✅ Cache aggiornata con {len(channels_data)} canali e {len(mixes_data)} mix")
        return {
            "status": "success",
            "message": f"Cache aggiornata con {len(channels_data)} canali e {len(mixes_data)} mix",
            "channels_count": len(channels_data),
            "mixes_count": len(mixes_data)
        }
    
    def recall_scene(self, bank: str, scene_number: int) -> dict:
        """Richiama una scena dal banco A o B.
        
        Args:
            bank: Il banco della scena ('a' o 'b')
            scene_number: Il numero della scena (0-99)
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if bank.lower() not in ['a', 'b']:
            return {"status": "error", "message": "Il banco deve essere 'a' o 'b'"}
        if not 0 <= scene_number <= 99:
            return {"status": "error", "message": "Il numero scena deve essere tra 0 e 99"}
        
        command = f"ssrecall_ex scene_{bank.lower()} {scene_number}"
        response = self._send_command(command)
        
        # Invalida la cache dopo il cambio scena
        self._cache["timestamp"] = 0
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Scena {bank.upper()}{scene_number} richiamata. Cache invalidata.",
            "response": response
        }
    
    def set_channel_level(self, channel: int, level_db: float) -> dict:
        """Imposta il livello del fader di un canale in dB.
        
        Args:
            channel: Numero del canale (1-40)
            level_db: Livello in dB (da -inf a +10.0)
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        
        # Converti dB in valore interno (moltiplicato per 100)
        if level_db <= -138:
            internal_value = -32768  # -inf
        else:
            internal_value = int(level_db * 100)
        
        command = f"set MIXER:Current/InCh/Fader/Level {channel-1} 0 {internal_value}"
        response = self._send_command(command)
        
        # Aggiorna cache dal mixer per avere dati precisi
        if "OK" in response:
            self._update_channel_cache(channel)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Canale {channel} impostato a {level_db:+.1f} dB",
            "response": response
        }
    
    def set_channel_on_off(self, channel: int, state: bool) -> dict:
        """Accende o spegne un canale.
        
        Args:
            channel: Numero del canale (1-40)
            state: True per accendere, False per spegnere
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        
        value = 1 if state else 0
        command = f"set MIXER:Current/InCh/Fader/On {channel-1} 0 {value}"
        response = self._send_command(command)
        
        # Aggiorna cache dal mixer per avere dati precisi
        if "OK" in response:
            self._update_channel_cache(channel)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Canale {channel} {'acceso' if state else 'spento'}",
            "response": response
        }
    
    def set_channel_pan(self, channel: int, pan_value: int) -> dict:
        """Imposta il pan di un canale.
        
        Args:
            channel: Numero del canale (1-40)
            pan_value: Valore pan da -63 (sinistra) a +63 (destra), 0 è centro
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        if not -63 <= pan_value <= 63:
            return {"status": "error", "message": "Il pan deve essere tra -63 e +63"}
        
        command = f"set MIXER:Current/InCh/ToSt/Pan {channel-1} 0 {pan_value}"
        response = self._send_command(command)
        
        # Aggiorna cache dal mixer per avere dati precisi
        if "OK" in response:
            self._update_channel_cache(channel)
        
        pan_desc = "centro"
        if pan_value < 0:
            pan_desc = f"sinistra {abs(pan_value)}"
        elif pan_value > 0:
            pan_desc = f"destra {pan_value}"
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Canale {channel} pan impostato a {pan_desc}",
            "response": response
        }
    
    def set_mix_level(self, mix_number: int, level_db: float) -> dict:
        """Imposta il livello di un mix/aux.
        
        Args:
            mix_number: Numero del mix (1-20)
            level_db: Livello in dB (da -inf a +10.0)
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        if level_db <= -138:
            internal_value = -32768
        else:
            internal_value = int(level_db * 100)
        
        command = f"set MIXER:Current/Mix/Fader/Level {mix_number-1} 0 {internal_value}"
        response = self._send_command(command)
        
        # Aggiorna cache dal mixer per avere dati precisi
        if "OK" in response:
            self._update_mix_cache(mix_number)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Mix {mix_number} impostato a {level_db:+.1f} dB",
            "response": response
        }
    
    def set_mix_on_off(self, mix_number: int, state: bool) -> dict:
        """Accende o spegne un mix/aux.
        
        Args:
            mix_number: Numero del mix (1-20)
            state: True per accendere, False per spegnere
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        value = 1 if state else 0
        command = f"set MIXER:Current/Mix/Fader/On {mix_number-1} 0 {value}"
        response = self._send_command(command)
        
        # Aggiorna cache dal mixer per avere dati precisi
        if "OK" in response:
            self._update_mix_cache(mix_number)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Mix {mix_number} {'acceso' if state else 'spento'}",
            "response": response
        }
    
    def mute_multiple_channels(self, channels: List[int]) -> dict:
        """Muta più canali contemporaneamente.
        
        Args:
            channels: Lista di numeri di canale da mutare (es: [1, 2, 5, 8])
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        results = []
        for ch in channels:
            result = self.set_channel_on_off(ch, False)
            results.append(result)
        
        success_count = sum(1 for r in results if r["status"] == "success")
        return {
            "status": "success" if success_count == len(channels) else "partial",
            "message": f"Mutati {success_count}/{len(channels)} canali: {channels}",
            "details": results
        }
    
    def unmute_multiple_channels(self, channels: List[int]) -> dict:
        """Riattiva più canali contemporaneamente.
        
        Args:
            channels: Lista di numeri di canale da riattivare (es: [1, 2, 5, 8])
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        results = []
        for ch in channels:
            result = self.set_channel_on_off(ch, True)
            results.append(result)
        
        success_count = sum(1 for r in results if r["status"] == "success")
        return {
            "status": "success" if success_count == len(channels) else "partial",
            "message": f"Riattivati {success_count}/{len(channels)} canali: {channels}",
            "details": results
        }
    
    def get_channel_info(self, channel: int, force_refresh: bool = False) -> dict:
        """Legge le informazioni di un canale (nome, livello, stato, pan).
        Usa la cache se disponibile e valida.
        
        Args:
            channel: Numero del canale (1-40)
            force_refresh: Se True, ignora la cache e legge dal mixer
        
        Returns:
            Un dizionario con tutte le informazioni del canale
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        
        # Usa cache se valida e non forzato il refresh
        if not force_refresh and self._is_cache_valid():
            cached_data = self._cache.get("channels", {}).get(str(channel))
            if cached_data:
                # Normalizza level_db (None -> -inf)
                level_db = self._normalize_level_from_cache(cached_data.get("level_db"))
                return {
                    "status": "success",
                    "source": "cache",
                    "channel": cached_data["channel"],
                    "name": cached_data["name"],
                    "on": cached_data["on"],
                    "level_db": level_db,
                    "pan": cached_data.get("pan")
                }
        
        # Altrimenti leggi dal mixer
        ch_idx = channel - 1
        
        resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0")
        name = self._parse_name(resp_name)
        
        resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0")
        is_on = self._parse_value(resp_on) == "1"
        
        resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0")
        level_raw = self._parse_value(resp_level)
        try:
            level_int = int(level_raw)
            level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
        except:
            level_db = None
        
        resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0")
        pan_raw = self._parse_value(resp_pan)
        try:
            pan_value = int(pan_raw)
        except:
            pan_value = None
        
        return {
            "status": "success",
            "source": "mixer",
            "channel": channel,
            "name": name,
            "on": is_on,
            "level_db": level_db,
            "pan": pan_value
        }
    
    def get_mix_info(self, mix_number: int, force_refresh: bool = False) -> dict:
        """Legge le informazioni di un mix/aux (nome, livello, stato).
        Usa la cache se disponibile e valida.
        
        Args:
            mix_number: Numero del mix (1-20)
            force_refresh: Se True, ignora la cache e legge dal mixer
        
        Returns:
            Un dizionario con tutte le informazioni del mix
        """
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        # Usa cache se valida e non forzato il refresh
        if not force_refresh and self._is_cache_valid():
            cached_data = self._cache.get("mixes", {}).get(str(mix_number))
            if cached_data:
                # Normalizza level_db (None -> -inf)
                level_db = self._normalize_level_from_cache(cached_data.get("level_db"))
                return {
                    "status": "success",
                    "source": "cache",
                    "mix": cached_data["mix"],
                    "name": cached_data["name"],
                    "on": cached_data["on"],
                    "level_db": level_db
                }
        
        # Altrimenti leggi dal mixer
        mix_idx = mix_number - 1
        
        resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0")
        name = self._parse_name(resp_name)
        
        resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0")
        is_on = self._parse_value(resp_on) == "1"
        
        resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0")
        level_raw = self._parse_value(resp_level)
        try:
            level_int = int(level_raw)
            level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
        except:
            level_db = None
        
        return {
            "status": "success",
            "source": "mixer",
            "mix": mix_number,
            "name": name,
            "on": is_on,
            "level_db": level_db
        }
    
    def search_channels_by_name(self, search_term: str) -> dict:
        """Cerca canali il cui nome contiene un determinato termine.
        Usa la cache per velocizzare la ricerca.
        
        Args:
            search_term: Il termine da cercare nei nomi dei canali (case-insensitive)
        
        Returns:
            Un dizionario con la lista dei canali trovati
        """
        # Aggiorna cache se non valida
        if not self._is_cache_valid():
            self.refresh_cache()
        
        search_lower = search_term.lower()
        found_channels = []
        
        for ch_str, info in self._cache.get("channels", {}).items():
            if search_lower in info.get("name", "").lower():
                # Normalizza level_db quando restituisci i risultati
                level_db = self._normalize_level_from_cache(info.get("level_db"))
                found_channels.append({
                    "channel": info["channel"],
                    "name": info["name"],
                    "on": info["on"],
                    "level_db": level_db
                })
        
        # Ordina per numero canale
        found_channels.sort(key=lambda x: x["channel"])
        
        return {
            "status": "success",
            "search_term": search_term,
            "found_count": len(found_channels),
            "channels": found_channels,
            "message": f"Trovati {len(found_channels)} canali contenenti '{search_term}'"
        }
    
    def search_mixes_by_name(self, search_term: str) -> dict:
        """Cerca mix/aux il cui nome contiene un determinato termine.
        Usa la cache per velocizzare la ricerca.
        
        Args:
            search_term: Il termine da cercare nei nomi dei mix (case-insensitive)
        
        Returns:
            Un dizionario con la lista dei mix trovati
        """
        # Aggiorna cache se non valida
        if not self._is_cache_valid():
            self.refresh_cache()
        
        search_lower = search_term.lower()
        found_mixes = []
        
        for mix_str, info in self._cache.get("mixes", {}).items():
            if search_lower in info.get("name", "").lower():
                # Normalizza level_db quando restituisci i risultati
                level_db = self._normalize_level_from_cache(info.get("level_db"))
                found_mixes.append({
                    "mix": info["mix"],
                    "name": info["name"],
                    "on": info["on"],
                    "level_db": level_db
                })
        
        # Ordina per numero mix
        found_mixes.sort(key=lambda x: x["mix"])
        
        return {
            "status": "success",
            "search_term": search_term,
            "found_count": len(found_mixes),
            "mixes": found_mixes,
            "message": f"Trovati {len(found_mixes)} mix contenenti '{search_term}'"
        }
    
    def get_all_channels_summary(self) -> dict:
        """Ottiene un riepilogo di tutti i canali con nome e stato.
        Usa la cache per velocizzare.
        
        Returns:
            Un dizionario con il riepilogo di tutti i canali
        """
        # Aggiorna cache se non valida
        if not self._is_cache_valid():
            self.refresh_cache()
        
        channels = []
        for ch_str, info in self._cache.get("channels", {}).items():
            channels.append({
                "channel": info["channel"],
                "name": info["name"],
                "on": info["on"]
            })
        
        # Ordina per numero canale
        channels.sort(key=lambda x: x["channel"])
        
        cache_age = time.time() - self._cache.get("timestamp", 0)
        
        return {
            "status": "success",
            "total_channels": len(channels),
            "channels": channels,
            "cache_age_seconds": int(cache_age),
            "message": f"Riepilogo di {len(channels)} canali (cache: {int(cache_age)}s fa)"
        }
    
    def get_all_mixes_summary(self) -> dict:
        """Ottiene un riepilogo di tutti i mix/aux con nome e stato.
        Usa la cache per velocizzare.
        
        Returns:
            Un dizionario con il riepilogo di tutti i mix
        """
        # Aggiorna cache se non valida
        if not self._is_cache_valid():
            self.refresh_cache()
        
        mixes = []
        for mix_str, info in self._cache.get("mixes", {}).items():
            mixes.append({
                "mix": info["mix"],
                "name": info["name"],
                "on": info["on"]
            })
        
        # Ordina per numero mix
        mixes.sort(key=lambda x: x["mix"])
        
        cache_age = time.time() - self._cache.get("timestamp", 0)
        
        return {
            "status": "success",
            "total_mixes": len(mixes),
            "mixes": mixes,
            "cache_age_seconds": int(cache_age),
            "message": f"Riepilogo di {len(mixes)} mix (cache: {int(cache_age)}s fa)"
        }

    def set_channel_to_mix_level(self, channel: int, mix_number: int, level_db: float) -> dict:
        """Imposta il livello di invio di un canale verso un mix/aux.
        
        Args:
            channel: Numero del canale (1-40)
            mix_number: Numero del mix (1-20)
            level_db: Livello in dB (da -inf a +10.0)
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        if level_db <= -138:
            internal_value = -32768
        else:
            internal_value = int(level_db * 100)
        
        command = f"set MIXER:Current/InCh/ToMix/Level {channel-1} {mix_number-1} {internal_value}"
        response = self._send_command(command)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Canale {channel} → Mix {mix_number} impostato a {level_db:+.1f} dB",
            "response": response
        }

    def set_channel_to_mix_on_off(self, channel: int, mix_number: int, state: bool) -> dict:
        """Accende o spegne l'invio di un canale verso un mix/aux.
        
        Args:
            channel: Numero del canale (1-40)
            mix_number: Numero del mix (1-20)
            state: True per accendere, False per spegnere
        
        Returns:
            Un dizionario con lo stato dell'operazione
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        value = 1 if state else 0
        command = f"set MIXER:Current/InCh/ToMix/On {channel-1} {mix_number-1} {value}"
        response = self._send_command(command)
        
        return {
            "status": "success" if "OK" in response else "error",
            "message": f"Invio canale {channel} → Mix {mix_number} {'acceso' if state else 'spento'}",
            "response": response
        }

    def get_channel_to_mix_info(self, channel: int, mix_number: int) -> dict:
        """Legge le informazioni dell'invio di un canale verso un mix.
        
        Args:
            channel: Numero del canale (1-40)
            mix_number: Numero del mix (1-20)
        
        Returns:
            Un dizionario con le informazioni del send
        """
        if not 1 <= channel <= TF5_INPUT_CHANNELS:
            return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"}
        if not 1 <= mix_number <= TF5_MIX_BUSSES:
            return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
        
        ch_idx = channel - 1
        mix_idx = mix_number - 1
        
        # Leggi livello
        resp_level = self._send_command(f"get MIXER:Current/InCh/ToMix/Level {ch_idx} {mix_idx}")
        level_raw = self._parse_value(resp_level)
        try:
            level_int = int(level_raw)
            level_db = level_int / 100.0 if level_int > -32768 else float('-inf')
        except:
            level_db = None
        
        # Leggi stato ON/OFF
        resp_on = self._send_command(f"get MIXER:Current/InCh/ToMix/On {ch_idx} {mix_idx}")
        is_on = self._parse_value(resp_on) == "1"
        
        return {
            "status": "success",
            "channel": channel,
            "mix": mix_number,
            "on": is_on,
            "level_db": level_db,
            "message": f"Canale {channel} → Mix {mix_number}: {level_db:+.1f} dB ({'ON' if is_on else 'OFF'})"
        }