use std::{borrow::Cow, fmt, sync::OnceLock};

use regex::Regex;

use super::{BerryPackage, DependencyMeta, LockfileData, Metadata};

fn simple_string() -> Option<&'static Regex> {
    static RE: OnceLock<Option<Regex>> = OnceLock::new();
    RE.get_or_init(|| {
        Regex::new(r#"^[^-?:,\]\[{}#&*!|>'"%@` \t\r\n]([ \t]*[^,\]\[{}:# \t\r\n])*$"#).ok()
    })
    .as_ref()
}

const HEADER: &str = "# This file is generated by running \"yarn install\" inside your project.
# Manual changes might be lost - proceed with caution!
";

// We implement Display in order to produce a correctly serialized `yarn.lock`
// Since Berry is so particular about the contents we can't use the serde_yml
// serializer without forking it and heavy modifications. Implementing Display
// is more honest than writing a Serializer implementation since the serializer
// would only support a single type.
impl fmt::Display for LockfileData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{HEADER}\n{}\n", self.metadata)?;

        for (key, entry) in &self.packages {
            let wrapped_key = wrap_string(key);
            // Yaml 1.2 spec says that keys over 1024 characters need to be prefixed with ?
            // and the : goes in a new line
            let key_line = match wrapped_key.len() <= 1024 {
                true => format!("{wrapped_key}:"),
                false => format!("? {wrapped_key}\n:"),
            };
            write!(f, "\n{key_line}\n{entry}\n")?;
        }

        Ok(())
    }
}

impl fmt::Display for Metadata {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "__metadata:\n  version: {}", self.version,)?;
        if let Some(cache_key) = &self.cache_key {
            write!(f, "\n  cacheKey: {}", wrap_string(cache_key))?;
        }
        Ok(())
    }
}

const SPACE: char = ' ';
const NEWLINE: char = '\n';
const INDENT: &str = "  ";

impl fmt::Display for BerryPackage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // we only want to write a newline there was something before
        let mut first = true;
        let mut write_line = |field: &str, whitespace: char, value: &str| -> fmt::Result {
            if !value.is_empty() {
                if !first {
                    writeln!(f)?;
                }
                write!(f, "  {field}:{whitespace}{value}")?;
                first = false;
            }
            Ok(())
        };

        write_line("version", SPACE, &wrap_string(self.version.as_ref()))?;
        write_line("resolution", SPACE, &wrap_string(&self.resolution))?;
        if let Some(deps) = &self.dependencies {
            write_line(
                "dependencies",
                NEWLINE,
                &stringify_dependencies(deps.iter()),
            )?;
        }
        if let Some(peer_deps) = &self.peer_dependencies {
            write_line(
                "peerDependencies",
                NEWLINE,
                &stringify_dependencies(peer_deps.iter()),
            )?;
        }
        if let Some(deps_meta) = &self.dependencies_meta {
            write_line(
                "dependenciesMeta",
                NEWLINE,
                &stringify_dependencies_meta(deps_meta.iter()),
            )?;
        }
        if let Some(peer_deps_meta) = &self.peer_dependencies_meta {
            write_line(
                "peerDependenciesMeta",
                NEWLINE,
                &stringify_dependencies_meta(peer_deps_meta.iter()),
            )?;
        }
        if let Some(bin) = &self.bin {
            write_line("bin", NEWLINE, &stringify_dependencies(bin.iter()))?;
        }

        if let Some(checksum) = &self.checksum {
            write_line("checksum", SPACE, &wrap_string(checksum))?;
        }
        if let Some(conditions) = &self.conditions {
            write_line("conditions", SPACE, &wrap_string(conditions))?;
        }
        if let Some(language_name) = &self.language_name {
            write_line("languageName", SPACE, &wrap_string(language_name))?;
        }
        if let Some(link_type) = &self.link_type {
            write_line("linkType", SPACE, &wrap_string(link_type))?;
        }

        Ok(())
    }
}

fn stringify_dependencies<I, S1, S2>(entries: I) -> String
where
    I: Iterator<Item = (S1, S2)>,
    S1: AsRef<str>,
    S2: AsRef<str>,
{
    let mut string = String::new();
    let mut first = true;
    for (key, value) in entries {
        let key = key.as_ref();
        let value = value.as_ref();

        if !first {
            string.push('\n');
        }
        string.push_str(&format!("    {}: {}", wrap_string(key), wrap_string(value)));
        first = false;
    }
    string
}

fn stringify_dependencies_meta<'a, I, S>(metadata: I) -> String
where
    I: Iterator<Item = (S, &'a DependencyMeta)>,
    S: AsRef<str>,
{
    struct Builder {
        s: String,
    }

    impl Builder {
        fn new() -> Self {
            Self { s: String::new() }
        }

        fn add_field(&mut self, field: &str, value: bool) {
            if !self.s.is_empty() {
                self.s.push('\n');
            }
            self.s.push_str(&format!(
                "{INDENT}{INDENT}{INDENT}{}: {value}",
                wrap_string(field)
            ));
        }

        fn add_dependency(&mut self, dependency: &str) {
            if !self.s.is_empty() {
                self.s.push('\n');
            }
            self.s
                .push_str(&format!("{INDENT}{INDENT}{}:", wrap_string(dependency),));
        }
    }

    let mut builder = Builder::new();

    for (dependency, meta) in metadata {
        let dependency = dependency.as_ref();
        let mut first = true;
        let mut add_line = |field: &str, value: bool| {
            if first {
                builder.add_dependency(dependency);
            }
            builder.add_field(field, value);
            first = false;
        };
        if let Some(optional) = meta.optional {
            add_line("optional", optional);
        }
        if let Some(unplugged) = meta.unplugged {
            add_line("unplugged", unplugged);
        }
        if let Some(built) = meta.built {
            add_line("built", built);
        }
    }

    builder.s
}

fn wrap_string(s: &str) -> Cow<'_, str> {
    match simple_string().is_some_and(|re| re.is_match(s)) {
        // Simple strings require no wrapping
        true => Cow::from(s),
        // Complex strings require wrapping
        false => {
            Cow::from(serde_json::to_string(s).unwrap_or_else(|_| panic!("Unable to encode '{s}'")))
        }
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_metadata_display() {
        let metadata = Metadata {
            version: "6".into(),
            cache_key: Some("8c0".to_string()),
        };
        assert_eq!(
            metadata.to_string(),
            "__metadata:
  version: 6
  cacheKey: 8c0"
        );
    }

    #[test]
    fn test_wrap_string() {
        fn assert(input: &str, expected: &str) {
            assert_eq!(wrap_string(input), expected);
        }
        assert("debug@4.3.4", "debug@4.3.4");
        assert(
            "eslint-module-utils@npm:^2.7.3",
            "\"eslint-module-utils@npm:^2.7.3\"",
        );
        assert("@babel/core", "\"@babel/core\"");
    }

    #[test]
    fn test_long_key_gets_wrapped() {
        let long_key = "a".repeat(1025);
        let lockfile = LockfileData {
            metadata: Metadata {
                version: "6".into(),
                cache_key: Some("8".into()),
            },
            packages: [(
                long_key.clone(),
                BerryPackage {
                    version: "1.2.3".to_string(),
                    ..Default::default()
                },
            )]
            .iter()
            .cloned()
            .collect(),
        };
        let serialized = lockfile.to_string();
        assert!(serialized.contains(&format!("? {long_key}\n")));
    }

    #[test]
    fn test_stringify_dependencies_meta() {
        let metadata = DependencyMeta {
            optional: Some(false),
            unplugged: None,
            built: Some(true),
        };
        assert_eq!(
            stringify_dependencies_meta(Some(("turbo", &metadata)).into_iter()),
            "    turbo:\n      optional: false\n      built: true"
        );
    }

    #[test]
    fn test_stringify_dependencies_meta_multi() {
        let foo = DependencyMeta {
            optional: Some(true),
            unplugged: None,
            built: None,
        };
        let bar = DependencyMeta {
            optional: None,
            unplugged: None,
            built: Some(true),
        };
        assert_eq!(
            stringify_dependencies_meta(vec![("foo", &foo), ("bar", &bar)].into_iter()),
            "    foo:\n      optional: true\n    bar:\n      built: true"
        );
    }

    #[test]
    fn test_empty_dep_meta() {
        let metadata = DependencyMeta {
            optional: None,
            unplugged: None,
            built: None,
        };
        assert_eq!(
            stringify_dependencies_meta(Some(("turbo", &metadata)).into_iter()),
            ""
        );
    }
}
