mod advanced;
mod article_list;
mod article_view;
mod category;
mod dialog;
mod error;
mod feed;
mod feed_list;
mod general;
mod keybindings;
mod share;
mod user_data_size;

pub use self::advanced::{AdvancedSettings, DEFAULT_TIMEOUT_SECONDS, ProxyModel, ProxyProtocoll};
pub use self::error::SettingsError;
pub use self::feed_list::GFeedOrder;
pub use self::general::SyncIntervalType;
use self::share::ShareSettings;
pub use self::user_data_size::UserDataSize;
use crate::sidebar::{FeedList, FeedListItem, FeedListItemID, FeedListTree};
use crate::util::constants::DEFAULT_ARTICLE_CONTENT_WIDTH;
use article_list::ArticleListSettings;
pub use article_list::{GArticleOrder, GOrderBy};
pub use article_view::ArticleViewSettings;
use category::CategorySettings;
pub use dialog::SettingsDialog;
use feed::FeedSettings;
use feed_list::FeedListSettings;
use general::GeneralSettings;
use glib::{Properties, prelude::*, subclass::prelude::*};
pub use keybindings::Keybindings;
use news_flash::models::{CategoryID, FeedID};
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::sync::RwLock;

static CONFIG_NAME: &str = "newsflash_gtk.json";

mod imp {
    use super::*;

    #[derive(Default, Debug, Serialize, Deserialize, Properties)]
    #[properties(wrapper_type = super::Settings)]
    pub struct Settings {
        #[property(get, set)]
        pub general: RefCell<GeneralSettings>,

        #[property(get, set)]
        pub feed_list: RefCell<FeedListSettings>,

        #[property(get, set)]
        pub article_list: RefCell<ArticleListSettings>,

        #[property(get, set)]
        pub article_view: RefCell<ArticleViewSettings>,

        #[property(get, set)]
        pub keybindings: RefCell<Keybindings>,

        #[property(get, set)]
        pub share: RefCell<ShareSettings>,

        #[serde(default)]
        pub feeds: RefCell<HashMap<FeedID, FeedSettings>>,

        #[serde(default)]
        pub category: RefCell<HashMap<CategoryID, CategorySettings>>,

        pub advanced: RwLock<AdvancedSettings>,

        #[serde(skip_serializing)]
        #[serde(skip_deserializing)]
        pub path: RefCell<PathBuf>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Settings {
        const NAME: &'static str = "NewsFlashSettings";
        type Type = super::Settings;
    }

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

glib::wrapper! {
    pub struct Settings(ObjectSubclass<imp::Settings>);
}

impl Serialize for Settings {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.imp().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for Settings {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let inner = imp::Settings::deserialize(deserializer)?;
        Ok(inner.into())
    }
}

impl From<imp::Settings> for Settings {
    fn from(inner: imp::Settings) -> Self {
        let obj = glib::Object::builder::<Settings>()
            .property("general", inner.general.borrow().clone())
            .property("feed-list", inner.feed_list.borrow().clone())
            .property("article-list", inner.article_list.borrow().clone())
            .property("article-view", inner.article_view.borrow().clone())
            .property("keybindings", inner.keybindings.borrow().clone())
            .property("share", inner.share.borrow().clone())
            .build();
        let imp = obj.imp();
        *imp.advanced.write().unwrap() = inner.advanced.read().unwrap().clone();
        imp.feeds.replace(inner.feeds.borrow().clone());
        obj
    }
}

impl Default for Settings {
    fn default() -> Self {
        imp::Settings::default().into()
    }
}

impl Settings {
    pub fn open() -> Result<Self, SettingsError> {
        let path = crate::app::CONFIG_DIR.join(CONFIG_NAME);
        tracing::info!(?path, "Attempting to open Newsflash config file");

        if path.as_path().exists() {
            let data =
                fs::read_to_string(&path).inspect_err(|error| tracing::error!(%error, "failed to open config"))?;
            let mut settings = serde_json::from_str::<Self>(&data)
                .inspect_err(|error| tracing::error!(%error, "failed to deserialize config"))?;
            settings.imp().path.replace(path);
            Self::run_migrations(&mut settings)?;
            return Ok(settings);
        }

        tracing::info!("No existing config. Creating new config with default settings");
        fs::create_dir_all(crate::app::CONFIG_DIR.as_path())?;

        let settings = Settings::default();
        settings.imp().path.replace(path);
        settings.write()?;
        Ok(settings)
    }

    fn run_migrations(settings: &mut Settings) -> Result<(), SettingsError> {
        let imp = settings.imp();
        let mut changed = false;

        let content_width = imp.article_view.borrow().content_width();
        if content_width < 200 {
            tracing::warn!(%content_width, "content_width is suspiciously small. Overwriting with default value");
            imp.article_view
                .borrow()
                .set_content_width(DEFAULT_ARTICLE_CONTENT_WIDTH);
            changed = true;
        }

        if changed {
            settings.write()?;
        }
        Ok(())
    }

    pub fn write(&self) -> Result<(), SettingsError> {
        let data = serde_json::to_string_pretty(self)?;
        fs::write(&*self.imp().path.borrow(), data)?;
        Ok(())
    }

    pub fn set_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> Result<(), SettingsError> {
        self.imp().advanced.write().unwrap().accept_invalid_certs = accept_invalid_certs;
        self.write()?;
        Ok(())
    }

    pub fn set_accept_invalid_hostnames(&mut self, accept_invalid_hostnames: bool) -> Result<(), SettingsError> {
        self.imp().advanced.write().unwrap().accept_invalid_hostnames = accept_invalid_hostnames;
        self.write()?;
        Ok(())
    }

    pub fn advanced(&self) -> AdvancedSettings {
        self.imp().advanced.read().unwrap().clone()
    }

    pub fn get_feed_settings(&self, feed_id: &FeedID) -> Option<FeedSettings> {
        self.imp().feeds.borrow().get(feed_id).cloned()
    }

    pub fn get_category_settings(&self, category_id: &CategoryID) -> Option<CategorySettings> {
        self.imp().category.borrow().get(category_id).cloned()
    }

    pub fn set_feed_settings(&mut self, feed_id: &FeedID, settings: &FeedSettings) -> Result<(), SettingsError> {
        self.imp().feeds.borrow_mut().insert(feed_id.clone(), settings.clone());
        self.write()?;
        Ok(())
    }

    pub fn set_category_settings(
        &mut self,
        category_id: &CategoryID,
        settings: &CategorySettings,
    ) -> Result<(), SettingsError> {
        self.imp()
            .category
            .borrow_mut()
            .insert(category_id.clone(), settings.clone());
        self.write()?;
        Ok(())
    }

    pub fn delete_old_feed_settings(&mut self, feed_ids: &HashSet<FeedID>) -> Result<(), SettingsError> {
        self.imp()
            .feeds
            .borrow_mut()
            .retain(|key, _value| feed_ids.contains(key));
        self.write()?;
        Ok(())
    }

    pub fn delete_old_category_settings(&mut self, category_ids: &HashSet<CategoryID>) -> Result<(), SettingsError> {
        self.imp()
            .category
            .borrow_mut()
            .retain(|key, _value| category_ids.contains(key));
        self.write()?;
        Ok(())
    }

    pub fn get_merged_feed_settings_map(&self) -> HashMap<FeedID, FeedSettings> {
        let tree = FeedList::instance().get_tree();
        self.get_merged_feed_settings_map_with_tree(&tree)
    }

    pub(self) fn get_merged_feed_settings_map_with_tree(&self, tree: &FeedListTree) -> HashMap<FeedID, FeedSettings> {
        let mut map = HashMap::new();
        let imp = self.imp();
        let feed_settings = imp.feeds.borrow();
        let category_settings = imp.category.borrow();

        fn walk(
            items: &[FeedListItem],
            current_settings: FeedSettings,
            category_settings: &HashMap<CategoryID, CategorySettings>,
            feed_settings: &HashMap<FeedID, FeedSettings>,
            result: &mut HashMap<FeedID, FeedSettings>,
        ) {
            for item in items {
                match &item.id {
                    FeedListItemID::Category(category_id) => {
                        let mut next_settings = current_settings.clone();
                        if let Some(cat_setting) = category_settings.get(category_id) {
                            if let Some(val) = &cat_setting.inline_math {
                                next_settings.inline_math = Some(val.clone());
                            }
                            if let Some(val) = &cat_setting.custom_user_agent {
                                next_settings.custom_user_agent = Some(val.clone());
                            }
                            if cat_setting.scrap_content {
                                next_settings.scrap_content = true;
                            }
                            if cat_setting.mute_notifications {
                                next_settings.mute_notifications = true;
                            }
                        }
                        walk(&item.children, next_settings, category_settings, feed_settings, result);
                    }
                    FeedListItemID::Feed(mapping) => {
                        let mut next_settings = current_settings.clone();
                        if let Some(feed_setting) = feed_settings.get(&mapping.feed_id) {
                            if let Some(val) = &feed_setting.inline_math {
                                next_settings.inline_math = Some(val.clone());
                            }
                            if let Some(val) = &feed_setting.custom_user_agent {
                                next_settings.custom_user_agent = Some(val.clone());
                            }
                            if feed_setting.scrap_content {
                                next_settings.scrap_content = true;
                            }
                            if feed_setting.mute_notifications {
                                next_settings.mute_notifications = true;
                            }
                        }
                        result.insert(mapping.feed_id.clone(), next_settings);
                    }
                }
            }
        }

        walk(
            &tree.top_level,
            FeedSettings::default(),
            &category_settings,
            &feed_settings,
            &mut map,
        );

        map
    }

    pub fn get_merged_feed_setting(&self, feed_id: &FeedID) -> FeedSettings {
        let tree = FeedList::instance().get_tree();
        self.get_merged_feed_setting_with_tree(feed_id, &tree)
    }

    pub(self) fn get_merged_feed_setting_with_tree(&self, feed_id: &FeedID, tree: &FeedListTree) -> FeedSettings {
        let imp = self.imp();
        let feed_settings = imp.feeds.borrow();
        let category_settings = imp.category.borrow();

        let mut result = FeedSettings::default();

        fn find_parents(items: &[FeedListItem], target_feed_id: &FeedID, current_path: &mut Vec<CategoryID>) -> bool {
            for item in items {
                match &item.id {
                    FeedListItemID::Feed(mapping) if mapping.feed_id == *target_feed_id => {
                        return true;
                    }
                    FeedListItemID::Category(category_id) => {
                        current_path.push(category_id.clone());
                        if find_parents(&item.children, target_feed_id, current_path) {
                            return true;
                        }
                        current_path.pop();
                    }
                    _ => {}
                }
            }
            false
        }

        let mut path = Vec::new();
        if find_parents(&tree.top_level, feed_id, &mut path) {
            for category_id in path.iter() {
                if let Some(cat_setting) = category_settings.get(category_id) {
                    if let Some(val) = &cat_setting.inline_math {
                        result.inline_math = Some(val.clone());
                    }
                    if let Some(val) = &cat_setting.custom_user_agent {
                        result.custom_user_agent = Some(val.clone());
                    }
                    if cat_setting.scrap_content {
                        result.scrap_content = true;
                    }
                    if cat_setting.mute_notifications {
                        result.mute_notifications = true;
                    }
                }
            }
        }

        if let Some(feed_setting) = feed_settings.get(feed_id) {
            if let Some(val) = &feed_setting.inline_math {
                result.inline_math = Some(val.clone());
            }
            if let Some(val) = &feed_setting.custom_user_agent {
                result.custom_user_agent = Some(val.clone());
            }
            if feed_setting.scrap_content {
                result.scrap_content = true;
            }
            if feed_setting.mute_notifications {
                result.mute_notifications = true;
            }
        }

        result
    }

    fn extract_header_map(custom_user_agent: &Option<String>) -> HeaderMap<HeaderValue> {
        let mut header_map = HeaderMap::new();
        if let Some(user_agent_str) = custom_user_agent
            && let Ok(user_agent_value) = HeaderValue::from_str(user_agent_str)
        {
            header_map.insert(USER_AGENT, user_agent_value);
        }
        header_map
    }

    pub fn get_feed_header_maps(&self) -> HashMap<FeedID, HeaderMap<HeaderValue>> {
        let tree = FeedList::instance().get_tree();
        self.get_feed_header_maps_with_tree(&tree)
    }

    pub(self) fn get_feed_header_maps_with_tree(&self, tree: &FeedListTree) -> HashMap<FeedID, HeaderMap<HeaderValue>> {
        let merged_settings = self.get_merged_feed_settings_map_with_tree(tree);
        let mut map = HashMap::new();

        for (feed_id, settings) in merged_settings {
            map.insert(feed_id, Self::extract_header_map(&settings.custom_user_agent));
        }

        map
    }

    pub fn get_category_header_maps(&self) -> HashMap<CategoryID, HeaderMap<HeaderValue>> {
        let category_settings = self.imp().category.borrow();
        let mut map = HashMap::new();

        for (category_id, settings) in category_settings.iter() {
            map.insert(
                category_id.clone(),
                Self::extract_header_map(&settings.custom_user_agent),
            );
        }

        map
    }

    pub fn get_feed_header_map(&self, feed_id: &FeedID) -> HeaderMap<HeaderValue> {
        let tree = FeedList::instance().get_tree();
        self.get_feed_header_map_with_tree(feed_id, &tree)
    }

    pub(self) fn get_feed_header_map_with_tree(&self, feed_id: &FeedID, tree: &FeedListTree) -> HeaderMap<HeaderValue> {
        let merged_setting = self.get_merged_feed_setting_with_tree(feed_id, tree);
        Self::extract_header_map(&merged_setting.custom_user_agent)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use news_flash::models::{Category, CategoryMapping, Feed, FeedMapping};

    #[test]
    fn test_user_agent_inheritance() {
        let settings = Settings::default();
        settings
            .imp()
            .path
            .replace(std::env::temp_dir().join("test_settings_inheritance.json"));

        let cat_id = CategoryID::new("cat1");
        let mut cat_settings = CategorySettings::default();
        cat_settings.custom_user_agent = Some("CatUA".to_string());
        settings
            .imp()
            .category
            .borrow_mut()
            .insert(cat_id.clone(), cat_settings);

        let feed_id = FeedID::new("feed1");
        let feed_settings = FeedSettings::default();
        settings.imp().feeds.borrow_mut().insert(feed_id.clone(), feed_settings);

        let mut tree = FeedListTree::new();
        let cat = Category {
            category_id: cat_id.clone(),
            label: "Cat 1".to_string(),
        };
        let cat_mapping = CategoryMapping {
            category_id: cat_id.clone(),
            parent_id: news_flash::models::NEWSFLASH_TOPLEVEL.clone(),
            sort_index: None,
        };
        tree.add_categories(vec![(&cat, &cat_mapping, 0)]);

        let feed = Feed {
            feed_id: feed_id.clone(),
            label: "Feed 1".to_string(),
            website: None,
            feed_url: None,
            icon_url: None,
            error_count: 0,
            error_message: None,
        };
        let feed_mapping = FeedMapping {
            feed_id: feed_id.clone(),
            category_id: cat_id.clone(),
            sort_index: None,
        };
        tree.add_feed(&feed, &feed_mapping, 0).unwrap();

        // Test get_feed_header_map_with_tree
        let header_map = settings.get_feed_header_map_with_tree(&feed_id, &tree);
        assert_eq!(header_map.get(USER_AGENT).unwrap(), "CatUA");

        // Test get_merged_feed_settings_map_with_tree
        let header_maps = settings.get_merged_feed_settings_map_with_tree(&tree);
        assert_eq!(
            header_maps.get(&feed_id).unwrap().custom_user_agent.as_ref().unwrap(),
            "CatUA"
        );
    }

    #[test]
    fn test_user_agent_override() {
        let settings = Settings::default();
        settings
            .imp()
            .path
            .replace(std::env::temp_dir().join("test_settings_override.json"));

        let cat_id = CategoryID::new("cat1");
        let mut cat_settings = CategorySettings::default();
        cat_settings.custom_user_agent = Some("CatUA".to_string());
        settings
            .imp()
            .category
            .borrow_mut()
            .insert(cat_id.clone(), cat_settings);

        let feed_id = FeedID::new("feed1");
        let mut feed_settings = FeedSettings::default();
        feed_settings.custom_user_agent = Some("FeedUA".to_string());
        settings.imp().feeds.borrow_mut().insert(feed_id.clone(), feed_settings);

        let mut tree = FeedListTree::new();
        let cat = Category {
            category_id: cat_id.clone(),
            label: "Cat 1".to_string(),
        };
        let cat_mapping = CategoryMapping {
            category_id: cat_id.clone(),
            parent_id: news_flash::models::NEWSFLASH_TOPLEVEL.clone(),
            sort_index: None,
        };
        tree.add_categories(vec![(&cat, &cat_mapping, 0)]);

        let feed = Feed {
            feed_id: feed_id.clone(),
            label: "Feed 1".to_string(),
            website: None,
            feed_url: None,
            icon_url: None,
            error_count: 0,
            error_message: None,
        };
        let feed_mapping = FeedMapping {
            feed_id: feed_id.clone(),
            category_id: cat_id.clone(),
            sort_index: None,
        };
        tree.add_feed(&feed, &feed_mapping, 0).unwrap();

        let header_map = settings.get_feed_header_map_with_tree(&feed_id, &tree);
        assert_eq!(header_map.get(USER_AGENT).unwrap(), "FeedUA");
    }

    #[test]
    fn test_user_agent_deep_inheritance() {
        let settings = Settings::default();
        settings
            .imp()
            .path
            .replace(std::env::temp_dir().join("test_settings_deep.json"));

        let root_cat_id = CategoryID::new("root");
        let mut root_cat_settings = CategorySettings::default();
        root_cat_settings.custom_user_agent = Some("RootUA".to_string());
        settings
            .imp()
            .category
            .borrow_mut()
            .insert(root_cat_id.clone(), root_cat_settings);

        let sub_cat_id = CategoryID::new("sub");
        let sub_cat_settings = CategorySettings::default(); // No UA here
        settings
            .imp()
            .category
            .borrow_mut()
            .insert(sub_cat_id.clone(), sub_cat_settings);

        let feed_id = FeedID::new("feed1");
        let feed_settings = FeedSettings::default();
        settings.imp().feeds.borrow_mut().insert(feed_id.clone(), feed_settings);

        let mut tree = FeedListTree::new();
        let root_cat = Category {
            category_id: root_cat_id.clone(),
            label: "Root Cat".to_string(),
        };
        let root_cat_mapping = CategoryMapping {
            category_id: root_cat_id.clone(),
            parent_id: news_flash::models::NEWSFLASH_TOPLEVEL.clone(),
            sort_index: None,
        };
        tree.add_categories(vec![(&root_cat, &root_cat_mapping, 0)]);

        let sub_cat = Category {
            category_id: sub_cat_id.clone(),
            label: "Sub Cat".to_string(),
        };
        let sub_cat_mapping = CategoryMapping {
            category_id: sub_cat_id.clone(),
            parent_id: root_cat_id.clone(),
            sort_index: None,
        };
        tree.add_categories(vec![(&sub_cat, &sub_cat_mapping, 0)]);

        let feed = Feed {
            feed_id: feed_id.clone(),
            label: "Feed 1".to_string(),
            website: None,
            feed_url: None,
            icon_url: None,
            error_count: 0,
            error_message: None,
        };
        let feed_mapping = FeedMapping {
            feed_id: feed_id.clone(),
            category_id: sub_cat_id.clone(),
            sort_index: None,
        };
        tree.add_feed(&feed, &feed_mapping, 0).unwrap();

        let header_map = settings.get_feed_header_map_with_tree(&feed_id, &tree);
        assert_eq!(header_map.get(USER_AGENT).unwrap(), "RootUA");
    }
}
