/* repo_view/branch.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 gettextrs::{gettext, ngettext};
use gtk::prelude::*;
use relm4::prelude::*;

use super::{BranchAction, RepoView, RepoViewMsg, RepoViewOutput};
use crate::confirm_dialog::{ConfirmDialog, ConfirmDialogInit, ConfirmDialogOutput};
use crate::merge_dialog::{MergeDialog, MergeDialogInit, MergeDialogOutput};
use crate::push_dialog::{PushDialog, PushDialogInit, PushDialogOutput};
use crate::stash_switch_dialog::{StashSwitchDialog, StashSwitchDialogInit, StashSwitchDialogOutput};
use crate::text_prompt_dialog::{TextPromptDialog, TextPromptDialogInit, TextPromptDialogOutput};
use crate::{git, preferences};

impl RepoView {
    pub(super) fn handle_branch_action(
        &mut self,
        action: BranchAction,
        sender: &ComponentSender<Self>,
        root: &adw::ToastOverlay,
    ) {
        match action {
            BranchAction::SwitchRequested(..)
            | BranchAction::DirtyCheckComplete { .. }
            | BranchAction::StashSwitchDialogDismissed
            | BranchAction::SwitchWithoutStash(..)
            | BranchAction::StashAndSwitch(..)
            | BranchAction::CheckoutComplete(..) => self.handle_branch_switch(action, sender, root),

            BranchAction::ConfirmDialogDismissed
            | BranchAction::TextPromptDismissed
            | BranchAction::DeleteRequested(..)
            | BranchAction::DeleteMergeCheckComplete { .. }
            | BranchAction::DeleteConfirmed(..)
            | BranchAction::RenameRequested(..)
            | BranchAction::RenameConfirmed { .. }
            | BranchAction::CreateFromRequested(..)
            | BranchAction::CreateFromConfirmed { .. }
            | BranchAction::OperationComplete(..) => self.handle_branch_modify(action, sender, root),

            BranchAction::CopyName(name) => {
                if let Some(display) = gtk::gdk::Display::default() {
                    display.clipboard().set_text(&name);
                }

                root.add_toast(adw::Toast::new(&gettext("Copied “{}”").replace("{}", &name)));
            }

            BranchAction::PushBranchRequested(name) => {
                let path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    RepoViewMsg::Branch(BranchAction::PushBranchDefaultsLoaded(git::get_push_defaults(
                        &path,
                        Some(&name),
                    )))
                });
            }

            BranchAction::PushBranchDefaultsLoaded(result) => match result {
                Ok(defaults) => {
                    let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();

                    self.push_dialog = Some(
                        PushDialog::builder()
                            .launch(PushDialogInit {
                                defaults,
                                parent_window: window,
                            })
                            .forward(sender.input_sender(), |output| match output {
                                PushDialogOutput::Confirmed(settings) => RepoViewMsg::PushConfirmed(settings),
                                PushDialogOutput::Cancelled => RepoViewMsg::PushDialogDismissed,
                            }),
                    );
                }

                Err(msg) => {
                    root.add_toast(adw::Toast::new(&msg));
                }
            },

            BranchAction::FetchRequested(name, kind) => {
                if self.remote_op_in_progress {
                    return;
                }
                self.remote_op_in_progress = true;
                sender.output(RepoViewOutput::RemoteOpStarted).ok();

                let path = self.path.clone();
                let git_command = preferences::git_command();

                sender.spawn_oneshot_command(move || {
                    let remote_name = git::upstream_remote(&path, &name, kind);

                    match remote_name {
                        Ok(remote) => RepoViewMsg::Branch(BranchAction::FetchComplete(git::fetch(
                            &path,
                            &remote,
                            false,
                            false,
                            &git_command,
                        ))),

                        Err(msg) => RepoViewMsg::Branch(BranchAction::FetchComplete(Err(msg))),
                    }
                });
            }

            BranchAction::FetchComplete(result) => {
                self.remote_op_in_progress = false;
                sender.output(RepoViewOutput::RemoteOpFinished).ok();

                match &result {
                    Ok(()) => root.add_toast(adw::Toast::new(&gettext("Fetch complete"))),
                    Err(msg) => root.add_toast(adw::Toast::new(msg)),
                }

                if result.is_ok() {
                    sender.input(RepoViewMsg::Refresh);
                }
            }

            BranchAction::MergeRequested(name, kind) => {
                if self.head_branch.is_none() {
                    root.add_toast(adw::Toast::new(&gettext("Cannot merge: HEAD is detached")));
                    return;
                }

                let path = self.path.clone();

                sender.spawn_oneshot_command(move || match git::dirty_summary(&path) {
                    Ok(summary) => RepoViewMsg::Branch(BranchAction::MergeDirtyCheckComplete { name, kind, summary }),
                    Err(msg) => RepoViewMsg::ShowToast(msg),
                });
            }

            BranchAction::MergeDirtyCheckComplete { name, kind, summary } => {
                let Some(current_branch) = self.head_branch.clone() else {
                    root.add_toast(adw::Toast::new(&gettext("Cannot merge: HEAD is detached")));
                    return;
                };

                let default_strategy = if self.settings.user_value("merge-strategy-default").is_some() {
                    git::MergeStrategy::from(self.settings.string("merge-strategy-default").as_str())
                } else {
                    git::read_default_merge_strategy_from_config(&self.path).unwrap_or_else(|| {
                        // fall back to gsettings value if no repo value is set
                        git::MergeStrategy::from(self.settings.string("merge-strategy-default").as_str())
                    })
                };

                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                let n = name.clone();
                let k = kind;

                let initial_squash_subject = gettext("Squash merge branch '{}'").replace("{}", &name);

                self.merge_dialog = Some(
                    MergeDialog::builder()
                        .launch(MergeDialogInit {
                            source_ref: name,
                            current_branch,
                            default_strategy,
                            dirty_summary: summary,
                            initial_squash_subject,
                            initial_squash_body: String::new(),
                            repo_settings: self.settings.clone(),
                            parent_window: window,
                        })
                        .forward(sender.input_sender(), move |output| match output {
                            MergeDialogOutput::Cancelled => RepoViewMsg::Branch(BranchAction::MergeDialogDismissed),

                            MergeDialogOutput::Confirmed {
                                strategy,
                                stash,
                                squash_message,
                            } => RepoViewMsg::Branch(BranchAction::MergeConfirmed {
                                source_ref: n.clone(),
                                source_kind: k,
                                strategy,
                                stash,
                                squash_message,
                            }),
                        }),
                );
            }

            BranchAction::MergeDialogDismissed => {
                self.merge_dialog = None;
            }

            BranchAction::MergeConfirmed {
                source_ref,
                source_kind,
                strategy,
                stash,
                squash_message,
            } => {
                self.merge_dialog = None;

                if self.remote_op_in_progress {
                    return;
                }

                let _ = self.settings.set_string("merge-strategy-default", strategy.as_ref());

                self.remote_op_in_progress = true;
                sender.output(RepoViewOutput::RemoteOpStarted).ok();

                let path = self.path.clone();
                let git_command = preferences::git_command();
                let settings = git::MergeSettings {
                    source_ref: source_ref.clone(),
                    source_kind,
                    strategy,
                    squash_message,
                };

                sender.spawn_oneshot_command(move || {
                    if let Some(flags) = stash
                        && let Err(msg) = git::stash_push(&path, None, flags)
                    {
                        return RepoViewMsg::Branch(BranchAction::MergeComplete {
                            result: Err(msg),
                            source_ref,
                        });
                    }

                    let result = git::merge_branch(&path, &settings, &git_command);
                    RepoViewMsg::Branch(BranchAction::MergeComplete { result, source_ref })
                });
            }

            BranchAction::MergeComplete { result, source_ref } => {
                self.remote_op_in_progress = false;
                sender.output(RepoViewOutput::RemoteOpFinished).ok();

                let has_conflicts = matches!(&result, Ok(outcome) if outcome.has_conflicts());

                match &result {
                    Ok(outcome) => root.add_toast(adw::Toast::new(&merge_toast_message(outcome, &source_ref))),
                    Err(msg) => root.add_toast(adw::Toast::new(msg)),
                }

                if has_conflicts {
                    sender.input(RepoViewMsg::WorkspaceSelected(super::WorkspaceTab::WorkingCopy));
                }

                sender.input(RepoViewMsg::Refresh);
            }
        }
    }

    fn handle_branch_switch(&mut self, action: BranchAction, sender: &ComponentSender<Self>, root: &adw::ToastOverlay) {
        match action {
            BranchAction::SwitchRequested(name, kind) => {
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || match git::dirty_summary(&repo_path) {
                    Ok(summary) => RepoViewMsg::Branch(BranchAction::DirtyCheckComplete { name, kind, summary }),
                    Err(msg) => RepoViewMsg::ShowToast(msg),
                });
            }

            BranchAction::StashSwitchDialogDismissed => {
                self.stash_switch_dialog = None;
            }

            BranchAction::DirtyCheckComplete { name, kind, summary } => {
                if summary.is_dirty() {
                    let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                    let n = name.clone();
                    let k = kind;

                    self.stash_switch_dialog = Some(
                        StashSwitchDialog::builder()
                            .launch(StashSwitchDialogInit {
                                heading: gettext("Uncommitted Changes"),
                                body: gettext(
                                    "You have uncommitted changes. How do you want to proceed switching to “{}”?",
                                )
                                .replace("{}", &name),
                                proceed_label: gettext("Switch"),
                                allow_skip_stash: true,
                                has_tracked_changes: summary.has_tracked_changes,
                                has_untracked: summary.has_untracked,
                                parent_window: window,
                            })
                            .forward(sender.input_sender(), move |output| match output {
                                StashSwitchDialogOutput::ProceedWithoutStash => {
                                    RepoViewMsg::Branch(BranchAction::SwitchWithoutStash(n.clone(), k))
                                }

                                StashSwitchDialogOutput::StashTrackedOnly => RepoViewMsg::Branch(
                                    BranchAction::StashAndSwitch(n.clone(), k, git::StashPushFlags::Default),
                                ),

                                StashSwitchDialogOutput::StashWithUntracked => RepoViewMsg::Branch(
                                    BranchAction::StashAndSwitch(n.clone(), k, git::StashPushFlags::IncludeUntracked),
                                ),

                                StashSwitchDialogOutput::Cancelled => {
                                    RepoViewMsg::Branch(BranchAction::StashSwitchDialogDismissed)
                                }
                            }),
                    );
                } else {
                    let repo_path = self.path.clone();

                    sender.spawn_oneshot_command(move || {
                        let result = git::checkout_branch(&repo_path, &name, kind);
                        RepoViewMsg::Branch(BranchAction::CheckoutComplete(result))
                    });
                }
            }

            BranchAction::SwitchWithoutStash(name, kind) => {
                self.stash_switch_dialog = None;
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    let result = git::checkout_branch(&repo_path, &name, kind);
                    RepoViewMsg::Branch(BranchAction::CheckoutComplete(result))
                });
            }

            BranchAction::StashAndSwitch(name, kind, flags) => {
                self.stash_switch_dialog = None;
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    if let Err(e) = git::stash_push(&repo_path, None, flags) {
                        return RepoViewMsg::Branch(BranchAction::CheckoutComplete(Err(e)));
                    }

                    let result = git::checkout_branch(&repo_path, &name, kind);
                    RepoViewMsg::Branch(BranchAction::CheckoutComplete(result))
                });
            }

            BranchAction::CheckoutComplete(result) => {
                let toast = match result {
                    Ok(git::CheckoutOutcome::Switched) => gettext("Switched branch"),

                    Ok(git::CheckoutOutcome::CreatedTrackingBranch) => gettext("Created tracking branch and switched"),

                    Ok(git::CheckoutOutcome::SwitchedToExistingLocal) => {
                        gettext("Switched to existing local branch (remote was not checked out)")
                    }

                    Err(msg) => msg,
                };

                root.add_toast(adw::Toast::new(&toast));
                sender.input(RepoViewMsg::Refresh);
            }

            // rest is handled in handle_branch_modify
            _ => unreachable!(),
        }
    }

    fn handle_branch_modify(&mut self, action: BranchAction, sender: &ComponentSender<Self>, root: &adw::ToastOverlay) {
        match action {
            BranchAction::ConfirmDialogDismissed => {
                self.confirm_dialog = None;
            }

            BranchAction::TextPromptDismissed => {
                self.text_prompt_dialog = None;
            }
            BranchAction::DeleteRequested(name, kind) => {
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    let merged = git::is_branch_merged(&repo_path, &name, kind).unwrap_or(true);
                    RepoViewMsg::Branch(BranchAction::DeleteMergeCheckComplete { name, kind, merged })
                });
            }

            BranchAction::DeleteMergeCheckComplete { name, kind, merged } => {
                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                let n = name.clone();

                let body = if merged {
                    gettext("Branch “{}” will be permanently deleted.").replace("{}", &name)
                } else {
                    gettext("Branch “{}” has unmerged commits that will be lost!").replace("{}", &name)
                };

                self.confirm_dialog = Some(
                    ConfirmDialog::builder()
                        .launch(ConfirmDialogInit {
                            heading: gettext("Delete Branch?"),
                            body,
                            action_label: gettext("Delete"),
                            appearance: adw::ResponseAppearance::Destructive,
                            parent_window: window,
                        })
                        .forward(sender.input_sender(), move |output| match output {
                            ConfirmDialogOutput::Confirmed => {
                                RepoViewMsg::Branch(BranchAction::DeleteConfirmed(n.clone(), kind))
                            }

                            ConfirmDialogOutput::Cancelled => RepoViewMsg::Branch(BranchAction::ConfirmDialogDismissed),
                        }),
                );
            }

            BranchAction::DeleteConfirmed(name, kind) => {
                self.confirm_dialog = None;
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    let result = git::delete_branch(&repo_path, &name, kind);
                    RepoViewMsg::Branch(BranchAction::OperationComplete(result, gettext("Branch deleted")))
                });
            }

            BranchAction::RenameRequested(name) => {
                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                let old = name.clone();

                self.text_prompt_dialog = Some(
                    TextPromptDialog::builder()
                        .launch(TextPromptDialogInit {
                            heading: gettext("Rename Branch"),
                            body: gettext("Enter a new name for “{}”.").replace("{}", &name),
                            placeholder: gettext("New branch name"),
                            initial: name,
                            confirm_label: gettext("Rename"),
                            parent_window: window,
                            validator: Some(git::is_valid_branch_name),
                            notice: None,
                        })
                        .forward(sender.input_sender(), move |output| match output {
                            TextPromptDialogOutput::Confirmed(new_name) => {
                                RepoViewMsg::Branch(BranchAction::RenameConfirmed {
                                    old_name: old.clone(),
                                    new_name,
                                })
                            }

                            TextPromptDialogOutput::Cancelled => RepoViewMsg::Branch(BranchAction::TextPromptDismissed),
                        }),
                );
            }

            BranchAction::RenameConfirmed { old_name, new_name } => {
                self.text_prompt_dialog = None;
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    let result = git::rename_branch(&repo_path, &old_name, &new_name);
                    RepoViewMsg::Branch(BranchAction::OperationComplete(result, gettext("Branch renamed")))
                });
            }

            BranchAction::CreateFromRequested(base_ref) => {
                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                let base = base_ref.clone();

                self.text_prompt_dialog = Some(
                    TextPromptDialog::builder()
                        .launch(TextPromptDialogInit {
                            heading: gettext("Create Branch"),
                            body: gettext("Create a new branch from “{}”.").replace("{}", &base_ref),
                            placeholder: gettext("New branch name"),
                            initial: String::new(),
                            confirm_label: gettext("Create"),
                            parent_window: window,
                            validator: Some(git::is_valid_branch_name),
                            notice: None,
                        })
                        .forward(sender.input_sender(), move |output| match output {
                            TextPromptDialogOutput::Confirmed(new_name) => {
                                RepoViewMsg::Branch(BranchAction::CreateFromConfirmed {
                                    new_name,
                                    base_ref: base.clone(),
                                })
                            }

                            TextPromptDialogOutput::Cancelled => RepoViewMsg::Branch(BranchAction::TextPromptDismissed),
                        }),
                );
            }

            BranchAction::CreateFromConfirmed { new_name, base_ref } => {
                self.text_prompt_dialog = None;
                let repo_path = self.path.clone();

                sender.spawn_oneshot_command(move || {
                    let result = git::create_branch_from(&repo_path, &new_name, &base_ref);
                    RepoViewMsg::Branch(BranchAction::OperationComplete(result, gettext("Branch created")))
                });
            }

            BranchAction::OperationComplete(result, success_msg) => {
                match result {
                    Ok(()) => root.add_toast(adw::Toast::new(&success_msg)),
                    Err(msg) => root.add_toast(adw::Toast::new(&msg)),
                }

                sender.input(RepoViewMsg::Refresh);
            }

            // rest is handled in handle_branch_switch
            _ => unreachable!(),
        }
    }
}

pub(super) fn merge_toast_message(outcome: &git::MergeOutcome, source_ref: &str) -> String {
    match outcome {
        git::MergeOutcome::AlreadyUpToDate => gettext("Already up to date"),

        git::MergeOutcome::FastForward { commits } => ngettext(
            "Merged %1 commit from %2 (fast-forward)",
            "Merged %1 commits from %2 (fast-forward)",
            *commits as u32,
        )
        .replace("%1", &commits.to_string())
        .replace("%2", source_ref),

        git::MergeOutcome::Merged { commits } => ngettext(
            "Merged %1 commit from %2 (merge commit)",
            "Merged %1 commits from %2 (merge commit)",
            *commits as u32,
        )
        .replace("%1", &commits.to_string())
        .replace("%2", source_ref),

        git::MergeOutcome::Rebased { commits } => {
            ngettext("Merged %1 commit from %2 (rebase)", "Merged %1 commits from %2 (rebase)", *commits as u32)
                .replace("%1", &commits.to_string())
                .replace("%2", source_ref)
        }

        git::MergeOutcome::Squashed { commits } => {
            ngettext("Squashed %1 commit from %2", "Squashed %1 commits from %2", *commits as u32)
                .replace("%1", &commits.to_string())
                .replace("%2", source_ref)
        }

        git::MergeOutcome::MergeConflicts { conflicted_files } => ngettext(
            "Merge conflict in %1 file, resolve conflicts and commit",
            "Merge conflicts in %1 files, resolve conflicts and commit",
            conflicted_files.len() as u32,
        )
        .replace("%1", &conflicted_files.len().to_string()),

        git::MergeOutcome::RebaseConflicts => gettext("Rebase paused due to conflicts, resolve and continue"),
    }
}
