pub mod constants;
mod date_util;
mod desktop_settings;
mod error;
mod gtk_util;

pub use date_util::DateUtil;
pub use desktop_settings::{ClockFormat, DesktopSettings};
pub use error::Elapsed;
use futures::future;
use gio::{NetworkConnectivity, Settings as GSettings, prelude::*};
use glib::GString;
pub use gtk_util::GTK_RESOURCE_FILE_ERROR;
pub use gtk_util::GtkUtil;
use news_flash::{
    NewsFlash,
    error::{FeedApiError, NewsFlashError},
};
use reqwest::{Client, ClientBuilder, NoProxy, Proxy};
use serde::Deserialize;
use serde::Deserializer;
use serde::de;
use url::Url;

use crate::config::VERSION;
use crate::settings::{AdvancedSettings, DEFAULT_TIMEOUT_SECONDS, ProxyModel, ProxyProtocoll};
use std::fmt;
use std::time::Duration;

pub const JOIN_ERROR: &str = "Error joining tokio task";

pub struct Util;

impl Util {
    pub fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct StringOrVec;

        impl<'de> de::Visitor<'de> for StringOrVec {
            type Value = Vec<String>;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("string or list of strings")
            }

            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                Ok(vec![s.to_owned()])
            }

            fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
            where
                S: de::SeqAccess<'de>,
            {
                Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
            }
        }

        deserializer.deserialize_any(StringOrVec)
    }

    pub fn has_image_extension(url: &str) -> bool {
        url.ends_with(".jpg")
            || url.ends_with(".jpeg")
            || url.ends_with(".png")
            || url.ends_with(".webp")
            || url.ends_with(".tiff")
    }

    pub async fn is_online(
        connectivity: NetworkConnectivity,
        network_available: bool,
        news_flash: &NewsFlash,
        client: &Client,
        ping_urls: &[String],
    ) -> bool {
        tracing::debug!(?connectivity, %network_available, "Networkmonitor connectivity");

        if network_available && connectivity == NetworkConnectivity::Full {
            return true;
        }

        let is_reachable = news_flash.is_reachable(client).await;
        tracing::debug!(?is_reachable, "backend reachable");
        match is_reachable {
            // if not supported (e.g. local RSS) try pinging configured urls
            Err(NewsFlashError::API(FeedApiError::Unsupported)) => {}

            Ok(reachable) => return reachable,
            Err(_) => return false,
        }

        let mut tasks = Vec::new();

        for ping_url in ping_urls {
            let ping_url = ping_url.clone();
            let client = client.clone();

            let task = tokio::spawn(async move {
                let res = client.head(&ping_url).timeout(Duration::from_secs(2)).send().await;

                match res {
                    Ok(res) => {
                        let status = res.status();
                        if status.is_success() {
                            tracing::info!(%ping_url, "ping succeeded");
                        } else {
                            tracing::info!(%ping_url, ?status, "ping failed");
                        }

                        status.is_success()
                    }
                    Err(error) => {
                        tracing::error!(%ping_url, %error, "ping failed");
                        false
                    }
                }
            });

            tasks.push(task);
        }

        future::join_all(tasks)
            .await
            .into_iter()
            .filter_map(|res| res.ok())
            .any(|res| res)
    }

    pub fn discover_gnome_proxy() -> Vec<ProxyModel> {
        let mut proxy_vec = Vec::new();

        let https_settings = GSettings::new("org.gnome.system.proxy.https");
        let https_url = https_settings.string("host");
        let https_port = https_settings.int("port") as u16;

        if !https_url.is_empty()
            && let Ok(mut https_url) = Url::parse(&https_url)
        {
            _ = https_url.set_port(Some(https_port));
            proxy_vec.push(ProxyModel {
                protocoll: ProxyProtocoll::Https,
                url: https_url.to_string(),
                user: None,
                password: None,
            });
        }

        let http_settings = GSettings::new("org.gnome.system.proxy.http");
        let http_url = http_settings.string("host");
        let http_port = http_settings.int("port") as u16;
        let http_user = http_settings.string("authentication-user");
        let http_pw = http_settings.string("authentication-password");

        if let Ok(mut http_url) = Url::parse(&http_url) {
            _ = http_url.set_port(Some(http_port));
            let user = if http_user.is_empty() {
                None
            } else {
                Some(http_user.to_string())
            };
            let password = if http_pw.is_empty() {
                None
            } else {
                Some(http_pw.to_string())
            };

            proxy_vec.push(ProxyModel {
                protocoll: ProxyProtocoll::Http,
                url: https_url.to_string(),
                user,
                password,
            });
        }

        proxy_vec
    }

    pub fn format_data_size(bytes: u64) -> String {
        let step = 1024.0;
        let kbytes = bytes as f64 / step;
        let mbytes = kbytes / step;
        let gbytes = mbytes / step;

        if bytes < 1024 {
            format!("{bytes} bytes")
        } else if kbytes < step {
            format!("{kbytes:.1} kb")
        } else if mbytes < step {
            format!("{mbytes:.1} mb")
        } else {
            format!("{gbytes:.1} gb")
        }
    }

    pub fn build_client(settings: AdvancedSettings) -> Client {
        let user_agent =
            format!("Newsflash/{VERSION} (RSS reader; +https://flathub.org/apps/io.gitlab.news_flash.NewsFlash)");

        let timeout = settings.timeout_seconds.unwrap_or(DEFAULT_TIMEOUT_SECONDS);

        let mut builder = ClientBuilder::new()
            .user_agent(user_agent)
            .use_rustls_tls()
            .hickory_dns(false)
            .gzip(true)
            .brotli(true)
            .timeout(Duration::from_secs(timeout))
            .danger_accept_invalid_certs(settings.accept_invalid_certs)
            .danger_accept_invalid_hostnames(settings.accept_invalid_hostnames);

        let ignore_hosts: Vec<GString> = GSettings::new("org.gnome.system.proxy").strv("ignore-hosts").into();
        let ignore_hosts = ignore_hosts.join(",");
        tracing::debug!(%ignore_hosts);
        let no_proxy = NoProxy::from_string(&ignore_hosts);
        tracing::debug!(?no_proxy);

        let mut proxys = settings.proxy.clone();
        proxys.append(&mut Util::discover_gnome_proxy());

        for proxy_model in proxys {
            if proxy_model.url.starts_with("socks4") {
                continue;
            }

            let mut proxy = match &proxy_model.protocoll {
                ProxyProtocoll::All => Proxy::all(proxy_model.url.clone()),
                ProxyProtocoll::Http => Proxy::http(proxy_model.url.clone()),
                ProxyProtocoll::Https => Proxy::https(proxy_model.url.clone()),
            }
            .unwrap_or_else(|_| panic!("Failed to build proxy: {}", proxy_model.url));

            if let Some(proxy_user) = &proxy_model.user
                && let Some(proxy_password) = &proxy_model.password
            {
                proxy = proxy.basic_auth(proxy_user, proxy_password);
            }

            let proxy = proxy.no_proxy(no_proxy.clone());
            tracing::debug!(?proxy);

            builder = builder.proxy(proxy);
        }

        builder.build().expect("Failed to build reqwest client")
    }
}
