//! Include processing for HeroScript.
//!
//! Handles `!!play.include` actions that allow including other HeroScript files.
//!
//! # Example
//!
//! ```heroscript
//! !!play.include path:'../../heroscriptall' replace:'SITENAME:geomind_memo'
//! !!play.include git_url:'https://github.com/user/repo' git_branch:'main'
//! ```

use super::errors::HeroScriptError;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Arguments for an include operation.
#[derive(Debug, Clone, Default)]
pub struct IncludeArgs {
    /// Local path (relative or absolute) to the file/directory to include.
    pub path: String,
    /// Git repository URL to clone.
    pub git_url: String,
    /// Whether to pull latest changes from git.
    pub git_pull: bool,
    /// Whether to reset local changes before pull.
    pub git_reset: bool,
    /// Git branch to checkout (optional, can be in URL).
    pub git_branch: String,
    /// Text replacements to apply: "KEY:VALUE,KEY2:VALUE2".
    pub replace: HashMap<String, String>,
    /// Base path for resolving relative paths.
    pub base_path: String,
}

impl IncludeArgs {
    /// Parse include arguments from action parameters.
    pub fn from_params(params: &super::params::Params, base_path: &str) -> Self {
        let mut args = Self {
            base_path: base_path.to_string(),
            ..Default::default()
        };

        // Parse path
        if let Ok(path) = params.get("path") {
            args.path = path.to_string();
        }

        // Parse git parameters
        if let Ok(url) = params.get("git_url") {
            args.git_url = url.to_string();
        }
        args.git_pull = params.get_default_true("git_pull");
        args.git_reset = params.get_default_false("git_reset");
        if let Ok(branch) = params.get("git_branch") {
            args.git_branch = branch.to_string();
        }

        // Parse replacements
        if let Ok(replace_str) = params.get("replace") {
            args.replace = parse_replacements(&replace_str);
        }

        args
    }

    /// Resolve the path to include (handles relative paths).
    pub fn resolve_path(&self) -> Result<PathBuf, HeroScriptError> {
        if !self.path.is_empty() {
            let path = Path::new(&self.path);

            // If absolute, use as-is
            if path.is_absolute() {
                return Ok(path.to_path_buf());
            }

            // If relative, resolve from base_path
            if !self.base_path.is_empty() {
                let base = Path::new(&self.base_path);
                // If base_path is a file, use its parent directory
                let base_dir = if base.is_file() {
                    base.parent().unwrap_or(base)
                } else {
                    base
                };
                return Ok(base_dir.join(path));
            }

            // Use current directory as fallback
            Ok(std::env::current_dir()?.join(path))
        } else if !self.git_url.is_empty() {
            // For git URLs, we need to clone to a temp/cache location
            self.resolve_git_path()
        } else {
            Err(HeroScriptError::InvalidAction(
                "play.include requires either 'path' or 'git_url' parameter".to_string(),
            ))
        }
    }

    /// Resolve path for git-based includes.
    fn resolve_git_path(&self) -> Result<PathBuf, HeroScriptError> {
        // Create a deterministic cache path based on URL
        let cache_dir = get_git_cache_dir()?;
        let repo_name = extract_repo_name(&self.git_url);
        let repo_path = cache_dir.join(&repo_name);

        Ok(repo_path)
    }

    /// Perform git operations if needed.
    #[cfg(feature = "git")]
    pub fn ensure_git_repo(&self) -> Result<PathBuf, HeroScriptError> {
        use herosal_system::git::GitTree;

        let cache_dir = get_git_cache_dir()?;

        // Use GitTree to clone/get the repo - it handles existing repos
        let git_tree = GitTree::new(cache_dir.to_string_lossy().as_ref())
            .map_err(|e| HeroScriptError::InvalidAction(format!("Git error: {}", e)))?;

        let repos = git_tree
            .get(&self.git_url)
            .map_err(|e| HeroScriptError::InvalidAction(format!("Git error: {}", e)))?;

        if let Some(repo) = repos.into_iter().next() {
            // Optionally pull/reset
            if self.git_reset {
                repo.reset().map_err(|e| {
                    HeroScriptError::InvalidAction(format!("Git reset error: {}", e))
                })?;
            }
            if self.git_pull {
                repo.pull().map_err(|e| {
                    HeroScriptError::InvalidAction(format!("Git pull error: {}", e))
                })?;
            }

            // Return the actual repo path from the git operation
            Ok(PathBuf::from(repo.path()))
        } else {
            Err(HeroScriptError::InvalidAction(format!(
                "Failed to clone or find repository: {}",
                self.git_url
            )))
        }
    }

    /// Perform git operations if needed (stub when git feature disabled).
    #[cfg(not(feature = "git"))]
    pub fn ensure_git_repo(&self) -> Result<PathBuf, HeroScriptError> {
        Err(HeroScriptError::InvalidAction(
            "Git support not enabled. Compile with 'git' feature.".to_string(),
        ))
    }
}

/// Parse replacement string "KEY:VALUE,KEY2:VALUE2" into HashMap.
fn parse_replacements(replace_str: &str) -> HashMap<String, String> {
    let mut replacements = HashMap::new();

    for pair in replace_str.split(',') {
        let pair = pair.trim();
        if let Some(pos) = pair.find(':') {
            let key = pair[..pos].trim().to_string();
            let value = pair[pos + 1..].trim().to_string();
            if !key.is_empty() {
                replacements.insert(key, value);
            }
        }
    }

    replacements
}

/// Get the git cache directory.
fn get_git_cache_dir() -> Result<PathBuf, HeroScriptError> {
    let cache_dir = if let Ok(hero_path) = std::env::var("HERO_PATH") {
        PathBuf::from(hero_path).join("git_cache")
    } else if let Some(home) = dirs::home_dir() {
        home.join(".hero").join("git_cache")
    } else {
        PathBuf::from("/tmp/hero_git_cache")
    };

    // Ensure directory exists
    if !cache_dir.exists() {
        fs::create_dir_all(&cache_dir)?;
    }

    Ok(cache_dir)
}

/// Extract repository name from URL.
fn extract_repo_name(url: &str) -> String {
    // Handle various URL formats:
    // https://github.com/user/repo.git
    // git@github.com:user/repo.git
    // https://github.com/user/repo

    let url = url.trim_end_matches(".git");

    // Extract the last path component
    if let Some(pos) = url.rfind('/') {
        url[pos + 1..].to_string()
    } else if let Some(pos) = url.rfind(':') {
        // git@github.com:user/repo format
        let after_colon = &url[pos + 1..];
        if let Some(slash_pos) = after_colon.rfind('/') {
            after_colon[slash_pos + 1..].to_string()
        } else {
            after_colon.to_string()
        }
    } else {
        url.to_string()
    }
}

/// Load and process an include, returning the expanded HeroScript text.
pub fn process_include(args: &IncludeArgs) -> Result<String, HeroScriptError> {
    // Resolve the path (either local or git)
    let path = if !args.git_url.is_empty() {
        args.ensure_git_repo()?
    } else {
        args.resolve_path()?
    };

    // Load the content
    let content = load_content(&path)?;

    // Apply replacements
    let content = apply_replacements(&content, &args.replace);

    Ok(content)
}

/// Load content from a file or directory.
fn load_content(path: &Path) -> Result<String, HeroScriptError> {
    if !path.exists() {
        return Err(HeroScriptError::InvalidAction(format!(
            "Include path does not exist: {}",
            path.display()
        )));
    }

    if path.is_file() {
        Ok(fs::read_to_string(path)?)
    } else if path.is_dir() {
        // Load all .hero files in the directory
        let mut content = String::new();

        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_content = fs::read_to_string(entry.path())?;
            if !content.is_empty() {
                content.push('\n');
            }
            content.push_str(&file_content);
        }

        Ok(content)
    } else {
        Err(HeroScriptError::InvalidAction(format!(
            "Include path is not a file or directory: {}",
            path.display()
        )))
    }
}

/// Apply text replacements to content.
/// Replaces both `{KEY}` and `KEY` with the value.
fn apply_replacements(content: &str, replacements: &HashMap<String, String>) -> String {
    let mut result = content.to_string();

    for (key, value) in replacements {
        // Replace {KEY} first (template syntax)
        let bracketed_key = format!("{{{}}}", key);
        result = result.replace(&bracketed_key, value);
        // Also replace plain KEY
        result = result.replace(key, value);
    }

    result
}

/// Process all `play.include` actions in HeroScript text, expanding them recursively.
///
/// This function scans for `!!play.include` actions, processes them, and replaces
/// them with the included content. It handles recursive includes up to a depth limit.
pub fn expand_includes(
    text: &str,
    base_path: &str,
    max_depth: usize,
) -> Result<String, HeroScriptError> {
    expand_includes_recursive(text, base_path, max_depth, 0)
}

fn expand_includes_recursive(
    text: &str,
    base_path: &str,
    max_depth: usize,
    current_depth: usize,
) -> Result<String, HeroScriptError> {
    if current_depth > max_depth {
        return Err(HeroScriptError::InvalidAction(format!(
            "Maximum include depth ({}) exceeded. Possible circular include.",
            max_depth
        )));
    }

    let mut result = String::new();
    let mut pending_comment = String::new();
    let lines: Vec<&str> = text.lines().collect();
    let mut i = 0;

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

        // Check for comment
        if trimmed.starts_with("//") {
            pending_comment.push_str(line);
            pending_comment.push('\n');
            i += 1;
            continue;
        }

        // Check for play.include action
        if trimmed.starts_with("!!play.include") || trimmed.starts_with("!!include.") {
            // Parse the include action
            let (params_text, lines_consumed) = extract_action_params(&lines, i);

            // Parse parameters
            let params = super::params::parse(&params_text)?;
            let include_args = IncludeArgs::from_params(&params, base_path);

            // Process the include
            let included_content = process_include(&include_args)?;

            // Recursively expand includes in the included content
            let new_base_path = include_args
                .resolve_path()
                .map(|p| p.to_string_lossy().to_string())
                .unwrap_or_else(|_| base_path.to_string());

            let expanded = expand_includes_recursive(
                &included_content,
                &new_base_path,
                max_depth,
                current_depth + 1,
            )?;

            // Add the expanded content
            if !pending_comment.is_empty() {
                result.push_str(&pending_comment);
            }
            result.push_str(&expanded);
            result.push('\n');

            pending_comment.clear();
            i += lines_consumed;
        } else {
            // Regular line - keep as-is
            if !pending_comment.is_empty() {
                result.push_str(&pending_comment);
                pending_comment.clear();
            }
            result.push_str(line);
            result.push('\n');
            i += 1;
        }
    }

    // Don't forget trailing comments
    if !pending_comment.is_empty() {
        result.push_str(&pending_comment);
    }

    Ok(result)
}

/// Extract action parameters from lines, handling multiline actions.
/// Returns (params_text, lines_consumed).
fn extract_action_params(lines: &[&str], start: usize) -> (String, usize) {
    let mut params = String::new();
    let first_line = lines[start].trim();

    // Extract params from the first line (after !!play.include or similar)
    if let Some(pos) = first_line.find(|c: char| c.is_whitespace()) {
        params.push_str(&first_line[pos..].trim());
    }

    let mut i = start + 1;

    // Look for indented continuation lines
    while i < lines.len() {
        let line = lines[i];
        let trimmed = line.trim();

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

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

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

        // Add to params (skip comments)
        if !trimmed.starts_with("//") {
            if !params.is_empty() {
                params.push(' ');
            }
            params.push_str(trimmed);
        }

        i += 1;
    }

    (params, i - start)
}

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

    #[test]
    fn test_parse_replacements() {
        let replacements = parse_replacements("SITENAME:geomind_memo,VERSION:1.0");
        assert_eq!(replacements.get("SITENAME").unwrap(), "geomind_memo");
        assert_eq!(replacements.get("VERSION").unwrap(), "1.0");
    }

    #[test]
    fn test_parse_replacements_single() {
        let replacements = parse_replacements("SITENAME:geomind_memo");
        assert_eq!(replacements.get("SITENAME").unwrap(), "geomind_memo");
        assert_eq!(replacements.len(), 1);
    }

    #[test]
    fn test_parse_replacements_empty() {
        let replacements = parse_replacements("");
        assert!(replacements.is_empty());
    }

    #[test]
    fn test_extract_repo_name() {
        assert_eq!(
            extract_repo_name("https://github.com/user/repo.git"),
            "repo"
        );
        assert_eq!(extract_repo_name("https://github.com/user/repo"), "repo");
        assert_eq!(extract_repo_name("git@github.com:user/repo.git"), "repo");
    }

    #[test]
    fn test_apply_replacements() {
        let mut replacements = HashMap::new();
        replacements.insert("SITENAME".to_string(), "mysite".to_string());
        replacements.insert("VERSION".to_string(), "2.0".to_string());

        let content = "Site: SITENAME, Version: VERSION";
        let result = apply_replacements(content, &replacements);
        assert_eq!(result, "Site: mysite, Version: 2.0");
    }

    #[test]
    fn test_include_args_from_params() {
        let mut params = Params::new();
        params.set("path", "../../heroscriptall");
        params.set("replace", "SITENAME:geomind_memo");

        let args = IncludeArgs::from_params(&params, "/base/path/file.hero");

        assert_eq!(args.path, "../../heroscriptall");
        assert_eq!(args.replace.get("SITENAME").unwrap(), "geomind_memo");
        assert_eq!(args.base_path, "/base/path/file.hero");
    }

    #[test]
    fn test_resolve_path_relative() {
        let args = IncludeArgs {
            path: "../other/file.hero".to_string(),
            base_path: "/home/user/scripts/main.hero".to_string(),
            ..Default::default()
        };

        let resolved = args.resolve_path().unwrap();
        // Should resolve relative to the parent of the base file
        assert!(resolved.to_string_lossy().contains("other"));
    }

    #[test]
    fn test_resolve_path_absolute() {
        let args = IncludeArgs {
            path: "/absolute/path/file.hero".to_string(),
            base_path: "/some/other/path".to_string(),
            ..Default::default()
        };

        let resolved = args.resolve_path().unwrap();
        assert_eq!(resolved.to_string_lossy(), "/absolute/path/file.hero");
    }

    #[test]
    fn test_extract_action_params_single_line() {
        let lines = vec!["!!play.include path:'../test.hero' replace:'A:B'"];
        let (params, consumed) = extract_action_params(&lines, 0);
        assert_eq!(params, "path:'../test.hero' replace:'A:B'");
        assert_eq!(consumed, 1);
    }

    #[test]
    fn test_extract_action_params_multiline() {
        let lines = vec![
            "!!play.include",
            "    path: '../test.hero'",
            "    replace: 'A:B'",
            "!!other.action",
        ];
        let (params, consumed) = extract_action_params(&lines, 0);
        assert!(params.contains("path:"));
        assert!(params.contains("replace:"));
        assert_eq!(consumed, 3);
    }
}
