//! A small, defensive parser for Valve's KeyValues / VDF text format, used by
//! `appmanifest_<appid>.acf`, `libraryfolders.vdf`, and `~/.steam/registry.vdf`.
//!
//! Pure (operates on already-read file *contents*) and unit-tested, per the
//! testable-seams-first workflow — the file I/O and size caps live in the Steam
//! adapter. Deliberately a real recursive-descent KeyValues parser rather than a
//! `"name"`-line regex: escapes, nesting, and truncation will eventually break a
//! regex (Codex's "use a real VDF parser" hardening). Hardened against hostile
//! input with a recursion-depth cap, so a deeply nested file errors instead of
//! overflowing the stack, and never panics on malformed/truncated input.

/// Max object nesting depth accepted before bailing out. Real Steam files nest a
/// handful of levels (`registry.vdf` is the deepest at ~6); this is generous while
/// still bounding a malicious file.
const MAX_DEPTH: usize = 32;

/// A parsed KeyValues value: either a leaf string or a nested object. Child order
/// is preserved and duplicate keys are kept (KeyValues permits them); lookups
/// return the first match.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
    Str(String),
    Obj(Vec<(String, Value)>),
}

impl Value {
    /// The leaf string at this node, if it is a string (not an object).
    pub fn as_str(&self) -> Option<&str> {
        match self {
            Value::Str(s) => Some(s),
            Value::Obj(_) => None,
        }
    }

    /// The first child value under `key`, if this is an object containing it.
    /// Case-insensitive on the key (KeyValues keys are conventionally
    /// case-insensitive, and Steam is inconsistent, e.g. `AppState`/`appid`).
    pub fn get(&self, key: &str) -> Option<&Value> {
        match self {
            Value::Obj(pairs) => pairs
                .iter()
                .find(|(k, _)| k.eq_ignore_ascii_case(key))
                .map(|(_, v)| v),
            Value::Str(_) => None,
        }
    }

    /// Follow a chain of object keys, returning the value at the end of the path.
    /// `root.get_path(&["AppState", "name"])`.
    pub fn get_path<'a>(&'a self, path: &[&str]) -> Option<&'a Value> {
        let mut cur = self;
        for key in path {
            cur = cur.get(key)?;
        }
        Some(cur)
    }

    /// Iterate the (key, value) child pairs if this is an object.
    pub fn entries(&self) -> &[(String, Value)] {
        match self {
            Value::Obj(pairs) => pairs,
            Value::Str(_) => &[],
        }
    }
}

/// Parse KeyValues/VDF text into a top-level object (the sequence of root
/// key→value pairs). Returns `Err` on unbalanced braces, a key with no value, or
/// nesting past [`MAX_DEPTH`]. Never panics.
pub fn parse(input: &str) -> Result<Value, String> {
    let mut lexer = Lexer { rest: input };
    let obj = parse_object(&mut lexer, 0, true)?;
    Ok(Value::Obj(obj))
}

/// Parse a run of `key value` pairs. `top_level` parses until EOF; otherwise it
/// parses until a closing `}` (which it consumes).
fn parse_object(
    lexer: &mut Lexer,
    depth: usize,
    top_level: bool,
) -> Result<Vec<(String, Value)>, String> {
    if depth > MAX_DEPTH {
        return Err("VDF nesting too deep".to_string());
    }
    let mut pairs = Vec::new();
    loop {
        match lexer.next_token()? {
            None => {
                if top_level {
                    return Ok(pairs);
                }
                return Err("unexpected end of input inside object".to_string());
            }
            Some(Token::Close) => {
                if top_level {
                    return Err("unexpected '}' at top level".to_string());
                }
                return Ok(pairs);
            }
            Some(Token::Open) => {
                return Err("expected key, found '{'".to_string());
            }
            Some(Token::Str(key)) => {
                // A key must be followed by a value: a string or a nested object.
                match lexer.next_token()? {
                    Some(Token::Str(val)) => pairs.push((key, Value::Str(val))),
                    Some(Token::Open) => {
                        let child = parse_object(lexer, depth + 1, false)?;
                        pairs.push((key, Value::Obj(child)));
                    }
                    Some(Token::Close) => {
                        return Err(format!("key '{key}' has no value (found '}}')"));
                    }
                    None => return Err(format!("key '{key}' has no value (end of input)")),
                }
            }
        }
    }
}

enum Token {
    Open,
    Close,
    Str(String),
}

struct Lexer<'a> {
    rest: &'a str,
}

impl Lexer<'_> {
    /// Produce the next token, skipping whitespace and `//` line comments.
    fn next_token(&mut self) -> Result<Option<Token>, String> {
        loop {
            self.rest = self.rest.trim_start();
            if self.rest.is_empty() {
                return Ok(None);
            }
            // Line comments: `//` to end of line.
            if let Some(after) = self.rest.strip_prefix("//") {
                match after.find('\n') {
                    Some(nl) => self.rest = &after[nl + 1..],
                    None => {
                        self.rest = "";
                        return Ok(None);
                    }
                }
                continue;
            }
            let mut chars = self.rest.char_indices();
            let (_, first) = chars.next().expect("non-empty checked above");
            return match first {
                '{' => {
                    self.advance_bytes(first.len_utf8());
                    Ok(Some(Token::Open))
                }
                '}' => {
                    self.advance_bytes(first.len_utf8());
                    Ok(Some(Token::Close))
                }
                '"' => self.lex_quoted(),
                _ => Ok(Some(self.lex_bareword())),
            };
        }
    }

    fn advance_bytes(&mut self, n: usize) {
        self.rest = &self.rest[n..];
    }

    /// Lex a `"..."` string, decoding `\\ \" \n \t` escapes. Errors if unterminated.
    fn lex_quoted(&mut self) -> Result<Option<Token>, String> {
        // Skip the opening quote.
        self.advance_bytes(1);
        let mut out = String::new();
        let mut chars = self.rest.char_indices();
        while let Some((i, c)) = chars.next() {
            match c {
                '"' => {
                    // Consume through the closing quote.
                    self.rest = &self.rest[i + 1..];
                    return Ok(Some(Token::Str(out)));
                }
                '\\' => {
                    // Decode the escape.
                    match chars.next() {
                        Some((_, esc)) => out.push(match esc {
                            'n' => '\n',
                            't' => '\t',
                            'r' => '\r',
                            // `\\`, `\"`, and anything else: take the literal char.
                            other => other,
                        }),
                        None => return Err("unterminated escape in quoted string".to_string()),
                    }
                }
                other => out.push(other),
            }
        }
        Err("unterminated quoted string".to_string())
    }

    /// Lex an unquoted token: run of non-whitespace, non-brace, non-quote chars.
    fn lex_bareword(&mut self) -> Token {
        let end = self
            .rest
            .find(|c: char| c.is_whitespace() || matches!(c, '{' | '}' | '"'))
            .unwrap_or(self.rest.len());
        let word = self.rest[..end].to_string();
        self.rest = &self.rest[end..];
        Token::Str(word)
    }
}

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

    #[test]
    fn parses_appmanifest_name() {
        // A trimmed-down real appmanifest_<id>.acf.
        let acf = r#"
"AppState"
{
	"appid"		"730"
	"name"		"Counter-Strike 2"
	"StateFlags"		"4"
	"installdir"		"Counter-Strike Global Offensive"
	"UserConfig"
	{
		"language"		"english"
	}
}
"#;
        let root = parse(acf).unwrap();
        assert_eq!(
            root.get_path(&["AppState", "name"]).and_then(Value::as_str),
            Some("Counter-Strike 2")
        );
        assert_eq!(
            root.get_path(&["AppState", "appid"])
                .and_then(Value::as_str),
            Some("730")
        );
        // Case-insensitive key lookup.
        assert_eq!(
            root.get_path(&["appstate", "NAME"]).and_then(Value::as_str),
            Some("Counter-Strike 2")
        );
    }

    #[test]
    fn parses_libraryfolders_paths_with_escaped_backslashes() {
        // Windows paths arrive with doubled backslashes (escaped).
        let vdf = r#"
"libraryfolders"
{
	"0"
	{
		"path"		"C:\\Program Files (x86)\\Steam"
		"apps"
		{
			"730"		"35000000000"
		}
	}
	"1"
	{
		"path"		"/home/eric/.local/share/Steam"
	}
}
"#;
        let root = parse(vdf).unwrap();
        let lf = root.get("libraryfolders").unwrap();
        assert_eq!(
            lf.get_path(&["0", "path"]).and_then(Value::as_str),
            Some(r"C:\Program Files (x86)\Steam")
        );
        assert_eq!(
            lf.get_path(&["1", "path"]).and_then(Value::as_str),
            Some("/home/eric/.local/share/Steam")
        );
        // The library folder ids are iterable for discovery.
        let ids: Vec<&str> = lf.entries().iter().map(|(k, _)| k.as_str()).collect();
        assert_eq!(ids, vec!["0", "1"]);
    }

    #[test]
    fn parses_registry_running_appid_deep_path() {
        let reg = r#"
"Registry"
{
	"HKCU"
	{
		"Software"
		{
			"Valve"
			{
				"Steam"
				{
					"RunningAppID"		"570"
					"language"		"english"
				}
			}
		}
	}
}
"#;
        let root = parse(reg).unwrap();
        let appid = root
            .get_path(&[
                "Registry",
                "HKCU",
                "Software",
                "Valve",
                "Steam",
                "RunningAppID",
            ])
            .and_then(Value::as_str);
        assert_eq!(appid, Some("570"));
    }

    #[test]
    fn handles_comments_and_barewords() {
        let vdf = "// a comment\n\"root\"\n{\n\tbarekey barevalue // trailing\n}\n";
        let root = parse(vdf).unwrap();
        assert_eq!(
            root.get_path(&["root", "barekey"]).and_then(Value::as_str),
            Some("barevalue")
        );
    }

    #[test]
    fn rejects_malformed_without_panicking() {
        // Unbalanced braces.
        assert!(parse("\"a\" {").is_err());
        // Stray closing brace.
        assert!(parse("}").is_err());
        // Key with no value at EOF.
        assert!(parse("\"lonely\"").is_err());
        // Unterminated quoted string.
        assert!(parse("\"key\" \"unterminated").is_err());
    }

    #[test]
    fn rejects_pathologically_deep_nesting() {
        // Build MAX_DEPTH+5 nested objects; must error, not overflow the stack.
        let mut s = String::new();
        for i in 0..(MAX_DEPTH + 5) {
            s.push_str(&format!("\"k{i}\" {{"));
        }
        for _ in 0..(MAX_DEPTH + 5) {
            s.push('}');
        }
        assert!(parse(&s).is_err());
    }

    #[test]
    fn missing_keys_return_none_not_error() {
        let root = parse("\"AppState\" { \"appid\" \"1\" }").unwrap();
        assert_eq!(root.get_path(&["AppState", "name"]), None);
        assert_eq!(root.get_path(&["Nope"]), None);
        // Treating a string as an object yields None rather than panicking.
        assert_eq!(root.get_path(&["AppState", "appid", "deeper"]), None);
    }
}
