use itertools::Itertools;
use ottavino::{Closure, Executor, Fuel};
use ottavino_util::serde::from_value;
use path_slash::PathBufExt;
use serde::{Deserialize, Deserializer};
/// Compatibility layer/adapter for the luarocks client
use std::{collections::HashMap, path::PathBuf};
use thiserror::Error;

use crate::{
    lua_rockspec::{DisplayAsLuaKV, DisplayAsLuaValue, DisplayLuaKV, DisplayLuaValue},
    ROCKSPEC_FUEL_LIMIT,
};

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifest {
    pub lib: RockManifestLib,
    pub lua: RockManifestLua,
    pub bin: RockManifestBin,
    pub doc: RockManifestDoc,
    pub conf: RockManifestConf,
    pub root: RockManifestRoot,
}

#[derive(Error, Debug)]
pub enum RockManifestError {
    #[error("could not parse rock_manifest: {0}")]
    Piccolo(#[from] ottavino::ExternError),
    #[error("could not deserialize rock_manifest: {0}")]
    Serde(#[from] ottavino_util::serde::de::Error),
    #[error("rock_manifest exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
    FuelLimitExceeded,
}

impl RockManifest {
    pub fn new(rock_manifest_content: &str) -> Result<Self, RockManifestError> {
        let mut lua = ottavino::Lua::core();

        Ok(lua
            .try_enter(|ctx| {
                let closure = Closure::load(ctx, None, rock_manifest_content.as_bytes())?;
                let executor = Executor::start(ctx, closure.into(), ());
                if !executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))? {
                    return Ok(Err(RockManifestError::FuelLimitExceeded));
                }
                Ok(
                    from_value::<Option<Self>>(ctx.globals().get_value(ctx, "rock_manifest"))
                        .map(Ok)?,
                )
            })??
            .unwrap_or_default())
    }

    pub fn to_lua_string(&self) -> String {
        self.display_lua().to_string()
    }
}

#[derive(Deserialize)]
struct RockManifestInternal {
    #[serde(default)]
    lib: HashMap<PathBuf, DirOrFileEntry>,
    #[serde(default)]
    lua: HashMap<PathBuf, DirOrFileEntry>,
    #[serde(default)]
    bin: HashMap<PathBuf, String>,
    #[serde(default)]
    doc: HashMap<PathBuf, DirOrFileEntry>,
    #[serde(default)]
    conf: HashMap<PathBuf, DirOrFileEntry>,
    #[serde(flatten)]
    root: HashMap<PathBuf, DirOrFileEntry>,
}

impl<'de> Deserialize<'de> for RockManifest {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let internal = RockManifestInternal::deserialize(deserializer)?;
        Ok(Self {
            lib: RockManifestLib {
                entries: internal.lib,
            },
            lua: RockManifestLua {
                entries: internal.lua,
            },
            bin: RockManifestBin {
                entries: internal.bin,
            },
            doc: RockManifestDoc {
                entries: internal.doc,
            },
            conf: RockManifestConf {
                entries: internal.conf,
            },
            root: RockManifestRoot {
                entries: internal.root,
            },
        })
    }
}

impl DisplayAsLuaKV for RockManifest {
    fn display_lua(&self) -> DisplayLuaKV {
        DisplayLuaKV {
            key: "rock_manifest".to_string(),
            value: DisplayLuaValue::Table(
                vec![
                    self.lua.display_lua(),
                    self.lib.display_lua(),
                    self.doc.display_lua(),
                    self.conf.display_lua(),
                    self.bin.display_lua(),
                ]
                .into_iter()
                .chain(self.root.entries.iter().map(|(key, entry)| DisplayLuaKV {
                    key: key.to_slash_lossy().to_string(),
                    value: entry.display_lua_value(),
                }))
                .collect_vec(),
            ),
        }
    }
}

impl DisplayAsLuaKV for (&PathBuf, &String) {
    fn display_lua(&self) -> DisplayLuaKV {
        DisplayLuaKV {
            key: format!("{}", self.0.display()),
            value: DisplayLuaValue::String(self.1.clone()),
        }
    }
}

impl DisplayAsLuaValue for HashMap<PathBuf, String> {
    fn display_lua_value(&self) -> DisplayLuaValue {
        DisplayLuaValue::Table(self.iter().map(|it| it.display_lua()).collect_vec())
    }
}

#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub(crate) enum DirOrFileEntry {
    DirEntry(HashMap<PathBuf, DirOrFileEntry>),
    FileEntry(String),
}

impl DisplayAsLuaValue for DirOrFileEntry {
    fn display_lua_value(&self) -> DisplayLuaValue {
        match self {
            Self::DirEntry(dir_map) => DisplayLuaValue::Table(to_lua_kv_vec(dir_map)),
            Self::FileEntry(md5sum) => DisplayLuaValue::String(md5sum.clone()),
        }
    }
}

impl DisplayAsLuaValue for HashMap<PathBuf, DirOrFileEntry> {
    fn display_lua_value(&self) -> DisplayLuaValue {
        DisplayLuaValue::Table(to_lua_kv_vec(self))
    }
}

fn to_lua_kv_vec(dir_map: &HashMap<PathBuf, DirOrFileEntry>) -> Vec<DisplayLuaKV> {
    dir_map
        .iter()
        .map(|(k, v)| DisplayLuaKV {
            key: k.to_slash_lossy().to_string(),
            value: v.display_lua_value(),
        })
        .collect_vec()
}

impl From<&str> for DirOrFileEntry {
    fn from(value: &str) -> Self {
        Self::FileEntry(value.into())
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestLua {
    pub entries: HashMap<PathBuf, DirOrFileEntry>,
}

impl DisplayAsLuaKV for RockManifestLua {
    fn display_lua(&self) -> DisplayLuaKV {
        DisplayLuaKV {
            key: "lua".to_string(),
            value: self.entries.display_lua_value(),
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestLib {
    pub entries: HashMap<PathBuf, DirOrFileEntry>,
}

impl DisplayAsLuaKV for RockManifestLib {
    fn display_lua(&self) -> crate::lua_rockspec::DisplayLuaKV {
        DisplayLuaKV {
            key: "lib".to_string(),
            value: self.entries.display_lua_value(),
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestBin {
    pub entries: HashMap<PathBuf, String>,
}

impl DisplayAsLuaKV for RockManifestBin {
    fn display_lua(&self) -> crate::lua_rockspec::DisplayLuaKV {
        DisplayLuaKV {
            key: "bin".to_string(),
            value: self.entries.display_lua_value(),
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestDoc {
    pub entries: HashMap<PathBuf, DirOrFileEntry>,
}

impl DisplayAsLuaKV for RockManifestDoc {
    fn display_lua(&self) -> crate::lua_rockspec::DisplayLuaKV {
        DisplayLuaKV {
            key: "doc".to_string(),
            value: self.entries.display_lua_value(),
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestConf {
    pub entries: HashMap<PathBuf, DirOrFileEntry>,
}

impl DisplayAsLuaKV for RockManifestConf {
    fn display_lua(&self) -> crate::lua_rockspec::DisplayLuaKV {
        DisplayLuaKV {
            key: "conf".to_string(),
            value: self.entries.display_lua_value(),
        }
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct RockManifestRoot {
    pub entries: HashMap<PathBuf, DirOrFileEntry>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    pub async fn rock_manifest_from_lua() {
        let rock_manifest_content = "
rock_manifest = {
   doc = {
      ['CHANGELOG.md'] = 'adbf3f997070946a5e61955d70bfadb2',
      LICENSE = '6bcb3636a93bdb8304439a4ff57e979c',
      ['README.md'] = '842bd0b364e36d982f02e22abee7742d'
   },
   conf = {
      ['config.toml'] = '8cbb3637a94bdb8304440a5ff58e980d',
   },
   lib = {
      ['toml_edit.so'] = '504d63aea7bb341a688ef28f1232fa9b',
   },
   plugin = {
      ['foo.lua'] = '506d61aea8bb340a688ef29f1235fa8c',
   },
   ['toml-edit-0.6.1-1.rockspec'] = 'fcdd3b0066632dec36cd5510e00bc55e'
}
        ";
        let rock_manifest = RockManifest::new(rock_manifest_content).unwrap();
        assert_eq!(
            rock_manifest,
            RockManifest {
                lib: RockManifestLib {
                    entries: HashMap::from_iter(vec![(
                        "toml_edit.so".into(),
                        "504d63aea7bb341a688ef28f1232fa9b".into()
                    )])
                },
                lua: RockManifestLua::default(),
                bin: RockManifestBin::default(),
                doc: RockManifestDoc {
                    entries: HashMap::from_iter(vec![
                        (
                            "CHANGELOG.md".into(),
                            "adbf3f997070946a5e61955d70bfadb2".into()
                        ),
                        ("LICENSE".into(), "6bcb3636a93bdb8304439a4ff57e979c".into()),
                        (
                            "README.md".into(),
                            "842bd0b364e36d982f02e22abee7742d".into()
                        ),
                    ])
                },
                conf: RockManifestConf {
                    entries: HashMap::from_iter(vec![(
                        "config.toml".into(),
                        "8cbb3637a94bdb8304440a5ff58e980d".into()
                    ),])
                },
                root: RockManifestRoot {
                    entries: HashMap::from_iter(vec![
                        (
                            "toml-edit-0.6.1-1.rockspec".into(),
                            "fcdd3b0066632dec36cd5510e00bc55e".into()
                        ),
                        (
                            "plugin".into(),
                            DirOrFileEntry::DirEntry(HashMap::from_iter(vec![(
                                "foo.lua".into(),
                                "506d61aea8bb340a688ef29f1235fa8c".into()
                            )])),
                        ),
                    ])
                },
            }
        );
    }

    #[tokio::test]
    pub async fn regression_http_rock_manifest_from_lua() {
        let content = String::from_utf8(
            tokio::fs::read("resources/test/http-0.4-0-rock_manifest")
                .await
                .unwrap(),
        )
        .unwrap();
        RockManifest::new(&content).unwrap();
    }
}
