#!/usr/bin/env python3
#
# Copyright (c) 2024-2025 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#

"""SSIP Proxy plugin for Cthulhu screen reader.

This plugin acts as a speech-dispatcher SSIP server, allowing applications
that use speech-dispatcher's TCP interface (like Say the Spire for Slay the Spire)
to have their speech output go through Cthulhu for both speech and braille support.
"""

import logging
import select
import socket
import threading
from threading import Thread, Lock
from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug

logger = logging.getLogger(__name__)

# Default speech-dispatcher TCP port
SSIP_PORT = 6560

# SSIP Response codes
RESP_OK = "200 OK"
RESP_CLIENT_NAME_SET = "208 OK CLIENT NAME SET"
RESP_RECEIVING_DATA = "230 OK RECEIVING DATA"
RESP_MSG_QUEUED = "225 OK MESSAGE QUEUED"
RESP_BYE = "231 HAPPY HACKING"
RESP_ERR_INVALID = "300 ERR INVALID COMMAND"


class SSIPConnection:
    """Handles a single SSIP client connection."""

    def __init__(self, clientSocket, plugin):
        """Initialize the connection handler.

        Args:
            clientSocket: The client socket connection
            plugin: Reference to the parent plugin
        """
        self.socket = clientSocket
        self.plugin = plugin
        self.clientName = None
        self.receivingData = False
        self.dataBuffer = []
        self.msgId = 0

    def send(self, message):
        """Send a response to the client.

        Args:
            message: The response message to send
        """
        try:
            response = message + "\r\n"
            self.socket.sendall(response.encode("utf-8"))
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"SSIP send error: {e}", True)

    def sendMessageQueued(self, msgId):
        """Send a message queued response with message ID.

        Sends both lines in a single write to avoid race conditions.

        Args:
            msgId: The message ID to include
        """
        self.sendMultiLine(f"225-{msgId}", "225 OK MESSAGE QUEUED")

    def sendMultiLine(self, line1, line2):
        """Send a two-line response in a single write.

        This avoids race conditions where the client reads between lines.

        Args:
            line1: First line of response
            line2: Second line of response
        """
        try:
            response = f"{line1}\r\n{line2}\r\n"
            self.socket.sendall(response.encode("utf-8"))
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING, f"SSIP send error: {e}", True)

    def handleLine(self, line):
        """Process a single line from the client.

        Args:
            line: The line to process

        Returns:
            True to continue, False to close connection
        """
        line = line.strip()
        if not line:
            return True

        logger.debug(f"SSIP <<< {line}")

        # If we're receiving SPEAK data
        if self.receivingData:
            return self.handleDataLine(line)

        # Parse command
        parts = line.split(None, 1)
        if not parts:
            return True

        cmd = parts[0].upper()
        args = parts[1] if len(parts) > 1 else ""

        if cmd == "SET":
            return self.handleSet(args)
        elif cmd == "SPEAK":
            return self.handleSpeak()
        elif cmd == "STOP":
            return self.handleStop(args)
        elif cmd == "CANCEL":
            return self.handleCancel(args)
        elif cmd == "PAUSE":
            self.send(RESP_OK)
            return True
        elif cmd == "RESUME":
            self.send(RESP_OK)
            return True
        elif cmd == "QUIT":
            self.send(RESP_BYE)
            return False
        elif cmd == "HELP":
            self.send("299-SSIP Proxy for Cthulhu")
            self.send("299 OK HELP SENT")
            return True
        elif cmd == "GET":
            return self.handleGet(args)
        elif cmd == "LIST":
            return self.handleList(args)
        elif cmd == "HISTORY":
            return self.handleHistory(args)
        elif cmd == "CHAR":
            # Character speaking - extract and speak the character
            if args:
                char = args.strip()
                if char.lower() == "space":
                    char = " "
                self.plugin.outputMessage(char)
            self.msgId += 1
            self.sendMessageQueued(self.msgId)
            return True
        elif cmd == "KEY":
            # Key speaking - speak the key name
            if args:
                self.plugin.outputMessage(args.strip())
            self.msgId += 1
            self.sendMessageQueued(self.msgId)
            return True
        elif cmd == "SOUND_ICON":
            # Sound icon - acknowledge but ignore
            self.msgId += 1
            self.sendMessageQueued(self.msgId)
            return True
        else:
            logger.debug(f"Unknown SSIP command: {cmd}")
            self.send(RESP_OK)
            return True

    def handleGet(self, args):
        """Handle GET command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        param = args.strip().upper() if args else ""

        if param == "VERSION":
            self.send("240-speechd-ssip-proxy 0.1")
            self.send("240 OK VERSION SENT")
        elif param == "OUTPUT_MODULES":
            self.send("250-cthulhu")
            self.send("250 OK MODULES LIST SENT")
        elif param == "VOICES":
            self.send("251-cthulhu\tdefault\tnone")
            self.send("251 OK VOICES LIST SENT")
        elif param == "RATE" or param == "PITCH" or param == "VOLUME":
            self.send("251-0")
            self.send("251 OK GET RETURNED")
        else:
            logger.debug(f"Unknown GET parameter: {param}")
            self.send("260-unknown")
            self.send("260 OK GET RETURNED")

        return True

    def handleList(self, args):
        """Handle LIST command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        param = args.strip().upper() if args else ""

        if param == "OUTPUT_MODULES":
            self.send("250-cthulhu")
            self.send("250 OK MODULES LIST SENT")
        elif param == "VOICES":
            self.send("251-cthulhu\tdefault\tnone")
            self.send("251 OK VOICE LIST SENT")
        else:
            self.send("250 OK LIST SENT")

        return True

    def handleHistory(self, args):
        """Handle HISTORY command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        parts = args.split() if args else []

        if len(parts) >= 2 and parts[0].upper() == "GET":
            subCmd = parts[1].upper()
            if subCmd == "CLIENT_ID":
                # Return a client ID - send both lines in single write
                self.sendMultiLine("240-1", "240 OK CLIENT ID SENT")
            elif subCmd == "MESSAGE_ID":
                self.sendMultiLine(f"240-{self.msgId}", "240 OK MESSAGE ID SENT")
            else:
                self.sendMultiLine("240-0", "240 OK HISTORY GET")
        elif len(parts) >= 1 and parts[0].upper() == "CURSOR":
            self.send("200 OK CURSOR SET")
        elif len(parts) >= 1 and parts[0].upper() == "SAY":
            # History say - acknowledge
            self.msgId += 1
            self.sendMessageQueued(self.msgId)
        else:
            self.send("200 OK HISTORY")

        return True

    def handleSet(self, args):
        """Handle SET command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        parts = args.split(None, 2)
        if len(parts) < 2:
            self.send(RESP_OK)
            return True

        scope = parts[0].lower()
        param = parts[1].upper()
        value = parts[2] if len(parts) > 2 else ""

        if param == "CLIENT_NAME":
            self.clientName = value
            logger.info(f"SSIP client connected: {value}")
            self.send(RESP_CLIENT_NAME_SET)
        elif param == "PRIORITY":
            self.send(f"200 OK PRIORITY SET")
        elif param == "RATE":
            self.send(f"200 OK RATE SET")
        elif param == "PITCH":
            self.send(f"200 OK PITCH SET")
        elif param == "VOLUME":
            self.send(f"200 OK VOLUME SET")
        elif param == "VOICE_TYPE":
            self.send(f"200 OK VOICE SET")
        elif param == "LANGUAGE":
            self.send(f"200 OK LANGUAGE SET")
        elif param == "SSML_MODE":
            self.send(f"200 OK SSML MODE SET")
        elif param == "PUNCTUATION":
            self.send(f"200 OK PUNCTUATION SET")
        elif param == "SPELLING":
            self.send(f"200 OK SPELLING SET")
        elif param == "CAP_LET_RECOGN":
            self.send(f"200 OK CAP LET RECOGNITION SET")
        elif param == "OUTPUT_MODULE":
            self.send(f"200 OK OUTPUT MODULE SET")
        else:
            logger.debug(f"Unknown SET parameter: {param}")
            self.send(RESP_OK)

        return True

    def handleSpeak(self):
        """Handle SPEAK command.

        Returns:
            True to continue
        """
        self.receivingData = True
        self.dataBuffer = []
        self.send(RESP_RECEIVING_DATA)
        return True

    def handleDataLine(self, line):
        """Handle a line of SPEAK data.

        Args:
            line: The data line

        Returns:
            True to continue
        """
        # Check for end-of-data marker (single dot)
        if line == ".":
            # End of data - process the message
            self.receivingData = False
            text = "\n".join(self.dataBuffer)
            self.dataBuffer = []

            # Send message ID and queued response together
            self.msgId += 1
            self.sendMessageQueued(self.msgId)

            # Forward to Cthulhu
            if text.strip():
                self.plugin.outputMessage(text.strip())

            return True

        # Handle dot-escaping (lines starting with . have extra dot)
        if line.startswith(".."):
            line = line[1:]

        self.dataBuffer.append(line)
        return True

    def handleStop(self, args):
        """Handle STOP command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        # Don't explicitly stop - let speech.speak(interrupt=True) handle it
        # Calling stop() followed by speak() can cause race conditions
        # where speech-dispatcher drops the new message
        self.send(RESP_OK)
        return True

    def handleCancel(self, args):
        """Handle CANCEL command.

        Args:
            args: Command arguments

        Returns:
            True to continue
        """
        self.send(RESP_OK)
        return True


class SSIPProxy(Plugin):
    """Plugin that provides an SSIP server interface for external applications."""

    def __init__(self):
        """Initialize the plugin."""
        super().__init__()
        self.lock = Lock()
        self.active = False
        self.serverThread = None
        self.serverSocket = None
        self.clientSockets = []  # Track active client connections
        self.clientLock = Lock()  # Lock for client list

    @cthulhu_hookimpl
    def activate(self):
        """Activate the SSIP proxy plugin."""
        super().activate()
        logger.info("Activating SSIP Proxy Plugin")
        self.activateServer()

    @cthulhu_hookimpl
    def deactivate(self):
        """Deactivate the SSIP proxy plugin."""
        logger.info("Deactivating SSIP Proxy Plugin")
        self.deactivateServer()
        super().deactivate()

    def activateServer(self):
        """Start the SSIP server thread."""
        with self.lock:
            self.active = True

        # Only start if not already running
        if self.serverThread is None or not self.serverThread.is_alive():
            self.serverThread = Thread(target=self.serverWorker)
            self.serverThread.daemon = True
            self.serverThread.start()

    def deactivateServer(self):
        """Stop the SSIP server thread."""
        with self.lock:
            self.active = False

        # Close all client connections first
        with self.clientLock:
            for clientSocket in self.clientSockets:
                try:
                    clientSocket.close()
                except Exception:
                    pass
            self.clientSockets.clear()

        # Close the server socket to interrupt accept()
        if self.serverSocket:
            try:
                self.serverSocket.close()
            except Exception:
                pass

        # Try to join the thread if it's alive, with a timeout
        if self.serverThread and self.serverThread.is_alive():
            try:
                self.serverThread.join(timeout=2.0)
            except Exception as e:
                logger.warning(f"Error stopping server thread: {e}")

    def isActive(self):
        """Check if the server is active."""
        with self.lock:
            return self.active

    def outputMessage(self, message):
        """Output a message through Cthulhu's speech and braille systems.

        Args:
            message: The message to output
        """
        try:
            # Use speech.speak directly for more reliable rapid-fire messages
            from cthulhu import speech
            speech.speak(message, interrupt=True)

            # Also update braille if possible
            try:
                scriptManager = self.app.getDynamicApiManager().getAPI('ScriptManager')
                manager = scriptManager.get_manager()
                script = manager.get_default_script()
                if script:
                    # Flash message to braille
                    from cthulhu import braille
                    braille.displayMessage(message)
            except Exception:
                pass  # Braille is optional
        except Exception as e:
            debug.printMessage(debug.LEVEL_WARNING,
                f"SSIP output error: {e} for: {message}", True)

    def serverWorker(self):
        """Worker thread that runs the SSIP server."""
        try:
            # Create TCP socket
            self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

            try:
                self.serverSocket.bind(("127.0.0.1", SSIP_PORT))
            except OSError as e:
                logger.error(f"Cannot bind to port {SSIP_PORT}: {e}")
                logger.error("Another process may be using this port.")
                return

            self.serverSocket.listen(5)
            debug.printMessage(debug.LEVEL_INFO,
                f"SSIP Proxy listening on 127.0.0.1:{SSIP_PORT}", True)

            while self.isActive():
                # Check for new connections with timeout
                try:
                    ready, _, _ = select.select([self.serverSocket], [], [], 0.8)
                except (select.error, ValueError):
                    break

                if not ready:
                    continue

                try:
                    clientSocket, addr = self.serverSocket.accept()
                    debug.printMessage(debug.LEVEL_INFO,
                        f"SSIP client connected from {addr}", True)

                    # Handle this client in a separate thread
                    clientThread = Thread(
                        target=self.handleClient,
                        args=(clientSocket,)
                    )
                    clientThread.daemon = True
                    clientThread.start()
                except Exception as e:
                    if self.isActive():
                        logger.error(f"Error accepting connection: {e}")

        except Exception as e:
            logger.error(f"SSIP server error: {e}")
        finally:
            if self.serverSocket:
                try:
                    self.serverSocket.close()
                except Exception:
                    pass
            logger.info("SSIP Proxy server stopped")

    def handleClient(self, clientSocket):
        """Handle a client connection.

        Args:
            clientSocket: The client socket
        """
        # Track this client socket
        with self.clientLock:
            self.clientSockets.append(clientSocket)

        # Disable Nagle's algorithm for immediate sending
        clientSocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        clientSocket.settimeout(30.0)
        connection = SSIPConnection(clientSocket, self)
        buffer = ""

        try:
            while self.isActive():
                try:
                    data = clientSocket.recv(4096)
                    if not data:
                        break

                    buffer += data.decode("utf-8")

                    # Process complete lines
                    while "\n" in buffer:
                        line, buffer = buffer.split("\n", 1)
                        line = line.rstrip("\r")
                        if not connection.handleLine(line):
                            return
                except socket.timeout:
                    continue
                except Exception as e:
                    logger.debug(f"Client read error: {e}")
                    break
        finally:
            # Remove from tracking list
            with self.clientLock:
                if clientSocket in self.clientSockets:
                    self.clientSockets.remove(clientSocket)

            try:
                clientSocket.close()
            except Exception:
                pass
            debug.printMessage(debug.LEVEL_INFO, "SSIP client disconnected", True)
