mod search_item_row;

use self::search_item_row::SearchItemRow;
use crate::add_dialog::{AddFeedWidget, SelectFeedWidget};
use crate::app::App;
use crate::content_page::ContentPage;
use crate::gobject_models::GFeed;
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::util::JOIN_ERROR;
use feedly_api::FeedlyApi;
use glib::{Object, Properties, clone, prelude::*, subclass::*};
use gtk4::{
    Accessible, Buildable, Button, CompositeTemplate, ConstraintTarget, ListBox, ListBoxRow, Stack, StringList, Widget,
    prelude::*, subclass::prelude::*,
};
use libadwaita::{Dialog, NavigationView, prelude::*, subclass::prelude::*};
use news_flash::error::NewsFlashError;
use news_flash::feed_api::FeedApiError;
use news_flash::models::{FeedID, Url};
use news_flash::{NewsFlash, ParsedUrl};
use std::cell::{Cell, RefCell};
use std::sync::Arc;
use tokio::sync::Semaphore;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::DiscoverDialog)]
    #[template(file = "data/resources/ui_templates/discover/dialog.blp")]
    pub struct DiscoverDialog {
        #[template_child]
        pub navigation_view: TemplateChild<NavigationView>,
        #[template_child]
        pub add_feed_widget: TemplateChild<AddFeedWidget>,
        #[template_child]
        pub select_feed_widget: TemplateChild<SelectFeedWidget>,
        #[template_child]
        pub language_dropdown_model: TemplateChild<StringList>,
        #[template_child]
        pub search_result_stack: TemplateChild<Stack>,
        #[template_child]
        pub search_result_list: TemplateChild<ListBox>,

        #[property(get, set = Self::set_search_term, name = "search-term")]
        pub search_term: RefCell<String>,

        #[property(get, set = Self::set_selected_language_index, name = "selected-language-index")]
        pub selected_language_index: Cell<u32>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DiscoverDialog {
        const NAME: &'static str = "DiscoverDialog";
        type ParentType = Dialog;
        type Type = super::DiscoverDialog;

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();
        }

        fn instance_init(obj: &InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for DiscoverDialog {}

    impl WidgetImpl for DiscoverDialog {}

    impl AdwDialogImpl for DiscoverDialog {}

    #[gtk4::template_callbacks]
    impl DiscoverDialog {
        fn set_search_term(&self, search_term: String) {
            self.search_term.replace(search_term);

            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    imp.feedly_search().await;
                }
            ));
        }

        fn set_selected_language_index(&self, selected_language_index: u32) {
            self.selected_language_index.replace(selected_language_index);

            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    imp.feedly_search().await;
                }
            ));
        }

        #[template_callback]
        fn on_feed_selected(&self, feed: GFeed) {
            self.add_feed_widget.reset();
            self.add_feed_widget.set_feed(feed);
            self.navigation_view.push_by_tag("feed_add_page");
        }

        #[template_callback]
        fn on_feed_added(&self) {
            self.obj().close();
        }

        #[template_callback]
        fn visible_child_name(&self, search_term: String) -> &'static str {
            if search_term.is_empty() { "featured" } else { "search" }
        }

        #[template_callback]
        fn on_card_clicked(&self, button: &Button) {
            let search_term = button.label().unwrap_or_default().as_str().to_lowercase();
            self.obj().set_search_term(format!("#{search_term}"));
        }

        #[template_callback]
        async fn on_row_activated(&self, row: &ListBoxRow) {
            if let Some(serach_row) = row.downcast_ref::<SearchItemRow>() {
                self.parse_feed(&serach_row.feed_url()).await;
            }
        }

        async fn feedly_search(&self) {
            let locale = self
                .language_dropdown_model
                .string(self.selected_language_index.get())
                .map(|id| id.as_str().to_owned());

            let query = self.search_term.borrow().clone();
            let query_clone = query.clone();
            self.search_result_stack.set_visible_child_name("spinner");
            self.search_result_list.remove_all();

            let future = TokioRuntime::spawn(async move {
                let client = App::client().await;
                FeedlyApi::search_feedly_cloud(&client, &query, Some(30), locale.as_deref()).await
            });

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

            if query_clone != *self.search_term.borrow() {
                return;
            }

            match search_result {
                Ok(search_result) => {
                    self.search_result_list.remove_all();

                    let result_count = search_result.results.len();
                    for search_item in search_result.results.into_iter() {
                        if search_item.title.is_none() {
                            // dont show items without title
                            continue;
                        }
                        self.search_result_list.insert(&SearchItemRow::from(search_item), -1);
                    }

                    let child_name = if result_count > 0 { "list" } else { "empty" };
                    self.search_result_stack.set_visible_child_name(child_name);
                }
                Err(error) => {
                    self.search_result_stack.set_visible_child_name("empty");
                    tracing::error!(%error, "Feedly search query failed");
                }
            }
        }

        async fn parse_feed(&self, url: &str) {
            self.navigation_view.replace_with_tags(&["spinner"]);
            let feed_id = FeedID::new(url);
            let url = Url::parse(url).unwrap();
            let url_clone = url.clone();

            let future = TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let client = App::client().await;
                let semaphore = news_flash
                    .read()
                    .await
                    .as_ref()
                    .map(NewsFlash::get_semaphore)
                    .unwrap_or(Arc::new(Semaphore::new(1)));
                news_flash::feed_parser::download_and_parse_feed(&url_clone, &feed_id, None, semaphore, &client).await
            });

            match future.await.expect(JOIN_ERROR) {
                Ok(ParsedUrl::SingleFeed(feed)) => {
                    self.add_feed_widget.reset();
                    self.add_feed_widget.set_feed(GFeed::from(*feed));
                    self.navigation_view.replace_with_tags(&["discover", "add_feed"]);
                }
                Ok(ParsedUrl::MultipleFeeds(feeds)) => {
                    self.select_feed_widget.fill(feeds);
                    self.navigation_view.replace_with_tags(&["discover", "select_feed"]);
                }
                Err(error) => {
                    tracing::error!(?url, %error, "No feed found for url");
                    self.obj().close();
                    ContentPage::instance().newsflash_error(
                        &i18n("Failed to parse feed"),
                        NewsFlashError::API(FeedApiError::ParseFeed(error)),
                    );
                }
            }
        }
    }
}

glib::wrapper! {
    pub struct DiscoverDialog(ObjectSubclass<imp::DiscoverDialog>)
        @extends Widget, Dialog,
        @implements Accessible, Buildable, ConstraintTarget;
}

impl Default for DiscoverDialog {
    fn default() -> Self {
        Object::builder().property("selected-language-index", 0u32).build()
    }
}
