//! Actions related to Debian packages.

#![allow(clippy::result_large_err)]

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
    action::{ActionError, Context},
    action_impl::{spawn, ActionImpl},
    qemu,
    util::{http_get_to_file, mkdir, UtilError},
};

const DEFAULT_SUITE: &str = "stable";
const SUBDIR: &str = "debian";

/// Download `deb` packages and their dependencies.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebGet {
    packages: Vec<String>,
    suite: Option<String>,
    cert: Option<String>,
}

impl DebGet {
    /// Create a new `DebGet` action.
    pub fn new(suite: &Option<String>, cert: &Option<String>, packages: &[String]) -> Self {
        Self {
            packages: packages.to_vec(),
            suite: suite.clone(),
            cert: cert.clone(),
        }
    }

    fn suite(&self) -> &str {
        self.suite.as_deref().unwrap_or(DEFAULT_SUITE)
    }

    fn packages_needed(
        &self,
        url: &str,
        suite: &str,
        arch: repo::Arch,
        deps_dir: &Path,
    ) -> Result<Vec<repo::Needed>, ActionError> {
        let debian = deps_dir.join(SUBDIR);
        mkdir(&debian).map_err(|err| DebError::Mkdir2(debian, err))?;

        let cached_in_release_file = deps_dir.join(SUBDIR).join("InRelease");
        let url = Url::parse(url).map_err(|err| DebError::UrlParse(url.to_string(), err))?;
        let mut certs = repo::Certs::empty();
        if let Some(cert) = &self.cert {
            certs.push(cert);
        } else {
            certs.push_for_suite(suite).map_err(DebError::Apt)?;
        }
        let debian_suite = repo::DebianSuite::new(&cached_in_release_file, url, suite, &certs)
            .map_err(DebError::Apt)?;

        let cached_packages_gz_file = deps_dir.join(SUBDIR).join("Packages.gz");
        let packages_file = debian_suite
            .packages_file(&cached_packages_gz_file, suite, arch)
            .map_err(DebError::Apt)?;
        let packages: HashMap<String, repo::Package> =
            HashMap::from_iter(packages_file.split("\n\n").filter(|s| !s.is_empty()).map(
                |stanza| match repo::Package::new(stanza) {
                    Ok(p) => (p.package.clone(), p),
                    Err(err) => panic!("can't parse package: {err}\nProblem stanza: {stanza:?}"),
                },
            ));

        Ok(debian_suite.needed(&self.packages, &packages))
    }

    fn download_needed(&self, deps_dir: &Path, needed: &[repo::Needed]) -> Result<(), ActionError> {
        for n in needed {
            let url =
                Url::parse(n.url()).map_err(|err| DebError::UrlParse(n.url().to_string(), err))?;
            let filename = deps_dir
                .join("debian")
                .join(format!("{}.deb", n.package_name()));
            http_get_to_file(url.as_str(), &filename)
                .map_err(|err| DebError::HttpGet(url, filename.clone(), err))?;
        }
        Ok(())
    }
}

impl ActionImpl for DebGet {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        use repo::*;

        const URL: &str = "http://deb.debian.org/debian";
        const ARCH: Arch = Arch::Amd64;

        let needed = self.packages_needed(URL, self.suite(), ARCH, context.deps_dir())?;
        self.download_needed(context.deps_dir(), &needed)?;
        context
            .runlog()
            .deb_get(crate::runlog::RunLogSource::PrePlan, &needed);
        Ok(())
    }
}

/// Install downloaded `deb` packages.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebInstall {}

impl ActionImpl for DebInstall {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let shell = "apt-get install -y /ci/deps/debian/*.deb";
        spawn(context, &["/bin/bash", "-c", shell])?;
        Ok(())
    }
}

/// Build a `deb` package.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deb {
    packages: Option<PathBuf>,
}

impl Deb {
    /// Create a new `Deb` action.
    pub fn new<P: AsRef<Path>>(packages: P) -> Self {
        Self {
            packages: Some(packages.as_ref().to_path_buf()),
        }
    }
}

impl ActionImpl for Deb {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        let packages = Path::new(qemu::ARTIFACTS_DIR)
            .join(self.packages.clone().unwrap_or(PathBuf::from(".")));
        if !packages.exists() {
            std::fs::create_dir(&packages).map_err(|err| DebError::mkdir(&packages, err))?;
        }
        let shell = format!(
            r#"#!/usr/bin/env bash
set -xeuo pipefail

echo "PATH at start: $PATH"
export PATH="/root/.cargo/bin:$PATH"
export CARGO_HOME=/ci/deps
export DEBEMAIL=liw@liw.fi
export DEBFULLNAME="Lars Wirzenius"
/bin/env

command -v cargo
command -v rustc

cargo --version
rustc --version

# Get name and version of source package.
name="$(dpkg-parsechangelog -SSource)"
version="$(dpkg-parsechangelog -SVersion)"

# Get upstream version: everything before the last dash.
uv="$(echo "$version" | sed 's/-[^-]*$//')"

# Files that will be created.
arch="$(dpkg --print-architecture)"
orig="../${{name}}_${{uv}}.orig.tar.xz"
deb="../${{name}}_${{version}}_${{arch}}.deb"
changes="../${{name}}_${{version}}_${{arch}}.changes"

# Create "upstream tarball".
git archive HEAD | xz >"$orig"

# Build package.
dpkg-buildpackage -us -uc

# Dump some information to make it easier to visually verify
# everything looks OK. Also, test the package with the lintian tool.

ls -l ..
for x in ../*.deb; do dpkg -c "$x"; done
# FIXME: disabled while this prevents radicle-native-ci deb from being built.
# lintian -i --allow-root --fail-on warning ../*.changes

# Move files to artifacts directory.
mv ../*_* {}
        "#,
            packages.display()
        );

        spawn(context, &["/bin/bash", "-c", &shell])?;

        Ok(())
    }
}

/// Errors from the `deb` action.
#[derive(Debug, thiserror::Error)]
pub enum DebError {
    /// Failed to create the artifacts directory for `deb` packages.
    #[error("could not create artifacts directory {0}")]
    Mkdir(PathBuf, #[source] std::io::Error),

    /// Failed to create the artifacts directory for `deb` packages.
    #[error("could not create artifacts directory {0}")]
    Mkdir2(PathBuf, #[source] crate::util::UtilError),

    /// APT repository error.
    #[error(transparent)]
    Apt(#[from] repo::AptRepoError),

    /// Parse URL.
    #[error("failed to parse URL {0:?}")]
    UrlParse(String, #[source] url::ParseError),

    /// Download file
    #[error("failed to download {0} to {1}")]
    HttpGet(Url, PathBuf, #[source] UtilError),
}

impl DebError {
    fn mkdir<P: Into<PathBuf>>(dirname: P, err: std::io::Error) -> Self {
        Self::Mkdir(dirname.into(), err)
    }
}

impl From<DebError> for ActionError {
    fn from(value: DebError) -> Self {
        Self::Deb(value)
    }
}

/// Represent a Debian package archive ("APT repository").
pub mod repo {
    use std::{
        collections::{HashMap, HashSet},
        io::Read,
        path::{Path, PathBuf},
        process::Command,
    };

    use rfc822_like::Deserializer;
    use serde::{Deserialize, Serialize};

    use clingwrap::runner::{CommandError, CommandRunner};
    use flate2::read::GzDecoder;
    use reqwest::StatusCode;
    use url::Url;

    use crate::util::{http_get_to_file, UtilError};

    const DEBIAN_TRIXIE_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEZ+Gq1RYJKwYBBAHaRw8BAQdARlh1OX84KPJRedAP/M7WxPFEthWypAp8nved
FhqaX0q0R0RlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEzL3RyaXhpZSkgPGRl
Ymlhbi1yZWxlYXNlQGxpc3RzLmRlYmlhbi5vcmc+iJYEExYIAD4WIQRBWH99uMd0
vM8TFBZ2L2egssOd5AUCZ+Gq1QIbAwUJDwmcAAULCQgHAgYVCgkICwIEFgIDAQIe
AQIXgAAKCRB2L2egssOd5DjVAP49S/e9VAtn9ip2DXj5zx87MpUXnLHAhT/LsJ7J
odnKoQEA8murEqdcC0pPsAA6Gmev/lz0MWUyfe5Y6DMB6t16FQuIdQQTFgoAHRYh
BMphnWWnKnut/JbSgBlkGKrrdMihBQJn4atmAAoJEBlkGKrrdMihxnAA/3i7WOUU
Dyea6tABVGfFKfHj6yAbfcKVr5GL0WSOQiXNAQC2YTKoTfCY/WOZnCwEHebpM3Mr
6+A8lenj8pFzm8mFAokCMgQQAQoAHRYhBHIDYw4sjnJyUWhP68XOXcLFQs1ZBQJn
4v59AAoJEMXOXcLFQs1ZfdkP90fbXOydWNb1iXwu/vaUjxSx5Nk0Zwkjj7Pi74PZ
Ifd9c0Luf/j7dEHuJOzkOKvkrpTYeQN8Ms9ITVTMeNSOoQn8tnYsxhHqHUcI9ym/
vJ6liIsE2K95gwH2mOQ24ot59pMyQX7sXE4DuQqlrEW1JRemKs7WJB2E/4I7smqG
3+/mqXAoHKkpFBMm1v9tNVx1Tp3ER6C4VFszONlmX0NLB5If+wSits1LnsVgBK1D
Hd3Nxh78YQL0W2xBCOfsHryaKkLW6tp1efTpMHfe4lDEgTFvEWaO3NlPEeD67uxA
KpEG1b/CzBqx+Og1RT+0iZkW9oXYoKJI0Vx0HlYeMrhuE+Q0Fqy/nZYcWhq3y6Tu
GC9J+hyB7D3Z7ne48zNGaAU4x7+6pvkmlBWnNwPH/IlGbexa/F8o+rU0AWrvYCVR
cbbcSsgkq1Dl/Xjbvlx9gwkKnhacbQ5VUOA2Zkp6oBn/pFD1W8xE0X4dNHcM3J5k
CAeZuszjPIZXvwuvAjaOE7ZxKvGDlQTWK8zR5h9tvcwMfVND+EqK0qfDGDMtJqu/
W0lSJj3G/FMO3aGn0Ks6b7cmjbCGjmn7WBGJT2PvH4A4ml1qopWy7Ont6zt1+Cll
y/0Uf75AamJSrAAYFFIJlcpWH/7NRW2L+n1FoIOv5xj5h675uHh/WFlPTNQqrRaP
p1o=
=k1cs
-----END PGP PUBLIC KEY BLOCK-----
"#;

    const DEBIAN_BOOKWORM_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: B8B8 0B5B 623E AB6A D877  5C45 B7C5 D7D6 3509 47F8
Comment: Debian Archive Automatic Signing Key (12/bookworm) <ftp

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
NlB2T6bKGcYGqL7L
=AKf0
-----END PGP PUBLIC KEY BLOCK-----
"#;

    pub(super) struct Certs {
        certs: Vec<String>,
    }

    impl Certs {
        pub fn empty() -> Self {
            Self { certs: vec![] }
        }

        pub fn push(&mut self, cert: impl Into<String>) {
            self.certs.push(cert.into());
        }

        pub fn push_for_suite(&mut self, suite: &str) -> Result<(), AptRepoError> {
            let cert = match suite {
                "trixie" => DEBIAN_TRIXIE_CERT,
                "bookworm" => DEBIAN_BOOKWORM_CERT,
                _ => return Err(AptRepoError::UnknownRelease(suite.into())),
            };
            self.push(cert);
            Ok(())
        }
    }

    impl Default for Certs {
        fn default() -> Self {
            Self {
                certs: vec![DEBIAN_TRIXIE_CERT.to_string()],
            }
        }
    }

    pub(super) struct DebianSuite {
        base_url: Url,
        in_release: InRelease,
    }

    impl DebianSuite {
        pub fn new(
            cached_in_release: &Path,
            base_url: Url,
            suite: &str,
            allowed_certs: &Certs,
        ) -> Result<Self, AptRepoError> {
            let in_release_url = Url::parse(&format!("{}/dists/{suite}/InRelease", base_url))?;

            let signed = http_get_to_file(in_release_url.as_str(), cached_in_release)?;
            let in_release = inline_verify(&signed, allowed_certs)?;

            Ok(Self {
                base_url,
                in_release: InRelease::new(&in_release)?,
            })
        }

        #[allow(clippy::unwrap_used)]
        pub fn packages_file(
            &self,
            cached_file: &Path,
            suite: &str,
            arch: Arch,
        ) -> Result<String, AptRepoError> {
            for line in self
                .in_release
                .sha256
                .lines()
                .filter(|line| !line.is_empty())
            {
                let mut words = line.split_ascii_whitespace();
                let checksum = words.next().unwrap();
                let _length = words.next().unwrap();
                let filename = words.next().unwrap();
                let wanted = format!("main/binary-{arch}/Packages.gz");
                if filename == wanted {
                    let url = format!("{}/dists/{suite}/{filename}", self.base_url);
                    let data = http_get_to_file(&url, cached_file)?;
                    let actual = sha256::digest(&data);

                    if actual == checksum {
                        let mut gz = GzDecoder::new(&*data);
                        let mut packages = vec![];
                        gz.read_to_end(&mut packages)
                            .map_err(|source| AptRepoError::Inflate { source })?;
                        let data = String::from_utf8_lossy(&packages).to_string();
                        return Ok(data);
                    } else {
                        panic!("bad checksum");
                    }
                }
            }
            Err(AptRepoError::NoPackagesGz)
        }

        pub fn needed(
            &self,
            package_names: &[String],
            packages: &HashMap<String, Package>,
        ) -> Vec<Needed> {
            let mut wanted = HashMap::new();
            let mut todo: HashSet<String> =
                HashSet::from_iter(package_names.iter().map(|s| s.to_string()));
            while !todo.is_empty() {
                let mut new = vec![];
                for name in todo.drain() {
                    if !wanted.contains_key(&name) {
                        if let Some(p) = packages.get(&name) {
                            wanted.insert(name.to_string(), p.clone());
                            for need in p.needs() {
                                new.push(need);
                            }
                        }
                    }
                }
                for name in new {
                    todo.insert(name);
                }
            }

            wanted
                .values()
                .map(|p| {
                    let url = format!("{}/{}", self.base_url, p.filename);
                    Needed::new(p.package.clone(), url)
                })
                .collect()
        }
    }

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "PascalCase")]
    struct InRelease {
        #[serde(rename = "SHA256")]
        sha256: String,
    }

    impl InRelease {
        fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
            Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
        }
    }

    #[derive(Debug, Clone, Deserialize)]
    #[serde(rename_all = "PascalCase")]
    pub(super) struct Package {
        pub package: String,
        pre_depends: Option<String>,
        depends: Option<String>,
        filename: String,
    }

    impl Package {
        pub fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
            Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
        }

        pub fn needs(&self) -> Vec<String> {
            let mut needs = vec![];
            if let Some(x) = &self.pre_depends {
                self.parse_needs(&mut needs, x);
            }
            if let Some(x) = &self.depends {
                self.parse_needs(&mut needs, x);
            }
            needs
        }

        #[allow(clippy::unwrap_used)]
        fn parse_needs(&self, needs: &mut Vec<String>, x: &str) {
            for item in x.split(',') {
                let item = item.split_ascii_whitespace().next().unwrap();
                needs.push(item.to_string());
            }
        }
    }

    /// A deb dependency that needs to be downloaded.
    #[derive(Debug, Clone, Deserialize, Serialize)]
    pub struct Needed {
        package_name: String,
        url: String,
    }

    impl Needed {
        fn new(package_name: impl Into<String>, url: impl Into<String>) -> Self {
            Self {
                package_name: package_name.into(),
                url: url.into(),
            }
        }

        /// Name of package.
        pub fn package_name(&self) -> &str {
            &self.package_name
        }

        /// URL from where to download package.
        pub fn url(&self) -> &str {
            &self.url
        }
    }

    #[derive(Debug, Copy, Clone)]
    pub(super) enum Arch {
        Amd64,
    }

    impl std::fmt::Display for Arch {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let arch = match self {
                Self::Amd64 => "amd64",
            };
            write!(f, "{arch}")
        }
    }

    fn inline_verify(data: &[u8], allowed_certs: &Certs) -> Result<String, AptRepoError> {
        fn try_sop(sop: &str, data: &[u8], allowed_certs: &Certs) -> Result<String, AptRepoError> {
            let mut cmd = Command::new(sop);
            cmd.arg("inline-verify");

            let tmp = tempfile::tempdir().map_err(|source| AptRepoError::TempDir { source })?;
            for (i, cert) in allowed_certs.certs.iter().enumerate() {
                let filename = tmp.path().join(format!("cert-{i}"));
                std::fs::write(&filename, cert.as_bytes())
                    .map_err(|source| AptRepoError::CertFile { source })?;
                cmd.arg(filename);
            }

            let mut runner = CommandRunner::new(cmd);
            runner.feed_stdin(data);
            runner.capture_stdout();
            let output = runner
                .execute()
                .map_err(|source| AptRepoError::InlineVerify { source })?;
            String::from_utf8(output.stdout)
                .map_err(|source| AptRepoError::InReleaseUtf8 { source })
        }

        try_sop("rsop", data, allowed_certs).or_else(|_| try_sop("sqop", data, allowed_certs))
    }

    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum AptRepoError {
        #[error("failed to fetch InRelease file for {suite} from {url}")]
        FetchInRelease {
            suite: String,
            url: Url,
            source: reqwest::Error,
        },

        #[error("failed to retrieve InRelease text from HTTP response")]
        InReleaseText { source: reqwest::Error },

        #[error(transparent)]
        Join(#[from] url::ParseError),

        #[error("failed to verify InRelease inline signature")]
        InlineVerify { source: CommandError },

        #[error("InRelease file is not UTF8")]
        InReleaseUtf8 { source: std::string::FromUtf8Error },

        #[error("failed to create temporary directory for verifying InRelease signature")]
        TempDir { source: std::io::Error },

        #[error("failed to write temparary file for verifying InRelease signature")]
        CertFile { source: std::io::Error },

        #[error("InRelease file doesn't have a single stanza")]
        NoStanzas,

        #[error("InRelease does not have a SHA256 field")]
        NoSha256Field,

        #[error("InRelease SHA256 field line lacks {0}")]
        Sha256Field(&'static str),

        #[error("failed to download Packages.gz file")]
        FetchPackagesGz { source: reqwest::Error },

        #[error("failed to get Packages.gz file from HTTP request")]
        PackagesGz { source: reqwest::Error },

        #[error("failed to decompress Packages.gz file")]
        Inflate { source: std::io::Error },

        #[error("Packages file is not UTF8")]
        PackagesUtf8 { source: std::string::FromUtf8Error },

        #[error("Packages.gz has the wrong checksum")]
        PackagesChecksum,

        #[error("failed to find desired Packages.gz in InRelease file")]
        NoPackagesGz,

        #[error(transparent)]
        HttpGet(#[from] crate::action_impl::HttpGetError),

        #[error("failed to format timestamp")]
        TimeFormat { source: time::error::Format },

        #[error("failed to create HTTP client")]
        ClientBuild(#[source] reqwest::Error),

        #[error("failed to build a reqwest client")]
        Client(#[source] reqwest::Error),

        #[error("failed to build a reqwest request")]
        BuildRequest(#[source] reqwest::Error),

        #[error("failed to GET URL {0:?}")]
        Get(String, reqwest::Error),

        #[error("failed to get body of response from {0:?}")]
        GetBody(String, reqwest::Error),

        #[error("failure getting file with HTTP GET: status code {0}")]
        UnwantedStatus(StatusCode),

        #[error("failed to write file {filename}")]
        Write {
            filename: PathBuf,
            source: std::io::Error,
        },

        #[error("failed to read file {filename}")]
        Read {
            filename: PathBuf,
            source: std::io::Error,
        },

        #[error("entry for {package} in Packages file lacks Filename field")]
        NoFilename { package: String },

        #[error(transparent)]
        Util(#[from] UtilError),

        #[error("failed to parse URL {0:?}")]
        UrlParse(String, #[source] url::ParseError),

        #[error(transparent)]
        Rfc822Like(#[from] rfc822_like::de::Error),

        #[error("do not know archive signing key for release {0}")]
        UnknownRelease(String),
    }
}
