use chrono::{DateTime, Duration, Utc};
use gdk4::Texture;
use gdk4::prelude::*;
use glib::Bytes;
use moka::future::Cache;
use news_flash::NewsFlash;
use news_flash::models::{FavIcon, FeedID};
use once_cell::sync::OnceCell;
use reqwest::{Client, header::HeaderMap};
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Debug, Clone)]
pub struct FaviconCacheEntry {
    pub texture: Option<Texture>,
    pub expires: DateTime<Utc>,
}

impl FaviconCacheEntry {
    pub fn is_expired(&self) -> bool {
        Utc::now() >= self.expires
    }
}

static INSTANCE: OnceCell<FaviconCache> = OnceCell::new();

#[derive(Debug, Clone)]
pub struct FaviconCache {
    cache: Cache<FeedID, FaviconCacheEntry>,
}

impl Default for FaviconCache {
    fn default() -> Self {
        Self {
            cache: Cache::builder()
                .max_capacity(Self::CACHE_SIZE)
                .time_to_idle(std::time::Duration::from_secs(3 * 24 * 60 * 60))
                .weigher(|_key: &FeedID, value: &FaviconCacheEntry| -> u32 {
                    match &value.texture {
                        Some(texture) => {
                            let bytes = (texture.width() * texture.height() * 4) as u32;
                            bytes / 1024 // weight in KB
                        }
                        None => 1,
                    }
                })
                .build(),
        }
    }
}

impl FaviconCache {
    const CACHE_SIZE: u64 = 99999;
    const EXPIRES_AFTER_DAYS: i64 = 30;

    fn instance() -> &'static FaviconCache {
        INSTANCE.get_or_init(FaviconCache::default)
    }

    pub async fn get_icon(
        feed_id: &FeedID,
        news_flash: Arc<RwLock<Option<NewsFlash>>>,
        client: Client,
        headermap: HeaderMap,
    ) -> Option<Texture> {
        Self::instance()
            .get_icon_impl(feed_id, news_flash, client, headermap)
            .await
    }

    async fn get_icon_impl(
        &self,
        feed_id: &FeedID,
        news_flash: Arc<RwLock<Option<NewsFlash>>>,
        client: Client,
        headermap: HeaderMap,
    ) -> Option<Texture> {
        let init_future = Self::init_icon(feed_id, news_flash, client, headermap);
        let entry = self
            .cache
            .entry_by_ref(feed_id)
            .or_insert_with_if(init_future, FaviconCacheEntry::is_expired)
            .await;
        entry.into_value().texture
    }

    async fn init_icon(
        feed_id: &FeedID,
        news_flash: Arc<RwLock<Option<NewsFlash>>>,
        client: Client,
        headermap: HeaderMap,
    ) -> FaviconCacheEntry {
        let favicon = if let Some(news_flash) = news_flash.read().await.as_ref() {
            news_flash.get_icon(feed_id, &client, headermap).await.ok()
        } else {
            None
        };

        let expires = favicon
            .as_ref()
            .map(|favicon| favicon.expires)
            .unwrap_or(Utc::now() + Duration::try_days(Self::EXPIRES_AFTER_DAYS).unwrap());

        let texture = Self::decode_icon(favicon).await;

        FaviconCacheEntry { texture, expires }
    }

    async fn decode_icon(favicon: Option<FavIcon>) -> Option<Texture> {
        let data = favicon?.data?;
        let bytes = Bytes::from_owned(data);
        let loader = glycin::Loader::new_bytes(bytes);
        let image = loader.load().await.ok()?;
        let first_frame = image.next_frame().await.ok()?;
        let texture = first_frame.texture();
        Some(texture)
    }
}
