# target_simulator/core/sfp_communicator.py

"""
Handles SFP (Simple Fragmentation Protocol) communication with the target device.
"""

import socket
import time
import os
from datetime import datetime
from typing import List, Optional, Dict, Any, Callable
from queue import Queue

from target_simulator.core.communicator_interface import CommunicatorInterface
from target_simulator.core.models import Scenario
from target_simulator.core.sfp_transport import SfpTransport, PayloadHandler
from target_simulator.core import command_builder
from target_simulator.utils.logger import get_logger
from target_simulator.gui.payload_router import DebugPayloadRouter
from target_simulator.analysis.simulation_state_hub import SimulationStateHub
from target_simulator.config import DEBUG_CONFIG
import json


class SFPCommunicator(CommunicatorInterface):
    """
    A communicator that uses the SFP transport layer to send commands
    and receive status updates. Manages a single shared transport instance.
    """

    def __init__(
        self,
        simulation_hub: Optional[SimulationStateHub] = None,
        update_queue: Optional["Queue"] = None,
    ):
        """Create a new SFPCommunicator.

        Args:
            simulation_hub: Optional SimulationStateHub used to feed simulation
                state into the payload router.
            update_queue: Optional queue used to post GUI/update events.
        """
        self.logger = get_logger(__name__)
        self.transport: Optional[SfpTransport] = None
        self.config: Optional[Dict[str, Any]] = None
        self._destination: Optional[tuple] = None
        self.simulation_hub = simulation_hub
        self.update_queue = update_queue
        self._connection_state_callbacks: List[Callable[[bool], None]] = []

        # State variable to determine which communication protocol to use
        self._use_json_protocol: bool = False

        # Attributes for debug file saving
        self.debug_config = DEBUG_CONFIG
        self.temp_folder_path = os.path.join(
            os.getcwd(), self.debug_config.get("temp_folder_name", "Temp")
        )

        # Unified payload router
        self.payload_router = DebugPayloadRouter(simulation_hub=simulation_hub)

    def _save_json_payload_to_temp(self, content: str, prefix: str):
        """Save a JSON payload to the temporary debug folder when enabled.

        The filename uses the provided prefix and a timestamp. No exception is
        propagated to callers; failures are logged.

        Args:
            content: The JSON string to save.
            prefix: Filename prefix (e.g. 'sfp_scenario_init').
        """
        if not self.debug_config.get("save_sfp_json_payloads", False):
            return

        try:
            if not os.path.exists(self.temp_folder_path):
                os.makedirs(self.temp_folder_path)

            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
            filename = f"{prefix}_{timestamp}.json"
            filepath = os.path.join(self.temp_folder_path, filename)

            with open(filepath, "w", encoding="utf-8") as f:
                f.write(content)
            self.logger.debug(f"Saved debug JSON payload to '{filepath}'")
        except IOError as e:
            self.logger.error(f"Failed to save debug JSON payload: {e}")

    def router(self) -> DebugPayloadRouter:
        """Return the unified `DebugPayloadRouter` instance used by this communicator."""
        return self.payload_router

    def add_connection_state_callback(self, callback: Callable[[bool], None]):
        """Register a callback to be invoked when the connection state changes.

        The callback will be called with a single boolean argument indicating
        whether the communicator is currently open.
        """
        if callback not in self._connection_state_callbacks:
            self._connection_state_callbacks.append(callback)

    def remove_connection_state_callback(self, callback: Callable[[bool], None]):
        """Unregister a previously registered connection state callback.

        Removing a callback that was not registered is a no-op.
        """
        try:
            self._connection_state_callbacks.remove(callback)
        except ValueError:
            pass

    def _notify_connection_state_changed(self):
        """Invoke all registered connection state callbacks safely.

        Exceptions raised by individual callbacks are caught and logged so
        a failing callback does not prevent others from running.
        """
        is_open = self.is_open
        for callback in self._connection_state_callbacks:
            try:
                callback(is_open)
            except Exception:
                self.logger.exception("Error in connection state callback")

    def connect(self, config: Dict[str, Any]) -> bool:
        """Establish and start the SFP transport based on `config`.

        Expected keys in `config`: 'ip' (remote IP), 'port' (remote UDP port),
        'local_port' (local bind port). Optional key: 'use_json_protocol'
        (bool) to enable the JSON-based payload protocol.

        This method initialises the underlying `SfpTransport`, passes the
        router's handlers and starts the transport. Returns True on success.
        """
        if self.is_open:
            self.logger.warning("Already connected. Returning True.")
            return True

        remote_ip = config.get("ip")
        remote_port = config.get("port")
        local_port = config.get("local_port")

        if not remote_ip or not remote_port or not local_port:
            self.logger.error(
                "SFP connection failed: Missing 'ip', 'port' (remote), or 'local_port' in config."
            )
            return False

        # Read the protocol selection flag from the configuration
        self._use_json_protocol = config.get("use_json_protocol", False)
        self.logger.info(f"SFP JSON protocol enabled: {self._use_json_protocol}")

        result = False
        try:
            self._destination = (remote_ip, int(remote_port))
            local_port_int = int(local_port)

            self.logger.info(
                f"Initializing SFP Transport: Bind {local_port_int} -> Remote {self._destination}"
            )

            # The unified router provides all necessary handlers
            payload_handlers = self.payload_router.get_handlers()

            self.transport = SfpTransport(
                host="0.0.0.0",
                port=local_port_int,
                payload_handlers=payload_handlers,
                # Pass the router's raw packet callback to the transport
                raw_packet_callback=self.payload_router.update_raw_packet,
            )

            if self.transport.start():
                self.config = config
                self.logger.info("SFP Transport started successfully.")
                result = True
            else:
                self.logger.error("Failed to start SFP Transport.")
                self.transport = None
                result = False
        except Exception as e:
            self.logger.error(f"Exception during SFP connect: {e}", exc_info=True)
            self.transport = None
            result = False

        self._notify_connection_state_changed()
        return result

    def add_payload_handlers(self, handlers: Dict[int, PayloadHandler]):
        """Deprecated API: payload handlers are managed by the router.

        The method remains for backwards compatibility and logs a warning.
        """
        # This method is intentionally a no-op; keep for compatibility.
        self.logger.warning(
            "add_payload_handlers is deprecated. Handlers are managed by the unified router."
        )
        return None

    def remove_payload_handlers(self, handlers: Dict[int, PayloadHandler]):
        """Deprecated API: removal of payload handlers is managed centrally.

        The method is kept for compatibility and does nothing.
        """
        self.logger.warning(
            "remove_payload_handlers is deprecated. Handlers are managed by the unified router."
        )
        return None

    def disconnect(self) -> None:
        """Shut down the transport and clear connection state.

        The method attempts to remove payload handlers, shut down the
        transport cleanly and notify registered callbacks about the state
        transition. Exceptions during shutdown are caught and logged.
        """
        if self.transport:
            self.logger.info("Shutting down SFP Transport.")
            # Remove any registered payload handlers from the transport first
            try:
                handlers = (
                    self.payload_router.get_handlers() if self.payload_router else {}
                )
                if handlers:
                    try:
                        self.transport.remove_payload_handlers(handlers)
                    except Exception:
                        self.logger.debug(
                            "Failed to remove payload handlers from transport before shutdown."
                        )
            except Exception:
                pass

            try:
                self.transport.shutdown()
            except Exception:
                self.logger.exception("Error while shutting down transport")
            self.transport = None

        self.config = None
        self._destination = None
        self._notify_connection_state_changed()

    @property
    def is_open(self) -> bool:
        """Returns True when the underlying transport socket is open."""
        return (
            self.transport is not None
            and getattr(self.transport, "_socket", None) is not None
        )

    def send_scenario(self, scenario: Scenario) -> bool:
        """Send the full `scenario` initialization to the remote device.

        Behavior depends on the configured protocol:
        - JSON protocol: builds a single compact JSON payload and sends it.
        - Legacy protocol: sends a sequence of text commands (pause, aclatch,
          tgtinit for each target, acunlatch, continue).

        Returns True on success and False if any part of the send fails.
        """
        if not self.is_open or not self._destination:
            self.logger.error("Cannot send scenario: SFP not connected.")
            return False

        self.logger.info(f"Sending scenario '{scenario.name}' via SFP...")

        if self._use_json_protocol:
            # --- JSON Protocol Logic for scenario initialization ---
            self.logger.debug("Using JSON protocol for scenario initialization.")
            json_command = command_builder.build_json_update(scenario.get_all_targets())
            # Compact the JSON payload to a single-line string before saving/sending
            json_command = self._compact_json_if_needed(json_command)
            self._save_json_payload_to_temp(json_command, "sfp_scenario_init")

            if not self._send_single_command(json_command):
                self.logger.error("Failed to send JSON scenario payload.")
                return False
        else:
            # --- Legacy Command Logic ---
            self.logger.debug(
                "Using legacy command protocol for scenario initialization."
            )
            commands = [command_builder.build_pause(), command_builder.build_aclatch()]
            for target in scenario.get_all_targets():
                commands.append(command_builder.build_tgtinit(target))
            commands.extend(
                [command_builder.build_acunlatch(), command_builder.build_continue()]
            )
            for cmd in commands:
                if not self._send_single_command(cmd):
                    self.logger.error(
                        f"Failed to send command '{cmd}'. Aborting scenario send."
                    )
                    self._send_single_command(command_builder.build_acunlatch())
                    self._send_single_command(command_builder.build_continue())
                    return False
                time.sleep(0.05)

        self.logger.info("Finished sending scenario via SFP.")
        return True

    def send_commands(self, commands: List[str]) -> bool:
        """
        Send a list of commands to the remote via SFP.

        Ingressi: commands (list of strings) - JSON or legacy commands depending on protocol
        Uscite: bool - True on success, False if send failed or transport closed
        Commento: when using JSON protocol expects a single JSON payload string.
        """
        if not self.is_open:
            return False

        if self._use_json_protocol:
            # --- JSON Protocol Logic for live updates ---
            if not commands:
                self.logger.debug(
                    "send_commands (JSON mode) called with empty list. Nothing to send."
                )
                return True

            # Expect a single command string which is the JSON payload
            json_payload = commands[0]
            # Compact JSON for live updates before saving/sending
            json_payload = self._compact_json_if_needed(json_payload)
            self._save_json_payload_to_temp(json_payload, "sfp_live_update")
            return self._send_single_command(json_payload)
        else:
            # --- Legacy Command Logic ---
            all_success = True
            for cmd in commands:
                if not self._send_single_command(cmd):
                    all_success = False
            return all_success

    def _send_single_command(self, command: str) -> bool:
        """
        Helper: send a single command string via the transport to the destination.

        Ingressi: command (str)
        Uscite: bool - True if transport.send_script_command returned success
        Commento: Assumes command is already compacted if needed (done in send_commands).
        """
        if not self.transport or not self._destination:
            return False
        return self.transport.send_script_command(command, self._destination)

    def _compact_json_if_needed(self, command: str) -> str:
        """If the command is a JSON document (starts with { or [), return a compact
        single-line JSON string (no unnecessary spaces/newlines) with a trailing newline.
        Otherwise return the original command unchanged.
        """
        if not isinstance(command, str):
            try:
                command = str(command)
            except Exception:
                return command

        s = command.strip()
        if not s:
            return command

        if not (s.startswith("{") or s.startswith("[")):
            return command

        try:
            obj = json.loads(s)
            compact = json.dumps(obj, separators=(",", ":"))
            if not compact.endswith("\n"):
                compact += "\n"
            return compact
        except Exception:
            # If parsing fails, return original command (avoid changing it)
            return command

    @staticmethod
    def test_connection(config: Dict[str, Any]) -> bool:
        local_port = config.get("local_port")
        if not local_port:
            return False
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.bind(("0.0.0.0", int(local_port)))
            sock.close()
            return True
        except Exception:
            return False

    @staticmethod
    def list_available_ports() -> List[str]:
        return []
