//! This module contains the ickiness for handling the XML bodies that
//! S3 returns.

use roxmltree::Node;

use crate::{Ezs3Err, Timestamp};

impl From<roxmltree::Error> for Ezs3Err {
    fn from(e: roxmltree::Error) -> Ezs3Err {
        Ezs3Err::XmlErr(e.to_string())
    }
}

/// Metadata value representing the S3 storage class; so far we only
/// recognize "Standard" and "Other".
#[derive(Default, Debug, Clone)]
pub enum StorageClass {
    #[default]
    Standard,
    Other(String),
}

impl From<&str> for StorageClass {
    fn from(s: &str) -> StorageClass {
        match s {
            "STANDARD" => StorageClass::Standard,
            _ => StorageClass::Other(s.to_string()),
        }
    }
}

/// The object metadata returned in a bucket listing.
#[derive(Debug, Clone)]
pub struct ObjectData {
    /// The path used for access to this object.
    pub key: String,
    /// A hash appropriate for use with the ETag header.
    pub etag: String,
    /// Size in bytes.
    pub size: usize,
    /// Storage class, although this crate only recognizes "Standard" and
    /// "Other" storage types.
    pub storage_class: StorageClass,
    /// Date/time of most recent modification.
    pub last_modified: Timestamp,
}

fn try_from_node_guts<'a, 'input>(node: Node<'a, 'input>) -> Result<ObjectData, String> {
    if node.tag_name().name() != "Contents" {
        let msg = format!(
            "expected tag name \"Contents\", got tag name {:?}",
            &node.tag_name().name()
        );
        return Err(msg);
    }

    let mut key = None;
    let mut etag = None;
    let mut size = None;
    let mut storage_class = None;
    let mut last_modified = None;

    for n in node.children() {
        let text = match n.text() {
            None => continue,
            Some(t) => t,
        };
        match n.tag_name().name() {
            "Key" => key = Some(text.to_string()),
            "ETag" => etag = Some(text.to_string()),
            "Size" => {
                let sz: usize = text
                    .parse()
                    .map_err(|e| format!("parsing size field: {}", &e))?;
                size = Some(sz);
            }
            "StorageClass" => storage_class = Some(StorageClass::from(text)),
            "LastModified" => {
                let t: Timestamp = text
                    .parse()
                    .map_err(|e| format!("parsing timestamp: {}", &e))?;
                last_modified = Some(t);
            }
            _ => continue,
        }
    }

    let key = key.ok_or_else(|| "no Key".to_string())?;
    let etag = etag.ok_or_else(|| "no ETag".to_string())?;
    let size = size.ok_or_else(|| "no Size".to_string())?;
    let storage_class = storage_class.ok_or_else(|| "no StorageClass".to_string())?;
    let last_modified = last_modified.ok_or_else(|| "no LastModified".to_string())?;

    let od = ObjectData {
        key,
        etag,
        size,
        storage_class,
        last_modified,
    };
    Ok(od)
}

impl<'a, 'input> TryFrom<Node<'a, 'input>> for ObjectData {
    type Error = Ezs3Err;

    fn try_from(node: Node) -> Result<ObjectData, Ezs3Err> {
        try_from_node_guts(node).map_err(|e| Ezs3Err::XmlErr(e))
    }
}

/// Slog through the tag-bag response body of a list-bucket request and
/// attempt to pull out the chunks of `ObjectData` we're looking for.
pub(crate) fn parse_list_response_body(body: &str) -> Result<Vec<ObjectData>, Ezs3Err> {
    let xml_doc = roxmltree::Document::parse(body)?;

    xml_doc
        .root_element()
        .children()
        .for_each(|node| println!("{:#?}", node));

    let contents = xml_doc
        .root_element()
        .children()
        .filter(|node| node.tag_name().name() == "Contents")
        .map(|node| ObjectData::try_from(node))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(contents)
}
