#!/usr/bin/python3
#
# Copyright 2026 RED SOFT
#
# Author: Anton Fadeev <anton.fadeev@red-soft.ru>
#
#
"""
Termidesk hook for VDSM

This hook adds Termidesk-specific SPICE channels and configuration
to virtual machines that have the Termidesk compatibility flag enabled.

Hook points: before_vm_start, before_vm_migrate_source
"""

from __future__ import absolute_import

import os
import sys
import json
import stat
import traceback
import hooking

TVMD_SOCKET_PATH = '/tmp/termidesk/tvmd.sock'
TERMIDESK_TAG = 'ovirt-vm:termideskCompatible'


def check_tvmd_state():
    """
    Check if TVMD agent is running by verifying the socket exists.

    Returns:
        bool: True if TVMD socket exists and is a valid socket
    """
    if not os.path.exists(TVMD_SOCKET_PATH):
        return False

    try:
        return stat.S_ISSOCK(os.stat(TVMD_SOCKET_PATH).st_mode)
    except (OSError, IOError):
        return False


def is_termidesk_enabled(domxml):
    """
    Check if Termidesk compatibility is enabled in VM configuration.

    Args:
        domxml: XML DOM object of the VM configuration

    Returns:
        bool: True if Termidesk is enabled, False otherwise
    """
    try:
        termidesk_elements = domxml.getElementsByTagName(TERMIDESK_TAG)
        if not termidesk_elements:
            return False

        termidesk_element = termidesk_elements[0]
        if not termidesk_element.firstChild:
            return False

        value = termidesk_element.firstChild.nodeValue.strip().lower()
        return json.loads(value)

    except (IndexError, ValueError, AttributeError):
        hooking.log("termidesk: Failed to parse Termidesk flag, defaulting to False")
        return False
    except json.JSONDecodeError as e:
        hooking.log(f"termidesk: Invalid JSON in Termidesk flag: {e}")
        return False


def is_hda_duplex_present(domxml):
    """
    Check if Intel HD Audio device is already configured.

    Args:
        domxml: XML DOM object of the VM configuration

    Returns:
        bool: True if Intel HD Audio device exists
    """
    # Check for sound devices with model='ich6' or 'ich9' (Intel HD Audio)
    for sound in domxml.getElementsByTagName('sound'):
        model = sound.getAttribute('model')
        if model in ['ich6', 'ich9', 'hda']:
            return True

    return False


def add_hda_duplex_device(domxml):
    """
    Add Intel HD Audio sound device if it's not already present.

    Args:
        domxml: XML DOM object of the VM configuration
    """
    if is_hda_duplex_present(domxml):
        hooking.log("termidesk: Intel HD Audio device already present, skipping")
        return

    devices_list = domxml.getElementsByTagName('devices')
    if not devices_list:
        hooking.log("termidesk: No devices section found, cannot add audio device")
        return

    devices = devices_list[0]

    sound_device = domxml.createElement('sound')
    sound_device.setAttribute('model', 'ich9')

    codec = domxml.createElement('codec')
    codec.setAttribute('type', 'duplex')
    sound_device.appendChild(codec)

    devices.appendChild(sound_device)

    hooking.log("termidesk: Added Intel HD Audio (ich9) sound device")


def configure_spice_compression(domxml):
    """
    Configure SPICE compression settings for Termidesk WAN optimization.

    According to libvirt documentation, SPICE compression settings are configured
    via child elements of the graphics element:
    - <image compression="auto_glz"/>
    - <jpeg compression="always"/>
    - <zlib compression="always"/>
    - <playback compression="on"/>

    Args:
        domxml: XML DOM object of the VM configuration
    """
    graphics_elements = domxml.getElementsByTagName('graphics')

    for graphics in graphics_elements:
        if graphics.getAttribute('type') != 'spice':
            continue

        hooking.log("termidesk: Configuring SPICE compression settings")

        image_elements = graphics.getElementsByTagName('image')
        if image_elements:
            image = image_elements[0]
            if not image.hasAttribute('compression'):
                image.setAttribute('compression', 'auto_glz')
                hooking.log("termidesk: Set image compression=auto_glz")
        else:
            image = domxml.createElement('image')
            image.setAttribute('compression', 'auto_glz')
            graphics.appendChild(image)
            hooking.log("termidesk: Created image element with compression=auto_glz")

        jpeg_elements = graphics.getElementsByTagName('jpeg')
        if jpeg_elements:
            jpeg = jpeg_elements[0]
            if not jpeg.hasAttribute('compression'):
                jpeg.setAttribute('compression', 'always')
                hooking.log("termidesk: Set jpeg compression=always")
        else:
            jpeg = domxml.createElement('jpeg')
            jpeg.setAttribute('compression', 'always')
            graphics.appendChild(jpeg)
            hooking.log("termidesk: Created jpeg element with compression=always")

        zlib_elements = graphics.getElementsByTagName('zlib')
        if zlib_elements:
            zlib = zlib_elements[0]
            if not zlib.hasAttribute('compression'):
                zlib.setAttribute('compression', 'always')
                hooking.log("termidesk: Set zlib compression=always")
        else:
            zlib = domxml.createElement('zlib')
            zlib.setAttribute('compression', 'always')
            graphics.appendChild(zlib)
            hooking.log("termidesk: Created zlib element with compression=always")

        playback_elements = graphics.getElementsByTagName('playback')
        if playback_elements:
            playback = playback_elements[0]
            if not playback.hasAttribute('compression'):
                playback.setAttribute('compression', 'on')
                hooking.log("termidesk: Set playback compression=on")
        else:
            playback = domxml.createElement('playback')
            playback.setAttribute('compression', 'on')
            graphics.appendChild(playback)
            hooking.log("termidesk: Created playback element with compression=on")

        if not graphics.hasAttribute('streamingMode'):
            graphics.setAttribute('streamingMode', 'filter')
            hooking.log("termidesk: Set streamingMode=filter")

        return

    hooking.log("termidesk: WARNING: No SPICE graphics element found")


def add_qemu_namespace(domxml):
    """
    Add QEMU namespace to the domain XML if not present.

    Args:
        domxml: XML DOM object of the VM configuration
    """
    domains = domxml.getElementsByTagName('domain')
    if not domains:
        return

    domain = domains[0]
    if not domain.hasAttribute('xmlns:qemu'):
        domain.setAttribute('xmlns:qemu',
                           'http://libvirt.org/schemas/domain/qemu/1.0')


def is_webdav_channel_present(domxml):
    """
    Check if WebDAV SPICE channel is already configured.

    Args:
        domxml: XML DOM object of the VM configuration

    Returns:
        bool: True if WebDAV channel exists
    """
    for source in domxml.getElementsByTagName('source'):
        if source.getAttribute('channel') == 'org.spice-space.webdav.0':
            return True
    return False


def create_spice_channel(domxml, channel_name, target_name):
    """
    Create a SPICE channel element.

    Args:
        domxml: XML DOM object
        channel_name: Source channel name
        target_name: Target device name

    Returns:
        Element: Created channel element
    """
    channel = domxml.createElement('channel')
    channel.setAttribute('type', 'spiceport')

    source = domxml.createElement('source')
    source.setAttribute('channel', channel_name)

    target = domxml.createElement('target')
    target.setAttribute('type', 'virtio')
    target.setAttribute('name', target_name)

    channel.appendChild(source)
    channel.appendChild(target)
    return channel


def create_tvm_channel(domxml):
    """
    Create TVM Unix socket channel.

    Args:
        domxml: XML DOM object

    Returns:
        Element: Created TVM channel element
    """
    channel = domxml.createElement('channel')
    channel.setAttribute('type', 'unix')

    source = domxml.createElement('source')
    source.setAttribute('mode', 'connect')
    source.setAttribute('path', TVMD_SOCKET_PATH)

    reconnect = domxml.createElement('reconnect')
    reconnect.setAttribute('enabled', 'yes')
    reconnect.setAttribute('timeout', '10')
    source.appendChild(reconnect)

    target = domxml.createElement('target')
    target.setAttribute('type', 'virtio')
    target.setAttribute('name', 'ru.termidesk.tvm.0')

    channel.appendChild(source)
    channel.appendChild(target)
    return channel


def add_termidesk_channels(domxml):
    """
    Add all required Termidesk channels to the VM configuration.

    Args:
        domxml: XML DOM object of the VM configuration
    """
    devices_list = domxml.getElementsByTagName('devices')
    if not devices_list:
        hooking.log("termidesk: No devices section found in domain XML")
        return

    devices = devices_list[0]

    if not is_webdav_channel_present(domxml):
        webdav_channel = create_spice_channel(
            domxml,
            'org.spice-space.webdav.0',
            'org.spice-space.webdav.0'
        )
        devices.appendChild(webdav_channel)
        hooking.log("termidesk: Added WebDAV SPICE channel")

    if check_tvmd_state():
        tvm_channel = create_tvm_channel(domxml)
        devices.appendChild(tvm_channel)
        hooking.log("termidesk: Added TVM Unix socket channel")
    else:
        hooking.log("termidesk: TVMD agent is not running, skipping TVM channel")

    channels = [
        ('TDSK_STREAM', 'ru.termidesk.RealtimeStreaming.0'),
        ('TDSK_PRINTER', 'ru.termidesk.Printer.0'),
        ('TDSK_PCSC', 'ru.termidesk.PCSC.0')
    ]

    for channel_name, target_name in channels:
        channel = create_spice_channel(domxml, channel_name, target_name)
        devices.appendChild(channel)

    hooking.log("termidesk: Added Termidesk SPICE channels")


def main():
    """
    Main hook entry point.

    This function is called by VDSM when the hook is triggered.
    """
    try:
        domxml = hooking.read_domxml()

        if not is_termidesk_enabled(domxml):
            hooking.log("termidesk: Termidesk compatibility not enabled, skipping")
            return

        hooking.log("termidesk: Termidesk compatibility enabled, configuring...")

        add_qemu_namespace(domxml)

        configure_spice_compression(domxml)

        add_hda_duplex_device(domxml)

        add_termidesk_channels(domxml)

        hooking.write_domxml(domxml)

        hooking.log("termidesk: Configuration completed successfully")

    except SystemExit:
        raise
    except Exception as e:
        hooking.log(f"termidesk: ERROR: {traceback.format_exc()}")

        try:
            if 'domxml' in locals():
                xml_debug = domxml.toxml()[:1000]
                hooking.log(f"termidesk: XML debug (first 1000 chars): {xml_debug}")
        except:
            pass

        sys.exit(2)


if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        hooking.log(f"termidesk: UNHANDLED EXCEPTION: {traceback.format_exc()}")
        sys.exit(2)