pub mod app;
pub mod audio;
pub mod avatar;
pub mod background;
pub mod codec;
pub mod config;
pub mod core;
pub mod discovery;
pub mod dsp;
pub mod files;
pub mod friends;
pub mod game;
pub mod hotkeys;
pub mod identity;
pub mod network;
pub mod notify;
pub mod playlist;
pub mod presence;
pub mod presence_net;
pub mod protocol;
pub mod recents;
pub mod sanitize;
pub mod screenshare;
pub mod theme;
pub mod widget;

use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

const LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
// Owner-only log permissions are a Unix concept (mode bits); on Windows the log
// inherits the directory's default ACL. Only referenced under `cfg(unix)`.
#[cfg(unix)]
const LOG_MODE: u32 = 0o600;

/// Resolves the log file path once: `$XDG_STATE_HOME/peerspeak/peerspeak.log`
/// (via `dirs::state_dir`), falling back to the system temp dir. Computed lazily
/// so we never hardcode a per-user path.
fn log_path() -> &'static PathBuf {
    static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
    LOG_PATH.get_or_init(|| {
        let mut dir = dirs::state_dir()
            .or_else(dirs::cache_dir)
            .unwrap_or_else(std::env::temp_dir);
        dir.push("peerspeak");
        let _ = std::fs::create_dir_all(&dir);
        dir.push("peerspeak.log");
        dir
    })
}

/// The resolved log file path. Exposed so diagnostic tools (e.g. the audio
/// probe) can tail the same log the app writes to.
pub fn log_file_path() -> PathBuf {
    log_path().clone()
}

/// Short, human-matchable id prefix for diagnostics. Never use this where the
/// full value is needed for protocol behavior.
pub fn short_id(id: &str) -> String {
    id.chars().take(8).collect()
}

/// Redact a capability-bearing value for logs while keeping a tiny prefix for
/// support correlation. Tickets and endpoint addresses are bearer capabilities:
/// logging the full string is equivalent to leaking the room/share.
pub fn redact_for_log(value: &str) -> String {
    let value = value.trim();
    if value.is_empty() {
        "<redacted:empty>".to_string()
    } else {
        format!("<redacted:{}...>", short_id(value))
    }
}

pub fn short_bytes_hex(bytes: &[u8]) -> String {
    bytes
        .iter()
        .take(6)
        .map(|b| format!("{b:02x}"))
        .collect::<Vec<_>>()
        .join("")
}

fn rotated_log_path(path: &Path) -> PathBuf {
    let file_name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("peerspeak.log");
    path.with_file_name(format!("{file_name}.1"))
}

fn prepare_log_file(path: &Path) -> std::io::Result<File> {
    prepare_log_file_with_limit(path, LOG_MAX_BYTES)
}

fn prepare_log_file_with_limit(path: &Path, max_bytes: u64) -> std::io::Result<File> {
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }

    if std::fs::metadata(path).is_ok_and(|m| m.len() > max_bytes) {
        let rotated = rotated_log_path(path);
        let _ = std::fs::remove_file(&rotated);
        if std::fs::rename(path, &rotated).is_err() {
            let _ = std::fs::OpenOptions::new()
                .write(true)
                .truncate(true)
                .open(path);
        }
    }

    let mut opts = std::fs::OpenOptions::new();
    opts.create(true).append(true);
    // The log can carry capability-bearing values (redacted, but still): keep it
    // owner-only on Unix via the open mode. Windows has no mode bits; it inherits
    // the directory ACL, so this hardening is Unix-only.
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        opts.mode(LOG_MODE);
    }
    let file = opts.open(path)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        // Re-assert the mode in case the file pre-existed with looser perms.
        let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(LOG_MODE));
    }
    Ok(file)
}

pub fn log_msg(msg: &str) {
    // Format the whole line into one buffer first, then emit it with a single
    // `write_all`. The file is opened with `O_APPEND`, so a lone `write()` is
    // atomic w.r.t. concurrent writers; building the line up front avoids the
    // multi-syscall `writeln!` path that would let threads interleave fragments.
    let line = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
        Ok(time) => format!("[{}.{:03}] {}\n", time.as_secs(), time.subsec_millis(), msg),
        Err(_) => format!("{}\n", msg),
    };
    if let Ok(mut file) = prepare_log_file(log_path()) {
        use std::io::Write;
        let _ = file.write_all(line.as_bytes());
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    fn temp_log_dir() -> PathBuf {
        let stamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        std::env::temp_dir().join(format!("peerspeak-log-test-{}-{stamp}", std::process::id()))
    }

    #[test]
    fn redaction_keeps_only_a_short_prefix() {
        let secret = "abcdefghijklmnopqrstuvwxyz";
        let redacted = redact_for_log(secret);
        assert!(redacted.contains("abcdefgh"));
        assert!(!redacted.contains("ijklmnopqrstuvwxyz"));
        assert_eq!(redact_for_log("   "), "<redacted:empty>");
    }

    // Owner-only log perms are a Unix concept; on Windows the file inherits the
    // directory ACL and there's no mode to assert.
    #[cfg(unix)]
    #[test]
    fn log_file_is_created_private() {
        let dir = temp_log_dir();
        let path = dir.join("peerspeak.log");
        let _file = prepare_log_file(&path).unwrap();

        let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
        assert_eq!(mode, LOG_MODE);
        let _ = std::fs::remove_dir_all(dir);
    }

    #[test]
    fn oversized_log_is_rotated_on_open() {
        let dir = temp_log_dir();
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("peerspeak.log");
        {
            let mut file = std::fs::File::create(&path).unwrap();
            file.write_all(b"oversized").unwrap();
        }

        let _file = prepare_log_file_with_limit(&path, 4).unwrap();
        let rotated = rotated_log_path(&path);

        assert_eq!(std::fs::read_to_string(rotated).unwrap(), "oversized");
        assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
        let _ = std::fs::remove_dir_all(dir);
    }
}
