use std::collections::HashSet;
use std::path::Path;
use std::process::Command;

use anyhow::{bail, Context, Result};
use cap_std::fs::Dir;
use chrono::{DateTime, Utc};
use fn_error_context::context;

/// Parse an environment variable as UTF-8
#[allow(dead_code)]
pub(crate) fn getenv_utf8(n: &str) -> Result<Option<String>> {
    if let Some(v) = std::env::var_os(n) {
        Ok(Some(
            v.to_str()
                .ok_or_else(|| anyhow::anyhow!("{} is invalid UTF-8", n))?
                .to_string(),
        ))
    } else {
        Ok(None)
    }
}

#[allow(dead_code)]
pub(crate) fn filenames(dir: &Dir) -> Result<HashSet<String>> {
    let mut ret = HashSet::new();
    for entry in dir.entries_utf8()? {
        let entry = entry?;
        let Ok(name) = entry.file_name() else {
            bail!("Invalid filename: {:?}", entry.file_name())
        };
        let file_type = entry.file_type()?;
        if file_type.is_file() {
            ret.insert(format!("/{name}"));
        } else if file_type.is_dir() {
            let child = dir.open_dir(&name)?;
            for mut k in filenames(&child)?.drain() {
                k.reserve(name.len() + 1);
                k.insert_str(0, &name);
                k.insert(0, '/');
                ret.insert(k);
            }
        } else if file_type.is_symlink() {
            bail!("Unsupported symbolic link {:?}", entry.file_name())
        } else {
            bail!("Unsupported non-file/directory {:?}", entry.file_name())
        }
    }
    Ok(ret)
}

pub(crate) fn ensure_writable_mount<P: AsRef<Path>>(p: P) -> Result<()> {
    let p = p.as_ref();
    let stat = rustix::fs::statvfs(p)?;
    if !stat.f_flag.contains(rustix::fs::StatVfsMountFlags::RDONLY) {
        return Ok(());
    }
    let status = std::process::Command::new("mount")
        .args(["-o", "remount,rw"])
        .arg(p)
        .status()?;
    if !status.success() {
        anyhow::bail!("Failed to remount {:?} writable", p);
    }
    Ok(())
}

/// Runs the provided Command object, captures its stdout, and swallows its stderr except on
/// failure. Returns a Result<String> describing whether the command failed, and if not, its
/// standard output. Output is assumed to be UTF-8. Errors are adequately prefixed with the full
/// command.
#[allow(dead_code)]
pub(crate) fn cmd_output(cmd: &mut Command) -> Result<String> {
    let result = cmd
        .output()
        .with_context(|| format!("running {:#?}", cmd))?;
    if !result.status.success() {
        eprintln!("{}", String::from_utf8_lossy(&result.stderr));
        bail!("{:#?} failed with {}", cmd, result.status);
    }
    String::from_utf8(result.stdout)
        .with_context(|| format!("decoding as UTF-8 output of `{:#?}`", cmd))
}

/// Copy from https://github.com/containers/bootc/blob/main/ostree-ext/src/container_utils.rs#L20
/// Attempts to detect if the current process is running inside a container.
/// This looks for the `container` environment variable or the presence
/// of Docker or podman's more generic `/run/.containerenv`.
/// This is a best-effort function, as there is not a 100% reliable way
/// to determine this.
pub fn running_in_container() -> bool {
    if std::env::var_os("container").is_some() {
        return true;
    }
    // https://stackoverflow.com/questions/20010199/how-to-determine-if-a-process-runs-inside-lxc-docker
    for p in ["/run/.containerenv", "/.dockerenv"] {
        if Path::new(p).exists() {
            return true;
        }
    }
    false
}

/// Suppress SIGTERM while active
// TODO: In theory we could record if we got SIGTERM and exit
// on drop, but in practice we don't care since we're going to exit anyways.
#[derive(Debug)]
pub(crate) struct SignalTerminationGuard(signal_hook_registry::SigId);

impl SignalTerminationGuard {
    pub(crate) fn new() -> Result<Self> {
        let signal = unsafe { signal_hook_registry::register(libc::SIGTERM, || {})? };
        Ok(Self(signal))
    }
}

impl Drop for SignalTerminationGuard {
    fn drop(&mut self) {
        signal_hook_registry::unregister(self.0);
    }
}

#[context("Getting timestamp for metadata")]
pub fn get_metadata_timestamp() -> Result<DateTime<Utc>> {
    let ts = match std::env::var("SOURCE_DATE_EPOCH") {
        Ok(value) => {
            let unix_secs = value
                .parse::<i64>()
                .context("Parsing SOURCE_DATE_EPOCH as integer")?;

            chrono::DateTime::from_timestamp_secs(unix_secs).ok_or_else(|| {
                anyhow::anyhow!("SOURCE_DATE_EPOCH value '{value}' is not a valid timestamp")
            })?
        }
        Err(std::env::VarError::NotPresent) => {
            let timestamp = std::time::SystemTime::now();
            chrono::DateTime::<Utc>::from(timestamp)
        }
        Err(e) => Err(e).context("Reading SOURCE_DATE_EPOCH")?,
    };

    Ok(ts)
}
