use crate::config::Config;
use crate::fs;
use crate::lockfile::LocalPackageLockType;
use crate::tree::InstallTree;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceError;
use crate::workspace::WorkspaceTreeError;
use crate::workspace::LUX_DIR_NAME;
use bon::Builder;
use itertools::Itertools;
use miette::Diagnostic;
use path_slash::PathBufExt;
use pathdiff::diff_paths;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
use thiserror::Error;

#[derive(Error, Debug, Diagnostic)]
pub enum GenLuaRcError {
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] fs::FsError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Workspace(#[from] WorkspaceError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    WorkspaceTree(#[from] WorkspaceTreeError),
    #[error("failed to serialize luarc content:\n{0}")]
    Serialize(String),
    #[error("failed to deserialize luarc content:\n{0}")]
    Deserialize(String),
}

#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub(crate) struct GenLuaRc<'a> {
    config: &'a Config,
    workspace: &'a Workspace,
}

impl<State> GenLuaRcBuilder<'_, State>
where
    State: gen_lua_rc_builder::State + gen_lua_rc_builder::IsComplete,
{
    pub async fn generate_luarc(self) -> Result<(), GenLuaRcError> {
        do_generate_luarc(self._build()).await
    }
}

#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
#[serde(default)]
struct LuaRC {
    #[serde(flatten)] // <-- capture any unknown keys here
    other: BTreeMap<String, serde_json::Value>,

    #[serde(default)]
    workspace: LuaRcWorkspace,
}

#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
struct LuaRcWorkspace {
    #[serde(flatten)] // <-- capture any unknown keys here
    other: BTreeMap<String, serde_json::Value>,

    #[serde(default)]
    library: Vec<String>,
}

async fn do_generate_luarc(args: GenLuaRc<'_>) -> Result<(), GenLuaRcError> {
    let config = args.config;
    if !config.generate_luarc() {
        return Ok(());
    }
    let workspace = args.workspace;
    let lockfile = workspace.lockfile()?;
    let luarc_path = workspace.luarc_path(config);

    // read the existing .luarc file or initialise a new one if it doesn't exist
    let luarc_content = fs::tokio::read_to_string(&luarc_path)
        .await
        .unwrap_or_else(|_| "{}".into());

    let dependency_tree = workspace.tree(config)?;
    let dependency_dirs = lockfile
        .local_pkg_lock(&LocalPackageLockType::Regular)
        .rocks()
        .values()
        .map(|dependency| dependency_tree.installed_rock_layout(dependency))
        .filter_map(Result::ok)
        .map(|rock_layout| rock_layout.src)
        .filter(|dir| dir.is_dir())
        .filter_map(|dependency_dir| diff_paths(dependency_dir, workspace.root()));

    let test_dependency_tree = workspace.test_tree(config)?;
    let test_dependency_dirs = lockfile
        .local_pkg_lock(&LocalPackageLockType::Test)
        .rocks()
        .values()
        .map(|dependency| test_dependency_tree.installed_rock_layout(dependency))
        .filter_map(Result::ok)
        .map(|rock_layout| rock_layout.src)
        .filter(|dir| dir.is_dir())
        .filter_map(|test_dependency_dir| diff_paths(test_dependency_dir, workspace.root()));

    let library_dirs = dependency_dirs
        .chain(test_dependency_dirs)
        .sorted()
        .collect_vec();

    let luarc_content = update_luarc_content(&luarc_content, library_dirs)?;

    fs::tokio::write(&luarc_path, luarc_content).await?;

    Ok(())
}

fn update_luarc_content(
    prev_contents: &str,
    extra_paths: Vec<PathBuf>,
) -> Result<String, GenLuaRcError> {
    let mut luarc: LuaRC = serde_json::from_str(prev_contents)
        .map_err(|err| GenLuaRcError::Deserialize(err.to_string()))?;

    // remove any preexisting lux library paths
    luarc
        .workspace
        .library
        .retain(|path| !path.starts_with(&format!("{LUX_DIR_NAME}/")));

    extra_paths
        .iter()
        .map(|path| path.to_slash_lossy().to_string())
        .for_each(|path_str| luarc.workspace.library.push(path_str));

    serde_json::to_string_pretty(&luarc).map_err(|err| GenLuaRcError::Serialize(err.to_string()))
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn test_generate_luarc_with_previous_libraries_parametrized() {
        let cases = vec![
            (
                "Empty existing libraries, adding single lib", // 📝 Description
                r#"{
                    "workspace": {
                        "library": []
                    }
                }"#,
                vec![".lux/5.1/my-lib".into()],
                r#"{
                    "workspace": {
                        "library": [".lux/5.1/my-lib"]
                    }
                }"#,
            ),
            (
                "Other fields present, adding libs", // 📝 Description
                r#"{
                    "any-other-field": true,
                    "workspace": {
                        "library": []
                    }
                }"#,
                vec![".lux/5.1/lib-A".into(), ".lux/5.1/lib-B".into()],
                r#"{
                    "any-other-field": true,
                    "workspace": {
                        "library": [".lux/5.1/lib-A", ".lux/5.1/lib-B"]
                    }
                }"#,
            ),
            (
                "Removes not present libs, without removing others", // 📝 Description
                r#"{
                    "workspace": {
                        "library": [".lux/5.1/lib-A", ".lux/5.4/lib-B"]
                    }
                }"#,
                vec![".lux/5.1/lib-C".into()],
                r#"{
                    "workspace": {
                        "library": [".lux/5.1/lib-C"]
                    }
                }"#,
            ),
        ];

        for (description, initial, new_libs, expected) in cases {
            let content = super::update_luarc_content(initial, new_libs.clone()).unwrap();

            assert_eq!(
                serde_json::from_str::<LuaRC>(&content).unwrap(),
                serde_json::from_str::<LuaRC>(expected).unwrap(),
                "Case failed: {}\nInitial input:\n{}\nNew libs: {:?}",
                description,
                initial,
                new_libs
            );
        }
    }
}
