use crate::container::Container;
use crate::errors::*;
use crate::pkgs::Pkg;
use peekread::{BufPeekReader, PeekRead};
use ruzstd::decoding::StreamingDecoder;
use std::fmt::Write;
use std::io::{BufRead, BufReader, Read};
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

// The default ruzstd value is 100MiB, which is smaller than libzstd's 128MiB default.
//
// See the following resources:
// https://github.com/facebook/zstd/wiki/Using-libzstd-in-a-memory-constrained-environment#zstd_d_maxwindowlog
// https://github.com/KillingSpark/zstd-rs/pull/119
const LIBZSTD_MAX_WINDOW_SIZE: u64 = 1024 * 1024 * 128;

pub const GPG_CONF_DIR: &str = "/etc/pacman.d/gnupg/";
pub const GPG_CONF_FILENAME: &str = "gpg.conf";

pub enum Compression {
    Xz,
    Zstd,
    None,
}

pub fn detect_compression<R: Read>(mut reader: R) -> Result<Compression> {
    let mut buf = [0u8; 6];

    reader
        .read_exact(&mut buf)
        .context("Failed to read magic bytes from archive")?;

    if buf.starts_with(&[0x28, 0xB5, 0x2F, 0xFD]) {
        Ok(Compression::Zstd)
    } else if buf.starts_with(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) {
        Ok(Compression::Xz)
    } else {
        Ok(Compression::None)
    }
}

pub fn parse_pkginfo<R: Read>(reader: R) -> Result<Pkg> {
    let reader = BufReader::new(reader);

    let mut name = None;
    let mut version = None;

    for line in reader.lines() {
        let line = line?;

        if let Some(value) = line.strip_prefix("pkgname = ") {
            name = Some(value.to_string());
        } else if let Some(value) = line.strip_prefix("pkgver = ") {
            version = Some(value.to_string());
        }
    }

    Ok(Pkg {
        name: name.context("Could not find pkgname in .PKGINFO")?,
        version: version.context("Could not find pkgver in .PKGINFO")?,
    })
}

pub fn parse_tar<R: Read>(reader: R) -> Result<Pkg> {
    let mut tar = tar::Archive::new(reader);
    for entry in tar.entries()? {
        let entry = entry?;
        let path = entry.path()?;
        if path.to_str() == Some(".PKGINFO") {
            return parse_pkginfo(entry);
        }
    }
    bail!("Failed to find .PKGINFO in package file")
}

pub fn parse<R: Read>(reader: R) -> Result<Pkg> {
    let mut reader = BufPeekReader::new(reader);
    match detect_compression(reader.peek())? {
        Compression::Xz => {
            let mut buf = Vec::new();
            lzma_rs::xz_decompress(&mut reader, &mut buf)?;
            parse_tar(&buf[..])
        }
        Compression::Zstd => {
            let decoder =
                StreamingDecoder::new_with_max_window_size(reader, LIBZSTD_MAX_WINDOW_SIZE)?;
            parse_tar(decoder)
        }
        Compression::None => parse_tar(reader),
    }
}

pub async fn set_pacman_verification_datetime(
    container: &Container,
    time: SystemTime,
) -> Result<()> {
    let path = format!("{GPG_CONF_DIR}{GPG_CONF_FILENAME}");
    let gpg_conf = container.cat(&path).await?;
    let mut gpg_conf = String::from_utf8(gpg_conf).context("Failed to parse gpg.conf as utf-8")?;
    if !gpg_conf.ends_with('\n') {
        gpg_conf.push('\n');
    }

    // check if a faked-system-time is already configured
    if let Some(line) = gpg_conf
        .lines()
        .find(|line| line.starts_with("faked-system-time"))
    {
        warn!("Container already defines a verification datetime: {line:?}");
        return Ok(());
    }

    let epoch = time
        .duration_since(UNIX_EPOCH)
        .with_context(|| anyhow!("Failed to derive unix epoch from time {time:?}"))?;
    writeln!(gpg_conf, "faked-system-time {}", epoch.as_secs())?;

    container
        .write_file(GPG_CONF_DIR, GPG_CONF_FILENAME, gpg_conf.as_bytes())
        .await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_pkg() -> Result<()> {
        let archive = {
            let data = br#"# Generated by makepkg 6.0.2
# using fakeroot version 1.31
pkgname = gcc
pkgbase = gcc
pkgver = 13.1.1-1
pkgdesc = The GNU Compiler Collection - C and C++ frontends
url = https://gcc.gnu.org
builddate = 1682849478
packager = Frederik Schwan <freswa@archlinux.org>
size = 190564290
arch = x86_64
license = GPL3
license = LGPL
license = FDL
license = custom
replaces = gcc-multilib
provides = gcc-multilib
depend = gcc-libs=13.1.1-1
depend = binutils>=2.28
depend = libmpc
depend = zstd
depend = libisl.so=23-64
optdepend = lib32-gcc-libs: for generating code for 32-bit ABI
makedepend = binutils
makedepend = doxygen
makedepend = gcc-ada
makedepend = gcc-d
makedepend = git
makedepend = lib32-glibc
makedepend = lib32-gcc-libs
makedepend = libisl
makedepend = libmpc
makedepend = python
makedepend = zstd
checkdepend = dejagnu
checkdepend = expect
checkdepend = inetutils
checkdepend = python-pytest
checkdepend = tcl
"#;

            let mut tar = tar::Builder::new(Vec::new());
            let mut header = tar::Header::new_gnu();
            header.set_path(".PKGINFO")?;
            header.set_size(data.len() as u64);
            header.set_cksum();
            tar.append(&header, &data[..])?;
            tar.into_inner()?
        };

        let mut buf = Vec::new();
        lzma_rs::xz_compress(&mut &archive[..], &mut buf)?;

        let pkg = parse(&buf[..]).context("Failed to parse package")?;
        assert_eq!(
            pkg,
            Pkg {
                name: "gcc".to_string(),
                version: "13.1.1-1".to_string(),
            }
        );

        Ok(())
    }
}
