use crate::app::{App, CACHE_DIR, IMAGE_DATA_DIR};
use crate::content_page::ContentPage;
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::settings::UserDataSize;
use crate::settings::general::{KeepArticlesDuration, PredefinedSyncInterval, SyncIntervalType};
use crate::util::{JOIN_ERROR, Util};
use chrono::TimeDelta;
use glib::{ParamSpec, Properties, RustClosure, clone, prelude::*, subclass::*};
use gtk4::{
    Accessible, Buildable, Button, CheckButton, ClosureExpression, CompositeTemplate, ConstraintTarget, Entry, Label,
    Switch, Widget, prelude::*, subclass::prelude::*,
};
use libadwaita::{ComboRow, EnumListItem, EnumListModel, PreferencesPage, prelude::*, subclass::prelude::*};
use news_flash::error::NewsFlashError;
use std::cell::RefCell;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::SettingsAppPage)]
    #[template(file = "data/resources/ui_templates/settings/app.blp")]
    pub struct SettingsAppPage {
        #[template_child]
        pub keep_running_switch: TemplateChild<Switch>,
        #[template_child]
        pub autostart_switch: TemplateChild<Switch>,
        #[template_child]
        pub startup_sync_switch: TemplateChild<Switch>,
        #[template_child]
        pub metered_sync_switch: TemplateChild<Switch>,
        #[template_child]
        pub predefined_sync_row: TemplateChild<ComboRow>,
        #[template_child]
        pub custom_update_entry: TemplateChild<Entry>,
        #[template_child]
        pub manual_update_check: TemplateChild<CheckButton>,
        #[template_child]
        pub predefined_update_check: TemplateChild<CheckButton>,
        #[template_child]
        pub custom_update_check: TemplateChild<CheckButton>,
        #[template_child]
        pub clean_db_button: TemplateChild<Button>,
        #[template_child]
        pub clear_cache_button: TemplateChild<Button>,
        #[template_child]
        pub user_data_label: TemplateChild<Label>,
        #[template_child]
        pub cache_label: TemplateChild<Label>,
        #[template_child]
        pub limit_articles_row: TemplateChild<ComboRow>,

        #[property(get, set = Self::set_custom_sync_interval, name = "custom-sync-interval")]
        pub custom_sync_interval: RefCell<String>,
    }

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

        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 SettingsAppPage {
        fn constructed(&self) {
            let general_settings = App::default().settings().general();

            general_settings
                .bind_property("keep-running-in-background", &*self.keep_running_switch, "active")
                .bidirectional()
                .sync_create()
                .build();

            general_settings
                .bind_property("keep-running-in-background", &*self.autostart_switch, "sensitive")
                .sync_create()
                .build();

            general_settings
                .bind_property("autostart", &*self.autostart_switch, "active")
                .bidirectional()
                .sync_create()
                .build();

            general_settings
                .bind_property("sync-on-startup", &*self.startup_sync_switch, "active")
                .bidirectional()
                .sync_create()
                .build();

            general_settings
                .bind_property("sync-on-metered", &*self.metered_sync_switch, "active")
                .bidirectional()
                .sync_create()
                .build();

            general_settings
                .bind_property("custom-sync-interval", &*self.obj(), "custom-sync-interval")
                .transform_to(|_binding, seconds: u32| Some(Self::format_custom_sync_interval(seconds)))
                .transform_from(|_binding, text: String| Self::parse_custom_sync_interval(&text))
                .bidirectional()
                .sync_create()
                .build();

            match general_settings.sync_type() {
                SyncIntervalType::Never => self.manual_update_check.set_active(true),
                SyncIntervalType::Predefined => self.predefined_update_check.set_active(true),
                SyncIntervalType::Custom => self.custom_update_check.set_active(true),
            }

            self.manual_update_check.connect_toggled(|_check| {
                App::default()
                    .settings()
                    .general()
                    .set_sync_type(SyncIntervalType::Never);
            });

            self.predefined_update_check.connect_toggled(|_check| {
                App::default()
                    .settings()
                    .general()
                    .set_sync_type(SyncIntervalType::Predefined);
            });

            self.custom_update_check.connect_toggled(|_check| {
                App::default()
                    .settings()
                    .general()
                    .set_sync_type(SyncIntervalType::Custom);
            });

            let params: &[gtk4::Expression] = &[];
            let closure = RustClosure::new(|values| {
                let e = values[0].get::<EnumListItem>().unwrap();
                let sync = PredefinedSyncInterval::from_u32(e.value() as u32)
                    .to_string()
                    .to_value();
                Some(sync)
            });
            let sync_interval_closure = ClosureExpression::new::<String>(params, closure);
            self.predefined_sync_row.set_expression(Some(&sync_interval_closure));
            self.predefined_sync_row
                .set_model(Some(&EnumListModel::new(PredefinedSyncInterval::static_type())));
            self.predefined_sync_row
                .set_selected(if let SyncIntervalType::Predefined = general_settings.sync_type() {
                    general_settings.predefined_sync_interval().as_u32()
                } else {
                    0
                });
            self.predefined_sync_row.connect_selected_notify(|row| {
                let interval = PredefinedSyncInterval::from_u32(row.selected());
                App::default()
                    .settings()
                    .general()
                    .set_predefined_sync_interval(interval);
            });

            let closure = RustClosure::new(|values| {
                let e = values[0].get::<EnumListItem>().unwrap();
                let keep = KeepArticlesDuration::from_u32(e.value() as u32).to_string().to_value();
                Some(keep)
            });
            let limit_articles_closure = ClosureExpression::new::<String>(params, closure);
            self.limit_articles_row.set_expression(Some(&limit_articles_closure));
            self.limit_articles_row
                .set_model(Some(&EnumListModel::new(KeepArticlesDuration::static_type())));

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

            self.parent_constructed();
        }
    }

    impl WidgetImpl for SettingsAppPage {}

    impl PreferencesPageImpl for SettingsAppPage {}

    #[gtk4::template_callbacks]
    impl SettingsAppPage {
        #[template_callback]
        async fn on_db_clean(&self) {
            self.clean_db_button.set_sensitive(false);

            TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let news_flash_guad = news_flash.read().await;
                if let Some(news_flash) = news_flash_guad.as_ref() {
                    _ = news_flash.clean_db();
                }
            })
            .await
            .expect(JOIN_ERROR);

            self.query_data_sizes().await;
            self.clean_db_button.set_sensitive(true);
        }

        #[template_callback]
        async fn on_clear_cache(&self) {
            self.clear_cache_button.set_sensitive(false);

            _ = TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let news_flash_guad = news_flash.read().await;
                if let Some(news_flash) = news_flash_guad.as_ref() {
                    _ = news_flash.delete_all_images();
                }

                tokio::fs::remove_dir_all(CACHE_DIR.as_path()).await
            })
            .await;

            self.query_data_sizes().await;
            self.clean_db_button.set_sensitive(true);
        }

        #[template_callback]
        async fn limit_articles_row_notify(&self, spec: ParamSpec) {
            if spec.name() == "selected" {
                let selected = self.limit_articles_row.selected();
                let limit_articles_duration = KeepArticlesDuration::from_u32(selected);
                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)?;
                    news_flash
                        .set_keep_articles_duration(limit_articles_duration.as_duration())
                        .await
                });

                if future.await.expect(JOIN_ERROR).is_err() {
                    ContentPage::instance().simple_message(&i18n("Failed to set setting 'limit articles duration'"));
                }
            }
        }

        async fn read_limit_articles(&self) {
            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_keep_articles_duration().await
            });

            let keep_articles_duration = future.await.expect(JOIN_ERROR);
            self.limit_articles_row
                .set_selected(KeepArticlesDuration::from_duration(keep_articles_duration).as_u32());
        }

        fn set_custom_sync_interval(&self, text: String) {
            if Self::parse_custom_sync_interval(&text).is_none() {
                self.custom_update_entry.add_css_class("error");
                return;
            };

            self.custom_update_entry.remove_css_class("error");
            self.custom_sync_interval.replace(text);
        }

        fn format_custom_sync_interval(total_seconds: u32) -> String {
            let hours = total_seconds / 3600;
            let minutes = (total_seconds - hours * 3600) / 60;
            let seconds = total_seconds - hours * 3600 - minutes * 60;

            format!("{hours:02}:{minutes:02}:{seconds:02}")
        }

        fn parse_custom_sync_interval(text: &str) -> Option<u32> {
            let char_count = text.chars().count();

            if char_count != 8 {
                return None;
            }

            let text_pieces: Vec<&str> = text.split(':').collect();
            if text_pieces.len() != 3 {
                return None;
            }

            let hours = text_pieces[0].parse::<u32>().ok()?;
            let minutes = text_pieces[1].parse::<u32>().ok()?;
            let seconds = text_pieces[2].parse::<u32>().ok()?;

            let duration = TimeDelta::try_hours(hours as i64).unwrap()
                + TimeDelta::try_minutes(minutes as i64).unwrap()
                + TimeDelta::try_seconds(seconds as i64).unwrap();
            let total_seconds = duration.num_seconds() as u32;

            if total_seconds == 0 {
                return None;
            }

            Some(total_seconds)
        }

        async fn query_data_sizes(&self) {
            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()?;

                let cache_size = news_flash::util::folder_size(&CACHE_DIR).unwrap_or(0);
                let pictures_size = news_flash::util::folder_size(&IMAGE_DATA_DIR).unwrap_or(0);
                let db_size = news_flash.database_size().ok();

                db_size.map(|db_size| UserDataSize {
                    database: db_size,
                    pictures: pictures_size,
                    cache: cache_size,
                })
            });

            if let Some(user_data_size) = future.await.expect(JOIN_ERROR) {
                self.user_data_label
                    .set_text(&Util::format_data_size(user_data_size.database.on_disk));
                self.cache_label
                    .set_text(&Util::format_data_size(user_data_size.cache + user_data_size.pictures));
            }
        }
    }
}

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

impl Default for SettingsAppPage {
    fn default() -> Self {
        glib::Object::new::<Self>()
    }
}
