Documentation
use crate::config::{AppConfig, SyncBackend};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
use tokio::process::Command;

#[derive(Debug, Clone)]
pub struct SyncResult {
    pub backend: String,
    pub message: String,
}

#[derive(Debug, Clone)]
pub struct SyncMetricsSnapshot {
    pub runs_by_trigger: HashMap<String, u64>,
    pub errors_by_trigger: HashMap<String, u64>,
    pub duration_seconds_sum: f64,
    pub duration_seconds_count: u64,
}

struct SyncMetrics {
    runs_by_trigger: Mutex<HashMap<String, u64>>,
    errors_by_trigger: Mutex<HashMap<String, u64>>,
    duration_micros_sum: AtomicU64,
    duration_count: AtomicU64,
}

fn sync_metrics() -> &'static SyncMetrics {
    static METRICS: OnceLock<SyncMetrics> = OnceLock::new();
    METRICS.get_or_init(|| SyncMetrics {
        runs_by_trigger: Mutex::new(HashMap::new()),
        errors_by_trigger: Mutex::new(HashMap::new()),
        duration_micros_sum: AtomicU64::new(0),
        duration_count: AtomicU64::new(0),
    })
}

pub fn metrics_snapshot() -> SyncMetricsSnapshot {
    let metrics = sync_metrics();
    SyncMetricsSnapshot {
        runs_by_trigger: metrics
            .runs_by_trigger
            .lock()
            .expect("sync runs metrics")
            .clone(),
        errors_by_trigger: metrics
            .errors_by_trigger
            .lock()
            .expect("sync errors metrics")
            .clone(),
        duration_seconds_sum: metrics.duration_micros_sum.load(Ordering::Relaxed) as f64
            / 1_000_000.0,
        duration_seconds_count: metrics.duration_count.load(Ordering::Relaxed),
    }
}

pub fn metrics_snapshot_json() -> serde_json::Value {
    let snapshot = metrics_snapshot();
    serde_json::json!({
        "runs_by_trigger": snapshot.runs_by_trigger,
        "errors_by_trigger": snapshot.errors_by_trigger,
        "duration_seconds_sum": snapshot.duration_seconds_sum,
        "duration_seconds_count": snapshot.duration_seconds_count,
    })
}

pub async fn sync_once(cfg: &AppConfig) -> Result<SyncResult> {
    sync_once_with_trigger(cfg, "unknown").await
}

pub async fn sync_once_with_trigger(cfg: &AppConfig, trigger: &'static str) -> Result<SyncResult> {
    let start = Instant::now();
    let result = sync_once_inner(cfg).await;
    record_sync_metrics(trigger, start.elapsed().as_micros() as u64, result.is_err());
    result
}

async fn sync_once_inner(cfg: &AppConfig) -> Result<SyncResult> {
    match cfg.sync.backend {
        SyncBackend::None => Ok(SyncResult {
            backend: "none".to_string(),
            message: "sync backend is none; no-op".to_string(),
        }),
        SyncBackend::Obsidian => sync_obsidian(cfg).await,
    }
}

fn record_sync_metrics(trigger: &str, duration_micros: u64, failed: bool) {
    let metrics = sync_metrics();
    {
        let mut runs = metrics.runs_by_trigger.lock().expect("sync runs metrics");
        *runs.entry(trigger.to_string()).or_insert(0) += 1;
    }
    if failed {
        let mut errors = metrics
            .errors_by_trigger
            .lock()
            .expect("sync errors metrics");
        *errors.entry(trigger.to_string()).or_insert(0) += 1;
    }
    metrics
        .duration_micros_sum
        .fetch_add(duration_micros, Ordering::Relaxed);
    metrics.duration_count.fetch_add(1, Ordering::Relaxed);
}

async fn sync_obsidian(cfg: &AppConfig) -> Result<SyncResult> {
    let output = Command::new("ob")
        .args(["sync", "--path", &cfg.vault_path])
        .output()
        .await
        .context("failed to execute 'ob sync'")?;

    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();

    if !output.status.success() {
        let mut message = String::from("ob sync failed");
        if !stderr.is_empty() {
            message.push_str(": ");
            message.push_str(&stderr);
        }
        if !stdout.is_empty() {
            message.push('\n');
            message.push_str(&stdout);
        }
        anyhow::bail!(message);
    }

    let message = if stdout.is_empty() {
        "ob sync ok".to_string()
    } else {
        stdout
    };

    Ok(SyncResult {
        backend: "obsidian".to_string(),
        message,
    })
}