import os
import json
import subprocess
import re
import traceback
from typing import Dict, Any, List, Optional, Callable
import sys
import time
from datetime import datetime

# Import necessary handlers/managers
from .directory_manager import DirectoryManager
from .config_handler import ConfigHandler
from .logger import Logger


class ScriptExecutor:
    def __init__(
        self,
        script_groups_path: str,
        dir_manager: DirectoryManager,
        config_handler: ConfigHandler,
        app_logger: Logger,
        get_exec_state_func: Callable[
            [], Dict[str, Any]
        ],  # Func to get {last_time, interval}
        set_last_exec_time_func: Callable[[float], None],  # Func to set last exec time
    ):
        self.script_groups_path = script_groups_path
        self.dir_manager = dir_manager
        self.config_handler = config_handler
        self.app_logger = app_logger  # Central application logger instance
        self._get_exec_state = get_exec_state_func
        self._set_last_exec_time = set_last_exec_time_func

        # Diccionario para rastrear procesos en ejecución
        # Key: f"{group}:{script_name}", Value: subprocess.Popen object
        self.running_processes = {}

    def execute_script(
        self,
        group: str,
        script_name: str,
        broadcast_fn: Optional[Callable[[str], None]] = None,
    ) -> Dict[str, Any]:
        """
        Execute script, broadcast output in real-time, and save final log
        to a script-specific file in the script's directory.
        """
        exec_state = self._get_exec_state()
        last_execution_time = exec_state.get("last_time", 0)
        min_execution_interval = exec_state.get("interval", 1)

        current_time = time.time()
        time_since_last = current_time - last_execution_time
        if time_since_last < min_execution_interval:
            msg = f"Por favor espere {min_execution_interval - time_since_last:.1f} segundo(s) más entre ejecuciones"
            self.app_logger.append_log(f"Warning: {msg}")  # Log throttling attempt
            if broadcast_fn:
                broadcast_fn(msg)
            return {"status": "throttled", "error": msg}

        self._set_last_exec_time(current_time)  # Update last execution time

        script_path = os.path.join(self.script_groups_path, group, script_name)
        script_dir = os.path.dirname(script_path)
        script_base_name = os.path.splitext(script_name)[0]

        # Create a dedicated log directory
        log_dir = os.path.join(script_dir, ".log")
        os.makedirs(log_dir, exist_ok=True)
        # Log file is per-script and will be overwritten
        script_log_path = os.path.join(log_dir, f"log_{script_base_name}.txt")

        if not os.path.exists(script_path):
            msg = f"Error Fatal: Script no encontrado en {script_path}"
            self.app_logger.append_log(msg)
            if broadcast_fn:
                broadcast_fn(msg)
            return {"status": "error", "error": "Script not found"}

        # Get working directory using DirectoryManager
        working_dir = self.dir_manager.get_work_dir_for_group(group)
        if not working_dir:
            msg = f"Error Fatal: Directorio de trabajo no configurado o inválido para el grupo '{group}'"
            self.app_logger.append_log(msg)
            if broadcast_fn:
                broadcast_fn(msg)
            return {"status": "error", "error": "Working directory not set"}
        if not os.path.isdir(working_dir):  # Double check validity
            msg = f"Error Fatal: El directorio de trabajo '{working_dir}' no es válido o no existe."
            self.app_logger.append_log(msg)
            if broadcast_fn:
                broadcast_fn(msg)
            return {"status": "error", "error": "Invalid working directory"}

        # Aggregate configurations using ConfigHandler
        configs = {
            "level1": self.config_handler.get_config("1"),
            "level2": self.config_handler.get_config("2", group),
            "level3": self.config_handler.get_config(
                "3", group
            ),  # Relies on workdir set in main manager
            "working_directory": working_dir,
        }
        print(
            f"Debug: Aggregated configs for script execution: {configs}"
        )  # Keep for debug

        config_file_path = os.path.join(script_dir, "script_config.json")
        try:
            with open(config_file_path, "w", encoding="utf-8") as f:
                json.dump(configs, f, indent=2, ensure_ascii=False)
        except Exception as e:
            msg = f"Error Fatal: No se pudieron guardar las configuraciones temporales en {config_file_path}: {str(e)}"
            self.app_logger.append_log(msg)
            if broadcast_fn:
                broadcast_fn(msg)
            # Optionally return error here

        stdout_capture = []
        stderr_capture = ""
        process = None
        start_time = datetime.now()
        process_key = f"{group}:{script_name}"

        try:
            if broadcast_fn:
                start_msg = f"[{start_time.strftime('%H:%M:%S')}] Iniciando ejecución de {script_name} en {working_dir}..."
                broadcast_fn(start_msg)

            creation_flags = (
                subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
            )

            process = subprocess.Popen(
                ["python", "-u", script_path],
                cwd=working_dir,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                encoding="utf-8",
                errors="replace",
                bufsize=1,
                env=dict(os.environ, PYTHONIOENCODING="utf-8"),
                creationflags=creation_flags,
            )

            # Registrar el proceso en ejecución
            self.running_processes[process_key] = process

            while True:
                line = process.stdout.readline()
                if not line and process.poll() is not None:
                    break
                if line:
                    cleaned_line = line.rstrip()
                    stdout_capture.append(cleaned_line)
                    if broadcast_fn:
                        broadcast_fn(cleaned_line)

            return_code = process.wait()
            end_time = datetime.now()
            duration = end_time - start_time
            stderr_capture = process.stderr.read()
            status = "success" if return_code == 0 else "error"
            completion_msg = f"[{end_time.strftime('%H:%M:%S')}] Ejecución de {script_name} finalizada ({status}). Duración: {duration}."

            if stderr_capture:
                if status == "error" and broadcast_fn:
                    broadcast_fn(f"--- ERRORES ---")
                    broadcast_fn(stderr_capture.strip())
                    broadcast_fn(f"--- FIN ERRORES ---")
                completion_msg += f" Se detectaron errores (ver log)."

            if broadcast_fn:
                broadcast_fn(completion_msg)

            # Write to script-specific log file (overwritten)
            try:
                with open(script_log_path, "w", encoding="utf-8") as log_f:
                    log_f.write(
                        f"--- Log de Ejecución: {script_name} ---\nGrupo: {group}\nDirectorio de Trabajo: {working_dir}\n"
                    )
                    log_f.write(
                        f"Inicio: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\nFin: {end_time.strftime('%Y-%m-%d %H:%M:%S')}\nDuración: {duration}\n"
                    )
                    log_f.write(
                        f"Estado: {status.upper()} (Código de Salida: {return_code})\n\n--- SALIDA ESTÁNDAR (STDOUT) ---\n"
                    )
                    log_f.write("\n".join(stdout_capture))
                    log_f.write("\n\n--- ERRORES (STDERR) ---\n")
                    log_f.write(stderr_capture if stderr_capture else "Ninguno")
                    log_f.write("\n--- FIN DEL LOG ---\n")
                if broadcast_fn:
                    broadcast_fn(f"Log completo guardado en: {script_log_path}")
                print(f"Info: Script log saved to {script_log_path}")
            except Exception as log_e:
                err_msg = f"Error al guardar el log específico del script en {script_log_path}: {log_e}"
                print(err_msg)
                self.app_logger.append_log(f"ERROR: {err_msg}")
                if broadcast_fn:
                    broadcast_fn(err_msg)

            return {
                "status": status,
                "return_code": return_code,
                "error": stderr_capture if stderr_capture else None,
                "log_file": script_log_path,
            }

        except Exception as e:
            end_time = datetime.now()
            duration = end_time - start_time
            error_msg = (
                f"Error inesperado durante la ejecución de {script_name}: {str(e)}"
            )
            traceback_info = traceback.format_exc()
            print(error_msg)
            print(traceback_info)
            self.app_logger.append_log(f"ERROR FATAL: {error_msg}\n{traceback_info}")
            if broadcast_fn:
                broadcast_fn(
                    f"[{end_time.strftime('%H:%M:%S')}] ERROR FATAL: {error_msg}"
                )

            try:  # Attempt to write error to script-specific log
                with open(script_log_path, "w", encoding="utf-8") as log_f:
                    log_f.write(
                        f"--- Log de Ejecución: {script_name} ---\nGrupo: {group}\nDirectorio de Trabajo: {working_dir}\n"
                    )
                    log_f.write(
                        f"Inicio: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\nFin: {end_time.strftime('%Y-%m-%d %H:%M:%S')} (Interrumpido por error)\n"
                    )
                    log_f.write(
                        f"Duración: {duration}\nEstado: FATAL ERROR\n\n--- ERROR ---\n{error_msg}\n\n--- TRACEBACK ---\n{traceback_info}\n--- FIN DEL LOG ---\n"
                    )
            except Exception as log_e:
                print(f"Error adicional al intentar guardar el log de error: {log_e}")

            return {"status": "error", "error": error_msg, "traceback": traceback_info}
        finally:
            # Remover el proceso del registro cuando termine
            if process_key in self.running_processes:
                del self.running_processes[process_key]

            if process and process.stderr:
                process.stderr.close()
            if process and process.stdout:
                process.stdout.close()

    def stop_script(
        self,
        group: str,
        script_name: str,
        broadcast_fn: Optional[Callable[[str], None]] = None,
    ) -> Dict[str, Any]:
        """
        Detiene un script en ejecución.
        """
        process_key = f"{group}:{script_name}"

        if process_key not in self.running_processes:
            msg = f"El script {script_name} no está ejecutándose actualmente"
            self.app_logger.append_log(f"Warning: {msg}")
            if broadcast_fn:
                broadcast_fn(msg)
            return {"status": "error", "error": "Script not running"}

        process = self.running_processes[process_key]

        try:
            # Verificar que el proceso aún esté vivo
            if process.poll() is not None:
                # El proceso ya terminó naturalmente
                del self.running_processes[process_key]
                msg = f"El script {script_name} ya había terminado"
                self.app_logger.append_log(f"Info: {msg}")
                if broadcast_fn:
                    broadcast_fn(msg)
                return {"status": "already_finished", "message": msg}

            # Intentar terminar el proceso suavemente
            process.terminate()

            # Esperar un poco para ver si termina suavemente
            try:
                process.wait(timeout=5)  # Esperar 5 segundos
                msg = f"Script {script_name} detenido correctamente"
                self.app_logger.append_log(f"Info: {msg}")
                if broadcast_fn:
                    broadcast_fn(msg)
                return {"status": "success", "message": msg}
            except subprocess.TimeoutExpired:
                # Si no termina suavemente, forzar la terminación
                process.kill()
                process.wait()  # Esperar a que termine definitivamente
                msg = f"Script {script_name} forzado a terminar"
                self.app_logger.append_log(f"Warning: {msg}")
                if broadcast_fn:
                    broadcast_fn(msg)
                return {"status": "forced_kill", "message": msg}

        except Exception as e:
            error_msg = f"Error al detener el script {script_name}: {str(e)}"
            self.app_logger.append_log(f"ERROR: {error_msg}")
            if broadcast_fn:
                broadcast_fn(error_msg)
            return {"status": "error", "error": error_msg}
        finally:
            # Asegurarse de que el proceso se elimine del registro
            if process_key in self.running_processes:
                del self.running_processes[process_key]
