use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use tauri::Manager;

use crate::commands::security::ensure_main_window;
use lumotia_audio::{DeviceInfo, MicrophoneCapture};

/// Enumerate every input device available to cpal, with metadata for the
/// Settings device-picker UI. Includes a flag for likely PulseAudio /
/// PipeWire monitor sources so the UI can warn the user.
#[tauri::command]
pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result<Vec<DeviceInfo>, String> {
    ensure_main_window(&window)?;
    tokio::task::spawn_blocking(MicrophoneCapture::list_devices)
        .await
        .map_err(|e| format!("join error: {e}"))?
        .map_err(|e| e.to_string())
}

/// Resolve the destination path for a new live-capture recording,
/// ensuring the parent directory exists. Used by
/// `start_live_transcription_session` to hand the path to the progressive
/// WAV writer before any samples arrive (brief item #19).
///
/// Trust boundary (Trust-2): `output_folder` arrives from the webview
/// as a free-form string and is otherwise joined verbatim into a
/// PathBuf that `create_dir_all` + WavWriter then operate on. Without
/// validation the webview can write recordings anywhere the Lumotia
/// process can write — `/etc`, `/var`, somebody else's home dir.
///
/// The fix:
/// 1. None or empty → fall back to the default `recordings` subtree
///    under the app-local data dir. Always safe.
/// 2. Otherwise → canonicalise the supplied path (resolves `..` and
///    symlinks) and ensure it sits inside the trusted base. Reject if
///    it escapes.
///
/// A user who wants recordings somewhere else must configure it via
/// Settings (a different trust boundary owned by the app's persisted
/// preferences); the live-session command surface is constrained.
pub fn resolve_recording_path(
    app: &tauri::AppHandle,
    output_folder: Option<&str>,
) -> Result<PathBuf, String> {
    let default_base = app
        .path()
        .app_local_data_dir()
        .map_err(|e: tauri::Error| e.to_string())?
        .join("recordings");

    let recordings_dir = match output_folder.map(str::trim).filter(|s| !s.is_empty()) {
        Some(folder) => validate_output_folder(folder, &default_base)?,
        None => default_base,
    };

    std::fs::create_dir_all(&recordings_dir)
        .map_err(|e| format!("Failed to create recordings dir: {e}"))?;

    Ok(recordings_dir.join(recording_filename()))
}

/// Validate that a webview-supplied `output_folder` does not escape the
/// trusted recordings base. Canonicalises both sides so `..`-walks and
/// symlinks resolve before the containment check.
///
/// Allows the supplied path to be either the base itself or a
/// descendant of it; everything else is rejected. The base must
/// already exist on disk (we `create_dir_all` it on the default
/// branch); we ensure it here too so canonicalisation can succeed even
/// on a clean install where neither side has been created yet.
fn validate_output_folder(folder: &str, default_base: &Path) -> Result<PathBuf, String> {
    // Make sure the trusted base exists before canonicalisation —
    // `canonicalize` errors on missing paths and we want a clean
    // failure mode on first launch.
    std::fs::create_dir_all(default_base)
        .map_err(|e| format!("Failed to ensure recordings base dir: {e}"))?;
    // Create the requested path too if it doesn't exist yet, so
    // canonicalisation can resolve it. If the user is pointing at a
    // path outside the trusted base the containment check rejects
    // them right after — a stray empty directory is the only side
    // effect, which is acceptable for the trust gain.
    let requested = PathBuf::from(folder);
    if !requested.exists() {
        std::fs::create_dir_all(&requested).map_err(|e| {
            format!(
                "Failed to create requested output folder '{}': {e}",
                requested.display()
            )
        })?;
    }
    let canonical_base = default_base.canonicalize().map_err(|e| {
        format!(
            "Failed to canonicalise recordings base '{}': {e}",
            default_base.display()
        )
    })?;
    let canonical_requested = requested.canonicalize().map_err(|e| {
        format!(
            "Failed to canonicalise output folder '{}': {e}",
            requested.display()
        )
    })?;
    if !canonical_requested.starts_with(&canonical_base) {
        return Err(format!(
            "outputFolder '{}' is outside the allowed recordings base '{}'",
            canonical_requested.display(),
            canonical_base.display()
        ));
    }
    Ok(canonical_requested)
}

/// Deterministic recording filename generator. Combines three fields
/// for absolute uniqueness across rapid calls:
///
/// - wall-clock seconds since the epoch — human-readable and
///   sortable;
/// - the sub-second nanosecond component — defeats same-second
///   collisions;
/// - a process-lifetime atomic counter — defeats even same-nanosecond
///   collisions, which `SystemTime::now()` alone cannot guarantee
///   (two calls in the same clock tick can return identical nanos).
///
/// Format: `lumotia-<secs>-<nanos_in_sec>-<counter>.wav`, e.g.
/// `lumotia-1776828000-123456789-0000.wav`.
fn recording_filename() -> String {
    let duration = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = duration.as_secs();
    let nanos = duration.subsec_nanos();
    let counter = RECORDING_COUNTER.fetch_add(1, Ordering::SeqCst);
    format!("lumotia-{secs}-{nanos:09}-{counter:04}.wav")
}

/// Process-lifetime monotonic counter for `recording_filename`. Starts
/// at 0 on each Lumotia launch; wall-clock secs/nanos still advance across
/// restarts, so cross-launch collisions are already impossible — the
/// counter is the last-mile guarantee against within-launch same-tick
/// collisions.
static RECORDING_COUNTER: AtomicU64 = AtomicU64::new(0);

#[cfg(test)]
mod tests {
    use super::{recording_filename, validate_output_folder};
    use std::path::PathBuf;

    #[test]
    fn recording_filenames_are_unique_across_rapid_calls() {
        // Regression for the 2026-04-22 review AND the review-of-
        // review MINOR: SystemTime::now() alone cannot guarantee
        // uniqueness under tight loops on every OS clock resolution,
        // so the filename now includes a process-lifetime atomic
        // counter. With the counter, uniqueness is absolute across
        // any number of in-process calls.
        let mut names = std::collections::HashSet::new();
        for _ in 0..1024 {
            names.insert(recording_filename());
        }
        assert_eq!(
            names.len(),
            1024,
            "every filename must be unique (counter-backed guarantee)"
        );
    }

    fn unique_tmp(label: &str) -> PathBuf {
        let pid = std::process::id();
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        std::env::temp_dir().join(format!("lumotia-audio-test-{label}-{pid}-{nanos}"))
    }

    #[test]
    fn validate_output_folder_accepts_base_itself() {
        // Trust-2 happy path: the recordings base is always allowed.
        let root = unique_tmp("base-itself");
        let base = root.join("recordings");
        std::fs::create_dir_all(&base).unwrap();

        let canonical = validate_output_folder(base.to_str().unwrap(), &base)
            .expect("base path must validate as itself");
        assert_eq!(canonical, base.canonicalize().unwrap());

        std::fs::remove_dir_all(&root).ok();
    }

    #[test]
    fn validate_output_folder_accepts_descendant() {
        // A user-configured subdirectory of the trusted base is fine.
        let root = unique_tmp("descendant");
        let base = root.join("recordings");
        let nested = base.join("2026-05");
        std::fs::create_dir_all(&nested).unwrap();

        let canonical = validate_output_folder(nested.to_str().unwrap(), &base)
            .expect("descendant must validate");
        assert!(canonical.starts_with(base.canonicalize().unwrap()));

        std::fs::remove_dir_all(&root).ok();
    }

    #[test]
    fn validate_output_folder_rejects_etc() {
        // Trust-2 attack shape: webview supplies `/etc`. Must be
        // rejected even though /etc exists (and `create_dir_all` on it
        // would no-op).
        let root = unique_tmp("etc-reject");
        let base = root.join("recordings");
        std::fs::create_dir_all(&base).unwrap();

        let err = validate_output_folder("/etc", &base)
            .expect_err("`/etc` must NEVER be accepted as a recordings dir");
        assert!(
            err.contains("outside the allowed recordings base"),
            "rejection message must name the trust boundary: {err}"
        );

        std::fs::remove_dir_all(&root).ok();
    }

    #[test]
    fn validate_output_folder_rejects_parent_escape() {
        // `..`-walk attack shape: a path that resolves OUT of the
        // trusted base via parent traversal. Canonicalisation must
        // resolve the `..` before the containment check runs.
        let root = unique_tmp("parent-escape");
        let base = root.join("recordings");
        let escape_target = root.join("escape");
        std::fs::create_dir_all(&base).unwrap();
        std::fs::create_dir_all(&escape_target).unwrap();

        // recordings/../escape — exists on disk, but resolves to root/escape.
        let attack = base.join("..").join("escape");
        let err = validate_output_folder(attack.to_str().unwrap(), &base)
            .expect_err("..-walk out of base must be rejected");
        assert!(
            err.contains("outside the allowed recordings base"),
            "rejection message must name the trust boundary: {err}"
        );

        std::fs::remove_dir_all(&root).ok();
    }

    #[test]
    fn validate_output_folder_rejects_sibling_dir() {
        // Plain sibling-of-base: equal path depth, prefix-matching the
        // base's parent but not the base itself. `starts_with` on
        // canonical paths must reject this (a bare-string prefix match
        // would have let `recordings-backdoor` through).
        let root = unique_tmp("sibling");
        let base = root.join("recordings");
        let sibling = root.join("recordings-backdoor");
        std::fs::create_dir_all(&base).unwrap();
        std::fs::create_dir_all(&sibling).unwrap();

        let err = validate_output_folder(sibling.to_str().unwrap(), &base)
            .expect_err("sibling of base must be rejected even with prefix overlap");
        assert!(err.contains("outside the allowed recordings base"));

        std::fs::remove_dir_all(&root).ok();
    }

    #[cfg(unix)]
    #[test]
    fn validate_output_folder_rejects_symlink_pointing_out() {
        // Symlink-escape attack shape: a path inside the base which
        // resolves through a symlink to a directory outside the base.
        // Canonicalisation must follow the link before the containment
        // check.
        let root = unique_tmp("symlink-escape");
        let base = root.join("recordings");
        let outside = root.join("outside");
        std::fs::create_dir_all(&base).unwrap();
        std::fs::create_dir_all(&outside).unwrap();
        let link = base.join("trapdoor");
        std::os::unix::fs::symlink(&outside, &link).unwrap();

        let err = validate_output_folder(link.to_str().unwrap(), &base)
            .expect_err("symlink to outside path must be rejected");
        assert!(err.contains("outside the allowed recordings base"));

        std::fs::remove_dir_all(&root).ok();
    }

    #[test]
    fn recording_filename_has_expected_shape() {
        let name = recording_filename();
        assert!(name.starts_with("lumotia-"));
        assert!(name.ends_with(".wav"));
        // Shape: lumotia-<digits>-<9 digits>-<>=4 digits>.wav
        let rest = name
            .strip_prefix("lumotia-")
            .and_then(|s| s.strip_suffix(".wav"))
            .expect("shape prefix/suffix");
        let parts: Vec<&str> = rest.split('-').collect();
        assert_eq!(
            parts.len(),
            3,
            "expected three '-' separated parts, got {parts:?}"
        );
        assert!(
            parts[0].chars().all(|c| c.is_ascii_digit()),
            "secs is digits"
        );
        assert_eq!(
            parts[1].len(),
            9,
            "nanos component is zero-padded to 9 digits"
        );
        assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
        assert!(
            parts[2].len() >= 4,
            "counter component is zero-padded to >=4 digits"
        );
        assert!(parts[2].chars().all(|c| c.is_ascii_digit()));
    }
}
