#!/usr/bin/env python3
"""
Utility functions

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

import gettext
import re
import queue
import threading
import subprocess
import logging
from typing import Protocol
from pathlib import Path
from enum import StrEnum
import psutil

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


_ = gettext.translation('solydxk-system', fallback=True).gettext


class DebSoftwareApp(Protocol):
    """ DebSoftwareApp protocol """
    logging: logging
    progressbar: Gtk.ProgressBar
    progressbar_label: Gtk.Label
    window: Gtk.Window
    spinner: Gtk.Spinner
    script_dir: str
    save_dir: str
    data_dir: str
    images_dir: str
    localize_dir: str
    amd_vendor_id: str
    broadcom_vendor_id: str
    nvidia_vendor_id: str
    store: Gtk.ListStore
    treeview_partitions: Gtk.TreeView
    treeview_locales: Gtk.TreeView
    password_1_entry: Gtk.Entry
    password_2_entry: Gtk.Entry
    img_password_check: Gtk.Image
    refresh_button: Gtk.Button
    encrypt_button: Gtk.Button
    dd_continent: Gtk.DropDown
    dd_city: Gtk.DropDown
    save_locale_button: Gtk.Button
    chk_select_all: Gtk.CheckButton
    listbox_cleanup: Gtk.ListBox
    listbox_hold_back: Gtk.ListBox
    listbox_repo_solydxk: Gtk.ListBox
    listbox_repo_debian: Gtk.ListBox
    listbox_repo_other: Gtk.ListBox
    package_search: Gtk.Entry
    mark_button: Gtk.Button
    unmark_button: Gtk.Button
    cleanup_button: Gtk.Button
    save_sources_button: Gtk.Button
    chk_backports: Gtk.CheckButton

class LogLevel(StrEnum):
    """ Possible log levels """
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"
    DEBUG = 'debug'
    SUCCESS = 'success'
    CANCELED = 'canceled'
    PROGRESS = 'progress'

class DialogIcon(StrEnum):
    """ Possible dialog icons """
    INFO = "dialog-information"
    WARNING = "dialog-warning"
    ERROR = "dialog-error"
    QUESTION = 'dialog-question'

# Map log level to dialog icon
LogLevel_To_DialogIcon = {
    LogLevel.INFO: DialogIcon.INFO,
    LogLevel.WARNING: DialogIcon.WARNING,
    LogLevel.ERROR: DialogIcon.ERROR,
    LogLevel.DEBUG: DialogIcon.INFO,
    LogLevel.SUCCESS: DialogIcon.INFO,
    LogLevel.CANCELED: DialogIcon.WARNING,
    LogLevel.PROGRESS: DialogIcon.INFO,
}

def show_dialog(
    title: str,
    text: str,
    main_window: Gtk.Window = None,
    icon: DialogIcon = DialogIcon.INFO,
    buttons: list = None,
) -> Gtk.ResponseType:
    if buttons is None:
        buttons = ["OK"]

    # Create the dialog window  
    dialog = Gtk.Window(
        title=title,
        transient_for=main_window,
        modal=True,
        deletable=False,
    )
    dialog.set_default_size(400, -1)
    dialog.set_modal(True)

    # Create a custom header bar with only a close button
    header_bar = Gtk.HeaderBar()
    header_bar.set_title_widget(Gtk.Label(label=title))
    header_bar.set_show_title_buttons(False)  # Hide minimize/maximize buttons

    # Manually add a close button
    close_button = Gtk.Button(icon_name="window-close-symbolic")
    close_button.connect("clicked", lambda _: dialog.close())
    header_bar.pack_end(close_button)

    dialog.set_titlebar(header_bar)

    # Main box for content
    main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    dialog.set_child(main_box)

    # Icon and text box
    content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    content_box.set_spacing(12)
    content_box.set_css_classes(['dialog_box'])
    main_box.append(content_box)

    # Add icon
    picture = Gtk.Picture()
    picture.set_can_shrink(False)
    picture.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
    display = Gdk.Display.get_default()
    icon_theme = Gtk.IconTheme.get_for_display(display)
    icon_paintable = icon_theme.lookup_icon(
        icon.value, None, 64, 1, Gtk.TextDirection.LTR, 0
    )
    if icon_paintable:
        picture.set_paintable(icon_paintable)
    content_box.append(picture)

    # Add text
    label = Gtk.Label(label=text, halign=Gtk.Align.START, wrap=True)
    label.set_css_classes(['dialog_text'])
    content_box.append(label)

    # Add buttons
    button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    button_box.set_spacing(12)
    button_box.set_halign(Gtk.Align.END)
    button_box.set_css_classes(['dialog_box'])
    main_box.append(button_box)

    response_id = None

    def on_response(button, response):
        nonlocal response_id
        response_id = response
        dialog.close()

    for btn_text in buttons:
        btn = Gtk.Button(label=btn_text)
        btn.connect("clicked",
                    on_response,
                    getattr(Gtk.ResponseType, btn_text.upper(),
                    Gtk.ResponseType.OK))
        button_box.append(btn)

    # Show the dialog
    dialog.present()

    # Run a local main loop to keep the dialog open
    context = GLib.MainContext.default()
    while dialog.is_visible():
        context.iteration(True)

    return response_id

def clear_listbox(listbox: Gtk.ListBox) -> None:
    """Clear all rows in a Gtk.ListBox."""
    while True:
        row = listbox.get_row_at_index(0)  # Get the first row
        if row is None:
            break  # No more rows
        listbox.remove(row)  # Remove the row

def is_dpkg_locked() -> bool:
    """Check if dpkg is locked."""
    filepath = "/var/lib/dpkg/lock"
    for proc in psutil.process_iter(['pid', 'name', 'open_files']):
        try:
            for file in proc.info['open_files'] or []:
                if filepath in file.path:
                    return True
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            continue
    return False

def is_package_installed(package_name):
    """
    Check if a package is installed on a Debian-based system.

    Args:
        package_name (str): The name of the package to check.

    Returns:
        bool: True if the package is installed, False otherwise.
    """
    try:
        subprocess.run(
            ["dpkg", "-s", package_name],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=True
        )
        return True
    except subprocess.CalledProcessError:
        return False

def get_domain_from_uri(uri: str) -> str:
    """
    Get the domain from the uri
    """
    match = re.search(r'https?://([a-z0-9\-\.]+)', uri)
    if match:
        return match.group(1)
    return None

def is_link_pointing_to(link_path: str, target_path: str) -> bool:
    """
    Check if the link points to the target path.
    This will return True if the symlink points to the target.
    It will return False if the link is broken, not a symlink, or the paths don’t match.

    Args:
        link_path (str): The link path.
        target_path (str): The target path.
    """
    try:
        return Path(link_path).resolve() == Path(target_path).resolve()
    except FileNotFoundError:
        return False

def get_debian_version() -> int:
    """ Get Debian's version number (int) """
    for version_file in ['/etc/debian_version','/etc/lsb-release', '/etc/os-release']:
        with open(file=version_file, mode='r', encoding='utf-8') as f:
            content = f.read().strip()
            match =re.search(r'^([\d]{2})\.*\d*$|"([\d]{2})"', content)
            if match:
                if match.group(1):
                    return int(match.group(1))
                return int(match.group(2))
    return 0

class SudoCommandThread(threading.Thread):
    """
    Thread to run a command with sudo.
    Queues output lines.

    Args:
        command (list): The command to run with sudo
        subject (str): The subject of the message
        result_queue (queue.Queue): The queue to add the output lines to
        logging_handler (logging): The logging handler
    """
    def __init__(self,
                 command: list,
                 subject: str,
                 result_queue: queue.Queue = None,
                 logging_handler = None):
        super().__init__(daemon=True)

        self.command = command
        self.subject = subject
        self.result_queue = result_queue
        self.logging_handler = logging_handler

    def queue_message(self,
                      level: LogLevel,
                      message: str,
                      percentage: int) -> None:
        """
        Add a message to the queue with its level (info/error).
        
        Args:
            level (ErrorLevel): The level of the message.
            message (str): The message to add to the queue.
            percentage (int): The progress percentage.
        """
        # Add the message to the queue
        if self.result_queue is not None:
            info_item = {'level': level,
                        'message': message,
                        'progress': percentage}
            self.result_queue.put(info_item)

        # Write the line to the log file
        if self.logging_handler is not None and message:
            message = message.replace('\n', ' ')
            if level == LogLevel.INFO:
                self.logging_handler.info(message)
            elif level == LogLevel.WARNING:
                self.logging_handler.warning(message)
            elif level == LogLevel.ERROR:
                self.logging_handler.error(message)
            elif level == LogLevel.DEBUG:
                self.logging_handler.debug(message)

    def run(self):
        """
        Run a command with sudo, handling password input if necessary.
        """
        current_progress = 0
        self.queue_message(LogLevel.INFO,
                           _("Running command: %s") % ' '.join(self.command),
                           0)
        try:
            # Running the command (parse progress from output)
            pattern = re.compile(r'(\d+)%')
            with subprocess.Popen(
                self.command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True
            ) as process:
                for line in process.stdout:
                    line = line.strip()
                    match = pattern.search(line)
                    if match:
                        progress = int(match.group(1))
                        # Only update if the new progress is greater than the current
                        current_progress = max(current_progress, progress)
                        self.queue_message(LogLevel.INFO,
                                           line,
                                           current_progress)
                    else:
                        # Increment progress slightly for each line
                        current_progress = min(90, current_progress + 1)
                        self.queue_message(LogLevel.INFO,
                                           line,
                                           current_progress)

                process.wait()

                if process.returncode != 0:
                    current_progress = 100
                    if process.returncode not in (126, 127):
                        error = process.stderr.read()
                        if not error:
                            error = _("See log file for details.")
                        self.queue_message(
                            LogLevel.ERROR,
                            _("Failed to save %s") % self.subject.lower() + ":\n" + error,
                            current_progress
                        )
                    else:
                        self.queue_message(
                            LogLevel.CANCELED,
                            _("Canceled by user"),
                            current_progress,
                        )
                else:
                    # Completion (e.g., 100% progress)
                    current_progress = 100
                    self.queue_message(LogLevel.INFO,
                                       _("%s saved successfully.") % self.subject,
                                       current_progress)

        except subprocess.CalledProcessError as e:
            current_progress = 100
            self.queue_message(LogLevel.ERROR,
                               _("An error occurred: %s") % e,
                               current_progress)
