//! JSON serialization helpers for the string-pool export/import feature.
//!
//! These functions are used by the CLI (`main.rs`) for the `--stringblocks-file`
//! option.  Keeping them here (instead of in `main.rs`) makes them unit-testable.

use crate::string_pool::StringPool;

// ─────────────────────────────── Export ──────────────────────────────────────

/// Serialize a string pool as a JSON object `{ "0": "…", "1": "…", … }`.
///
/// The returned string can be written to a file and later re-imported with
/// [`import_stringblocks`].
pub fn export_stringblocks(pool: &StringPool) -> String {
    let count = pool.strings.len();
    let mut json = String::from("{\n");
    for i in 0..count {
        let s = pool.decode(i as u32).unwrap_or_default();
        let comma = if i + 1 < count { "," } else { "" };
        json.push_str(&format!("  \"{}\": \"{}\"{}\n", i, json_escape(&s), comma));
    }
    json.push('}');
    json
}

/// Maximum number of strings accepted from a stringblocks JSON file.
const MAX_STRINGBLOCKS_ENTRIES: usize = 200_000;

/// Populate a string pool from a JSON object previously produced by
/// [`export_stringblocks`].
pub fn import_stringblocks(json: &str, pool: &mut StringPool) {
    let mut count = 0usize;
    for line in json.lines() {
        let line = line.trim();
        // Each data line looks like:  "key": "value",
        if let Some(colon) = line.find(':') {
            let value_part = line[colon + 1..].trim().trim_end_matches(',').trim();
            if value_part.starts_with('"') && value_part.ends_with('"') && value_part.len() >= 2 {
                // MED-6: cap entries to prevent OOM from a malicious stringblocks file.
                if count >= MAX_STRINGBLOCKS_ENTRIES {
                    break;
                }
                let inner = &value_part[1..value_part.len() - 1];
                pool.add(&json_unescape(inner));
                count += 1;
            }
        }
    }
}

// ──────────────────────────── JSON string helpers ─────────────────────────────

/// Escape a string for safe embedding inside a JSON double-quoted value.
pub fn json_escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    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 => out.push_str(&format!("\\u{:04x}", c as u32)),
            c => out.push(c),
        }
    }
    out
}

/// Unescape a JSON double-quoted string value (inverse of [`json_escape`]).
pub fn json_unescape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    while let Some(c) = chars.next() {
        if c != '\\' {
            out.push(c);
            continue;
        }
        match chars.next() {
            Some('"') => out.push('"'),
            Some('\\') => out.push('\\'),
            Some('n') => out.push('\n'),
            Some('r') => out.push('\r'),
            Some('t') => out.push('\t'),
            Some('u') => {
                let hex: String = (0..4).filter_map(|_| chars.next()).collect();
                if let Ok(v) = u32::from_str_radix(&hex, 16) {
                    if let Some(ch) = char::from_u32(v) {
                        out.push(ch);
                    }
                }
            }
            Some(c) => {
                out.push('\\');
                out.push(c);
            }
            None => break,
        }
    }
    out
}

// ─────────────────────────────── Unit tests ──────────────────────────────────

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

    #[test]
    fn test_json_escape_plain() {
        assert_eq!(json_escape("hello"), "hello");
    }

    #[test]
    fn test_json_escape_special() {
        assert_eq!(json_escape("a\"b\\c\nd"), r#"a\"b\\c\nd"#);
    }

    #[test]
    fn test_json_escape_control() {
        assert_eq!(json_escape("\x01"), "\\u0001");
    }

    #[test]
    fn test_json_unescape_plain() {
        assert_eq!(json_unescape("hello"), "hello");
    }

    #[test]
    fn test_json_unescape_special() {
        assert_eq!(json_unescape(r#"a\"b\\c\nd"#), "a\"b\\c\nd");
    }

    #[test]
    fn test_json_unescape_unicode() {
        assert_eq!(json_unescape("\\u0041"), "A");
    }

    #[test]
    fn test_roundtrip_escape_unescape() {
        let original = "hello \"world\"\nnewline\ttab\\backslash";
        assert_eq!(json_unescape(&json_escape(original)), original);
    }

    #[test]
    fn test_export_import_roundtrip() {
        let mut pool = StringPool::new(false);
        pool.add("hello");
        pool.add("world");
        pool.add("with \"quotes\"");

        let json = export_stringblocks(&pool);

        let mut pool2 = StringPool::new(false);
        import_stringblocks(&json, &mut pool2);

        assert_eq!(pool2.strings.len(), 3);
        assert_eq!(pool2.decode(0).unwrap(), "hello");
        assert_eq!(pool2.decode(1).unwrap(), "world");
        assert_eq!(pool2.decode(2).unwrap(), "with \"quotes\"");
    }
}
