/* merge_dialog.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 adw::prelude::*;
use gettextrs::gettext;
use relm4::gtk::{gio, glib};
use relm4::prelude::*;

use crate::{git, wrapping};

pub struct MergeDialogInit {
    pub source_ref: String,
    pub current_branch: String,
    pub default_strategy: git::MergeStrategy,
    pub dirty_summary: git::DirtySummary,
    pub initial_squash_subject: String,
    pub initial_squash_body: String,
    pub repo_settings: gio::Settings,
    pub parent_window: Option<gtk::Window>,
}

#[derive(Debug)]
pub enum MergeDialogOutput {
    Confirmed {
        strategy: git::MergeStrategy,
        stash: Option<git::StashPushFlags>,
        squash_message: Option<String>,
    },
    Cancelled,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashChoice {
    None,
    Tracked,
    All,
}

#[derive(Debug)]
pub enum MergeDialogMsg {
    StrategyChanged(git::MergeStrategy),
    StashChanged(StashChoice),
    SubjectChanged(String),
    WrapBody,
    MaxLineLengthChanged(u32),
    Submit,
    Cancel,
}

pub struct MergeDialog {
    dialog: adw::AlertDialog,
    subject: String,
    body_buffer: gtk::TextBuffer,
    selected_strategy: git::MergeStrategy,
    selected_stash: StashChoice,
    dirty_summary: git::DirtySummary,
    max_line_length: u32,
    wrapping_body: bool,
    repo_settings: gio::Settings,
    settings_handler: Option<glib::SignalHandlerId>,
}

fn is_rebase_strategy(strategy: git::MergeStrategy) -> bool {
    matches!(strategy, git::MergeStrategy::RebaseFastForward | git::MergeStrategy::RebaseMergeCommit)
}

#[relm4::component(pub)]
impl Component for MergeDialog {
    type Init = MergeDialogInit;
    type Input = MergeDialogMsg;
    type Output = MergeDialogOutput;
    type CommandOutput = ();

    view! {
        #[root]
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
            add_css_class: "gap-l",
            set_width_request: 480,

            adw::PreferencesGroup {
                adw::ActionRow {
                    set_title: &gettext("Fast-forward, otherwise merge commit"),
                    set_subtitle: &gettext("Linear when possible, otherwise create a merge commit"),
                    set_activatable: true,

                    #[name = "ff_or_merge_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StrategyChanged(git::MergeStrategy::FastForwardOrMergeCommit));
                            }
                        },
                    },
                    set_activatable_widget: Some(&ff_or_merge_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Merge commit"),
                    set_subtitle: &gettext("Always create a merge commit (--no-ff)"),
                    set_activatable: true,

                    #[name = "merge_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&ff_or_merge_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StrategyChanged(git::MergeStrategy::MergeCommit));
                            }
                        },
                    },
                    set_activatable_widget: Some(&merge_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Rebase, then fast-forward"),
                    set_subtitle: &gettext("Linear history, no merge commit"),
                    set_activatable: true,

                    #[name = "rebase_ff_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&ff_or_merge_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StrategyChanged(git::MergeStrategy::RebaseFastForward));
                            }
                        },
                    },
                    set_activatable_widget: Some(&rebase_ff_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Rebase, then merge commit"),
                    set_subtitle: &gettext("Rebase source, then create a merge commit"),
                    set_activatable: true,

                    #[name = "rebase_merge_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&ff_or_merge_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StrategyChanged(git::MergeStrategy::RebaseMergeCommit));
                            }
                        },
                    },
                    set_activatable_widget: Some(&rebase_merge_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Squash commit"),
                    set_subtitle: &gettext("Combine all source commits into a single commit"),
                    set_activatable: true,

                    #[name = "squash_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&ff_or_merge_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StrategyChanged(git::MergeStrategy::Squash));
                            }
                        },
                    },
                    set_activatable_widget: Some(&squash_check),
                },
            },

            adw::PreferencesGroup {
                set_title: &gettext("Uncommitted Changes"),
                set_description: Some(&gettext("Your working copy has uncommitted changes. Choose what to do with them.")),
                #[watch]
                set_visible: model.dirty_summary.is_dirty(),

                adw::ActionRow {
                    set_title: &gettext("Don't stash"),
                    set_subtitle: &gettext("Keep changes in the working copy (not allowed for rebase)"),
                    set_activatable: true,
                    #[watch]
                    set_sensitive: !is_rebase_strategy(model.selected_strategy),

                    #[name = "no_stash_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StashChanged(StashChoice::None));
                            }
                        },
                    },
                    set_activatable_widget: Some(&no_stash_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Stash tracked changes"),
                    set_subtitle: &gettext("Stash modifications to tracked files before merging"),
                    set_activatable: true,
                    #[watch]
                    set_visible: model.dirty_summary.has_tracked_changes,

                    #[name = "stash_tracked_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&no_stash_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StashChanged(StashChoice::Tracked));
                            }
                        },
                    },
                    set_activatable_widget: Some(&stash_tracked_check),
                },

                adw::ActionRow {
                    set_title: &gettext("Stash including untracked"),
                    set_subtitle: &gettext("Stash tracked and untracked files before merging"),
                    set_activatable: true,
                    #[watch]
                    set_visible: model.dirty_summary.has_untracked,

                    #[name = "stash_all_check"]
                    add_prefix = &gtk::CheckButton {
                        set_valign: gtk::Align::Center,
                        set_group: Some(&no_stash_check),
                        connect_toggled[sender] => move |btn| {
                            if btn.is_active() {
                                sender.input(MergeDialogMsg::StashChanged(StashChoice::All));
                            }
                        },
                    },
                    set_activatable_widget: Some(&stash_all_check),
                },
            },

            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                add_css_class: "gap-s",
                #[watch]
                set_visible: matches!(model.selected_strategy, git::MergeStrategy::Squash),

                gtk::Label {
                    set_label: &gettext("Squash Commit Message"),
                    set_halign: gtk::Align::Start,
                    add_css_class: "heading",
                },

                gtk::Label {
                    set_label: &gettext("Subject and details for the squashed commit"),
                    set_halign: gtk::Align::Start,
                    set_wrap: true,
                    add_css_class: "dim-label",
                },

                #[name = "subject_entry"]
                gtk::Entry {
                    set_placeholder_text: Some(&gettext("Commit Subject")),
                    update_property: &[gtk::accessible::Property::Label(&gettext("Commit Subject"))],
                    set_activates_default: false,
                    add_css_class: "card",
                    #[watch]
                    set_max_length: model.max_line_length as i32,
                    connect_changed[sender] => move |entry| {
                        sender.input(MergeDialogMsg::SubjectChanged(entry.text().to_string()));
                    },
                },

                gtk::ScrolledWindow {
                    set_hscrollbar_policy: gtk::PolicyType::Never,
                    set_min_content_height: 140,
                    set_vexpand: true,

                    #[name = "body_view"]
                    gtk::TextView {
                        set_buffer: Some(&body_buffer),
                        set_wrap_mode: gtk::WrapMode::Word,
                        set_top_margin: 6,
                        set_bottom_margin: 6,
                        set_left_margin: 6,
                        set_right_margin: 6,
                        update_property: &[gtk::accessible::Property::Label(&gettext("Commit Details"))],
                        add_css_class: "card",
                    },
                },
            },
        }
    }

    fn init(init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let max_line_length = init.repo_settings.int("commit-line-length").max(0) as u32;

        let initial_stash = if init.dirty_summary.has_tracked_changes {
            StashChoice::Tracked
        } else if init.dirty_summary.has_untracked {
            StashChoice::All
        } else {
            StashChoice::None
        };

        let dialog = adw::AlertDialog::builder()
            .heading(
                gettext("Merge “{source}” into “{branch}”")
                    .replace("{source}", &init.source_ref)
                    .replace("{branch}", &init.current_branch),
            )
            .build();

        dialog.add_response("cancel", &gettext("Cancel"));
        dialog.add_response("confirm", &gettext("Merge"));
        dialog.set_response_appearance("confirm", adw::ResponseAppearance::Suggested);
        dialog.set_default_response(Some("confirm"));
        dialog.set_close_response("cancel");

        let body_buffer = gtk::TextBuffer::new(None);

        let mut model = MergeDialog {
            dialog: dialog.clone(),
            subject: init.initial_squash_subject.clone(),
            body_buffer: body_buffer.clone(),
            selected_strategy: init.default_strategy,
            selected_stash: initial_stash,
            dirty_summary: init.dirty_summary,
            max_line_length,
            wrapping_body: false,
            repo_settings: init.repo_settings.clone(),
            settings_handler: None,
        };

        let widgets = view_output!();

        widgets.subject_entry.set_text(&init.initial_squash_subject);
        model.body_buffer.set_text(&init.initial_squash_body);

        match init.default_strategy {
            git::MergeStrategy::FastForwardOrMergeCommit => widgets.ff_or_merge_check.set_active(true),
            git::MergeStrategy::MergeCommit => widgets.merge_check.set_active(true),
            git::MergeStrategy::RebaseFastForward => widgets.rebase_ff_check.set_active(true),
            git::MergeStrategy::RebaseMergeCommit => widgets.rebase_merge_check.set_active(true),
            git::MergeStrategy::Squash => widgets.squash_check.set_active(true),
        }

        match initial_stash {
            StashChoice::None => widgets.no_stash_check.set_active(true),
            StashChoice::Tracked => widgets.stash_tracked_check.set_active(true),
            StashChoice::All => widgets.stash_all_check.set_active(true),
        }

        {
            let s = sender.clone();
            model.body_buffer.connect_changed(move |_| {
                s.input(MergeDialogMsg::WrapBody);
            });
        }

        {
            let s = sender.clone();
            let handler = init
                .repo_settings
                .connect_changed(Some("commit-line-length"), move |settings, _| {
                    let len = settings.int("commit-line-length").max(0) as u32;
                    s.input(MergeDialogMsg::MaxLineLengthChanged(len));
                });

            model.settings_handler = Some(handler);
        }

        dialog.set_extra_child(Some(&root));

        {
            let s = sender.clone();
            dialog.connect_response(None, move |_dialog, response| match response {
                "confirm" => s.input(MergeDialogMsg::Submit),
                _ => s.input(MergeDialogMsg::Cancel),
            });
        }

        dialog.present(init.parent_window.as_ref());
        model.dialog.set_response_enabled("confirm", model.is_confirm_enabled());

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        msg: Self::Input,
        sender: ComponentSender<Self>,
        _root: &Self::Root,
    ) {
        match msg {
            MergeDialogMsg::StrategyChanged(strategy) => {
                self.selected_strategy = strategy;

                if is_rebase_strategy(strategy)
                    && self.dirty_summary.is_dirty()
                    && self.selected_stash == StashChoice::None
                {
                    if self.dirty_summary.has_tracked_changes {
                        widgets.stash_tracked_check.set_active(true);
                    } else if self.dirty_summary.has_untracked {
                        widgets.stash_all_check.set_active(true);
                    }
                }
            }

            MergeDialogMsg::StashChanged(choice) => {
                self.selected_stash = choice;
            }

            MergeDialogMsg::SubjectChanged(text) => {
                self.subject = text;
            }

            MergeDialogMsg::WrapBody => {
                if self.max_line_length > 0 && !self.wrapping_body {
                    self.wrapping_body = true;
                    wrapping::hard_wrap_buffer(&self.body_buffer, self.max_line_length as usize);
                    self.wrapping_body = false;
                }
            }

            MergeDialogMsg::MaxLineLengthChanged(len) => {
                self.max_line_length = len;
            }

            MergeDialogMsg::Submit => {
                let strategy = self.selected_strategy;

                let stash = match self.selected_stash {
                    StashChoice::None => None,
                    StashChoice::Tracked => Some(git::StashPushFlags::Default),
                    StashChoice::All => Some(git::StashPushFlags::IncludeUntracked),
                };

                let squash_message = if matches!(strategy, git::MergeStrategy::Squash) {
                    let subject = self.subject.trim().to_string();
                    if subject.is_empty() {
                        tracing::error!("MergeDialog::Submit reached with empty squash subject");
                        sender.output(MergeDialogOutput::Cancelled).ok();
                        return;
                    }

                    let buf = &self.body_buffer;
                    let body = buf
                        .text(&buf.start_iter(), &buf.end_iter(), false)
                        .to_string()
                        .trim()
                        .to_string();

                    Some(if body.is_empty() {
                        subject
                    } else {
                        format!("{subject}\n\n{body}")
                    })
                } else {
                    None
                };

                sender
                    .output(MergeDialogOutput::Confirmed {
                        strategy,
                        stash,
                        squash_message,
                    })
                    .ok();
                return;
            }

            MergeDialogMsg::Cancel => {
                sender.output(MergeDialogOutput::Cancelled).ok();
                return;
            }
        }

        self.dialog.set_response_enabled("confirm", self.is_confirm_enabled());
        self.update_view(widgets, sender);
    }

    fn shutdown(&mut self, _widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
        if let Some(handler) = self.settings_handler.take() {
            self.repo_settings.disconnect(handler);
        }
    }
}

impl MergeDialog {
    fn is_confirm_enabled(&self) -> bool {
        if matches!(self.selected_strategy, git::MergeStrategy::Squash) {
            !self.subject.trim().is_empty()
        } else {
            true
        }
    }
}
