/* preferences.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;

use adw::prelude::*;
use gettextrs::gettext;
use relm4::gtk::{self, gio};

use crate::git::{TagSortDirection, TagSortMethod};

pub fn app_settings() -> gio::Settings {
    gio::Settings::new("de.wwwtech.gitte")
}

pub fn git_command() -> Vec<String> {
    let raw = app_settings().string("git-command");
    let trimmed = raw.trim();

    if trimmed.is_empty() {
        return vec!["git".to_string()];
    }

    shell_words::split(trimmed).unwrap_or_else(|_| vec![trimmed.to_string()])
}

pub fn bypass_local_git_config() -> bool {
    let settings = app_settings();
    if let Some(v) = settings.user_value("bypass-local-git-config") {
        return v.get::<bool>().unwrap_or(false);
    }
    crate::portal::is_flatpak()
}

pub fn repo_settings(repo_id: &str) -> gio::Settings {
    let path = format!("/de/wwwtech/gitte/repo-state/{repo_id}/");
    gio::Settings::with_path("de.wwwtech.gitte.repo-state", &path)
}

pub fn persist_paned_position(paned: &gtk::Paned, settings_key: &str) {
    let settings = app_settings();
    paned.set_position(settings.int(settings_key));

    let key = settings_key.to_string();
    paned.connect_position_notify(move |p| {
        let _ = app_settings().set_int(&key, p.position());
    });
}

pub fn show_repo_settings_dialog(root: &impl IsA<gtk::Widget>, repo_id: &str, repo_path: &Path) {
    let settings = repo_settings(repo_id);

    let (git_identity_group, ssh_keys_group) = if let Ok(repo) = git2::Repository::open(repo_path) {
        let identity_group = adw::PreferencesGroup::builder()
            .title(gettext("Git Identity"))
            .description(gettext("Override the global git user configuration for this repository"))
            .build();

        let repo = Rc::new(repo);
        build_git_identity_rows(Rc::clone(&repo), &identity_group);

        let ssh_group = build_ssh_key_group(Rc::clone(&repo), repo_path);

        (Some(identity_group), ssh_group)
    } else {
        tracing::warn!("Failed to open repository at {:?} for identity settings", repo_path);

        (None, None)
    };

    let mode_model = gtk::StringList::new(&[&gettext("Global Default"), &gettext("Enabled"), &gettext("Disabled")]);
    let current_mode = settings.string("auto-fetch-mode");
    let mode_index = match current_mode.as_str() {
        "enabled" => 1,
        "disabled" => 2,
        _ => 0, // "global-default"
    };

    let interval_seconds = settings.int("auto-fetch-interval");
    let interval_adjustment = gtk::Adjustment::new((interval_seconds / 60) as f64, 1.0, 60.0, 1.0, 5.0, 0.0);

    let method_names: Vec<String> = TagSortMethod::ALL
        .iter()
        .map(|m| tag_sort_method_display_name(*m))
        .collect();
    let method_refs: Vec<&str> = method_names.iter().map(|s| s.as_str()).collect();
    let method_model = gtk::StringList::new(&method_refs);
    let current_method: TagSortMethod = settings.string("tag-sort-method").parse().unwrap_or_default();
    let method_index = TagSortMethod::ALL
        .iter()
        .position(|m| *m == current_method)
        .unwrap_or(0);

    let direction_names: Vec<String> = TagSortDirection::ALL
        .iter()
        .map(|d| tag_sort_direction_display_name(*d))
        .collect();
    let direction_refs: Vec<&str> = direction_names.iter().map(|s| s.as_str()).collect();
    let direction_model = gtk::StringList::new(&direction_refs);
    let current_direction: TagSortDirection = settings.string("tag-sort-direction").parse().unwrap_or_default();
    let direction_index = TagSortDirection::ALL
        .iter()
        .position(|d| *d == current_direction)
        .unwrap_or(0);

    let line_length = settings.int("commit-line-length");
    let ll_adjustment = gtk::Adjustment::new(line_length as f64, 0.0, 999.0, 1.0, 10.0, 0.0);

    relm4::view! {
        #[name = "dialog"]
        adw::PreferencesDialog {
            set_title: &gettext("Repository Settings"),

            #[name = "page"]
            add = &adw::PreferencesPage {
                set_title: &gettext("General"),
                set_icon_name: Some("preferences-system-symbolic"),

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Commit Messages"),

                    #[name = "ll_row"]
                    adw::SpinRow {
                        set_title: &gettext("Max Line Length"),
                        set_subtitle: &gettext("0 means unlimited"),
                        set_adjustment: Some(&ll_adjustment),
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Auto-Fetch"),

                    #[name = "mode_row"]
                    adw::ComboRow {
                        set_title: &gettext("Auto-Fetch Mode"),
                        set_subtitle: &gettext("Use global default, or override per repository"),
                        set_model: Some(&mode_model),
                        set_selected: mode_index as u32,
                    },

                    #[name = "interval_row"]
                    adw::SpinRow {
                        set_title: &gettext("Fetch Interval (minutes)"),
                        set_adjustment: Some(&interval_adjustment),
                        set_sensitive: current_mode.as_str() == "enabled",
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Tag Sorting"),

                    #[name = "method_row"]
                    adw::ComboRow {
                        set_title: &gettext("Sort Method"),
                        set_model: Some(&method_model),
                        set_selected: method_index as u32,
                    },

                    #[name = "direction_row"]
                    adw::ComboRow {
                        set_title: &gettext("Sort Direction"),
                        set_model: Some(&direction_model),
                        set_selected: direction_index as u32,
                    },
                },
            },
        }
    }

    if let Some(ref group) = git_identity_group {
        page.add(group);
    }

    if let Some(ref group) = ssh_keys_group {
        page.add(group);
    }

    let s = settings.clone();
    ll_row.connect_value_notify(move |row| {
        let _ = s.set_int("commit-line-length", row.value() as i32);
    });

    let s = settings.clone();
    let interval_ref = interval_row.clone();
    mode_row.connect_selected_notify(move |row| {
        let mode = match row.selected() {
            1 => "enabled",
            2 => "disabled",
            _ => "global-default",
        };
        let _ = s.set_string("auto-fetch-mode", mode);
        interval_ref.set_sensitive(mode == "enabled");
    });

    let s = settings.clone();
    interval_row.connect_value_notify(move |row| {
        let minutes = row.value() as i32;
        let _ = s.set_int("auto-fetch-interval", minutes * 60);
    });

    let s = settings.clone();
    method_row.connect_selected_notify(move |row| {
        if let Some(method) = TagSortMethod::ALL.get(row.selected() as usize) {
            let _ = s.set_string("tag-sort-method", &method.to_string());
        }
    });

    let s = settings.clone();
    direction_row.connect_selected_notify(move |row| {
        if let Some(direction) = TagSortDirection::ALL.get(row.selected() as usize) {
            let _ = s.set_string("tag-sort-direction", &direction.to_string());
        }
    });

    let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
    dialog.present(window.as_ref());
}

fn build_git_identity_rows(repo: Rc<git2::Repository>, group: &adw::PreferencesGroup) {
    let Ok(config) = repo.config() else {
        return;
    };

    let local_config = config.open_level(git2::ConfigLevel::Local).ok();
    let local_config = Rc::new(RefCell::new(local_config));

    let items: [(&str, String, String); 2] = [
        ("user.name", gettext("Name"), gettext("Override user.name")),
        ("user.email", gettext("Email"), gettext("Override user.email")),
    ];

    for (key, label, switch_label) in items {
        let local_value = local_config.borrow().as_ref().and_then(|c| c.get_string(key).ok());
        let has_local = local_value.is_some();

        let global_value = config
            .open_level(git2::ConfigLevel::Global)
            .ok()
            .and_then(|c| c.get_string(key).ok())
            .or_else(|| {
                config
                    .open_level(git2::ConfigLevel::System)
                    .ok()
                    .and_then(|c| c.get_string(key).ok())
            });

        let subtitle = match &global_value {
            Some(val) => format!("{}: {val}", gettext("Global")),
            None => gettext("Not configured globally"),
        };

        let entry_row = adw::EntryRow::builder()
            .title(&label)
            .text(local_value.as_deref().unwrap_or(""))
            .sensitive(has_local)
            .build();

        let switch_row = adw::SwitchRow::builder()
            .title(&switch_label)
            .subtitle(&subtitle)
            .active(has_local)
            .build();

        let entry_ref = entry_row.clone();
        let key_owned = key.to_string();

        let cfg_ref = Rc::clone(&local_config);
        let k = key_owned.clone();
        switch_row.connect_active_notify(move |row| {
            if !row.is_active() {
                row.grab_focus();
            }

            entry_ref.set_sensitive(row.is_active());

            if let Some(ref mut local) = *cfg_ref.borrow_mut() {
                if row.is_active() {
                    let text = entry_ref.text();

                    if let Err(e) = local.set_str(&k, text.as_str()) {
                        tracing::warn!("Failed to set {k}: {e}");
                    }
                } else if let Err(e) = local.remove(&k) {
                    tracing::warn!("Failed to remove {k}: {e}");
                }
            }
        });

        let cfg_ref = Rc::clone(&local_config);
        let k = key_owned.clone();
        let save_to_config = move |row: &adw::EntryRow| {
            if !row.is_sensitive() {
                return;
            }

            if let Some(ref mut local) = *cfg_ref.borrow_mut() {
                let text = row.text();

                if let Err(e) = local.set_str(&k, text.as_str()) {
                    tracing::warn!("Failed to set {k}: {e}");
                }
            }
        };

        let save_clone = save_to_config.clone();
        entry_row.connect_activate(move |row| {
            save_clone(row);
        });

        let focus_controller = gtk::EventControllerFocus::new();
        let entry_ref = entry_row.clone();
        focus_controller.connect_leave(move |_| {
            save_to_config(&entry_ref);
        });
        entry_row.add_controller(focus_controller);

        group.add(&switch_row);
        group.add(&entry_row);
    }
}

fn build_ssh_key_group(repo: Rc<git2::Repository>, repo_path: &Path) -> Option<adw::PreferencesGroup> {
    let ssh_remotes = crate::git::list_ssh_remotes(repo_path).ok()?;
    if ssh_remotes.is_empty() {
        return None;
    }

    let group = adw::PreferencesGroup::builder()
        .title(gettext("SSH Keys"))
        .description(gettext("Configure which SSH private key to use for each remote"))
        .build();

    let config = repo.config().ok()?;
    let local_config = config.open_level(git2::ConfigLevel::Local).ok();
    let local_config = Rc::new(RefCell::new(local_config));

    for (remote_name, remote_url) in ssh_remotes {
        let config_key = format!("gitte.ssh-key.{remote_name}");
        let current_value = config.get_string(&config_key).ok().unwrap_or_default();

        let action_row = adw::ActionRow::builder()
            .title(&remote_name)
            .subtitle(&remote_url)
            .activatable(false)
            .build();

        let entry_row = adw::EntryRow::builder()
            .title(gettext("SSH Key"))
            .text(&current_value)
            .build();

        let browse_button = gtk::Button::builder()
            .icon_name("folder-open-symbolic")
            .valign(gtk::Align::Center)
            .tooltip_text(gettext("Browse…"))
            .css_classes(["flat"])
            .build();

        let clear_button = gtk::Button::builder()
            .icon_name("edit-clear-symbolic")
            .valign(gtk::Align::Center)
            .tooltip_text(gettext("Clear"))
            .css_classes(["flat"])
            .build();

        entry_row.add_suffix(&browse_button);
        entry_row.add_suffix(&clear_button);

        let entry_ref = entry_row.clone();
        let cfg_ref = Rc::clone(&local_config);
        let key = config_key.clone();
        browse_button.connect_clicked(move |btn| {
            let dialog = gtk::FileDialog::new();
            dialog.set_title(&gettext("Select SSH Private Key"));

            if let Some(ssh_dir) = dirs::home_dir().map(|h| h.join(".ssh"))
                && ssh_dir.exists()
            {
                dialog.set_initial_folder(Some(&gio::File::for_path(&ssh_dir)));
            }

            let entry = entry_ref.clone();
            let cfg = Rc::clone(&cfg_ref);
            let k = key.clone();
            let window = btn.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
            dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result| {
                if let Ok(file) = result
                    && let Some(path) = file.path()
                {
                    let path_str = path.to_string_lossy();
                    entry.set_text(&path_str);

                    if let Some(ref mut local) = *cfg.borrow_mut()
                        && let Err(e) = local.set_str(&k, &path_str)
                    {
                        tracing::warn!("Failed to set {k}: {e}");
                    }
                }
            });
        });

        // Clear button: remove config entry
        let entry_ref = entry_row.clone();
        let cfg_ref = Rc::clone(&local_config);
        let key = config_key.clone();
        clear_button.connect_clicked(move |_| {
            entry_ref.set_text("");
            if let Some(ref mut local) = *cfg_ref.borrow_mut()
                && let Err(e) = local.remove(&key)
            {
                tracing::debug!("Failed to remove {key}: {e}");
            }
        });

        // Save on focus leave
        let cfg_ref = Rc::clone(&local_config);
        let key = config_key.clone();
        let save_to_config = move |row: &adw::EntryRow| {
            if let Some(ref mut local) = *cfg_ref.borrow_mut() {
                let text = row.text();
                if text.is_empty() {
                    let _ = local.remove(&key);
                } else if let Err(e) = local.set_str(&key, text.as_str()) {
                    tracing::warn!("Failed to set {key}: {e}");
                }
            }
        };

        let save_clone = save_to_config.clone();
        entry_row.connect_activate(move |row| {
            save_clone(row);
        });

        let focus_controller = gtk::EventControllerFocus::new();
        let entry_ref = entry_row.clone();
        focus_controller.connect_leave(move |_| {
            save_to_config(&entry_ref);
        });
        entry_row.add_controller(focus_controller);

        group.add(&action_row);
        group.add(&entry_row);
    }

    Some(group)
}

fn tag_sort_method_display_name(method: TagSortMethod) -> String {
    match method {
        TagSortMethod::Semver => gettext("Semantic Version"),
        TagSortMethod::Alphabetical => gettext("Alphabetical"),
        TagSortMethod::Date => gettext("Date"),
    }
}

fn tag_sort_direction_display_name(direction: TagSortDirection) -> String {
    match direction {
        TagSortDirection::Desc => gettext("Descending"),
        TagSortDirection::Asc => gettext("Ascending"),
    }
}

pub fn show_preferences_dialog(root: &gtk::Window) {
    let settings = app_settings();

    let interval_seconds = settings.int("auto-fetch-interval");
    let interval_adjustment = gtk::Adjustment::new((interval_seconds / 60) as f64, 1.0, 60.0, 1.0, 5.0, 0.0);

    let layout_model = gtk::StringList::new(&[&gettext("Unified"), &gettext("Split Staged/Unstaged")]);
    let layout_index = match settings.string("working-copy-view").as_str() {
        "split" => 1,
        _ => 0,
    };

    let subtitle_for = |value: &str| -> String {
        if value.trim().is_empty() {
            gettext("System monospace default")
        } else {
            value.to_string()
        }
    };

    let initial_font_subtitle = subtitle_for(&settings.string("diff-font"));

    relm4::view! {
        #[name = "dialog"]
        adw::PreferencesDialog {
            set_title: &gettext("Preferences"),

            add = &adw::PreferencesPage {
                set_title: &gettext("General"),
                set_icon_name: Some("preferences-system-symbolic"),

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Auto-Fetch"),
                    set_description: Some(&gettext("Default settings for automatic fetching. Repositories can override these.")),

                    #[name = "enabled_row"]
                    adw::SwitchRow {
                        set_title: &gettext("Auto-Fetch Enabled"),
                        set_subtitle: &gettext("Automatically fetch from all remotes at a regular interval"),
                        set_active: settings.boolean("auto-fetch-enabled"),
                    },

                    #[name = "interval_row"]
                    adw::SpinRow {
                        set_title: &gettext("Fetch Interval (minutes)"),
                        set_adjustment: Some(&interval_adjustment),
                        set_sensitive: settings.boolean("auto-fetch-enabled"),
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Network"),

                    #[name = "bypass_config_row"]
                    adw::SwitchRow {
                        set_title: &gettext("Bypass local git config"),
                        set_subtitle: &gettext("Ignore credential helpers, hooks, and pager when fetching, pushing, or pulling. Recommended in Flatpak."),
                        set_active: bypass_local_git_config(),
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Working Copy"),

                    #[name = "layout_row"]
                    adw::ComboRow {
                        set_title: &gettext("Layout"),
                        set_subtitle: &gettext("Show staged and unstaged changes in one or two sections"),
                        set_model: Some(&layout_model),
                        set_selected: layout_index as u32,
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Appearance"),

                    #[name = "compact_row"]
                    adw::SwitchRow {
                        set_title: &gettext("Compact Mode"),
                        set_subtitle: &gettext("Reduce spacing and padding throughout the UI"),
                        set_active: settings.boolean("compact-mode"),
                    },
                },

                add = &adw::PreferencesGroup {
                    set_title: &gettext("Diff View"),

                    #[name = "font_row"]
                    adw::ActionRow {
                        set_title: &gettext("Font"),
                        set_subtitle: &initial_font_subtitle,
                        set_activatable: true,
                    },
                },
            },
        }
    }

    relm4::view! {
        #[name = "reset_button"]
        gtk::Button {
            set_icon_name: "edit-clear-symbolic",
            set_valign: gtk::Align::Center,
            set_tooltip_text: Some(&gettext("Reset to default")),
            add_css_class: "flat",
        }
    }

    font_row.add_suffix(&reset_button);

    let s = settings.clone();
    let interval_ref = interval_row.clone();
    enabled_row.connect_active_notify(move |row| {
        let _ = s.set_boolean("auto-fetch-enabled", row.is_active());
        interval_ref.set_sensitive(row.is_active());
    });

    let s = settings.clone();
    interval_row.connect_value_notify(move |row| {
        let minutes = row.value() as i32;
        let _ = s.set_int("auto-fetch-interval", minutes * 60);
    });

    let s = settings.clone();
    compact_row.connect_active_notify(move |row| {
        let _ = s.set_boolean("compact-mode", row.is_active());
    });

    let s = settings.clone();
    bypass_config_row.connect_active_notify(move |row| {
        let _ = s.set_boolean("bypass-local-git-config", row.is_active());
    });

    let s = settings.clone();
    layout_row.connect_selected_notify(move |row| {
        let value = match row.selected() {
            1 => "split",
            _ => "unified",
        };

        let _ = s.set_string("working-copy-view", value);
    });

    let row_ref = font_row.clone();
    settings.connect_changed(Some("diff-font"), move |s, _| {
        let value = s.string("diff-font");

        let subtitle = if value.trim().is_empty() {
            gettext("System monospace default")
        } else {
            value.to_string()
        };

        row_ref.set_subtitle(&subtitle);
    });

    let s = settings.clone();
    let root_weak = root.downgrade();
    font_row.connect_activated(move |_| {
        let current = s.string("diff-font");

        let initial = if current.trim().is_empty() {
            gtk::pango::FontDescription::new()
        } else {
            gtk::pango::FontDescription::from_string(&current)
        };

        let parent = root_weak.upgrade();
        let dialog = gtk::FontDialog::new();
        let s = s.clone();
        dialog.choose_font(parent.as_ref(), Some(&initial), gio::Cancellable::NONE, move |result| {
            if let Ok(desc) = result {
                let _ = s.set_string("diff-font", &desc.to_str());
            }
        });
    });

    let s = settings.clone();
    reset_button.connect_clicked(move |_| {
        s.reset("diff-font");
    });

    dialog.present(Some(root));
}
