//! PlayBook - collection and management of HeroScript actions.

use super::action::{Action, MatchFilter};
use super::errors::HeroScriptError;
use super::include;
use super::params::Params;
use super::scanner;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::path::Path;

/// Arguments for creating a new PlayBook.
#[derive(Debug, Clone, Default)]
pub struct PlayBookNewArgs {
    /// Local path to scan for .hero files.
    pub path: String,
    /// Direct text to parse.
    pub text: String,
    /// Default priority for actions.
    pub prio: i32,
    /// Priority filters: priority -> filter_string.
    pub priorities: HashMap<i32, String>,
    /// Text replacements before parsing.
    pub replace: HashMap<String, String>,
    /// Whether to process !!play.include actions (default: true).
    pub process_includes: Option<bool>,
    /// Maximum depth for recursive includes (default: 10).
    pub max_include_depth: Option<usize>,
}

/// Arguments for finding actions.
#[derive(Debug, Clone, Default)]
pub struct FindArgs {
    /// Filter string: "actor.name" or "actor.*" or "*.*".
    /// Comma-separated for multiple filters.
    pub filter: String,
    /// Include done actions in results.
    pub include_done: bool,
}

/// A collection of HeroScript actions.
///
/// PlayBook manages a set of actions parsed from HeroScript text,
/// providing methods for finding, filtering, and processing actions.
///
/// # Example
///
/// ```
/// use herosal_heroscript::{PlayBook, PlayBookNewArgs, FindArgs};
///
/// let script = r#"
///     !!person.define name:'John' age:30
///     !!person.define name:'Jane' age:25
/// "#;
///
/// let playbook = PlayBook::new(PlayBookNewArgs {
///     text: script.to_string(),
///     ..Default::default()
/// }).unwrap();
///
/// let actions = playbook.find(&FindArgs {
///     filter: "person.define".to_string(),
///     ..Default::default()
/// }).unwrap();
///
/// assert_eq!(actions.len(), 2);
/// ```
#[derive(Debug, Clone, Default)]
pub struct PlayBook {
    /// All actions in the playbook.
    pub actions: Vec<Action>,
    /// Non-action text from source.
    pub othertext: String,
    /// Execution result.
    pub result: String,
    /// Counter for action IDs.
    pub nr_actions: usize,
    /// Source path (if loaded from file).
    pub path: String,
}

impl PlayBook {
    /// Create a new empty playbook.
    pub fn new_empty() -> Self {
        Self::default()
    }

    /// Create a new playbook from the given arguments.
    pub fn new(args: PlayBookNewArgs) -> Result<Self, HeroScriptError> {
        let mut playbook = Self::default();
        playbook.add(&args)?;
        Ok(playbook)
    }

    /// Add content to the playbook.
    pub fn add(&mut self, args: &PlayBookNewArgs) -> Result<(), HeroScriptError> {
        let mut text = args.text.clone();
        let mut base_path = args.path.clone();

        // Load from path if specified
        if !args.path.is_empty() {
            let path = Path::new(&args.path);

            if path.is_file() {
                text = fs::read_to_string(path)?;
                self.path = args.path.clone();
                base_path = args.path.clone();
            } else if path.is_dir() {
                // Scan directory for .hero and .heroscript files
                let mut entries: Vec<_> = fs::read_dir(path)?
                    .filter_map(|e| e.ok())
                    .filter(|e| {
                        e.path()
                            .extension()
                            .map_or(false, |ext| ext == "hero" || ext == "heroscript")
                    })
                    .collect();

                // Sort for deterministic ordering
                entries.sort_by_key(|e| e.path());

                for entry in entries {
                    let file_text = fs::read_to_string(entry.path())?;
                    text.push('\n');
                    text.push_str(&file_text);
                }
                self.path = args.path.clone();
                base_path = args.path.clone();
            }
        }

        // Apply replacements (replaces both {KEY} and KEY with value)
        for (from, to) in &args.replace {
            // Replace {KEY} first (template syntax)
            let bracketed_key = format!("{{{}}}", from);
            text = text.replace(&bracketed_key, to);
            // Also replace plain KEY
            text = text.replace(from, to);
        }

        // Process includes if enabled (default: true)
        let process_includes = args.process_includes.unwrap_or(true);
        if process_includes {
            let max_depth = args.max_include_depth.unwrap_or(10);
            text = include::expand_includes(&text, &base_path, max_depth)?;
        }

        // Parse the text
        let mut new_actions = scanner::scan(&text)?;

        // Apply default priority and priorities from args
        for action in &mut new_actions {
            action.priority = args.prio;

            // Check if action matches any priority filter
            for (prio, filter) in &args.priorities {
                if self.matches_filter(action, filter) {
                    action.priority = *prio;
                    break;
                }
            }

            // Update action ID
            action.id = self.nr_actions;
            self.nr_actions += 1;
        }

        self.actions.extend(new_actions);
        Ok(())
    }

    /// Add text directly to the playbook.
    pub fn add_text(&mut self, text: &str) -> Result<(), HeroScriptError> {
        self.add(&PlayBookNewArgs {
            text: text.to_string(),
            ..Default::default()
        })
    }

    /// Find actions matching the filter.
    ///
    /// Filter format: "actor.name" with glob support (*, ?).
    /// Multiple filters can be comma-separated.
    pub fn find(&self, args: &FindArgs) -> Result<Vec<&Action>, HeroScriptError> {
        let filters: Vec<MatchFilter> = args
            .filter
            .split(',')
            .map(|f| MatchFilter::from_str(f.trim()))
            .collect();

        let results: Vec<&Action> = self
            .actions
            .iter()
            .filter(|action| {
                // Skip done actions unless included
                if action.done && !args.include_done {
                    return false;
                }

                // Match against any filter
                filters.iter().any(|filter| action.matches(filter))
            })
            .collect();

        Ok(results)
    }

    /// Find mutable references to actions matching the filter.
    pub fn find_mut(&mut self, args: &FindArgs) -> Result<Vec<&mut Action>, HeroScriptError> {
        let filters: Vec<MatchFilter> = args
            .filter
            .split(',')
            .map(|f| MatchFilter::from_str(f.trim()))
            .collect();

        let include_done = args.include_done;

        let results: Vec<&mut Action> = self
            .actions
            .iter_mut()
            .filter(|action| {
                if action.done && !include_done {
                    return false;
                }
                filters.iter().any(|filter| action.matches(filter))
            })
            .collect();

        Ok(results)
    }

    /// Get a single action matching the filter.
    ///
    /// Returns an error if no action or multiple actions match.
    pub fn get(&self, filter: &str) -> Result<&Action, HeroScriptError> {
        let actions = self.find(&FindArgs {
            filter: filter.to_string(),
            ..Default::default()
        })?;

        match actions.len() {
            0 => Err(HeroScriptError::ActionNotFound(filter.to_string())),
            1 => Ok(actions[0]),
            _ => Err(HeroScriptError::MultipleActions(filter.to_string())),
        }
    }

    /// Get a mutable reference to a single action.
    pub fn get_mut(&mut self, filter: &str) -> Result<&mut Action, HeroScriptError> {
        let filter_obj = MatchFilter::from_str(filter);

        let matching: Vec<usize> = self
            .actions
            .iter()
            .enumerate()
            .filter(|(_, a)| !a.done && a.matches(&filter_obj))
            .map(|(i, _)| i)
            .collect();

        match matching.len() {
            0 => Err(HeroScriptError::ActionNotFound(filter.to_string())),
            1 => Ok(&mut self.actions[matching[0]]),
            _ => Err(HeroScriptError::MultipleActions(filter.to_string())),
        }
    }

    /// Ensure exactly one action exists for the filter.
    pub fn ensure_once(&self, filter: &str) -> Result<&Action, HeroScriptError> {
        self.get(filter)
    }

    /// Check if at most one action exists for the filter.
    pub fn max_once(&self, filter: &str) -> Result<bool, HeroScriptError> {
        let actions = self.find(&FindArgs {
            filter: filter.to_string(),
            ..Default::default()
        })?;

        Ok(actions.len() <= 1)
    }

    /// Check if any action exists for the filter.
    pub fn exists(&self, filter: &str) -> bool {
        self.find(&FindArgs {
            filter: filter.to_string(),
            ..Default::default()
        })
        .map(|a| !a.is_empty())
        .unwrap_or(false)
    }

    /// Check if exactly one action exists for the filter.
    pub fn exists_once(&self, filter: &str) -> bool {
        self.find(&FindArgs {
            filter: filter.to_string(),
            ..Default::default()
        })
        .map(|a| a.len() == 1)
        .unwrap_or(false)
    }

    /// Get all actions sorted by priority.
    pub fn actions_sorted(&self) -> Vec<&Action> {
        let mut sorted: Vec<&Action> = self.actions.iter().collect();
        sorted.sort_by_key(|a| (a.priority, a.id));
        sorted
    }

    /// Get all undone actions sorted by priority.
    pub fn actions_pending(&self) -> Vec<&Action> {
        let mut sorted: Vec<&Action> = self.actions.iter().filter(|a| !a.done).collect();
        sorted.sort_by_key(|a| (a.priority, a.id));
        sorted
    }

    /// Ensure all matching actions are processed.
    pub fn ensure_processed(&self, filter: &str) -> Result<(), HeroScriptError> {
        let unprocessed: Vec<&Action> = self.find(&FindArgs {
            filter: filter.to_string(),
            include_done: false,
        })?;

        if !unprocessed.is_empty() {
            let names: Vec<String> = unprocessed.iter().map(|a| a.full_name()).collect();
            return Err(HeroScriptError::UnprocessedActions(names.join(", ")));
        }

        Ok(())
    }

    /// Check that no actions remain unprocessed.
    pub fn empty_check(&self) -> Result<(), HeroScriptError> {
        let unprocessed: Vec<&Action> = self.actions.iter().filter(|a| !a.done).collect();

        if !unprocessed.is_empty() {
            let names: Vec<String> = unprocessed.iter().map(|a| a.full_name()).collect();
            return Err(HeroScriptError::UnprocessedActions(names.join(", ")));
        }

        Ok(())
    }

    /// Serialize the playbook to HeroScript.
    pub fn heroscript(&self, show_done: bool) -> String {
        let mut result = String::new();

        for action in &self.actions {
            if !show_done && action.done {
                continue;
            }
            result.push_str(&action.heroscript());
            result.push_str("\n\n");
        }

        result.trim_end().to_string()
    }

    /// Get a hash of the playbook content.
    pub fn hashkey(&self) -> String {
        let mut hasher = Sha256::new();

        for action in &self.actions {
            hasher.update(action.hashkey().as_bytes());
            hasher.update(b";");
        }

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

    /// Get all unique action names in the playbook.
    pub fn names(&self) -> Vec<String> {
        let mut names: Vec<String> = self.actions.iter().map(|a| a.full_name()).collect();
        names.sort();
        names.dedup();
        names
    }

    /// Get merged params from all matching actions.
    pub fn params_get(&self, filter: &str) -> Result<Params, HeroScriptError> {
        let actions = self.find(&FindArgs {
            filter: filter.to_string(),
            ..Default::default()
        })?;

        let mut merged = Params::new();
        for action in actions {
            merged.merge(&action.params);
        }

        Ok(merged)
    }

    /// Get the number of actions.
    pub fn len(&self) -> usize {
        self.actions.len()
    }

    /// Check if the playbook is empty.
    pub fn is_empty(&self) -> bool {
        self.actions.is_empty()
    }

    /// Mark all matching actions as done.
    pub fn mark_done(&mut self, filter: &str) -> Result<usize, HeroScriptError> {
        let filter_obj = MatchFilter::from_str(filter);
        let mut count = 0;

        for action in &mut self.actions {
            if !action.done && action.matches(&filter_obj) {
                action.mark_done();
                count += 1;
            }
        }

        Ok(count)
    }

    /// Helper to check if an action matches a filter string.
    fn matches_filter(&self, action: &Action, filter: &str) -> bool {
        let filter_obj = MatchFilter::from_str(filter);
        action.matches(&filter_obj)
    }
}

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

    const TEST_SCRIPT: &str = r#"
// Comment for first action
!!payment.add person:fatayera
    name: 'TF Wallet'
    blockchain: stellar
    preferred: false

!!payment.add person:despiegk
    name: 'TF Wallet2'

!!payment.configure
    provider: 'stripe'
"#;

    #[test]
    fn test_playbook_new() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        assert_eq!(pb.actions.len(), 3);
    }

    #[test]
    fn test_find_all() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let actions = pb
            .find(&FindArgs {
                filter: "payment.*".to_string(),
                ..Default::default()
            })
            .unwrap();

        assert_eq!(actions.len(), 3);
    }

    #[test]
    fn test_find_specific() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let actions = pb
            .find(&FindArgs {
                filter: "payment.add".to_string(),
                ..Default::default()
            })
            .unwrap();

        assert_eq!(actions.len(), 2);
    }

    #[test]
    fn test_get_single() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let action = pb.get("payment.configure").unwrap();
        assert_eq!(action.params.get("provider").unwrap(), "stripe");
    }

    #[test]
    fn test_get_multiple_error() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let result = pb.get("payment.add");
        assert!(matches!(result, Err(HeroScriptError::MultipleActions(_))));
    }

    #[test]
    fn test_exists() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        assert!(pb.exists("payment.configure"));
        assert!(!pb.exists("payment.nonexistent"));
    }

    #[test]
    fn test_exists_once() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        assert!(pb.exists_once("payment.configure"));
        assert!(!pb.exists_once("payment.add")); // multiple exist
    }

    #[test]
    fn test_actions_sorted() {
        let mut pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        // Set different priorities
        pb.actions[0].priority = 5;
        pb.actions[1].priority = 1;
        pb.actions[2].priority = 3;

        let sorted = pb.actions_sorted();
        assert_eq!(sorted[0].priority, 1);
        assert_eq!(sorted[1].priority, 3);
        assert_eq!(sorted[2].priority, 5);
    }

    #[test]
    fn test_mark_done() {
        let mut pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let count = pb.mark_done("payment.configure").unwrap();
        assert_eq!(count, 1);

        // Should not be found anymore (done = true)
        assert!(!pb.exists("payment.configure"));
    }

    #[test]
    fn test_empty_check() {
        let mut pb = PlayBook::new(PlayBookNewArgs {
            text: "!!test.action".to_string(),
            ..Default::default()
        })
        .unwrap();

        // Should fail - action not done
        assert!(pb.empty_check().is_err());

        // Mark as done
        pb.actions[0].done = true;

        // Should pass now
        assert!(pb.empty_check().is_ok());
    }

    #[test]
    fn test_heroscript_output() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: "!!person.define name:john".to_string(),
            ..Default::default()
        })
        .unwrap();

        let output = pb.heroscript(true);
        assert!(output.contains("!!person.define"));
        assert!(output.contains("name:john"));
    }

    #[test]
    fn test_names() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let names = pb.names();
        assert!(names.contains(&"payment.add".to_string()));
        assert!(names.contains(&"payment.configure".to_string()));
    }

    #[test]
    fn test_params_get_merged() {
        let pb = PlayBook::new(PlayBookNewArgs {
            text: TEST_SCRIPT.to_string(),
            ..Default::default()
        })
        .unwrap();

        let params = pb.params_get("payment.add").unwrap();
        // Should have params from both payment.add actions
        assert!(params.exists("name"));
        assert!(params.exists("person"));
    }
}
