use std::collections::{HashMap, HashSet};

use chrono::{DateTime, Utc};
use gio::{ActionEntry, prelude::*};
use news_flash::error::NewsFlashError;
use news_flash::models::{FeedID, LoginData};

use crate::app::{App, NotificationCounts};
use crate::content_page::{ArticleListColumn, ContentPage};
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::util::{JOIN_ERROR, constants};

pub struct SyncActions;

impl SyncActions {
    pub fn setup() {
        // -------------------------
        // sync
        // -------------------------
        let sync = ActionEntry::builder("sync")
            .activate(|_app, _action, _parameter| {
                glib::spawn_future_local(Self::sync());
            })
            .build();

        // -------------------------
        // initial sync
        // -------------------------
        let init_sync = ActionEntry::builder("init-sync")
            .activate(|_app, _action, _parameter| {
                glib::spawn_future_local(Self::init_sync());
            })
            .build();

        // -------------------------
        // reset account
        // -------------------------
        let reset = ActionEntry::builder("show-reset-page")
            .activate(|_app, _action, _parameter| MainWindow::instance().show_reset_page())
            .build();

        // -------------------------
        // update account login
        // -------------------------
        let update_login = ActionEntry::builder("update-login")
            .activate(|_app, _action, _parameter| {
                glib::spawn_future_local(Self::update_login());
            })
            .build();

        App::default().add_action_entries([sync, init_sync, reset, update_login]);
    }

    pub async fn sync() {
        if App::default().is_syncing() {
            tracing::warn!("sync already in progress");
            return;
        }

        App::default().set_is_syncing(true);
        App::set_background_status(constants::BACKGROUND_SYNC);

        let header_maps = App::default().settings().get_feed_header_maps();

        let res = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            let news_flash_guad = news_flash.read().await;
            let news_flash = news_flash_guad.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
            let last_sync_datetime = news_flash.last_sync().await;
            let client = App::client().await;

            let new_article_count_map = news_flash.sync(&client, header_maps).await?;
            let unread_map =
                news_flash.unread_count_feed_map(App::default().settings().article_list().hide_future_articles())?;
            let (feeds, _feed_mappings) = news_flash.get_feeds()?;
            Ok((new_article_count_map, unread_map, feeds, last_sync_datetime))
        })
        .await
        .expect(JOIN_ERROR);

        App::default().set_is_syncing(false);

        match res {
            Ok((new_article_count_map, unread_map, feeds, last_sync_datetime)) => {
                let feed_ids = feeds.iter().map(|f| f.feed_id.clone()).collect::<HashSet<FeedID>>();
                let feed_names = feeds
                    .iter()
                    .map(|f| (f.feed_id.clone(), f.label.clone()))
                    .collect::<HashMap<FeedID, String>>();

                _ = App::default().settings().delete_old_feed_settings(&feed_ids);

                ContentPage::instance().update_sidebar();
                ArticleListColumn::instance().update_list();
                let counts = NotificationCounts {
                    new: new_article_count_map,
                    unread: unread_map,
                    names: feed_names,
                };
                App::show_notification(counts);
                Self::scrap_content_feeds(last_sync_datetime, feed_ids).await;
            }
            Err(error) => {
                ContentPage::instance().newsflash_error(&i18n("Failed to sync"), error);

                App::set_background_status(constants::BACKGROUND_IDLE);
            }
        }
    }

    async fn init_sync() {
        App::default().set_is_syncing(true);

        let header_maps = App::default().settings().get_feed_header_maps();

        let res = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            let news_flash_guad = news_flash.read().await;
            let news_flash = news_flash_guad.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
            let client = App::client().await;

            let new_article_map = news_flash.initial_sync(&client, header_maps).await?;
            let unread_map =
                news_flash.unread_count_feed_map(App::default().settings().article_list().hide_future_articles())?;
            let (feeds, _feed_mappings) = news_flash.get_feeds()?;
            Ok((new_article_map, unread_map, feeds))
        })
        .await
        .expect(JOIN_ERROR);

        App::default().set_is_syncing(false);

        match res {
            Ok((new_article_map, unread_map, feeds)) => {
                let feed_names = feeds
                    .iter()
                    .map(|f| (f.feed_id.clone(), f.label.clone()))
                    .collect::<HashMap<FeedID, String>>();

                ContentPage::instance().update_sidebar();
                ArticleListColumn::instance().update_list();
                let counts = NotificationCounts {
                    new: new_article_map,
                    unread: unread_map,
                    names: feed_names,
                };
                App::show_notification(counts);
            }
            Err(error) => ContentPage::instance().newsflash_error(&i18n("Failed to sync"), error),
        }
    }

    pub async fn scrap_content_feeds(last_sync: DateTime<Utc>, feed_ids: HashSet<FeedID>) {
        let scrap_feed_ids = feed_ids
            .into_iter()
            .filter(|id| App::default().settings().get_merged_feed_setting(id).scrap_content)
            .collect::<Vec<_>>();

        tracing::debug!(?scrap_feed_ids);

        if scrap_feed_ids.is_empty() {
            App::set_background_status(constants::BACKGROUND_IDLE);
            return;
        }

        App::default().set_is_scraping_content(true);
        App::default().set_is_syncing(false);

        let future = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            let news_flash_guad = news_flash.read().await;
            let news_flash = news_flash_guad.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
            let client = App::client().await;
            news_flash
                .scrap_content_feeds(last_sync, &scrap_feed_ids, &client)
                .await
        });

        let res = future.await.expect(JOIN_ERROR);

        App::default().set_is_scraping_content(false);
        ArticleListColumn::instance().update_list();

        App::set_background_status(constants::BACKGROUND_IDLE);

        if let Err(error) = res {
            tracing::warn!(%error, "Internal scraper failed");
            ContentPage::instance().newsflash_error(&i18n("Scraper failed to extract content"), error);
        }
    }

    async fn update_login() {
        ContentPage::instance().dismiss_notification();

        let future = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            let news_flash_guad = news_flash.read().await;
            let news_flash = news_flash_guad.as_ref()?;
            news_flash.get_login_data().await
        });

        if let Some(login_data) = future.await.expect(JOIN_ERROR) {
            let plugin_id = login_data.id();
            match login_data {
                LoginData::None(_id) => tracing::error!("updating login for local should never happen!"),
                LoginData::Direct(_) | LoginData::OAuth(_) => {
                    MainWindow::instance().show_login_page(&plugin_id, Some(login_data))
                }
            }
        }
    }
}
