"""
Module de mise à jour automatique pour MyMind Desktop Client
"""
import os
import sys
import json
import requests
import hashlib
import shutil
from pathlib import Path
from typing import Dict, Optional, List, Tuple, Callable
import threading
import time
from datetime import datetime

class Updater:
    """Gère les mises à jour automatiques de l'application"""
    
    def __init__(self, api_client, config, base_dir: Optional[Path] = None):
        self.api_client = api_client
        self.config = config
        self.base_dir = base_dir or self._get_base_directory()
        self.update_dir = self.base_dir / 'update'
        self.update_dir.mkdir(exist_ok=True)
        self.version_file = self.base_dir / 'version.json'
        self.current_version = self._get_current_version()
        self.log_file = self.base_dir / 'update.log'
        self._log("=== Initialisation du système de mise à jour ===")
        
    def _get_base_directory(self) -> Path:
        """Retourne le répertoire de base de l'application"""
        if getattr(sys, 'frozen', False):
            # Mode exécutable
            return Path(sys.executable).parent
        elif Path(__file__).parent.name == 'app':
            # Mode portable
            return Path(__file__).parent.parent
        else:
            # Mode développement
            return Path(__file__).parent
    
    def _log(self, message: str, level: str = "INFO"):
        """Journalise un message dans le fichier de log"""
        try:
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            log_message = f"[{timestamp}] [{level}] {message}\n"
            with open(self.log_file, 'a', encoding='utf-8') as f:
                f.write(log_message)
            print(f"[Updater] {log_message.strip()}")
            
            # Limiter la taille du fichier de log (garder les 1000 dernières lignes)
            self._limit_log_size()
        except Exception as e:
            print(f"[Updater] Erreur lors de l'écriture du log: {e}")
    
    def _limit_log_size(self, max_lines: int = 1000):
        """Limite la taille du fichier de log en gardant les dernières lignes"""
        try:
            if not self.log_file.exists():
                return
            
            # Vérifier la taille du fichier (limiter à ~500KB)
            if self.log_file.stat().st_size > 500 * 1024:
                # Lire toutes les lignes
                with open(self.log_file, 'r', encoding='utf-8') as f:
                    lines = f.readlines()
                
                # Garder seulement les dernières max_lines lignes
                if len(lines) > max_lines:
                    with open(self.log_file, 'w', encoding='utf-8') as f:
                        f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [INFO] Log tronqué (conservation des {max_lines} dernières lignes)\n")
                        f.writelines(lines[-max_lines:])
        except Exception:
            pass  # Ignorer les erreurs de limitation de log
    
    def _get_current_version(self) -> str:
        """Récupère la version actuelle de l'application"""
        if self.version_file.exists():
            try:
                with open(self.version_file, 'r', encoding='utf-8') as f:
                    version_data = json.load(f)
                    return version_data.get('version', '1.0.0')
            except Exception as e:
                print(f"[Updater] Erreur lors de la lecture de la version: {e}")
        return '1.0.0'
    
    def _save_version(self, version: str):
        """Sauvegarde la version actuelle"""
        try:
            version_data = {
                'version': version,
                'updated_at': time.strftime('%Y-%m-%d %H:%M:%S')
            }
            with open(self.version_file, 'w', encoding='utf-8') as f:
                json.dump(version_data, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"[Updater] Erreur lors de la sauvegarde de la version: {e}")
    
    def _check_recent_update(self, hours_threshold: int = 24) -> Optional[str]:
        """
        Vérifie si une mise à jour récente a eu lieu (dans les dernières heures)
        Retourne la date de mise à jour si récente, None sinon
        """
        try:
            if not self.version_file.exists():
                return None
            
            with open(self.version_file, 'r', encoding='utf-8') as f:
                version_data = json.load(f)
            
            updated_at_str = version_data.get('updated_at')
            if not updated_at_str:
                return None
            
            # Parser la date
            try:
                updated_at = time.strptime(updated_at_str, '%Y-%m-%d %H:%M:%S')
                updated_timestamp = time.mktime(updated_at)
                current_timestamp = time.time()
                hours_since_update = (current_timestamp - updated_timestamp) / 3600
                
                if hours_since_update < hours_threshold:
                    return updated_at_str
            except ValueError:
                # Format de date invalide, ignorer
                pass
            
            return None
        except Exception:
            return None
    
    def check_for_updates(self) -> Optional[Dict]:
        """
        Vérifie s'il y a des mises à jour disponibles
        Retourne les informations de mise à jour ou None
        Si aucune mise à jour n'est disponible mais qu'une mise à jour récente a eu lieu,
        retourne un dict avec 'recently_updated': True
        """
        try:
            # Vérifier si une mise à jour récente a eu lieu
            recently_updated = self._check_recent_update()
            
            # Récupérer le fichier de version depuis le serveur
            api_url = self.config.get('api_url') or self.config.get_api_url()
            version_url = f"{api_url}/update/version.json"
            
            self._log(f"Vérification des mises à jour sur: {version_url}")
            response = requests.get(version_url, timeout=10)
            response.raise_for_status()
            
            server_version_data = response.json()
            server_version = server_version_data.get('version', '1.0.0')
            
            self._log(f"Version actuelle: {self.current_version}, Version serveur: {server_version}")
            
            if self._is_newer_version(server_version, self.current_version):
                self._log(f"Mise à jour disponible: {server_version}", "INFO")
                return {
                    'available': True,
                    'current_version': self.current_version,
                    'new_version': server_version,
                    'files': server_version_data.get('files', []),
                    'changelog': server_version_data.get('changelog', ''),
                    'mandatory': server_version_data.get('mandatory', False)
                }
            else:
                if recently_updated:
                    self._log("Application récemment mise à jour")
                    return {
                        'available': False,
                        'recently_updated': True,
                        'current_version': self.current_version,
                        'updated_at': recently_updated
                    }
                else:
                    self._log("Aucune mise à jour disponible")
                    return None
                
        except requests.exceptions.RequestException as e:
            self._log(f"Erreur lors de la vérification des mises à jour: {e}", "ERROR")
            return None
        except Exception as e:
            self._log(f"Erreur inattendue: {e}", "ERROR")
            import traceback
            self._log(traceback.format_exc(), "ERROR")
            return None
    
    def _is_newer_version(self, version1: str, version2: str) -> bool:
        """Compare deux versions (format: X.Y.Z)"""
        try:
            v1_parts = [int(x) for x in version1.split('.')]
            v2_parts = [int(x) for x in version2.split('.')]
            
            # Normaliser les longueurs
            max_len = max(len(v1_parts), len(v2_parts))
            v1_parts.extend([0] * (max_len - len(v1_parts)))
            v2_parts.extend([0] * (max_len - len(v2_parts)))
            
            return v1_parts > v2_parts
        except Exception:
            # En cas d'erreur, comparer comme des strings
            return version1 > version2
    
    def download_file(self, file_path: str, file_hash: Optional[str] = None, progress_callback: Optional[Callable] = None) -> bool:
        """Télécharge un fichier depuis le serveur avec suivi de progression"""
        try:
            api_url = self.config.get('api_url') or self.config.get_api_url()
            file_url = f"{api_url}/update/files/{file_path}"
            
            self._log(f"Début du téléchargement: {file_path}")
            if progress_callback:
                progress_callback(f"Téléchargement de {file_path}...", 0)
            
            response = requests.get(file_url, timeout=30, stream=True)
            response.raise_for_status()
            
            # Obtenir la taille totale du fichier
            total_size = int(response.headers.get('content-length', 0))
            
            # Créer le répertoire de destination si nécessaire
            dest_path = self.update_dir / file_path
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            
            # Télécharger le fichier avec suivi de progression
            downloaded = 0
            with open(dest_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
                        downloaded += len(chunk)
                        if total_size > 0 and progress_callback:
                            progress = int((downloaded / total_size) * 100)
                            progress_callback(f"Téléchargement de {file_path}... ({downloaded}/{total_size} octets)", progress)
            
            # Vérifier le hash si fourni
            if file_hash:
                self._log(f"Vérification du hash pour {file_path}")
                if progress_callback:
                    progress_callback(f"Vérification de {file_path}...", 90)
                calculated_hash = self._calculate_file_hash(dest_path)
                if calculated_hash != file_hash:
                    self._log(f"ERREUR: Hash invalide pour {file_path} (attendu: {file_hash[:16]}..., obtenu: {calculated_hash[:16]}...)", "ERROR")
                    dest_path.unlink()
                    return False
                self._log(f"Hash vérifié avec succès pour {file_path}")
            
            self._log(f"Fichier téléchargé avec succès: {file_path} ({downloaded} octets)")
            if progress_callback:
                progress_callback(f"{file_path} téléchargé", 100)
            return True
            
        except Exception as e:
            self._log(f"Erreur lors du téléchargement de {file_path}: {e}", "ERROR")
            import traceback
            self._log(traceback.format_exc(), "ERROR")
            return False
    
    def _calculate_file_hash(self, file_path: Path) -> str:
        """Calcule le hash SHA256 d'un fichier"""
        sha256_hash = hashlib.sha256()
        with open(file_path, "rb") as f:
            for byte_block in iter(lambda: f.read(4096), b""):
                sha256_hash.update(byte_block)
        return sha256_hash.hexdigest()
    
    def apply_updates(self, update_info: Dict, progress_callback: Optional[Callable[[str, int], None]] = None) -> Tuple[bool, str]:
        """
        Applique les mises à jour avec suivi de progression
        progress_callback(status_message, progress_percent)
        Retourne (succès, message)
        """
        try:
            files = update_info.get('files', [])
            if not files:
                self._log("Aucun fichier à mettre à jour", "WARNING")
                return False, "Aucun fichier à mettre à jour"
            
            self._log(f"=== Début de la mise à jour vers la version {update_info['new_version']} ===")
            self._log(f"Nombre de fichiers à mettre à jour: {len(files)}")
            
            if progress_callback:
                progress_callback("Initialisation de la mise à jour...", 0)
            
            # Télécharger tous les fichiers
            downloaded_files = []
            total_files = len(files)
            
            for index, file_info in enumerate(files):
                file_path = file_info.get('path')
                file_hash = file_info.get('hash')
                
                # Calculer la progression (0-70% pour le téléchargement)
                download_progress = int((index / total_files) * 70)
                if progress_callback:
                    progress_callback(f"Téléchargement ({index + 1}/{total_files}): {file_path}...", download_progress)
                
                def file_progress_callback(msg, progress):
                    # Ajuster la progression pour ce fichier spécifique
                    file_progress = download_progress + int((progress / 100) * (70 / total_files))
                    if progress_callback:
                        progress_callback(msg, file_progress)
                
                if self.download_file(file_path, file_hash, file_progress_callback):
                    downloaded_files.append((file_path, file_info))
                    self._log(f"Fichier téléchargé: {file_path}")
                else:
                    error_msg = f"Échec du téléchargement de {file_path}"
                    self._log(error_msg, "ERROR")
                    return False, error_msg
            
            # Appliquer les mises à jour (70-95%)
            if progress_callback:
                progress_callback("Application des mises à jour...", 70)
            
            self._log("Début de l'application des mises à jour")
            
            for index, (file_path, file_info) in enumerate(downloaded_files):
                source = self.update_dir / file_path
                dest = self.base_dir / file_path
                
                # Calculer la progression (70-95%)
                apply_progress = 70 + int((index / len(downloaded_files)) * 25)
                if progress_callback:
                    progress_callback(f"Application de {file_path}...", apply_progress)
                
                # Créer le répertoire de destination si nécessaire
                dest.parent.mkdir(parents=True, exist_ok=True)
                
                # Sauvegarder l'ancien fichier si il existe
                if dest.exists():
                    backup = dest.with_suffix(dest.suffix + '.backup')
                    try:
                        shutil.copy2(dest, backup)
                        self._log(f"Ancien fichier sauvegardé: {backup}")
                    except Exception as e:
                        self._log(f"Erreur lors de la sauvegarde de {file_path}: {e}", "WARNING")
                
                # Copier le nouveau fichier
                try:
                    shutil.copy2(source, dest)
                    self._log(f"Fichier mis à jour: {file_path}")
                except Exception as e:
                    error_msg = f"Erreur lors de la copie de {file_path}: {e}"
                    self._log(error_msg, "ERROR")
                    return False, error_msg
            
            # Installer les dépendances si nécessaire (90-95%)
            if progress_callback:
                progress_callback("Vérification des dépendances...", 90)
            
            dependencies = update_info.get('dependencies', [])
            if dependencies:
                self._log(f"Installation des dépendances: {', '.join(dependencies)}")
                if not self._install_dependencies(dependencies, progress_callback):
                    self._log("Avertissement: Certaines dépendances n'ont pas pu être installées", "WARNING")
            
            # Mettre à jour la version (95-100%)
            if progress_callback:
                progress_callback("Mise à jour de la version...", 95)
            
            self._save_version(update_info['new_version'])
            self.current_version = update_info['new_version']
            self._log(f"Version mise à jour: {update_info['new_version']}")
            
            # Nettoyer les fichiers de mise à jour
            if progress_callback:
                progress_callback("Nettoyage des fichiers temporaires...", 98)
            self._cleanup_update_files()
            
            if progress_callback:
                progress_callback("Mise à jour terminée avec succès!", 100)
            
            self._log(f"=== Mise à jour terminée avec succès vers la version {update_info['new_version']} ===")
            return True, f"Mise à jour vers la version {update_info['new_version']} réussie"
            
        except Exception as e:
            error_msg = f"Erreur lors de l'application des mises à jour: {e}"
            self._log(error_msg, "ERROR")
            import traceback
            self._log(traceback.format_exc(), "ERROR")
            return False, error_msg
    
    def _install_dependencies(self, dependencies: List[str], progress_callback: Optional[Callable] = None) -> bool:
        """Installe les dépendances Python requises"""
        try:
            import subprocess
            import sys
            
            for dep in dependencies:
                if progress_callback:
                    progress_callback(f"Installation de {dep}...", 92)
                self._log(f"Installation de la dépendance: {dep}")
                try:
                    subprocess.check_call(
                        [sys.executable, "-m", "pip", "install", dep, "--quiet", "--upgrade"],
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL
                    )
                    self._log(f"Dépendance installée avec succès: {dep}")
                except subprocess.CalledProcessError as e:
                    self._log(f"Erreur lors de l'installation de {dep}: {e}", "ERROR")
                    return False
            return True
        except Exception as e:
            self._log(f"Erreur lors de l'installation des dépendances: {e}", "ERROR")
            return False
    
    def _cleanup_update_files(self):
        """Nettoie les fichiers de mise à jour temporaires"""
        try:
            if self.update_dir.exists():
                for file_path in self.update_dir.rglob('*'):
                    if file_path.is_file():
                        file_path.unlink()
                print(f"[Updater] Fichiers de mise à jour nettoyés")
        except Exception as e:
            print(f"[Updater] Erreur lors du nettoyage: {e}")
    
    def check_and_update_async(self, callback: Optional[callable] = None):
        """Vérifie et applique les mises à jour de manière asynchrone"""
        def update_thread():
            update_info = self.check_for_updates()
            if update_info:
                if callback:
                    callback(update_info)
                else:
                    # Mise à jour automatique silencieuse
                    success, message = self.apply_updates(update_info)
                    if success:
                        print(f"[Updater] {message}")
                    else:
                        print(f"[Updater] Échec: {message}")
        
        thread = threading.Thread(target=update_thread, daemon=True)
        thread.start()
        return thread

