/* repo_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::Cell;
use std::path::PathBuf;

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

use crate::commit_log_view::{CommitLogView, CommitLogViewMsg, CommitLogViewOutput, ReferenceAction, SearchAction};
use crate::commit_message_dialog::CommitMessageDialog;
use crate::confirm_dialog::ConfirmDialog;
use crate::create_tag_dialog::CreateTagDialog;
use crate::merge_dialog::MergeDialog;
use crate::pull_dialog::PullDialog;
use crate::push_dialog::PushDialog;
use crate::select_remote_dialog::SelectRemoteDialog;
use crate::stash_switch_dialog::StashSwitchDialog;
use crate::stashes_view::{StashesView, StashesViewMsg, StashesViewOutput};
use crate::tag_list_view::{TagListView, TagListViewMsg, TagListViewOutput};
use crate::text_prompt_dialog::TextPromptDialog;
use crate::tree_list_view::{TreeListView, TreeListViewInit, TreeListViewKind, TreeListViewMsg, TreeListViewOutput};
use crate::working_copy_view::{
    CommitAction as WorkingCopyCommitAction, FilterAction as WorkingCopyFilterAction,
    RefreshAction as WorkingCopyRefreshAction, WorkingCopyView, WorkingCopyViewMsg, WorkingCopyViewOutput,
};
use crate::{git, preferences, tree_node, ui_helpers};

mod branch;
mod detached_checkout;
mod drop;
mod edit_commit;
mod fetch;
mod pull;
mod push;
mod rebase;
mod reorder;
mod reword;
mod rewrite_helpers;
mod squash;
mod stash;
mod tag;

pub use drop::DropAction;
pub use edit_commit::EditCommitAction;
pub use reorder::ReorderAction;
pub use reword::RewordAction;
pub use squash::SquashAction;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SidebarSection {
    Workspace,
    Branches,
    Tags,
    Remotes,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceTab {
    WorkingCopy = 0,
    Stashes = 1,
    CurrentBranch = 2,
}

impl WorkspaceTab {
    fn from_index(index: i32) -> Option<Self> {
        match index {
            0 => Some(Self::WorkingCopy),
            1 => Some(Self::Stashes),
            2 => Some(Self::CurrentBranch),
            _ => None,
        }
    }
}

#[derive(Debug, Clone)]
enum SelectedRef {
    LocalBranch(String),
    RemoteBranch(String),
    Tag(String),
}

pub struct RepoView {
    path: PathBuf,
    head_branch: Option<String>,
    head_kind: git::HeadKind,
    head_upstream: Option<git::HeadUpstreamStatus>,
    selected_ref: Option<SelectedRef>,
    stash_count: usize,
    changed_file_count: usize,
    workspace_expanded: bool,
    branches_expanded: bool,
    tags_expanded: bool,
    remotes_expanded: bool,
    sidebar_visible: bool,
    working_copy_view: Controller<WorkingCopyView>,
    stashes_view: Controller<StashesView>,
    commit_log_view: Controller<CommitLogView>,
    branches_view: Controller<TreeListView>,
    tag_list_view: Controller<TagListView>,
    remotes_view: Controller<TreeListView>,
    last_branches: Vec<git::BranchEntry>,
    last_head_branch: Option<String>,
    last_head_kind: git::HeadKind,
    last_remotes: Vec<(String, Vec<String>)>,
    remote_op_in_progress: bool,
    auto_fetch_source_id: Option<glib::SourceId>,
    settings: gio::Settings,
    global_settings: gio::Settings,
    selected_commit_oid: Option<String>,
    create_tag_dialog: Option<Controller<CreateTagDialog>>,
    select_remote_dialog: Option<Controller<SelectRemoteDialog>>,
    pull_dialog: Option<Controller<PullDialog>>,
    push_dialog: Option<Controller<PushDialog>>,
    merge_dialog: Option<Controller<MergeDialog>>,
    stash_switch_dialog: Option<Controller<StashSwitchDialog>>,
    confirm_dialog: Option<Controller<ConfirmDialog>>,
    text_prompt_dialog: Option<Controller<TextPromptDialog>>,
    commit_message_dialog: Option<Controller<CommitMessageDialog>>,
}

#[derive(Debug)]
pub enum RepoViewMsg {
    Refresh,
    WorkspaceSelected(WorkspaceTab),
    BranchSelected(String),
    TagSelected(String),
    RemoteBranchSelected(String),
    ToggleSection(SidebarSection),
    ToggleSidebar,
    GitDataLoaded(Result<git::GitData, String>),
    FileCountChanged(usize),
    ShowToast(String),
    Branch(BranchAction),
    Tag(TagAction),
    DetachedCheckout(DetachedCheckoutAction),
    SelectedCommitChanged(Option<String>),
    FocusCommit,
    Stash,
    StashWithUntracked,
    StashPop,
    StashComplete(Result<(), String>, String),
    StashCountChanged(usize),
    Push,
    PushDefaultsLoaded(Result<git::PushDefaults, String>),
    PushDialogDismissed,
    PushConfirmed(git::PushSettings),
    PushComplete(Result<(), String>),
    Pull,
    PullDefaultsLoaded(Result<git::PullDefaults, String>),
    PullDialogDismissed,
    PullConfirmed(git::PullSettings),
    PullComplete {
        result: Result<git::PullOutcome, String>,
        settings: git::PullSettings,
        stashed: bool,
    },
    FetchAll,
    AutoFetchTick,
    AutoFetchComplete(Result<(), String>),
    AutoFetchSettingsChanged,
    FetchAllComplete(Result<(), String>),
    RebaseContinue,
    RebaseContinueComplete(Result<git::RebaseOutcome, String>),
    MergeTodoComplete(Result<git::TodoOutcome, String>),
    RebaseAbort,
    RebaseAbortComplete {
        result: Result<(), String>,
        was_pending_merge: bool,
    },
    ToggleSearch,
    ArchiveRequested(String),
    ArchiveComplete(Result<PathBuf, String>),
    CreateBranch,
    Squash(SquashAction),
    Reorder(ReorderAction),
    Reword(RewordAction),
    Drop(DropAction),
    EditCommit(EditCommitAction),
}

#[derive(Debug)]
pub enum RepoViewOutput {
    StashCountChanged(usize),
    HeadBranchChanged(Option<String>, git::HeadKind),
    RemoteOpStarted,
    RemoteOpFinished,
    SidebarVisibilityChanged(bool),
    ActiveWorkspaceTabChanged(Option<WorkspaceTab>),
}

#[derive(Debug)]
pub enum BranchAction {
    SwitchRequested(String, git::BranchKind),
    DeleteRequested(String, git::BranchKind),
    DeleteConfirmed(String, git::BranchKind),
    RenameRequested(String),
    RenameConfirmed {
        old_name: String,
        new_name: String,
    },
    CreateFromRequested(String),
    CreateFromConfirmed {
        new_name: String,
        base_ref: String,
    },
    CopyName(String),
    DirtyCheckComplete {
        name: String,
        kind: git::BranchKind,
        summary: git::DirtySummary,
    },
    DeleteMergeCheckComplete {
        name: String,
        kind: git::BranchKind,
        merged: bool,
    },
    StashSwitchDialogDismissed,
    ConfirmDialogDismissed,
    TextPromptDismissed,
    SwitchWithoutStash(String, git::BranchKind),
    StashAndSwitch(String, git::BranchKind, git::StashPushFlags),
    OperationComplete(Result<(), String>, String),
    CheckoutComplete(Result<git::CheckoutOutcome, String>),
    PushBranchRequested(String),
    PushBranchDefaultsLoaded(Result<git::PushDefaults, String>),
    FetchRequested(String, git::BranchKind),
    FetchComplete(Result<(), String>),
    MergeRequested(String, git::BranchKind),
    MergeDirtyCheckComplete {
        name: String,
        kind: git::BranchKind,
        summary: git::DirtySummary,
    },
    MergeDialogDismissed,
    MergeConfirmed {
        source_ref: String,
        source_kind: git::BranchKind,
        strategy: git::MergeStrategy,
        stash: Option<git::StashPushFlags>,
        squash_message: Option<String>,
    },
    MergeComplete {
        result: Result<git::MergeOutcome, String>,
        source_ref: String,
    },
}

#[derive(Debug)]
pub enum TagAction {
    CreateRequested,
    CreateFromCommit(String),
    CreateDialogDismissed,
    CreateConfirmed {
        name: String,
        target: String,
        message: Option<String>,
    },
    ConfirmDialogDismissed,
    DeleteRequested(String),
    DeleteConfirmed(String),
    PushRequested(String),
    PushRemotesLoaded {
        tag_name: String,
        remotes: Vec<String>,
    },
    PushDialogDismissed,
    PushConfirmed {
        tag_name: String,
        remote: String,
    },
    OperationComplete(Result<(), String>, Option<String>),
}

#[derive(Debug)]
pub enum DetachedCheckoutAction {
    CheckoutRequested(String),
    DirtyCheckComplete { target: String, summary: git::DirtySummary },
    StashSwitchDialogDismissed,
    CheckoutWithoutStash(String),
    StashAndCheckout(String, git::StashPushFlags),
    CheckoutComplete(Result<(), String>),
}

fn map_tree_output(
    kind: git::BranchKind,
    selected: fn(String) -> RepoViewMsg,
) -> impl Fn(TreeListViewOutput) -> RepoViewMsg + 'static {
    move |output| match output {
        TreeListViewOutput::ItemSelected(name) => selected(name),
        TreeListViewOutput::SwitchRequested(name) => RepoViewMsg::Branch(BranchAction::SwitchRequested(name, kind)),
        TreeListViewOutput::DeleteRequested(name) => RepoViewMsg::Branch(BranchAction::DeleteRequested(name, kind)),
        TreeListViewOutput::RenameRequested(name) => RepoViewMsg::Branch(BranchAction::RenameRequested(name)),
        TreeListViewOutput::CopyNameRequested(name) => RepoViewMsg::Branch(BranchAction::CopyName(name)),
        TreeListViewOutput::PushRequested(name) => RepoViewMsg::Branch(BranchAction::PushBranchRequested(name)),
        TreeListViewOutput::FetchRequested(name) => RepoViewMsg::Branch(BranchAction::FetchRequested(name, kind)),
        TreeListViewOutput::ArchiveRequested(name) => RepoViewMsg::ArchiveRequested(name),
        TreeListViewOutput::MergeRequested(name) => RepoViewMsg::Branch(BranchAction::MergeRequested(name, kind)),

        TreeListViewOutput::CreateBranchFromRequested(name) => {
            RepoViewMsg::Branch(BranchAction::CreateFromRequested(name))
        }
    }
}

#[relm4::component(pub)]
impl Component for RepoView {
    type Init = (PathBuf, String);
    type Input = RepoViewMsg;
    type Output = RepoViewOutput;
    type CommandOutput = RepoViewMsg;

    view! {
        adw::ToastOverlay {
            #[name = "sidebar_paned"]
            #[wrap(Some)]
            set_child = &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: 140,
                    #[watch]
                    set_visible: model.sidebar_visible,

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

                        gtk::Button {
                            add_css_class: "flat",
                            #[watch]
                            set_tooltip_text: Some(&if model.workspace_expanded {
                                gettext("Collapse Workspace")
                            } else {
                                gettext("Expand Workspace")
                            }),
                            #[watch]
                            update_state: &[gtk::accessible::State::Expanded(Some(model.workspace_expanded))],
                            connect_clicked[sender] => move |_| {
                                sender.input(RepoViewMsg::ToggleSection(SidebarSection::Workspace));
                            },

                            #[wrap(Some)]
                            set_child = &gtk::Box {
                                set_orientation: gtk::Orientation::Horizontal,
                                add_css_class: "gap-m",

                                gtk::Image {
                                    #[watch]
                                    set_icon_name: Some(if model.workspace_expanded { "pan-down-symbolic" } else { "pan-end-symbolic" }),
                                },

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

                        #[name = "workspace_list"]
                        gtk::ListBox {
                            #[watch]
                            set_visible: model.workspace_expanded,
                            set_selection_mode: gtk::SelectionMode::Single,
                            add_css_class: "boxed-list",

                            adw::ActionRow {
                                set_title: &gettext("Working Copy"),
                                add_prefix = &gtk::Image {
                                    set_icon_name: Some("working-copy-symbolic"),
                                },
                                add_suffix = &gtk::Label {
                                    #[watch]
                                    set_label: &if model.changed_file_count > 0 {
                                        model.changed_file_count.to_string()
                                    } else {
                                        String::new()
                                    },
                                    #[watch]
                                    set_visible: model.changed_file_count > 0,
                                    add_css_class: "badge",
                                },
                            },

                            adw::ActionRow {
                                set_title: &gettext("Stashes"),
                                add_prefix = &gtk::Image {
                                    set_icon_name: Some("stash-symbolic"),
                                },
                                add_suffix = &gtk::Label {
                                    #[watch]
                                    set_label: &if model.stash_count > 0 {
                                        model.stash_count.to_string()
                                    } else {
                                        String::new()
                                    },
                                    #[watch]
                                    set_visible: model.stash_count > 0,
                                    add_css_class: "badge",
                                },
                            },

                            adw::ActionRow {
                                #[watch]
                                set_title: model.head_branch.as_deref().unwrap_or("HEAD"),
                                add_prefix = &gtk::Image {
                                    #[watch]
                                    set_icon_name: Some(match model.head_kind {
                                        git::HeadKind::Branch => "branch-symbolic",
                                        git::HeadKind::Tag => "tag-symbolic",
                                        git::HeadKind::Detached => "commit-symbolic",
                                    }),
                                },
                                add_suffix = &gtk::Label {
                                    #[watch]
                                    set_label: &model.head_upstream
                                        .map(|u| format!("↓{} ↑{}", u.behind, u.ahead))
                                        .unwrap_or_default(),
                                    #[watch]
                                    set_visible: model.head_upstream
                                        .is_some_and(|u| u.ahead > 0 || u.behind > 0),
                                    set_valign: gtk::Align::Center,
                                    add_css_class: "badge",
                                    add_css_class: "numeric",
                                },
                            },
                        },

                        gtk::Button {
                            add_css_class: "flat",
                            #[watch]
                            set_tooltip_text: Some(&if model.branches_expanded {
                                gettext("Collapse Branches")
                            } else {
                                gettext("Expand Branches")
                            }),
                            #[watch]
                            update_state: &[gtk::accessible::State::Expanded(Some(model.branches_expanded))],
                            connect_clicked[sender] => move |_| {
                                sender.input(RepoViewMsg::ToggleSection(SidebarSection::Branches));
                            },

                            #[wrap(Some)]
                            set_child = &gtk::Box {
                                set_orientation: gtk::Orientation::Horizontal,
                                add_css_class: "gap-m",

                                gtk::Image {
                                    #[watch]
                                    set_icon_name: Some(if model.branches_expanded { "pan-down-symbolic" } else { "pan-end-symbolic" }),
                                },

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

                        #[local_ref]
                        branch_list_widget -> gtk::Box {
                            #[watch]
                            set_visible: model.branches_expanded,
                        },

                        gtk::Button {
                            add_css_class: "flat",
                            #[watch]
                            set_tooltip_text: Some(&if model.tags_expanded {
                                gettext("Collapse Tags")
                            } else {
                                gettext("Expand Tags")
                            }),
                            #[watch]
                            update_state: &[gtk::accessible::State::Expanded(Some(model.tags_expanded))],
                            connect_clicked[sender] => move |_| {
                                sender.input(RepoViewMsg::ToggleSection(SidebarSection::Tags));
                            },

                            #[wrap(Some)]
                            set_child = &gtk::Box {
                                set_orientation: gtk::Orientation::Horizontal,
                                add_css_class: "gap-m",

                                gtk::Image {
                                    #[watch]
                                    set_icon_name: Some(if model.tags_expanded { "pan-down-symbolic" } else { "pan-end-symbolic" }),
                                },

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

                        #[local_ref]
                        tag_list_widget -> gtk::Box {
                            #[watch]
                            set_visible: model.tags_expanded,
                        },

                        gtk::Button {
                            add_css_class: "flat",
                            #[watch]
                            set_tooltip_text: Some(&if model.remotes_expanded {
                                gettext("Collapse Remotes")
                            } else {
                                gettext("Expand Remotes")
                            }),
                            #[watch]
                            update_state: &[gtk::accessible::State::Expanded(Some(model.remotes_expanded))],
                            connect_clicked[sender] => move |_| {
                                sender.input(RepoViewMsg::ToggleSection(SidebarSection::Remotes));
                            },

                            #[wrap(Some)]
                            set_child = &gtk::Box {
                                set_orientation: gtk::Orientation::Horizontal,
                                add_css_class: "gap-m",

                                gtk::Image {
                                    #[watch]
                                    set_icon_name: Some(if model.remotes_expanded { "pan-down-symbolic" } else { "pan-end-symbolic" }),
                                },

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

                        #[local_ref]
                        remote_list_widget -> gtk::Box {
                            #[watch]
                            set_visible: model.remotes_expanded,
                        },
                    },
                },

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

                    add_named[Some("working-copy")] = model.working_copy_view.widget(),
                    add_named[Some("stashes")] = model.stashes_view.widget(),
                    add_named[Some("commit-log")] = model.commit_log_view.widget(),
                },
            },
        }
    }

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

        let settings = preferences::repo_settings(&repo_id);
        let workspace_expanded = settings.boolean("workspace-expanded");
        let branches_expanded = settings.boolean("branches-expanded");
        let tags_expanded = settings.boolean("tags-expanded");
        let remotes_expanded = settings.boolean("remotes-expanded");
        let sidebar_visible = preferences::app_settings().boolean("sidebar-visible");

        let working_copy_view = WorkingCopyView::builder()
            .launch((path.clone(), repo_id.clone()))
            .forward(sender.input_sender(), |output| match output {
                WorkingCopyViewOutput::ShowToast(msg) => RepoViewMsg::ShowToast(msg),
                WorkingCopyViewOutput::DataChanged => RepoViewMsg::Refresh,
                WorkingCopyViewOutput::FileCountChanged(count) => RepoViewMsg::FileCountChanged(count),
                WorkingCopyViewOutput::RebaseContinue => RepoViewMsg::RebaseContinue,
                WorkingCopyViewOutput::RebaseAbort => RepoViewMsg::RebaseAbort,
            });

        let stashes_view = StashesView::builder().launch((path.clone(), vec![])).forward(
            sender.input_sender(),
            |output| match output {
                StashesViewOutput::ShowToast(msg) => RepoViewMsg::ShowToast(msg),
                StashesViewOutput::DataChanged => RepoViewMsg::Refresh,
                StashesViewOutput::StashCountChanged(count) => RepoViewMsg::StashCountChanged(count),
            },
        );

        let branches_view = TreeListView::builder()
            .launch(TreeListViewInit {
                repo_id: repo_id.clone(),
                settings_key: "collapsed-branch-groups",
                empty_label: gettext("No branches"),
                top_level_collapsed: false,
                kind: TreeListViewKind::LocalBranches,
            })
            .forward(sender.input_sender(), map_tree_output(git::BranchKind::Local, RepoViewMsg::BranchSelected));

        let commit_log_view = CommitLogView::builder()
            .launch(path.clone())
            .forward(sender.input_sender(), |output| match output {
                CommitLogViewOutput::ShowToast(msg) => RepoViewMsg::ShowToast(msg),
                CommitLogViewOutput::CreateTagRequested(oid) => RepoViewMsg::Tag(TagAction::CreateFromCommit(oid)),
                CommitLogViewOutput::SelectedCommitChanged(oid) => RepoViewMsg::SelectedCommitChanged(oid),
                CommitLogViewOutput::ArchiveRequested(oid) => RepoViewMsg::ArchiveRequested(oid),

                CommitLogViewOutput::CheckoutRequested(oid) => {
                    RepoViewMsg::DetachedCheckout(DetachedCheckoutAction::CheckoutRequested(oid))
                }

                CommitLogViewOutput::SquashRequested { oids, mode } => {
                    RepoViewMsg::Squash(SquashAction::Requested { oids, mode })
                }

                CommitLogViewOutput::ReorderRequested { new_order } => {
                    RepoViewMsg::Reorder(ReorderAction::Requested { new_order })
                }

                CommitLogViewOutput::RewordRequested(oid) => RepoViewMsg::Reword(RewordAction::Requested { oid }),

                CommitLogViewOutput::DropRequested(oid) => RepoViewMsg::Drop(DropAction::Requested { oid }),

                CommitLogViewOutput::EditRequested(oid) => RepoViewMsg::EditCommit(EditCommitAction::Requested { oid }),
            });

        let tag_list_view = TagListView::builder()
            .launch(())
            .forward(sender.input_sender(), |output| match output {
                TagListViewOutput::TagSelected(name) => RepoViewMsg::TagSelected(name),
                TagListViewOutput::DeleteRequested(name) => RepoViewMsg::Tag(TagAction::DeleteRequested(name)),
                TagListViewOutput::PushRequested(name) => RepoViewMsg::Tag(TagAction::PushRequested(name)),
                TagListViewOutput::ArchiveRequested(name) => RepoViewMsg::ArchiveRequested(name),
                TagListViewOutput::CreateBranchRequested(name) => {
                    RepoViewMsg::Branch(BranchAction::CreateFromRequested(name))
                }

                TagListViewOutput::CheckoutRequested(name) => {
                    RepoViewMsg::DetachedCheckout(DetachedCheckoutAction::CheckoutRequested(name))
                }
            });

        let remotes_view = TreeListView::builder()
            .launch(TreeListViewInit {
                repo_id: repo_id.clone(),
                settings_key: "expanded-remote-groups",
                empty_label: gettext("No remotes"),
                top_level_collapsed: true,
                kind: TreeListViewKind::RemoteBranches,
            })
            .forward(
                sender.input_sender(),
                map_tree_output(git::BranchKind::Remote, RepoViewMsg::RemoteBranchSelected),
            );

        let mut model = RepoView {
            path: path.clone(),
            head_branch: None,
            head_kind: git::HeadKind::Detached,
            head_upstream: None,
            selected_ref: None,
            stash_count: 0,
            changed_file_count: 0,
            workspace_expanded,
            branches_expanded,
            tags_expanded,
            remotes_expanded,
            sidebar_visible,
            working_copy_view,
            stashes_view,
            commit_log_view,
            branches_view,
            tag_list_view,
            remotes_view,
            last_branches: Vec::new(),
            last_head_branch: None,
            last_head_kind: git::HeadKind::Detached,
            last_remotes: Vec::new(),
            remote_op_in_progress: false,
            auto_fetch_source_id: None,
            settings,
            global_settings: preferences::app_settings(),
            selected_commit_oid: None,
            create_tag_dialog: None,
            select_remote_dialog: None,
            pull_dialog: None,
            push_dialog: None,
            merge_dialog: None,
            stash_switch_dialog: None,
            confirm_dialog: None,
            text_prompt_dialog: None,
            commit_message_dialog: None,
        };

        let branch_list_widget = model.branches_view.widget();
        let tag_list_widget = model.tag_list_view.widget();
        let remote_list_widget = model.remotes_view.widget();

        let widgets = view_output!();

        // Connect sidebar list selections with mutual exclusion
        {
            let s = sender.input_sender().clone();
            widgets.workspace_list.connect_row_selected(move |_, row| {
                if let Some(row) = row
                    && let Some(tab) = WorkspaceTab::from_index(row.index())
                {
                    s.emit(RepoViewMsg::WorkspaceSelected(tab));
                }
            });
        }

        // Select "Working Copy" by default
        if let Some(row) = widgets.workspace_list.row_at_index(0) {
            widgets.workspace_list.select_row(Some(&row));
        }

        preferences::persist_paned_position(&widgets.sidebar_paned, "paned-sidebar-position");

        setup_window_focus_refresh(&root, &sender);

        model.restart_auto_fetch_timer(&sender);

        let s = sender.input_sender().clone();
        model.settings.connect_changed(Some("auto-fetch-mode"), move |_, _| {
            s.emit(RepoViewMsg::AutoFetchSettingsChanged);
        });

        let s = sender.input_sender().clone();
        model
            .settings
            .connect_changed(Some("auto-fetch-interval"), move |_, _| {
                s.emit(RepoViewMsg::AutoFetchSettingsChanged);
            });

        let s = sender.input_sender().clone();
        model
            .global_settings
            .connect_changed(Some("auto-fetch-enabled"), move |_, _| {
                s.emit(RepoViewMsg::AutoFetchSettingsChanged);
            });

        let s = sender.input_sender().clone();
        model
            .global_settings
            .connect_changed(Some("auto-fetch-interval"), move |_, _| {
                s.emit(RepoViewMsg::AutoFetchSettingsChanged);
            });

        let s = sender.input_sender().clone();
        model.settings.connect_changed(Some("tag-sort-method"), move |_, _| {
            s.emit(RepoViewMsg::Refresh);
        });

        let s = sender.input_sender().clone();
        model.settings.connect_changed(Some("tag-sort-direction"), move |_, _| {
            s.emit(RepoViewMsg::Refresh);
        });

        // Load git data asynchronously
        let p = path;
        let tag_sort = Self::tag_sort_config(&model.settings);
        sender.spawn_oneshot_command(move || RepoViewMsg::GitDataLoaded(git::load_git_data(&p, tag_sort)));

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        msg: Self::Input,
        sender: ComponentSender<Self>,
        root: &Self::Root,
    ) {
        match msg {
            RepoViewMsg::Refresh => {
                let p = self.path.clone();
                let tag_sort = Self::tag_sort_config(&self.settings);
                sender.spawn_oneshot_command(move || RepoViewMsg::GitDataLoaded(git::load_git_data(&p, tag_sort)));
                self.working_copy_view
                    .emit(WorkingCopyViewMsg::Refresh(WorkingCopyRefreshAction::Refresh));
                self.commit_log_view
                    .emit(CommitLogViewMsg::Reference(ReferenceAction::Refresh));
            }

            RepoViewMsg::ToggleSearch => match widgets.content_stack.visible_child_name().as_deref() {
                Some("commit-log") => self
                    .commit_log_view
                    .emit(CommitLogViewMsg::Search(SearchAction::ToggleSearch)),
                Some("working-copy") => self
                    .working_copy_view
                    .emit(WorkingCopyViewMsg::Filter(WorkingCopyFilterAction::Focus)),
                _ => {}
            },

            RepoViewMsg::ArchiveRequested(rev_spec) => {
                let dialog = gtk::FileDialog::builder()
                    .title(gettext("Create Archive"))
                    .modal(true)
                    .build();

                let filters = gio::ListStore::new::<gtk::FileFilter>();

                let tar_gz_filter = gtk::FileFilter::new();
                tar_gz_filter.set_name(Some("Tar Gzip (.tar.gz)"));
                tar_gz_filter.add_pattern("*.tar.gz");
                filters.append(&tar_gz_filter);

                let tar_xz_filter = gtk::FileFilter::new();
                tar_xz_filter.set_name(Some("Tar XZ (.tar.xz)"));
                tar_xz_filter.add_pattern("*.tar.xz");
                filters.append(&tar_xz_filter);

                let zip_filter = gtk::FileFilter::new();
                zip_filter.set_name(Some("Zip (.zip)"));
                zip_filter.add_pattern("*.zip");
                filters.append(&zip_filter);

                dialog.set_filters(Some(&filters));
                dialog.set_default_filter(Some(&tar_gz_filter));

                let repo_name = self.path.file_name().and_then(|n| n.to_str()).unwrap_or("archive");

                let short_rev = rev_spec.rsplit('/').next().unwrap_or(&rev_spec);

                let suggested = format!("{repo_name}-{short_rev}.tar.gz");
                dialog.set_initial_name(Some(&suggested));

                let window = root.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();
                let sender = sender.clone();
                let path = self.path.clone();

                dialog.save(window.as_ref(), None::<&gio::Cancellable>, move |result| {
                    if let Ok(file) = result
                        && let Some(dest) = file.path()
                    {
                        sender.spawn_oneshot_command(move || {
                            RepoViewMsg::ArchiveComplete(git::create_archive(&path, &rev_spec, &dest).map(|()| dest))
                        });
                    }
                });
            }

            RepoViewMsg::ArchiveComplete(result) => match result {
                Ok(dest_path) => {
                    let toast = adw::Toast::builder()
                        .title(gettext("Archive created successfully"))
                        .button_label(gettext("Show in File Manager"))
                        .build();

                    toast.connect_button_clicked(move |_| {
                        ui_helpers::open_in_file_manager(&dest_path);
                    });

                    root.add_toast(toast);
                }

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

            RepoViewMsg::CreateBranch => {
                let base_ref = match &self.selected_ref {
                    Some(SelectedRef::LocalBranch(name) | SelectedRef::RemoteBranch(name) | SelectedRef::Tag(name)) => {
                        name.clone()
                    }

                    None => match &self.head_branch {
                        Some(name) => name.clone(),
                        None => return,
                    },
                };

                sender.input(RepoViewMsg::Branch(BranchAction::CreateFromRequested(base_ref)));
            }

            RepoViewMsg::WorkspaceSelected(tab) => {
                self.selected_ref = None;
                self.deselect_all_sidebar_items(None);
                self.commit_log_view
                    .emit(CommitLogViewMsg::Search(SearchAction::CancelSearch));

                sender.output(RepoViewOutput::ActiveWorkspaceTabChanged(Some(tab))).ok();

                match tab {
                    WorkspaceTab::WorkingCopy => widgets.content_stack.set_visible_child_name("working-copy"),

                    WorkspaceTab::Stashes => {
                        self.stashes_view.emit(StashesViewMsg::Refresh);
                        widgets.content_stack.set_visible_child_name("stashes");
                    }

                    WorkspaceTab::CurrentBranch => {
                        if let Some(ref name) = self.head_branch {
                            match self.head_kind {
                                git::HeadKind::Branch => {
                                    self.commit_log_view.emit(CommitLogViewMsg::Reference(
                                        ReferenceAction::ShowBranch {
                                            name: name.clone(),
                                            kind: git::BranchKind::Local,
                                            is_head: true,
                                        },
                                    ));
                                }

                                git::HeadKind::Tag => {
                                    self.commit_log_view
                                        .emit(CommitLogViewMsg::Reference(ReferenceAction::ShowTag(name.clone())));
                                }

                                git::HeadKind::Detached => {
                                    self.commit_log_view.emit(CommitLogViewMsg::Reference(
                                        ReferenceAction::ShowDetachedHead(name.clone()),
                                    ));
                                }
                            }

                            widgets.content_stack.set_visible_child_name("commit-log");
                        }
                    }
                }
            }

            RepoViewMsg::BranchSelected(name) => {
                self.selected_ref = Some(SelectedRef::LocalBranch(name.clone()));
                widgets.workspace_list.select_row(gtk::ListBoxRow::NONE);
                self.deselect_all_sidebar_items(Some(SidebarSection::Branches));

                sender.output(RepoViewOutput::ActiveWorkspaceTabChanged(None)).ok();

                let is_head = self.head_branch.as_deref() == Some(name.as_str());
                self.commit_log_view
                    .emit(CommitLogViewMsg::Reference(ReferenceAction::ShowBranch {
                        name,
                        kind: git::BranchKind::Local,
                        is_head,
                    }));
                widgets.content_stack.set_visible_child_name("commit-log");
            }

            RepoViewMsg::TagSelected(name) => {
                self.selected_ref = Some(SelectedRef::Tag(name.clone()));
                widgets.workspace_list.select_row(gtk::ListBoxRow::NONE);
                self.deselect_all_sidebar_items(Some(SidebarSection::Tags));

                sender.output(RepoViewOutput::ActiveWorkspaceTabChanged(None)).ok();
                self.commit_log_view
                    .emit(CommitLogViewMsg::Reference(ReferenceAction::ShowTag(name)));

                widgets.content_stack.set_visible_child_name("commit-log");
            }

            RepoViewMsg::RemoteBranchSelected(name) => {
                self.selected_ref = Some(SelectedRef::RemoteBranch(name.clone()));
                widgets.workspace_list.select_row(gtk::ListBoxRow::NONE);
                self.deselect_all_sidebar_items(Some(SidebarSection::Remotes));
                sender.output(RepoViewOutput::ActiveWorkspaceTabChanged(None)).ok();

                self.commit_log_view
                    .emit(CommitLogViewMsg::Reference(ReferenceAction::ShowBranch {
                        name,
                        kind: git::BranchKind::Remote,
                        is_head: false,
                    }));

                widgets.content_stack.set_visible_child_name("commit-log");
            }

            RepoViewMsg::GitDataLoaded(result) => self.apply_git_data(result, root, &sender),

            RepoViewMsg::FileCountChanged(count) => {
                self.changed_file_count = count;
            }

            RepoViewMsg::StashCountChanged(count) => {
                sender.output(RepoViewOutput::StashCountChanged(count)).ok();
            }

            RepoViewMsg::ToggleSidebar => {
                self.sidebar_visible = !self.sidebar_visible;
                let _ = self
                    .global_settings
                    .set_boolean("sidebar-visible", self.sidebar_visible);

                sender
                    .output(RepoViewOutput::SidebarVisibilityChanged(self.sidebar_visible))
                    .ok();
            }

            RepoViewMsg::ToggleSection(section) => {
                let (field, key) = match section {
                    SidebarSection::Workspace => (&mut self.workspace_expanded, "workspace-expanded"),
                    SidebarSection::Branches => (&mut self.branches_expanded, "branches-expanded"),
                    SidebarSection::Tags => (&mut self.tags_expanded, "tags-expanded"),
                    SidebarSection::Remotes => (&mut self.remotes_expanded, "remotes-expanded"),
                };

                *field = !*field;

                let _ = self.settings.set_boolean(key, *field);
            }

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

            RepoViewMsg::FocusCommit => {
                self.working_copy_view
                    .emit(WorkingCopyViewMsg::Commit(WorkingCopyCommitAction::FocusCommit));
            }

            RepoViewMsg::Stash
            | RepoViewMsg::StashWithUntracked
            | RepoViewMsg::StashPop
            | RepoViewMsg::StashComplete(..) => {
                self.handle_stash_message(msg, &sender, root);
            }

            RepoViewMsg::Push
            | RepoViewMsg::PushDefaultsLoaded(_)
            | RepoViewMsg::PushDialogDismissed
            | RepoViewMsg::PushConfirmed(_)
            | RepoViewMsg::PushComplete(_) => {
                self.handle_push_message(msg, &sender, root);
            }

            RepoViewMsg::Pull
            | RepoViewMsg::PullDefaultsLoaded(_)
            | RepoViewMsg::PullDialogDismissed
            | RepoViewMsg::PullConfirmed(_)
            | RepoViewMsg::PullComplete { .. } => {
                self.handle_pull_message(msg, &sender, root);
            }

            RepoViewMsg::RebaseContinue
            | RepoViewMsg::RebaseContinueComplete(_)
            | RepoViewMsg::MergeTodoComplete(_)
            | RepoViewMsg::RebaseAbort
            | RepoViewMsg::RebaseAbortComplete { .. } => {
                self.handle_rebase_message(msg, &sender, root);
            }

            RepoViewMsg::FetchAll
            | RepoViewMsg::FetchAllComplete(_)
            | RepoViewMsg::AutoFetchTick
            | RepoViewMsg::AutoFetchComplete(_)
            | RepoViewMsg::AutoFetchSettingsChanged => {
                self.handle_fetch_message(msg, &sender, root);
            }

            RepoViewMsg::SelectedCommitChanged(oid) => {
                self.selected_commit_oid = oid;
            }

            RepoViewMsg::Branch(action) => self.handle_branch_action(action, &sender, root),

            RepoViewMsg::Tag(action) => self.handle_tag_action(action, &sender, root),

            RepoViewMsg::DetachedCheckout(action) => self.handle_detached_checkout(action, &sender, root),

            RepoViewMsg::Squash(action) => self.handle_squash_action(action, &sender, root),

            RepoViewMsg::Reorder(action) => self.handle_reorder_action(action, &sender, root),

            RepoViewMsg::Reword(action) => self.handle_reword_action(action, &sender, root),

            RepoViewMsg::Drop(action) => self.handle_drop_action(action, &sender, root),

            RepoViewMsg::EditCommit(action) => self.handle_edit_commit_action(action, &sender, root),
        }

        self.update_view(widgets, sender);
    }

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

impl RepoView {
    fn tag_sort_config(settings: &gio::Settings) -> git::TagSortConfig {
        git::TagSortConfig {
            method: settings.string("tag-sort-method").parse().unwrap_or_default(),
            direction: settings.string("tag-sort-direction").parse().unwrap_or_default(),
        }
    }

    fn apply_git_data(
        &mut self,
        result: Result<git::GitData, String>,
        root: &adw::ToastOverlay,
        sender: &ComponentSender<Self>,
    ) {
        let data = match result {
            Ok(data) => data,
            Err(msg) => {
                root.add_toast(adw::Toast::new(&msg));
                return;
            }
        };

        self.head_branch = data.head_branch.clone();
        self.head_kind = data.head_kind;
        self.head_upstream = data.head_upstream;
        self.stash_count = data.stashes.len();

        sender
            .output(RepoViewOutput::HeadBranchChanged(data.head_branch.clone(), data.head_kind))
            .ok();

        if data.branches != self.last_branches
            || data.head_branch != self.last_head_branch
            || data.head_kind != self.last_head_kind
        {
            self.last_branches = data.branches.clone();
            self.last_head_branch = data.head_branch.clone();
            self.last_head_kind = data.head_kind;

            let store = tree_node::build_branch_store(data.branches, data.head_branch.as_deref());

            self.branches_view.emit(TreeListViewMsg::SetStore(store));
        }

        self.tag_list_view.emit(TagListViewMsg::SetTags(data.tags));

        self.stashes_view.emit(StashesViewMsg::SetStashes(data.stashes));

        if data.remotes != self.last_remotes {
            self.last_remotes = data.remotes.clone();
            let store = tree_node::build_remote_store(data.remotes);

            self.remotes_view.emit(TreeListViewMsg::SetStore(store));
        }

        self.working_copy_view
            .emit(WorkingCopyViewMsg::RepoStateChanged(data.repo_state, data.rebase_has_conflicts));

        self.commit_log_view
            .emit(CommitLogViewMsg::RewriteableOidsLoaded(data.rewriteable_oids));
    }

    fn deselect_all_sidebar_items(&self, except: Option<SidebarSection>) {
        if except != Some(SidebarSection::Branches) {
            self.branches_view.emit(TreeListViewMsg::Deselect);
        }

        if except != Some(SidebarSection::Tags) {
            self.tag_list_view.emit(TagListViewMsg::Deselect);
        }

        if except != Some(SidebarSection::Remotes) {
            self.remotes_view.emit(TreeListViewMsg::Deselect);
        }
    }
}

impl Drop for RepoView {
    fn drop(&mut self) {
        self.stop_auto_fetch_timer();
    }
}

fn setup_window_focus_refresh(root: &adw::ToastOverlay, sender: &ComponentSender<RepoView>) {
    let s = sender.input_sender().clone();
    root.connect_realize(move |widget| {
        if let Some(window) = widget.root().and_downcast_ref::<gtk::Window>() {
            let s = s.clone();
            let deactivated_at: std::rc::Rc<Cell<Option<std::time::Instant>>> = std::rc::Rc::new(Cell::new(None));

            let id = window.connect_is_active_notify(move |w| {
                if w.is_active() {
                    let dominated_by_popover = deactivated_at
                        .get()
                        .is_some_and(|t| t.elapsed() < std::time::Duration::from_millis(200));

                    deactivated_at.set(None);

                    if !dominated_by_popover {
                        s.emit(RepoViewMsg::Refresh);
                    }
                } else {
                    deactivated_at.set(Some(std::time::Instant::now()));
                }
            });

            let window = window.clone();
            let id = Cell::new(Some(id));
            widget.connect_unrealize(move |_| {
                if let Some(id) = id.take() {
                    window.disconnect(id);
                }
            });
        }
    });
}
