use crate::consts::{ 
    DIRECTIVE_SNIPPET,
    ATTR_NAME, 
    ATTR_IS, 
    ATTR_CONTENT, 
    VAL_LIST
};

use super::types::{ContentPointer, Directive, SnippetDirective, SnippetMode};
use std::collections::{HashMap, HashSet};

/// Parse an HTML comment inner text into a directive, e.g.
///   @snippet name="card" is="list" content="foo/bar.baz"
pub fn parse_directive(inner_comment: &str) -> Option<Directive> {
    let s = inner_comment.trim();
    if !s.starts_with(DIRECTIVE_SNIPPET) { return None; }
    let attrs = parse_attrs(&s[DIRECTIVE_SNIPPET.len()..]);

    let name = attrs.get(ATTR_NAME)?.trim().to_string();

    let mode = match attrs.get(ATTR_IS).map(|v| v.to_ascii_lowercase()) {
        Some(v) if v == VAL_LIST => SnippetMode::List,
        _ => SnippetMode::Single,
    };

    let content = attrs.get(ATTR_CONTENT).map(|raw| parse_content_pointer(raw));
    // Any non-reserved attribute key is treated as a presence-only flag
    let mut flags: HashSet<String> = HashSet::new();
    for k in attrs.keys() {
        if k != ATTR_NAME && k != ATTR_IS && k != ATTR_CONTENT {
            flags.insert(k.to_string());
        }
    }
    Some(Directive::Snippet(SnippetDirective { name, mode, content, flags }))
}

/// Parse attributes of the directive into a map (key="value" pairs).
fn parse_attrs(rem: &str) -> HashMap<String, String> {
    // small capacity hint for common case: name, is, content, maybe one more
    let mut out = HashMap::with_capacity(4);
    let mut i = 0usize;
    let b = rem.as_bytes();

    // skip leading spaces
    while i < b.len() && b[i].is_ascii_whitespace() { i += 1; }

    while i < b.len() {
        // read key
        let start_key = i;
        while i < b.len() && !b[i].is_ascii_whitespace() && b[i] != b'=' { i += 1; }
        if i == start_key { break; }
        let key = rem[start_key..i].trim().to_string();

        // spaces
        while i < b.len() && b[i].is_ascii_whitespace() { i += 1; }

        // if there's no '=', treat as presence-only flag
        if i >= b.len() || b[i] != b'=' {
            out.insert(key, String::new());
            while i < b.len() && b[i].is_ascii_whitespace() { i += 1; }
            continue;
        }
        i += 1;

        // spaces
        while i < b.len() && b[i].is_ascii_whitespace() { i += 1; }

        // quoted value "..."
        if i < b.len() && b[i] == b'"' {
            i += 1;
            let start_val = i;
            while i < b.len() && b[i] != b'"' { i += 1; }
            // only insert if a closing quote was found
            if i < b.len() && b[i] == b'"' {
                let val = rem[start_val..i].to_string();
                out.insert(key, val);
                i += 1; // consume closing quote
            } else {
                // unmatched quote → stop parsing further attributes
                break;
            }
        } else {
            // unquoted until space
            let start_val = i;
            while i < b.len() && !b[i].is_ascii_whitespace() { i += 1; }
            let val = rem[start_val..i].to_string();
            out.insert(key, val);
        }

        // next
        while i < b.len() && b[i].is_ascii_whitespace() { i += 1; }
    }

    out
}

/// Parse content pointer string like "summertime-dreaming/djInfo.Tiago".
/// Left of the LAST '/' is the content file key,
/// and the right (if present) is the dotted path inside that TOML.
fn parse_content_pointer(s: &str) -> ContentPointer {
    if let Some((file, path)) = s.rsplit_once('/') {
        ContentPointer {
            file: file.to_string(),
            path: if path.is_empty() { None } else { Some(path.to_string()) },
        }
    } else {
        ContentPointer { file: s.to_string(), path: None }
    }
}

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

    #[test]
    fn parse_with_content() {
        let inner = format!(r#"@snippet name="x/y" content="a/b.c" is="{}""#, VAL_LIST);
        if let Some(Directive::Snippet(sd)) = parse_directive(&inner) {
            assert_eq!(sd.name, "x/y");
            assert_eq!(sd.mode, SnippetMode::List);
            let cp = sd.content.unwrap();
            assert_eq!(cp.file, "a");
            assert_eq!(cp.path.as_deref(), Some("b.c"));
        } else { panic!("not parsed"); }
    }

    #[test]
    fn parse_with_flag_attributes() {
        let inner = r#"@snippet name="card" audioPlayer fancyMode"#;
        if let Some(Directive::Snippet(sd)) = parse_directive(&inner) {
            assert_eq!(sd.name, "card");
            assert_eq!(sd.mode, SnippetMode::Single);
            assert!(sd.flags.contains("audioPlayer"));
            assert!(sd.flags.contains("fancyMode"));
        } else {
            panic!("not parsed");
        }
    }
}
