mod diff;
mod file;
mod log;
mod selection;
#[cfg(test)]
mod test;
mod window;

use iced_utils::{task, Task};
use ignore::gitignore::Gitignore;
use inflorescence_iced_widget::{dir_picker, nav_scrollable, report};
use inflorescence_model::model::{
    Job, Log, Logs, PickingProjectSelection, ReadyState, RecordChanges,
};
use inflorescence_model::{action, model, to_record};
use inflorescence_view::{view, Theme};
use libflorescence::identity::Id;
use libflorescence::prelude::*;
use libflorescence::{identity, repo, store, terrors, PijulConfig};

use iced::widget::text_editor;
use iced::{Element, Subscription};
use notify_debouncer_full::notify::{self, RecommendedWatcher, RecursiveMode};
use notify_debouncer_full::{
    new_debouncer, DebounceEventResult, Debouncer, RecommendedCache,
};
use tokio::sync::{mpsc, watch};
use tokio::task::spawn_blocking;
use tokio::time::Duration;
use tokio_stream::wrappers::{UnboundedReceiverStream, WatchStream};

use std::path::PathBuf;
use std::sync::Arc;
use std::{cmp, env, mem};

pub fn main() -> iced::Result {
    use core::str;
    use std::str::FromStr;
    use tracing_subscriber::EnvFilter;

    const LOG_ENV: &str = "RUST_LOG";

    let filter = std::env::var(LOG_ENV)
        .map(|env| {
            EnvFilter::from_str(&env).unwrap_or_else(|err| {
                panic!("invalid `{LOG_ENV}` environment variable {err:?}")
            })
        })
        .unwrap_or_else(|_| {
            EnvFilter::new("inflorescence=info,libflorescence=info")
        });
    tracing_subscriber::fmt().with_env_filter(filter).init();

    #[cfg(not(test))]
    {
        const PATH_ARG: &str = "repository_path";
        let cli = clap::Command::new("Inflorescence")
            .bin_name("inflorescence")
            .version(clap::crate_version!())
            .about("GUI for version control backed by Pijul")
            .arg(clap::Arg::new(PATH_ARG).required(false).help(
                "Repository path to manage, absolute or relative (optional)",
            ));
        let matches = cli.get_matches();
        let repo_path = matches
            .get_one::<String>(PATH_ARG)
            .map(|path| std::fs::canonicalize(path).unwrap());
        iced_utils::daemon(move || init(repo_path.clone()), update, view)
            .subscription(subs)
            .theme(inflorescence_view::theme)
            .title(title)
            .run()
    }
    #[cfg(test)]
    {
        let _ = (update_managing_repo, view, subs, title);
        Ok(())
    }
}

#[derive(Debug)]
struct State {
    model: model::State,
    /// Some when `model.sub` is `ManagingRepo`
    managing_repo: Option<ManagingRepo>,
}

#[derive(Debug)]
struct ManagingRepo {
    repo_fs_watch: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,
    repo_tx_in: mpsc::UnboundedSender<repo::MsgIn>,
    /// Cache for untracked and changed files loaded from disk
    files: file::State,
    /// ".ignore" file loaded to memory
    ignore: Gitignore,
    _repo_thread: std::thread::JoinHandle<()>,
}

#[derive(Debug)]
enum Msg {
    View(view::Msg),
    NoOp,
    Window(window::Event),
    ManagingRepo(ManagingRepoMsg),
    ReadProjectsResult(Result<store::Projects, store::ProjectsFileErr>),
    UpsertProjectResult(Result<(), (store::ProjectsFileErr, store::Project)>),
    InitedRepo(anyhow::Result<PathBuf>),
}

#[derive(Debug)]
enum ManagingRepoMsg {
    LoadedIdentities(anyhow::Result<Vec<Id>>),
    ToRepo(repo::MsgIn),
    FromRepo(repo::MsgOut),
    File(file::Msg),
    WatchedFileChange(PathBuf),
}

fn init(repo_path: Option<PathBuf>) -> (State, Task<Msg>) {
    let window_settings = window::settings();
    let window_size = window_settings.size;
    let (window_id, open_window_task) = task::window_open(window_settings);
    let set_icon_task = task::window_set_icon(
        window_id,
        include_bytes!("../../assets/icon.png").to_vec(),
    );

    let window_scale = 1.0;
    let allowed_actions = default();
    let sub_menu = None;
    let mut report = report::Container::default();

    if let Some(repo_path) = repo_path {
        let (sub, managing_repo, managing_repo_task) =
            init_managing_repo(repo_path, &mut report);

        let task = Task::batch([
            open_window_task.map(|_id| Msg::NoOp),
            set_icon_task,
            managing_repo_task,
        ]);

        let model = model::State {
            sub: model::SubState::ManagingRepo(sub),
            window_size,
            window_scale,
            allowed_actions,
            sub_menu,
            report,
        };

        (
            State {
                model,
                managing_repo: Some(managing_repo),
            },
            task,
        )
    } else {
        let (sub, projects_task) = init_picking_project();
        let model = model::State {
            sub,
            window_size,
            window_scale,
            allowed_actions,
            sub_menu,
            report,
        };

        let task = Task::batch([
            open_window_task.map(|_id| Msg::NoOp),
            set_icon_task,
            projects_task,
        ]);
        (
            State {
                model,
                managing_repo: None,
            },
            task,
        )
    }
}

fn init_picking_project() -> (model::SubState, Task<Msg>) {
    let read_projects_task = Task::perform(
        async { store::read_projects().await },
        Msg::ReadProjectsResult,
    );
    let state = model::PickingProject {
        is_blocking: false,
        projects: None,
        selection: default(),
        projects_nav: nav_scrollable::State::default(),
    };
    let sub = model::SubState::PickingProject(state);
    (sub, read_projects_task)
}

fn init_picking_repo_dir() -> (model::PickingRepoDir, Task<Msg>) {
    let start_path = env::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let (picker, picker_task) = dir_picker::init(start_path);

    let model = model::PickingRepoDir {
        picker,
        waiting_to_init: None,
    };
    let task = Task::batch([picker_task
        .map(view::Msg::PickingRepoDir)
        .map(Msg::View)]);
    (model, task)
}

fn init_managing_repo(
    repo_path: PathBuf,
    report: &mut report::Container,
) -> (model::ManagingRepo, ManagingRepo, Task<Msg>) {
    let (repo_thread, repo_task, repo_tx_in) =
        start_thread_to_manage_repo(repo_path.clone());

    let load_ids_task = load_identities_task();

    let (files, files_task) = file::init(repo_path.clone());

    let tasks = Task::batch([
        repo_task.map(Msg::ManagingRepo),
        load_ids_task.map(Msg::ManagingRepo),
        files_task.map(ManagingRepoMsg::File).map(Msg::ManagingRepo),
    ]);

    // Load `.ignore` file
    let (ignore, ignore_err) =
        Gitignore::new(repo_path.join(repo::IGNORE_FILE));
    if let Some(err) = ignore_err {
        let msg = format!("Error loading .ignore file {err:?}");
        error!("{msg}");
        report::show_err(report, msg);
    }

    let sub = model::ManagingRepo {
        repo_path,
        sub: model::ManagingRepoSubState::Loading {
            user_ids: None,
            repo: None,
        },
    };
    let managing_repo = ManagingRepo {
        repo_fs_watch: None,
        repo_tx_in,
        files,
        ignore,
        _repo_thread: repo_thread,
    };
    (sub, managing_repo, tasks)
}

fn update(state: &mut State, msg: Msg) -> Task<Msg> {
    let task = match msg {
        Msg::NoOp => Task::none(),
        Msg::View(msg) => match &mut state.model.sub {
            model::SubState::PickingRepoDir(model) => {
                let (task, new_sub_state) = update_picking_repo_dir_from_view(
                    model,
                    &mut state.model.sub_menu,
                    &mut state.model.report,
                    msg,
                );
                if let Some((new_sub, managing_repo)) = new_sub_state {
                    state.model.sub = model::SubState::ManagingRepo(new_sub);
                    state.managing_repo = Some(managing_repo);
                }
                task
            }
            model::SubState::ManagingRepo(model) => {
                let sub = state.managing_repo.as_mut().unwrap();
                update_managing_repo_from_view(
                    sub,
                    model,
                    &mut state.model.sub_menu,
                    &mut state.model.report,
                    &state.model.allowed_actions,
                    msg,
                )
            }
            model::SubState::PickingProject(model) => {
                let (task, new_sub_state, managing_repo) =
                    update_picking_project_from_view(
                        model,
                        msg,
                        &mut state.model.report,
                    );
                if let Some(new_sub) = new_sub_state {
                    if let model::SubState::ManagingRepo(_) = new_sub {
                        state.managing_repo = Some(
                            managing_repo.expect("managing_repo is required"),
                        );
                    }
                    state.model.sub = new_sub;
                }
                task
            }
        },
        Msg::Window(event) => update_from_window_event(&mut state.model, event)
            .map(Msg::ManagingRepo),
        Msg::ManagingRepo(msg) => {
            if let model::SubState::ManagingRepo(model) = &mut state.model.sub {
                let sub = state.managing_repo.as_mut().unwrap();
                let (task, new_state) = update_managing_repo(
                    sub,
                    model,
                    &mut state.model.report,
                    msg,
                );
                if let Some(new_state) = new_state {
                    state.model.sub = new_state;
                }
                task
            } else {
                Task::none()
            }
        }
        Msg::ReadProjectsResult(result) => match result {
            Ok(projects) => {
                if projects.is_empty() {
                    // Move on to dir picker
                    let (model, task) = init_picking_repo_dir();
                    let sub = model::SubState::PickingRepoDir(model);
                    state.model.sub = sub;
                    task
                } else {
                    if let model::SubState::PickingProject(sub) =
                        &mut state.model.sub
                    {
                        sub.projects = Some(projects);
                        sub.is_blocking = false;
                    }
                    Task::none()
                }
            }
            Err(err) => {
                if let model::SubState::PickingProject(sub) =
                    &mut state.model.sub
                {
                    match err.as_enum() {
                        terrors::E2::A(store::BlockingLockError) => {
                            sub.is_blocking = true;
                            // retry
                            Task::perform(
                                async { store::read_projects().await },
                                Msg::ReadProjectsResult,
                            )
                        }
                        _ => {
                            let msg = format!(
                                "Failed to read projects from store: {err:?}"
                            );
                            report::show_err(&mut state.model.report, msg);
                            Task::none()
                        }
                    }
                } else {
                    Task::none()
                }
            }
        },
        Msg::UpsertProjectResult(result) => match result {
            Ok(()) => Task::none(),
            Err((err, project)) => match err.as_enum() {
                terrors::E2::A(store::BlockingLockError) => Task::perform(
                    async move { store::upsert_project(project).await },
                    Msg::UpsertProjectResult,
                ),
                _ => {
                    let msg =
                        format!("Failed to write projects to store: {err:?}");
                    report::show_err(&mut state.model.report, msg);
                    Task::none()
                }
            },
        },
        Msg::InitedRepo(result) => {
            if let model::SubState::PickingRepoDir(state) = &mut state.model.sub
            {
                state.waiting_to_init = None;
            }
            let task = match result {
                Ok(path) => {
                    let (sub, managing_repo, managing_repo_task) =
                        init_managing_repo(
                            path.clone(),
                            &mut state.model.report,
                        );
                    state.model.sub = model::SubState::ManagingRepo(sub);
                    state.managing_repo = Some(managing_repo);
                    managing_repo_task
                }
                Err(err) => {
                    let msg =
                        format!("Failed to initialize a new repo: {err:?}");
                    report::show_err(&mut state.model.report, msg);
                    Task::none()
                }
            };
            state.model.sub_menu = None;
            task
        }
    };

    action::update_allowed_actions(&mut state.model);

    task
}

fn update_picking_project_from_view(
    model: &mut model::PickingProject,
    msg: view::Msg,
    report: &mut report::Container,
) -> (Task<Msg>, Option<model::SubState>, Option<ManagingRepo>) {
    let mut new_state = None;
    let mut new_managing_repo = None;
    let task = match msg {
        view::Msg::Action(msg) => match msg {
            action::FilteredMsg::Confirm => match model.selection {
                PickingProjectSelection::FindOrCreate => {
                    let (model, task) = init_picking_repo_dir();
                    new_state = Some(model::SubState::PickingRepoDir(model));
                    task
                }
                PickingProjectSelection::Existing { ix } => {
                    if let Some(store::Project {
                        last_closed_time: _,
                        path,
                    }) = model
                        .projects
                        .as_ref()
                        .and_then(|projects| projects.iter().nth(ix))
                    {
                        let (sub, managing_repo, managing_repo_task) =
                            init_managing_repo(path.clone(), report);
                        new_state = Some(model::SubState::ManagingRepo(sub));
                        new_managing_repo = Some(managing_repo);
                        managing_repo_task
                    } else {
                        Task::none()
                    }
                }
            },
            action::FilteredMsg::Selection(msg) => {
                picking_project_selection(model, msg)
            }
            action::FilteredMsg::Cancel
            | action::FilteredMsg::PostponeRecord
            | action::FilteredMsg::SaveRecord
            | action::FilteredMsg::DiscardRecord
            | action::FilteredMsg::ToggleRecursive
            | action::FilteredMsg::RmChange
            | action::FilteredMsg::StartRecord
            | action::FilteredMsg::SelectChannel
            | action::FilteredMsg::ForkChannel
            | action::FilteredMsg::RefreshRepo
            | action::FilteredMsg::ShowEntireLog
            | action::FilteredMsg::FocusNext
            | action::FilteredMsg::FocusPrev
            | action::FilteredMsg::ClipboardCopy
            | action::FilteredMsg::ToggleReports
            | action::FilteredMsg::ClipboardCopyReports
            | action::FilteredMsg::ToRecord(_)
            | action::FilteredMsg::ToRecordToggleSelectedFileOrChange
            | action::FilteredMsg::EnterSubMenu(_)
            | action::FilteredMsg::ReloadIdentity
            | action::FilteredMsg::SubMenuPushOption(_)
            | action::FilteredMsg::SubMenuPullOption(_)
            | action::FilteredMsg::SubMenuCompareRemoteOption(_) => {
                Task::none()
            }
        },
        view::Msg::OpenProject(dir) => {
            let (sub, managing_repo, managing_repo_task) =
                init_managing_repo(dir, report);
            new_state = Some(model::SubState::ManagingRepo(sub));
            new_managing_repo = Some(managing_repo);
            managing_repo_task
        }
        view::Msg::PickNewProject => {
            let (model, task) = init_picking_repo_dir();
            new_state = Some(model::SubState::PickingRepoDir(model));
            task
        }
        view::Msg::PickingRepoDir(_)
        | view::Msg::EditRecordMsg(_)
        | view::Msg::EditRecordDesc(_)
        | view::Msg::EditForkChannelName(_)
        | view::Msg::UnfilteredSelection(_)
        | view::Msg::ToRecord(_)
        | view::Msg::SelectIdentity(_)
        | view::Msg::SubMenuPushSelectRemote(_)
        | view::Msg::SubMenuPullSelectRemote(_)
        | view::Msg::SubMenuCompareRemoteSelectRemote(_)
        | view::Msg::SubMenuCompareRemoteInputRemoteChannel(_) => Task::none(),
    };
    (task, new_state, new_managing_repo)
}

fn picking_project_selection(
    model: &mut model::PickingProject,
    msg: inflorescence_model::selection::Msg,
) -> Task<Msg> {
    match msg {
        selection::Msg::PressDir(dir) => {
            if let Some(projects) = model.projects.as_mut()
                && !projects.is_empty()
            {
                match model.selection {
                    PickingProjectSelection::FindOrCreate => match dir {
                        selection::Dir::Down => {
                            model.selection =
                                PickingProjectSelection::Existing { ix: 0 };
                        }
                        selection::Dir::Up => {
                            model.selection =
                                PickingProjectSelection::Existing {
                                    ix: projects.len() - 1,
                                };
                        }
                        selection::Dir::Right | selection::Dir::Left => {}
                    },
                    PickingProjectSelection::Existing { ix } => match dir {
                        selection::Dir::Down => {
                            if ix < projects.len() - 1 {
                                let ix = ix + 1;
                                model.selection =
                                    PickingProjectSelection::Existing { ix };
                                nav_scrollable::scroll_down_to_section(
                                    &mut model.projects_nav,
                                    ix,
                                );
                            } else {
                                model.selection =
                                    PickingProjectSelection::FindOrCreate;
                                nav_scrollable::scroll_up_to_section(
                                    &mut model.projects_nav,
                                    0,
                                );
                            }
                        }
                        selection::Dir::Up => {
                            if ix > 0 {
                                let ix = ix - 1;
                                model.selection =
                                    PickingProjectSelection::Existing { ix };
                                nav_scrollable::scroll_up_to_section(
                                    &mut model.projects_nav,
                                    ix,
                                );
                            } else {
                                model.selection =
                                    PickingProjectSelection::FindOrCreate;
                                nav_scrollable::scroll_up_to_section(
                                    &mut model.projects_nav,
                                    0,
                                );
                            }
                        }
                        selection::Dir::Right | selection::Dir::Left => {}
                    },
                }
            }
            Task::none()
        }
        selection::Msg::AltPressDir(_) => Task::none(),
    }
}

fn update_managing_repo(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    report: &mut report::Container,
    msg: ManagingRepoMsg,
) -> (Task<Msg>, Option<model::SubState>) {
    let mut new_state = None;
    let task = match msg {
        ManagingRepoMsg::LoadedIdentities(ids) => {
            match ids {
                Ok(mut user_ids) => {
                    let get_repo = |sub: &mut model::ManagingRepoSubState| {
                        match sub {
                            model::ManagingRepoSubState::Loading {
                                repo,
                                ..
                            } => repo.take(),
                            model::ManagingRepoSubState::SelectingIdentity {
                                repo,
                                ..
                            } => repo.take(),
                            model::ManagingRepoSubState::NoIdFound { repo } => {
                                repo.take()
                            }
                            model::ManagingRepoSubState::Ready(_) => {
                                unreachable!()
                            }
                        }
                    };
                    if user_ids.is_empty() {
                        let repo = get_repo(&mut model.sub);
                        model.sub =
                            model::ManagingRepoSubState::NoIdFound { repo }
                    } else if user_ids.len() == 1 {
                        let user_id = user_ids.pop().unwrap();
                        match &mut model.sub {
                            model::ManagingRepoSubState::Loading {
                                user_ids,
                                repo,
                            } => {
                                if let Some(repo) = repo.take() {
                                    model.sub =
                                        model::ManagingRepoSubState::Ready(
                                            ReadyState {
                                                user_id,
                                                repo,
                                                selection: default(),
                                                navigation: default(),
                                                record_changes: default(),
                                                forking_channel_name: default(),
                                                logs: default(),
                                                to_record: default(),
                                                jobs: default(),
                                                record_dichotomy: default(),
                                            },
                                        )
                                } else {
                                    *user_ids = Some(vec![user_id]);
                                }
                            }
                            model::ManagingRepoSubState::SelectingIdentity {
                                ..
                            } => {
                                unreachable!()
                            }
                            model::ManagingRepoSubState::Ready(..) => {
                                unreachable!()
                            }
                            model::ManagingRepoSubState::NoIdFound {
                                repo,
                            } => {
                                if let Some(repo) = repo.take() {
                                    model.sub =
                                        model::ManagingRepoSubState::Ready(
                                            ReadyState {
                                                user_id,
                                                repo,
                                                selection: default(),
                                                navigation: default(),
                                                record_changes: default(),
                                                forking_channel_name: default(),
                                                logs: default(),
                                                to_record: default(),
                                                jobs: default(),
                                                record_dichotomy: default(),
                                            },
                                        )
                                } else {
                                    model.sub = model::ManagingRepoSubState::SelectingIdentity {
                                        ids: user_ids,
                                        selection_ix: default(),
                                        selection_nav: default(),
                                        confirmed_selection_ix: default(),
                                        repo: default(),
                                    };
                                }
                            }
                        }
                    } else {
                        let repo = get_repo(&mut model.sub);
                        model.sub =
                            model::ManagingRepoSubState::SelectingIdentity {
                                ids: user_ids,
                                selection_ix: default(),
                                selection_nav: default(),
                                confirmed_selection_ix: default(),
                                repo,
                            }
                    }
                }
                Err(err) => {
                    let msg =
                        format!("Failed to load Pijul identity with {err:#?}");
                    report::show_err(report, msg);
                }
            }
            Task::none()
        }
        ManagingRepoMsg::ToRepo(msg) => {
            state.repo_tx_in.send(msg).unwrap();
            Task::none()
        }
        ManagingRepoMsg::FromRepo(msg) => {
            info!("Repo sent msg {msg}");
            let (task, state) = update_from_repo(state, model, report, msg);
            new_state = state;
            task
        }
        ManagingRepoMsg::File(msg) => {
            update_file(state, model, msg).map(Msg::ManagingRepo)
        }
        ManagingRepoMsg::WatchedFileChange(path) => {
            // Reload ignore file if it's changed
            let repo_ignore_file = model.repo_path.join(repo::IGNORE_FILE);
            if path == repo_ignore_file {
                let (ignore, ignore_err) = Gitignore::new(repo_ignore_file);
                if let Some(err) = ignore_err {
                    let msg = format!("Error loading .ignore file {err:?}");
                    error!("{msg}");
                    report::show_err(report, msg);
                }
                state.ignore = ignore;
            }

            // Check if the files is not ignored
            let is_dir = path.metadata().map(|md| md.is_dir()).unwrap_or(false);
            if !state
                .ignore
                .matched_path_or_any_parents(&path, is_dir)
                .is_ignore()
                && let Some(fs_watch) = state.repo_fs_watch.as_mut()
            {
                // Refresh repo
                state
                    .repo_tx_in
                    .send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
                    .unwrap();

                // Start watching it in case it's a new file
                if let Err(err) =
                    fs_watch.watch(&path, RecursiveMode::NonRecursive)
                {
                    match &err.kind {
                        // Ignore path not found as this might be triggered on a
                        // watched file that's been removed
                        notify::ErrorKind::PathNotFound => {}
                        notify::ErrorKind::Generic(_)
                        | notify::ErrorKind::Io(_)
                        | notify::ErrorKind::WatchNotFound
                        | notify::ErrorKind::InvalidConfig(_)
                        | notify::ErrorKind::MaxFilesWatch => {
                            let msg = format!(
                    "Error setting up file watch for path {}: {err:?}",
                    path.to_string_lossy()
                );
                            error!("{msg}");
                            report::show_err(report, msg)
                        }
                    }
                }
            }

            Task::none()
        }
    };
    (task, new_state)
}

fn update_file(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    msg: file::Msg,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState {
        repo,
        navigation,
        selection,
        ..
    }) = model::is_ready_mut(model)
    {
        let loaded = file::update(&mut state.files, repo, msg);
        if let Some(file::Loaded {
            file_id,
            unchanged_sections,
        }) = loaded
        {
            let is_selected = match selection.status.as_ref() {
                Some(selection::Status::UntrackedFile {
                    ix: _,
                    path,
                    diff_selected: _,
                }) => {
                    let selected_file_id =
                        file::id_parts_hash(path, file::Kind::Untracked);
                    selected_file_id == file_id
                }
                Some(selection::Status::ChangedFile {
                    ix: _,
                    path,
                    diff_selected: _,
                }) => {
                    let selected_file_id =
                        file::id_parts_hash(path, file::Kind::Changed);
                    selected_file_id == file_id
                }
                _ => false,
            };

            if is_selected {
                diff::init_diffs_nav(
                    &mut navigation.files_diffs,
                    #[cfg(debug_assertions)]
                    file_id,
                )
                .set_skip_sections(unchanged_sections);
            }
            navigation.files_diffs.diffs.entry(file_id).or_default();
        }
    }
    Task::none()
}

fn update_from_window_event(
    model: &mut model::State,
    event: window::Event,
) -> Task<ManagingRepoMsg> {
    match event {
        window::Event::Opened { position: _, size }
        | window::Event::Resized(size) => {
            model.window_size = size;
        }
        window::Event::Rescaled(scale) => {
            model.window_scale = scale;
        }
        window::Event::Closed => {
            if let model::SubState::ManagingRepo(sub) = &model.sub
                && let model::ManagingRepoSubState::Ready(_) = &sub.sub
            {
                let _ =
                    store::updated_closed_time_blocking(sub.repo_path.clone());
            }
        }
        window::Event::Moved(_)
        | window::Event::RedrawRequested(_)
        | window::Event::CloseRequested
        | window::Event::Focused
        | window::Event::Unfocused
        | window::Event::FileHovered(_)
        | window::Event::FileDropped(_)
        | window::Event::FilesHoveredLeft => {}
    }
    Task::none()
}

fn start_thread_to_manage_repo(
    repo_path: PathBuf,
) -> (
    std::thread::JoinHandle<()>,
    Task<ManagingRepoMsg>,
    mpsc::UnboundedSender<repo::MsgIn>,
) {
    let (repo_tx_in, repo_rx_in) = mpsc::unbounded_channel::<repo::MsgIn>();
    let (repo_tx_out, repo_rx_out) = mpsc::unbounded_channel::<repo::MsgOut>();
    let repo_task = repo::manage(repo_path, repo_rx_in, repo_tx_out);
    let repo_rx_out = UnboundedReceiverStream::new(repo_rx_out);
    let repo_msg_out_task = Task::run(repo_rx_out, ManagingRepoMsg::FromRepo);

    (repo_task, repo_msg_out_task, repo_tx_in)
}

fn update_picking_repo_dir_from_view(
    state: &mut model::PickingRepoDir,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
    msg: view::Msg,
) -> (Task<Msg>, Option<(model::ManagingRepo, ManagingRepo)>) {
    let mut new_state = None;
    let model::PickingRepoDir {
        picker,
        waiting_to_init: _,
    } = state;

    let mut handle_action =
        |sub_menu: &mut Option<model::SubMenu>,
         action: Option<dir_picker::Action>| {
            if let Some(action) = action {
                match action {
                    dir_picker::Action::Picked(dir) => {
                        if repo::is_pijul(&dir) {
                            // If it contains Pijul repo, init ManagingRepo
                            // state
                            let project = store::Project {
                                last_closed_time: cmp::Reverse(None),
                                path: dir.clone(),
                            };
                            let store_project_task = Task::perform(
                                async move { store::upsert_project(project).await },
                                Msg::UpsertProjectResult,
                            );
                            let (sub, managing_repo, managing_repo_task) =
                                init_managing_repo(dir, report);
                            new_state = Some((sub, managing_repo));
                            Task::batch([
                                managing_repo_task,
                                store_project_task,
                            ])
                        } else {
                            if repo::is_git(&dir) {
                                // Allow to import from Git
                                *sub_menu =
                                    Some(model::SubMenu::ImportFromGit {
                                        path: dir.clone(),
                                    });
                            } else {
                                // If it's not Git repo either, allow to
                                // initialize Pijul
                                *sub_menu = Some(model::SubMenu::InitRepo {
                                    path: dir.clone(),
                                });
                            }
                            Task::none()
                        }
                    }
                    dir_picker::Action::CreateNew(path) => {
                        *sub_menu = Some(model::SubMenu::InitRepo { path });
                        Task::none()
                    }
                }
            } else {
                Task::none()
            }
        };

    let task = match msg {
        view::Msg::Action(msg) => match msg {
            action::FilteredMsg::Selection(msg) => match msg {
                selection::Msg::PressDir(_) => Task::none(),
                selection::Msg::AltPressDir(dir) => {
                    update_dir_picker_selection(picker, dir);
                    Task::none()
                }
            },
            action::FilteredMsg::FocusNext => task::widget_focus_next(),
            action::FilteredMsg::Confirm => {
                let (task, clear_sub_menu) = match &sub_menu {
                    Some(model::SubMenu::InitRepo { path }) => {
                        state.waiting_to_init =
                            Some(model::ProjectInitKind::New);
                        let path = path.clone();
                        (
                            Task::perform(
                                async move { repo::init(path).await },
                                Msg::InitedRepo,
                            ),
                            true,
                        )
                    }
                    Some(model::SubMenu::ImportFromGit { path }) => {
                        state.waiting_to_init =
                            Some(model::ProjectInitKind::ImportFromGit);
                        let path = path.clone();
                        (
                            Task::perform(
                                async move { repo::git::import(path).await },
                                Msg::InitedRepo,
                            ),
                            true,
                        )
                    }

                    Some(
                        model::SubMenu::Push { .. }
                        | model::SubMenu::Pull { .. }
                        | model::SubMenu::ResetChange
                        | model::SubMenu::Add { .. }
                        | model::SubMenu::CompareRemote { .. },
                    )
                    | None => {
                        let (task, action) =
                            dir_picker::confirm_input_or_selection(picker);
                        let dir_picker_task =
                            task.map(view::Msg::PickingRepoDir).map(Msg::View);
                        let action_task = handle_action(sub_menu, action);
                        (Task::batch([dir_picker_task, action_task]), false)
                    }
                };
                if clear_sub_menu {
                    *sub_menu = None;
                }
                task
            }
            action::FilteredMsg::Cancel => {
                *sub_menu = None;
                Task::none()
            }
            action::FilteredMsg::PostponeRecord
            | action::FilteredMsg::SaveRecord
            | action::FilteredMsg::DiscardRecord
            | action::FilteredMsg::ToggleRecursive
            | action::FilteredMsg::RmChange
            | action::FilteredMsg::StartRecord
            | action::FilteredMsg::SelectChannel
            | action::FilteredMsg::ForkChannel
            | action::FilteredMsg::RefreshRepo
            | action::FilteredMsg::ShowEntireLog
            | action::FilteredMsg::FocusPrev
            | action::FilteredMsg::ClipboardCopy
            | action::FilteredMsg::ToggleReports
            | action::FilteredMsg::ClipboardCopyReports
            | action::FilteredMsg::ToRecord(_)
            | action::FilteredMsg::ToRecordToggleSelectedFileOrChange
            | action::FilteredMsg::EnterSubMenu(_)
            | action::FilteredMsg::ReloadIdentity
            | action::FilteredMsg::SubMenuPushOption(_)
            | action::FilteredMsg::SubMenuPullOption(_)
            | action::FilteredMsg::SubMenuCompareRemoteOption(_) => {
                Task::none()
            }
        },
        view::Msg::UnfilteredSelection(_msg) => Task::none(),
        view::Msg::PickingRepoDir(msg) => {
            let (task, action) = dir_picker::update(picker, msg);
            let dir_picker_task =
                task.map(view::Msg::PickingRepoDir).map(Msg::View);
            let action_task = handle_action(sub_menu, action);
            Task::batch([dir_picker_task, action_task])
        }
        view::Msg::EditRecordMsg(_)
        | view::Msg::EditRecordDesc(_)
        | view::Msg::EditForkChannelName(_)
        | view::Msg::ToRecord(_)
        | view::Msg::OpenProject(_)
        | view::Msg::PickNewProject
        | view::Msg::SelectIdentity(_)
        | view::Msg::SubMenuPushSelectRemote(_)
        | view::Msg::SubMenuPullSelectRemote(_)
        | view::Msg::SubMenuCompareRemoteSelectRemote(_)
        | view::Msg::SubMenuCompareRemoteInputRemoteChannel(_) => Task::none(),
    };
    (task, new_state)
}

fn update_dir_picker_selection(
    picker: &mut dir_picker::State,
    dir: selection::Dir,
) {
    let dir = match dir {
        selection::Dir::Down => dir_picker::SelectionDir::Down,
        selection::Dir::Up => dir_picker::SelectionDir::Up,
        selection::Dir::Right => dir_picker::SelectionDir::Right,
        selection::Dir::Left => dir_picker::SelectionDir::Left,
    };
    dir_picker::move_selection(picker, dir);
}

fn update_managing_repo_from_view(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
    allowed_actions: &[action::Binding],
    msg: view::Msg,
) -> Task<Msg> {
    // Return early if the action is not allowed in the current state
    if let view::Msg::Action(msg) = &msg
        && !action::is_allowed(allowed_actions, msg)
    {
        info!("Action not allowed: {msg:?}");
        return Task::none();
    }

    match msg {
        view::Msg::Action(msg) => {
            update_from_action(state, model, sub_menu, report, msg)
        }
        view::Msg::EditRecordMsg(action) => {
            edit_record_msg(model, action).map(Msg::ManagingRepo)
        }
        view::Msg::EditRecordDesc(action) => {
            edit_record_desc(model, action).map(Msg::ManagingRepo)
        }
        view::Msg::EditForkChannelName(name) => {
            if let Some(ReadyState {
                forking_channel_name,
                ..
            }) = model::is_ready_mut(model)
            {
                *forking_channel_name = Some(name);
            }
            Task::none()
        }
        view::Msg::UnfilteredSelection(msg) => {
            if let Some(ReadyState {
                repo,
                selection,
                navigation,
                logs,
                record_dichotomy,
                ..
            }) = model::is_ready_mut(model)
            {
                let record_dichotomy =
                    selection.compare_remote.as_ref().and_then(
                        |selection::CompareRemote {
                             ix: _,
                             hash: _,
                             file: _,
                             remote,
                             remote_channel,
                         }| {
                            model::get_record_dichotomy(
                                record_dichotomy,
                                remote,
                                remote_channel,
                            )
                        },
                    );

                selection::update_unfiltered(
                    msg,
                    selection,
                    &mut state.files,
                    navigation,
                    repo,
                    logs,
                    record_dichotomy,
                )
                .map(Msg::ManagingRepo)
            } else {
                Task::none()
            }
        }
        view::Msg::ToRecord(msg) => {
            if let Some(ReadyState {
                to_record, repo, ..
            }) = model::is_ready_mut(model)
            {
                to_record::update(to_record, msg, &repo.changed_files);
            }
            Task::none()
        }
        view::Msg::SelectIdentity(ix) => {
            if let model::ManagingRepoSubState::SelectingIdentity {
                ids,
                repo,
                confirmed_selection_ix,
                ..
            } = &mut model.sub
            {
                if let Some(repo) = repo.take() {
                    let user_id = ids.get(ix).unwrap().clone();
                    model.sub =
                        model::ManagingRepoSubState::Ready(ReadyState {
                            user_id,
                            repo,
                            selection: default(),
                            navigation: default(),
                            record_changes: default(),
                            forking_channel_name: default(),
                            logs: default(),
                            to_record: default(),
                            jobs: default(),
                            record_dichotomy: default(),
                        });
                } else {
                    *confirmed_selection_ix = Some(ix);
                }
            }
            Task::none()
        }
        view::Msg::SubMenuPushSelectRemote(selection) => {
            if let Some(model::SubMenu::Push {
                opt: Some(model::PushOption::SelectingRemote { remote }),
                ..
            }) = sub_menu
            {
                *remote = Some(selection);
            }
            Task::none()
        }
        view::Msg::SubMenuPullSelectRemote(selection) => {
            if let Some(model::SubMenu::Pull {
                opt: Some(model::PullOption::SelectingRemote { remote }),
                ..
            }) = sub_menu
            {
                *remote = Some(selection);
            }
            Task::none()
        }
        view::Msg::SubMenuCompareRemoteSelectRemote(selection) => {
            if let Some(model::SubMenu::CompareRemote {
                opt:
                    Some(model::CompareRemoteOption::SelectingRemote { remote }),
                ..
            }) = sub_menu
            {
                *remote = Some(selection);
            }
            Task::none()
        }
        view::Msg::SubMenuCompareRemoteInputRemoteChannel(input) => {
            if let Some(model::SubMenu::CompareRemote {
                opt:
                    Some(model::CompareRemoteOption::InputingRemoteChannel {
                        channel,
                    }),
                ..
            }) = sub_menu
            {
                *channel = Some(input);
            }
            Task::none()
        }
        view::Msg::PickingRepoDir(_)
        | view::Msg::OpenProject(_)
        | view::Msg::PickNewProject => Task::none(),
    }
}

fn update_from_action(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
    msg: action::FilteredMsg,
) -> Task<Msg> {
    let task = match msg {
        action::FilteredMsg::Confirm => match &sub_menu {
            Some(model::SubMenu::Push { remote, opt }) => {
                if let Some(model::PushOption::SelectingRemote { remote }) = opt
                {
                    *sub_menu = Some(model::SubMenu::Push {
                        remote: remote.clone(),
                        opt: None,
                    });
                    Task::none()
                } else {
                    push(state, model, sub_menu, report, remote.clone())
                }
            }
            Some(model::SubMenu::Pull { remote, opt }) => {
                if let Some(model::PullOption::SelectingRemote { remote }) = opt
                {
                    *sub_menu = Some(model::SubMenu::Pull {
                        remote: remote.clone(),
                        opt: None,
                    });
                    Task::none()
                } else {
                    pull(state, model, sub_menu, report, remote.clone())
                }
            }
            Some(model::SubMenu::ResetChange) => {
                reset_change(state, model, sub_menu, report)
            }
            Some(model::SubMenu::Add { recursive }) => {
                let task = add_untracked_file(state, model, *recursive);
                *sub_menu = None;
                task
            }
            Some(model::SubMenu::CompareRemote {
                remote,
                remote_channel,
                opt,
            }) => {
                if let Some(opt) = opt {
                    match opt {
                        model::CompareRemoteOption::SelectingRemote {
                            remote,
                        } => {
                            *sub_menu = Some(model::SubMenu::CompareRemote {
                                remote: remote.clone(),
                                remote_channel: remote_channel.clone(),
                                opt: None,
                            });
                            Task::none()
                        }
                        model::CompareRemoteOption::InputingRemoteChannel {
                            channel,
                        } => {
                            *sub_menu = Some(model::SubMenu::CompareRemote {
                                remote: remote.clone(),
                                remote_channel: channel.clone(),
                                opt: None,
                            });
                            Task::none()
                        }
                    }
                } else if let Some(ReadyState {
                    repo,
                    selection,
                    jobs,
                    ..
                }) = model::is_ready_mut(model)
                {
                    let remote = remote
                        .as_ref()
                        .or(repo.remotes.default.as_ref())
                        .cloned();
                    if let Some(remote) = remote {
                        let remote_channel = remote_channel
                            .clone()
                            .unwrap_or_else(|| repo.channel.clone());
                        jobs.insert(model::Job::CompareRemote {
                            remote: remote.clone(),
                            remote_channel: remote_channel.clone(),
                        });
                        selection.primary = selection::Primary::CompareRemote;
                        selection.compare_remote =
                            Some(selection::CompareRemote {
                                ix: None,
                                hash: None,
                                file: None,
                                remote: remote.clone(),
                                remote_channel: remote_channel.clone(),
                            });
                        state
                            .repo_tx_in
                            .send(repo::MsgIn::CompareRemote {
                                remote,
                                remote_channel,
                            })
                            .unwrap();
                    } else {
                        report::show_err(
                            report,
                            "Missing remote configuration (check your `.pijul/config.toml`)".to_string(),
                        );
                    }
                    *sub_menu = None;
                    Task::none()
                } else {
                    Task::none()
                }
            }
            Some(
                model::SubMenu::InitRepo { .. }
                | model::SubMenu::ImportFromGit { .. },
            )
            | None => {
                if let Some(ReadyState {
                    repo,
                    navigation: _,
                    selection,
                    forking_channel_name,
                    ..
                }) = model::is_ready_mut(model)
                {
                    #[allow(clippy::collapsible_if)]
                    if matches!(selection.primary, selection::Primary::Channel)
                    {
                        if let Some(selection::Channel {
                            ix: _,
                            name,
                            log: _,
                        }) = selection.channel.take()
                        {
                            state
                                .repo_tx_in
                                .send(repo::MsgIn::SwitchToChannel(name))
                                .unwrap();

                            selection.primary = selection::Primary::default();
                        }
                    } else if let Some(name) =
                        forking_channel_name.take_if(|name| {
                            let name = name.trim();
                            let empty = name.is_empty();
                            let unique = || {
                                repo.channel != name
                                    && !repo
                                        .other_channels
                                        .iter()
                                        .any(|n| n == name)
                            };
                            !empty && unique()
                        })
                    {
                        state
                            .repo_tx_in
                            .send(repo::MsgIn::ForkChannel(name))
                            .unwrap();
                    }
                    Task::none()
                } else if let model::ManagingRepoSubState::SelectingIdentity {
                    ids,
                    selection_ix,
                    selection_nav: _,
                    confirmed_selection_ix,
                    repo,
                } = &mut model.sub
                {
                    if let Some(repo) = repo.take() {
                        let user_id = ids.get(*selection_ix).unwrap().clone();
                        model.sub =
                            model::ManagingRepoSubState::Ready(ReadyState {
                                user_id,
                                repo,
                                selection: default(),
                                navigation: default(),
                                record_changes: default(),
                                forking_channel_name: default(),
                                logs: default(),
                                to_record: default(),
                                jobs: default(),
                                record_dichotomy: default(),
                            });
                    } else {
                        *confirmed_selection_ix = Some(*selection_ix);
                    }
                    Task::none()
                } else {
                    Task::none()
                }
            }
        },
        action::FilteredMsg::Cancel => {
            if let Some(sub) = sub_menu.take() {
                match sub {
                    model::SubMenu::Push { remote, opt } => {
                        if opt.is_some() {
                            *sub_menu = Some(model::SubMenu::Push {
                                remote,
                                opt: None,
                            });
                            return Task::none();
                        }
                    }
                    model::SubMenu::Pull { remote, opt } => {
                        if opt.is_some() {
                            *sub_menu = Some(model::SubMenu::Pull {
                                remote,
                                opt: None,
                            });
                            return Task::none();
                        }
                    }
                    model::SubMenu::CompareRemote {
                        remote,
                        remote_channel,
                        opt,
                    } => {
                        if opt.is_some() {
                            *sub_menu = Some(model::SubMenu::CompareRemote {
                                remote,
                                remote_channel,
                                opt: None,
                            });
                            return Task::none();
                        }
                    }
                    model::SubMenu::ResetChange
                    | model::SubMenu::InitRepo { path: _ }
                    | model::SubMenu::ImportFromGit { path: _ }
                    | model::SubMenu::Add { recursive: _ } => {}
                }
            } else if let Some(ReadyState {
                selection,
                record_changes: record_msg,
                forking_channel_name,
                ..
            }) = model::is_ready_mut(model)
            {
                if let Some(RecordChanges::Typing { msg, desc }) =
                    record_msg.as_ref()
                {
                    *record_msg = Some(RecordChanges::Canceled {
                        old_msg: msg.clone(),
                        old_desc: desc.text(),
                    });
                }
                match selection.primary {
                    selection::Primary::Status => {
                        if forking_channel_name.is_some() {
                            *forking_channel_name = None;
                        }
                    }
                    selection::Primary::Channel
                    | selection::Primary::EntireLog
                    | selection::Primary::CompareRemote => {
                        selection.primary = selection::Primary::default();
                    }
                }
            }
            Task::none()
        }
        action::FilteredMsg::Selection(msg) => {
            if let Some(ReadyState {
                repo,
                selection,
                navigation,
                logs,
                record_dichotomy,
                ..
            }) = model::is_ready_mut(model)
            {
                if let Some(sub) = sub_menu {
                    match sub {
                        model::SubMenu::Push { opt: Some(opt), .. } => {
                            return sub_menu_push_opt_selection(
                                msg,
                                &mut navigation.status_nav,
                                opt,
                                repo,
                            );
                        }
                        model::SubMenu::Pull { opt: Some(opt), .. } => {
                            return sub_menu_pull_opt_selection(
                                msg,
                                &mut navigation.status_nav,
                                opt,
                                repo,
                            );
                        }
                        model::SubMenu::CompareRemote {
                            opt: Some(opt),
                            ..
                        } => {
                            return sub_menu_compare_remote_opt_selection(
                                msg,
                                &mut navigation.status_nav,
                                opt,
                                repo,
                            );
                        }
                        model::SubMenu::Push { opt: _, remote: _ }
                        | model::SubMenu::Pull { opt: _, remote: _ }
                        | model::SubMenu::CompareRemote {
                            opt: _,
                            remote: _,
                            remote_channel: _,
                        }
                        | model::SubMenu::ResetChange
                        | model::SubMenu::InitRepo { path: _ }
                        | model::SubMenu::ImportFromGit { path: _ }
                        | model::SubMenu::Add { recursive: _ } => {}
                    }
                }

                let record_dichotomy =
                    selection.compare_remote.as_ref().and_then(
                        |selection::CompareRemote {
                             ix: _,
                             hash: _,
                             file: _,
                             remote,
                             remote_channel,
                         }| {
                            model::get_record_dichotomy(
                                record_dichotomy,
                                remote,
                                remote_channel,
                            )
                        },
                    );

                selection::update(
                    msg,
                    selection,
                    &mut state.files,
                    navigation,
                    repo,
                    logs,
                    record_dichotomy,
                )
                .map(Msg::ManagingRepo)
            } else if let model::ManagingRepoSubState::SelectingIdentity {
                ids,
                selection_ix,
                selection_nav: _,
                confirmed_selection_ix,
                repo: _,
            } = &mut model.sub
            {
                match msg {
                    selection::Msg::PressDir(dir) => match dir {
                        inflorescence_model::selection::Dir::Down => {
                            *confirmed_selection_ix = None;
                            if *selection_ix == ids.len().saturating_sub(1) {
                                *selection_ix = 0;
                            } else {
                                *selection_ix += 1;
                            }
                        }
                        inflorescence_model::selection::Dir::Up => {
                            *confirmed_selection_ix = None;
                            if *selection_ix == 0 {
                                *selection_ix = ids.len().saturating_sub(1);
                            } else {
                                *selection_ix -= 1;
                            }
                        }
                        inflorescence_model::selection::Dir::Right
                        | inflorescence_model::selection::Dir::Left => {}
                    },
                    selection::Msg::AltPressDir(_) => {}
                }
                Task::none()
            } else {
                Task::none()
            }
        }
        action::FilteredMsg::SaveRecord => save_record(state, model),
        action::FilteredMsg::PostponeRecord => defer_record(model),
        action::FilteredMsg::DiscardRecord => abandon_record(model),
        action::FilteredMsg::FocusNext => focus_next(model),
        action::FilteredMsg::FocusPrev => focus_prev(model),
        action::FilteredMsg::ToggleRecursive => toggle_recursive(sub_menu),
        action::FilteredMsg::RmChange => rm_change(state, model, sub_menu),
        action::FilteredMsg::StartRecord => start_record(model),
        action::FilteredMsg::SelectChannel => {
            if let Some(ReadyState {
                selection, repo, ..
            }) = model::is_ready_mut(model)
                && !repo.other_channels.is_empty()
            {
                selection.primary = selection::Primary::Channel;
            }
            Task::none()
        }
        action::FilteredMsg::ForkChannel => {
            if let Some(ReadyState {
                forking_channel_name,
                ..
            }) = model::is_ready_mut(model)
            {
                *forking_channel_name = Some(String::new());
                task::widget_focus_next()
            } else {
                Task::none()
            }
        }
        action::FilteredMsg::RefreshRepo => {
            state
                .repo_tx_in
                .send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
                .unwrap();
            Task::none()
        }
        action::FilteredMsg::ShowEntireLog => {
            if let Some(ReadyState {
                selection, logs, ..
            }) = model::is_ready_mut(model)
            {
                selection.primary = selection::Primary::EntireLog;
                match logs.entire_log.as_ref() {
                    None => {
                        state
                            .repo_tx_in
                            .send(repo::MsgIn::LoadEntireLog)
                            .unwrap();
                        logs.entire_log = Some(Log::Loading);
                    }
                    Some(Log::Loading | Log::Loaded { .. }) => {}
                }
            }
            Task::none()
        }
        action::FilteredMsg::ClipboardCopy => clipboard_copy(model),
        action::FilteredMsg::ToggleReports => {
            if !report.hidden {
                // Mark all entries as seen
                report
                    .entries
                    .iter_mut()
                    .for_each(|entry| entry.is_read = true);
            }
            report.hidden = !report.hidden;
            Task::none()
        }
        action::FilteredMsg::ClipboardCopyReports => {
            let to_copy = report::entries_to_string(report);
            task::clipboard_write(to_copy)
        }
        action::FilteredMsg::ToRecord(msg) => {
            if let Some(ReadyState {
                to_record, repo, ..
            }) = model::is_ready_mut(model)
            {
                to_record::update(to_record, msg, &repo.changed_files);
            }
            Task::none()
        }
        action::FilteredMsg::ToRecordToggleSelectedFileOrChange => {
            if let Some(ReadyState {
                to_record,
                repo,
                selection,
                navigation,
                ..
            }) = model::is_ready_mut(model)
                && let Some(selection::Status::ChangedFile {
                    ix: _,
                    path,
                    diff_selected,
                }) = selection.status.as_ref()
            {
                if *diff_selected {
                    let diff_ix = navigation
                        .files_diffs
                        .diffs_nav
                        .get_selected_section_ix()
                        .unwrap();
                    let selected_diff = repo
                        .changed_files
                        .get(path)
                        .and_then(|diffs| diffs.iter().nth(diff_ix));
                    if let Some(diff) = selected_diff {
                        let msg = to_record::Msg::ToggleChange {
                            path: path.clone(),
                            diff_id: diff::id_parts_hash(diff),
                        };
                        to_record::update(to_record, msg, &repo.changed_files);
                    }
                } else {
                    let msg = to_record::Msg::ToggleFile { path: path.clone() };
                    to_record::update(to_record, msg, &repo.changed_files);
                }
            }
            Task::none()
        }
        action::FilteredMsg::EnterSubMenu(new_sub_menu) => {
            *sub_menu = Some(new_sub_menu);
            Task::none()
        }
        action::FilteredMsg::SubMenuPushOption(mut new_opt) => {
            if let Some(model::SubMenu::Push { opt, remote }) = sub_menu {
                match &mut new_opt {
                    model::PushOption::SelectingRemote {
                        remote: selecting_remote,
                    } => {
                        *selecting_remote = remote.clone();
                    }
                }
                *opt = Some(new_opt);
            }
            Task::none()
        }
        action::FilteredMsg::SubMenuPullOption(mut new_opt) => {
            if let Some(model::SubMenu::Pull { opt, remote }) = sub_menu {
                match &mut new_opt {
                    model::PullOption::SelectingRemote {
                        remote: selecting_remote,
                    } => {
                        *selecting_remote = remote.clone();
                    }
                }
                *opt = Some(new_opt);
            }
            Task::none()
        }
        action::FilteredMsg::SubMenuCompareRemoteOption(mut new_opt) => {
            if let Some(model::SubMenu::CompareRemote {
                opt,
                remote,
                remote_channel,
            }) = sub_menu
            {
                let task = match &mut new_opt {
                    model::CompareRemoteOption::SelectingRemote {
                        remote: selecting_remote,
                    } => {
                        *selecting_remote = remote.clone();
                        Task::none()
                    }
                    model::CompareRemoteOption::InputingRemoteChannel {
                        channel: selecting_channel,
                    } => {
                        *selecting_channel = remote_channel.clone();
                        task::widget_focus_next()
                    }
                };
                *opt = Some(new_opt);
                return task;
            }
            Task::none()
        }
        action::FilteredMsg::ReloadIdentity => {
            load_identities_task().map(Msg::ManagingRepo)
        }
    };

    #[cfg(debug_assertions)]
    {
        // Check that selection corresponds to the navigation state
        if let Some(ReadyState {
            selection,
            navigation,
            ..
        }) = model::is_ready(model)
        {
            let selection::State {
                primary: _,
                status,
                channel,
                entire_log,
                compare_remote,
                held_key: _,
            } = selection;

            if let Some(selection) = status {
                match selection {
                    selection::Status::UntrackedFile { .. } => {}
                    selection::Status::ChangedFile { .. } => {}
                    selection::Status::LogChange(selection::LogChange {
                        hash,
                        file,
                        ..
                    }) => {
                        if let Some(selection::LogChangeFileSelection {
                            path,
                            ..
                        }) = file
                        {
                            assert_eq!(
                                navigation.status_logs_navs.change_hash,
                                Some(*hash)
                            );
                            assert_eq!(
                                navigation.status_logs_navs.file_id,
                                Some(file::log_id_parts_hash(*hash, path))
                            );
                        } else {
                            assert_eq!(
                                navigation.status_logs_navs.change_hash,
                                Some(*hash)
                            );
                        }
                    }
                }
            }

            if let Some(selection::Channel { name, log, .. }) = channel {
                assert_eq!(navigation.other_channel_name.as_ref(), Some(name));
                match log {
                    None => {}
                    Some(selection::LogChange {
                        hash, file: None, ..
                    }) => {
                        assert_eq!(
                            navigation.other_channel_logs_navs.change_hash,
                            Some(*hash)
                        );
                    }
                    Some(selection::LogChange {
                        hash,
                        file:
                            Some(selection::LogChangeFileSelection { path, .. }),
                        ..
                    }) => {
                        assert_eq!(
                            navigation.other_channel_logs_navs.change_hash,
                            Some(*hash)
                        );
                        assert_eq!(
                            navigation.other_channel_logs_navs.file_id,
                            Some(file::log_id_parts_hash(*hash, path))
                        );
                    }
                }
            }

            if let Some(selection::LogChange { hash, file, .. }) = entire_log {
                assert_eq!(
                    navigation.entire_logs_navs.change_hash,
                    Some(*hash)
                );
                if let Some(selection::LogChangeFileSelection {
                    path, ..
                }) = file
                {
                    assert_eq!(
                        navigation.entire_logs_navs.change_hash,
                        Some(*hash)
                    );

                    let file_id = file::log_id_parts_hash(*hash, path);
                    if navigation.log_diffs.diffs.contains_key(&file_id) {
                        assert_eq!(
                            navigation.entire_logs_navs.file_id,
                            Some(file_id)
                        );
                    }
                } else {
                    assert_eq!(
                        navigation.entire_logs_navs.change_hash,
                        Some(*hash)
                    );
                }
            }

            if let Some(selection::CompareRemote {
                ix: _,
                hash: Some(hash),
                file,
                remote: _,
                remote_channel: _,
            }) = compare_remote
            {
                assert_eq!(
                    navigation.compare_remote_navs.change_hash,
                    Some(*hash)
                );
                if let Some(selection::LogChangeFileSelection {
                    path, ..
                }) = file
                {
                    assert_eq!(
                        navigation.compare_remote_navs.change_hash,
                        Some(*hash)
                    );

                    let file_id = file::log_id_parts_hash(*hash, path);
                    if navigation.log_diffs.diffs.contains_key(&file_id) {
                        assert_eq!(
                            navigation.compare_remote_navs.file_id,
                            Some(file_id)
                        );
                    }
                } else {
                    assert_eq!(
                        navigation.compare_remote_navs.change_hash,
                        Some(*hash)
                    );
                }
            }
        }
    }

    task
}

fn sub_menu_push_opt_selection(
    msg: selection::Msg,
    nav: &mut nav_scrollable::State,
    opt: &mut model::PushOption,
    repo: &mut repo::State,
) -> Task<Msg> {
    let repo::Remotes { default, other } = &repo.remotes;
    let remote_names: Vec<_> = default.iter().chain(other).collect();
    match opt {
        model::PushOption::SelectingRemote { remote } => {
            remote_selection(msg, nav, remote, remote_names)
        }
    }
}

fn sub_menu_pull_opt_selection(
    msg: selection::Msg,
    nav: &mut nav_scrollable::State,
    opt: &mut model::PullOption,
    repo: &mut repo::State,
) -> Task<Msg> {
    let repo::Remotes { default, other } = &repo.remotes;
    let remote_names: Vec<_> = default.iter().chain(other).collect();
    match opt {
        model::PullOption::SelectingRemote { remote } => {
            remote_selection(msg, nav, remote, remote_names)
        }
    }
}

fn sub_menu_compare_remote_opt_selection(
    msg: selection::Msg,
    nav: &mut nav_scrollable::State,
    opt: &mut model::CompareRemoteOption,
    repo: &mut repo::State,
) -> Task<Msg> {
    match opt {
        model::CompareRemoteOption::SelectingRemote { remote } => {
            let repo::Remotes { default, other } = &repo.remotes;
            let remote_names: Vec<_> = default.iter().chain(other).collect();
            remote_selection(msg, nav, remote, remote_names)
        }
        model::CompareRemoteOption::InputingRemoteChannel { channel: _ } => {
            // Remote channel is just a text input
            Task::none()
        }
    }
}

fn remote_selection(
    msg: selection::Msg,
    nav: &mut nav_scrollable::State,
    remote: &mut Option<String>,
    remote_names: Vec<&String>,
) -> Task<Msg> {
    match msg {
        selection::Msg::PressDir(dir) => match dir {
            selection::Dir::Down => {
                if let Some(current) = remote {
                    let ix = remote_names.iter().enumerate().find_map(
                        |(ix, name)| (*name == current).then_some(ix),
                    );
                    if let Some(ix) = ix
                        && ix < remote_names.len().saturating_sub(1)
                    {
                        let ix = ix + 1;
                        nav_scrollable::scroll_down_to_section(nav, ix);
                        *remote = remote_names.into_iter().nth(ix).cloned();
                        return Task::none();
                    }
                }
                nav_scrollable::scroll_up_to_section(nav, 0);
                *remote = remote_names.into_iter().next().cloned();
            }
            selection::Dir::Up => {
                if let Some(current) = remote {
                    let ix = remote_names.iter().enumerate().find_map(
                        |(ix, name)| (*name == current).then_some(ix),
                    );
                    if let Some(ix) = ix
                        && ix > 0
                    {
                        let ix = ix - 1;
                        nav_scrollable::scroll_up_to_section(nav, ix);
                        *remote = remote_names.into_iter().nth(ix).cloned();
                        return Task::none();
                    }
                }
                nav_scrollable::scroll_down_to_section(
                    nav,
                    remote_names.len().saturating_sub(1),
                );
                *remote = remote_names.into_iter().next_back().cloned();
            }
            selection::Dir::Right | selection::Dir::Left => {}
        },
        selection::Msg::AltPressDir(_) => {}
    }
    Task::none()
}

fn load_identities_task() -> Task<ManagingRepoMsg> {
    Task::future(async {
        let ids = spawn_blocking(identity::load).await.unwrap();
        ManagingRepoMsg::LoadedIdentities(ids)
    })
}

fn toggle_recursive(sub_menu: &mut Option<model::SubMenu>) -> Task<Msg> {
    if let Some(model::SubMenu::Add { recursive }) = sub_menu.as_mut() {
        *recursive = !*recursive;
    }
    Task::none()
}

fn push(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
    remote: Option<String>,
) -> Task<Msg> {
    if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
        if let Some(remote) =
            remote.clone().or_else(|| repo.remotes.default.clone())
        {
            jobs.insert(model::Job::Push {
                remote: remote.clone(),
                channel: repo.channel.clone(),
            });
            state
                .repo_tx_in
                .send(repo::MsgIn::Push {
                    remote,
                    channel: repo.channel.clone(),
                })
                .unwrap();
        } else {
            report::show_err(
                report,
                "Cannot push as there is no default remote configured."
                    .to_string(),
            );
        }
    }
    *sub_menu = None;
    Task::none()
}

fn pull(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
    remote: Option<String>,
) -> Task<Msg> {
    if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
        if let Some(remote) =
            remote.clone().or_else(|| repo.remotes.default.clone())
        {
            jobs.insert(model::Job::Pull {
                remote: remote.clone(),
                channel: repo.channel.clone(),
            });
            state
                .repo_tx_in
                .send(repo::MsgIn::Pull {
                    remote,
                    channel: repo.channel.clone(),
                })
                .unwrap();
        } else {
            report::show_err(
                report,
                "Cannot pull as there is no default remote configured."
                    .to_string(),
            );
        }
    }
    *sub_menu = None;
    Task::none()
}

fn reset_change(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
    report: &mut report::Container,
) -> Task<Msg> {
    if let Some(ReadyState { selection, .. }) = model::is_ready_mut(model)
        && let Some(selection::Status::ChangedFile {
            ix: _,
            path,
            diff_selected,
        }) = selection.status.as_ref()
    {
        if !diff_selected {
            state
                .repo_tx_in
                .send(repo::MsgIn::ResetFile {
                    path: path.raw.clone(),
                })
                .unwrap();
        } else {
            todo!("reset selected hunk")
        }
    } else {
        report::show_err(
            report,
            "Cannot reset the current selection. This should never happen, please report it! State: {state:?}"
                .to_string(),
        );
    }
    *sub_menu = None;
    Task::none()
}

fn clipboard_copy(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState { selection, .. }) = model::is_ready(model) {
        let to_copy = match selection::unify(selection) {
            selection::Unified::Status(Some(
                inflorescence_model::selection::Status::LogChange(
                    selection::LogChange { hash, .. },
                ),
            )) => Some(repo::hash_to_string(hash)),
            selection::Unified::Channel(Some(selection::Channel {
                log: Some(selection::LogChange { hash, .. }),
                ..
            })) => Some(repo::hash_to_string(hash)),
            selection::Unified::EntireLog(Some(selection::LogChange {
                hash,
                ..
            })) => Some(repo::hash_to_string(hash)),
            selection::Unified::CompareRemote(Some(
                selection::CompareRemote {
                    hash: Some(hash), ..
                },
            )) => Some(repo::hash_to_string(hash)),
            selection::Unified::Status(_)
            | selection::Unified::Channel(_)
            | selection::Unified::EntireLog(_)
            | selection::Unified::CompareRemote(_) => None,
        };
        if let Some(to_copy) = to_copy {
            task::clipboard_write(to_copy)
        } else {
            Task::none()
        }
    } else {
        Task::none()
    }
}

fn update_from_repo(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    report: &mut report::Container,
    msg: repo::MsgOut,
) -> (Task<Msg>, Option<model::SubState>) {
    let mut new_state = None;
    let task = match msg {
        repo::MsgOut::Init(repo) => {
            repo_init(state, model, report, repo).map(Msg::ManagingRepo)
        }
        repo::MsgOut::InitFailed {
            err,
            switch_to_project_picker,
        } => {
            report::show_err(report, err);
            if switch_to_project_picker {
                let (sub, projects_task) = init_picking_project();
                new_state = Some(sub);
                projects_task
            } else {
                Task::none()
            }
        }
        repo::MsgOut::RepoTaskExited => {
            report::show_err(report, "Task managing repo has crashed. This shouldn't happen, please report what happened!".to_string());
            Task::none()
        }
        repo::MsgOut::Refreshed {
            state: repo_state,
            invalidate_logs,
        } => match repo_state {
            Ok(repo_state) => {
                repo_refreshed(state, model, repo_state, invalidate_logs)
                    .map(Msg::ManagingRepo)
            }
            Err(err) => {
                let msg = format!("Failed to refresh repository with {err:?}");
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::AddedUntrackedFile { result, path: _ } => match result {
            Ok(()) => Task::none(),
            Err(err) => {
                let msg = format!("Failed to add untracked file with {err:?}");
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::RmedAddedFile { result, path: _ } => match result {
            Ok(()) => Task::none(),
            Err(err) => {
                let msg = format!("Failed to remove added file with {err:?}");
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::GotChangeDiffs { hash, diffs } => match diffs {
            Ok(diffs) => {
                repo_got_change_diffs(model, hash, diffs).map(Msg::ManagingRepo)
            }
            Err(err) => {
                let msg = format!(
                    "Failed to get diff of change {} with {err:?}",
                    repo::hash_to_string(&hash)
                );
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::LoadedEntireLog(log) => match log {
            Ok(log) => got_entire_log(model, log).map(Msg::ManagingRepo),
            Err(err) => {
                let msg = format!("Failed to load a log with {err:?}");
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::LoadedOtherChannelLog { channel, log } => match log {
            Ok(log) => loaded_other_channel_log(model, channel, log)
                .map(Msg::ManagingRepo),
            Err(err) => {
                let msg = format!(
                    "Failed to load a log of channel {channel} with {err:?}"
                );
                report::show_err(report, msg);
                Task::none()
            }
        },
        repo::MsgOut::Pushed {
            remote,
            channel,
            result,
        } => {
            match result {
                Ok(()) => {
                    if let Some(ReadyState {
                        record_dichotomy, ..
                    }) = model::is_ready_mut(model)
                        && let Some(record_dichotomy) =
                            model::get_record_dichotomy_mut(
                                record_dichotomy,
                                &remote,
                                &channel,
                            )
                    {
                        let repo::RecordDichotomy {
                            local_records,
                            remote_records,
                            remote_unrecords,
                        } = record_dichotomy;
                        // Remove remote unrecords that were re-pushed
                        remote_unrecords.retain(|entry| {
                            local_records
                                .iter()
                                .all(|local| local.hash != entry.hash)
                        });
                        // Move local records into remote records
                        remote_records.extend(mem::take(local_records));
                    }
                    report::show_info(
                        report,
                        format!("Pushed to {remote}, channel {channel}"),
                    )
                }
                Err(repo::PushError::Empty) => report::show_info(
                    report,
                    format!("Nothing to push to {remote}, channel {channel}"),
                ),
                Err(err) => {
                    let msg = format!(
                        "Failed to push to {remote}, channel {channel} with {err:?}"
                    );
                    report::show_err(report, msg);
                }
            };
            if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
                jobs.swap_remove(&Job::Push { remote, channel });
            }
            Task::none()
        }
        repo::MsgOut::Pulled {
            remote,
            channel,
            result,
        } => {
            match result {
                Ok(()) => {
                    if let Some(ReadyState {
                        record_dichotomy, ..
                    }) = model::is_ready_mut(model)
                        && let Some(record_dichotomy) =
                            model::get_record_dichotomy_mut(
                                record_dichotomy,
                                &remote,
                                &channel,
                            )
                    {
                        let repo::RecordDichotomy {
                            local_records: _,
                            remote_records,
                            remote_unrecords: _,
                        } = record_dichotomy;
                        // Remove remote records as they were pulled
                        *remote_records = default();
                    }
                    report::show_info(
                        report,
                        format!("Pulled from {remote}, channel {channel}"),
                    )
                }
                Err(repo::PullError::Empty) => report::show_info(
                    report,
                    format!("Nothing to pull from {remote}, channel {channel}"),
                ),
                Err(err) => {
                    let msg = format!(
                        "Failed to pull from {remote}, channel {channel} with {err:?}"
                    );
                    report::show_err(report, msg);
                }
            };
            if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
                jobs.swap_remove(&Job::Pull { remote, channel });
            }
            Task::none()
        }
        repo::MsgOut::ComparedRemote {
            result,
            remote_channel,
            remote,
        } => {
            match result {
                Ok(repo::ComparedRemote {
                    record_dichotomy: d,
                    remote,
                    remote_channel,
                }) => {
                    if let Some(ReadyState {
                        record_dichotomy, ..
                    }) = model::is_ready_mut(model)
                    {
                        record_dichotomy
                            .entry(remote)
                            .or_default()
                            .insert(remote_channel, d);
                    }
                }
                Err(err) => {
                    let msg =
                        format!("Failed to compare with remote with {err:?}. Remote {remote:?}, channel: {remote_channel:?}");
                    report::show_err(report, msg);
                }
            }
            if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
                jobs.swap_remove(&Job::CompareRemote {
                    remote,
                    remote_channel,
                });
            }
            Task::none()
        }
    };
    (task, new_state)
}

fn add_untracked_file(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    recursive: bool,
) -> Task<Msg> {
    if let Some(ReadyState {
        repo,
        selection,
        navigation,
        logs,
        to_record,
        ..
    }) = model::is_ready_mut(model)
        && let Some(selection::Status::UntrackedFile {
            ix,
            path,
            diff_selected: _,
        }) = selection.status.as_ref()
    {
        state
            .repo_tx_in
            .send(repo::MsgIn::AddUntrackedFile {
                path: path.raw.clone(),
                recursive,
            })
            .unwrap();

        // If we started tracking a dir it might have contents, so refresh to
        // find them
        if path.is_dir {
            state
                .repo_tx_in
                .send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
                .unwrap();
        }

        let removed = repo.untracked_files.remove(path);
        debug_assert!(
            removed,
            "{:?}, path: {}",
            repo.untracked_files, path.raw
        );
        repo.changed_files
            .entry(path.clone())
            .or_default()
            .insert(repo::ChangedFileDiff::Add { contents: None });

        // Add this file to to-record
        to_record::add_untracked_file_to_record(to_record, path.clone());

        // Select the next untracked file, if any
        if repo.untracked_files.is_empty() {
            selection.status = None;
        } else {
            let ix = cmp::min(*ix, repo.untracked_files.len() - 1);
            let (new_selection, selection_task) =
                selection::untracked_file_selection(
                    ix,
                    selection::VDir::Down,
                    &mut selection::Ctx {
                        state: selection,
                        files: &mut state.files,
                        navigation,
                        repo,
                        logs,
                        record_dichotomy: None,
                    },
                );

            selection.status = Some(new_selection);
            return selection_task.map(Msg::ManagingRepo);
        };

        return Task::none();
    }
    Task::none()
}

fn rm_change(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    sub_menu: &mut Option<model::SubMenu>,
) -> Task<Msg> {
    if let Some(ReadyState {
        repo,
        selection,
        navigation,
        logs,
        to_record,
        ..
    }) = model::is_ready_mut(model)
        && let Some(selection::Status::ChangedFile {
            ix,
            path,
            diff_selected: _,
        }) = selection.status.as_ref()
    {
        let diffs = repo.changed_files.get(path).unwrap();

        // See if the change is a previously untracked file
        if diffs
            .iter()
            .any(|diff| matches!(diff, repo::ChangedFileDiff::Add { .. }))
        {
            state
                .repo_tx_in
                .send(repo::MsgIn::RmAddedFile {
                    path: path.raw.clone(),
                })
                .unwrap();

            // If we removed added dir it might have contents, so refresh to to
            // hide them again
            if path.is_dir {
                state
                    .repo_tx_in
                    .send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
                    .unwrap();
            }

            // Remove from changed files
            let removed = repo.changed_files.remove(path);
            debug_assert!(
                removed.is_some(),
                "{:?} not found in {:?}",
                path.raw,
                repo.changed_files
            );
            // Update untracked files
            repo.untracked_files.insert(path.clone());

            // Rm this file from to-record
            to_record::rm_untracked_file_to_record(
                to_record,
                path,
                &repo.changed_files,
            );

            // Select the next changed file, if any
            if repo.changed_files.is_empty() {
                selection.status = None
            } else {
                let ix = cmp::min(*ix, repo.changed_files.len() - 1);
                let (new_selection, selection_task) =
                    selection::changed_file_selection(
                        ix,
                        selection::VDir::Down,
                        &mut selection::Ctx {
                            state: selection,
                            files: &mut state.files,
                            navigation,
                            repo,
                            logs,
                            record_dichotomy: None,
                        },
                    );

                selection.status = Some(new_selection);
                return selection_task.map(Msg::ManagingRepo);
            };
        } else {
            *sub_menu = Some(model::SubMenu::ResetChange);
        }
    }
    Task::none()
}

fn start_record(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState {
        repo,
        record_changes,
        ..
    }) = model::is_ready_mut(model)
    {
        if repo.changed_files.is_empty() {
            info!("Trying to record with no changes");
        } else if let Some(RecordChanges::Typing { .. }) =
            record_changes.as_ref()
        {
            info!("Requested to record, but already recording");
            return Task::none();
        } else {
            let (msg, desc) = match record_changes.take() {
                Some(RecordChanges::Canceled { old_msg, old_desc }) => {
                    (old_msg, text_editor::Content::with_text(&old_desc))
                }
                None | Some(RecordChanges::Typing { .. }) => {
                    (String::new(), text_editor::Content::new())
                }
            };
            *record_changes = Some(RecordChanges::Typing { msg, desc });
            // TODO: change to use ID once https://github.com/iced-rs/iced/pull/2653 is merged
            return task::widget_focus_next();
        }
    }
    Task::none()
}

fn edit_record_msg(
    model: &mut model::ManagingRepo,
    new_msg: String,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState {
        record_changes: Some(RecordChanges::Typing { msg, desc: _ }),
        ..
    }) = model::is_ready_mut(model)
    {
        *msg = new_msg;
    }
    Task::none()
}

fn edit_record_desc(
    model: &mut model::ManagingRepo,
    action: text_editor::Action,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState {
        record_changes: Some(RecordChanges::Typing { msg: _, desc }),
        ..
    }) = model::is_ready_mut(model)
    {
        desc.perform(action);
    }
    Task::none()
}

fn save_record(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
) -> Task<Msg> {
    if let Some(ReadyState {
        record_changes,
        repo,
        user_id,
        selection,
        to_record,
        ..
    }) = model::is_ready_mut(model)
        && let Some(RecordChanges::Typing { msg, desc }) =
            record_changes.as_ref()
    {
        let msg = msg.trim().to_string();
        let desc = desc.text();
        let desc = desc.trim();
        let desc = if desc.is_empty() {
            None
        } else {
            Some(desc.to_string())
        };
        if msg.is_empty() {
            info!("Cannot record with an empty message");
        } else {
            // TODO: Config is only used for terminal
            let config = PijulConfig::default();
            let use_keyring = true;
            // TODO: this call has CLI prompt - replace it
            let (sk, _) = user_id.decrypt(&config, use_keyring).unwrap();
            let sk = Arc::new(sk);
            let id_pk = user_id.public_key.key.clone();
            state
                .repo_tx_in
                .send(repo::MsgIn::Record {
                    msg,
                    desc,
                    sk,
                    id_pk,
                    to_record: to_record.clone(),
                })
                .unwrap();

            // Reset most things
            *to_record = default();
            *selection = selection::State::default();
            *record_changes = None;

            repo.changed_files = repo::ChangedFiles::default();
            file::diffs_cache_clear(&mut state.files.diffs_cache);
        }
    }
    Task::none()
}

fn defer_record(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState { record_changes, .. }) = model::is_ready_mut(model)
        && let Some(RecordChanges::Typing { msg, desc }) =
            record_changes.as_ref()
    {
        let old_msg = msg.trim();
        let old_desc = desc.text();
        let old_desc = old_desc.trim();
        *record_changes = if !old_msg.is_empty() || !old_desc.is_empty() {
            let old_msg = old_msg.to_string();
            let old_desc = old_desc.to_string();
            Some(RecordChanges::Canceled { old_msg, old_desc })
        } else {
            None
        };
    }
    Task::none()
}

fn abandon_record(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState {
        record_changes: record_msg,
        ..
    }) = model::is_ready_mut(model)
        && let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
    {
        *record_msg = None;
    }
    Task::none()
}

fn focus_next(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState {
        record_changes: record_msg,
        ..
    }) = model::is_ready_mut(model)
        && let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
    {
        task::widget_focus_next()
    } else {
        Task::none()
    }
}

fn focus_prev(model: &mut model::ManagingRepo) -> Task<Msg> {
    if let Some(ReadyState {
        record_changes: record_msg,
        ..
    }) = model::is_ready_mut(model)
        && let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
    {
        task::widget_focus_previous()
    } else {
        Task::none()
    }
}

fn repo_init(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    report: &mut report::Container,
    repo_state: repo::State,
) -> Task<ManagingRepoMsg> {
    match &mut model.sub {
        model::ManagingRepoSubState::Loading { user_ids, repo: _ } => {
            if let Some(user_ids) = user_ids {
                model.sub = if user_ids.len() == 1 {
                    let user_id = user_ids.pop().unwrap();
                    model::ManagingRepoSubState::Ready(ReadyState {
                        user_id,
                        repo: repo_state,
                        selection: default(),
                        navigation: default(),
                        record_changes: default(),
                        forking_channel_name: default(),
                        logs: default(),
                        to_record: default(),
                        jobs: default(),
                        record_dichotomy: default(),
                    })
                } else {
                    model::ManagingRepoSubState::SelectingIdentity {
                        ids: mem::take(user_ids),
                        selection_ix: default(),
                        selection_nav: default(),
                        confirmed_selection_ix: default(),
                        repo: Some(repo_state),
                    }
                }
            }
        }
        model::ManagingRepoSubState::SelectingIdentity { repo, .. } => {
            *repo = Some(repo_state)
        }
        model::ManagingRepoSubState::Ready(_) => {}
        model::ManagingRepoSubState::NoIdFound { repo } => {
            *repo = Some(repo_state)
        }
    };

    file::diffs_cache_clear(&mut state.files.diffs_cache);

    // Start watching the repo's dir for changes
    let (fs_watch_tx, fs_watch_rx) = watch::channel(model.repo_path.clone());
    let mut fs_watch = new_debouncer(
        Duration::from_secs(1),
        None,
        move |result: DebounceEventResult| match result {
            Ok(events) => events.iter().for_each(|event| {
                // dbg!(event);
                if event.kind.is_create()
                    || event.kind.is_modify()
                    || event.kind.is_remove()
                {
                    event.paths.iter().for_each(|path| {
                        let _ = fs_watch_tx.send(path.clone());
                    })
                }
            }),
            Err(errors) => {
                errors.iter().for_each(|error| eprintln!("{error:?}"))
            }
        },
    )
    .unwrap();

    let mut already_got_max_files_err = false;
    for entry in ignore::WalkBuilder::new(&model.repo_path)
        .standard_filters(false) // disable git related stuff
        .ignore(true) // use ".ignore" file
        .build()
        // ignore errors
        .flatten()
    {
        if let Err(err) =
            fs_watch.watch(entry.path(), RecursiveMode::NonRecursive)
        {
            let msg = if let notify::ErrorKind::MaxFilesWatch = &err.kind {
                {
                    if already_got_max_files_err {
                        continue;
                    } else {
                        already_got_max_files_err = true;
                    }
                }
                #[cfg(target_os = "linux")]
                const HELP: &str = " Consider increasing the limits (`fs.inotify.max_user_instances` and/or `fs.inotify.max_user_watches`)";
                #[cfg(not(target_os = "linux"))]
                const MORE: &str = "";
                format!("Error setting up file watch. Reached a maximum.{HELP}")
            } else {
                format!(
                    "Error setting up file watch for path {}: {err:?}",
                    entry.path().to_string_lossy()
                )
            };
            error!("{msg}");
            report::show_err(report, msg)
        }
    }
    let fs_watch_rx = WatchStream::from_changes(fs_watch_rx);
    let watch_task = Task::run(fs_watch_rx, ManagingRepoMsg::WatchedFileChange);

    state.repo_fs_watch = Some(fs_watch);

    watch_task
}

fn repo_refreshed(
    state: &mut ManagingRepo,
    model: &mut model::ManagingRepo,
    repo_state: repo::State,
    invalidate_logs: bool,
) -> Task<ManagingRepoMsg> {
    file::diffs_cache_clear(&mut state.files.diffs_cache);

    match &mut model.sub {
        model::ManagingRepoSubState::Loading { user_ids: _, repo } => {
            *repo = Some(repo_state)
        }
        model::ManagingRepoSubState::SelectingIdentity { repo, .. } => {
            *repo = Some(repo_state)
        }
        model::ManagingRepoSubState::Ready(ReadyState {
            repo,
            user_id: _,
            selection,
            navigation: _,
            record_changes: _,
            forking_channel_name: _,
            logs,
            to_record: _,
            jobs: _,
            record_dichotomy: _,
        }) => {
            *repo = repo_state;
            // Throw away the logs
            if invalidate_logs {
                *logs = default();
            };

            // TODO update to_record state

            // Re-index selection
            let selection_task =
                reindex_selection(repo, selection, &mut state.files, logs);
            return selection_task;
        }
        model::ManagingRepoSubState::NoIdFound { repo } => {
            *repo = Some(repo_state)
        }
    }
    Task::none()
}

/// Try to find the file with the same name. If not found, remove selection
fn reindex_selection(
    repo: &repo::State,
    selection: &mut selection::State,
    files: &mut file::State,
    logs: &Logs,
) -> Task<ManagingRepoMsg> {
    let status_task = if let Some(current_selection) = selection.status.take() {
        let (new_selection, task) = match current_selection {
            selection::Status::UntrackedFile {
                ix: _,
                path,
                diff_selected,
            } => {
                let selection = repo
                    .untracked_files
                    .iter()
                    .enumerate()
                    .find(|(_ix, file_path)| *file_path == &path)
                    .map(|(ix, _path)| selection::Status::UntrackedFile {
                        ix,
                        path: path.clone(),
                        diff_selected,
                    });

                if selection.is_some() {
                    file::load_src_file_if_not_cached(
                        files,
                        file::Id {
                            path,
                            file_kind: file::Kind::Untracked,
                        },
                    );
                }

                (selection, Task::none())
            }
            selection::Status::ChangedFile {
                ix: _,
                path,
                diff_selected,
            } => {
                let selection = repo
                    .changed_files
                    .iter()
                    .enumerate()
                    .find(|(_ix, (file_path, _diffs))| *file_path == &path)
                    .map(|(ix, (file_path, _diffs))| {
                        selection::Status::ChangedFile {
                            ix,
                            path: file_path.clone(),
                            diff_selected,
                        }
                    });

                let task = if selection.is_some()
                    && let Some(diffs) = repo.changed_files.get(&path)
                {
                    let id = file::Id {
                        path: path.clone(),
                        file_kind: file::Kind::Changed,
                    };
                    if diff::should_file_exist(diffs) {
                        file::load_src_file_if_not_cached(files, id);
                        Task::none()
                    } else {
                        file::src_file_doesnt_exist(files, id, diffs)
                            .map(ManagingRepoMsg::File)
                    }
                } else {
                    Task::none()
                };

                (selection, task)
            }
            selection::Status::LogChange(selection::LogChange {
                ix: _,
                hash,
                message,
                file,
            }) => {
                let selection = repo
                    .short_log
                    .iter()
                    .enumerate()
                    .find(|(_ix, entry)| entry.hash == hash)
                    .map(|(ix, entry)| {
                        let file = file.and_then(
                            |selection::LogChangeFileSelection {
                                 ix: _,
                                 path: selected_path,
                                 diff_selected,
                             }| {
                                entry
                                    .file_paths
                                    .iter()
                                    .enumerate()
                                    .find(|(_ix, path)| *path == &selected_path)
                                    .map(|(ix, path)| {
                                        selection::LogChangeFileSelection {
                                            ix,
                                            path: path.clone(),
                                            diff_selected,
                                        }
                                    })
                            },
                        );
                        selection::Status::LogChange(selection::LogChange {
                            ix,
                            hash: entry.hash,
                            message,
                            file,
                        })
                    });

                let task = if selection.is_some() {
                    // Request to get the diffs
                    Task::done(ManagingRepoMsg::ToRepo(
                        repo::MsgIn::GetChangeDiffs { hash },
                    ))
                } else {
                    Task::none()
                };

                (selection, task)
            }
        };

        selection.status = new_selection;

        task
    } else {
        Task::none()
    };

    let channel_task = if let Some(selection::Channel { ix: _, name, log }) =
        selection.channel.take()
    {
        let (new_selection, task) = {
            let selection = repo
                .other_channels
                .iter()
                .enumerate()
                .find(|(_ix, channel_name)| *channel_name == &name)
                // TODO: re-index log selection once loaded for channels
                .map(|(ix, _name)| selection::Channel { ix, name, log });

            let task = if let Some(selection::Channel { name, .. }) =
                selection.as_ref()
            {
                // Request to get the log
                Task::done(ManagingRepoMsg::ToRepo(
                    repo::MsgIn::LoadOtherChannelLog(name.clone()),
                ))
            } else {
                Task::none()
            };

            (selection, task)
        };
        selection.channel = new_selection;
        task
    } else {
        Task::none()
    };

    let entire_log_task = if let Some(selection::LogChange {
        ix: _,
        hash,
        message,
        file,
    }) = selection.entire_log.take()
    {
        let new_selection =
            if let Some(Log::Loaded { log }) = logs.entire_log.as_ref() {
                log.iter()
                    .enumerate()
                    .find(|(_ix, entry)| entry.hash == hash)
                    .map(|(ix, entry)| {
                        let file = file.and_then(
                            |selection::LogChangeFileSelection {
                                 ix: _,
                                 path: selected_path,
                                 diff_selected,
                             }| {
                                entry
                                    .file_paths
                                    .iter()
                                    .enumerate()
                                    .find(|(_ix, path)| *path == &selected_path)
                                    .map(|(ix, path)| {
                                        selection::LogChangeFileSelection {
                                            ix,
                                            path: path.clone(),
                                            diff_selected,
                                        }
                                    })
                            },
                        );
                        selection::LogChange {
                            ix,
                            hash: entry.hash,
                            message,
                            file,
                        }
                    })
            } else {
                None
            };

        let task = if new_selection.is_some() {
            // Request to get the entire log
            Task::done(ManagingRepoMsg::ToRepo(repo::MsgIn::LoadEntireLog))
        } else {
            Task::none()
        };

        selection.entire_log = new_selection;

        task
    } else {
        Task::none()
    };

    Task::batch([status_task, channel_task, entire_log_task])
}

fn repo_got_change_diffs(
    model: &mut model::ManagingRepo,
    hash: repo::ChangeHash,
    diffs: repo::ChangedFiles,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState {
        navigation,
        selection,
        ..
    }) = model::is_ready_mut(model)
    {
        // Store the changes
        navigation.log_diffs.changes_with_loaded_diffs.insert(hash);
        diffs.into_iter().for_each(|(path, diffs)| {
            // NOTE: using unknown encoding as we don't yet have the file
            // for past changes
            let file = diff::init_file(
                diff::FileContent::UnknownEncoding,
                Some(&diffs),
            );
            let file_id = file::log_id_parts_hash(hash, &path.raw);
            let log_file_diff = diff::FileAndState {
                file,
                // The nav is initialized only once a file is selected,
                // because its tasks need it to be visible to complete
                state: diff::State::default(),
            };

            navigation.log_diffs.diffs.insert(file_id, log_file_diff);
        });

        // If a file is selected, init the nav for its diff
        let selected_file_and_its_navs = match selection::unify(selection) {
            selection::Unified::Status(Some(selection::Status::LogChange(
                selection::LogChange {
                    ix: _,
                    hash: selected_hash,
                    message: _,
                    file,
                },
            ))) if *selected_hash == hash => file
                .as_ref()
                .map(|file| (file, &mut navigation.status_logs_navs)),
            selection::Unified::EntireLog(Some(selection::LogChange {
                ix: _,
                hash: selected_hash,
                message: _,
                file,
            })) if *selected_hash == hash => file
                .as_ref()
                .map(|file| (file, &mut navigation.entire_logs_navs)),
            selection::Unified::Channel(Some(selection::Channel {
                ix: _,
                name: _,
                log:
                    Some(selection::LogChange {
                        ix: _,
                        hash: selected_hash,
                        message: _,
                        file,
                    }),
            })) if *selected_hash == hash => file
                .as_ref()
                .map(|file| (file, &mut navigation.other_channel_logs_navs)),
            selection::Unified::CompareRemote(Some(
                selection::CompareRemote {
                    hash: Some(selected_hash),
                    file,
                    ..
                },
            )) if *selected_hash == hash => file
                .as_ref()
                .map(|file| (file, &mut navigation.compare_remote_navs)),
            selection::Unified::Status(_)
            | selection::Unified::Channel(_)
            | selection::Unified::EntireLog(_)
            | selection::Unified::CompareRemote(_) => None,
        };

        if let Some((file, navs)) = selected_file_and_its_navs {
            let file_id = file::log_id_parts_hash(hash, &file.path);
            if let Some(log) = navigation.log_diffs.diffs.get(&file_id) {
                let unchanged_sections = diff::unchanged_sections(&log.file);
                log::init_diffs_nav(navs, file_id)
                    .set_skip_sections(unchanged_sections);
            }
        }
    }
    Task::none()
}

fn got_entire_log(
    model: &mut model::ManagingRepo,
    log: repo::Log,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState { logs, .. }) = model::is_ready_mut(model) {
        logs.entire_log = Some(Log::Loaded { log });
    }
    Task::none()
}

fn loaded_other_channel_log(
    model: &mut model::ManagingRepo,
    channel: String,
    log: Vec<repo::LogEntry>,
) -> Task<ManagingRepoMsg> {
    if let Some(ReadyState { logs, .. }) = model::is_ready_mut(model) {
        logs.other_channels_logs
            .insert(channel, Log::Loaded { log });
    }
    Task::none()
}

fn subs(state: &State) -> Subscription<Msg> {
    use iced::keyboard::{self, key, Key};

    let allowed_actions = state.model.allowed_actions.clone();
    let key_subs = keyboard::listen().with(allowed_actions).filter_map(
        |(allowed_actions, event)| {
            let unfiltered = |selection| {
                Some(Msg::View(view::Msg::UnfilteredSelection(selection)))
            };

            match event {
                keyboard::Event::KeyPressed { key, modifiers, .. } => {
                    allowed_actions
                        .iter()
                        .find_map(|binding| {
                            binding.msg.as_ref().and_then(|msg| {
                                if binding.keys.iter().any(|binding| {
                                    binding.key == key
                                        && binding.mods == modifiers
                                }) {
                                    Some(msg.clone())
                                } else {
                                    None
                                }
                            })
                        })
                        .map(view::Msg::Action)
                        .map(Msg::View)
                }
                keyboard::Event::KeyReleased {
                    key,
                    modifiers: mods,
                    ..
                } => {
                    if mods.is_empty() {
                        match key {
                            Key::Character(c) => match c.as_str() {
                                "j" => unfiltered(
                                    selection::UnfilteredMsg::ReleaseDir(
                                        selection::Dir::Down,
                                    ),
                                ),
                                "k" => unfiltered(
                                    selection::UnfilteredMsg::ReleaseDir(
                                        selection::Dir::Up,
                                    ),
                                ),
                                "h" => unfiltered(
                                    selection::UnfilteredMsg::ReleaseDir(
                                        selection::Dir::Left,
                                    ),
                                ),
                                "l" => unfiltered(
                                    selection::UnfilteredMsg::ReleaseDir(
                                        selection::Dir::Right,
                                    ),
                                ),
                                _ => None,
                            },
                            Key::Named(key::Named::ArrowDown) => unfiltered(
                                selection::UnfilteredMsg::ReleaseDir(
                                    selection::Dir::Down,
                                ),
                            ),
                            Key::Named(key::Named::ArrowUp) => unfiltered(
                                selection::UnfilteredMsg::ReleaseDir(
                                    selection::Dir::Up,
                                ),
                            ),
                            Key::Named(key::Named::ArrowLeft) => unfiltered(
                                selection::UnfilteredMsg::ReleaseDir(
                                    selection::Dir::Left,
                                ),
                            ),
                            Key::Named(key::Named::ArrowRight) => unfiltered(
                                selection::UnfilteredMsg::ReleaseDir(
                                    selection::Dir::Right,
                                ),
                            ),
                            Key::Named(_) | Key::Unidentified => None,
                        }
                    } else {
                        None
                    }
                }
                keyboard::Event::ModifiersChanged(_) => None,
            }
        },
    );

    let window_subs = window::events().map(|(_id, event)| Msg::Window(event));

    Subscription::batch([key_subs, window_subs])
}

fn view(state: &State, window_id: window::Id) -> Element<'_, Msg, Theme> {
    let get_diff = |id_hash| {
        if let Some(managing_repo) = &state.managing_repo {
            file::try_get_src_file(&managing_repo.files, id_hash)
        } else {
            None
        }
    };
    view::main(&state.model, get_diff, window_id).map(Msg::View)
}

fn title(state: &State, _: window::Id) -> String {
    const APP: &str = "Inflorescence";
    match &state.model.sub {
        model::SubState::PickingProject(_)
        | model::SubState::PickingRepoDir(_) => {
            format!("{APP}: project picker")
        }
        model::SubState::ManagingRepo(managing_repo) => {
            format!(
                "{APP}: {}",
                managing_repo
                    .repo_path
                    .file_name()
                    .map(|name| name.to_string_lossy())
                    .unwrap_or_else(|| managing_repo
                        .repo_path
                        .to_string_lossy())
            )
        }
    }
}