#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

"""
Remote controll:
section<space>command<space>parameters
sections:command,setting
setting commands:
- set section#setting=value[;section#setting=value]
- reset
command commands:
- say text to speech
- interrupt
examples
settings:
settings set section#setting=value[,section#setting=value]
setting set speech#voice=de
setting reset
setting save /path/settings.conf
command:
command say this is a test
command interrupt
"""


import hashlib
import fcntl
import os
import tempfile
import time

from fenrirscreenreader.core import debug
from fenrirscreenreader.core import remoteInstanceRegistry
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.i18n import _


REMOTE_COMMAND_LOCK_TIMEOUT_SEC = 2.0
REMOTE_COMMAND_LOCK_FILE = "fenrirscreenreader-remote-command.lock"


class RemoteManager:
    def __init__(self):
        # command controll
        self.commandConst = "COMMAND "
        self.sayConst = "SAY "
        self.vmenuConst = "VMENU "
        self.resetVmenuConst = "RESETVMENU"
        self.interruptConst = "INTERRUPT"
        self.quitAppConst = "QUITAPPLICATION"
        self.tempDisableSpeechConst = "TEMPDISABLESPEECH"
        self.defineWindowConst = "WINDOW "
        self.resetWindowConst = "RESETWINDOW"
        self.setClipboardConst = "CLIPBOARD "
        self.exportClipboardConst = "EXPORTCLIPBOARD"
        self.listInstancesConst = "LS"
        self.listInstancesLongConst = "LIST"
        # setting controll
        self.settingConst = "SETTING "
        self.setSettingConst = "SET "
        self.saveAsSettingConst = "SAVEAS "
        self.saveSettingConst = "SAVE"
        self.resetSettingConst = "RESET"

    def initialize(self, environment):
        self.env = environment
        self.env["runtime"]["SettingsManager"].load_driver(
            self.env["runtime"]["SettingsManager"].get_setting(
                "remote", "driver"
            ),
            "RemoteDriver",
        )

    def shutdown(self):
        self.env["runtime"]["SettingsManager"].shutdown_driver("RemoteDriver")

    def handle_settings_change_with_response(self, settings_text):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "remote", "enable_settings_remote"
        ):
            return {
                "success": False,
                "message": "Settings remote control is disabled",
            }

        upper_settings_text = settings_text.upper()
        try:
            # set setting
            if upper_settings_text.startswith(self.setSettingConst):
                parameter_text = settings_text[len(self.setSettingConst) :]
                self.set_settings(parameter_text)
                return {
                    "success": True,
                    "message": f"Setting applied: {parameter_text}",
                }
            # save as setting
            elif upper_settings_text.startswith(self.saveAsSettingConst):
                parameter_text = settings_text[len(self.saveAsSettingConst) :]
                self.save_settings(parameter_text)
                return {
                    "success": True,
                    "message": f"Settings saved to: {parameter_text}",
                }
            # save setting
            elif upper_settings_text == self.saveSettingConst:
                self.save_settings()
                return {"success": True, "message": "Settings saved"}
            # reset setting
            elif upper_settings_text == self.resetSettingConst:
                self.reset_settings()
                return {
                    "success": True,
                    "message": "Settings reset to defaults",
                }
            else:
                return {
                    "success": False,
                    "message": f"Unknown settings command: {settings_text}",
                }
        except Exception as e:
            return {"success": False, "message": f"Settings error: {str(e)}"}

    def handle_settings_change(self, settings_text):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "remote", "enable_settings_remote"
        ):
            return

        upper_settings_text = settings_text.upper()
        # set setting
        if upper_settings_text.startswith(self.setSettingConst):
            parameter_text = settings_text[len(self.setSettingConst) :]
            self.set_settings(parameter_text)
        # save as setting
        elif upper_settings_text.startswith(self.saveAsSettingConst):
            parameter_text = settings_text[len(self.saveAsSettingConst) :]
            self.save_settings(parameter_text)
        # save setting
        elif upper_settings_text == self.saveSettingConst:
            self.save_settings()
        # reset setting
        elif upper_settings_text == self.resetSettingConst:
            self.reset_settings()

    def handle_command_execution_with_response(self, command_text):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "remote", "enable_command_remote"
        ):
            return {
                "success": False,
                "message": "Command remote control is disabled",
            }

        upper_command_text = command_text.upper()

        try:
            # say
            if upper_command_text.startswith(self.sayConst):
                parameter_text = command_text[len(self.sayConst) :]
                self.say(parameter_text)
                return {
                    "success": True,
                    "message": f"Speaking: {parameter_text[:50]}{'...' if len(parameter_text) > 50 else ''}",
                }
            # interrupt
            elif upper_command_text == self.interruptConst:
                self.interrupt_speech()
                return {"success": True, "message": "Speech interrupted"}
            # temp disable speech
            elif upper_command_text == self.tempDisableSpeechConst:
                self.temp_disable_speech()
                return {
                    "success": True,
                    "message": "Speech temporarily disabled",
                }
            # set vmenu
            elif upper_command_text.startswith(self.vmenuConst):
                parameter_text = command_text[len(self.vmenuConst) :]
                self.set_vmenu(parameter_text)
                return {
                    "success": True,
                    "message": f"VMenu set to: {parameter_text}",
                }
            # reset vmenu
            elif upper_command_text == self.resetVmenuConst:
                self.reset_vmenu()
                return {"success": True, "message": "VMenu reset"}
            # quit fenrir
            elif upper_command_text == self.quitAppConst:
                self.quit_fenrir()
                return {"success": True, "message": "Fenrir shutting down"}
            # define window
            elif upper_command_text.startswith(self.defineWindowConst):
                parameter_text = command_text[len(self.defineWindowConst) :]
                self.define_window(parameter_text)
                return {
                    "success": True,
                    "message": f"Window defined: {parameter_text}",
                }
            # reset window
            elif upper_command_text == self.resetWindowConst:
                self.reset_window()
                return {"success": True, "message": "Window reset"}
            # set clipboard
            elif upper_command_text.startswith(self.setClipboardConst):
                parameter_text = command_text[len(self.setClipboardConst) :]
                self.set_clipboard(parameter_text)
                return {
                    "success": True,
                    "message": f"Clipboard set: {parameter_text[:50]}{'...' if len(parameter_text) > 50 else ''}",
                }
            elif upper_command_text.startswith(self.exportClipboardConst):
                self.export_clipboard()
                return {
                    "success": True,
                    "message": "Clipboard exported to file",
                }
            elif upper_command_text in (
                self.listInstancesConst,
                self.listInstancesLongConst,
            ):
                return {
                    "success": True,
                    "message": self.list_instances(),
                }
            else:
                return {
                    "success": False,
                    "message": f"Unknown command: {command_text}",
                }
        except Exception as e:
            return {"success": False, "message": f"Command error: {str(e)}"}

    def handle_command_execution(self, command_text):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "remote", "enable_command_remote"
        ):
            return

        upper_command_text = command_text.upper()

        # say
        if upper_command_text.startswith(self.sayConst):
            parameter_text = command_text[len(self.sayConst) :]
            self.say(parameter_text)
        # interrupt
        elif upper_command_text == self.interruptConst:
            self.interrupt_speech()
        # temp disable speech
        elif upper_command_text == self.tempDisableSpeechConst:
            self.temp_disable_speech()
        # set vmenu
        elif upper_command_text.startswith(self.vmenuConst):
            parameter_text = command_text[len(self.vmenuConst) :]
            self.set_vmenu(parameter_text)
        # reset vmenu
        elif upper_command_text == self.resetVmenuConst:
            self.reset_vmenu()
        # quit fenrir
        elif upper_command_text == self.quitAppConst:
            self.quit_fenrir()
        # define window
        elif upper_command_text.startswith(self.defineWindowConst):
            parameter_text = command_text[len(self.defineWindowConst) :]
            self.define_window(parameter_text)
        # reset window
        elif upper_command_text == self.resetWindowConst:
            self.reset_window()
        # set clipboard
        elif upper_command_text.startswith(self.setClipboardConst):
            parameter_text = command_text[len(self.setClipboardConst) :]
            self.set_clipboard(parameter_text)
        elif upper_command_text.startswith(self.exportClipboardConst):
            self.export_clipboard()
        elif upper_command_text in (
            self.listInstancesConst,
            self.listInstancesLongConst,
        ):
            return

    def _get_remote_command_lock_path(self, event_data):
        return os.path.join(tempfile.gettempdir(), REMOTE_COMMAND_LOCK_FILE)

    def _get_remote_command_hash(self, event_data):
        return hashlib.sha256(event_data.encode("utf-8")).hexdigest()

    def _read_remote_command_lock(self, lock_file):
        lock_file.seek(0)
        lock_parts = lock_file.readline().strip().split()
        try:
            lock_pid = int(lock_parts[0]) if lock_parts else 0
        except ValueError:
            lock_pid = 0
        lock_hash = lock_parts[1] if len(lock_parts) > 1 else ""
        return lock_pid, lock_hash

    def _write_remote_command_lock(self, lock_file, event_hash, now):
        lock_file.seek(0)
        lock_file.truncate()
        lock_file.write(f"{os.getpid()} {event_hash} {now}\n")
        lock_file.flush()
        os.fsync(lock_file.fileno())

    def _open_remote_command_lock(self, lock_path):
        try:
            lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o666)
        except OSError:
            return None
        try:
            os.chmod(lock_path, 0o666)
        except OSError:
            pass

        return os.fdopen(lock_fd, "r+", encoding="utf-8")

    def _claim_remote_command(self, event_data):
        lock_path = self._get_remote_command_lock_path(event_data)
        event_hash = self._get_remote_command_hash(event_data)
        now = time.time()
        lock_file = self._open_remote_command_lock(lock_path)
        if lock_file is None:
            return False

        with lock_file:
            try:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
                lock_pid, lock_hash = self._read_remote_command_lock(lock_file)
                lock_stat = os.fstat(lock_file.fileno())

                if lock_pid == os.getpid():
                    self._write_remote_command_lock(lock_file, event_hash, now)
                    return True

                if not lock_pid or lock_hash != event_hash:
                    self._write_remote_command_lock(lock_file, event_hash, now)
                    return True

                if now - lock_stat.st_mtime < REMOTE_COMMAND_LOCK_TIMEOUT_SEC:
                    return False

                self._write_remote_command_lock(lock_file, event_hash, now)
                return True
            finally:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)

    def list_instances(self):
        instances = remoteInstanceRegistry.list_instances()
        if not instances:
            return "No Fenrir instances registered"

        lines = []
        for instance in instances:
            socket_files = ", ".join(instance.get("socket_files", []))
            x11_window_id = instance.get("x11_window_id") or "none"
            main_socket = "yes" if instance.get("main_socket") else "no"
            lines.append(
                "pid={pid} ppid={ppid} screen={screen} keyboard={keyboard} "
                "main_socket={main_socket} x11_window_id={x11_window_id} "
                "sockets={sockets}".format(
                    pid=instance.get("pid", ""),
                    ppid=instance.get("ppid", ""),
                    screen=instance.get("screen_driver", ""),
                    keyboard=instance.get("keyboard_driver", ""),
                    main_socket=main_socket,
                    x11_window_id=x11_window_id,
                    sockets=socket_files,
                )
            )
        return "\n".join(lines)

    def temp_disable_speech(self):
        self.env["runtime"]["OutputManager"].temp_disable_speech()

    def set_vmenu(self, vmenu=""):
        self.env["runtime"]["VmenuManager"].set_curr_menu(vmenu)

    def reset_vmenu(self):
        self.env["runtime"]["VmenuManager"].set_curr_menu()

    def set_clipboard(self, text=""):
        self.env["runtime"]["MemoryManager"].add_value_to_first_index(
            "clipboardHistory", text
        )

    def quit_fenrir(self):
        self.env["runtime"]["EventManager"].stop_main_event_loop()

    def define_window(self, window_text):
        start = {}
        end = {}
        try:
            window_list = window_text.split(" ")
            if len(window_list) < 4:
                return
            start["x"] = int(window_list[0])
            start["y"] = int(window_list[1])
            end["x"] = int(window_list[2])
            end["y"] = int(window_list[3])

            self.env["runtime"]["CursorManager"].set_window_for_application(
                start, end
            )
        except Exception as e:
            pass

    def reset_window(self):
        self.env["runtime"]["CursorManager"].clear_window_for_application()

    def say(self, text):
        if not text:
            return
        if text == "":
            return
        self.env["runtime"]["OutputManager"].speak_text(text)

    def interrupt_speech(self):
        self.env["runtime"]["OutputManager"].interrupt_output()

    def export_clipboard(self):
        clipboard_file_path = self.env["runtime"][
            "SettingsManager"
        ].get_setting("general", "clipboard_export_path")
        clipboard_file_path = clipboard_file_path.replace(
            "$user", self.env["general"]["curr_user"]
        )
        clipboard_file_path = clipboard_file_path.replace(
            "$USER", self.env["general"]["curr_user"]
        )
        clipboard_file_path = clipboard_file_path.replace(
            "$User", self.env["general"]["curr_user"]
        )
        clipboard_file = open(clipboard_file_path, "w")
        try:
            if self.env["runtime"]["MemoryManager"].is_index_list_empty(
                "clipboardHistory"
            ):
                self.env["runtime"]["OutputManager"].present_text(
                    _("clipboard empty"), interrupt=True
                )
                return
            clipboard = self.env["runtime"][
                "MemoryManager"
            ].get_index_list_element("clipboardHistory")
            # Fenrir will crash if the clipboard variable is type None
            if clipboard is not None:
                clipboard_file.write(clipboard)
            else:
                clipboard_file.write("")
            clipboard_file.close()
            os.chmod(clipboard_file_path, 0o644)
            self.env["runtime"]["OutputManager"].present_text(
                _("clipboard exported to file"), interrupt=True
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "export_clipboard_to_file:run: Filepath:"
                + clipboard_file
                + " trace:"
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def save_settings(self, setting_config_path=None):
        self.env["runtime"]["SettingsManager"].save_settings(
            setting_config_path
        )

    def reset_settings(self):
        self.env["runtime"]["SettingsManager"].reset_setting_arg_dict()

    def set_settings(self, settingsArgs):
        self.env["runtime"]["SettingsManager"].parse_setting_args(settingsArgs)
        self.env["runtime"]["ScreenManager"].update_screen_ignored()
        self.env["runtime"]["InputManager"].handle_device_grab(force=True)

    def handle_remote_incomming_with_response(self, event_data):
        if not event_data:
            return {"success": False, "message": "No data received"}

        upper_event_data = event_data.upper()
        self.env["runtime"]["DebugManager"].write_debug_out(
            "RemoteManager:handle_remote_incomming_with_response: event: "
            + str(event_data),
            debug.DebugLevel.INFO,
        )

        try:
            if upper_event_data in (
                self.listInstancesConst,
                self.listInstancesLongConst,
            ):
                return {
                    "success": True,
                    "message": self.list_instances(),
                }
            if upper_event_data.startswith(self.settingConst):
                settings_text = event_data[len(self.settingConst) :]
                return self.handle_settings_change_with_response(settings_text)
            elif upper_event_data.startswith(self.commandConst):
                command_text = event_data[len(self.commandConst) :]
                return self.handle_command_execution_with_response(
                    command_text
                )
            else:
                return {
                    "success": False,
                    "message": "Unknown command format. Use 'COMMAND ...' or 'SETTING ...'",
                }
        except Exception as e:
            return {"success": False, "message": f"Exception: {str(e)}"}

    def handle_remote_incomming(self, event_data):
        if not event_data:
            return
        upper_event_data = event_data.upper()
        self.env["runtime"]["DebugManager"].write_debug_out(
            "RemoteManager:handle_remote_incomming: event: " + str(event_data),
            debug.DebugLevel.INFO,
        )

        if not self._claim_remote_command(event_data):
            return

        if upper_event_data.startswith(self.settingConst):
            settings_text = event_data[len(self.settingConst) :]
            self.handle_settings_change(settings_text)
        elif upper_event_data.startswith(self.commandConst):
            command_text = event_data[len(self.commandConst) :]
            self.handle_command_execution(command_text)
