use crate::compression::{self, CompressedWith};
use crate::errors::*;
use crate::plot::{self, Artifacts, PkgRef};
use indexmap::IndexMap;
use std::fmt;
use std::io::prelude::*;
use std::str;
use warp::hyper::body::Bytes;

type PatchPkgDatabaseConfig = plot::PatchPkgDatabaseConfig<Vec<String>>;

#[derive(Debug, PartialEq, Eq, Default)]
pub struct Pkg {
    name: String,
    version: String,
    map: IndexMap<String, Vec<String>>,
}

impl PkgRef for Pkg {
    fn name(&self) -> &str {
        &self.name
    }

    fn version(&self) -> &str {
        &self.version
    }
}

impl fmt::Display for Pkg {
    fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
        for (key, values) in &self.map {
            // we always expect at least one value
            if let Some((first, extra)) = values.split_first() {
                write!(w, "{key}")?;

                match first.as_str() {
                    "" => writeln!(w, ":")?,
                    " " => writeln!(w, ": ")?,
                    _ => writeln!(w, ": {first}")?,
                }

                for value in extra {
                    writeln!(w, " {value}")?;
                }
            }
        }

        Ok(())
    }
}

impl Pkg {
    fn from_map<'a>(map: &'a IndexMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
        map.get(key)?.first().map(String::as_str)
    }

    pub fn parse(mut bytes: &[u8]) -> Result<(Pkg, &[u8])> {
        let mut fields = IndexMap::<String, Vec<String>>::new();
        while let Some(idx) = memchr::memchr(b'\n', bytes) {
            let line = str::from_utf8(&bytes[..idx])
                .context("Failed to utf-8 decode line in package index")?;
            bytes = &bytes[idx + 1..];

            if line.is_empty() {
                let mut map = IndexMap::new();
                std::mem::swap(&mut fields, &mut map);

                let name = Self::from_map(&map, "Package")
                    .context("Missing package name")?
                    .to_string();
                let version = Self::from_map(&map, "Version")
                    .context("Missing package version")?
                    .to_string();

                let pkg = Pkg { name, version, map };
                trace!("Found pkg in index: {:?}", pkg);
                return Ok((pkg, bytes));
            } else if let Some(line) = line.strip_prefix(' ') {
                let (_, last) = fields
                    .last_mut()
                    .context("Can't continue non-existant previous line")?;
                last.push(line.to_string());
            } else if let Some((key, value)) = line.split_once(": ") {
                let value = if value.is_empty() {
                    " ".to_string()
                } else {
                    value.to_string()
                };
                fields.entry(key.to_string()).or_default().push(value);
            } else if let Some(key) = line.strip_suffix(": ") {
                fields
                    .entry(key.to_string())
                    .or_default()
                    .push(" ".to_string());
            } else if let Some(key) = line.strip_suffix(':') {
                fields
                    .entry(key.to_string())
                    .or_default()
                    .push("".to_string());
            } else {
                bail!("Unrecognized input: {:?}", line);
            }
        }

        bail!("Unexpected end of index, trailing fields: {:?}", fields);
    }

    pub fn delete_key(&mut self, key: &str) -> Result<()> {
        if key == "Package" {
            bail!("Can't delete `Package` from debian package");
        }
        if key == "Version" {
            bail!("Can't delete `Version` from debian package");
        }
        debug!("Removing {:?} from package", key);
        self.map.shift_remove(key);
        Ok(())
    }

    pub fn set_key(&mut self, key: String, values: Vec<String>) -> Result<()> {
        let Some(first) = values.first() else {
            return self.delete_key(&key);
        };

        if key == "Package" {
            debug!("Updating name to: {:?}", first);
            self.name = first.to_string();
        }
        if key == "Version" {
            debug!("Updating version to: {:?}", first);
            self.version = first.to_string();
        }

        debug!("Setting {:?} to {:?}", key, values);
        self.map.insert(key.to_string(), values);
        Ok(())
    }

    pub fn add_values(&mut self, key: &str, values: &[&str]) -> Result<()> {
        let values = values.iter().map(|x| String::from(*x)).collect();
        self.set_key(key.to_string(), values)?;
        Ok(())
    }

    pub fn get_key_str(&self, key: &str) -> Option<&str> {
        let values = self.map.get(key)?;
        values.first().map(|s| s.as_str())
    }
}

pub fn patch<W: Write>(
    config: &PatchPkgDatabaseConfig,
    compression: Option<CompressedWith>,
    artifacts: &Artifacts,
    bytes: &[u8],
    out: &mut W,
) -> Result<()> {
    let detected_compression = compression::detect_compression(bytes);

    let mut out = compression::stream_compress(out, compression.unwrap_or(detected_compression))?;
    let mut reader = compression::stream_decompress(bytes, detected_compression)?;
    let mut bytes = Vec::new();
    reader.read_to_end(&mut bytes)?;
    let mut bytes = &bytes[..];

    while !bytes.is_empty() {
        let (mut pkg, remaining) = Pkg::parse(bytes).context("Failed to parse package index")?;
        bytes = remaining;

        if config.is_excluded(&pkg) {
            debug!("Filtering package: {:?}", pkg.name());
            continue;
        }

        if let Some(artifact) = config.artifact(&pkg) {
            let artifact = artifacts
                .get(artifact)
                .with_context(|| anyhow!("Referencing undefined artifact: {:?}", artifact))?;

            pkg.set_key("Size".to_string(), vec![artifact.len().to_string()])
                .context("Failed to patch package")?;

            pkg.set_key("MD5sum".to_string(), vec![artifact.md5().to_string()])
                .context("Failed to patch package")?;

            pkg.set_key("SHA1".to_string(), vec![artifact.sha1().to_string()])
                .context("Failed to patch package")?;

            pkg.set_key("SHA256".to_string(), vec![artifact.sha256().to_string()])
                .context("Failed to patch package")?;

            pkg.set_key("SHA512".to_string(), vec![artifact.sha512().to_string()])
                .context("Failed to patch package")?;
        }

        if let Some(patch) = config.get_patches(&pkg) {
            debug!("Patching package {:?} with {:?}", pkg.name(), patch);
            for (key, value) in patch {
                pkg.set_key(key.to_string(), value.clone())
                    .context("Failed to patch package")?;
            }
        }

        writeln!(out, "{pkg}")?;
    }

    Ok(())
}

pub fn modify_response(
    config: &PatchPkgDatabaseConfig,
    artifacts: &Artifacts,
    bytes: &[u8],
) -> Result<Bytes> {
    let mut out = Vec::new();
    patch(config, None, artifacts, bytes, &mut out)?;
    Ok(Bytes::from(out))
}

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

    #[test]
    pub fn test_parse_pkg() -> Result<()> {
        let data = b"Package: sniffglue
Source: rust-sniffglue (0.11.1-6)
Version: 0.11.1-6+b1
Installed-Size: 2812
Maintainer: Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>
Architecture: amd64
Depends: libc6 (>= 2.18), libgcc-s1 (>= 4.2), libpcap0.8 (>= 1.5.1), libseccomp2 (>= 0.0.0~20120605)
Description: Secure multithreaded packet sniffer
Multi-Arch: allowed
Built-Using: rust-nix (= 0.19.0-1), rust-pktparse (= 0.5.0-1), rust-seccomp-sys (= 0.1.3-1), rustc (= 1.48.0+dfsg1-2)
Description-md5: bf43056627c9a77e6e736a953362be2c
X-Cargo-Built-Using: gcc-10 (= 10.2.1-6), rust-aho-corasick (= 0.7.10-1), rust-ansi-term (= 0.12.1-1), rust-atty (= 0.2.14-2), rust-backtrace (= 0.3.44-6), rust-backtrace-sys (= 0.1.35-1), rust-base64 (= 0.12.1-1), rust-bitflags (= 1.2.1-1), rust-block-buffer (= 0.9.0-4), rust-block-padding (= 0.2.1-1), rust-byteorder (= 1.3.4-1), rust-cfg-if-0.1 (= 0.1.10-2), rust-cfg-if (= 1.0.0-1), rust-clap (= 2.33.3-1), rust-cpuid-bool (= 0.1.2-4), rust-dhcp4r (= 0.2.0-1), rust-digest (= 0.9.0-1), rust-dirs (= 3.0.1-1), rust-dirs-sys (= 0.3.5-1), rust-dns-parser (= 0.8.0-1), rust-enum-primitive (= 0.1.1-1), rust-env-logger (= 0.7.1-2), rust-failure (= 0.1.7-1), rust-generic-array (= 0.14.4-1), rust-humantime (= 2.0.0-1), rust-itoa (= 0.4.3-1), rust-lazy-static (= 1.4.0-1), rust-lexical-core (= 0.4.3-2), rust-libc (= 0.2.80-1), rust-log (= 0.4.11-2), rust-memchr (= 2.3.3-1), rust-nix (= 0.19.0-1), rust-nom (= 5.0.1-4), rust-num-cpus (= 1.13.0-1), rust-num-traits (= 0.2.14-1), rust-opaque-debug (= 0.3.0-1), rust-pcap-sys (= 0.1.3-2), rust-phf (= 0.8.0-2), rust-phf-shared (= 0.8.0-1), rust-pktparse (= 0.5.0-1), rust-quick-error (= 1.2.3-1), rust-reduce (= 0.1.1-1), rust-regex (= 1.3.7-1), rust-regex-syntax (= 0.6.17-1), rust-rustc-demangle (= 0.1.16-4), rust-rusticata-macros (= 2.0.4-1), rust-ryu (= 1.0.2-1), rust-seccomp-sys (= 0.1.3-1), rust-serde (= 1.0.106-1), rust-serde-json (= 1.0.41-1), rust-sha2 (= 0.9.2-2), rust-siphasher (= 0.3.1-1), rust-stackvector (= 1.0.6-3), rust-static-assertions (= 1.1.0-1), rust-strsim (= 0.9.3-1), rust-structopt (= 0.3.20-1), rust-strum (= 0.19.2-1), rust-syscallz (= 0.15.0-1), rust-termcolor (= 1.1.0-1), rust-textwrap (= 0.11.0-1), rust-thread-local (= 1.0.1-1), rust-time (= 0.1.42-1), rust-tls-parser (= 0.9.2-3), rust-toml (= 0.5.5-1), rust-typenum (= 1.12.0-1), rust-unicode-width (= 0.1.8-1), rust-unreachable (= 1.0.0-1), rust-users (= 0.10.0-1), rust-vec-map (= 0.8.1-2), rust-void (= 1.0.2-1), rustc (= 1.48.0+dfsg1-2)
Section: net
Priority: optional
Filename: pool/main/r/rust-sniffglue/sniffglue_0.11.1-6+b1_amd64.deb
Size: 789284
MD5sum: 9cf8663a1276fee4e54aeea078186ca3
SHA256: 406a3de1f6357554e1606f4dd7c7a8d1360d815cf1453d9e72cee36d92eba7c7

";
        let (pkg, remaining) = Pkg::parse(data)?;
        assert_eq!(remaining, b"");

        let mut expected = Pkg::default();
        expected.add_values("Package", &["sniffglue"])?;
        expected.add_values("Source", &["rust-sniffglue (0.11.1-6)"])?;
        expected.add_values("Version", &["0.11.1-6+b1"])?;
        expected.add_values("Installed-Size", &["2812"])?;
        expected.add_values(
            "Maintainer",
            &["Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>"],
        )?;
        expected.add_values("Architecture", &["amd64"])?;
        expected.add_values("Depends", &["libc6 (>= 2.18), libgcc-s1 (>= 4.2), libpcap0.8 (>= 1.5.1), libseccomp2 (>= 0.0.0~20120605)"])?;
        expected.add_values("Description", &["Secure multithreaded packet sniffer"])?;
        expected.add_values("Multi-Arch", &["allowed"])?;
        expected.add_values("Built-Using", &["rust-nix (= 0.19.0-1), rust-pktparse (= 0.5.0-1), rust-seccomp-sys (= 0.1.3-1), rustc (= 1.48.0+dfsg1-2)"])?;
        expected.add_values("Description-md5", &["bf43056627c9a77e6e736a953362be2c"])?;
        expected.add_values("X-Cargo-Built-Using", &["gcc-10 (= 10.2.1-6), rust-aho-corasick (= 0.7.10-1), rust-ansi-term (= 0.12.1-1), rust-atty (= 0.2.14-2), rust-backtrace (= 0.3.44-6), rust-backtrace-sys (= 0.1.35-1), rust-base64 (= 0.12.1-1), rust-bitflags (= 1.2.1-1), rust-block-buffer (= 0.9.0-4), rust-block-padding (= 0.2.1-1), rust-byteorder (= 1.3.4-1), rust-cfg-if-0.1 (= 0.1.10-2), rust-cfg-if (= 1.0.0-1), rust-clap (= 2.33.3-1), rust-cpuid-bool (= 0.1.2-4), rust-dhcp4r (= 0.2.0-1), rust-digest (= 0.9.0-1), rust-dirs (= 3.0.1-1), rust-dirs-sys (= 0.3.5-1), rust-dns-parser (= 0.8.0-1), rust-enum-primitive (= 0.1.1-1), rust-env-logger (= 0.7.1-2), rust-failure (= 0.1.7-1), rust-generic-array (= 0.14.4-1), rust-humantime (= 2.0.0-1), rust-itoa (= 0.4.3-1), rust-lazy-static (= 1.4.0-1), rust-lexical-core (= 0.4.3-2), rust-libc (= 0.2.80-1), rust-log (= 0.4.11-2), rust-memchr (= 2.3.3-1), rust-nix (= 0.19.0-1), rust-nom (= 5.0.1-4), rust-num-cpus (= 1.13.0-1), rust-num-traits (= 0.2.14-1), rust-opaque-debug (= 0.3.0-1), rust-pcap-sys (= 0.1.3-2), rust-phf (= 0.8.0-2), rust-phf-shared (= 0.8.0-1), rust-pktparse (= 0.5.0-1), rust-quick-error (= 1.2.3-1), rust-reduce (= 0.1.1-1), rust-regex (= 1.3.7-1), rust-regex-syntax (= 0.6.17-1), rust-rustc-demangle (= 0.1.16-4), rust-rusticata-macros (= 2.0.4-1), rust-ryu (= 1.0.2-1), rust-seccomp-sys (= 0.1.3-1), rust-serde (= 1.0.106-1), rust-serde-json (= 1.0.41-1), rust-sha2 (= 0.9.2-2), rust-siphasher (= 0.3.1-1), rust-stackvector (= 1.0.6-3), rust-static-assertions (= 1.1.0-1), rust-strsim (= 0.9.3-1), rust-structopt (= 0.3.20-1), rust-strum (= 0.19.2-1), rust-syscallz (= 0.15.0-1), rust-termcolor (= 1.1.0-1), rust-textwrap (= 0.11.0-1), rust-thread-local (= 1.0.1-1), rust-time (= 0.1.42-1), rust-tls-parser (= 0.9.2-3), rust-toml (= 0.5.5-1), rust-typenum (= 1.12.0-1), rust-unicode-width (= 0.1.8-1), rust-unreachable (= 1.0.0-1), rust-users (= 0.10.0-1), rust-vec-map (= 0.8.1-2), rust-void (= 1.0.2-1), rustc (= 1.48.0+dfsg1-2)"])?;
        expected.add_values("Section", &["net"])?;
        expected.add_values("Priority", &["optional"])?;
        expected.add_values(
            "Filename",
            &["pool/main/r/rust-sniffglue/sniffglue_0.11.1-6+b1_amd64.deb"],
        )?;
        expected.add_values("Size", &["789284"])?;
        expected.add_values("MD5sum", &["9cf8663a1276fee4e54aeea078186ca3"])?;
        expected.add_values(
            "SHA256",
            &["406a3de1f6357554e1606f4dd7c7a8d1360d815cf1453d9e72cee36d92eba7c7"],
        )?;

        assert_eq!(pkg, expected);
        assert_eq!(format!("{pkg}\n").as_bytes(), data);
        Ok(())
    }

    #[test]
    pub fn test_parse_src_pkg() -> Result<()> {
        let data = b"Package: rust-sniffglue
Binary: librust-sniffglue-dev, sniffglue
Version: 0.11.1-6
Maintainer: Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>
Uploaders: kpcyrd <git@rxv.cc>
Build-Depends: debhelper (>= 11), dh-cargo (>= 18), cargo:native, rustc:native, libstd-rust-dev, librust-ansi-term-0.12+default-dev, librust-atty-0.2+default-dev, librust-base64-0.12+default-dev, librust-dhcp4r-0.2+default-dev, librust-dirs-3+default-dev, librust-dns-parser-0.8+default-dev, librust-env-logger-0.7+default-dev, librust-failure-0.1+default-dev, librust-libc-0.2+default-dev, librust-log-0.4+default-dev, librust-nix-0.19+default-dev, librust-nom-5+default-dev, librust-num-cpus-1+default-dev (>= 1.6-~~), librust-pcap-sys-0.1+default-dev (>= 0.1.3-~~), librust-pktparse-0.5+default-dev, librust-pktparse-0.5+serde-dev, librust-reduce-0.1+default-dev (>= 0.1.1-~~), librust-serde-1+default-dev, librust-serde-derive-1+default-dev, librust-serde-json-1+default-dev, librust-sha2-0.9+default-dev, librust-structopt-0.3+default-dev, librust-syscallz-0.15+default-dev, librust-tls-parser-0.9+default-dev, librust-toml-0.5+default-dev, librust-users-0.10+default-dev
Architecture: any
Standards-Version: 4.2.0
Format: 3.0 (quilt)
Files:
 ad1fcb8ad604c9459b0c91c8391a6510 3044 rust-sniffglue_0.11.1-6.dsc
 13b61029622b872d22b529f40917b79b 143493 rust-sniffglue_0.11.1.orig.tar.gz
 5ca448f901ce5a5536066ce3c8b289d2 4624 rust-sniffglue_0.11.1-6.debian.tar.xz
Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/sniffglue
Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/sniffglue]
Checksums-Sha256:
 d03c20d775a88fe8b06252281fb18119225f270795f6972687d2cf39c280a2db 3044 rust-sniffglue_0.11.1-6.dsc
 1f6957f4a803e171690bb9cbe8260f40c84b14e0eca7ba8c1cc31f6b47bbe9ab 143493 rust-sniffglue_0.11.1.orig.tar.gz
 69d7feab89c8d1c444a2f5a118dc41a3cd67a6539e07325d6a71b844da37a0a3 4624 rust-sniffglue_0.11.1-6.debian.tar.xz
Package-List: 
 librust-sniffglue-dev deb net optional arch=any
 sniffglue deb net optional arch=any
Testsuite: autopkgtest
Testsuite-Triggers: dh-cargo
Directory: pool/main/r/rust-sniffglue
Priority: extra
Section: misc

";
        let (pkg, remaining) = Pkg::parse(data)?;
        assert_eq!(remaining, b"");

        let mut expected = Pkg::default();
        expected.add_values("Package", &["rust-sniffglue"])?;
        expected.add_values("Binary", &["librust-sniffglue-dev, sniffglue"])?;
        expected.add_values("Version", &["0.11.1-6"])?;
        expected.add_values(
            "Maintainer",
            &["Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>"],
        )?;
        expected.add_values("Uploaders", &["kpcyrd <git@rxv.cc>"])?;
        expected.add_values("Build-Depends", &["debhelper (>= 11), dh-cargo (>= 18), cargo:native, rustc:native, libstd-rust-dev, librust-ansi-term-0.12+default-dev, librust-atty-0.2+default-dev, librust-base64-0.12+default-dev, librust-dhcp4r-0.2+default-dev, librust-dirs-3+default-dev, librust-dns-parser-0.8+default-dev, librust-env-logger-0.7+default-dev, librust-failure-0.1+default-dev, librust-libc-0.2+default-dev, librust-log-0.4+default-dev, librust-nix-0.19+default-dev, librust-nom-5+default-dev, librust-num-cpus-1+default-dev (>= 1.6-~~), librust-pcap-sys-0.1+default-dev (>= 0.1.3-~~), librust-pktparse-0.5+default-dev, librust-pktparse-0.5+serde-dev, librust-reduce-0.1+default-dev (>= 0.1.1-~~), librust-serde-1+default-dev, librust-serde-derive-1+default-dev, librust-serde-json-1+default-dev, librust-sha2-0.9+default-dev, librust-structopt-0.3+default-dev, librust-syscallz-0.15+default-dev, librust-tls-parser-0.9+default-dev, librust-toml-0.5+default-dev, librust-users-0.10+default-dev"])?;
        expected.add_values("Architecture", &["any"])?;
        expected.add_values("Standards-Version", &["4.2.0"])?;
        expected.add_values("Format", &["3.0 (quilt)"])?;
        expected.add_values(
            "Files",
            &[
                "",
                "ad1fcb8ad604c9459b0c91c8391a6510 3044 rust-sniffglue_0.11.1-6.dsc",
                "13b61029622b872d22b529f40917b79b 143493 rust-sniffglue_0.11.1.orig.tar.gz",
                "5ca448f901ce5a5536066ce3c8b289d2 4624 rust-sniffglue_0.11.1-6.debian.tar.xz",
            ],
        )?;
        expected.add_values(
            "Vcs-Browser",
            &["https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/sniffglue"],
        )?;
        expected.add_values(
            "Vcs-Git",
            &["https://salsa.debian.org/rust-team/debcargo-conf.git [src/sniffglue]"],
        )?;
        expected.add_values("Checksums-Sha256", &["", "d03c20d775a88fe8b06252281fb18119225f270795f6972687d2cf39c280a2db 3044 rust-sniffglue_0.11.1-6.dsc", "1f6957f4a803e171690bb9cbe8260f40c84b14e0eca7ba8c1cc31f6b47bbe9ab 143493 rust-sniffglue_0.11.1.orig.tar.gz", "69d7feab89c8d1c444a2f5a118dc41a3cd67a6539e07325d6a71b844da37a0a3 4624 rust-sniffglue_0.11.1-6.debian.tar.xz"])?;
        expected.add_values(
            "Package-List",
            &[
                " ",
                "librust-sniffglue-dev deb net optional arch=any",
                "sniffglue deb net optional arch=any",
            ],
        )?;
        expected.add_values("Testsuite", &["autopkgtest"])?;
        expected.add_values("Testsuite-Triggers", &["dh-cargo"])?;
        expected.add_values("Directory", &["pool/main/r/rust-sniffglue"])?;
        expected.add_values("Priority", &["extra"])?;
        expected.add_values("Section", &["misc"])?;

        assert_eq!(pkg, expected);
        assert_eq!(format!("{pkg}\n").as_bytes(), data);
        Ok(())
    }
}
