//! The entry point for the OpenHuman core application.
//!
//! This file is responsible for:
//! - Initializing error tracking with Sentry.
//! - Setting up secret scrubbing for outgoing error reports.
//! - Dispatching command-line arguments to the core logic in `openhuman_core`.

use once_cell::sync::Lazy;
use regex::Regex;

/// Main application entry point.
///
/// It initializes the Sentry SDK for error monitoring, ensuring that sensitive
/// information is redacted before being sent to the server. After setup, it
/// delegates execution to the core library based on CLI arguments.
fn main() {
    // Load `.env` before `sentry::init` so a DSN defined only in the dotenv
    // file is visible to the Sentry client at startup. `dotenvy::dotenv()` is
    // a no-op for variables already present in the process environment, and
    // the CLI dispatcher later calls `load_dotenv_for_cli` which honors
    // `OPENHUMAN_DOTENV_PATH`; this early call handles the common default
    // case (repo-local `.env`) so startup-time consumers (Sentry, config
    // overrides) see the same values as runtime RPC handlers.
    let _ = dotenvy::dotenv();

    // Initialize Sentry as the very first operation so the guard outlives everything.
    // Resolves the core Sentry DSN by checking, in order:
    //   1. `OPENHUMAN_CORE_SENTRY_DSN` at runtime (preferred, namespaced name)
    //   2. `OPENHUMAN_SENTRY_DSN` at runtime (legacy unprefixed name — kept
    //      so existing CI vars and contributor `.env` files keep working until
    //      the GH org-level variable can be renamed)
    //   3. Each of the same names baked at compile time via `option_env!`
    // If none resolve to a non-empty value, `sentry::init` returns a no-op guard.
    let _sentry_guard = sentry::init(sentry::ClientOptions {
        dsn: std::env::var("OPENHUMAN_CORE_SENTRY_DSN")
            .ok()
            .filter(|s| !s.is_empty())
            .or_else(|| std::env::var("OPENHUMAN_SENTRY_DSN").ok())
            .filter(|s| !s.is_empty())
            .or_else(|| option_env!("OPENHUMAN_CORE_SENTRY_DSN").map(|s| s.to_string()))
            .filter(|s| !s.is_empty())
            .or_else(|| option_env!("OPENHUMAN_SENTRY_DSN").map(|s| s.to_string()))
            .filter(|s| !s.is_empty())
            .and_then(|s| s.parse().ok()),
        release: Some(std::borrow::Cow::Owned(build_release_tag())),
        environment: Some(std::borrow::Cow::Owned(resolve_environment())),
        send_default_pii: false,
        before_send: Some(std::sync::Arc::new(|mut event| {
            // Strip server_name (hostname) to avoid leaking machine identity
            event.server_name = None;
            // Attach the cached account uid so Sentry can count unique users
            // affected by an issue. We only carry `id` — never email, name,
            // or IP — so this stays consistent with `send_default_pii: false`.
            // Empty/missing on early-startup events (cache populates after
            // the first `auth_get_me` RPC); that's expected.
            event.user = openhuman_core::openhuman::app_state::peek_cached_current_user_identity()
                .and_then(|identity| identity.id)
                .map(|id| sentry::User {
                    id: Some(id),
                    ..Default::default()
                });
            // Scrub exception messages for secrets
            for exc in &mut event.exception.values {
                if let Some(ref value) = exc.value {
                    exc.value = Some(scrub_secrets(value));
                }
            }
            Some(event)
        })),
        sample_rate: 1.0,
        ..sentry::ClientOptions::default()
    });

    // Collect command-line arguments, skipping the binary name.
    let args: Vec<String> = std::env::args().skip(1).collect();

    // Delegate to the core library to handle the command.
    if let Err(err) = openhuman_core::run_core_from_args(&args) {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

// ---------------------------------------------------------------------------
// Release / environment resolution for Sentry
// ---------------------------------------------------------------------------

/// Canonical release tag: `openhuman@<version>[+<short_sha>]`.
///
/// Matches the string the frontend reports (`SENTRY_RELEASE` in
/// `app/src/utils/config.ts`) so events from every surface group under
/// the same release in the Sentry dashboard and benefit from the same
/// source-map upload.
fn build_release_tag() -> String {
    let version = env!("CARGO_PKG_VERSION");
    let sha = option_env!("OPENHUMAN_BUILD_SHA").unwrap_or("").trim();
    let sha_short: String = sha.chars().take(12).collect();
    if sha_short.is_empty() {
        format!("openhuman@{version}")
    } else {
        format!("openhuman@{version}+{sha_short}")
    }
}

/// Resolve the deployment environment reported to Sentry.
///
/// Honors `OPENHUMAN_APP_ENV` at runtime (`staging` / `production`) so the
/// same binary could in principle be redeployed between environments; falls
/// back to debug/release detection when unset.
fn resolve_environment() -> String {
    if let Ok(value) = std::env::var("OPENHUMAN_APP_ENV") {
        let trimmed = value.trim().to_ascii_lowercase();
        if !trimmed.is_empty() {
            return trimmed;
        }
    }
    if cfg!(debug_assertions) {
        "development".to_string()
    } else {
        "production".to_string()
    }
}

// ---------------------------------------------------------------------------
// Secret scrubbing
// ---------------------------------------------------------------------------

/// A static list of regular expression patterns used to identify and redact
/// sensitive information such as API keys and bearer tokens.
static SECRET_PATTERNS: Lazy<Vec<(Regex, &'static str)>> = Lazy::new(|| {
    vec![
        // Matches "Bearer <token>" and redacts the token.
        (Regex::new(r"(?i)(bearer\s+)\S+").unwrap(), "${1}[REDACTED]"),
        // Matches "api-key: <key>" or "api_key=<key>" and redacts the key.
        (
            Regex::new(r"(?i)(api[_-]?key[=:\s]+)\S+").unwrap(),
            "${1}[REDACTED]",
        ),
        // Matches "token: <token>" or "token=<token>" and redacts the token.
        (
            Regex::new(r"(?i)(token[=:\s]+)\S+").unwrap(),
            "${1}[REDACTED]",
        ),
        // Matches OpenAI-style secret keys (sk-...) and redacts them.
        (Regex::new(r"sk-[a-zA-Z0-9]{20,}").unwrap(), "[REDACTED]"),
    ]
});

/// Replaces patterns that look like secrets with `[REDACTED]`.
///
/// This function iterates through a predefined list of sensitive data patterns
/// and applies them to the input string.
///
/// # Arguments
///
/// * `input` - A string slice that potentially contains sensitive information.
///
/// # Returns
///
/// A new `String` with sensitive patterns replaced by `[REDACTED]`.
fn scrub_secrets(input: &str) -> String {
    let mut result = input.to_string();
    for (re, replacement) in SECRET_PATTERNS.iter() {
        result = re.replace_all(&result, *replacement).into_owned();
    }
    result
}
