use std::{
    rc::Rc,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::Duration,
};

use anyhow::Result;
use crossbeam_channel::Sender;
use crossterm::event::Event;
use gnostr_asyncgit::{
    asyncjob::AsyncSingleJob,
    sync::{
        self, filter_commit_by_search, CommitId, LogFilterSearch, LogFilterSearchOptions,
        RepoPathRef,
    },
    AsyncBranchesJob, AsyncCommitFilterJob, AsyncGitNotification, AsyncLog, AsyncTags,
    CommitFilesParams, FetchStatus, ProgressPercent,
};
use indexmap::IndexSet;
use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    text::Span,
    widgets::{Block, Borders, Paragraph},
    Frame,
};
use sync::CommitTags;

use crate::{
    app::Environment,
    components::{
        visibility_blocking, CommandBlocking, CommandInfo, CommitDetailsComponent, CommitList,
        Component, DrawableComponent, EventState,
    },
    keys::{key_match, SharedKeyConfig},
    popups::{FileTreeOpen, InspectCommitOpen},
    queue::{InternalEvent, Queue, StackablePopupOpen},
    strings::{self, order},
    try_or_popup,
    ui::style::{SharedTheme, Theme},
};

struct LogSearchResult {
    options: LogFilterSearchOptions,
    duration: Duration,
}

//TODO: deserves its own component
enum LogSearch {
    Off,
    Searching(
        AsyncSingleJob<AsyncCommitFilterJob>,
        LogFilterSearchOptions,
        Option<ProgressPercent>,
        Arc<AtomicBool>,
    ),
    Results(LogSearchResult),
}

/// Represents the commit log view and its associated operations.
pub struct Revlog {
    repo: RepoPathRef,
    commit_details: CommitDetailsComponent,
    list: CommitList,
    git_log: AsyncLog,
    search: LogSearch,
    git_tags: AsyncTags,
    git_local_branches: AsyncSingleJob<AsyncBranchesJob>,
    git_remote_branches: AsyncSingleJob<AsyncBranchesJob>,
    queue: Queue,
    visible: bool,
    key_config: SharedKeyConfig,
    sender: Sender<AsyncGitNotification>,
    theme: SharedTheme,
}

/// Implements methods for the Revlog struct.
impl Revlog {
    /// new
    pub fn new(env: &Environment) -> Self {
        Self {
            repo: env.repo.clone(),
            queue: env.queue.clone(),
            commit_details: CommitDetailsComponent::new(env, true),
            list: CommitList::new(env, &strings::log_title(&env.key_config)),
            git_log: AsyncLog::new(env.repo.borrow().clone(), &env.sender_git, None),
            search: LogSearch::Off,
            git_tags: AsyncTags::new(env.repo.borrow().clone(), &env.sender_git),
            git_local_branches: AsyncSingleJob::new(env.sender_git.clone()),
            git_remote_branches: AsyncSingleJob::new(env.sender_git.clone()),
            visible: false,
            key_config: env.key_config.clone(),
            sender: env.sender_git.clone(),
            theme: env.theme.clone(),
        }
    }

    /// any_work_pending
    pub fn any_work_pending(&self) -> bool {
        self.git_log.is_pending()
            || self.is_search_pending()
            || self.git_tags.is_pending()
            || self.git_local_branches.is_pending()
            || self.git_remote_branches.is_pending()
            || self.commit_details.any_work_pending()
    }

    const fn is_search_pending(&self) -> bool {
        matches!(self.search, LogSearch::Searching(_, _, _, _))
    }

    /// update
    pub fn update(&mut self) -> Result<()> {
        if self.is_visible() {
            if self.git_log.fetch()? == FetchStatus::Started {
                self.list.clear();
            }

            self.list.refresh_extend_data(self.git_log.extract_items()?);

            self.git_tags.request(Duration::from_secs(3), false)?;

            if self.commit_details.is_visible() {
                let commit = self.selected_commit();
                let tags = self.selected_commit_tags(&commit);

                self.commit_details
                    .set_commits(commit.map(CommitFilesParams::from), &tags)?;
            }
        }

        Ok(())
    }

    /// update_git
    pub fn update_git(&mut self, ev: AsyncGitNotification) -> Result<()> {
        if self.visible {
            match ev {
                AsyncGitNotification::CommitFiles | AsyncGitNotification::Log => self.update()?,
                AsyncGitNotification::CommitFilter => {
                    self.update_search_state();
                }
                AsyncGitNotification::Tags => {
                    if let Some(tags) = self.git_tags.last()? {
                        self.list.set_tags(tags);
                        self.update()?;
                    }
                }
                AsyncGitNotification::Branches => {
                    if let Some(local_branches) = self.git_local_branches.take_last() {
                        if let Some(Ok(local_branches)) = local_branches.result() {
                            self.list.set_local_branches(local_branches);
                            self.update()?;
                        }
                    }

                    if let Some(remote_branches) = self.git_remote_branches.take_last() {
                        if let Some(Ok(remote_branches)) = remote_branches.result() {
                            self.list.set_remote_branches(remote_branches);
                            self.update()?;
                        }
                    }
                }
                _ => (),
            }
        }

        Ok(())
    }

    fn selected_commit(&self) -> Option<CommitId> {
        self.list.selected_entry().map(|e| e.id)
    }

    fn selected_commit_tags(&self, commit: &Option<CommitId>) -> Option<CommitTags> {
        let tags = self.list.tags();

        commit.and_then(|commit| tags.and_then(|tags| tags.get(&commit).cloned()))
    }

    /// select_commit
    pub fn select_commit(&mut self, id: CommitId) -> Result<()> {
        self.list.select_commit(id)
    }

    fn revert_commit(&self) -> Result<()> {
        if let Some(c) = self.selected_commit() {
            sync::revert_commit(&self.repo.borrow(), c)?;
            self.queue.push(InternalEvent::TabSwitchStatus);
        }

        Ok(())
    }

    fn inspect_commit(&self) {
        if let Some(commit_id) = self.selected_commit() {
            let tags = self.selected_commit_tags(&Some(commit_id));
            self.queue
                .push(InternalEvent::OpenPopup(StackablePopupOpen::InspectCommit(
                    InspectCommitOpen::new_with_tags(commit_id, tags),
                )));
        }
    }

    pub fn search(&mut self, options: LogFilterSearchOptions) {
        if !self.can_start_search() {
            return;
        }

        if matches!(self.search, LogSearch::Off | LogSearch::Results(_)) {
            log::info!("start search: {:?}", options);

            let filter = filter_commit_by_search(LogFilterSearch::new(options.clone()));

            let cancellation_flag = Arc::new(AtomicBool::new(false));

            let mut job = AsyncSingleJob::new(self.sender.clone());
            job.spawn(AsyncCommitFilterJob::new(
                self.repo.borrow().clone(),
                self.list.copy_items(),
                filter,
                Arc::clone(&cancellation_flag),
            ));

            self.search = LogSearch::Searching(job, options, None, Arc::clone(&cancellation_flag));

            self.list.set_highlighting(None);
        }
    }

    fn cancel_search(&mut self) -> bool {
        if let LogSearch::Searching(_, _, _, cancellation_flag) = &self.search {
            cancellation_flag.store(true, Ordering::Relaxed);
            self.list.set_highlighting(None);
            return true;
        }

        false
    }

    fn update_search_state(&mut self) {
        match &mut self.search {
            LogSearch::Off | LogSearch::Results(_) => (),
            LogSearch::Searching(search, options, progress, cancel) => {
                if search.is_pending() {
                    //update progress
                    *progress = search.progress();
                } else if let Some(search) = search.take_last().and_then(|search| search.result()) {
                    match search {
                        Ok(search) => {
                            let was_aborted = cancel.load(Ordering::Relaxed);

                            self.search = if was_aborted {
                                LogSearch::Off
                            } else {
                                self.list.set_highlighting(Some(Rc::new(
                                    search.result.into_iter().collect::<IndexSet<_>>(),
                                )));

                                LogSearch::Results(LogSearchResult {
                                    options: options.clone(),
                                    duration: search.duration,
                                })
                            };
                        }
                        Err(err) => {
                            self.queue
                                .push(InternalEvent::ShowErrorMsg(format!("search error: {err}",)));

                            self.search = LogSearch::Off;
                        }
                    }
                }
            }
        }
    }

    const fn is_in_search_mode(&self) -> bool {
        !matches!(self.search, LogSearch::Off)
    }

    fn draw_search(&self, f: &mut Frame, area: Rect) {
        let (text, title) = match &self.search {
            LogSearch::Searching(_, options, progress, _) => (
                format!("'{}'", options.search_pattern.clone()),
                format!(
                    "({}%)",
                    progress
                        .map(|progress| progress.progress)
                        .unwrap_or_default()
                ),
            ),
            LogSearch::Results(results) => {
                let info = self.list.highlighted_selection_info();

                (
                    format!(
                        "'{}' (duration: {:?})",
                        results.options.search_pattern.clone(),
                        results.duration,
                    ),
                    format!("({}/{})", (info.0 + 1).min(info.1), info.1),
                )
            }
            LogSearch::Off => (String::new(), String::new()),
        };

        f.render_widget(
            Paragraph::new(text)
                .block(
                    Block::default()
                        .title(Span::styled(
                            format!("{} {}", strings::POPUP_TITLE_LOG_SEARCH, title),
                            self.theme.title(true),
                        ))
                        .borders(Borders::ALL)
                        .border_style(Theme::attention_block()),
                )
                .alignment(Alignment::Left),
            area,
        );
    }

    const fn can_close_search(&self) -> bool {
        self.is_in_search_mode() && !self.is_search_pending()
    }

    fn can_start_search(&self) -> bool {
        !self.git_log.is_pending() && !self.is_search_pending()
    }
}

/// Implements the drawing functionality for the Revlog component.
impl DrawableComponent for Revlog {
    fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {
        let area = if self.is_in_search_mode() {
            Layout::default()
                .direction(Direction::Vertical)
                .constraints([Constraint::Min(1), Constraint::Length(3)].as_ref())
                .split(area)
        } else {
            Rc::new([area])
        };

        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(60), Constraint::Percentage(40)].as_ref())
            .split(area[0]);

        if self.commit_details.is_visible() {
            self.list.draw(f, chunks[0])?;
            self.commit_details.draw(f, chunks[1])?;
        } else {
            self.list.draw(f, area[0])?;
        }

        if self.is_in_search_mode() {
            self.draw_search(f, area[1]);
        }

        Ok(())
    }
}

impl Component for Revlog {
    //TODO: cleanup
    #[allow(clippy::too_many_lines)]
    fn event(&mut self, ev: &Event) -> Result<EventState> {
        if self.visible {
            let event_used = self.list.event(ev)?;

            if event_used.is_consumed() {
                self.update()?;
                return Ok(EventState::Consumed);
            } else if let Event::Key(k) = ev {
                if key_match(k, self.key_config.keys.enter) {
                    self.commit_details.toggle_visible()?;
                    self.update()?;
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.exit_popup) {
                    if self.is_search_pending() {
                        self.cancel_search();
                    } else if self.can_close_search() {
                        self.list.set_highlighting(None);
                        self.search = LogSearch::Off;
                    }
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.copy) {
                    try_or_popup!(self, strings::POPUP_FAIL_COPY, self.list.copy_commit_hash());
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.push) {
                    self.queue.push(InternalEvent::PushTags);
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.log_tag_commit) {
                    return self
                        .selected_commit()
                        .map_or(Ok(EventState::NotConsumed), |id| {
                            self.queue.push(InternalEvent::TagCommit(id));
                            Ok(EventState::Consumed)
                        });
                } else if key_match(k, self.key_config.keys.move_right)
                    && self.commit_details.is_visible()
                {
                    self.inspect_commit();
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.select_branch)
                    && !self.is_search_pending()
                {
                    self.queue.push(InternalEvent::SelectBranch);
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.status_reset_item)
                    && !self.is_search_pending()
                {
                    try_or_popup!(self, "revert error:", self.revert_commit());

                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.open_file_tree)
                    && !self.is_search_pending()
                {
                    return self
                        .selected_commit()
                        .map_or(Ok(EventState::NotConsumed), |id| {
                            self.queue.push(InternalEvent::OpenPopup(
                                StackablePopupOpen::FileTree(FileTreeOpen::new(id)),
                            ));
                            Ok(EventState::Consumed)
                        });
                } else if key_match(k, self.key_config.keys.tags) {
                    self.queue.push(InternalEvent::Tags);
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.log_reset_commit)
                    && !self.is_search_pending()
                {
                    return self
                        .selected_commit()
                        .map_or(Ok(EventState::NotConsumed), |id| {
                            self.queue.push(InternalEvent::OpenResetPopup(id));
                            Ok(EventState::Consumed)
                        });
                } else if key_match(k, self.key_config.keys.log_reword_commit)
                    && !self.is_search_pending()
                {
                    return self
                        .selected_commit()
                        .map_or(Ok(EventState::NotConsumed), |id| {
                            self.queue.push(InternalEvent::RewordCommit(id));
                            Ok(EventState::Consumed)
                        });
                } else if key_match(k, self.key_config.keys.log_find) && self.can_start_search() {
                    self.queue.push(InternalEvent::OpenLogSearchPopup);
                    return Ok(EventState::Consumed);
                } else if key_match(k, self.key_config.keys.compare_commits)
                    && self.list.marked_count() > 0
                    && !self.is_search_pending()
                {
                    if self.list.marked_count() == 1 {
                        // compare against head
                        self.queue.push(InternalEvent::OpenPopup(
                            StackablePopupOpen::CompareCommits(InspectCommitOpen::new(
                                self.list.marked()[0].1,
                            )),
                        ));
                        return Ok(EventState::Consumed);
                    } else if self.list.marked_count() == 2 {
                        //compare two marked commits
                        let marked = self.list.marked();
                        self.queue.push(InternalEvent::OpenPopup(
                            StackablePopupOpen::CompareCommits(InspectCommitOpen {
                                commit_id: marked[0].1,
                                compare_id: Some(marked[1].1),
                                tags: None,
                            }),
                        ));
                        return Ok(EventState::Consumed);
                    }
                }
            }
        }

        Ok(EventState::NotConsumed)
    }

    fn commands(&self, out: &mut Vec<CommandInfo>, force_all: bool) -> CommandBlocking {
        if self.visible || force_all {
            self.list.commands(out, force_all);
        }

        out.push(
            CommandInfo::new(
                strings::commands::log_close_search(&self.key_config),
                true,
                (self.visible && (self.can_close_search() || self.is_search_pending()))
                    || force_all,
            )
            .order(order::PRIORITY),
        );

        out.push(CommandInfo::new(
            strings::commands::log_details_toggle(&self.key_config),
            true,
            self.visible,
        ));

        out.push(CommandInfo::new(
            strings::commands::commit_details_open(&self.key_config),
            true,
            (self.visible && self.commit_details.is_visible()) || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::open_branch_select_popup(&self.key_config),
            true,
            (self.visible && !self.is_search_pending()) || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::compare_with_head(&self.key_config),
            self.list.marked_count() == 1,
            (self.visible && !self.is_search_pending() && self.list.marked_count() <= 1)
                || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::compare_commits(&self.key_config),
            true,
            (self.visible && !self.is_search_pending() && self.list.marked_count() == 2)
                || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::copy_hash(&self.key_config),
            self.selected_commit().is_some(),
            self.visible || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::log_tag_commit(&self.key_config),
            self.selected_commit().is_some(),
            self.visible || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::log_checkout_commit(&self.key_config),
            self.selected_commit().is_some(),
            self.visible || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::open_tags_popup(&self.key_config),
            true,
            self.visible || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::push_tags(&self.key_config),
            true,
            self.visible || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::inspect_file_tree(&self.key_config),
            self.selected_commit().is_some(),
            (self.visible && !self.is_search_pending()) || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::revert_commit(&self.key_config),
            self.selected_commit().is_some(),
            (self.visible && !self.is_search_pending()) || force_all,
        ));

        out.push(CommandInfo::new(
            strings::commands::log_reset_commit(&self.key_config),
            self.selected_commit().is_some(),
            (self.visible && !self.is_search_pending()) || force_all,
        ));
        out.push(CommandInfo::new(
            strings::commands::log_reword_commit(&self.key_config),
            self.selected_commit().is_some(),
            (self.visible && !self.is_search_pending()) || force_all,
        ));
        out.push(CommandInfo::new(
            strings::commands::log_find_commit(&self.key_config),
            self.can_start_search(),
            self.visible || force_all,
        ));

        visibility_blocking(self)
    }

    fn is_visible(&self) -> bool {
        self.visible
    }

    fn hide(&mut self) {
        self.visible = false;
        self.git_log.set_background();
    }

    fn show(&mut self) -> Result<()> {
        self.visible = true;

        self.git_local_branches
            .spawn(AsyncBranchesJob::new(self.repo.borrow().clone(), true));

        self.git_remote_branches
            .spawn(AsyncBranchesJob::new(self.repo.borrow().clone(), false));

        self.update()?;

        Ok(())
    }
}
