use crate::concat_pixiv_downloader_error;
use crate::db::PixivDownloaderDb;
use crate::downloader::{DownloaderHelper, DownloaderResult};
use crate::error::PixivDownloaderError;
use crate::get_helper;
use crate::utils::get_file_name_from_url;
use crate::webclient::ToHeaders;
use chrono::{DateTime, Utc};
use futures_util::lock::Mutex;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::Arc;

#[derive(Debug)]
pub struct TmpCacheEntry {
    pub url: String,
    pub path: String,
    pub last_used: DateTime<Utc>,
}

pub struct TmpCache {
    used: Mutex<HashMap<String, ()>>,
    db: Arc<Box<dyn PixivDownloaderDb + Send + Sync>>,
    in_cleaning: Mutex<()>,
}

impl TmpCache {
    pub fn new(db: Arc<Box<dyn PixivDownloaderDb + Send + Sync>>) -> Self {
        Self {
            used: Mutex::new(HashMap::new()),
            db,
            in_cleaning: Mutex::new(()),
        }
    }

    async fn _get_cache<H: ToHeaders>(
        &self,
        url: &str,
        headers: H,
    ) -> Result<PathBuf, PixivDownloaderError> {
        match self.db.get_tmp_cache(url).await? {
            Some(ent) => {
                if tokio::fs::try_exists(&ent.path).await.unwrap_or(false) {
                    match self.db.update_tmp_cache(url).await {
                        Ok(_) => {}
                        Err(e) => {
                            log::warn!(target: "tmp_cache", "Failed to update tmp cache entry {} in database: {}", url, e);
                        }
                    }
                    return Ok(PathBuf::from(&ent.path));
                }
            }
            None => match self.db.delete_tmp_cache(url).await {
                _ => {}
            },
        }
        let mut tmp_dir = get_helper().temp_dir();
        let u = get_file_name_from_url(url).unwrap_or_else(|| {
            let mut hasher = DefaultHasher::new();
            url.hash(&mut hasher);
            hasher.finish().to_string()
        });
        let dh = DownloaderHelper::builder(url)?
            .file_name(&u)
            .headers(headers)
            .build();
        let d = dh.download_local(Some(true), &tmp_dir)?;
        match d {
            DownloaderResult::Ok(d) => {
                d.disable_progress_bar();
                d.download();
                d.join().await?;
            }
            DownloaderResult::Canceled => {
                return Err(PixivDownloaderError::from("Download canceled."));
            }
        }
        tmp_dir.push(u);
        match self
            .db
            .put_tmp_cache(url, tmp_dir.to_string_lossy().trim())
            .await
        {
            Ok(()) => {}
            Err(e) => {
                log::warn!(target: "tmp_cache", "Failed to write cache {} to database: {}", url, e);
            }
        }
        Ok(tmp_dir)
    }

    pub async fn get_cache<H: ToHeaders>(
        &self,
        url: &str,
        headers: H,
    ) -> Result<PathBuf, PixivDownloaderError> {
        self.wait_for_url(url).await;
        let re = self._get_cache(url, headers).await;
        self.remove_for_url(url).await;
        re
    }

    async fn _get_local_cache(&self, url: &str) -> Result<Option<PathBuf>, PixivDownloaderError> {
        match self.db.get_tmp_cache(url).await? {
            Some(ent) => {
                if tokio::fs::try_exists(&ent.path).await.unwrap_or(false) {
                    match self.db.update_tmp_cache(url).await {
                        Ok(_) => {}
                        Err(e) => {
                            log::warn!(target: "tmp_cache", "Failed to update tmp cache entry {} in database: {}", url, e);
                        }
                    }
                    return Ok(Some(PathBuf::from(&ent.path)));
                }
            }
            None => match self.db.delete_tmp_cache(url).await {
                _ => {}
            },
        }
        Ok(None)
    }

    pub async fn get_local_cache(
        &self,
        filename: &str,
    ) -> Result<Option<PathBuf>, PixivDownloaderError> {
        let mut tmp_dir = get_helper().temp_dir();
        tmp_dir.push(filename);
        if !tmp_dir.is_absolute() {
            tmp_dir = tmp_dir.canonicalize()?;
        }
        let url = reqwest::Url::from_file_path(tmp_dir).expect("file is not absolute.");
        self.wait_for_url(url.as_str()).await;
        let re = self._get_local_cache(url.as_str()).await;
        self.remove_for_url(url.as_str()).await;
        re
    }

    pub async fn push_local_cache(&self, filename: &str) -> Result<(), PixivDownloaderError> {
        let mut tmp_dir = get_helper().temp_dir();
        tmp_dir.push(filename);
        if !tmp_dir.is_absolute() {
            tmp_dir = tmp_dir.canonicalize()?;
        }
        let url = reqwest::Url::from_file_path(&tmp_dir).expect("file is not absolute.");
        self.wait_for_url(url.as_str()).await;
        let re = self
            .db
            .put_tmp_cache(url.as_str(), tmp_dir.to_string_lossy().trim())
            .await;
        self.remove_for_url(url.as_str()).await;
        Ok(re?)
    }

    pub async fn remove_cache_entry(&self, ent: TmpCacheEntry) -> Result<(), PixivDownloaderError> {
        let t = self.in_cleaning.try_lock();
        if t.is_none() {
            return Ok(());
        }
        self.wait_for_url(&ent.url).await;
        match tokio::fs::remove_file(&ent.path).await {
            Ok(_) => {}
            Err(e) => {
                log::warn!(target: "tmp_cache", "Failed to remove cache {}: {}", ent.path, e);
            }
        }
        match self.db.delete_tmp_cache(&ent.url).await {
            Ok(_) => {}
            Err(e) => {
                self.remove_for_url(&ent.url).await;
                Err(e)?;
            }
        }
        self.remove_for_url(&ent.url).await;
        Ok(())
    }

    pub async fn remove_expired_cache(&self) -> Result<(), PixivDownloaderError> {
        let entries = self.db.get_tmp_caches(3600).await?;
        let mut err = Ok(());
        for ent in entries {
            let e = self.remove_cache_entry(ent).await;
            concat_pixiv_downloader_error!(err, e);
        }
        err
    }

    async fn remove_for_url(&self, url: &str) {
        let mut m = self.used.lock().await;
        m.remove(url);
    }

    async fn wait_for_url(&self, url: &str) {
        loop {
            {
                let mut m = self.used.lock().await;
                if !m.contains_key(url) {
                    m.insert(url.to_owned(), ());
                    break;
                }
            }
            tokio::time::sleep(std::time::Duration::new(0, 100_000_000)).await;
        }
    }
}
