#!/usr/bin/env python3
"""Ubuntu Studio Audio Configuration — main entry point."""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

# Ensure the package directory is importable when launched from /usr/bin
_pkg_dir = Path(__file__).resolve().parent
if str(_pkg_dir) not in sys.path:
    sys.path.insert(0, str(_pkg_dir.parent))

from backend.audio_config import (  # noqa: E402
    APP_TITLE,
    JACK_BOOL_PARAMS,
    JACK_PARAM_DESCRIPTIONS,
    VALID_BUFFER_SIZES,
    VALID_SAMPLE_RATES,
    SELF_CONNECT_DESCRIPTIONS,
    SELF_CONNECT_MODES,
    BootParams,
    FFADODevice,
    JackConfig,
    audio_limits_configured,
    cli_dummy_start,
    cli_dummy_stop,
    cli_startup,
    cli_write_params,
    dpkg_installed,
    dummy_device_active,
    dummy_device_enabled,
    dummy_device_disable,
    dummy_device_enable,
    dummy_device_start,
    dummy_device_stop,
    ffado_pw_configured,
    ffado_tools_installed,
    firewire_controller_present,
    firstrun_marker_exists,
    get_default_quantum,
    get_user_quantum,
    has_gnome_pw_extension,
    has_plasma_pw_widget,
    install_pipewire_config,
    is_live_session,
    is_pipewire_config,
    list_ffado_devices,
    pipewire_jack_enabled,
    read_boot_params,
    read_jack_config,
    remove_ffado_pw_config,
    run_installer_fix_and_reboot,
    toggle_pipewire_jack,
    touch_firstrun_marker,
    user_in_audio_group,
    validate_quantum,
    write_boot_params,
    write_ffado_pw_config,
    write_jack_config,
)
from ui.base import AudioConfigUI, ChecklistItem, ComboItem  # noqa: E402
from i18n import _  # noqa: E402


# ======================================================================
# Desktop-environment detection & toolkit selection
# ======================================================================


def _detect_toolkit() -> str:
    """Return ``"qt"`` or ``"gtk"`` based on the running DE.

    Qt desktops:  KDE Plasma (Kubuntu, Ubuntu Studio), LXQt (Lubuntu)
    GTK desktops: GNOME (Ubuntu), Xfce (Xubuntu), MATE (Ubuntu MATE),
                  Budgie (Ubuntu Budgie), Cinnamon (Ubuntu Cinnamon),
                  Unity (Ubuntu Unity)
    """
    desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
    session = os.environ.get("DESKTOP_SESSION", "").lower()
    if any(tok in desktop for tok in ("kde", "plasma", "lxqt")):
        return "qt"
    if "plasma" in session or "lxqt" in session:
        return "qt"
    return "gtk"


def _create_ui() -> AudioConfigUI:
    """Instantiate the correct UI backend, with fallback."""
    preferred = _detect_toolkit()
    if preferred == "qt":
        try:
            from ui.qt_ui import QtUI
            ui = QtUI()
            ui.init()
            return ui
        except ImportError:
            pass  # fall through to GTK
    try:
        from ui.gtk_ui import GtkUI
        ui = GtkUI()
        ui.init()
        return ui
    except ImportError:
        pass
    # Last resort: try Qt even on non-KDE desktops
    try:
        from ui.qt_ui import QtUI
        ui = QtUI()
        ui.init()
        return ui
    except ImportError:
        print(
            _("ERROR: Neither GTK 4 (python3-gi + gir1.2-adw-1) nor "
              "PyQt6 (python3-pyqt6) is available."),
            file=sys.stderr,
        )
        sys.exit(1)


# ======================================================================
# Application controller
# ======================================================================


def _configure_audio(ui: AudioConfigUI) -> None:
    """Show the audio control panel and apply selected settings."""

    has_pw_widget = has_plasma_pw_widget()
    has_gnome_ext = has_gnome_pw_extension()
    widget_manages_quantum = has_pw_widget or has_gnome_ext

    default_buf, default_rate = get_default_quantum()
    user_buf, user_rate = get_user_quantum()

    cfg = read_jack_config()
    jack_items = [
        ChecklistItem(
            key=param,
            label=param,
            description=JACK_PARAM_DESCRIPTIONS[param],
            checked=cfg.bool_params.get(param, False),
        )
        for param in JACK_BOOL_PARAMS
    ]

    sc_items = [
        ChecklistItem(
            key=mode,
            label=mode,
            description=SELF_CONNECT_DESCRIPTIONS[mode],
            checked=(mode == cfg.self_connect_mode),
        )
        for mode in SELF_CONNECT_MODES
    ]

    panel_result = ui.show_audio_control_panel(
        APP_TITLE,
        _("Audio Control Panel\n\n"
          "Adjust PipeWire and JACK behavior in one place.\n"
          "If you are unsure, leave current values as-is.\n\n"
                    "System defaults (Quantum/Sample Rate): {dbuf}/{drate}"
          ).format(dbuf=default_buf, drate=default_rate),
        buffer_options=[str(s) for s in VALID_BUFFER_SIZES],
        sample_rate_options=[str(r) for r in VALID_SAMPLE_RATES],
        current_buffer=str(user_buf),
        current_sample_rate=str(user_rate),
        jack_items=jack_items,
        self_connect_items=sc_items,
        quantum_managed_by_widget=widget_manages_quantum,
        ok_label=_("Apply"),
        cancel_label=_("Cancel"),
    )
    if panel_result is None:
        return

    buf, rate, selected, sc_mode = panel_result
    buf_i, rate_i = int(buf), int(rate)
    new_bool_params = {p: (p in selected) for p in JACK_BOOL_PARAMS}

    # --- Write configuration ---
    new_cfg = JackConfig(
        buffer_size=buf_i,
        sample_rate=rate_i,
        show_monitor=new_bool_params["jack.show-monitor"],
        merge_monitor=new_bool_params["jack.merge-monitor"],
        show_midi=new_bool_params["jack.show-midi"],
        short_name=new_bool_params["jack.short-name"],
        self_connect_mode=sc_mode,
    )
    write_jack_config(new_cfg, set_quantum=not widget_manages_quantum)

    # Restart dummy if active
    if dummy_device_active():
        subprocess.run(
            ["systemctl", "--user", "restart", "ubuntustudio-dummy-audio.service"]
        )

    if widget_manages_quantum:
        ui.show_info(
            APP_TITLE,
            _("JACK configuration applied.\n\n"
              "Quantum and Sample Rate are managed by your\n"
              "desktop widget / extension."),
        )
    else:
        ui.show_info(
            APP_TITLE,
            _("Configuration applied.\n\n"
              "Quantum: {buf}\n"
                            "Sample Rate: {rate} Hz").format(buf=buf_i, rate=rate_i),
        )


def _change_boot_params(ui: AudioConfigUI) -> None:
    """The 'Change Kernel Boot Parameters' flow."""
    params = read_boot_params()

    items = [
        ChecklistItem(
            key="preempt=full",
            label="preempt=full",
            description=params.PARAM_DESCRIPTIONS["preempt=full"],
            checked=params.preempt_full,
        ),
        ChecklistItem(
            key="threadirqs",
            label="threadirqs",
            description=params.PARAM_DESCRIPTIONS["threadirqs"],
            checked=params.threadirqs,
        ),
        ChecklistItem(
            key="rcu_nocbs=all",
            label="rcu_nocbs=all",
            description=params.PARAM_DESCRIPTIONS["rcu_nocbs=all"],
            checked=params.rcu_nocbs_all,
        ),
        ChecklistItem(
            key="nohz_full=all",
            label="nohz_full=all",
            description=params.PARAM_DESCRIPTIONS["nohz_full=all"],
            checked=params.nohz_full_all,
        ),
    ]

    selected = ui.show_checklist(
        APP_TITLE,
        _("Select which kernel boot parameters you would like. Default is first three."),
        items,
        ok_label=_("Apply (Password Required)"),
        cancel_label=_("Back"),
    )
    if selected is None:
        return

    new_params = BootParams(
        preempt_full="preempt=full" in selected,
        threadirqs="threadirqs" in selected,
        rcu_nocbs_all="rcu_nocbs=all" in selected,
        nohz_full_all="nohz_full=all" in selected,
    )

    ok = ui.show_progress(
        APP_TITLE,
        _("Applying boot parameters…"),
        lambda: write_boot_params(new_params),
    )
    if ok:
        ui.show_info(
            APP_TITLE,
            _("You must restart your computer for these changes to take effect."),
        )
    else:
        ui.show_error(
            APP_TITLE,
            _("An error has occurred or action was cancelled."),
        )


def _toggle_dummy(ui: AudioConfigUI) -> None:
    """Start / stop the dummy audio device."""
    active = dummy_device_active()

    if not active:
        text = _(
            "Sometimes, you may wish to enable a dummy audio device for your "
            "system audio or an audio application so that you don't "
            "inadvertently feed that to your main output or draw from your "
            "main input. This can be useful when you wish to send a music "
            "application to your DAW or other mixer without sending to your "
            "main system output. There may be other workflow reasons as well."
            "\n\nDo you wish to start a stereo dummy audio device?"
        )
        if ui.show_question(APP_TITLE, text, ok_label=_("Yes"), cancel_label=_("Go Back")):
            dummy_device_start()
            if ui.show_question(
                APP_TITLE,
                _("Would you like the dummy audio device to start at login?"),
            ):
                dummy_device_enable()
            else:
                dummy_device_disable()
    else:
        if ui.show_question(APP_TITLE, _("Would you like to stop the dummy audio device?")):
            dummy_device_stop()
            if ui.show_question(
                APP_TITLE,
                _("Would you like the dummy audio device to start at login?"),
            ):
                dummy_device_enable()
            else:
                dummy_device_disable()


def _toggle_pipewire_jack(ui: AudioConfigUI) -> None:
    """Enable / disable PipeWire-JACK."""
    if not toggle_pipewire_jack():
        ui.show_error(APP_TITLE, _("An error has occurred."))


def _configure_firewire(ui: AudioConfigUI) -> None:
    """Configure or remove FireWire (FFADO) audio support in PipeWire."""

    # If already configured, offer to remove first
    if ffado_pw_configured():
        keep = ui.show_question(
            APP_TITLE,
            _("A FireWire (FFADO) PipeWire configuration already exists.\n\n"
              "Would you like to reconfigure it?\n"
              "(Choose 'No' to remove the existing configuration)"),
            ok_label=_("Reconfigure"),
            cancel_label=_("Remove"),
        )
        if not keep:
            remove_ffado_pw_config()
            ui.show_info(
                APP_TITLE,
                _("FireWire (FFADO) PipeWire configuration removed.\n"
                  "PipeWire has been restarted."),
            )
            return

    # Ensure ffado-tools is installed
    if not ffado_tools_installed():
        install = ui.show_question(
            APP_TITLE,
            _("FireWire (FFADO) audio support requires the \'ffado-tools\' "
              "package to be installed.\n\n"
              "Would you like to install it now?"),
            ok_label=_("Install"),
            cancel_label=_("Cancel"),
        )
        if not install:
            return
        ok = ui.show_progress(
            APP_TITLE,
            _("Installing ffado-tools…"),
            lambda: subprocess.run(
                ["pkexec", "apt-get", "install", "-y", "ffado-tools", "libffado2"]
            ),
        )
        if not ok:
            ui.show_error(APP_TITLE, _("Failed to install ffado-tools."))
            return

    # Enumerate connected devices
    devices = list_ffado_devices()
    if not devices:
        ui.show_warning(
            APP_TITLE,
            _("No FireWire audio devices were detected.\n\n"
              "Make sure your FireWire device is connected and powered on, "
              "and that the firewire_ohci and firewire_core kernel modules "
              "are loaded."),
        )
        return

    # Device selection (skip radio if only one device)
    if len(devices) == 1:
        device_index = 0
    else:
        device_items = [
            ChecklistItem(
                key=str(dev.index),
                label=dev.display_name,
                description=_("GUID: {guid}").format(guid=dev.guid),
                checked=(dev.index == 0),
            )
            for dev in devices
        ]
        selected_key = ui.show_radiolist(
            APP_TITLE,
            _("Select the FireWire audio device to use with PipeWire:"),
            device_items,
            ok_label=_("Next"),
            cancel_label=_("Cancel"),
        )
        if selected_key is None:
            return
        device_index = int(selected_key)

        # Quantum / sample rate
    combo_result = ui.show_combo_dialog(
        APP_TITLE,
                _("Select the Quantum and Sample Rate for the FireWire device.\n\n"
                    "Note: Many FireWire devices require a quantum of 256 or larger."),
        combos=[
            ComboItem(
                                label=_("Quantum"),
                options=[str(s) for s in VALID_BUFFER_SIZES],
                default="256",
            ),
            ComboItem(
                label=_("Sample Rate (Hz)"),
                options=[str(r) for r in VALID_SAMPLE_RATES],
                default="48000",
            ),
        ],
        ok_label=_("Apply"),
        cancel_label=_("Cancel"),
    )
    if combo_result is None:
        return

    buf, rate = int(combo_result[0]), int(combo_result[1])

    write_ffado_pw_config(buf, rate, device_index)

    ui.show_info(
        APP_TITLE,
        _("FireWire audio configured.\n\n"
          "Device:      {device}\n"
                    "Quantum: {buf}\n"
          "Sample Rate: {rate} Hz\n\n"
          "PipeWire has been restarted.").format(
            device=devices[device_index].display_name,
            buf=buf,
            rate=rate,
        ),
    )


# ======================================================================
# First-run / system check
# ======================================================================


def _check_system(ui: AudioConfigUI) -> bool:
    """Run initial system checks.  Returns False if we should exit."""
    if is_live_session():
        return True

    if not audio_limits_configured() or not user_in_audio_group():
        confirmed = ui.show_question(
            APP_TITLE,
            _("Your system and/or user configuration is not optimally "
              "configured for audio production. In order to fix this, "
              "a system restart will be required."),
            ok_label=_("Reboot"),
            cancel_label=_("Cancel"),
        )
        if not confirmed:
            return False
        touch_firstrun_marker()
        if not run_installer_fix_and_reboot():
            ui.show_error(
                APP_TITLE,
                _("An error has occurred or action was cancelled."),
            )
        return False

    if not firstrun_marker_exists():
        touch_firstrun_marker()
        return False

    return True


def _run_tabbed_control_panel(ui: AudioConfigUI) -> bool:
    """Run the one-window tabbed control panel if supported.

    Returns False when the user closes the panel without applying.
    """
    show_panel = getattr(ui, "show_tabbed_control_panel", None)
    if not callable(show_panel):
        return False

    has_pw_widget = has_plasma_pw_widget()
    has_gnome_ext = has_gnome_pw_extension()
    widget_manages_quantum = has_pw_widget or has_gnome_ext

    default_buf, default_rate = get_default_quantum()
    user_buf, user_rate = get_user_quantum()
    cfg = read_jack_config()

    jack_items = [
        ChecklistItem(
            key=param,
            label=param,
            description=JACK_PARAM_DESCRIPTIONS[param],
            checked=cfg.bool_params.get(param, False),
        )
        for param in JACK_BOOL_PARAMS
    ]
    sc_items = [
        ChecklistItem(
            key=mode,
            label=mode,
            description=SELF_CONNECT_DESCRIPTIONS[mode],
            checked=(mode == cfg.self_connect_mode),
        )
        for mode in SELF_CONNECT_MODES
    ]

    boot_params = read_boot_params()
    boot_items = [
        ChecklistItem(
            key="preempt=full",
            label="preempt=full",
            description=boot_params.PARAM_DESCRIPTIONS["preempt=full"],
            checked=boot_params.preempt_full,
        ),
        ChecklistItem(
            key="threadirqs",
            label="threadirqs",
            description=boot_params.PARAM_DESCRIPTIONS["threadirqs"],
            checked=boot_params.threadirqs,
        ),
        ChecklistItem(
            key="rcu_nocbs=all",
            label="rcu_nocbs=all",
            description=boot_params.PARAM_DESCRIPTIONS["rcu_nocbs=all"],
            checked=boot_params.rcu_nocbs_all,
        ),
        ChecklistItem(
            key="nohz_full=all",
            label="nohz_full=all",
            description=boot_params.PARAM_DESCRIPTIONS["nohz_full=all"],
            checked=boot_params.nohz_full_all,
        ),
    ]

    dummy_active_now = dummy_device_active()
    dummy_autostart_now = dummy_device_enabled()
    fw_present = firewire_controller_present() or ffado_pw_configured()

    def _apply_from_panel(result: dict[str, object]) -> None:
        # Refresh mutable system state on each Apply press.
        boot_before = read_boot_params()
        dummy_active_before = dummy_device_active()
        pw_jack_enabled_before = pipewire_jack_enabled()
        fw_configured_before = ffado_pw_configured()

        # Audio settings
        buf_i = int(result["audio_buffer"])
        rate_i = int(result["audio_rate"])
        if not validate_quantum(buf_i, rate_i):
            ui.show_error(APP_TITLE, _("Invalid quantum or sample rate selected."))
            return

        selected_jack = result["audio_jack_selected"]
        sc_mode = result["audio_self_connect_mode"]
        desired_pw_jack = result["pw_jack_enabled"]
        if desired_pw_jack:
            new_bool_params = {p: (p in selected_jack) for p in JACK_BOOL_PARAMS}
            new_cfg = JackConfig(
                buffer_size=buf_i,
                sample_rate=rate_i,
                show_monitor=new_bool_params["jack.show-monitor"],
                merge_monitor=new_bool_params["jack.merge-monitor"],
                show_midi=new_bool_params["jack.show-midi"],
                short_name=new_bool_params["jack.short-name"],
                self_connect_mode=sc_mode,
            )
            write_jack_config(new_cfg, set_quantum=not widget_manages_quantum)

        # Boot parameters
        boot_selected = result["boot_selected"]
        new_boot = BootParams(
            preempt_full="preempt=full" in boot_selected,
            threadirqs="threadirqs" in boot_selected,
            rcu_nocbs_all="rcu_nocbs=all" in boot_selected,
            nohz_full_all="nohz_full=all" in boot_selected,
        )
        if new_boot.as_dict() != boot_before.as_dict():
            def _apply_boot() -> None:
                if not write_boot_params(new_boot):
                    raise RuntimeError("boot-params-failed")

            ok = ui.show_progress(
                APP_TITLE,
                _("Applying boot parameters…"),
                _apply_boot,
            )
            if not ok:
                ui.show_error(APP_TITLE, _("An error has occurred or action was cancelled."))

        # Dummy device
        desired_dummy_active = result["dummy_active"]
        desired_dummy_autostart = result["dummy_autostart"]
        if desired_dummy_active and not dummy_active_before:
            dummy_device_start()
        elif not desired_dummy_active and dummy_active_before:
            dummy_device_stop()

        if desired_dummy_autostart:
            dummy_device_enable()
        else:
            dummy_device_disable()

        # PipeWire-JACK
        if desired_pw_jack != pw_jack_enabled_before:
            if not toggle_pipewire_jack():
                ui.show_error(APP_TITLE, _("An error has occurred."))

        # FireWire action
        fw_action = result["firewire_action"]
        if fw_action == "configure":
            _configure_firewire(ui)
        elif fw_action == "remove" and fw_configured_before:
            remove_ffado_pw_config()
            ui.show_info(
                APP_TITLE,
                _("FireWire (FFADO) PipeWire configuration removed.\n"
                  "PipeWire has been restarted."),
            )

        if widget_manages_quantum:
            ui.show_info(
                APP_TITLE,
                _("Settings applied.\n\n"
                  "Quantum and Sample Rate are managed by your\n"
                  "desktop widget / extension."),
            )
        else:
            ui.show_info(
                APP_TITLE,
                _("Settings applied.\n\n"
                  "Quantum: {buf}\n"
                  "Sample Rate: {rate} Hz").format(buf=buf_i, rate=rate_i),
            )

    show_panel(
        APP_TITLE,
                "",
        buffer_options=[str(s) for s in VALID_BUFFER_SIZES],
        sample_rate_options=[str(r) for r in VALID_SAMPLE_RATES],
        current_buffer=str(user_buf),
        current_sample_rate=str(user_rate),
                default_buffer=str(default_buf),
                default_sample_rate=str(default_rate),
        jack_items=jack_items,
        self_connect_items=sc_items,
        boot_items=boot_items,
        dummy_active=dummy_active_now,
        dummy_autostart=dummy_autostart_now,
        pw_jack_enabled=pipewire_jack_enabled(),
        fw_present=fw_present,
        fw_configured=ffado_pw_configured(),
        quantum_managed_by_widget=widget_manages_quantum,
        on_apply=_apply_from_panel,
        ok_label=_("Apply"),
        cancel_label=_("Close"),
    )
    return True


# ======================================================================
# Main menu loop
# ======================================================================


def _main_loop(ui: AudioConfigUI) -> None:
    """Run the main menu in a loop until the user closes it."""

    if callable(getattr(ui, "show_tabbed_control_panel", None)):
        _run_tabbed_control_panel(ui)
        return

    while True:
        # Detect current state
        pw_config = is_pipewire_config()

        if not pw_config:
            ui.show_warning(
                APP_TITLE,
                _("A default Ubuntu Studio audio configuration package could "
                  "not be found. An audio configuration package will now be "
                  "installed."),
            )
            install_pipewire_config()
            ui.show_info(
                APP_TITLE,
                _("You must restart your computer for these changes to take effect."),
            )
            return

        pw_jack_enabled = pipewire_jack_enabled()

        active = dummy_device_active()
        fw_configured = ffado_pw_configured()
        fw_present = firewire_controller_present() or fw_configured

        items = [
            ChecklistItem(
                key="configure",
                label=_("Audio Settings"),
                description=_("Open the audio control panel for PipeWire and JACK."),
            ),
            ChecklistItem(
                key="boot",
                label=_("Kernel Boot Parameters"),
                description=_("Tune low-latency boot options (restart required)."),
            ),
            ChecklistItem(
                key="dummy",
                label=_("Dummy Audio Device"),
                description=(
                    _("Status: Active. Manage start/stop and autostart.")
                    if active
                    else _("Status: Inactive. Start and configure autostart.")
                ),
            ),
            ChecklistItem(
                key="pwjack",
                label=_("PipeWire-JACK Integration"),
                description=(
                    _("Status: Enabled. Disable to use JACKd2/QJackCtl instead.")
                    if pw_jack_enabled
                    else _("Status: Disabled. Enable default PipeWire-JACK mode.")
                ),
            ),
        ]

        if fw_present:
            items.append(
                ChecklistItem(
                    key="firewire",
                    label=_("FireWire (FFADO) Device"),
                    description=(
                        _("Configured. Reconfigure or remove FireWire integration.")
                        if fw_configured
                        else _("Detected. Configure FireWire device integration.")
                    ),
                )
            )

        choice = ui.show_radiolist(
            APP_TITLE,
            _("Audio Control Panel\n\nSelect a section to manage:"),
            items,
            ok_label=_("Open"),
            cancel_label=_("Close"),
        )

        if choice is None:
            return  # user closed

        if choice == "configure":
            _configure_audio(ui)

        elif choice == "boot":
            _change_boot_params(ui)

        elif choice == "dummy":
            _toggle_dummy(ui)

        elif choice == "pwjack":
            _toggle_pipewire_jack(ui)
            if pipewire_jack_enabled():
                ui.show_info(APP_TITLE, _("PipeWire-JACK is now enabled."))
            else:
                ui.show_info(APP_TITLE, _("PipeWire-JACK is now disabled."))

        elif choice == "firewire":
            _configure_firewire(ui)


# ======================================================================
# CLI entry points (no GUI)
# ======================================================================


def _handle_cli(args: list[str]) -> int:
    """Handle CLI-only sub-commands that need no UI.  Returns exit code."""
    cmd = args[0] if args else ""

    if cmd == "startup":
        cli_startup()
        return 0

    if cmd == "dummystart":
        cli_dummy_start()
        return 0

    if cmd == "dummystop":
        cli_dummy_stop()
        return 0

    if cmd == "writeparams":
        if len(args) >= 2:
            cli_write_params(args[1], args[2:])
        return 0

    return -1  # not a CLI command


# ======================================================================
# Entry point
# ======================================================================


def main() -> None:
    args = sys.argv[1:]

    # CLI sub-commands run without a toolkit
    if args:
        rc = _handle_cli(args)
        if rc >= 0:
            sys.exit(rc)

    # GUI path
    ui = _create_ui()

    if not _check_system(ui):
        sys.exit(0)

    _main_loop(ui)
    ui.quit()


if __name__ == "__main__":
    main()
