import os
import time
from datetime import datetime
import hashlib
import json
import logging
from logging.handlers import RotatingFileHandler
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# 📂 Detectar carpeta updates/client relativa al script
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CARPETA_VIGILAR = os.path.join(BASE_DIR, "updates", "client")
RUTA_MANIFEST = os.path.join(BASE_DIR, "updates", "manifest.json")
CARPETA_LOGS = os.path.join(BASE_DIR, "logs_updates")

# 📜 Configurar logging con rotación de archivos
os.makedirs(CARPETA_LOGS, exist_ok=True)
LOG_FILE = os.path.join(CARPETA_LOGS, "duty_actualizacion.log")
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
        RotatingFileHandler(LOG_FILE, maxBytes=10_000_000, backupCount=5),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# Extensiones ignoradas
EXTENSIONES_IGNORADAS = (".tmp", ".log", ".filepart")

def calcular_hash(ruta):
    """Calcula el hash SHA256 de un archivo"""
    try:
        sha256 = hashlib.sha256()
        with open(ruta, "rb") as f:
            while chunk := f.read(8192):
                sha256.update(chunk)
        return sha256.hexdigest()
    except (IOError, OSError) as e:
        logger.error(f"Error al calcular hash de {ruta}: {e}")
        return None

def load_manifest():
    """Carga el manifest.json existente o devuelve un diccionario vacío"""
    if os.path.exists(RUTA_MANIFEST):
        try:
            with open(RUTA_MANIFEST, "r", encoding="utf-8") as f:
                return {item["path"]: item for item in json.load(f)}
        except json.JSONDecodeError as e:
            logger.error(f"Error al cargar manifest.json: {e}")
    return {}

def generar_manifest(changed_file=None, force_full=False):
    """
    Genera o actualiza el manifest.json
    - changed_file: si se pasa, actualiza solo ese archivo
    - force_full=True: recalcula todo
    """
    manifest = load_manifest()
    updated = False

    def add_or_update_file(ruta_completa):
        if ruta_completa.endswith(EXTENSIONES_IGNORADAS):
            return False
        if not os.path.exists(ruta_completa):
            return False

        ruta_relativa = os.path.relpath(ruta_completa, os.path.join(BASE_DIR, "updates")).replace("\\", "/")
        hash_archivo = calcular_hash(ruta_completa)
        mtime_archivo = os.path.getmtime(ruta_completa)

        if hash_archivo:
            manifest[ruta_relativa] = {
                "path": ruta_relativa,
                "size": os.path.getsize(ruta_completa),
                "hash": hash_archivo,
                "last_modified": mtime_archivo  # 🔹 Nueva clave para comparar fecha
            }
            return True
        return False

    if force_full:
        logger.info("Recalculando manifest completo por seguridad...")
        manifest = {}
        for root, _, files in os.walk(CARPETA_VIGILAR):
            for archivo in files:
                ruta_completa = os.path.join(root, archivo)
                if add_or_update_file(ruta_completa):
                    updated = True
    elif changed_file:
        if add_or_update_file(changed_file):
            updated = True

    if updated:
        try:
            with open(RUTA_MANIFEST, "w", encoding="utf-8") as f:
                json.dump(list(manifest.values()), f, indent=4)
            logger.info(f"Manifest actualizado con {len(manifest)} archivos.")
        except IOError as e:
            logger.error(f"Error al escribir manifest.json: {e}")

class MiHandler(FileSystemEventHandler):
    def on_created(self, event):
        if event.is_directory:
            return
        if event.src_path.endswith(EXTENSIONES_IGNORADAS):
            logger.info(f"Ignorado archivo temporal: {event.src_path}")
            return
        logger.info(f"Archivo creado: {event.src_path}")
        generar_manifest(event.src_path)

    def on_modified(self, event):
        if event.is_directory:
            return
        if event.src_path.endswith(".filepart"):
            logger.info(f"Detectado archivo parcial: {event.src_path}")
            return
        logger.info(f"Archivo modificado: {event.src_path}")
        # Blindaje: Si llega un archivo final y antes había un .filepart, recalcular todo
        generar_manifest(event.src_path, force_full=True)


    def on_deleted(self, event):
        if event.is_directory or event.src_path.endswith(EXTENSIONES_IGNORADAS):
            return
        logger.info(f"Archivo eliminado: {event.src_path}")
        manifest = load_manifest()
        ruta_relativa = os.path.relpath(event.src_path, os.path.join(BASE_DIR, "updates")).replace("\\", "/")
        if ruta_relativa in manifest:
            del manifest[ruta_relativa]
            try:
                with open(RUTA_MANIFEST, "w", encoding="utf-8") as f:
                    json.dump(list(manifest.values()), f, indent=4)
                logger.info(f"Manifest actualizado tras eliminación: {len(manifest)} archivos.")
            except IOError as e:
                logger.error(f"Error al actualizar manifest.json: {e}")

    def on_moved(self, event):
        if event.is_directory:
            return
        if event.src_path.endswith(EXTENSIONES_IGNORADAS) or event.dest_path.endswith(EXTENSIONES_IGNORADAS):
            return
        logger.info(f"Archivo movido: {event.src_path} -> {event.dest_path}")
        manifest = load_manifest()
        updated = False
        try:
            ruta_relativa_src = os.path.relpath(event.src_path, os.path.join(BASE_DIR, "updates")).replace("\\", "/")
            if ruta_relativa_src in manifest:
                del manifest[ruta_relativa_src]
                updated = True
        except ValueError:
            pass
        generar_manifest(event.dest_path, force_full=True if updated else False)

if __name__ == "__main__":
    if not os.path.exists(CARPETA_VIGILAR):
        logger.error(f"No se encontró la carpeta: {CARPETA_VIGILAR}")
        exit(1)

    logger.info(f"Vigilando carpeta: {CARPETA_VIGILAR}")
    generar_manifest(force_full=True)  # Manifest inicial completo

    event_handler = MiHandler()
    observer = Observer()
    observer.schedule(event_handler, CARPETA_VIGILAR, recursive=True)
    observer.start()

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        logger.info("Deteniendo el observador...")
        observer.stop()
    observer.join()
