/* commit_log_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::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::rc::Rc;

use adw::prelude::*;
use dragging::{DragState, attach_drag_source, attach_drop_target};
use gettextrs::gettext;
use model::{CommitItem, CommitListModel, build_commit_item};
use relm4::gtk::glib::BoxedAnyObject;
use relm4::prelude::*;

use crate::branch_info_box::BranchInfoBox;
use crate::commit_search_bar::CommitSearchBar;
use crate::file_diff_row::FileDiffRow;
use crate::git::{self, BranchRefs, SquashMode, UpstreamRef};
use crate::tag_info_box::TagInfoBox;
use crate::{preferences, ui_helpers};

mod context_menu;
mod dragging;
mod model;
mod pagination;
mod reference;
mod search;
mod selection;

pub use context_menu::ContextMenuAction;
pub use pagination::PaginationAction;
pub use reference::ReferenceAction;
pub use search::SearchAction;
pub use selection::SelectionAction;

const PAGE_SIZE: usize = 200;
const SPINNER_DELAY_MS: u64 = 200;

struct CommitItemWidgets {
    summary: gtk::Label,
    subtitle: gtk::Label,
    tag_icon: gtk::Image,
    tag_label: gtk::Label,
    local_icon: gtk::Image,
    local_label: gtk::Label,
    remote_icon: gtk::Image,
    remote_label: gtk::Label,
    upstream_icon: gtk::Image,
    upstream_label: gtk::Label,
    badge_row: gtk::Box,
}

pub struct CommitLogView {
    path: PathBuf,
    list_view: CommitListModel,
    drag_state: Rc<RefCell<DragState>>,
    branch_info_box: Controller<BranchInfoBox>,
    tag_info_box: Controller<TagInfoBox>,
    diff_factory: FactoryVecDeque<FileDiffRow>,
    current_ref: Option<String>,
    current_kind: Option<git::BranchKind>,
    current_is_head: bool,
    current_is_detached: bool,
    last_oid: Option<String>,
    has_more: bool,
    pending_load: bool,
    loading: bool,
    generation: u64,
    diff_generation: u64,
    tag_map: HashMap<String, Vec<String>>,
    branch_map: HashMap<String, BranchRefs>,
    upstream_ref: Option<UpstreamRef>,
    tip_oid: Option<String>,
    selected_oid: Option<String>,
    context_commit_oid: Option<String>,
    pending_select_range: Option<(u32, u32)>,
    search_bar: Controller<CommitSearchBar>,
    squash_action: gtk::gio::SimpleAction,
    fixup_action: gtk::gio::SimpleAction,
    reword_action: gtk::gio::SimpleAction,
    drop_action: gtk::gio::SimpleAction,
    edit_action: gtk::gio::SimpleAction,
    rewriteable_oids: HashSet<String>,
}

#[derive(Debug)]
pub enum CommitLogViewMsg {
    Reference(ReferenceAction),
    Pagination(PaginationAction),
    ContextMenu(ContextMenuAction),
    Search(SearchAction),
    Selection(SelectionAction),
    ShowToast(String),
    ReorderDrop { dragged_oid: String, target_oid: String },
    ReorderFailed,
    RewriteableOidsLoaded(HashSet<String>),
}

#[derive(Debug)]
pub enum CommitLogViewOutput {
    ShowToast(String),
    CreateTagRequested(String),
    CheckoutRequested(String),
    ArchiveRequested(String),
    SelectedCommitChanged(Option<String>),
    SquashRequested { oids: Vec<String>, mode: SquashMode },
    ReorderRequested { new_order: Vec<String> },
    RewordRequested(String),
    DropRequested(String),
    EditRequested(String),
}

#[relm4::component(pub)]
impl Component for CommitLogView {
    type Init = PathBuf;
    type Input = CommitLogViewMsg;
    type Output = CommitLogViewOutput;
    type CommandOutput = CommitLogViewMsg;

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

            #[wrap(Some)]
            set_start_child = &gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                add_css_class: "gap-l",
                add_css_class: "m-l",
                set_width_request: 300,

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

                #[local_ref]
                branch_info_widget -> gtk::Box {},

                #[local_ref]
                tag_info_widget -> gtk::Box {},

                #[local_ref]
                search_bar_widget -> gtk::Box {},

                #[name = "commits_scroll"]
                gtk::ScrolledWindow {
                    add_css_class: "card",
                    set_overflow: gtk::Overflow::Hidden,
                    set_hscrollbar_policy: gtk::PolicyType::Never,
                    set_vexpand: true,

                    #[name = "commits_list_view"]
                    #[wrap(Some)]
                    set_child = &gtk::ListView {
                        set_show_separators: true,
                        set_single_click_activate: false,
                        set_model: Some(&model.list_view.selection_model),
                    },
                },

                gtk::Box {
                    set_orientation: gtk::Orientation::Horizontal,
                    add_css_class: "gap-m",
                    set_halign: gtk::Align::Center,
                    #[watch]
                    set_visible: model.loading,

                    gtk::Spinner {
                        #[watch]
                        set_spinning: model.loading,
                    },

                    gtk::Label {
                        set_label: &gettext("Loading…"),
                        add_css_class: "dim-label",
                    },
                },
            },

            #[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("object-select-symbolic"),
                    set_title: &gettext("Commit Details"),
                    set_description: Some(&gettext("Select a commit to view its changes")),
                },

                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 = "meta_box"]
                        gtk::Box {
                            set_orientation: gtk::Orientation::Vertical,
                            add_css_class: "gap-m",
                            add_css_class: "card",
                            add_css_class: "mb-m",

                            #[name = "summary_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "title-3",
                                set_wrap: true,
                                set_wrap_mode: gtk::pango::WrapMode::Word,
                                add_css_class: "mt-l",
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_use_markup: false,
                            },

                            #[name = "body_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                set_wrap: true,
                                set_wrap_mode: gtk::pango::WrapMode::Word,
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_visible: false,
                                set_use_markup: false,
                            },

                            #[name = "oid_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "monospace",
                                add_css_class: "dim-label",
                                set_selectable: true,
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_use_markup: false,
                            },

                            #[name = "parents_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "monospace",
                                add_css_class: "dim-label",
                                set_selectable: true,
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_use_markup: false,
                                set_visible: false,
                            },

                            #[name = "author_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "dim-label",
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_use_markup: false,
                            },

                            #[name = "committer_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "dim-label",
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                set_use_markup: false,
                                set_visible: false,
                            },

                            #[name = "stats_label"]
                            gtk::Label {
                                set_halign: gtk::Align::Start,
                                add_css_class: "dim-label",
                                add_css_class: "ms-l",
                                add_css_class: "me-l",
                                add_css_class: "mb-l",
                                set_use_markup: false,
                                set_visible: false,
                            },
                        },

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

                set_visible_child_name: "empty",
            },
        }
    }

    fn init(path: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let list_view = CommitListModel::new();
        let drag_state = Rc::new(RefCell::new(DragState::default()));
        let diff_factory = FactoryVecDeque::builder().launch(gtk::Box::default()).detach();
        let branch_info_box = BranchInfoBox::builder().launch(()).detach();
        let tag_info_box = TagInfoBox::builder().launch(()).detach();
        let search_bar = Self::build_search_bar(path.clone(), &sender);
        let (squash_action, fixup_action, reword_action, drop_action, edit_action) = Self::build_history_actions();

        let model = CommitLogView {
            path,
            list_view,
            drag_state: drag_state.clone(),
            branch_info_box,
            tag_info_box,
            diff_factory,
            current_ref: None,
            current_kind: None,
            current_is_head: false,
            current_is_detached: false,
            last_oid: None,
            has_more: false,
            pending_load: false,
            loading: false,
            generation: 0,
            diff_generation: 0,
            tag_map: HashMap::new(),
            branch_map: HashMap::new(),
            upstream_ref: None,
            tip_oid: None,
            selected_oid: None,
            context_commit_oid: None,
            pending_select_range: None,
            search_bar,
            squash_action,
            fixup_action,
            reword_action,
            drop_action,
            edit_action,
            rewriteable_oids: HashSet::new(),
        };

        model.install_selection_listener(&sender);

        let diff_list = model.diff_factory.widget();
        let branch_info_widget = model.branch_info_box.widget();
        let tag_info_widget = model.tag_info_box.widget();
        let search_bar_widget = model.search_bar.widget();

        let widgets = view_output!();

        let factory = build_list_factory(drag_state, model.list_view.clone(), sender.input_sender().clone());
        widgets.commits_list_view.set_factory(Some(&factory));

        model.install_context_menu(&widgets.commits_list_view, &sender);
        Self::install_infinite_scroll(&widgets.commits_scroll, &sender);
        Self::install_unmap_clear(root.upcast_ref::<gtk::Widget>(), &sender);

        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 {
            CommitLogViewMsg::Reference(action) => self.handle_reference_action(action, widgets, &sender),

            CommitLogViewMsg::Pagination(action) => self.handle_pagination_action(action, &sender),

            CommitLogViewMsg::ContextMenu(action) => self.handle_context_menu_action(action, &sender),

            CommitLogViewMsg::Search(action) => self.handle_search_action(action, widgets, &sender),

            CommitLogViewMsg::Selection(action) => self.handle_selection_action(action, widgets, &sender),

            CommitLogViewMsg::ShowToast(msg) => {
                sender.output(CommitLogViewOutput::ShowToast(msg)).ok();
            }

            CommitLogViewMsg::ReorderFailed => {
                self.pending_select_range = None;
            }

            CommitLogViewMsg::ReorderDrop {
                dragged_oid,
                target_oid,
            } => {
                self.handle_reorder_drop(&dragged_oid, &target_oid, &sender);
            }

            CommitLogViewMsg::RewriteableOidsLoaded(set) => {
                self.rewriteable_oids = set;
                self.update_history_actions_enabled();
            }
        }

        self.update_view(widgets, sender);
    }

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

impl CommitLogView {
    fn selected_positions(&self) -> Vec<u32> {
        let bitset = self.list_view.selection_model.selection();
        let Some((iter, first)) = gtk::BitsetIter::init_first(&bitset) else {
            return Vec::new();
        };
        let mut result = vec![first];
        result.extend(iter);
        result
    }

    fn selected_oids(&self) -> Vec<String> {
        self.selected_positions()
            .into_iter()
            .filter_map(|pos| {
                self.list_view
                    .get(pos)
                    .map(|obj| obj.borrow::<CommitItem>().commit.oid.clone())
            })
            .collect()
    }

    pub(super) fn update_history_actions_enabled(&self) {
        if !self.current_is_head {
            self.squash_action.set_enabled(false);
            self.fixup_action.set_enabled(false);
            self.reword_action.set_enabled(false);
            self.drop_action.set_enabled(false);
            self.edit_action.set_enabled(false);

            return;
        }

        let selected = self.selected_oids();

        let squash_ok = selected.len() >= 2 && selected.iter().all(|o| self.rewriteable_oids.contains(o));
        self.squash_action.set_enabled(squash_ok);
        self.fixup_action.set_enabled(squash_ok);

        let single_ok = self
            .context_commit_oid
            .as_deref()
            .map(|o| self.rewriteable_oids.contains(o))
            .unwrap_or(false);

        self.reword_action.set_enabled(single_ok);
        self.drop_action.set_enabled(single_ok);
        self.edit_action.set_enabled(single_ok);
    }

    fn start_load(
        &mut self,
        widgets: &mut <Self as Component>::Widgets,
        ref_name: &str,
        sender: &ComponentSender<Self>,
    ) {
        self.generation = self.generation.wrapping_add(1);
        self.diff_generation = self.diff_generation.wrapping_add(1);
        self.current_ref = Some(ref_name.to_string());
        self.last_oid = None;
        self.has_more = false;
        self.pending_load = true;
        self.loading = false;
        self.selected_oid = None;
        self.context_commit_oid = None;
        self.list_view.clear();

        sender.output(CommitLogViewOutput::SelectedCommitChanged(None)).ok();

        widgets.detail_stack.set_visible_child_name("empty");
        self.diff_factory.guard().clear();

        let generation = self.generation;
        let p = self.path.clone();
        let ref_name = ref_name.to_string();
        let kind = self.current_kind;

        sender.spawn_oneshot_command(move || {
            let page = git::load_commits_page(&p, &ref_name, None, PAGE_SIZE);
            let tag_map = git::build_tag_oid_map(&p).unwrap_or_default();
            let branch_map = git::build_branch_oid_map(&p).unwrap_or_default();
            let upstream_ref = kind.and_then(|k| git::resolve_upstream_oid(&p, &ref_name, k));

            CommitLogViewMsg::Pagination(PaginationAction::PageLoaded {
                generation,
                is_first: true,
                page,
                tag_map: Some(tag_map),
                branch_map: Some(branch_map),
                upstream_ref: Some(upstream_ref),
            })
        });

        Self::schedule_spinner(sender, generation);
    }
}

fn build_list_factory(
    drag_state: Rc<RefCell<DragState>>,
    list_view: CommitListModel,
    sender: relm4::Sender<CommitLogViewMsg>,
) -> gtk::SignalListItemFactory {
    let factory = gtk::SignalListItemFactory::new();

    factory.connect_setup(move |_factory, item| {
        let Some(list_item) = item.downcast_ref::<gtk::ListItem>() else {
            return;
        };

        let (root, widgets) = build_commit_row();
        attach_drag_source(&root, drag_state.clone(), list_view.clone(), sender.clone());
        attach_drop_target(&root, drag_state.clone(), sender.clone());
        list_item.set_child(Some(&root));

        let widgets = Rc::new(widgets);
        list_item.connect_item_notify(move |li| {
            let Some(obj) = li.item().and_downcast::<BoxedAnyObject>() else {
                return;
            };
            let commit_item = obj.borrow::<CommitItem>();
            bind_commit_row(&commit_item, &widgets, &root);
        });
    });

    factory
}

fn build_commit_row() -> (gtk::Box, CommitItemWidgets) {
    let root = gtk::Box::new(gtk::Orientation::Vertical, 2);
    root.add_css_class("commit-item");

    let summary = gtk::Label::new(None);
    summary.set_halign(gtk::Align::Start);
    summary.set_ellipsize(gtk::pango::EllipsizeMode::End);
    summary.set_xalign(0.0);
    summary.set_use_markup(false);

    let subtitle = gtk::Label::new(None);
    subtitle.set_halign(gtk::Align::Start);
    subtitle.add_css_class("dim-label");
    subtitle.add_css_class("caption");
    subtitle.set_ellipsize(gtk::pango::EllipsizeMode::End);
    subtitle.set_xalign(0.0);
    subtitle.set_use_markup(false);

    let badge_row = gtk::Box::new(gtk::Orientation::Horizontal, 6);
    badge_row.set_visible(false);

    let tag_icon = gtk::Image::from_icon_name("tag-symbolic");
    tag_icon.set_pixel_size(12);
    tag_icon.set_visible(false);
    tag_icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let tag_label = gtk::Label::new(None);
    tag_label.add_css_class("badge");
    tag_label.add_css_class("tag-badge");
    tag_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
    tag_label.set_visible(false);

    let local_icon = gtk::Image::from_icon_name("branch-symbolic");
    local_icon.set_pixel_size(12);
    local_icon.set_visible(false);
    local_icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let local_label = gtk::Label::new(None);
    local_label.add_css_class("badge");
    local_label.add_css_class("branch-badge");
    local_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
    local_label.set_visible(false);

    let remote_icon = gtk::Image::from_icon_name("branch-symbolic");
    remote_icon.set_pixel_size(12);
    remote_icon.set_visible(false);
    remote_icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let remote_label = gtk::Label::new(None);
    remote_label.add_css_class("badge");
    remote_label.add_css_class("remote-branch-badge");
    remote_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
    remote_label.set_visible(false);

    let upstream_icon = gtk::Image::from_icon_name("branch-symbolic");
    upstream_icon.set_pixel_size(12);
    upstream_icon.set_visible(false);
    upstream_icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let upstream_label = gtk::Label::new(None);
    upstream_label.add_css_class("badge");
    upstream_label.add_css_class("upstream-badge");
    upstream_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
    upstream_label.set_visible(false);

    let badge_spacer = gtk::Box::new(gtk::Orientation::Horizontal, 0);
    badge_spacer.set_hexpand(true);

    badge_row.append(&tag_icon);
    badge_row.append(&tag_label);
    badge_row.append(&local_icon);
    badge_row.append(&local_label);
    badge_row.append(&badge_spacer);
    badge_row.append(&remote_icon);
    badge_row.append(&remote_label);
    badge_row.append(&upstream_icon);
    badge_row.append(&upstream_label);

    root.append(&summary);
    root.append(&subtitle);
    root.append(&badge_row);

    (
        root,
        CommitItemWidgets {
            summary,
            subtitle,
            tag_icon,
            tag_label,
            local_icon,
            local_label,
            remote_icon,
            remote_label,
            upstream_icon,
            upstream_label,
            badge_row,
        },
    )
}

fn bind_commit_row(item: &CommitItem, widgets: &CommitItemWidgets, root: &gtk::Box) {
    root.set_widget_name(&item.commit.oid);

    widgets.summary.set_label(&item.commit.summary);
    widgets
        .subtitle
        .set_label(&format!("{} · {}", item.commit.author, ui_helpers::format_time(item.commit.time)));

    let has_tags = !item.tags.is_empty();
    widgets.tag_icon.set_visible(has_tags);
    widgets.tag_label.set_visible(has_tags);
    if has_tags {
        widgets.tag_label.set_label(&item.tags.join(", "));
    }

    let has_local = !item.local_branches.is_empty();
    widgets.local_icon.set_visible(has_local);
    widgets.local_label.set_visible(has_local);
    if has_local {
        widgets.local_label.set_label(&item.local_branches.join(", "));
    }

    let has_remote = !item.remote_branches.is_empty();
    widgets.remote_icon.set_visible(has_remote);
    widgets.remote_label.set_visible(has_remote);
    if has_remote {
        widgets.remote_label.set_label(&item.remote_branches.join(", "));
    }

    let has_upstream = item.upstream_name.is_some();
    widgets.upstream_icon.set_visible(has_upstream);
    widgets.upstream_label.set_visible(has_upstream);
    if let Some(ref name) = item.upstream_name {
        widgets.upstream_label.set_label(name);
    }

    let has_badges = has_tags || has_local || has_remote || has_upstream;
    widgets.badge_row.set_visible(has_badges);
}
