use crate::{Diagnostics, LoadOptions, LocalJournalProvider, clock_gettime_usec, parse_uuid_text};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uuid::Uuid;

pub(crate) fn load(options: LoadOptions) -> io::Result<LocalJournalProvider> {
    let (machine_id, machine_id_source) =
        load_machine_id(options.host_filesystem_prefix.as_deref())?;
    let (boot_id, boot_id_source, degraded_reason) =
        match load_boot_id(options.host_filesystem_prefix.as_deref()) {
            Ok(id) => (id, crate::BootIdSource::Native, None),
            Err(err) => (
                Uuid::new_v4(),
                crate::BootIdSource::Degraded,
                Some(format!("linux boot_id unavailable: {err}")),
            ),
        };
    let mut diagnostics = Diagnostics {
        machine_id_source,
        boot_id_source,
        monotonic_source_detail: "CLOCK_MONOTONIC".to_string(),
        degraded_reason,
        ..Diagnostics::default()
    };
    let monotonic = options
        .monotonic_now
        .clone()
        .unwrap_or_else(|| Arc::new(|| clock_gettime_usec(libc::CLOCK_MONOTONIC)));
    let label = options
        .monotonic_label
        .clone()
        .unwrap_or_else(|| "CLOCK_MONOTONIC".to_string());
    diagnostics.monotonic_source_detail = label.clone();
    Ok(LocalJournalProvider::new(
        machine_id,
        boot_id,
        diagnostics,
        label,
        monotonic,
    ))
}

fn load_machine_id(host_filesystem_prefix: Option<&Path>) -> io::Result<(Uuid, String)> {
    load_machine_id_from_root(Path::new("/"), host_filesystem_prefix)
}

fn load_machine_id_from_root(
    root: &Path,
    host_filesystem_prefix: Option<&Path>,
) -> io::Result<(Uuid, String)> {
    let rel_paths = [
        Path::new("etc/machine-id"),
        Path::new("var/lib/dbus/machine-id"),
    ];
    if let Some(prefix) = host_filesystem_prefix.filter(|path| !path.as_os_str().is_empty()) {
        let prefix_root = rooted_path(root, prefix);
        for rel in rel_paths {
            let source = format!("linux:{}", display_prefixed_path(prefix, rel));
            match read_machine_id_candidate(&prefix_root.join(rel), &source) {
                Ok(id) => return Ok(id),
                Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
                Err(err) => return Err(err),
            }
        }
    }

    for rel in rel_paths {
        let source = format!("linux:/{}", rel.display());
        match read_machine_id_candidate(&root.join(rel), &source) {
            Ok(id) => return Ok(id),
            Err(_) => continue,
        }
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        "linux machine-id not found",
    ))
}

fn read_machine_id_candidate(path: &Path, source: &str) -> io::Result<(Uuid, String)> {
    fs::read_to_string(path)
        .and_then(|text| parse_uuid_text(&text))
        .map(|id| (id, source.to_string()))
        .map_err(|err| {
            if err.kind() == io::ErrorKind::NotFound {
                err
            } else {
                io::Error::new(err.kind(), format!("failed to read {source}: {err}"))
            }
        })
}

fn rooted_path(root: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        match path.strip_prefix("/") {
            Ok(rel) => root.join(rel),
            Err(_) => path.to_path_buf(),
        }
    } else {
        root.join(path)
    }
}

fn display_prefixed_path(prefix: &Path, rel: &Path) -> String {
    let mut prefix = prefix.display().to_string();
    while prefix.ends_with('/') && prefix.len() > 1 {
        prefix.pop();
    }
    if prefix == "/" {
        format!("/{}", rel.display())
    } else {
        format!("{}/{}", prefix, rel.display())
    }
}

fn load_boot_id(host_filesystem_prefix: Option<&Path>) -> io::Result<Uuid> {
    load_boot_id_from_root(Path::new("/"), host_filesystem_prefix)
}

fn load_boot_id_from_root(root: &Path, host_filesystem_prefix: Option<&Path>) -> io::Result<Uuid> {
    let rel = Path::new("proc/sys/kernel/random/boot_id");
    if let Some(prefix) = host_filesystem_prefix.filter(|path| !path.as_os_str().is_empty()) {
        let source = format!("linux:{}", display_prefixed_path(prefix, rel));
        match read_boot_id_candidate(&rooted_path(root, prefix).join(rel), &source) {
            Ok(id) => return Ok(id),
            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
            Err(err) => return Err(err),
        }
    }
    read_boot_id_candidate(&root.join(rel), "linux:/proc/sys/kernel/random/boot_id")
}

fn read_boot_id_candidate(path: &Path, source: &str) -> io::Result<Uuid> {
    let text = fs::read_to_string(path).map_err(|err| {
        if err.kind() == io::ErrorKind::NotFound {
            err
        } else {
            io::Error::new(err.kind(), format!("failed to read {source}: {err}"))
        }
    })?;
    parse_uuid_text(&text)
        .map_err(|err| io::Error::new(err.kind(), format!("failed to read {source}: {err}")))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    const CONTAINER_ID: &str = "00112233445566778899aabbccddeeff";
    const HOST_ID: &str = "ffeeddccbbaa99887766554433221100";
    const DBUS_ID: &str = "0123456789abcdef0123456789abcdef";
    const CONTAINER_BOOT_ID: &str = "11111111111111111111111111111111";
    const HOST_BOOT_ID: &str = "22222222222222222222222222222222";

    #[test]
    fn machine_id_uses_container_paths_by_default() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "etc/machine-id", CONTAINER_ID);
        write_machine_id(dir.path(), "host/etc/machine-id", HOST_ID);

        let (id, source) = load_machine_id_from_root(dir.path(), None).unwrap();

        assert_eq!(id, Uuid::parse_str(CONTAINER_ID).unwrap());
        assert_eq!(source, "linux:/etc/machine-id");
    }

    #[test]
    fn machine_id_prefers_explicit_host_prefix() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "etc/machine-id", CONTAINER_ID);
        write_machine_id(dir.path(), "host/etc/machine-id", HOST_ID);

        let (id, source) = load_machine_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap();

        assert_eq!(id, Uuid::parse_str(HOST_ID).unwrap());
        assert_eq!(source, "linux:/host/etc/machine-id");
    }

    #[test]
    fn machine_id_falls_back_when_host_prefix_absent() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "var/lib/dbus/machine-id", DBUS_ID);

        let (id, source) = load_machine_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap();

        assert_eq!(id, Uuid::parse_str(DBUS_ID).unwrap());
        assert_eq!(source, "linux:/var/lib/dbus/machine-id");
    }

    #[test]
    fn machine_id_checks_host_dbus_path_before_container_paths() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "etc/machine-id", CONTAINER_ID);
        write_machine_id(dir.path(), "host/var/lib/dbus/machine-id", DBUS_ID);

        let (id, source) = load_machine_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap();

        assert_eq!(id, Uuid::parse_str(DBUS_ID).unwrap());
        assert_eq!(source, "linux:/host/var/lib/dbus/machine-id");
    }

    #[test]
    fn machine_id_errors_on_invalid_explicit_host_prefix_file() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "etc/machine-id", CONTAINER_ID);
        write_text(dir.path(), "host/etc/machine-id", "not-a-machine-id\n");

        let err = load_machine_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap_err();

        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("linux:/host/etc/machine-id"));
    }

    #[test]
    fn machine_id_errors_on_invalid_first_host_file_even_when_host_dbus_exists() {
        let dir = TempDir::new().unwrap();
        write_text(dir.path(), "host/etc/machine-id", "not-a-machine-id\n");
        write_machine_id(dir.path(), "host/var/lib/dbus/machine-id", DBUS_ID);

        let err = load_machine_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap_err();

        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(err.to_string().contains("linux:/host/etc/machine-id"));
    }

    #[test]
    fn machine_id_empty_host_prefix_keeps_container_default() {
        let dir = TempDir::new().unwrap();
        write_machine_id(dir.path(), "etc/machine-id", CONTAINER_ID);
        write_machine_id(dir.path(), "host/etc/machine-id", HOST_ID);

        let (id, source) = load_machine_id_from_root(dir.path(), Some(Path::new(""))).unwrap();

        assert_eq!(id, Uuid::parse_str(CONTAINER_ID).unwrap());
        assert_eq!(source, "linux:/etc/machine-id");
    }

    #[test]
    fn boot_id_uses_container_path_by_default() {
        let dir = TempDir::new().unwrap();
        write_boot_id(
            dir.path(),
            "proc/sys/kernel/random/boot_id",
            CONTAINER_BOOT_ID,
        );
        write_boot_id(
            dir.path(),
            "host/proc/sys/kernel/random/boot_id",
            HOST_BOOT_ID,
        );

        let id = load_boot_id_from_root(dir.path(), None).unwrap();

        assert_eq!(id, Uuid::parse_str(CONTAINER_BOOT_ID).unwrap());
    }

    #[test]
    fn boot_id_prefers_explicit_host_prefix() {
        let dir = TempDir::new().unwrap();
        write_boot_id(
            dir.path(),
            "proc/sys/kernel/random/boot_id",
            CONTAINER_BOOT_ID,
        );
        write_boot_id(
            dir.path(),
            "host/proc/sys/kernel/random/boot_id",
            HOST_BOOT_ID,
        );

        let id = load_boot_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap();

        assert_eq!(id, Uuid::parse_str(HOST_BOOT_ID).unwrap());
    }

    #[test]
    fn boot_id_falls_back_when_host_prefix_absent() {
        let dir = TempDir::new().unwrap();
        write_boot_id(
            dir.path(),
            "proc/sys/kernel/random/boot_id",
            CONTAINER_BOOT_ID,
        );

        let id = load_boot_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap();

        assert_eq!(id, Uuid::parse_str(CONTAINER_BOOT_ID).unwrap());
    }

    #[test]
    fn boot_id_errors_on_invalid_explicit_host_prefix_file() {
        let dir = TempDir::new().unwrap();
        write_boot_id(
            dir.path(),
            "proc/sys/kernel/random/boot_id",
            CONTAINER_BOOT_ID,
        );
        write_text(
            dir.path(),
            "host/proc/sys/kernel/random/boot_id",
            "not-a-boot-id\n",
        );

        let err = load_boot_id_from_root(dir.path(), Some(Path::new("/host"))).unwrap_err();

        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
        assert!(
            err.to_string()
                .contains("linux:/host/proc/sys/kernel/random/boot_id")
        );
    }

    fn write_machine_id(root: &Path, rel: &str, value: &str) {
        write_text(root, rel, &format!("{value}\n"));
    }

    fn write_boot_id(root: &Path, rel: &str, value: &str) {
        write_text(root, rel, &format!("{value}\n"));
    }

    fn write_text(root: &Path, rel: &str, value: &str) {
        let path = root.join(rel);
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(path, value).unwrap();
    }
}
