//! Lightweight, dependency-free diagnostics sink for the orchestrator rendezvous.
//!
//! The orchestrator already emits best-effort traces through `eprintln!` (see
//! [`crate::orchestrator`]). When IdeA is launched by **clicking the AppImage**,
//! that stderr is discarded — so an inter-agent block (an `ask` that never gets
//! its `idea_reply`) leaves no retrievable trace. This module mirrors those
//! traces to a **persistent log file** the user can hand back for diagnosis,
//! while keeping the exact same "zero new dependency, never breaks the
//! rendezvous" discipline: every write is best-effort and infallible.
//!
//! - [`set_log_path`] is called once by the composition root (`app-tauri`) with
//!   `<app-data>/logs/idea.log`. Until then (and in tests) writes go to stderr
//!   only.
//! - [`diag`] (and the [`diag!`] macro) timestamps a line with epoch-millis — the
//!   same clock as the conversation logs' `atMs` — and appends it to the file,
//!   always also echoing to stderr so a terminal launch and the test suite still
//!   see it.

use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};

/// The resolved log file path, set once at startup. `None` ⇒ stderr-only.
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();

/// Serialises concurrent appends so interleaved rendezvous beacons stay on their
/// own lines (the orchestrator handles many connections in parallel).
static WRITE_LOCK: Mutex<()> = Mutex::new(());

/// Points the diagnostics sink at `path`, creating its parent directory.
///
/// Idempotent and infallible: a second call (or a failure to create the
/// directory) is ignored — diagnostics must never break startup. Called by the
/// composition root once the app-data directory is known.
pub fn set_log_path(path: PathBuf) {
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = LOG_PATH.set(path);
}

/// Current epoch-millis (same clock as the conversation logs' `atMs`).
fn now_ms() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0)
}

/// Appends one timestamped diagnostic line. Best-effort and infallible: a missing
/// path or an I/O error is swallowed (the line still reaches stderr). Never call
/// in a hot loop — these are lifecycle beacons, not a metrics stream.
pub fn diag(msg: impl std::fmt::Display) {
    let line = format!("{} {msg}", now_ms());
    // Always echo to stderr: keeps a terminal launch and the test suite working,
    // and preserves the pre-existing `eprintln!` behaviour for these beacons.
    eprintln!("{line}");
    if let Some(path) = LOG_PATH.get() {
        let _guard = WRITE_LOCK.lock();
        if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
            let _ = writeln!(f, "{line}");
        }
    }
}

/// `diag!("...", ...)` — formats then forwards to [`diag`]. Mirrors `eprintln!`.
#[macro_export]
macro_rules! diag {
    ($($arg:tt)*) => {
        $crate::diag::diag(format!($($arg)*))
    };
}
