use std::{collections::HashMap, fmt, path::{Path, PathBuf}, sync::{LazyLock, RwLock}};
use fs_err as fs;

use serde::{Deserialize, Serialize};
use tracing::debug;

use crate::error::{IndexLoadError, IndexSaveError};

use super::CacheId;


pub(super) const CURRENT_INDEX_VERSION: usize = 3;

pub(super) static CACHE_INDEX: LazyLock<CacheIndex> = LazyLock::new(|| {
    CacheIndex::load_from_file(&default_cache_index_file())
        .expect("Fatal error while accessing cache file")
});


pub(super) fn default_cache_index_file() -> PathBuf {
    cicero_path_core::cicero::internal().join("distribution/cache_index.json")
}

#[derive(Serialize, Deserialize)]
pub(super) struct CacheIndex {
    pub items: RwLock<HashMap<CacheId, IndexOutputEntry>>,
    file: PathBuf,
}

#[derive(Clone, Serialize, Deserialize)]
pub(super) struct IndexOutputEntry {
    pub change_indicator: IndexChangeIndicator,
    pub input_states_in_last_successful_build: HashMap<CacheId, IndexChangeIndicator>,
}

impl CacheIndex {
    pub(super) fn load_from_file(file: &Path) -> Result<Self, IndexLoadError> {
        let loaded_index = if file.exists() {
            let file = fs::File::open(file)
                .map_err(|source| IndexLoadError::ExistsButCannotOpen { file: file.to_owned(), source })?; //break build, as this likely means the permissions are wrong, which needs user intervention

            if let Ok(previous_cache_index) = serde_json::from_reader::<_, IndexFileContent>(file) {

                if previous_cache_index.version == CURRENT_INDEX_VERSION {
                    Some(previous_cache_index.items)
                } else {
                    None //ignore index which doesn't match our version (and overwrite it when we save back)
                }
            } else {
                None
            }
        } else {
            None
        };

        let items = if let Some(index) = loaded_index {
            debug!("Loaded cache index from {file:?}.");
            index
        } else {
            debug!("Loaded empty cache index. Tried to load from {file:?}.");
            HashMap::new()
        };

        Ok(CacheIndex { items: RwLock::new(items), file: file.to_owned() })
    }

    pub(super) fn get(&self, output_id: &CacheId) -> Option<IndexOutputEntry> {
        let items = self.items.read()
            .expect("Failed to lock CacheIndex items for reading an item");

        items.get(output_id).cloned()
    }

    pub(super) fn insert(&self, output_id: CacheId, index_output_entry: IndexOutputEntry) {
        self.items.write()
            .expect("Failed to lock CacheIndex items for inserting a new item") //our logic should prevent more than one processes from locking this for now
            .insert(output_id, index_output_entry);
    }

    pub(super) fn save_to_file(&self) -> Result<(), IndexSaveError> {
        let current_index_items = self.items.read()
            .expect("Failed to lock CacheIndex items for saving to file")
            .clone();

        let file = self.file.clone();
        let index = IndexFileContent {
            version: CURRENT_INDEX_VERSION,
            items: current_index_items,
        };

        let index = serde_json::to_string_pretty(&index)
            .map_err(|source| IndexSaveError::Serialize { file: file.clone(), source })?;

        if let Some(parent_dir) = file.parent() {
            fs::create_dir_all(parent_dir)
                .map_err(|source| IndexSaveError::Io { file: file.clone(), source })?;
        }
        fs::write(&file, &index)
            .map_err(|source| IndexSaveError::Io { file: file.clone(), source })?;

        debug!("Stored updated cache index to {file:?}.");

        Ok(())
    }
}


#[derive(Serialize, Deserialize)]
struct IndexFileContent {
    version: usize,
    items: HashMap<CacheId, IndexOutputEntry>,
}


#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub(super) struct IndexChangeIndicator {
    pub value: String,
}
impl fmt::Debug for IndexChangeIndicator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}
#[allow(clippy::from_over_into)] //don't need both conversion directions
impl Into<super::ChangeIndicator> for Option<&IndexChangeIndicator> {
    fn into(self) -> super::ChangeIndicator {
        match self {
            Some(change_indicator) => super::ChangeIndicator::Exists(change_indicator.value.to_owned()),
            None => super::ChangeIndicator::ExistsNot,
        }
    }
}
#[allow(clippy::from_over_into)] //don't need both conversion directions
impl Into<super::ChangeIndicator> for IndexChangeIndicator {
    fn into(self) -> super::ChangeIndicator {
        super::ChangeIndicator::Exists(self.value.to_owned())
    }
}
impl From<super::ChangeIndicator> for Option<IndexChangeIndicator> {
    fn from(value: super::ChangeIndicator) -> Self {
        match value {
            super::ChangeIndicator::Exists(value) => Some(IndexChangeIndicator { value }),
            super::ChangeIndicator::ExistsNot => None,
        }
    }
}
