# target_simulator/core/simulation_engine.py

"""
The core simulation engine that runs in a separate thread to update and
broadcast target states, supporting different operational modes.
"""
import threading
import time
import copy
from queue import Queue
from typing import Optional

# Use absolute imports for robustness
from target_simulator.core.communicator_interface import CommunicatorInterface
from target_simulator.core.models import Scenario, Target
from target_simulator.core import command_builder
from target_simulator.utils.logger import get_logger
from target_simulator.analysis.simulation_state_hub import SimulationStateHub
from target_simulator.utils.csv_logger import append_sent_position

# Simulation frequency in Hertz
TICK_RATE_HZ = 20.0
TICK_INTERVAL_S = 1.0 / TICK_RATE_HZ


class SimulationEngine(threading.Thread):
    def __init__(
        self,
        communicator: Optional[CommunicatorInterface],
        simulation_hub: Optional[SimulationStateHub] = None,
        archive: Optional[str] = None,
        update_queue: Optional[Queue] = None,
    ):
        super().__init__(daemon=True, name="SimulationEngineThread")
        self.logger = get_logger(__name__)

        self.communicator = communicator
        # Backwards-compat: older callers passed an update_queue as the
        # second positional argument. Detect a Queue and treat it as the
        # update queue while leaving simulation_hub unset. Also accept an
        # explicit `update_queue` keyword argument for newer callers.
        self.update_queue: Optional[Queue] = None
        if isinstance(simulation_hub, Queue):
            self.update_queue = simulation_hub
            self.simulation_hub = None
        else:
            self.simulation_hub = simulation_hub  # Hub for data analysis

        # If caller provided update_queue explicitly, prefer it (overrides
        # any queue accidentally passed in simulation_hub position).
        if isinstance(update_queue, Queue):
            self.update_queue = update_queue
        self.archive = archive  # Archive path if needed
        self.time_multiplier = 1.0
        self.update_interval_s = 1.0
        self.prediction_horizon_s = 0.0  # Latency compensation in seconds

        # Determine communication protocol from the communicator's config
        self.use_json_protocol = False
        if communicator:
            # Safely access the _use_json_protocol flag if it exists (for SFPCommunicator)
            self.use_json_protocol = getattr(communicator, "_use_json_protocol", False)
            self.logger.info(
                f"SimulationEngine will use JSON protocol: {self.use_json_protocol}"
            )

        self.scenario: Optional[Scenario] = None
        self._stop_event = threading.Event()
        self._is_running_event = threading.Event()
        self._last_tick_time = 0.0
        self._last_update_time = 0.0
        self._is_paused = False

    def set_prediction_horizon(self, horizon_s: float):
        """
        Sets the prediction horizon for latency compensation.
        This should be the estimated one-way client-to-server latency.
        """
        # Set a reasonable cap to avoid excessive prediction
        self.prediction_horizon_s = max(0.0, min(horizon_s, 1.0))

    def load_scenario(self, scenario: Scenario):
        """Loads a new scenario into the engine and resets its simulation state."""
        self.logger.info(f"Loading scenario '{scenario.name}' into simulation engine.")
        self.scenario = scenario
        self.scenario.reset_simulation()

    def set_time_multiplier(self, multiplier: float):
        """Sets the simulation time speed multiplier."""
        self.logger.info(f"Setting simulation time multiplier to {multiplier}x")
        self.time_multiplier = multiplier

    def set_update_interval(self, interval_s: float):
        """Sets the interval for sending target updates."""
        self.logger.info(f"Setting target update interval to {interval_s}s")
        self.update_interval_s = interval_s

    def set_simulation_time(self, new_time_s: float):
        """
        Jump the simulation to a specific point in time.
        This should be called while the simulation is paused for best results.
        """
        if self.scenario:
            self.logger.debug(f"Seeking simulation time to {new_time_s:.2f}s.")
            for target in self.scenario.get_all_targets():
                # Directly set the internal simulation time of the target
                setattr(target, "_sim_time_s", new_time_s)
                # Force an update to calculate the new position
                target.update_state(0)

            self._last_tick_time = time.monotonic()

            if self.update_queue:
                self.update_queue.put_nowait(self.scenario.get_all_targets())

    def run(self):
        """The main loop of the simulation thread."""
        self.logger.info("Simulation engine thread started.")
        self._is_running_event.set()
        self._last_tick_time = time.monotonic()
        self._last_update_time = self._last_tick_time

        while not self._stop_event.is_set():
            if not self.scenario:
                time.sleep(TICK_INTERVAL_S)
                continue

            if self._is_paused:
                time.sleep(TICK_INTERVAL_S)
                self._last_tick_time = time.monotonic()
                continue

            current_time = time.monotonic()
            delta_time = current_time - self._last_tick_time
            self._last_tick_time = current_time
            simulated_delta_time = delta_time * self.time_multiplier

            self.scenario.update_state(simulated_delta_time)
            if self.archive:
                log_timestamp = time.monotonic()
                for target in self.scenario.get_all_targets():
                    if target.active:
                        state_tuple = (
                            getattr(target, "_pos_x_ft", 0.0),
                            getattr(target, "_pos_y_ft", 0.0),
                            getattr(target, "_pos_z_ft", 0.0),
                        )
                        self.archive.add_simulated_state(
                            target.target_id, log_timestamp, state_tuple
                        )

            # --- High-Frequency State Logging ---
            tick_timestamp = time.monotonic()
            active_targets = [t for t in self.scenario.get_all_targets() if t.active]

            if self.simulation_hub:
                for target in active_targets:
                    # Include velocities in the state tuple for the hub
                    state_tuple = (
                        getattr(target, "_pos_x_ft", 0.0),
                        getattr(target, "_pos_y_ft", 0.0),
                        getattr(target, "_pos_z_ft", 0.0),
                        getattr(target, "current_velocity_fps", 0.0),
                        getattr(target, "current_vertical_velocity_fps", 0.0),
                    )
                    self.simulation_hub.add_simulated_state(
                        target.target_id, tick_timestamp, state_tuple
                    )

            if self.scenario.is_finished():
                self.logger.info("Scenario finished. Stopping engine.")
                # Notify any GUI preview consumers that the simulation finished
                try:
                    if self.update_queue:
                        try:
                            self.update_queue.put_nowait("SIMULATION_FINISHED")
                        except Exception:
                            # fallback to blocking put to ensure delivery
                            try:
                                self.update_queue.put("SIMULATION_FINISHED")
                            except Exception:
                                pass
                except Exception:
                    pass
                break

            # --- Throttled Communication Step ---
            if (current_time - self._last_update_time) >= self.update_interval_s:
                self._last_update_time = current_time

                # If an update_queue is provided (e.g., preview mode), send the
                # current simulated targets to the queue so the UI can render
                # them even when no communicator is present.
                if self.update_queue:
                    try:
                        # Send a shallow list of target objects (UI will copy if needed)
                        targets_snapshot = [
                            t for t in self.scenario.get_all_targets() if t.active
                        ]
                        try:
                            self.update_queue.put_nowait(targets_snapshot)
                        except Exception:
                            # fallback to blocking put
                            try:
                                self.update_queue.put(targets_snapshot)
                            except Exception:
                                pass
                    except Exception:
                        # Don't let queue failures stop the engine
                        pass

                if self.communicator and self.communicator.is_open:
                    commands_to_send = []

                    # Create a list of targets to be sent, potentially predicted
                    targets_to_send = []
                    if self.prediction_horizon_s > 0.0 and active_targets:
                        # Apply prediction
                        for target in active_targets:
                            # Create a deep copy to avoid altering the main simulation state
                            predicted_target = copy.deepcopy(target)
                            # Advance its state by the prediction horizon
                            predicted_target.update_state(self.prediction_horizon_s)
                            targets_to_send.append(predicted_target)
                    else:
                        # No prediction, use current state
                        targets_to_send = active_targets

                    # --- Protocol-dependent command generation ---
                    if self.use_json_protocol:
                        # --- JSON Protocol Logic ---
                        if targets_to_send:  # Usa la lista (potenzialmente predetta)
                            json_payload = command_builder.build_json_update(
                                targets_to_send
                            )
                            commands_to_send.append(json_payload)

                            # Log to CSV for debugging (log the original state)
                            for target in active_targets:
                                state_tuple = (
                                    getattr(target, "_pos_x_ft", 0.0),
                                    getattr(target, "_pos_y_ft", 0.0),
                                    getattr(target, "_pos_z_ft", 0.0),
                                )
                                append_sent_position(
                                    tick_timestamp,
                                    target.target_id,
                                    state_tuple[0],
                                    state_tuple[1],
                                    state_tuple[2],
                                    "JSON_BULK_UPDATE",  # Use a placeholder in the command field
                                )
                    else:
                        # --- Legacy Protocol Logic ---
                        for (
                            target
                        ) in targets_to_send:  # Usa la lista (potenzialmente predetta)
                            cmd = command_builder.build_tgtset_from_target_state(target)
                            commands_to_send.append(cmd)

                            # Log to CSV for debugging (log the original state)
                            # Find the original target corresponding to the predicted one
                            original_target = next(
                                (
                                    t
                                    for t in active_targets
                                    if t.target_id == target.target_id
                                ),
                                None,
                            )
                            if original_target:
                                state_tuple = (
                                    getattr(original_target, "_pos_x_ft", 0.0),
                                    getattr(original_target, "_pos_y_ft", 0.0),
                                    getattr(original_target, "_pos_z_ft", 0.0),
                                )
                                append_sent_position(
                                    tick_timestamp,
                                    original_target.target_id,
                                    state_tuple[0],
                                    state_tuple[1],
                                    state_tuple[2],
                                    cmd,
                                )

                    # --- Send the batch of commands ---
                    if commands_to_send:
                        self.communicator.send_commands(commands_to_send)

            time.sleep(TICK_INTERVAL_S)

        self._is_running_event.clear()
        self.logger.info("Simulation engine thread stopped.")

    def pause(self):
        """Pauses the simulation loop."""
        self.logger.info("Simulation paused.")
        self._is_paused = True

    def resume(self):
        """Resumes the simulation loop."""
        self.logger.info("Simulation resumed.")
        self._is_paused = False

    def stop(self):
        """Signals the simulation thread to stop."""
        self.logger.info("Stop signal received for simulation thread.")
        self._stop_event.set()
        self.resume()
        self.join(timeout=2.0)
        if self.is_alive():
            self.logger.warning("Simulation thread did not stop gracefully.")

    def is_running(self) -> bool:
        """Checks if the simulation loop is currently active."""
        return self._is_running_event.is_set()
