//! Changes files

use rowan::ast::AstNode;

/// Changes file
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Changes(deb822_lossless::Paragraph);

/// Errors that can occur when parsing a Changes file.
#[derive(Debug)]
pub enum ParseError {
    /// An error occurred while parsing a Deb822 file.
    Deb822(deb822_lossless::Error),

    /// No paragraphs were found in the file.
    NoParagraphs,

    /// Multiple paragraphs were found in the file.
    MultipleParagraphs,
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::Deb822(e) => write!(f, "{}", e),
            Self::NoParagraphs => write!(f, "no paragraphs found"),
            Self::MultipleParagraphs => write!(f, "multiple paragraphs found"),
        }
    }
}

impl std::error::Error for ParseError {}

impl From<deb822_lossless::Error> for ParseError {
    fn from(e: deb822_lossless::Error) -> Self {
        Self::Deb822(e)
    }
}

/// A file in a source package.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct File {
    /// MD5 checksum of the file.
    pub md5sum: String,
    /// Size of the file in bytes.
    pub size: usize,
    /// Section the file belongs to.
    pub section: String,
    /// Priority of the file.
    pub priority: crate::Priority,
    /// Filename of the file.
    pub filename: String,
}

impl std::fmt::Display for File {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{} {} {} {} {}",
            self.md5sum, self.size, self.section, self.priority, self.filename
        )
    }
}

impl std::str::FromStr for File {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.split_whitespace();
        let md5sum = parts.next().ok_or(())?;
        let size = parts.next().ok_or(())?.parse().map_err(|_| ())?;
        let section = parts.next().ok_or(())?.to_string();
        let priority = parts.next().ok_or(())?.parse().map_err(|_| ())?;
        let filename = parts.next().ok_or(())?.to_string();
        Ok(Self {
            md5sum: md5sum.to_string(),
            size,
            section,
            priority,
            filename,
        })
    }
}

impl Changes {
    /// Parse changes text, returning a Parse result
    ///
    /// Note: This expects a single paragraph, not a full deb822 document
    pub fn parse(text: &str) -> deb822_lossless::Parse<Changes> {
        let deb822_parse = deb822_lossless::Deb822::parse(text);
        let green = deb822_parse.green().clone();
        let mut errors = deb822_parse.errors().to_vec();

        // Check if there's exactly one paragraph
        if errors.is_empty() {
            let deb822 = deb822_parse.tree();
            let paragraph_count = deb822.paragraphs().count();
            if paragraph_count == 0 {
                errors.push("No paragraphs found".to_string());
            } else if paragraph_count > 1 {
                errors.push("Multiple paragraphs found, expected one".to_string());
            }
        }

        deb822_lossless::Parse::new(green, errors)
    }

    /// Returns the format of the Changes file.
    pub fn format(&self) -> Option<String> {
        self.0.get("Format").map(|s| s.to_string())
    }

    /// Set the format of the Changes file.
    pub fn set_format(&mut self, value: &str) {
        self.0.set("Format", value);
    }

    /// Returns the name of the source package.
    pub fn source(&self) -> Option<String> {
        self.0.get("Source").map(|s| s.to_string())
    }

    /// Returns the list of binary packages generated by the source package.
    pub fn binary(&self) -> Option<Vec<String>> {
        self.0
            .get("Binary")
            .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
    }

    /// Returns the architecture the source package is intended for.
    pub fn architecture(&self) -> Option<Vec<String>> {
        self.0
            .get("Architecture")
            .map(|s| s.split_whitespace().map(|s| s.to_string()).collect())
    }

    /// Returns the version of the source package.
    pub fn version(&self) -> Option<debversion::Version> {
        self.0.get("Version").map(|s| s.parse().unwrap())
    }

    /// Returns the distribution the source package is intended for.
    pub fn distribution(&self) -> Option<String> {
        self.0.get("Distribution").map(|s| s.to_string())
    }

    /// Returns the urgency of the source package.
    pub fn urgency(&self) -> Option<crate::fields::Urgency> {
        self.0.get("Urgency").map(|s| s.parse().unwrap())
    }

    /// Returns the name and email address of the person who maintains the package.
    pub fn maintainer(&self) -> Option<String> {
        self.0.get("Maintainer").map(|s| s.to_string())
    }

    /// Returns the name and email address of the person who uploaded the package.
    pub fn changed_by(&self) -> Option<String> {
        self.0.get("Changed-By").map(|s| s.to_string())
    }

    /// Returns the description of the source package.
    pub fn description(&self) -> Option<String> {
        self.0.get_multiline("Description")
    }

    /// Returns the SHA-1 checksums of the files in the source package.
    pub fn checksums_sha1(&self) -> Option<Vec<crate::fields::Sha1Checksum>> {
        self.0
            .get("Checksums-Sha1")
            .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
    }

    /// Returns the SHA-256 checksums of the files in the source package.
    pub fn checksums_sha256(&self) -> Option<Vec<crate::fields::Sha256Checksum>> {
        self.0
            .get("Checksums-Sha256")
            .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
    }

    /// Returns the list of files in the source package.
    pub fn files(&self) -> Option<Vec<File>> {
        self.0
            .get("Files")
            .map(|s| s.lines().map(|line| line.parse().unwrap()).collect())
    }

    /// Returns the path to the pool directory for the source package.
    pub fn get_pool_path(&self) -> Option<String> {
        let files = self.files()?;

        let section = &files.first().unwrap().section;

        let section = if let Some((section, _subsection)) = section.split_once('/') {
            section
        } else {
            "main"
        };

        let source = self.source()?;

        let subdir = if source.starts_with("lib") {
            "lib".to_string()
        } else {
            source[..1].to_lowercase()
        };

        Some(format!("pool/{}/{}/{}", section, subdir, source))
    }

    /// Create a new Changes file.
    pub fn new() -> Self {
        let mut slf = Self(deb822_lossless::Paragraph::new());
        slf.set_format("1.8");
        slf
    }

    /// Read a Changes file from a file.
    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ParseError> {
        let deb822 = deb822_lossless::Deb822::from_file(path)?;
        let mut paras = deb822.paragraphs();
        let para = match paras.next() {
            Some(para) => para,
            None => return Err(ParseError::NoParagraphs),
        };
        if paras.next().is_some() {
            return Err(ParseError::MultipleParagraphs);
        }
        Ok(Self(para))
    }

    /// Read a Changes file from a file, allowing syntax errors.
    pub fn from_file_relaxed<P: AsRef<std::path::Path>>(
        path: P,
    ) -> Result<(Self, Vec<String>), std::io::Error> {
        let (mut deb822, mut errors) = deb822_lossless::Deb822::from_file_relaxed(path)?;
        let mut paras = deb822.paragraphs();
        let para = match paras.next() {
            Some(para) => para,
            None => deb822.add_paragraph(),
        };
        if paras.next().is_some() {
            errors.push("multiple paragraphs found".to_string());
        }
        Ok((Self(para), errors))
    }

    /// Read a Changes file from a reader.
    pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, ParseError> {
        let deb822 = deb822_lossless::Deb822::read(&mut r)?;
        let mut paras = deb822.paragraphs();
        let para = match paras.next() {
            Some(para) => para,
            None => return Err(ParseError::NoParagraphs),
        };
        if paras.next().is_some() {
            return Err(ParseError::MultipleParagraphs);
        }
        Ok(Self(para))
    }

    /// Read a Changes file from a reader, allowing syntax errors.
    pub fn read_relaxed<R: std::io::Read>(
        mut r: R,
    ) -> Result<(Self, Vec<String>), deb822_lossless::Error> {
        let (mut deb822, mut errors) = deb822_lossless::Deb822::read_relaxed(&mut r)?;
        let mut paras = deb822.paragraphs();
        let para = match paras.next() {
            Some(para) => para,
            None => deb822.add_paragraph(),
        };
        if paras.next().is_some() {
            errors.push("multiple paragraphs found".to_string());
        }
        Ok((Self(para), errors))
    }
}

impl Default for Changes {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for Changes {
    type Target = pyo3::PyAny;
    type Output = pyo3::Bound<'py, Self::Target>;
    type Error = pyo3::PyErr;

    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
        self.0.into_pyobject(py)
    }
}

#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for &Changes {
    type Target = pyo3::PyAny;
    type Output = pyo3::Bound<'py, Self::Target>;
    type Error = pyo3::PyErr;

    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
        (&self.0).into_pyobject(py)
    }
}

#[cfg(feature = "python-debian")]
impl<'py> pyo3::FromPyObject<'_, 'py> for Changes {
    type Error = pyo3::PyErr;

    fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
        Ok(Changes(ob.extract()?))
    }
}

impl AstNode for Changes {
    type Language = deb822_lossless::Lang;

    fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
        deb822_lossless::Paragraph::can_cast(kind) || deb822_lossless::Deb822::can_cast(kind)
    }

    fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
        if let Some(para) = deb822_lossless::Paragraph::cast(syntax.clone()) {
            Some(Changes(para))
        } else if let Some(deb822) = deb822_lossless::Deb822::cast(syntax) {
            deb822.paragraphs().next().map(Changes)
        } else {
            None
        }
    }

    fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
        self.0.syntax()
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_new() {
        let changes = super::Changes::new();
        assert_eq!(changes.format(), Some("1.8".to_string()));
    }

    #[test]
    fn test_parse() {
        let changes = r#"Format: 1.8
Date: Fri, 08 Sep 2023 18:23:59 +0100
Source: buildlog-consultant
Binary: python3-buildlog-consultant
Architecture: all
Version: 0.0.34-1
Distribution: unstable
Urgency: medium
Maintainer: Jelmer Vernooĳ <jelmer@debian.org>
Changed-By: Jelmer Vernooĳ <jelmer@debian.org>
Description:
 python3-buildlog-consultant - build log parser and analyser
Changes:
 buildlog-consultant (0.0.34-1) UNRELEASED; urgency=medium
 .
   * New upstream release.
   * Update standards version to 4.6.2, no changes needed.
Checksums-Sha1:
 f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
 b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
Checksums-Sha256:
 342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo
 7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb
Files:
 aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo
 a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb
"#;
        let changes = super::Changes::read(changes.as_bytes()).unwrap();
        assert_eq!(changes.format(), Some("1.8".to_string()));
        assert_eq!(changes.source(), Some("buildlog-consultant".to_string()));
        assert_eq!(
            changes.binary(),
            Some(vec!["python3-buildlog-consultant".to_string()])
        );
        assert_eq!(changes.architecture(), Some(vec!["all".to_string()]));
        assert_eq!(changes.version(), Some("0.0.34-1".parse().unwrap()));
        assert_eq!(changes.distribution(), Some("unstable".to_string()));
        assert_eq!(changes.urgency(), Some(crate::fields::Urgency::Medium));
        assert_eq!(
            changes.maintainer(),
            Some("Jelmer Vernooĳ <jelmer@debian.org>".to_string())
        );
        assert_eq!(
            changes.changed_by(),
            Some("Jelmer Vernooĳ <jelmer@debian.org>".to_string())
        );
        assert_eq!(
            changes.description(),
            Some("python3-buildlog-consultant - build log parser and analyser".to_string())
        );
        assert_eq!(
            changes.checksums_sha1(),
            Some(vec![
                "f1657e628254428ad74542e82c253a181894e8d0 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
                "b44493c05d014bcd59180942d0125b20ddf45d03 2550812 python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
            ])
        );
        assert_eq!(
            changes.checksums_sha256(),
            Some(vec![
                "342a5782bf6a4f282d9002f726d2cac9c689c7e0fa7f61a1b0ecbf4da7916bdb 17153 buildlog-consultant_0.0.34-1_amd64.buildinfo"
                    .parse()
                    .unwrap(),
                "7f7e5df81ee23fbbe89015edb37e04f4bb40672fa6e9b1afd4fd698e57db78fd 2550812 python3-buildlog-consultant_0.0.34-1_all.deb"
                    .parse()
                    .unwrap()
            ])
        );
        assert_eq!(
            changes.files(),
            Some(vec![
                "aa83112b0f8774a573bcf0b7b5cc12cc 17153 python optional buildlog-consultant_0.0.34-1_amd64.buildinfo".parse().unwrap(),
                "a55858b90fe0ca728c89c1a1132b45c5 2550812 python optional python3-buildlog-consultant_0.0.34-1_all.deb".parse().unwrap()
            ])
        );

        assert_eq!(
            changes.get_pool_path(),
            Some("pool/main/b/buildlog-consultant".to_string())
        );
    }
}
