#include "gsr-window.h"

#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include <glib/gi18n.h>

#include "gsr-config-page.h"
#include "gsr-config.h"
#include "gsr-hotkeys.h"
#include "gsr-info.h"
#include "gsr-record-page.h"
#include "gsr-replay-page.h"
#include "gsr-stream-page.h"

#ifdef __linux__
#include <sys/prctl.h>
#endif

struct _GsrWindow {
    AdwApplicationWindow parent_instance;

    /* Header bar */
    AdwHeaderBar       *header_bar;
    AdwViewSwitcher    *header_switcher;
    GtkStack           *header_title_stack;
    GtkLabel           *header_title_label;
    GtkMenuButton      *menu_button;

    /* View stack & bottom switcher */
    AdwViewStack       *view_stack;
    AdwViewSwitcherBar *view_switcher_bar;

    /* Toast overlay */
    AdwToastOverlay    *toast_overlay;

    /* System info (owned, lifetime = window) */
    GsrInfo             info;

    /* Config (owned, lifetime = window) */
    GsrConfig           config;

    /* Pages */
    GsrConfigPage      *config_page;
    GsrStreamPage      *stream_page;
    GsrRecordPage      *record_page;
    GsrReplayPage      *replay_page;

    /* ── Hamburger menu ─── */
    GMenu              *primary_menu;       /* top-level menu model */
    GMenu              *view_section;       /* "View" section (Simple/Advanced) */
    gboolean            view_section_visible;

    /* ── Hotkeys ─── */
    GsrHotkeys         *hotkeys;
#ifdef HAVE_WAYLAND
    gboolean            wayland_shortcuts_registered;
#endif

    /* ── Process management ─── */
    pid_t               child_pid;          /* -1 when idle */
    int                 prev_exit_status;
    GsrActiveMode       active_mode;
    char               *record_filename;    /* owned, recording only */
    guint               poll_timer_id;      /* 500ms poll timer */

    /* ── Desktop notifications ─── */
    gboolean            showing_notification;
    guint               notification_timeout_id; /* auto-withdraw timer */
    gboolean            is_kde;             /* KDE workaround */

    /* ── Startup error state ─── */
    GsrInfoExitStatus   info_status;        /* cached for deferred dialog */
    guint               startup_idle_id;    /* deferred error-check idle */
};

G_DEFINE_FINAL_TYPE(GsrWindow, gsr_window, ADW_TYPE_APPLICATION_WINDOW)

/* ── Forward declarations ────────────────────────────────────────── */

static void handle_child_death(GsrWindow *self, int exit_status);
static void send_notification(GsrWindow *self, const char *title,
                              const char *body, GNotificationPriority priority);

/* ── Mode-to-string helper ───────────────────────────────────────── */

static const char *
active_mode_to_string(GsrActiveMode mode)
{
    switch (mode) {
    case GSR_ACTIVE_MODE_STREAM: return _("streaming");
    case GSR_ACTIVE_MODE_RECORD: return _("recording");
    case GSR_ACTIVE_MODE_REPLAY: return _("replay");
    default:                     return _("unknown");
    }
}

/* ── Desktop notification helpers ────────────────────────────────── */

/**
 * Withdraw the desktop notification if its timeout has elapsed.
 * Called as a one-shot GLib timeout.
 */
static void
on_notification_withdraw(gpointer user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);

    self->notification_timeout_id = 0; /* source is being removed */

    if (self->showing_notification) {
        GtkApplication *app = GTK_APPLICATION(
            gtk_window_get_application(GTK_WINDOW(self)));
        if (app)
            g_application_withdraw_notification(G_APPLICATION(app),
                "gpu-screen-recorder");
        self->showing_notification = FALSE;
    }
}

/**
 * Send a desktop notification AND an in-app toast.
 *
 * GNotification goes to the desktop notification daemon via GApplication;
 * AdwToast shows inside our window.  The desktop notification is auto-withdrawn
 * after 3 s (normal) or 10 s (urgent).
 *
 * KDE workaround: when a capture is active, KDE suppresses normal-priority
 * notifications because the app is "screen sharing".  We force URGENT in that
 * case so the user still sees them.
 */
static void
send_notification_full(GsrWindow *self, const char *title,
                       const char *body, GNotificationPriority priority,
                       const char *open_file_path)
{
    GtkApplication *app = GTK_APPLICATION(
        gtk_window_get_application(GTK_WINDOW(self)));
    if (!app)
        return;

    /* ── Desktop notification ── */
    g_autoptr(GNotification) notif = g_notification_new(title);
    g_notification_set_body(notif, body);

    /* KDE workaround: force urgent while capturing */
    GNotificationPriority effective = priority;
    if (self->is_kde && self->child_pid > 0 &&
        effective < G_NOTIFICATION_PRIORITY_URGENT)
        effective = G_NOTIFICATION_PRIORITY_URGENT;

    g_notification_set_priority(notif, effective);

    /* Clicking the notification reveals the saved file in the file manager */
    if (open_file_path && *open_file_path)
        g_notification_set_default_action_and_target(notif,
            "app.open-folder", "s", open_file_path);

    /* Cancel any pending withdrawal timer, then send */
    if (self->showing_notification) {
        g_application_withdraw_notification(G_APPLICATION(app),
            "gpu-screen-recorder");
    }
    g_application_send_notification(G_APPLICATION(app),
        "gpu-screen-recorder", notif);
    self->showing_notification = TRUE;

    /* Auto-withdraw after timeout */
    g_clear_handle_id(&self->notification_timeout_id, g_source_remove);
    guint timeout_ms = (effective >= G_NOTIFICATION_PRIORITY_URGENT) ? 10000 : 3000;
    self->notification_timeout_id = g_timeout_add_once(timeout_ms, on_notification_withdraw, self);

    /* ── In-app toast ── */
    AdwToast *toast = adw_toast_new(body);
    adw_toast_set_timeout(toast, (effective >= G_NOTIFICATION_PRIORITY_URGENT) ? 10 : 3);
    if (open_file_path && *open_file_path) {
        adw_toast_set_button_label(toast, _("Open Folder"));
        adw_toast_set_action_name(toast, "app.open-folder");
        adw_toast_set_action_target_value(toast,
            g_variant_new_string(open_file_path));
    }
    adw_toast_overlay_add_toast(self->toast_overlay, toast);
}

static void
send_notification(GsrWindow *self, const char *title,
                  const char *body, GNotificationPriority priority)
{
    send_notification_full(self, title, body, priority, NULL);
}

/* ── Container compatibility fix ─────────────────────────────────── */

static const char *
fix_container_for_codec(const char *container, const char *codec)
{
    gboolean is_vp = g_str_equal(codec, "vp8") || g_str_equal(codec, "vp9");

    if (is_vp) {
        /* VP8/VP9 needs webm or matroska */
        if (!g_str_equal(container, "webm") && !g_str_equal(container, "matroska"))
            return "webm";
    } else {
        /* Non-VP codec in webm container → force mp4 */
        if (g_str_equal(container, "webm"))
            return "mp4";
    }
    return container;
}

/* ── Resolve codec for "auto" ────────────────────────────────────── */

static void
resolve_codec_and_encoder(GsrWindow *self, const char **out_codec,
                          gboolean *out_use_software)
{
    const char *selected = gsr_config_page_get_video_codec_id(self->config_page);
    *out_use_software = FALSE;

    if (g_str_equal(selected, "h264_software")) {
        *out_codec = "h264";
        *out_use_software = TRUE;
        return;
    }

    if (g_str_equal(selected, "auto")) {
        const char *hw = gsr_info_get_first_usable_hw_video_codec(&self->info);
        if (hw) {
            *out_codec = hw;
        } else {
            /* Fallback to h264 software */
            *out_codec = "h264";
            *out_use_software = TRUE;
        }
        return;
    }

    *out_codec = selected;
}

/* ── Build recording filename ────────────────────────────────────── */

static char *
build_record_filename(const char *dir, const char *container_display)
{
    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    if (!tm) {
        return g_strdup_printf("%s/Video.%s", dir, container_display);
    }
    char date_buf[64];
    strftime(date_buf, sizeof(date_buf), "%Y-%m-%d_%H-%M-%S", tm);
    return g_strdup_printf("%s/Video_%s.%s", dir, date_buf, container_display);
}

/* Map internal container ID to display extension for filename */
static const char *
container_id_to_extension(const char *id)
{
    if (g_str_equal(id, "matroska")) return "mkv";
    if (g_str_equal(id, "mpegts"))   return "ts";
    if (g_str_equal(id, "hls"))      return "m3u8";
    return id; /* mp4, flv, mov, webm pass through */
}

/* ── Build command-line args ─────────────────────────────────────── */

static GPtrArray *
build_command_args(GsrWindow *self, GsrActiveMode mode)
{
    GPtrArray *args = g_ptr_array_new_with_free_func(g_free);

    g_ptr_array_add(args, g_strdup("gpu-screen-recorder"));

    /* ── Record area / window ─── */
    const char *area_id = gsr_config_page_get_record_area_id(self->config_page);
    g_ptr_array_add(args, g_strdup("-w"));

    if (g_str_equal(area_id, "focused")) {
        int aw = gsr_config_page_get_area_width(self->config_page);
        int ah = gsr_config_page_get_area_height(self->config_page);
        g_ptr_array_add(args, g_strdup_printf("focused:%dx%d", aw, ah));
    } else if (g_str_equal(area_id, "portal")) {
        g_ptr_array_add(args, g_strdup("portal"));
    } else if (g_str_equal(area_id, "window")) {
        unsigned long wid = gsr_config_page_get_selected_window(self->config_page);
        if (wid == 0) {
            g_ptr_array_unref(args);
            return NULL;
        }
        g_ptr_array_add(args, g_strdup_printf("%lu", wid));
    } else {
        /* Monitor name */
        g_ptr_array_add(args, g_strdup(area_id));
    }

    /* ── Codec & encoder ─── */
    const char *codec = NULL;
    gboolean use_software = FALSE;
    resolve_codec_and_encoder(self, &codec, &use_software);

    /* ── Container (mode-specific, with compat fix) ─── */
    char *container_owned = NULL;
    const char *container = NULL;

    switch (mode) {
    case GSR_ACTIVE_MODE_STREAM:
        container_owned = gsr_stream_page_get_container(self->stream_page);
        container = container_owned;
        break;
    case GSR_ACTIVE_MODE_RECORD:
        container_owned = gsr_record_page_get_container(self->record_page);
        container = container_owned;
        break;
    case GSR_ACTIVE_MODE_REPLAY:
        container_owned = gsr_replay_page_get_container(self->replay_page);
        container = container_owned;
        break;
    default:
        container = "mp4";
        break;
    }

    container = fix_container_for_codec(container, codec);

    g_ptr_array_add(args, g_strdup("-c"));
    g_ptr_array_add(args, g_strdup(container));

    g_ptr_array_add(args, g_strdup("-k"));
    g_ptr_array_add(args, g_strdup(codec));

    /* Audio codec */
    g_ptr_array_add(args, g_strdup("-ac"));
    g_ptr_array_add(args, g_strdup(gsr_config_page_get_audio_codec_id(self->config_page)));

    /* FPS */
    g_ptr_array_add(args, g_strdup("-f"));
    g_ptr_array_add(args, g_strdup_printf("%d", gsr_config_page_get_fps(self->config_page)));

    /* Cursor */
    g_ptr_array_add(args, g_strdup("-cursor"));
    g_ptr_array_add(args, g_strdup(
        gsr_config_page_get_record_cursor(self->config_page) ? "yes" : "no"));

    /* Restore portal session */
    g_ptr_array_add(args, g_strdup("-restore-portal-session"));
    g_ptr_array_add(args, g_strdup(
        gsr_config_page_get_restore_portal_session(self->config_page) ? "yes" : "no"));

    /* Color range */
    g_ptr_array_add(args, g_strdup("-cr"));
    g_ptr_array_add(args, g_strdup(gsr_config_page_get_color_range_id(self->config_page)));

    /* Encoder */
    g_ptr_array_add(args, g_strdup("-encoder"));
    g_ptr_array_add(args, g_strdup(use_software ? "cpu" : "gpu"));

    /* ── Quality args ─── */
    const char *quality = gsr_config_page_get_quality_id(self->config_page);
    if (g_str_equal(quality, "custom")) {
        g_ptr_array_add(args, g_strdup("-bm"));
        g_ptr_array_add(args, g_strdup("cbr"));
        g_ptr_array_add(args, g_strdup("-q"));
        g_ptr_array_add(args, g_strdup_printf("%d",
            gsr_config_page_get_video_bitrate(self->config_page)));
    } else {
        g_ptr_array_add(args, g_strdup("-q"));
        g_ptr_array_add(args, g_strdup(quality));
    }

    /* ── Framerate mode ─── */
    const char *fm = gsr_config_page_get_framerate_mode_id(self->config_page);
    if (!g_str_equal(fm, "auto")) {
        g_ptr_array_add(args, g_strdup("-fm"));
        g_ptr_array_add(args, g_strdup(fm));
    }

    /* ── Resolution ─── */
    if (gsr_config_page_get_change_video_resolution(self->config_page) &&
        !g_str_equal(area_id, "focused"))
    {
        int vw = gsr_config_page_get_video_width(self->config_page);
        int vh = gsr_config_page_get_video_height(self->config_page);
        g_ptr_array_add(args, g_strdup("-s"));
        g_ptr_array_add(args, g_strdup_printf("%dx%d", vw, vh));
    }

    /* ── Overclock ─── */
    if (gsr_config_page_get_overclock(self->config_page)) {
        g_ptr_array_add(args, g_strdup("-oc"));
        g_ptr_array_add(args, g_strdup("yes"));
    }

    /* ── Audio args ─── */
    gboolean split = gsr_config_page_get_split_audio(self->config_page);
    gboolean merge = !split;
    GPtrArray *audio_tracks = gsr_config_page_build_audio_args(self->config_page, merge);
    for (guint i = 0; i < audio_tracks->len; i++) {
        g_ptr_array_add(args, g_strdup("-a"));
        g_ptr_array_add(args, g_strdup(g_ptr_array_index(audio_tracks, i)));
    }
    g_ptr_array_unref(audio_tracks);

    /* ── Mode-specific: output (-o) and extra flags ─── */
    switch (mode) {
    case GSR_ACTIVE_MODE_REPLAY: {
        int replay_time = gsr_replay_page_get_time(self->replay_page);
        g_ptr_array_add(args, g_strdup("-r"));
        g_ptr_array_add(args, g_strdup_printf("%d", replay_time));

        const char *save_dir = gsr_replay_page_get_save_dir(self->replay_page);
        g_ptr_array_add(args, g_strdup("-o"));
        g_ptr_array_add(args, g_strdup(save_dir ? save_dir : "/tmp"));
        break;
    }
    case GSR_ACTIVE_MODE_RECORD: {
        const char *save_dir = gsr_record_page_get_save_dir(self->record_page);
        const char *ext = container_id_to_extension(container);
        char *filename = build_record_filename(
            save_dir ? save_dir : "/tmp", ext);

        g_set_str(&self->record_filename, filename);

        g_ptr_array_add(args, g_strdup("-o"));
        g_ptr_array_add(args, filename); /* transfers ownership */
        break;
    }
    case GSR_ACTIVE_MODE_STREAM: {
        char *url = gsr_stream_page_get_stream_url(self->stream_page);
        g_ptr_array_add(args, g_strdup("-o"));
        g_ptr_array_add(args, url); /* transfers ownership */
        break;
    }
    default:
        break;
    }

    g_free(container_owned);

    /* NULL-terminate for execvp */
    g_ptr_array_add(args, NULL);

    return args;
}

/* ── fork/exec ───────────────────────────────────────────────────── */

static gboolean
start_child_process(GsrWindow *self, GPtrArray *args)
{
    pid_t pid = fork();
    if (pid == -1) {
        g_warning("fork() failed: %s", g_strerror(errno));
        return FALSE;
    }

    if (pid == 0) {
        /* Child process */
#ifdef __linux__
        prctl(PR_SET_PDEATHSIG, SIGTERM);
#endif
        execvp(g_ptr_array_index(args, 0), (char **)args->pdata);
        /* If execvp returns, it failed */
        _exit(127);
    }

    /* Parent */
    self->child_pid = pid;

    /* Log the command line for debugging */
    g_autofree char *cmdline = g_strjoinv(" ", (char **)args->pdata);
    g_debug("Started gpu-screen-recorder (pid=%d): %s", pid, cmdline);

    return TRUE;
}

/* ── Kill helper (matches kill_gpu_screen_recorder_get_result) ───── */

static gboolean
kill_and_wait(pid_t pid, gboolean *already_dead)
{
    if (already_dead) *already_dead = FALSE;

    if (pid <= 0) {
        if (already_dead) *already_dead = TRUE;
        return TRUE;
    }

    /* Check if already dead */
    int status = 0;
    pid_t ret = waitpid(pid, &status, WNOHANG);
    if (ret == pid) {
        /* Already exited */
        if (already_dead) *already_dead = TRUE;
        if (WIFEXITED(status))
            return WEXITSTATUS(status) == 0;
        return FALSE;
    }

    /* Still running — send SIGINT and block until exit */
    kill(pid, SIGINT);
    ret = waitpid(pid, &status, 0);
    if (ret == pid && WIFEXITED(status))
        return WEXITSTATUS(status) == 0;
    return FALSE;
}

/* ── 500ms poll timer ────────────────────────────────────────────── */

static gboolean
on_poll_timer(gpointer user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);

    if (self->child_pid <= 0)
        return G_SOURCE_CONTINUE;

    /* Check if child died */
    int status = 0;
    pid_t ret = waitpid(self->child_pid, &status, WNOHANG);
    if (ret == self->child_pid) {
        /* Child died */
        int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
        self->child_pid = -1;
        handle_child_death(self, exit_status);
    }

    return G_SOURCE_CONTINUE;
}

/* ── Handle unexpected child death ───────────────────────────────── */

static void
handle_child_death(GsrWindow *self, int exit_status)
{
    self->prev_exit_status = exit_status;
    GsrActiveMode mode = self->active_mode;

    g_debug("Child died with exit_status=%d, mode=%d", exit_status, mode);

    /* Stop the poll timer */
    g_clear_handle_id(&self->poll_timer_id, g_source_remove);

    /* Enter the "stopped" state on the appropriate page */
    switch (mode) {
    case GSR_ACTIVE_MODE_STREAM:
        gsr_stream_page_set_active(self->stream_page, FALSE);
        break;
    case GSR_ACTIVE_MODE_RECORD:
        gsr_record_page_set_active(self->record_page, FALSE);
        break;
    case GSR_ACTIVE_MODE_REPLAY:
        gsr_replay_page_set_active(self->replay_page, FALSE);
        break;
    default:
        break;
    }

    self->active_mode = GSR_ACTIVE_MODE_NONE;
    gsr_window_set_recording_active(self, FALSE);

    /* Show notification based on exit code */
    if (exit_status == 60) {
        /* Canceled by user — silent */
    } else if (exit_status == 0) {
        /* Success */
        if (mode == GSR_ACTIVE_MODE_RECORD && self->record_filename) {
            if (gsr_config_page_get_notify_saved(self->config_page)) {
                g_autofree char *msg = g_strdup_printf(_("Recording saved to %s"),
                    self->record_filename);
                send_notification_full(self, "GPU Screen Recorder", msg,
                    G_NOTIFICATION_PRIORITY_NORMAL, self->record_filename);
            }
        } else if (gsr_config_page_get_notify_stopped(self->config_page)) {
            const char *mode_str = active_mode_to_string(mode);
            g_autofree char *msg = g_strdup_printf(_("Stopped %s"), mode_str);
            send_notification(self, "GPU Screen Recorder", msg,
                G_NOTIFICATION_PRIORITY_NORMAL);
        }
    } else {
        /* Error — always notify regardless of user prefs */
        g_autofree char *msg = NULL;
        if (exit_status == 10)
            msg = g_strdup(_("You need to have pkexec installed and have "
                "a polkit agent running to record your monitor"));
        else if (exit_status == 50)
            msg = g_strdup(_("Desktop portal capture failed. Either you "
                "canceled the desktop portal or your Wayland compositor "
                "doesn't support desktop portal capture or it's incorrectly "
                "setup on your system"));
        else
            msg = g_strdup(_("Failed to save video. Either your graphics "
                "card doesn't support GPU Screen Recorder with the settings "
                "you used or you don't have enough disk space. "
                "Start GPU Screen Recorder from the terminal for more info"));
        send_notification(self, "GPU Screen Recorder", msg,
            G_NOTIFICATION_PRIORITY_URGENT);
    }
}

/* ── Actions ─────────────────────────────────────────────────────── */

static void
save_config(GsrWindow *self)
{
    /* Read current widget state into config struct */
    gsr_config_page_read_config(self->config_page, &self->config);
    gsr_stream_page_read_config(self->stream_page, &self->config);
    gsr_record_page_read_config(self->record_page, &self->config);
    gsr_replay_page_read_config(self->replay_page, &self->config);

    /* Persist view-mode */
    GAction *action = g_action_map_lookup_action(G_ACTION_MAP(self), "view-mode");
    if (action) {
        GVariant *state = g_action_get_state(action);
        const char *mode = g_variant_get_string(state, NULL);
        self->config.main_config.advanced_view = g_str_equal(mode, "advanced");
        g_variant_unref(state);
    }

    gsr_config_save(&self->config);
}

static void
on_view_mode_change(GSimpleAction *action,
                    GVariant      *parameter,
                    gpointer       user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);
    const char *mode = g_variant_get_string(parameter, NULL);
    g_simple_action_set_state(action, g_variant_new_string(mode));

    gboolean advanced = g_str_equal(mode, "advanced");
    gsr_config_page_set_advanced(self->config_page, advanced);

    /* Persist the change immediately */
    save_config(self);

    g_debug("View mode changed to: %s", mode);
}

/* ── Close request — save config while widgets are still alive ──── */

static gboolean
on_close_request(GtkWindow *window, gpointer user_data G_GNUC_UNUSED)
{
    GsrWindow *self = GSR_WINDOW(window);

    /* If child is running, kill it before exiting */
    if (self->child_pid > 0) {
        g_debug("Window closing — killing child pid %d", self->child_pid);
        kill(self->child_pid, SIGINT);
        waitpid(self->child_pid, NULL, 0);
        self->child_pid = -1;
    }

    /* Stop poll timer */
    g_clear_handle_id(&self->poll_timer_id, g_source_remove);

    /* Free hotkeys before the window is destroyed */
    g_clear_pointer(&self->hotkeys, gsr_hotkeys_free);

    /* Withdraw any pending desktop notification and cancel its timer */
    g_clear_handle_id(&self->notification_timeout_id, g_source_remove);
    if (self->showing_notification) {
        GtkApplication *app = GTK_APPLICATION(
            gtk_window_get_application(GTK_WINDOW(self)));
        if (app)
            g_application_withdraw_notification(G_APPLICATION(app),
                "gpu-screen-recorder");
        self->showing_notification = FALSE;
    }

    save_config(self);
    /* Return FALSE to let the default handler proceed */
    return FALSE;
}

/* ── Hamburger menu ──────────────────────────────────────────────── */

static void
create_primary_menu(GsrWindow *self)
{
    self->primary_menu = g_menu_new();

    /* View mode section (with "View" header label) — kept separate for
       dynamic insertion/removal based on the active tab. */
    self->view_section = g_menu_new();
    g_autoptr(GMenuItem) simple_item = g_menu_item_new(_("Simple"), NULL);
    g_menu_item_set_action_and_target_value(simple_item,
        "win.view-mode", g_variant_new_string("simple"));
    g_menu_append_item(self->view_section, simple_item);

    g_autoptr(GMenuItem) advanced_item = g_menu_item_new(_("Advanced"), NULL);
    g_menu_item_set_action_and_target_value(advanced_item,
        "win.view-mode", g_variant_new_string("advanced"));
    g_menu_append_item(self->view_section, advanced_item);

    /* Start with the View section hidden; it will be shown when the
       Config tab becomes visible. */
    self->view_section_visible = FALSE;

    /* About section (always present) */
    g_autoptr(GMenu) about_section = g_menu_new();
    g_menu_append(about_section, _("Keyboard Shortcuts"), "app.shortcuts");
    g_menu_append(about_section, _("About"), "app.about");
    g_menu_append_section(self->primary_menu, NULL,
        G_MENU_MODEL(about_section));
}

/* ── Hotkey: page changed → regrab + register Wayland shortcuts ─── */

static void
update_view_section_visibility(GsrWindow *self, gboolean show)
{
    if (show && !self->view_section_visible) {
        g_menu_insert_section(self->primary_menu, 0,
            _("View"), G_MENU_MODEL(self->view_section));
        self->view_section_visible = TRUE;
    } else if (!show && self->view_section_visible) {
        g_menu_remove(self->primary_menu, 0);
        self->view_section_visible = FALSE;
    }
}

static void
on_visible_page_changed(GObject *object G_GNUC_UNUSED,
                        GParamSpec *pspec G_GNUC_UNUSED,
                        gpointer user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);

    /* Show/hide the View section depending on the active tab */
    const char *page = adw_view_stack_get_visible_child_name(self->view_stack);
    gboolean on_config = page && g_str_equal(page, "config");
    update_view_section_visibility(self, on_config);

    /* X11: re-grab hotkeys for the now-visible page */
#ifdef HAVE_X11
    if (self->hotkeys)
        gsr_hotkeys_regrab_for_visible_page(self->hotkeys);
#endif

    /* Wayland: register shortcuts once when first visiting an action page */
#ifdef HAVE_WAYLAND
    if (self->hotkeys && !self->wayland_shortcuts_registered) {
        if (page && (!g_str_equal(page, "config"))) {
            self->wayland_shortcuts_registered = TRUE;
            gsr_hotkeys_register_wayland_shortcuts_once(self->hotkeys);
        }
    }
#endif
}

/* ── Startup error dialogs (AdwAlertDialog) ──────────────────────── */

static void
on_fatal_error_response(AdwAlertDialog *dialog G_GNUC_UNUSED,
                        const char *response G_GNUC_UNUSED,
                        gpointer user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);
    GtkApplication *app = GTK_APPLICATION(
        gtk_window_get_application(GTK_WINDOW(self)));
    if (app)
        g_application_quit(G_APPLICATION(app));
}

static void
show_fatal_error(GsrWindow *self, const char *heading, const char *body)
{
    AdwAlertDialog *dlg = ADW_ALERT_DIALOG(adw_alert_dialog_new(heading, body));
    adw_alert_dialog_add_response(dlg, "ok", _("OK"));
    adw_alert_dialog_set_default_response(dlg, "ok");
    adw_alert_dialog_set_close_response(dlg, "ok");
    adw_alert_dialog_set_body_use_markup(dlg, TRUE);
    g_signal_connect(dlg, "response",
        G_CALLBACK(on_fatal_error_response), self);
    adw_dialog_present(ADW_DIALOG(dlg), GTK_WIDGET(self));
}

static void
check_startup_errors_idle(gpointer user_data)
{
    GsrWindow *self = GSR_WINDOW(user_data);
    self->startup_idle_id = 0; /* source auto-removes after this one-shot */

    switch (self->info_status) {
    case GSR_INFO_EXIT_FAILED_TO_RUN:
        show_fatal_error(self,
            _("Failed to run gpu-screen-recorder"),
            _("Failed to run the <tt>gpu-screen-recorder</tt> command.\n\n"
            "Make sure <tt>gpu-screen-recorder</tt> is installed and "
            "accessible in your PATH."));
        return;

    case GSR_INFO_EXIT_OPENGL_FAILED:
        show_fatal_error(self,
            _("OpenGL initialization failed"),
            _("Failed to get OpenGL information.\n\n"
            "Make sure your GPU drivers are properly installed. "
            "You may need to install the Vulkan or Mesa drivers for your GPU."));
        return;

    case GSR_INFO_EXIT_NO_DRM_CARD:
        show_fatal_error(self,
            _("No DRM card found"),
            _("Failed to find a valid DRM card for your GPU.\n\n"
            "If you are running in a VM, make sure GPU passthrough is "
            "enabled and properly configured."));
        return;

    case GSR_INFO_EXIT_OK:
        break;
    }

    /* Check display server */
    if (self->info.system_info.display_server == GSR_DISPLAY_SERVER_UNKNOWN) {
        show_fatal_error(self,
            _("No display server detected"),
            _("Neither X11 nor Wayland is running.\n\n"
            "GPU Screen Recorder requires either X11 or Wayland."));
        return;
    }

    /* Check for monitors (Wayland without portal needs monitors) */
    if (self->info.supported_capture_options.n_monitors == 0 &&
        self->info.system_info.display_server == GSR_DISPLAY_SERVER_WAYLAND &&
        !self->info.supported_capture_options.portal)
    {
        show_fatal_error(self,
            _("No monitors found"),
            _("No monitors to record were found.\n\n"
            "Make sure GPU Screen Recorder is running on the same GPU "
            "that your monitors are connected to. You can use the "
            "<tt>DRI_PRIME</tt> environment variable to choose a GPU."));
        return;
    }
}

/* ── GObject boilerplate ─────────────────────────────────────────── */

static void
gsr_window_init(GsrWindow *self)
{
    /* ── Init process state ─── */
    self->child_pid = -1;
    self->prev_exit_status = 0;
    self->active_mode = GSR_ACTIVE_MODE_NONE;
    self->record_filename = NULL;
    self->poll_timer_id = 0;

    /* ── Init notification state ─── */
    self->showing_notification = FALSE;
    const char *desktop = g_getenv("XDG_CURRENT_DESKTOP");
    self->is_kde = desktop && (g_strstr_len(desktop, -1, "KDE") != NULL);

    /* ── Window properties ─── */
    gtk_window_set_title(GTK_WINDOW(self), _("GPU Screen Recorder"));
    gtk_window_set_default_size(GTK_WINDOW(self), 810, 685);

    /* ── Load system info ─── */
    GsrInfoExitStatus info_status = gsr_info_load(&self->info);
    if (info_status != GSR_INFO_EXIT_OK)
        g_warning("gsr_info_load returned status %d", info_status);

    /* Store for deferred error dialog */
    self->info_status = info_status;

    /* ── Load config ─── */
    gsr_config_init_defaults(&self->config);
    gsr_config_read(&self->config);

    /* ── View stack ─── */
    self->view_stack = ADW_VIEW_STACK(adw_view_stack_new());

    self->config_page = gsr_config_page_new(&self->info);
    self->stream_page = gsr_stream_page_new(&self->info);
    self->record_page = gsr_record_page_new(&self->info);
    self->replay_page = gsr_replay_page_new(&self->info);

    adw_view_stack_add_titled_with_icon(self->view_stack,
        GTK_WIDGET(self->config_page), "config", _("Config"), "preferences-system-symbolic");
    adw_view_stack_add_titled_with_icon(self->view_stack,
        GTK_WIDGET(self->stream_page), "stream", _("Stream"), "network-transmit-symbolic");
    adw_view_stack_add_titled_with_icon(self->view_stack,
        GTK_WIDGET(self->record_page), "record", _("Record"), "media-record-symbolic");
    adw_view_stack_add_titled_with_icon(self->view_stack,
        GTK_WIDGET(self->replay_page), "replay", _("Replay"), "media-playlist-repeat-symbolic");

    /* ── Header bar with view switcher / title stack ─── */
    self->header_switcher = ADW_VIEW_SWITCHER(adw_view_switcher_new());
    adw_view_switcher_set_stack(self->header_switcher, self->view_stack);
    adw_view_switcher_set_policy(self->header_switcher, ADW_VIEW_SWITCHER_POLICY_WIDE);

    self->header_title_label = GTK_LABEL(gtk_label_new(_("GPU Screen Recorder")));
    gtk_widget_add_css_class(GTK_WIDGET(self->header_title_label), "title");

    /* Stack to hold both the switcher (wide) and title label (narrow) */
    self->header_title_stack = GTK_STACK(gtk_stack_new());
    gtk_stack_set_transition_type(self->header_title_stack, GTK_STACK_TRANSITION_TYPE_CROSSFADE);
    gtk_stack_add_named(self->header_title_stack,
        GTK_WIDGET(self->header_switcher), "switcher");
    gtk_stack_add_named(self->header_title_stack,
        GTK_WIDGET(self->header_title_label), "title");

    self->header_bar = ADW_HEADER_BAR(adw_header_bar_new());
    adw_header_bar_set_centering_policy(self->header_bar,
        ADW_CENTERING_POLICY_STRICT);
    adw_header_bar_set_title_widget(self->header_bar,
        GTK_WIDGET(self->header_title_stack));

    /* Hamburger menu button */
    self->menu_button = GTK_MENU_BUTTON(gtk_menu_button_new());
    gtk_menu_button_set_icon_name(self->menu_button, "open-menu-symbolic");
    create_primary_menu(self);
    gtk_menu_button_set_menu_model(self->menu_button,
        G_MENU_MODEL(self->primary_menu));
    /* Config is the default visible page, so show the View section */
    update_view_section_visibility(self, TRUE);
    adw_header_bar_pack_end(self->header_bar, GTK_WIDGET(self->menu_button));

    /* ── Bottom view switcher bar (narrow mode fallback) ─── */
    self->view_switcher_bar = ADW_VIEW_SWITCHER_BAR(adw_view_switcher_bar_new());
    adw_view_switcher_bar_set_stack(self->view_switcher_bar, self->view_stack);

    /* ── Toast overlay wrapping the view stack ─── */
    self->toast_overlay = ADW_TOAST_OVERLAY(adw_toast_overlay_new());
    adw_toast_overlay_set_child(self->toast_overlay, GTK_WIDGET(self->view_stack));

    /* ── Layout: toolbar-view wrapping the toast overlay ─── */
    AdwToolbarView *toolbar_view = ADW_TOOLBAR_VIEW(adw_toolbar_view_new());
    adw_toolbar_view_add_top_bar(toolbar_view, GTK_WIDGET(self->header_bar));
    adw_toolbar_view_set_content(toolbar_view, GTK_WIDGET(self->toast_overlay));
    adw_toolbar_view_add_bottom_bar(toolbar_view, GTK_WIDGET(self->view_switcher_bar));

    adw_application_window_set_content(ADW_APPLICATION_WINDOW(self),
        GTK_WIDGET(toolbar_view));

    /* ── Breakpoint: narrow window → show title label in header, show bottom bar ─── */
    AdwBreakpoint *bp = adw_breakpoint_new(
        adw_breakpoint_condition_parse("max-width: 550sp"));
    adw_breakpoint_add_setters(bp,
        G_OBJECT(self->header_title_stack), "visible-child-name", "title",
        G_OBJECT(self->view_switcher_bar), "reveal", TRUE,
        NULL);
    adw_application_window_add_breakpoint(ADW_APPLICATION_WINDOW(self), bp);

    /* ── Save config on close ─── */
    g_signal_connect(self, "close-request",
        G_CALLBACK(on_close_request), NULL);

    /* ── Window actions ─── */
    const char *initial_mode = self->config.main_config.advanced_view
        ? "'advanced'" : "'simple'";
    GActionEntry win_actions[] = {
        { .name = "view-mode", .activate = on_view_mode_change,
          .parameter_type = "s", .state = initial_mode },
    };
    g_action_map_add_action_entries(G_ACTION_MAP(self),
        win_actions, G_N_ELEMENTS(win_actions), self);

    /* ── Apply config to all pages ─── */
    gsr_config_page_apply_config(self->config_page, &self->config);
    gsr_stream_page_apply_config(self->stream_page, &self->config);
    gsr_record_page_apply_config(self->record_page, &self->config);
    gsr_replay_page_apply_config(self->replay_page, &self->config);

    /* Apply advanced view mode */
    gsr_config_page_set_advanced(self->config_page,
        self->config.main_config.advanced_view);

    /* ── Hotkeys ─── */
    self->hotkeys = gsr_hotkeys_new(self->info.system_info.display_server, self);
#ifdef HAVE_WAYLAND
    self->wayland_shortcuts_registered = FALSE;
#endif

    /* Re-grab hotkeys whenever the visible page changes */
    g_signal_connect(self->view_stack, "notify::visible-child-name",
        G_CALLBACK(on_visible_page_changed), self);

    /* Initial grab for the default visible page */
#ifdef HAVE_X11
    if (self->hotkeys)
        gsr_hotkeys_regrab_for_visible_page(self->hotkeys);
#endif

    /* ── Deferred startup error check ─── */
    self->startup_idle_id = g_idle_add_once(check_startup_errors_idle, self);
}

static void
gsr_window_finalize(GObject *object)
{
    GsrWindow *self = GSR_WINDOW(object);

    g_clear_handle_id(&self->startup_idle_id, g_source_remove);

    g_clear_handle_id(&self->poll_timer_id, g_source_remove);

    g_clear_handle_id(&self->notification_timeout_id, g_source_remove);

    g_clear_pointer(&self->hotkeys, gsr_hotkeys_free);

    g_free(self->record_filename);
    g_clear_object(&self->primary_menu);
    g_clear_object(&self->view_section);
    gsr_config_clear(&self->config);
    gsr_info_clear(&self->info);
    G_OBJECT_CLASS(gsr_window_parent_class)->finalize(object);
}

static void
gsr_window_class_init(GsrWindowClass *klass)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    obj_class->finalize = gsr_window_finalize;
}

/* ── Public API ──────────────────────────────────────────────────── */

GsrWindow *
gsr_window_new(AdwApplication *app)
{
    return g_object_new(GSR_TYPE_WINDOW,
                        "application", app,
                        NULL);
}

void
gsr_window_set_recording_active(GsrWindow *self, gboolean active)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    gtk_widget_set_sensitive(GTK_WIDGET(self->header_switcher), !active);
    gtk_widget_set_sensitive(GTK_WIDGET(self->view_switcher_bar), !active);
}

gboolean
gsr_window_start_process(GsrWindow *self, GsrActiveMode mode)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), FALSE);
    g_return_val_if_fail(self->child_pid <= 0, FALSE);

    /* Validate window selection if in "window" mode */
    if (!gsr_config_page_has_valid_window_selection(self->config_page)) {
        send_notification(self, "GPU Screen Recorder",
            _("No window selected! Please select a window first."),
            G_NOTIFICATION_PRIORITY_URGENT);
        return FALSE;
    }

    /* Build command line */
    GPtrArray *args = build_command_args(self, mode);
    if (!args) {
        send_notification(self, "GPU Screen Recorder",
            _("Failed to build command (no window selected)"),
            G_NOTIFICATION_PRIORITY_URGENT);
        return FALSE;
    }

    /* Fork/exec */
    gboolean ok = start_child_process(self, args);
    g_ptr_array_unref(args);

    if (!ok) {
        const char *mode_str = active_mode_to_string(mode);
        g_autofree char *msg = g_strdup_printf(_("Failed to start %s (failed to fork)"), mode_str);
        send_notification(self, "GPU Screen Recorder", msg,
            G_NOTIFICATION_PRIORITY_URGENT);
        return FALSE;
    }

    self->active_mode = mode;

    /* Start 500ms poll timer */
    if (self->poll_timer_id == 0)
        self->poll_timer_id = g_timeout_add(500, on_poll_timer, self);

    /* Show "started" notification */
    if (gsr_config_page_get_notify_started(self->config_page)) {
        const char *mode_str = active_mode_to_string(mode);
        g_autofree char *msg = g_strdup_printf(_("Started %s"), mode_str);
        send_notification(self, "GPU Screen Recorder", msg,
            G_NOTIFICATION_PRIORITY_NORMAL);
    }

    return TRUE;
}

gboolean
gsr_window_stop_process(GsrWindow *self, gboolean *already_dead)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), FALSE);

    if (self->child_pid <= 0) {
        if (already_dead) *already_dead = TRUE;
        return TRUE;
    }

    pid_t pid = self->child_pid;
    self->child_pid = -1;
    GsrActiveMode mode = self->active_mode;

    gboolean success = kill_and_wait(pid, already_dead);

    /* Stop poll timer */
    g_clear_handle_id(&self->poll_timer_id, g_source_remove);

    self->active_mode = GSR_ACTIVE_MODE_NONE;

    /* Show stop notification (respecting user prefs) */
    if (success && mode == GSR_ACTIVE_MODE_RECORD && self->record_filename) {
        if (gsr_config_page_get_notify_saved(self->config_page)) {
            g_autofree char *msg = g_strdup_printf(_("Recording saved to %s"),
                self->record_filename);
            send_notification_full(self, "GPU Screen Recorder", msg,
                G_NOTIFICATION_PRIORITY_NORMAL, self->record_filename);
        }
    } else if (gsr_config_page_get_notify_stopped(self->config_page)) {
        const char *mode_str = active_mode_to_string(mode);
        g_autofree char *msg = g_strdup_printf(_("Stopped %s"), mode_str);
        send_notification(self, "GPU Screen Recorder", msg,
            G_NOTIFICATION_PRIORITY_NORMAL);
    }

    return success;
}

void
gsr_window_send_signal(GsrWindow *self, int sig)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    if (self->child_pid > 0)
        kill(self->child_pid, sig);
}

void
gsr_window_notify_replay_saved(GsrWindow *self)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    if (gsr_config_page_get_notify_saved(self->config_page))
        send_notification(self, "GPU Screen Recorder", _("Saved replay"),
            G_NOTIFICATION_PRIORITY_NORMAL);
}

void
gsr_window_show_toast(GsrWindow *self, const char *message)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    AdwToast *toast = adw_toast_new(message);
    adw_toast_set_timeout(toast, 3);
    adw_toast_overlay_add_toast(self->toast_overlay, toast);
}

gboolean
gsr_window_is_process_running(GsrWindow *self)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), FALSE);
    return self->child_pid > 0;
}

GsrActiveMode
gsr_window_get_active_mode(GsrWindow *self)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), GSR_ACTIVE_MODE_NONE);
    return self->active_mode;
}

/* ── Hotkey dispatch (called from gsr-hotkeys) ───────────────────── */

const char *
gsr_window_get_visible_page_name(GsrWindow *self)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), NULL);
    return adw_view_stack_get_visible_child_name(self->view_stack);
}

void
gsr_window_hotkey_start_stop(GsrWindow *self)
{
    g_return_if_fail(GSR_IS_WINDOW(self));

    const char *page = adw_view_stack_get_visible_child_name(self->view_stack);
    if (!page)
        return;

    if (g_str_equal(page, "stream"))
        gsr_stream_page_activate_start_stop(self->stream_page);
    else if (g_str_equal(page, "record"))
        gsr_record_page_activate_start_stop(self->record_page);
    else if (g_str_equal(page, "replay"))
        gsr_replay_page_activate_start_stop(self->replay_page);
}

void
gsr_window_hotkey_pause_unpause(GsrWindow *self)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    gsr_record_page_activate_pause(self->record_page);
}

void
gsr_window_hotkey_save_replay(GsrWindow *self)
{
    g_return_if_fail(GSR_IS_WINDOW(self));
    gsr_replay_page_activate_save(self->replay_page);
}

#ifdef HAVE_WAYLAND
void
gsr_window_on_wayland_hotkeys_init(GsrWindow *self, gboolean success)
{
    g_return_if_fail(GSR_IS_WINDOW(self));

    gsr_stream_page_set_wayland_hotkeys_supported(self->stream_page, success);
    gsr_record_page_set_wayland_hotkeys_supported(self->record_page, success);
    gsr_replay_page_set_wayland_hotkeys_supported(self->replay_page, success);
}
#endif /* HAVE_WAYLAND */

void
gsr_window_on_hotkey_changed(GsrWindow *self)
{
    g_return_if_fail(GSR_IS_WINDOW(self));

    /* Save config so the new hotkey bindings are persisted */
    save_config(self);

    /* Re-grab X11 hotkeys with the updated bindings */
#ifdef HAVE_X11
    if (self->hotkeys)
        gsr_hotkeys_regrab_for_visible_page(self->hotkeys);
#endif
}

const GsrConfig *
gsr_window_get_config(GsrWindow *self)
{
    g_return_val_if_fail(GSR_IS_WINDOW(self), NULL);
    return &self->config;
}
