use regex::Regex;
use std::{collections::BTreeMap, sync::OnceLock};

use crate::cli::schemas::v1::CommandSchemaInput;

pub const SECRET_MASK: &str = "***";

const MAX_ITERATIONS: usize = 10;

static INPUTS_RE: OnceLock<Regex> = OnceLock::new();
static ENV_RE: OnceLock<Regex> = OnceLock::new();

fn get_inputs_re() -> &'static Regex {
    INPUTS_RE.get_or_init(|| Regex::new(r"@\{inputs\.([a-zA-Z_-][a-zA-Z0-9_-]*)\}").unwrap())
}

fn get_env_re() -> &'static Regex {
    ENV_RE.get_or_init(|| Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)\}").unwrap())
}

/// Resolve a single input variable reference to its value.
fn resolve_input_value(
    name: &str,
    input: &CommandSchemaInput,
    matches: &getopts::Matches,
) -> String {
    match input.r#type.as_str() {
        "boolean" | "bool" => {
            if matches.opt_present(name) {
                #[cfg(unix)]
                {
                    "true".to_string()
                }
                #[cfg(windows)]
                {
                    "$true".to_string()
                }
            } else {
                #[cfg(unix)]
                {
                    input.default.as_deref().unwrap_or("false").to_string()
                }
                #[cfg(windows)]
                {
                    input.default.as_deref().unwrap_or("$false").to_string()
                }
            }
        }
        _ => matches
            .opt_str(name)
            .or_else(|| input.default.clone())
            .unwrap_or_default(),
    }
}

pub fn resolve_environment_variables(
    environment: &BTreeMap<String, Option<String>>,
    inputs: &BTreeMap<String, CommandSchemaInput>,
    matches: &getopts::Matches,
) -> BTreeMap<String, String> {
    let inputs_re = get_inputs_re();
    let env_re = get_env_re();

    let mut resolved: BTreeMap<String, String> = BTreeMap::new();
    let mut pending: BTreeMap<String, String> = BTreeMap::new();

    // Separate variables that need substitution from those that don't
    for (key, value) in environment {
        if let Some(val) = value {
            if val.contains("${") || val.contains("@{") {
                // Has substitution patterns - add to pending
                pending.insert(key.clone(), val.clone());
            } else {
                // No substitution needed - directly resolved
                resolved.insert(key.clone(), val.clone());
            }
        }
    }

    let mut iterations = 0;

    while !pending.is_empty() && iterations < MAX_ITERATIONS {
        let mut progress_made = false;
        let mut keys_to_remove = Vec::new();

        for (key, value) in &pending {
            let mut result = value.clone();
            let mut variable_resolved = true;

            // Handle @{inputs.xxx}
            // Skip if iteration is > 0 since `inputs` will resolve on first go
            if iterations == 0 {
                result = inputs_re
                    .replace_all(&result, |caps: &regex::Captures| {
                        let variable_name = &caps[1];

                        if let Some(input) = inputs.get(variable_name) {
                            resolve_input_value(variable_name, input, matches)
                        } else {
                            tracing::warn!("Unknown input reference '@{{inputs.{}}}', resolving to empty string", variable_name);
                            "".to_string()
                        }
                    })
                    .to_string();
            }

            // Handle ${ENV_VAR}
            result = env_re
                .replace_all(&result, |caps: &regex::Captures| {
                    let variable_name = &caps[1];

                    if let Some(resolved_val) = resolved.get(variable_name) {
                        resolved_val.clone()
                    } else if pending.contains_key(variable_name) {
                        // Still pending
                        variable_resolved = false;
                        caps[0].to_string() // Keep ${VAR} unchanged
                    } else {
                        match std::env::var(variable_name) {
                            Ok(val) => val,
                            Err(_) => {
                                tracing::warn!("Environment variable '{}' is not set, resolving to empty string", variable_name);
                                String::new()
                            }
                        }
                    }
                })
                .to_string();

            if variable_resolved || !result.contains("${") {
                resolved.insert(key.clone(), result);
                keys_to_remove.push(key.clone());

                progress_made = true
            }
        }

        for key in keys_to_remove {
            pending.remove(&key);
        }

        if !progress_made {
            // No progress made - likely circular references
            tracing::warn!(
                "Possible circular references in environment variables, resolving remaining with OS environment"
            );
            for (key, value) in pending {
                let result = env_re
                    .replace_all(&value, |caps: &regex::Captures| {
                        std::env::var(&caps[1]).unwrap_or_default()
                    })
                    .to_string();
                resolved.insert(key, result);
            }
            break;
        }

        iterations += 1;
    }

    resolved.into_iter().collect()
}

pub fn resolve_input_variables(
    text: &str,
    inputs: &BTreeMap<String, CommandSchemaInput>,
    matches: &getopts::Matches,
) -> String {
    let inputs_re = get_inputs_re();

    inputs_re
        .replace_all(text, |caps: &regex::Captures| {
            let variable_name = &caps[1];

            if let Some(input) = inputs.get(variable_name) {
                resolve_input_value(variable_name, input, matches)
            } else {
                tracing::warn!(
                    "Unknown input reference '@{{inputs.{}}}', resolving to empty string",
                    variable_name
                );
                "".to_string()
            }
        })
        .to_string()
}
