//! Parsers and helpers for SQL and related inputs.
//!
//! Submodules contain concrete parsers.
pub mod query_builder;
pub mod sql;

use std::env::var;
use tokio::time::Duration;

pub fn parse_env_reference(uri: &str) -> Option<String> {
    uri.strip_prefix("${")
        .and_then(|value| value.strip_suffix('}'))
        .map(str::to_string)
}

pub fn resolve_postgres_uri(uri: &str) -> String {
    if let Some(env_name) = parse_env_reference(uri) {
        var(env_name).unwrap_or_else(|_| uri.to_string())
    } else {
        uri.to_string()
    }
}

/// When this returns [`Some`], the value must not be passed to SQLx or deadpool: it will not form a valid connection URL.
///
/// Covers empty values, unresolved `${ENV}` placeholders, and non-`postgres` schemes so operators see actionable messages instead of "relative URL without a base".
pub fn describe_postgres_uri_problem(uri: &str) -> Option<String> {
    let trimmed = uri.trim();
    if trimmed.is_empty() {
        return Some("connection string is empty".to_string());
    }

    let resolved = resolve_postgres_uri(trimmed);
    if resolved.trim().is_empty() {
        if let Some(env_name) = parse_env_reference(trimmed) {
            return Some(format!(
                "environment variable `{env_name}` is unset, empty, or only whitespace"
            ));
        }
        return Some("connection string resolves to an empty value".to_string());
    }

    if let Some(env_name) = parse_env_reference(trimmed) {
        if resolved == trimmed {
            return Some(format!("environment variable `{env_name}` is not set"));
        }
    }

    if !resolved.starts_with("postgres://") && !resolved.starts_with("postgresql://") {
        return Some(
            "expected postgres:// or postgresql:// after resolving env references (see config.yaml)"
                .to_string(),
        );
    }

    None
}

pub fn parse_secs_or_default(value: Option<&String>, default_secs: u64) -> Duration {
    Duration::from_secs(
        value
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(default_secs),
    )
}

pub fn parse_usize(value: &str) -> Option<usize> {
    value.parse().ok()
}

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

    #[test]
    fn describe_postgres_uri_problem_empty() {
        assert_eq!(
            describe_postgres_uri_problem("  "),
            Some("connection string is empty".to_string())
        );
    }

    #[test]
    fn describe_postgres_uri_problem_unresolved_env_placeholder() {
        let name = "ATHENA_PARSER_TEST_URI_UNSET_7E4B";
        let template = format!("${{{name}}}");
        let msg = describe_postgres_uri_problem(&template).expect("expected problem");
        assert!(
            msg.contains(name) && msg.contains("not set"),
            "unexpected message: {msg}"
        );
    }

    #[test]
    fn describe_postgres_uri_problem_ok_postgres_url() {
        assert_eq!(
            describe_postgres_uri_problem("postgres://user:pass@localhost/db"),
            None
        );
    }

    #[test]
    fn describe_postgres_uri_problem_bad_scheme() {
        let msg = describe_postgres_uri_problem("mysql://localhost/db").expect("expected problem");
        assert!(
            msg.contains("postgres://") || msg.contains("postgresql://"),
            "unexpected message: {msg}"
        );
    }
}
