//! `Manifest.lock` writer.
//!
//! The lockfile is itself a Lua return-table (matches the plan's
//! "default to Lua return-table for consistency" decision in the open
//! questions section). Sha256 maps are emitted with sorted keys so
//! lockfile diffs are stable across re-installs.
//!
//! Timestamps are intentionally omitted: a lockfile that changes every
//! re-install is noisy in version control and complicates "did anything
//! change?" diffs.

use std::collections::BTreeMap;
use std::fmt::Write;

use super::fetch::FetchPlan;
use super::manifest::{Extension, Lib};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lockfile {
    pub assay: Option<String>,
    pub extensions: Vec<LockExtension>,
    pub libs: Vec<LockLib>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockExtension {
    pub name: String,
    pub version: String,
    pub url: String,
    /// Sorted by arch token for stable diffs.
    pub sha256: BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockLib {
    pub name: String,
    pub version: String,
    pub url: String,
    pub sha256: String,
}

impl LockExtension {
    pub fn new(ext: &Extension, plan: &FetchPlan) -> Self {
        let sha256 = ext
            .sha256
            .iter()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();
        LockExtension {
            name: ext.name.clone(),
            version: ext.version.clone(),
            url: plan.url.clone(),
            sha256,
        }
    }
}

impl LockLib {
    pub fn new(lib: &Lib, plan: &FetchPlan) -> Self {
        LockLib {
            name: lib.name.clone(),
            version: lib.version.clone(),
            url: plan.url.clone(),
            sha256: lib.sha256.clone(),
        }
    }
}

impl Lockfile {
    /// Render the lockfile as a Lua return-table source string.
    pub fn to_lua(&self) -> String {
        let mut out = String::with_capacity(256);
        out.push_str("-- Generated by `assay install`. Do not edit by hand.\n");
        out.push_str("return {\n");

        if let Some(v) = &self.assay {
            writeln!(out, "  assay = {},", lua_string(v)).unwrap();
        }

        out.push_str("  extensions = {\n");
        for e in &self.extensions {
            writeln!(out, "    {{").unwrap();
            writeln!(out, "      name = {},", lua_string(&e.name)).unwrap();
            writeln!(out, "      version = {},", lua_string(&e.version)).unwrap();
            writeln!(out, "      url = {},", lua_string(&e.url)).unwrap();
            out.push_str("      sha256 = {");
            let mut first = true;
            for (arch, hash) in &e.sha256 {
                if !first {
                    out.push(',');
                }
                write!(out, " {arch} = {}", lua_string(hash)).unwrap();
                first = false;
            }
            out.push_str(" },\n");
            writeln!(out, "    }},").unwrap();
        }
        out.push_str("  },\n");

        out.push_str("  libs = {\n");
        for l in &self.libs {
            writeln!(out, "    {{").unwrap();
            writeln!(out, "      name = {},", lua_string(&l.name)).unwrap();
            writeln!(out, "      version = {},", lua_string(&l.version)).unwrap();
            writeln!(out, "      url = {},", lua_string(&l.url)).unwrap();
            writeln!(out, "      sha256 = {},", lua_string(&l.sha256)).unwrap();
            writeln!(out, "    }},").unwrap();
        }
        out.push_str("  },\n");

        out.push_str("}\n");
        out
    }
}

fn lua_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => write!(out, "\\{}", c as u32).unwrap(),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}
