//! HeroScript scanner/parser for parsing actions from text.

use super::action::Action;
use super::errors::HeroScriptError;
use super::params;

/// Parse HeroScript text into a list of actions.
///
/// # Example
///
/// ```
/// use herosal_heroscript::scanner::scan;
///
/// let script = r#"
///     !!person.define name:'John' age:30
///
///     !!person.define
///         name: 'Jane'
///         age: 25
/// "#;
///
/// let actions = scan(script).unwrap();
/// assert_eq!(actions.len(), 2);
/// ```
pub fn scan(text: &str) -> Result<Vec<Action>, HeroScriptError> {
    let mut actions = Vec::new();
    let mut other_text = String::new();
    let mut pending_comment = String::new();

    let lines: Vec<&str> = text.lines().collect();
    let mut i = 0;
    let mut action_id = 0;

    while i < lines.len() {
        let line = lines[i];
        let trimmed = line.trim();

        // Skip empty lines
        if trimmed.is_empty() {
            i += 1;
            continue;
        }

        // Check for comment
        if trimmed.starts_with("//") {
            let comment = trimmed[2..].trim();
            if !pending_comment.is_empty() {
                pending_comment.push('\n');
            }
            pending_comment.push_str(comment);
            i += 1;
            continue;
        }

        // Check for action start
        if trimmed.starts_with("!!") {
            let (mut action, params_on_line) = parse_action_line(trimmed)?;
            action.id = action_id;
            action_id += 1;

            // Attach pending comment
            if !pending_comment.is_empty() {
                action.comments = pending_comment.clone();
                pending_comment.clear();
            }

            // Parse initial params on the action line
            if !params_on_line.is_empty() {
                action.params = params::parse(&params_on_line)?;
            }

            // Look for indented continuation lines
            i += 1;
            while i < lines.len() {
                let next_line = lines[i];

                // Empty line might end the action or continue
                if next_line.trim().is_empty() {
                    i += 1;
                    continue;
                }

                // Check if this line is indented (continuation)
                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
                let next_trimmed = next_line.trim();

                // If not indented, or starts with !!, this is a new action/content
                if !is_indented || next_trimmed.starts_with("!!") {
                    break;
                }

                // Parse and merge the parameters
                if !next_trimmed.is_empty() && !next_trimmed.starts_with("//") {
                    action.params.add(next_trimmed)?;
                } else if next_trimmed.starts_with("//") {
                    // Inline comment within action params
                    action
                        .params
                        .comments
                        .push(next_trimmed[2..].trim().to_string());
                }

                i += 1;
            }

            // Extract cid if present
            if let Ok(cid) = action.params.get("cid") {
                action.cid = cid.to_string();
            }

            actions.push(action);
        } else {
            // Non-action text
            other_text.push_str(line);
            other_text.push('\n');
            pending_comment.clear();
            i += 1;
        }
    }

    Ok(actions)
}

/// Parse the first line of an action.
///
/// Returns the Action with actor and name set, plus any params text on the same line.
fn parse_action_line(line: &str) -> Result<(Action, String), HeroScriptError> {
    // Remove the !! prefix
    let line = line.trim_start_matches('!').trim();

    // Split into action name and params
    let mut parts = line.splitn(2, |c: char| c.is_whitespace());

    let action_part = parts
        .next()
        .ok_or_else(|| HeroScriptError::InvalidAction("Empty action line".to_string()))?;

    let params_part = parts.next().unwrap_or("").to_string();

    // Parse actor.name
    let name_parts: Vec<&str> = action_part.splitn(2, '.').collect();

    let (actor, name) = match name_parts.len() {
        0 => {
            return Err(HeroScriptError::InvalidAction(
                "Missing actor and name".to_string(),
            ))
        }
        1 => (name_parts[0].to_lowercase(), String::new()),
        _ => (name_parts[0].to_lowercase(), name_parts[1].to_lowercase()),
    };

    if actor.is_empty() {
        return Err(HeroScriptError::InvalidAction(
            "Actor name cannot be empty".to_string(),
        ));
    }

    let action = Action {
        actor,
        name,
        ..Default::default()
    };

    Ok((action, params_part))
}

/// Scan text and return actions along with any non-action text.
pub fn scan_with_other(text: &str) -> Result<(Vec<Action>, String), HeroScriptError> {
    let actions = scan(text)?;

    // Collect non-action text
    let mut other = String::new();
    for line in text.lines() {
        let trimmed = line.trim();
        if !trimmed.starts_with("!!") && !trimmed.starts_with("//") && !trimmed.is_empty() {
            // Check if this looks like action params (indented after !!)
            if !line.starts_with(' ') && !line.starts_with('\t') {
                other.push_str(line);
                other.push('\n');
            }
        }
    }

    Ok((actions, other))
}

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

    #[test]
    fn test_scan_single_action() {
        let script = "!!person.define name:john age:30";
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 1);
        assert_eq!(actions[0].actor, "person");
        assert_eq!(actions[0].name, "define");
        assert_eq!(actions[0].params.get("name").unwrap(), "john");
        assert_eq!(actions[0].params.get_int("age").unwrap(), 30);
    }

    #[test]
    fn test_scan_multiline_action() {
        let script = r#"
!!person.define
    name: 'John Doe'
    age: 30
    active: true
"#;
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 1);
        assert_eq!(actions[0].params.get("name").unwrap(), "John Doe");
        assert_eq!(actions[0].params.get_int("age").unwrap(), 30);
        assert!(actions[0].params.get_default_true("active"));
    }

    #[test]
    fn test_scan_multiple_actions() {
        let script = r#"
!!person.define name:john
!!person.define name:jane
"#;
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 2);
        assert_eq!(actions[0].params.get("name").unwrap(), "john");
        assert_eq!(actions[1].params.get("name").unwrap(), "jane");
    }

    #[test]
    fn test_scan_with_comments() {
        let script = r#"
// This is a comment for the action
!!person.define name:john
"#;
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 1);
        assert_eq!(actions[0].comments, "This is a comment for the action");
    }

    #[test]
    fn test_scan_action_ids() {
        let script = r#"
!!person.define name:john
!!payment.add amount:100
!!notification.send to:john
"#;
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 3);
        assert_eq!(actions[0].id, 0);
        assert_eq!(actions[1].id, 1);
        assert_eq!(actions[2].id, 2);
    }

    #[test]
    fn test_scan_compact_and_expanded() {
        let script = r#"
!!person.define name:john age:30

!!payment.configure
    provider: 'stripe'
    currency: 'USD'
"#;
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 2);
        assert_eq!(actions[0].params.get("name").unwrap(), "john");
        assert_eq!(actions[1].params.get("provider").unwrap(), "stripe");
    }

    #[test]
    fn test_scan_with_cid() {
        let script = "!!person.define cid:person1 name:john";
        let actions = scan(script).unwrap();

        assert_eq!(actions.len(), 1);
        assert_eq!(actions[0].cid, "person1");
    }

    #[test]
    fn test_scan_empty() {
        let actions = scan("").unwrap();
        assert!(actions.is_empty());
    }

    #[test]
    fn test_scan_only_comments() {
        let script = r#"
// comment 1
// comment 2
"#;
        let actions = scan(script).unwrap();
        assert!(actions.is_empty());
    }

    #[test]
    fn test_parse_action_line() {
        let (action, params) = parse_action_line("!!person.define name:john").unwrap();
        assert_eq!(action.actor, "person");
        assert_eq!(action.name, "define");
        assert_eq!(params, "name:john");
    }

    #[test]
    fn test_parse_action_line_no_params() {
        let (action, params) = parse_action_line("!!person.define").unwrap();
        assert_eq!(action.actor, "person");
        assert_eq!(action.name, "define");
        assert!(params.is_empty());
    }
}
