use std::collections::HashMap;

use chrono::{DateTime, Utc};
use gio::{ActionEntry, prelude::*};
use glib::{Variant, VariantType};
use news_flash::error::{FeedApiError, NewsFlashError};
use news_flash::models::{CategoryID, CategoryMapping, FeedID, FeedMapping, TagID, Url};

use crate::app::{App, NotificationCounts};
use crate::content_page::{ArticleListColumn, ArticleViewColumn, ContentPage};
use crate::global_actions::SyncActions;
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::settings::GFeedOrder;
use crate::undo_action::UndoAction;
use crate::util::{JOIN_ERROR, constants};

pub struct EditActions;

impl EditActions {
    pub fn setup() {
        // -------------------------
        // add feed
        // -------------------------
        let add_feed = ActionEntry::builder("add-feed")
            .parameter_type(VariantType::new("(sssb)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(variant) = parameter else {
                    return;
                };

                let feed_url = variant.child_get::<String>(0);
                let title = variant.child_get::<String>(1);
                let category_id = variant.child_get::<String>(2);
                let scrape = variant.child_get::<bool>(3);

                glib::spawn_future_local(Self::add_feed(feed_url, title, category_id, scrape));
            })
            .build();

        // -------------------------
        // add category
        // -------------------------
        let add_category = ActionEntry::builder("add-category")
            .parameter_type(VariantType::new("s").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(title) = parameter.and_then(Variant::str) else {
                    return;
                };

                let title = title.to_string();
                glib::spawn_future_local(Self::add_category(title));
            })
            .build();

        // -------------------------
        // add tag
        // -------------------------
        let add_tag = ActionEntry::builder("add-tag")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(variant) = parameter else {
                    return;
                };

                let color = variant.child_get::<String>(0);
                let title = variant.child_get::<String>(1);

                glib::spawn_future_local(Self::add_tag(color, title));
            })
            .build();

        // -------------------------
        // enqueue delete category
        // -------------------------
        let enqueue_delete_category = ActionEntry::builder("enqueue-delete-category")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| Self::enqueue_delete_category(parameter))
            .build();

        // -------------------------
        // enqueue delete feed
        // -------------------------
        let enqueue_delete_feed = ActionEntry::builder("enqueue-delete-feed")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| Self::enqueue_delete_feed(parameter))
            .build();

        // -------------------------
        // enqueue delete tag
        // -------------------------
        let enqueue_delete_tag = ActionEntry::builder("enqueue-delete-tag")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| Self::enqueue_delete_tag(parameter))
            .build();

        // ---------------------------
        // rename feed
        // ---------------------------
        let rename_feed = ActionEntry::builder("rename-feed")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("Rename feed: no parameter");
                    return;
                };

                let id_value = parameter.child_value(0);
                let new_title_value = parameter.child_value(1);

                let (Some(id_str), Some(new_title_str)) = (id_value.str(), new_title_value.str()) else {
                    return;
                };

                let feed_id = FeedID::new(id_str);
                let new_title = new_title_str.to_string();

                glib::spawn_future_local(Self::rename_feed(feed_id, new_title));
            })
            .build();

        // ---------------------------
        // rename category
        // ---------------------------
        let rename_category = ActionEntry::builder("rename-category")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("Rename category: no parameter");
                    return;
                };

                let id_value = parameter.child_value(0);
                let new_title_value = parameter.child_value(1);

                let (Some(id_str), Some(new_title_str)) = (id_value.str(), new_title_value.str()) else {
                    return;
                };

                let category_id = CategoryID::new(id_str);
                let new_title = new_title_str.to_string();

                glib::spawn_future_local(Self::rename_category(category_id, new_title));
            })
            .build();

        // ---------------------------
        // edit tag
        // ---------------------------
        let edit_tag = ActionEntry::builder("edit-tag")
            .parameter_type(VariantType::new("(sss)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("Rename category: no parameter");
                    return;
                };

                let id_value = parameter.child_value(0);
                let new_title_value = parameter.child_value(1);
                let color_value = parameter.child_value(2);

                let (Some(id_str), Some(new_title_str), Some(color_str)) =
                    (id_value.str(), new_title_value.str(), color_value.str())
                else {
                    return;
                };

                let tag_id = TagID::new(id_str);
                let new_title = new_title_str.to_string();
                let new_color = color_str.to_string();

                glib::spawn_future_local(Self::edit_tag(tag_id, new_title, new_color));
            })
            .build();

        // ---------------------------
        // edit feed url
        // ---------------------------
        let edit_feed_url = ActionEntry::builder("edit-feed-url")
            .parameter_type(VariantType::new("(ss)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("edit feed url: no parameter");
                    return;
                };

                let id_value = parameter.child_value(0);
                let new_url_value = parameter.child_value(1);

                let (Some(id_str), Some(new_url_str)) = (id_value.str(), new_url_value.str()) else {
                    return;
                };

                let feed_id = FeedID::new(id_str);
                let new_url = new_url_str.to_string();

                glib::spawn_future_local(Self::edit_feed_url(feed_id, new_url));
            })
            .build();

        // ---------------------------
        // move feed
        // ---------------------------
        let move_feed = ActionEntry::builder("move-feed")
            .parameter_type(VariantType::new("(sssi)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("move feed: no parameter");
                    return;
                };

                let move_id_value = parameter.child_value(0);
                let from_id_value = parameter.child_value(1);
                let to_id_value = parameter.child_value(2);
                let sort_index_value = parameter.child_value(3);

                if let Some(move_id) = move_id_value.str().map(FeedID::new)
                    && let Some(from_id) = from_id_value.str().map(CategoryID::new)
                    && let Some(to_id) = to_id_value.str().map(CategoryID::new)
                    && let Some(sort_index) = sort_index_value.get::<i32>()
                {
                    glib::spawn_future_local(Self::move_feed(move_id, from_id, to_id, sort_index));
                }
            })
            .build();

        // ---------------------------
        // move category
        // ---------------------------
        let move_category = ActionEntry::builder("move-category")
            .parameter_type(VariantType::new("(ssi)").as_deref().ok())
            .activate(|_win, _action, parameter| {
                let Some(parameter) = parameter else {
                    tracing::warn!("move category: no parameter");
                    return;
                };

                let move_id_value = parameter.child_value(0);
                let to_id_value = parameter.child_value(1);
                let sort_index_value = parameter.child_value(2);

                if let Some(move_id) = move_id_value.str().map(CategoryID::new)
                    && let Some(to_id) = to_id_value.str().map(CategoryID::new)
                    && let Some(sort_index) = sort_index_value.get::<i32>()
                {
                    glib::spawn_future_local(Self::move_category(move_id, to_id, sort_index));
                }
            })
            .build();

        MainWindow::instance().add_action_entries([
            add_feed,
            add_category,
            add_tag,
            enqueue_delete_category,
            enqueue_delete_feed,
            enqueue_delete_tag,
            rename_feed,
            rename_category,
            edit_tag,
            edit_feed_url,
            move_feed,
            move_category,
        ]);
    }

    async fn add_feed(feed_url: String, title: String, category_id: String, scrape: bool) {
        let feed_url = Url::parse(&feed_url).unwrap();
        let title = if title.is_empty() { None } else { Some(title) };
        let category_id = if category_id.is_empty() {
            None
        } else {
            Some(CategoryID::from_owned(category_id))
        };

        tracing::debug!(%feed_url, "add feed");

        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.add_feed(&feed_url, title, category_id, &client).await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok((feed, _feed_mapping, _category, _category_mapping)) => {
                ContentPage::instance().update_sidebar();
                Self::fetch_feed(feed.feed_id.clone()).await;

                if scrape {
                    let mut feed_settings = App::default().settings().get_merged_feed_setting(&feed.feed_id);

                    feed_settings.scrap_content = true;

                    _ = App::default()
                        .settings()
                        .set_feed_settings(&feed.feed_id, &feed_settings);

                    let mut hash_set = std::collections::HashSet::new();
                    hash_set.insert(feed.feed_id.clone());
                    SyncActions::scrap_content_feeds(DateTime::<Utc>::MIN_UTC, hash_set).await;
                }
            }
            Err(error) => {
                tracing::error!(%error, "Failed to add feed");
                ContentPage::instance().newsflash_error(&i18n("Failed to add feed"), error);
            }
        }
    }

    async fn fetch_feed(feed_id: FeedID) {
        App::default().set_is_syncing(false);
        App::set_background_status(constants::BACKGROUND_SYNC);

        let header_map = App::default().settings().get_feed_header_map(&feed_id);

        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_count = news_flash.fetch_feed(&feed_id, &client, header_map).await?;
            let mut new_article_map = HashMap::new();
            new_article_map.insert(feed_id, new_article_count);
            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;

        App::set_background_status(constants::BACKGROUND_IDLE);
        App::default().set_is_syncing(false);

        match res.expect(JOIN_ERROR) {
            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) => {
                if let NewsFlashError::API(FeedApiError::Unsupported) = error {
                    return;
                }

                ContentPage::instance().newsflash_error(&i18n("Failed to fetch feed"), error);
            }
        }
    }

    async fn add_category(title: String) {
        tracing::debug!(%title, "add category");

        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.add_category(&title, None, &client).await
        });

        if let Err(error) = future.await.expect(JOIN_ERROR) {
            tracing::error!(%error, "Failed to add category");
            ContentPage::instance().newsflash_error(&i18n("Failed to add category"), error);
        } else {
            ContentPage::instance().update_sidebar();
        }
    }

    async fn add_tag(color: String, title: String) {
        tracing::debug!(%title, "add tag");

        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.add_tag(&title, Some(color), &client).await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(_tag) => {
                ContentPage::instance().update_sidebar();
                ArticleViewColumn::instance().refresh_article_metadata_from_db();
            }
            Err(error) => {
                tracing::error!(%error, "Failed to add tag");
                ContentPage::instance().newsflash_error(&i18n("Failed to add tag"), error);
            }
        }
    }

    pub async fn delete_category(category_id: CategoryID) {
        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.remove_category(&category_id, true, &client).await
        });

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

        if let Err(error) = res {
            tracing::error!(%error, "Failed to delete category");
            ContentPage::instance().update_sidebar();
            ContentPage::instance().newsflash_error(&i18n("Failed to delete category"), error);
        }
    }

    pub async fn delete_feed(feed_id: FeedID) {
        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.remove_feed(&feed_id, &client).await
        });

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

        if let Err(error) = res {
            tracing::error!(%error, "Failed to delete feed");
            ContentPage::instance().update_sidebar();
            ContentPage::instance().newsflash_error(&i18n("Failed to delete feed"), error);
        }
    }

    pub async fn delete_tag(tag_id: TagID) {
        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.remove_tag(&tag_id, &client).await
        });

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

        if let Err(error) = res {
            tracing::error!(%error, "Failed to delete tag");
            ContentPage::instance().update_sidebar();
            ContentPage::instance().newsflash_error(&i18n("Failed to delete tag"), error);
        }
    }

    fn enqueue_delete_category(parameter: Option<&Variant>) {
        let Some(parameter) = parameter else {
            tracing::warn!("Delete category: no parameter");
            return;
        };

        let id_value = parameter.child_value(0);
        let name_value = parameter.child_value(1);

        let (Some(id_str), Some(name_str)) = (id_value.str(), name_value.str()) else {
            return;
        };

        let category_id = CategoryID::new(id_str);
        ContentPage::instance().add_undo_notification(UndoAction::DeleteCategory(category_id, name_str.into()));
        ContentPage::instance().update_sidebar();
    }

    fn enqueue_delete_feed(parameter: Option<&Variant>) {
        let Some(parameter) = parameter else {
            tracing::warn!("Delete category: no parameter");
            return;
        };

        let id_value = parameter.child_value(0);
        let name_value = parameter.child_value(1);

        let (Some(id_str), Some(name_str)) = (id_value.str(), name_value.str()) else {
            return;
        };

        let feed_id = FeedID::new(id_str);
        ContentPage::instance().add_undo_notification(UndoAction::DeleteFeed(feed_id, name_str.into()));
        ContentPage::instance().update_sidebar();
    }

    fn enqueue_delete_tag(parameter: Option<&Variant>) {
        let Some(parameter) = parameter else {
            tracing::warn!("Delete category: no parameter");
            return;
        };

        let id_value = parameter.child_value(0);
        let name_value = parameter.child_value(1);

        let (Some(id_str), Some(name_str)) = (id_value.str(), name_value.str()) else {
            return;
        };

        let tag_id = TagID::new(id_str);
        ContentPage::instance().add_undo_notification(UndoAction::DeleteTag(tag_id, name_str.into()));
        ContentPage::instance().update_sidebar();
    }

    async fn rename_feed(feed_id: FeedID, new_title: String) {
        let feed_id_clone = feed_id.clone();

        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.rename_feed(&feed_id, &new_title, &client).await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(_feed) => {
                ArticleListColumn::instance().update_list();
                ContentPage::instance().update_sidebar();

                // if the visible article belongs to the renamed feed
                // show it again to gather the updated feed name
                if let Some(article) = ArticleViewColumn::instance().article()
                    && article.feed_id().as_ref() == &feed_id_clone
                {
                    ArticleViewColumn::instance().show_article(article.article_id(), false);
                }
            }
            Err(error) => {
                tracing::error!(%error, "Failed to rename feed");
                ContentPage::instance().newsflash_error(&i18n("Failed to rename feed"), error);
            }
        }
    }

    async fn rename_category(category_id: CategoryID, new_title: String) {
        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.rename_category(&category_id, &new_title, &client).await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(_feed) => {
                ContentPage::instance().update_sidebar();
            }
            Err(error) => {
                tracing::error!(%error, "Failed to rename category");
                ContentPage::instance().newsflash_error(&i18n("Failed to rename category"), error);
            }
        }
    }

    async fn edit_tag(tag_id: TagID, new_title: String, new_color: String) {
        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
                .edit_tag(&tag_id, &new_title, &Some(new_color), &client)
                .await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(_tag) => {
                ArticleListColumn::instance().update_list();
                ContentPage::instance().update_sidebar();
            }
            Err(error) => {
                let message = &i18n("Failed to edit tag");
                tracing::error!(%message);
                ContentPage::instance().newsflash_error(message, error);
            }
        }
    }

    async fn edit_feed_url(feed_id: FeedID, new_url: String) {
        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.edit_feed_url(&feed_id, &new_url, &client).await
        });

        if let Err(error) = future.await.expect(JOIN_ERROR) {
            tracing::error!(%error, "Failed to rename feed");
            ContentPage::instance().newsflash_error(&i18n("Failed to rename feed"), error);
        }
    }

    async fn move_feed(move_id: FeedID, from_id: CategoryID, to_id: CategoryID, sort_index: i32) {
        let from_mapping = FeedMapping {
            feed_id: move_id.clone(),
            category_id: from_id,
            sort_index: None,
        };
        let to_mapping = FeedMapping {
            feed_id: move_id,
            category_id: to_id,
            sort_index: Some(sort_index),
        };

        if App::default().settings().feed_list().order() == GFeedOrder::Alphabetical {
            App::default().settings().feed_list().set_order(GFeedOrder::Manual);
        }

        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.move_feed(&from_mapping, &to_mapping, &client).await
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(()) => {
                ContentPage::instance().update_sidebar();
            }
            Err(error) => {
                tracing::error!(%error, "Failed to move feed");
                ContentPage::instance().newsflash_error(&i18n("Failed to move feed"), error);
            }
        }
    }

    async fn move_category(move_id: CategoryID, to_id: CategoryID, sort_index: i32) {
        if App::default().settings().feed_list().order() == GFeedOrder::Alphabetical {
            App::default().settings().feed_list().set_order(GFeedOrder::Manual);
        }

        let to_mapping = CategoryMapping {
            parent_id: to_id,
            category_id: move_id,
            sort_index: Some(sort_index),
        };

        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;
            news_flash.move_category(&to_mapping, &client).await
        })
        .await;

        match res.expect(JOIN_ERROR) {
            Ok(()) => ContentPage::instance().update_sidebar(),
            Err(error) => {
                tracing::error!(%error, "Failed to move category");
                ContentPage::instance().newsflash_error(&i18n("Failed to move category"), error);
            }
        }
    }
}
