use crate::{DataProviderConfig, DataSourceError};
use cid::{multibase::Base, multihash::Multihash};
use directories::ProjectDirs;
use newton_prover_core::config::{ipfs::IpfsConfig, NewtonAvsConfig};
use ring::digest::{Context, SHA256};
use std::{collections::VecDeque, path::PathBuf};
use tokio::fs;
use tracing::debug;

#[derive(Debug)]
pub struct WasmCache {
    cache_dir: PathBuf,
    max_cache_size: u64,
    // fifo deque
    cache_order: VecDeque<String>,
    current_size: u64,
    ipfs_config: IpfsConfig,
}

impl WasmCache {
    pub fn new<P: Into<PathBuf>>(cache_dir: P, max_cache_size: u64, ipfs_config: IpfsConfig) -> Self {
        Self {
            cache_dir: cache_dir.into(),
            max_cache_size,
            cache_order: VecDeque::new(),
            current_size: 0,
            ipfs_config,
        }
    }

    pub fn default_cache() -> Self {
        let cache_dir = ProjectDirs::from("", "", "newton-prover")
            .map(|dirs| dirs.cache_dir().join("wasm"))
            .unwrap_or_else(|| PathBuf::from("./cache/wasm")); // default

        let ipfs_config = IpfsConfig::default();
        Self::new(cache_dir, 100 * 1024 * 1024, ipfs_config) // 100 MiB default
    }

    pub fn from_config(config: &NewtonAvsConfig<DataProviderConfig>) -> Self {
        let cache_dir = ProjectDirs::from("com", "newton", "prover")
            .map(|dirs| dirs.cache_dir().join("wasm"))
            .unwrap_or_else(|| PathBuf::from("./cache/wasm"));

        Self::new(cache_dir, config.service.max_cache_size, config.ipfs.clone())
    }

    pub async fn initialize(&mut self) -> Result<(), DataSourceError> {
        // load existing cache files on startup
        if !self.cache_dir.exists() {
            return Ok(());
        }

        let mut entries = fs::read_dir(&self.cache_dir)
            .await
            .map_err(|e| DataSourceError::NetworkError(format!("failed to read cache dir: {e}")))?;

        let mut cache_entries = Vec::new();

        while let Some(entry) = entries
            .next_entry()
            .await
            .map_err(|e| DataSourceError::NetworkError(format!("failed to read cache entry: {e}")))?
        {
            let path = entry.path();
            if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
                let metadata = entry
                    .metadata()
                    .await
                    .map_err(|e| DataSourceError::NetworkError(format!("failed to read metadata: {e}")))?;
                let modified = metadata
                    .modified()
                    .map_err(|e| DataSourceError::NetworkError(format!("failed to read modified time: {e}")))?;

                cache_entries.push((file_name.to_string(), metadata.len(), modified));
            }
        }

        // sorts by modified time
        cache_entries.sort_by_key(|(_, _, modified)| *modified);

        // add in order and evict if for some reason the initial wasms are over
        // max wasm size
        for (cache_key, size, _) in cache_entries {
            if self.current_size + size <= self.max_cache_size {
                self.cache_order.push_back(cache_key);
                self.current_size += size;
            } else {
                // remove this file as it exceeds our cache limit
                let file_path = self.cache_dir.join(cache_key);
                if let Err(e) = fs::remove_file(&file_path).await {
                    debug!("failed to remove oversized cache file {}: {}", file_path.display(), e);
                }
            }
        }

        debug!(
            "initialized wasm cache with {} entries, {} bytes",
            self.cache_order.len(),
            self.current_size
        );
        Ok(())
    }

    fn cache_key(&self, url: &str) -> String {
        let mut context = Context::new(&SHA256);
        context.update(url.as_bytes());
        let digest = context.finish();

        let m_hash: Multihash<64> = Multihash::wrap(0x12, digest.as_ref()).expect("failed to wrap hash");
        cid::multibase::encode(Base::Base16Lower, m_hash.to_bytes())
    }

    fn cache_path(&self, url: &str) -> PathBuf {
        self.cache_dir.join(self.cache_key(url))
    }

    pub async fn get(&mut self, url: &str) -> Result<Vec<u8>, DataSourceError> {
        self.get_or_download(url).await
    }

    pub async fn add(&mut self, url: &str, wasm_bytes: Vec<u8>) -> Result<(), DataSourceError> {
        let cache_key = self.cache_key(url);
        let cache_path = self.cache_path(url);
        let file_size = wasm_bytes.len() as u64;

        if let Some(parent) = cache_path.parent() {
            fs::create_dir_all(parent)
                .await
                .map_err(|e| DataSourceError::NetworkError(format!("failed to create cache dir: {e}")))?
        }

        // evict entries if needed to make room
        while self.current_size + file_size > self.max_cache_size && !self.cache_order.is_empty() {
            if let Some(old_key) = self.cache_order.pop_front() {
                let old_path = self.cache_dir.join(old_key);
                if let Ok(metadata) = fs::metadata(&old_path).await {
                    self.current_size = self.current_size.saturating_sub(metadata.len());
                }
                if let Err(e) = fs::remove_file(&old_path).await {
                    debug!("failed to remove old cache file {}: {}", old_path.display(), e);
                }
            }
        }

        // this is the max wasm size
        if file_size > self.max_cache_size {
            return Err(DataSourceError::NetworkError(format!(
                "wasm file too large ({} bytes) for cache limit ({} bytes)",
                file_size, self.max_cache_size
            )));
        }

        fs::write(&cache_path, &wasm_bytes)
            .await
            .map_err(|e| DataSourceError::NetworkError(format!("failed to write wasm to cache: {e}")))?;

        self.cache_order.push_back(cache_key.clone());
        self.current_size += file_size;

        debug!(
            "cached wasm at: {} (size: {} bytes, total: {} bytes)",
            cache_path.display(),
            file_size,
            self.current_size
        );
        Ok(())
    }

    async fn get_or_download(&mut self, url: &str) -> Result<Vec<u8>, DataSourceError> {
        let cache_path = self.cache_path(url);

        // try to load from cache first
        if cache_path.exists() {
            debug!("loading wasm from cache: {}", cache_path.display());
            return fs::read(&cache_path)
                .await
                .map_err(|e| DataSourceError::NetworkError(format!("failed to read cached wasm: {e}")));
        }

        debug!("downloading wasm from url: {}", url);

        // GET
        let response = newton_prover_core::common::fetch_from_ipfs(url, Some(&self.ipfs_config))
            .await
            .map_err(|e| DataSourceError::NetworkError(format!("failed to download wasm: {e}")))?;

        if !response.status().is_success() {
            return Err(DataSourceError::NetworkError(format!(
                "http error downloading wasm: {} {}",
                response.status(),
                url
            )));
        }

        let wasm_bytes = response
            .bytes()
            .await
            .map_err(|e| DataSourceError::NetworkError(format!("failed to read wasm response: {e}")))?
            .to_vec();

        // add fetched wasm
        self.add(url, wasm_bytes.clone()).await?;

        debug!("downloaded and cached wasm from url: {}", url);
        Ok(wasm_bytes)
    }

    pub async fn clear_cache(&mut self) -> Result<(), DataSourceError> {
        if self.cache_dir.exists() {
            fs::remove_dir_all(&self.cache_dir)
                .await
                .map_err(|e| DataSourceError::NetworkError(format!("failed to clear cache: {e}")))?;
        }
        self.cache_order.clear();
        self.current_size = 0;
        Ok(())
    }

    pub fn cache_size(&self) -> u64 {
        self.current_size
    }

    pub fn max_cache_size(&self) -> u64 {
        self.max_cache_size
    }

    pub fn cache_entries(&self) -> usize {
        self.cache_order.len()
    }
}
