/* stashes_view.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::path::PathBuf;

use adw::prelude::*;
use gettextrs::gettext;
use gtk::gdk;
use relm4::factory::FactoryComponent;
use relm4::prelude::*;

use crate::confirm_dialog::{ConfirmDialog, ConfirmDialogInit, ConfirmDialogOutput};
use crate::file_diff_row::FileDiffRow;
use crate::git::{self, FileDiff, StashEntry};
use crate::{menus, preferences, ui_helpers};

pub struct StashRow {
    entry: StashEntry,
    popover: Option<gtk::PopoverMenu>,
}

impl std::fmt::Debug for StashRow {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("StashRow").field("entry", &self.entry).finish()
    }
}

#[derive(Debug, Clone, Copy)]
pub enum StashRowInput {
    ShowContextMenu,
    Apply,
    Pop,
    Drop,
}

#[derive(Debug)]
pub enum StashRowOutput {
    Apply(usize),
    Pop(usize),
    Drop(usize),
}

#[relm4::factory(pub)]
impl FactoryComponent for StashRow {
    type Init = StashEntry;
    type Input = StashRowInput;
    type Output = StashRowOutput;
    type CommandOutput = ();
    type ParentWidget = gtk::ListBox;

    view! {
        #[name = "row"]
        adw::ActionRow {
            set_use_markup: false,
            set_title: &self.entry.message,
            set_subtitle: &format!("stash@{{{}}}", self.entry.index),

            add_prefix = &gtk::Image {
                set_icon_name: Some("document-save-symbolic"),
                set_accessible_role: gtk::AccessibleRole::Presentation,
            },

            add_controller = gtk::GestureClick {
                set_button: gdk::BUTTON_SECONDARY,
                connect_released[sender] => move |gesture, _, _, _| {
                    gesture.set_state(gtk::EventSequenceState::Claimed);
                    sender.input(StashRowInput::ShowContextMenu);
                },
            },
        }
    }

    fn init_model(entry: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
        Self { entry, popover: None }
    }

    fn init_widgets(
        &mut self,
        _index: &DynamicIndex,
        _root: Self::Root,
        _returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
        sender: FactorySender<Self>,
    ) -> Self::Widgets {
        let widgets = view_output!();

        let popover = gtk::PopoverMenu::from_model(Some(&menus::stash_item_context_menu()));
        popover.set_parent(&widgets.row);
        popover.set_has_arrow(false);
        self.popover = Some(popover);

        let group = gtk::gio::SimpleActionGroup::new();

        for (name, input) in [
            ("apply", StashRowInput::Apply),
            ("pop", StashRowInput::Pop),
            ("drop", StashRowInput::Drop),
        ] {
            let action = gtk::gio::SimpleAction::new(name, None);
            let s = sender.clone();
            action.connect_activate(move |_, _| s.input(input));
            group.add_action(&action);
        }

        widgets.row.insert_action_group("stash-item", Some(&group));

        widgets
    }

    fn update(&mut self, msg: Self::Input, sender: FactorySender<Self>) {
        match msg {
            StashRowInput::ShowContextMenu => {
                if let Some(popover) = &self.popover {
                    popover.popup();
                }
            }

            StashRowInput::Apply => {
                sender.output(StashRowOutput::Apply(self.entry.index)).ok();
            }

            StashRowInput::Pop => {
                sender.output(StashRowOutput::Pop(self.entry.index)).ok();
            }

            StashRowInput::Drop => {
                sender.output(StashRowOutput::Drop(self.entry.index)).ok();
            }
        }
    }

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

pub struct StashesView {
    path: PathBuf,
    stashes: Vec<StashEntry>,
    stashes_factory: FactoryVecDeque<StashRow>,
    diff_factory: FactoryVecDeque<FileDiffRow>,
    confirm_dialog: Option<Controller<ConfirmDialog>>,
}

#[derive(Debug)]
pub enum StashesViewOutput {
    ShowToast(String),
    DataChanged,
    StashCountChanged(usize),
}

#[derive(Debug)]
pub enum StashesViewMsg {
    Refresh,
    SetStashes(Vec<StashEntry>),
    StashesLoaded(Result<Vec<StashEntry>, String>),
    StashSelected(usize),
    StashDiffLoaded(Result<Vec<FileDiff>, String>),
    ApplyStash(usize),
    PopStash(usize),
    DropRequested(usize),
    ConfirmDialogDismissed,
    DropConfirmed(usize),
    StashOpComplete(Result<(), String>, String),
}

#[relm4::component(pub)]
impl Component for StashesView {
    type Init = (PathBuf, Vec<StashEntry>);
    type Input = StashesViewMsg;
    type Output = StashesViewOutput;
    type CommandOutput = StashesViewMsg;

    view! {
        gtk::Paned {
            set_orientation: gtk::Orientation::Horizontal,
            set_shrink_start_child: false,
            set_shrink_end_child: false,

            #[wrap(Some)]
            set_start_child = &gtk::ScrolledWindow {
                set_hscrollbar_policy: gtk::PolicyType::Never,
                set_width_request: 180,

                gtk::Box {
                    set_orientation: gtk::Orientation::Vertical,
                    add_css_class: "gap-l",
                    add_css_class: "m-l",

                    gtk::Label {
                        set_label: &gettext("Stashes"),
                        add_css_class: "title-4",
                        set_halign: gtk::Align::Start,
                    },

                    #[local_ref]
                    stashes_list -> gtk::ListBox {
                        set_selection_mode: gtk::SelectionMode::Single,
                        add_css_class: "boxed-list",
                        set_vexpand: true,
                    },
                },
            },

            #[wrap(Some)]
            #[name = "detail_stack"]
            set_end_child = &gtk::Stack {
                set_transition_type: gtk::StackTransitionType::Crossfade,

                add_named[Some("empty")] = &adw::StatusPage {
                    set_icon_name: Some("document-save-symbolic"),
                    set_title: &gettext("Stash Details"),
                    set_description: Some(&gettext("Select a stash to view its contents")),
                },

                add_named[Some("detail")] = &gtk::ScrolledWindow {
                    set_hscrollbar_policy: gtk::PolicyType::Never,

                    #[name = "detail_box"]
                    gtk::Box {
                        set_orientation: gtk::Orientation::Vertical,
                        add_css_class: "gap-l",
                        add_css_class: "m-l",

                        #[name = "stash_message_label"]
                        gtk::Label {
                            set_halign: gtk::Align::Start,
                            add_css_class: "title-3",
                            set_wrap: true,
                            set_wrap_mode: gtk::pango::WrapMode::Word,
                        },

                        #[local_ref]
                        diff_list -> gtk::Box {
                            set_orientation: gtk::Orientation::Vertical,
                            add_css_class: "gap-l",
                        },
                    },
                },

                set_visible_child_name: "empty",
            },
        }
    }

    fn init(init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let (path, stashes) = init;

        let mut stashes_factory =
            FactoryVecDeque::builder()
                .launch(gtk::ListBox::default())
                .forward(sender.input_sender(), |output| match output {
                    StashRowOutput::Apply(index) => StashesViewMsg::ApplyStash(index),
                    StashRowOutput::Pop(index) => StashesViewMsg::PopStash(index),
                    StashRowOutput::Drop(index) => StashesViewMsg::DropRequested(index),
                });
        let diff_factory = FactoryVecDeque::builder().launch(gtk::Box::default()).detach();

        for stash in &stashes {
            stashes_factory.guard().push_back(stash.clone());
        }

        let model = StashesView {
            path,
            stashes,
            stashes_factory,
            diff_factory,
            confirm_dialog: None,
        };

        let stashes_list = model.stashes_factory.widget();
        let diff_list = model.diff_factory.widget();
        let widgets = view_output!();

        stashes_list.set_placeholder(Some(&ui_helpers::empty_placeholder(&gettext("No stashes"))));

        let s = sender.input_sender().clone();
        stashes_list.connect_row_selected(move |_, row| {
            if let Some(row) = row {
                s.emit(StashesViewMsg::StashSelected(row.index() as usize));
            }
        });

        preferences::persist_paned_position(&root, "paned-files-position");

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        msg: Self::Input,
        sender: ComponentSender<Self>,
        root: &Self::Root,
    ) {
        match msg {
            StashesViewMsg::Refresh => {
                let p = self.path.clone();
                sender.spawn_oneshot_command(move || StashesViewMsg::StashesLoaded(git::load_stashes_from_path(&p)));
            }

            StashesViewMsg::SetStashes(stashes) => {
                if stashes == self.stashes {
                    return;
                }
                self.replace_stashes(stashes, &widgets.detail_stack, &sender);
            }

            StashesViewMsg::StashesLoaded(result) => match result {
                Ok(stashes) => {
                    self.replace_stashes(stashes, &widgets.detail_stack, &sender);
                }

                Err(msg) => {
                    sender.output(StashesViewOutput::ShowToast(msg)).ok();
                }
            },

            StashesViewMsg::StashSelected(index) => {
                if let Some(stash) = self.stashes.get(index) {
                    let oid = stash.oid.clone();

                    widgets.stash_message_label.set_label(&stash.message);
                    self.diff_factory.guard().clear();
                    widgets.detail_stack.set_visible_child_name("detail");

                    let p = self.path.clone();
                    sender
                        .spawn_oneshot_command(move || StashesViewMsg::StashDiffLoaded(git::load_stash_diff(&p, &oid)));
                }
            }

            StashesViewMsg::StashDiffLoaded(result) => match result {
                Ok(file_diffs) => {
                    let gutter_digits = crate::file_diff_row::gutter_digits_for(&file_diffs);

                    let mut guard = self.diff_factory.guard();
                    guard.clear();

                    for file_diff in file_diffs {
                        guard.push_back(crate::file_diff_row::FileDiffRowInit {
                            file_diff,
                            gutter_digits,
                        });
                    }
                }

                Err(msg) => {
                    sender.output(StashesViewOutput::ShowToast(msg)).ok();
                }
            },

            StashesViewMsg::ApplyStash(index) => {
                let p = self.path.clone();
                let msg = gettext("Stash applied");

                sender.spawn_oneshot_command(move || StashesViewMsg::StashOpComplete(git::stash_apply(&p, index), msg));
            }

            StashesViewMsg::PopStash(index) => {
                let p = self.path.clone();
                let msg = gettext("Stash popped");

                sender.spawn_oneshot_command(move || StashesViewMsg::StashOpComplete(git::stash_pop(&p, index), msg));
            }

            StashesViewMsg::DropRequested(index) => {
                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();

                self.confirm_dialog = Some(
                    ConfirmDialog::builder()
                        .launch(ConfirmDialogInit {
                            heading: gettext("Drop Stash?"),
                            body: gettext("This stash will be permanently deleted."),
                            action_label: gettext("Drop"),
                            appearance: adw::ResponseAppearance::Destructive,
                            parent_window: window,
                        })
                        .forward(sender.input_sender(), move |output| match output {
                            ConfirmDialogOutput::Confirmed => StashesViewMsg::DropConfirmed(index),
                            ConfirmDialogOutput::Cancelled => StashesViewMsg::ConfirmDialogDismissed,
                        }),
                );
            }

            StashesViewMsg::ConfirmDialogDismissed => {
                self.confirm_dialog = None;
            }

            StashesViewMsg::DropConfirmed(index) => {
                self.confirm_dialog = None;
                let p = self.path.clone();
                let msg = gettext("Stash dropped");

                sender.spawn_oneshot_command(move || StashesViewMsg::StashOpComplete(git::stash_drop(&p, index), msg));
            }

            StashesViewMsg::StashOpComplete(result, success_msg) => {
                match &result {
                    Ok(()) => sender.output(StashesViewOutput::ShowToast(success_msg)).ok(),
                    Err(msg) => sender.output(StashesViewOutput::ShowToast(msg.clone())).ok(),
                };

                if result.is_ok() {
                    sender.output(StashesViewOutput::DataChanged).ok();
                }
            }
        }
    }

    fn update_cmd(&mut self, msg: Self::CommandOutput, sender: ComponentSender<Self>, _root: &Self::Root) {
        sender.input(msg);
    }
}

impl StashesView {
    fn replace_stashes(&mut self, stashes: Vec<StashEntry>, detail_stack: &gtk::Stack, sender: &ComponentSender<Self>) {
        self.stashes = stashes;
        let mut guard = self.stashes_factory.guard();
        guard.clear();
        for stash in &self.stashes {
            guard.push_back(stash.clone());
        }
        detail_stack.set_visible_child_name("empty");

        sender
            .output(StashesViewOutput::StashCountChanged(self.stashes.len()))
            .ok();
    }
}
