//! Action type and structures for HeroScript.

use super::params::{ExportArgs, Params};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

/// Action state during execution.
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub enum ActionState {
    /// Action is initialized but not started.
    #[default]
    Init,
    /// Action is ready for next step.
    Next,
    /// Action needs to restart.
    Restart,
    /// Action encountered an error.
    Error,
    /// Action is complete.
    Done,
}

/// A single HeroScript action.
///
/// Actions are parsed from HeroScript text in the format:
/// ```heroscript
/// !!actor.action_name param1:value1 param2:value2
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Action {
    /// Unique ID within the playbook.
    pub id: usize,
    /// Content ID (optional, from cid parameter).
    pub cid: String,
    /// Action name (e.g., "define", "add", "configure").
    pub name: String,
    /// Actor name (e.g., "person", "payment", "server").
    pub actor: String,
    /// Priority (0 = highest, 10 = default, lower numbers execute first).
    pub priority: i32,
    /// Action parameters.
    pub params: Params,
    /// Result parameters (output from execution).
    pub result: Params,
    /// Current state of the action.
    pub state: ActionState,
    /// Comments before the action.
    pub comments: String,
    /// Whether the action has been processed.
    pub done: bool,
}

impl Action {
    /// Create a new action with the given actor and name.
    pub fn new(actor: &str, name: &str) -> Self {
        Self {
            actor: actor.to_lowercase(),
            name: name.to_lowercase(),
            priority: 10,
            ..Default::default()
        }
    }

    /// Serialize the action to HeroScript format.
    pub fn heroscript(&self) -> String {
        let prefix = format!("!!{}.{}", self.actor, self.name);

        let params_str = self.params.export(&ExportArgs {
            pre: prefix,
            ..Default::default()
        });

        if !self.comments.is_empty() {
            format!("// {}\n{}", self.comments, params_str)
        } else {
            params_str
        }
    }

    /// Serialize to compact one-line format.
    pub fn to_oneline(&self) -> String {
        let prefix = format!("!!{}.{}", self.actor, self.name);

        self.params.export(&ExportArgs {
            pre: prefix,
            oneline: true,
            indent: String::new(),
            ..Default::default()
        })
    }

    /// Get the full action name as "actor.name".
    pub fn full_name(&self) -> String {
        format!("{}.{}", self.actor, self.name)
    }

    /// Get normalized names split by '.'.
    ///
    /// Returns a vector like ["actor", "name"] or ["actor", "sub", "name"].
    pub fn names(&self) -> Vec<String> {
        let mut result = Vec::new();
        result.push(self.actor.clone());

        for part in self.name.split('.') {
            result.push(part.to_string());
        }

        result
    }

    /// Get a hash of the action content for comparison.
    pub fn hashkey(&self) -> String {
        let mut hasher = Sha256::new();
        hasher.update(self.actor.as_bytes());
        hasher.update(b".");
        hasher.update(self.name.as_bytes());
        hasher.update(b":");
        hasher.update(self.params.hexhash().as_bytes());

        let result = hasher.finalize();
        hex::encode(result)
    }

    /// Check if the action matches the given filter.
    ///
    /// Filter format: "actor.name" with glob support (*, ?).
    pub fn matches(&self, filter: &MatchFilter) -> bool {
        // Check actor
        if !filter.actor.is_empty() && !glob_match(&filter.actor, &self.actor) {
            return false;
        }

        // Check name
        if !filter.name.is_empty() && !glob_match(&filter.name, &self.name) {
            return false;
        }

        // Check cid if specified
        if !filter.cid.is_empty() && self.cid != filter.cid {
            return false;
        }

        true
    }

    /// Mark the action as done.
    pub fn mark_done(&mut self) {
        self.done = true;
        self.state = ActionState::Done;
    }
}

/// Filter for matching actions.
#[derive(Debug, Clone, Default)]
pub struct MatchFilter {
    /// Actor pattern (supports *, ?).
    pub actor: String,
    /// Action name pattern (supports *, ?).
    pub name: String,
    /// Content ID (exact match).
    pub cid: String,
}

impl MatchFilter {
    /// Create a filter from a string like "actor.name" or "actor.*".
    pub fn from_str(filter: &str) -> Self {
        let parts: Vec<&str> = filter.splitn(2, '.').collect();

        match parts.len() {
            0 => Self::default(),
            1 => Self {
                actor: parts[0].to_lowercase(),
                ..Default::default()
            },
            _ => Self {
                actor: parts[0].to_lowercase(),
                name: parts[1].to_lowercase(),
                ..Default::default()
            },
        }
    }

    /// Create a filter with a specific CID.
    pub fn with_cid(mut self, cid: &str) -> Self {
        self.cid = cid.to_string();
        self
    }
}

/// Simple glob pattern matching supporting * and ?.
fn glob_match(pattern: &str, text: &str) -> bool {
    if pattern == "*" {
        return true;
    }

    let pattern_chars: Vec<char> = pattern.chars().collect();
    let text_chars: Vec<char> = text.chars().collect();

    glob_match_impl(&pattern_chars, &text_chars, 0, 0)
}

fn glob_match_impl(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
    if pi == pattern.len() && ti == text.len() {
        return true;
    }

    if pi == pattern.len() {
        return false;
    }

    let pc = pattern[pi];

    if pc == '*' {
        // Try matching zero or more characters
        for i in ti..=text.len() {
            if glob_match_impl(pattern, text, pi + 1, i) {
                return true;
            }
        }
        return false;
    }

    if ti == text.len() {
        return false;
    }

    if pc == '?' || pc == text[ti] {
        return glob_match_impl(pattern, text, pi + 1, ti + 1);
    }

    false
}

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

    #[test]
    fn test_action_new() {
        let action = Action::new("person", "define");
        assert_eq!(action.actor, "person");
        assert_eq!(action.name, "define");
        assert_eq!(action.priority, 10);
    }

    #[test]
    fn test_action_full_name() {
        let action = Action::new("person", "define");
        assert_eq!(action.full_name(), "person.define");
    }

    #[test]
    fn test_action_names() {
        let action = Action::new("person", "define");
        assert_eq!(action.names(), vec!["person", "define"]);
    }

    #[test]
    fn test_match_filter() {
        let action = Action::new("payment", "add");

        // Exact match
        let filter = MatchFilter::from_str("payment.add");
        assert!(action.matches(&filter));

        // Wildcard actor
        let filter = MatchFilter::from_str("*.add");
        assert!(action.matches(&filter));

        // Wildcard name
        let filter = MatchFilter::from_str("payment.*");
        assert!(action.matches(&filter));

        // All wildcard
        let filter = MatchFilter::from_str("*.*");
        assert!(action.matches(&filter));

        // No match
        let filter = MatchFilter::from_str("person.add");
        assert!(!action.matches(&filter));
    }

    #[test]
    fn test_glob_match() {
        assert!(glob_match("*", "anything"));
        assert!(glob_match("pay*", "payment"));
        assert!(glob_match("*ment", "payment"));
        assert!(glob_match("pa?ment", "payment"));
        assert!(glob_match("pay*ent", "payment"));
        assert!(!glob_match("person", "payment"));
    }

    #[test]
    fn test_heroscript() {
        let mut action = Action::new("person", "define");
        action.params.set("name", "john");
        action.params.set("age", "30");

        let hs = action.heroscript();
        assert!(hs.contains("!!person.define"));
        assert!(hs.contains("name:john"));
        assert!(hs.contains("age:30"));
    }

    #[test]
    fn test_to_oneline() {
        let mut action = Action::new("person", "define");
        action.params.set("name", "john");

        let hs = action.to_oneline();
        assert!(hs.starts_with("!!person.define"));
        assert!(!hs.contains('\n'));
    }
}
