#!/usr/bin/env python3
"""
DebSoftwareApp protocol

- full static type‑checking
- no circular imports
- no dependency on the SoftwareGtk class
- explicit documentation of what PackageActionHandler expects

You can later add other objects to the class:
- Cancel button
- Status label
- Spinner
- Progress page
"""

import os
import glob
import gettext
from typing import Protocol
import logging
from enum import StrEnum

_ = gettext.translation('iso-constructor', fallback=True).gettext

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


class DebSoftwareApp(Protocol):
    """ DebSoftwareApp protocol """
    logging: logging.Logger
    script_dir: str
    scripts_dir: str
    window: Gtk.Window

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,
}

class PathSelectionAction(StrEnum):
    """Actions for path selection dialogs."""
    FILE = "file"
    DIRECTORY = "directory"


def show_dialog(
    title: str,
    text: str,
    main_window: Gtk.Window = None,
    icon: DialogIcon = DialogIcon.INFO,
    buttons: list = None,
) -> Gtk.ResponseType:
    """
    Show a dialog.

    Args:
        title (str): The title of the dialog.
        text (str): The text to display in the dialog.
        main_window (Gtk.Window, optional): The main window of the application. Defaults to None.
        icon (DialogIcon, optional): The icon to display in the dialog. Defaults to DialogIcon.INFO.
        buttons (list, optional): A list of button labels to display in the dialog. Defaults to ["OK"].
                                  Possible values: OK, CANCEL, YES, NO

    Returns:
        Gtk.ResponseType: The response type of the dialog.
    """
    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 select_path(
    main_window: Gtk.Window = None,
    action: PathSelectionAction = PathSelectionAction.FILE,
    filter_patterns: list[str] = None,
    start_directory: str = None,
) -> str | None:
    """
    Open a file or directory selection dialog.

    Args:
        main_window: Parent window.
        action: PathSelectionAction.FILE or PathSelectionAction.DIRECTORY.
        filter_patterns: List of file patterns (e.g., ["*.txt"]). Ignored for directories.
        start_directory: Initial directory to show in the dialog. If None, uses the default.

    Returns:
        Path to the selected file/directory, or None if canceled.
    """
    # Set title based on action
    title = _("Select a directory") if action == PathSelectionAction.DIRECTORY else _("Select a path")

    # Set GTK action
    gtk_action = (
        Gtk.FileChooserAction.OPEN if action == PathSelectionAction.FILE
        else Gtk.FileChooserAction.SELECT_FOLDER
    )

    # Create dialog
    dialog = Gtk.FileChooserDialog(
        title=title,
        transient_for=main_window,
        modal=True,
        action=gtk_action,
    )
    dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Select"), Gtk.ResponseType.OK)

    # Set start directory if provided
    if start_directory:
        dialog.set_current_folder(Gio.File.new_for_path(start_directory))

    # Add file filter if patterns are provided and action is FILE
    if action == PathSelectionAction.FILE and filter_patterns:
        file_filter = Gtk.FileFilter()
        for pattern in filter_patterns:
            file_filter.add_pattern(pattern)
        dialog.set_filter(file_filter)

    # Track the response
    selected_path = None

    def on_response(dialog, response_id):
        nonlocal selected_path
        if response_id == Gtk.ResponseType.OK:
            selected_path = dialog.get_file().get_path()
        dialog.close()

    dialog.connect("response", on_response)
    dialog.present()

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

    return selected_path

def get_lsb_release_info(repo_dir: str=None) -> dict[str, str, str, str]:
    '''
    Get lsb-release information by directly reading all /etc/*release files.

    Args:
        repo_dir: Directory where the repo is unpacked (root subdirectory).

    Returns:
        Dictionary of lsb-release information.
    '''
    lsb_dict = {'path': repo_dir}
    base_path = '/etc' if repo_dir is None else os.path.join(repo_dir, 'root/etc')
    if not os.path.isdir(base_path):
        return {}

    # Find all *release files in the base_path
    release_files = glob.glob(os.path.join(base_path, '*release'))

    # Priority order for fields
    field_priority = {
        'name': ['DISTRIB_DESCRIPTION'],
        'id': ['ID', 'DISTRIB_ID'],
        'codename': ['VERSION_CODENAME', 'DISTRIB_CODENAME'],
        'version': ['VERSION_ID', 'DISTRIB_RELEASE', 'VERSION']
    }

    # Read each file and extract fields
    for filepath in release_files:
        try:
            with open(file=filepath, mode='r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith('#'):
                        continue
                    if '=' in line:
                        key, value = line.split('=', 1)
                        key = key.strip()
                        value = value.strip().strip('"')
                        for field, keys in field_priority.items():
                            if key in keys and field not in lsb_dict:
                                lsb_dict[field] = value
        except (IOError, OSError):
            continue  # Skip files we can't read

    return lsb_dict

def get_row_count(listbox: Gtk.ListBox) -> int:
    """ Return the number of rows in a Gtk.ListBox """
    count = 0
    while listbox.get_row_at_index(count) is not None:
        count += 1
    return count
