#!/usr/bin/env python3
#
# Copyright (c) 2026 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.

"""Cthulhu Remote plugin."""

from __future__ import annotations

import logging
import secrets
from typing import Any

import gi

gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk

from cthulhu import dbus_service
from cthulhu import speech
from cthulhu.plugin import Plugin, cthulhu_hookimpl

from cthulhu.plugins.CthulhuRemote.connection_info import (
    ConnectionInfo,
    ConnectionMode,
    ConnectionState,
    URLParsingError,
)
from cthulhu.plugins.CthulhuRemote.local_machine import LocalMachine
from cthulhu.plugins.CthulhuRemote.protocol import RemoteMessageType, SERVER_PORT
from cthulhu.plugins.CthulhuRemote.serializer import JSONSerializer
from cthulhu.plugins.CthulhuRemote.socket_utils import address_to_host_port
from cthulhu.plugins.CthulhuRemote.transport import RelayTransport

logger = logging.getLogger(__name__)


class CthulhuRemote(Plugin):
    """Remote access plugin compatible with NVDA Remote relay servers."""

    def __init__(self) -> None:
        super().__init__()
        self._transport: RelayTransport | None = None
        self._connectionInfo: ConnectionInfo | None = None
        self._connectionState = ConnectionState.DISCONNECTED
        self._localMachine = LocalMachine(self._present_message)
        self._muted = False
        self._speechMonitorRegistered = False

    @cthulhu_hookimpl
    def activate(self, plugin=None):
        if plugin is not None and plugin is not self:
            return
        self.registerGestureByString(
            self.disconnect,
            "Disconnect Cthulhu Remote",
            "kb:cthulhu+alt+page_down",
        )
        self.registerGestureByString(
            self.toggle_mute,
            "Toggle Cthulhu Remote mute",
            "kb:cthulhu+alt+delete",
        )
        self.registerGestureByString(
            self.push_clipboard,
            "Push clipboard to Cthulhu Remote peers",
            "kb:cthulhu+control+shift+c",
        )
        logger.info("Cthulhu Remote activated")

    @cthulhu_hookimpl
    def deactivate(self, plugin=None):
        if plugin is not None and plugin is not self:
            return
        self.disconnect(notify_user=False)
        logger.info("Cthulhu Remote deactivated")

    @dbus_service.parameterized_command
    def connect(
        self,
        host: str,
        key: str,
        mode: str = "master",
        port: int = SERVER_PORT,
        insecure: bool = False,
        notify_user: bool = True,
    ) -> bool:
        """Connect to a Cthulhu Remote relay server."""

        if not host or not key:
            if notify_user:
                self._present_message("Cthulhu Remote requires host and key")
            return False
        try:
            connectionInfo = ConnectionInfo(
                hostname=host,
                port=port,
                key=key,
                mode=ConnectionMode(mode.lower()),
                insecure=insecure,
            )
        except ValueError:
            if notify_user:
                self._present_message("Cthulhu Remote mode must be master or slave")
            return False
        return self._connect(connectionInfo, notify_user=notify_user)

    @dbus_service.parameterized_command
    def connect_to_address(
        self,
        address: str,
        key: str,
        mode: str = "master",
        insecure: bool = False,
        notify_user: bool = True,
    ) -> bool:
        """Connect using host[:port] address text."""

        host, port = address_to_host_port(address)
        return self.connect(host, key, mode=mode, port=port, insecure=insecure, notify_user=notify_user)

    @dbus_service.parameterized_command
    def connect_to_url(self, url: str, notify_user: bool = True) -> bool:
        """Connect using a cthulhuremote:// or nvdaremote:// URL."""

        if url.startswith("nvdaremote://"):
            url = "cthulhuremote://" + url.split("://", 1)[1]
        try:
            connectionInfo = ConnectionInfo.from_url(url)
        except URLParsingError as error:
            logger.warning("Invalid Cthulhu Remote URL: %s", error)
            if notify_user:
                self._present_message("Invalid Cthulhu Remote URL")
            return False
        return self._connect(connectionInfo, notify_user=notify_user)

    @dbus_service.command
    def disconnect(self, script=None, inputEvent=None, notify_user: bool = True) -> bool:
        """Disconnect the current Cthulhu Remote session."""

        if self._transport is not None:
            self._connectionState = ConnectionState.DISCONNECTING
            self._transport.close()
            self._transport = None
        self._deregister_speech_monitor()
        self._connectionState = ConnectionState.DISCONNECTED
        if notify_user:
            self._present_message("Cthulhu Remote disconnected")
        return True

    @dbus_service.command
    def toggle_mute(self, script=None, inputEvent=None) -> bool:
        """Mute or unmute incoming remote output."""

        self._muted = not self._muted
        self._localMachine.isMuted = self._muted
        self._present_message("Cthulhu Remote muted" if self._muted else "Cthulhu Remote unmuted")
        return True

    @dbus_service.command
    def push_clipboard(self, script=None, inputEvent=None) -> bool:
        """Push local clipboard text to connected remote peers."""

        if not self._transport or not self._transport.connected:
            self._present_message("Cthulhu Remote is not connected")
            return True
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        text = clipboard.wait_for_text()
        if not text:
            self._present_message("Clipboard does not contain text")
            return True
        self._transport.send(RemoteMessageType.set_clipboard_text, text=text)
        self._present_message("Clipboard pushed")
        return True

    @dbus_service.command
    def copy_invite_url(self, script=None, inputEvent=None) -> bool:
        """Copy a remote invite URL for the opposite connection role."""

        if not self._connectionInfo:
            self._present_message("Cthulhu Remote is not connected")
            return True
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(self._connectionInfo.get_url_to_connect(), -1)
        clipboard.store()
        self._present_message("Cthulhu Remote invite copied")
        return True

    @dbus_service.command
    def generate_key(self) -> str:
        """Generate a random remote connection key."""

        return secrets.token_urlsafe(18)

    @dbus_service.getter
    def get_state(self) -> str:
        """Return the Cthulhu Remote connection state."""

        return self._connectionState.value

    @dbus_service.getter
    def get_connection_url(self) -> str:
        """Return the current connection URL, if connected."""

        if not self._connectionInfo:
            return ""
        return self._connectionInfo.get_url()

    def _connect(self, connectionInfo: ConnectionInfo, notify_user: bool = True) -> bool:
        self.disconnect(notify_user=False)
        serializer = JSONSerializer()
        transport = RelayTransport.create(connectionInfo, serializer)
        self._register_transport_handlers(transport, connectionInfo.mode)
        transport.transportConnected.register(self._handle_transport_connected)
        transport.transportDisconnected.register(self._handle_transport_disconnected)
        transport.transportConnectionFailed.register(self._handle_transport_failed)
        transport.transportCertificateAuthenticationFailed.register(self._handle_certificate_failed)
        self._transport = transport
        self._connectionInfo = connectionInfo
        self._connectionState = ConnectionState.CONNECTING
        self._register_speech_monitor(connectionInfo.mode)
        transport.start()
        if notify_user:
            self._present_message("Cthulhu Remote connecting")
        return True

    def _register_speech_monitor(self, mode: ConnectionMode) -> None:
        if mode != ConnectionMode.SLAVE or self._speechMonitorRegistered:
            return

        speech.add_monitor_callback(self._send_local_speech)
        self._speechMonitorRegistered = True

    def _deregister_speech_monitor(self) -> None:
        if not self._speechMonitorRegistered:
            return

        speech.remove_monitor_callback(self._send_local_speech)
        self._speechMonitorRegistered = False

    def _send_local_speech(self, text: str) -> None:
        if not text or not text.strip():
            return
        if not self._connectionInfo or self._connectionInfo.mode != ConnectionMode.SLAVE:
            return
        if not self._transport or not self._transport.connected:
            return

        self._transport.send(RemoteMessageType.speak, sequence=[text])

    def _register_transport_handlers(self, transport: RelayTransport, mode: ConnectionMode) -> None:
        transport.register_inbound(RemoteMessageType.speak, self._localMachine.speak)
        transport.register_inbound(RemoteMessageType.cancel, self._localMachine.cancel_speech)
        transport.register_inbound(RemoteMessageType.pause_speech, self._localMachine.pause_speech)
        transport.register_inbound(RemoteMessageType.set_clipboard_text, self._localMachine.set_clipboard_text)
        transport.register_inbound(RemoteMessageType.motd, self._handle_motd)
        transport.register_inbound(RemoteMessageType.version_mismatch, self._handle_version_mismatch)
        transport.register_inbound(RemoteMessageType.channel_joined, self._handle_channel_joined)
        transport.register_inbound(RemoteMessageType.client_joined, self._handle_client_joined)
        transport.register_inbound(RemoteMessageType.client_left, self._handle_client_left)
        if mode == ConnectionMode.SLAVE:
            transport.register_inbound(RemoteMessageType.key, self._localMachine.send_key)
            transport.register_inbound(RemoteMessageType.braille_input, self._localMachine.braille_input)
            transport.register_inbound(
                RemoteMessageType.send_SAS,
                self._localMachine.send_secure_attention_sequence,
            )
        else:
            transport.register_inbound(RemoteMessageType.display, self._localMachine.display)
            transport.register_inbound(
                RemoteMessageType.set_display_size,
                self._localMachine.set_braille_display_size,
            )

    def _handle_transport_connected(self, **kwargs: Any) -> None:
        self._connectionState = ConnectionState.CONNECTED
        self._present_message("Cthulhu Remote connected")

    def _handle_transport_disconnected(self, **kwargs: Any) -> None:
        if self._connectionState != ConnectionState.DISCONNECTING:
            self._connectionState = ConnectionState.DISCONNECTED
            self._present_message("Cthulhu Remote disconnected")

    def _handle_transport_failed(self, **kwargs: Any) -> None:
        self._connectionState = ConnectionState.DISCONNECTED
        self._present_message("Cthulhu Remote connection failed")

    def _handle_certificate_failed(self, **kwargs: Any) -> None:
        self._connectionState = ConnectionState.DISCONNECTED
        fingerprint = self._transport.lastFailFingerprint if self._transport else None
        if fingerprint:
            logger.warning("Cthulhu Remote certificate fingerprint: %s", fingerprint)
        self._present_message("Cthulhu Remote certificate verification failed")

    def _handle_motd(self, motd: str = "", **kwargs: Any) -> None:
        if motd:
            self._present_message(motd)

    def _handle_version_mismatch(self, **kwargs: Any) -> None:
        self._present_message("Cthulhu Remote relay protocol mismatch")
        self.disconnect(notify_user=False)

    def _handle_channel_joined(self, **kwargs: Any) -> None:
        logger.info("Cthulhu Remote channel joined: %r", kwargs)

    def _handle_client_joined(self, client: dict[str, Any] | None = None, **kwargs: Any) -> None:
        logger.info("Cthulhu Remote client joined: %r", client or kwargs)
        self._present_message("Cthulhu Remote client joined")

    def _handle_client_left(self, client: dict[str, Any] | None = None, **kwargs: Any) -> None:
        logger.info("Cthulhu Remote client left: %r", client or kwargs)
        self._present_message("Cthulhu Remote client left")

    def _present_message(self, message: str) -> None:
        if not self.app:
            return
        try:
            state = self.app.getDynamicApiManager().getAPI("CthulhuState")
            if state and state.activeScript:
                state.activeScript.presentMessage(message, resetStyles=False)
        except Exception:
            logger.exception("Failed to present Cthulhu Remote message")
