use crate::api::client::Client;
use crate::config::build_config;
use crate::error::Error;
use crate::project::Project;
use chrono::Duration;
use chrono::{DateTime, Utc};
use eyre::{ContextCompat, WrapErr};
use kinetics_api::orgs::validators;
use kinetics_api::projects;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
const CACHE_EXPIRES_IN: Duration = Duration::minutes(10);

/// The structure of entire cache file
///
/// The cache is stored in a file, and gets refreshed automatically when it expires
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct Cache {
    pub(super) projects: HashMap<String, Project>,
    last_updated: DateTime<Utc>,
}

impl Cache {
    /// Load the project cache from disk with automatic refresh logic
    pub(super) async fn new(org: Option<&str>) -> eyre::Result<Self> {
        let cache_path = Self::path(org)?;

        let cache: Option<Self> = if !cache_path.exists() {
            // Create the cache directory if it doesn't exist
            if let Some(parent) = cache_path.parent() {
                fs::create_dir_all(parent)
                    .inspect_err(|e| {
                        log::error!("Failed to create cache directory {parent:?}: {e:?}")
                    })
                    .wrap_err("Failed to create project cache")?;
            }

            None
        } else {
            // Read existing cache
            let cache_content = fs::read_to_string(&cache_path)
                .inspect_err(|e| log::error!("Failed to read cache file {cache_path:?}: {e:?}"))
                .wrap_err("Failed to load project cache")?;

            match serde_json::from_str(&cache_content) {
                Ok::<Cache, _>(cache) if Utc::now() - cache.last_updated < CACHE_EXPIRES_IN => {
                    Some(cache)
                }
                // The cache will be updated
                _ => None,
            }
        };

        // Load projects and populate cache if failed to read from disk
        let cache = match cache {
            Some(x) => x,
            None => Self::load(org).await?,
        };

        // Save cache to the file
        let cache_json = serde_json::to_string_pretty(&cache)
            .inspect_err(|e| log::error!("Failed to serialize project cache: {e:?}"))
            .wrap_err("Failed to process cache")?;

        let cache_path = Self::path(org)?;

        fs::write(&cache_path, cache_json)
            .inspect_err(|e| log::error!("Failed to write cache file {cache_path:?}: {e:?}"))
            .wrap_err("Failed to write cache")?;

        Ok(cache)
    }

    /// Get a project from cache
    pub(super) fn get(&self, project_name: &str) -> eyre::Result<Project> {
        self.projects
            .get(project_name)
            .wrap_err("Project not found")
            .cloned()
    }

    pub(super) fn clear() -> eyre::Result<()> {
        let cache_dir = PathBuf::from(build_config()?.kinetics_path);

        if !cache_dir.exists() {
            return Ok(());
        }

        for entry in fs::read_dir(&cache_dir)
            .inspect_err(|e| log::error!("Failed to read cache directory {cache_dir:?}: {e:?}"))
            .wrap_err("Failed to clear the projects cache")?
        {
            let entry = entry
                .inspect_err(|e| log::error!("Failed to read cache entry in {cache_dir:?}: {e:?}"))
                .wrap_err("Failed to clear the projects cache")?;

            let path = entry.path();
            let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
                continue;
            };

            // Project caches include `.projects` and org-scoped `.projects.<org>` files.
            if file_name.starts_with(".projects") {
                fs::remove_file(&path)
                    .inspect_err(|e| log::error!("Failed to remove cache file {path:?}: {e:?}"))
                    .wrap_err("Failed to clear the projects cache")?;
            }
        }

        Ok(())
    }

    /// Get the static cache path for storing project information.
    fn path(org: Option<&str>) -> eyre::Result<PathBuf> {
        let file_name = match org {
            Some(org) => {
                if !validators::Name::validate(org) {
                    return Err(eyre::eyre!(validators::Name::message()));
                }

                format!(".projects.{org}")
            }
            None => ".projects".to_string(),
        };

        Ok(PathBuf::from(build_config()?.kinetics_path).join(file_name))
    }

    async fn load(org: Option<&str>) -> eyre::Result<Self> {
        let response = Client::new(false)
            .await?
            .request::<projects::Request, projects::Response>(
                "/projects",
                projects::Request {
                    org: org.map(str::to_owned),
                },
            )
            .await
            .wrap_err(Error::new(
                "Failed to fetch project information",
                Some("Try again in a few seconds."),
            ))?;

        let projects = HashMap::from_iter(
            response
                .projects
                .into_iter()
                .map(|project| (project.name.clone(), project.into())),
        );

        Ok(Self {
            projects,
            last_updated: Utc::now(),
        })
    }
}
