use std::sync::Arc;

use eframe::egui;
use tracing::info;
use tracing_subscriber::EnvFilter;

use std::fs::{File, OpenOptions, TryLockError};

use doctate_desktop::app::DoctateApp;
use doctate_desktop::paths::{cases_dir, lock_file_path, pending_dir, snapshot_cache_path};

/// Unwrap a startup `Result` or print `context: {err}` to stderr and
/// `exit(1)`. Used for unrecoverable bootstrap failures before the
/// egui event loop runs — there is no UI to show an error in.
fn or_die<T, E: std::fmt::Display>(result: Result<T, E>, context: &str) -> T {
    match result {
        Ok(v) => v,
        Err(e) => {
            eprintln!("{context}: {e}");
            std::process::exit(1);
        }
    }
}

fn main() -> eframe::Result {
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new("doctate_desktop=info")),
        )
        .init();

    info!("starting doctate-desktop");

    let runtime = Arc::new(
        tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()
            .expect("build tokio runtime"),
    );

    let pending = or_die(pending_dir(), "cannot determine pending directory");

    // Single-instance guard. The lock is held by the `_lock_file` binding
    // for the entire `main` lifetime — OS releases it automatically on
    // process exit or crash, so a stale lock file after a crash is
    // harmless (the next launch re-acquires).
    let _lock_file = acquire_single_instance_lock();
    let cases = or_die(cases_dir(), "cannot determine cases directory");
    let cache_path = or_die(
        snapshot_cache_path(),
        "cannot determine snapshot cache path",
    );

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([400.0, 560.0])
            .with_min_inner_size([320.0, 400.0])
            .with_resizable(true),
        ..Default::default()
    };

    eframe::run_native(
        "doctate",
        options,
        Box::new(move |_cc| {
            let app = DoctateApp::new(runtime, pending, cases, cache_path);
            Ok(Box::new(app) as Box<dyn eframe::App>)
        }),
    )
}

/// Open and exclusively lock a per-user `client.lock`. Returns the
/// handle; dropping it (process exit) releases the lock. If another
/// instance already holds it, prints a friendly message and exits.
fn acquire_single_instance_lock() -> File {
    let path = or_die(lock_file_path(), "cannot determine lock file path");
    if let Some(parent) = path.parent()
        && let Err(e) = std::fs::create_dir_all(parent)
    {
        eprintln!("cannot create data directory {}: {e}", parent.display());
        std::process::exit(1);
    }
    let file = match OpenOptions::new()
        .create(true)
        .read(true)
        .write(true)
        .truncate(false)
        .open(&path)
    {
        Ok(f) => f,
        Err(e) => {
            eprintln!("cannot open lock file {}: {e}", path.display());
            std::process::exit(1);
        }
    };
    match file.try_lock() {
        Ok(()) => file,
        Err(TryLockError::WouldBlock) => {
            eprintln!(
                "doctate läuft bereits (Lock hält auf {}). Nur eine Instanz pro User zulässig.",
                path.display()
            );
            std::process::exit(1);
        }
        Err(TryLockError::Error(e)) => {
            eprintln!("lock acquire failed on {}: {e}", path.display());
            std::process::exit(1);
        }
    }
}
