#!/usr/bin/env python3
"""
Encryption tab for SolydXK System

Author: Arjen Balfoort
Date: 2026-02-22
"""

import gettext
import os
import re
import tempfile
import stat
import subprocess
import threading
import time

from encryption import PartitionInfo
from utils import (
    show_dialog,
    DialogIcon,
    DebSoftwareApp
)

import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib, GdkPixbuf

# i18n: http://docs.python.org/3/library/gettext.html
_ = gettext.translation('solydxk-system', fallback=True).gettext


class TabEncryption:
    """
    Encryption tab for SolydXK System
    """
    def __init__(self, app: DebSoftwareApp) -> None:
        self.app: DebSoftwareApp = app
        self.partitions_info: PartitionInfo = PartitionInfo(self.app)

    def setup_encryption_tab(self) -> None:
        """
        Set up the encryption tab.
        """
        # Reset the progress bar
        self.app.progressbar.set_fraction(0)

        # Check if the treeview is already set up
        if self.app.treeview_partitions.get_model():
            return

        # Create a ListStore with columns:
        # Is_Encrypted (bool:hidden), Select (bool), Icon (str), Device (str),
        # Label (str), Mount Point (str), Fstab Mounted (bool)
        store = Gtk.ListStore(bool, bool, GdkPixbuf.Pixbuf, str, str, str, bool)
        self.app.store = store
        self.app.treeview_partitions.set_model(store)

        # Set the cell data function to control the 'sensitive' property
        def on_cell_data_func(_column, cell, model, model_iter, _data):
            is_encrypted = model[model_iter][0]
            cell.set_property("sensitive", not is_encrypted)

        # Checkbox column (set active=1 because the first column is hidden)
        renderer = Gtk.CellRendererToggle()
        renderer.connect("toggled", self.on_partition_toggled, store)
        column_select = Gtk.TreeViewColumn(_("Select"), renderer, active=1)
        column_select.set_expand(True)
        column_select.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
        column_select.set_cell_data_func(renderer, on_cell_data_func, None)
        self.app.treeview_partitions.append_column(column_select)

        # Image column
        renderer = Gtk.CellRendererPixbuf()
        column_icon = Gtk.TreeViewColumn("", renderer, pixbuf=2)
        column_icon.set_expand(True)
        column_icon.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
        self.app.treeview_partitions.append_column(column_icon)

        # String columns
        renderer = Gtk.CellRendererText()
        column_device = Gtk.TreeViewColumn(_("Device"), renderer, text=3)
        column_device.set_expand(True)
        column_device.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
        self.app.treeview_partitions.append_column(column_device)

        renderer = Gtk.CellRendererText()
        column_label = Gtk.TreeViewColumn(_("Label"), renderer, text=4)
        column_label.set_expand(True)
        column_label.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
        self.app.treeview_partitions.append_column(column_label)

        renderer = Gtk.CellRendererText()
        column_mount = Gtk.TreeViewColumn(_("Mount Point"), renderer, text=5)
        column_mount.set_expand(True)
        column_mount.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
        self.app.treeview_partitions.append_column(column_mount)

        # Fill treeview
        self.fill_treeview(store)

        # Password entries
        self.app.password_1_entry.connect("changed", self.on_password_changed)
        self.app.password_2_entry.connect("changed", self.on_password_changed)

        # Add buttons
        self.app.refresh_button.connect("clicked", self.on_refresh_clicked, store)
        self.app.encrypt_button.connect("clicked", self.on_encrypt_clicked)

    def fill_treeview(self, store: Gtk.ListStore) -> None:
        """
        Fill the treeview with partitions.

        Args:
            store (Gtk.ListStore): The ListStore to fill.
        """
        # Create a row for each hardware entry
        for partition in self.partitions_info.partitions:
            key = list(partition.keys())[0]
            info = list(partition.values())[0]

            # Decide which icon to show
            img = "encrypted.svg" if info.get("is_encrypted") else "unencrypted.svg"
            if info.get("is_usb"):
                img = "encrypted-usb.svg" if info.get("is_encrypted") else "unencrypted-usb.svg"
            icon = GdkPixbuf.Pixbuf.new_from_file_at_size(
                   os.path.join(self.app.images_dir, img), 32, 32)

            # Create a ListStore row
            row = [info.get("is_encrypted"),
                False,
                icon,
                key, info.get("label"),
                info.get("mount_point"),
                info.get("is_fstab_mounted")]
            store.append(row)

    def on_password_changed(self, _entry: Gtk.Entry) -> None:
        """ 
        Callback for when the password entries change.
        """
        password_1 = self.app.password_1_entry.get_text()
        password_2 = self.app.password_2_entry.get_text()

        if password_1 == "" and password_2 == "":
            # Clear the image if both entries are empty
            self.app.img_password_check.set_from_pixbuf(None)
        elif password_1 == password_2 and password_1 != "":
            self.app.img_password_check.set_from_icon_name("emblem-checked")
            self.app.img_password_check.set_pixel_size(48)
            self.app.img_password_check.set_tooltip_text("Passwords match")
        else:
            self.app.img_password_check.set_from_icon_name("emblem-error")
            self.app.img_password_check.set_pixel_size(48)
            self.app.img_password_check.set_tooltip_text("Passwords do not match")


    def on_refresh_clicked(self, _button: Gtk.Button, store: Gtk.ListStore) -> None:
        """
        Callback for when the refresh button is clicked.
        """
        self.partitions_info.refresh_partitions()
        store.clear()
        self.fill_treeview(store)

    @staticmethod
    def on_partition_toggled(_renderer: Gtk.CellRendererToggle,
                             path: int,
                             store: Gtk.ListStore) -> None:
        """
        Handle the click event for the installed check button.
        """
        s_iter = store.get_iter(path)
        current_state = store[s_iter][1]
        store[s_iter][1] = not current_state

    def run_encryption_script(self, *command):
        """Run the encryption script and update the progress bar."""
        with subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,  # Line buffered
            universal_newlines=True
        ) as process:

            # Pulse the progress bar while running
            def pulse_progress():
                self.app.progressbar.pulse()
                return True  # Keep pulsing

            pulse_id = GLib.idle_add(pulse_progress)

            # Read output in real-time and log it
            def read_stream(stream, log_level):
                for line in stream:
                    # Remove all backspace characters and the characters they are meant to overwrite
                    line = re.sub(r'.\x08', '', line.strip())

                    if any(string in ['error', 'failed'] for string in line.lower()):
                        log_level = self.app.logging.ERROR
                    if line:
                        if log_level == self.app.logging.ERROR:
                            self.app.logging.error(line)
                        else:
                            self.app.logging.info(line)

            # Start threads to read stdout and stderr
            stdout_thread = threading.Thread(
                target=read_stream,
                args=(process.stdout, self.app.logging.INFO),
                daemon=True
            )
            stderr_thread = threading.Thread(
                target=read_stream,
                args=(process.stderr, self.app.logging.ERROR),
                daemon=True
            )
            stdout_thread.start()
            stderr_thread.start()

            # Wait for process to complete
            process.wait()

            # Stop pulsing
            GLib.idle_add(lambda: GLib.source_remove(pulse_id), priority=GLib.PRIORITY_HIGH)

            # Update UI from main thread
            def on_finished():
                self.app.progressbar.set_fraction(1.0)
                self.app.progressbar_label.set_text("")
                if process.returncode == 0:
                    # Refresh partitions
                    self.on_refresh_clicked(None, self.app.store)

                    # Show success dialog
                    show_dialog(_("Encryption"),
                                _("Encryption completed successfully."),
                                self.app.window)
                else:
                    # Show error dialog
                    show_dialog(_("Encryption"),
                                _("Encryption failed:\nCheck log for details."),
                                self.app.window,
                                DialogIcon.ERROR)

            GLib.idle_add(on_finished, priority=GLib.PRIORITY_HIGH)

    @staticmethod
    def create_fifo() -> str:
        """ Create a named pipe for password input. """
        fifo_path = os.path.join(tempfile.gettempdir(), "luks_passphrase_fifo")
        try:
            os.mkfifo(fifo_path, mode=stat.S_IRUSR | stat.S_IWUSR)
        except FileExistsError:
            pass
        return fifo_path

    def on_encrypt_clicked(self, _button: Gtk.Button) -> None:
        """
        Callback for when the encrypt button is clicked.
        """
        # Warn the user
        response = show_dialog(_("Encryption"),
                               _("This action will remove all data on the partition.\n"
                               "Please backup your data before you continue.\n\n"
                               "Are you sure you want to continue?"),
                               self.app.window,
                               DialogIcon.WARNING,
                               ['OK', 'Cancel'])

        if response == Gtk.ResponseType.CANCEL:
            return

        # Check passwords
        password_1 = self.app.password_1_entry.get_text()
        password_2 = self.app.password_2_entry.get_text()

        if password_1 != password_2:
            show_dialog(_("Encryption"),
                        _("Passwords do not match."),
                        self.app.window,
                        DialogIcon.WARNING)
            return

        if len(password_1) < 6:
            show_dialog(_("Encryption"),
                        _("Password must be at least 6 characters long."),
                        self.app.window,
                        DialogIcon.WARNING)
            return

        # Get selected devices
        devices = []
        for row in self.app.store:
            _is_encrypted, selected, _icon, key, _label, _mount_point, _is_fstab_mounted = row
            if selected:
                devices.append(key)

        if not devices:
            show_dialog(_("Encryption"),
                        _("No device selected for encryption."),
                        self.app.window,
                        DialogIcon.WARNING)
            return

        # Create a unique FIFO for each device
        fifo_path = tempfile.mktemp(suffix='.fifo', prefix='solydxk_')
        os.mkfifo(fifo_path)

        # pkexec will prompt for root password via polkit
        command = ["pkexec",
                   "--disable-internal-agent",
                   "python3",
                   os.path.join(self.app.script_dir, "encryption_sudo.py"),
                   fifo_path] + devices

        self.app.logging.debug("Running command: %s", ' '.join(command))

        encryption_thread = threading.Thread(
            target=self.run_encryption_script,
            args=(command),
            daemon=True
        )
        encryption_thread.start()

        # Give the reader a moment to start (optional, but recommended)
        time.sleep(0.1)

        # Now write the passphrase to the FIFO
        with open(file=fifo_path, mode="w", encoding="utf-8") as fifo:
            fifo.write(password_1)
