//! XML (de)serialization for KeePass databases.
//!
//! This module provides types that mirror the ones in `crate::db`, but are tailored to closely fit
//! the XML structure of KeePass databases for easy `#[derive(Serialize, Deserialize)]`.
//!
//! See <https://keepass.info/help/download/KDBX_XML.xsd> for an XML schema.

pub mod custom_serde;
pub mod entry;
pub mod group;
pub mod meta;
pub mod tags;
pub mod times;
pub mod timestamp;

use serde::{Deserialize, Serialize, Serializer};

use base64::{engine::general_purpose as base64_engine, Engine as _};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use uuid::Uuid;

use crate::{
    crypt::ciphers::Cipher,
    db::{GroupId, Value},
    format::xml_db::{
        custom_serde::cs_opt_string, entry::UnprotectError, group::Group, meta::Meta, timestamp::Timestamp,
    },
};
#[cfg(feature = "save_kdbx4")]
use crate::{crypt::CryptographyError, db::DatabaseSaveError};

pub fn parse_xml(
    data: &[u8],
    header_attachments: &[Value<Vec<u8>>],
    inner_decryptor: &mut dyn Cipher,
) -> Result<crate::db::Database, ParseXmlError> {
    let kdbx: KeePassFile = quick_xml::de::from_reader(data)?;
    Ok(kdbx.xml_to_db(inner_decryptor, header_attachments)?)
}

/// Errors that can occur during parsing of the inner XML database of a KDBX file
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ParseXmlError {
    /// Errors related to XML deserialization or serialization.
    #[error("Error parsing XML inside KDBX: {0}")]
    Xml(#[from] quick_xml::DeError),

    /// Errors related to unprotecting entries, such as decryption failures or unsupported
    /// encryption methods.
    #[error("Error unprotecting entry: {0}")]
    Unprotect(#[from] UnprotectError),
}

#[cfg(feature = "save_kdbx4")]
#[allow(clippy::type_complexity)]
pub fn to_xml(
    db: &crate::db::Database,
    inner_encryptor: &mut dyn Cipher,
) -> Result<(Vec<u8>, Vec<crate::db::Value<Vec<u8>>>), DatabaseSaveError> {
    let kdbx = KeePassFile::db_to_xml(db, inner_encryptor)?;
    let xml = quick_xml::se::to_string_with_root("KeePassFile", &kdbx)?
        .as_bytes()
        .to_vec();

    let mut attachments: Vec<(usize, Value<Vec<u8>>)> = db
        .attachments
        .iter()
        .map(|(id, attachment)| (id.id(), attachment.data.clone()))
        .collect();

    attachments.sort_by_key(|(id, _)| *id);

    let attachments = attachments.into_iter().map(|(_, data)| data).collect();

    Ok((xml, attachments))
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct KeePassFile {
    meta: Meta,
    root: Root,
}

impl KeePassFile {
    /// Convert from XML representation to database representation.
    fn xml_to_db(
        mut self,
        inner_decryptor: &mut dyn Cipher,
        header_attachments: &[Value<Vec<u8>>],
    ) -> Result<crate::db::Database, UnprotectError> {
        let mut db = crate::db::Database::new_with_root_id(GroupId::from_uuid(self.root.group.uuid.0));

        let mut attachments = HashMap::new();

        // convert header attachments (KDBX4-style) to database attachments
        for (i, header_attachment) in header_attachments.iter().enumerate() {
            let attachment = crate::db::Attachment {
                id: crate::db::AttachmentId::new(i),
                entries: HashSet::new(),
                data: header_attachment.clone(),
            };
            attachments.insert(attachment.id, attachment);
        }

        // convert XML attachments (KDBX3-style) to database attachments
        if let Some(binaries) = self.meta.binaries.take() {
            for binary in binaries.binaries {
                let id = crate::db::AttachmentId::next_free(&db);
                let data = binary.xml_to_db(inner_decryptor)?;

                attachments.insert(
                    id,
                    crate::db::Attachment {
                        id,
                        entries: HashSet::new(),
                        data,
                    },
                );
            }
        }

        let custom_icons = self
            .meta
            .custom_icons
            .take()
            .map(|ci| {
                ci.icons
                    .into_iter()
                    .map(|icon| {
                        let ci: crate::db::CustomIcon = icon.into();
                        (ci.id, ci)
                    })
                    .collect()
            })
            .unwrap_or_default();

        db.meta = self.meta.into();

        db.deleted_objects = self
            .root
            .deleted_objects
            .map(|del_objs| {
                del_objs
                    .objects
                    .into_iter()
                    .map(|obj| (obj.uuid.0, obj.deletion_time.map(|ts| ts.time)))
                    .collect()
            })
            .unwrap_or_default();

        self.root
            .group
            .xml_to_db_handle(db.root_mut(), &attachments, &custom_icons, inner_decryptor)?;

        db.attachments = attachments;
        db.custom_icons = custom_icons;

        // Re-populate CustomIcon back-reference sets.
        //
        // The XML parser creates CustomIcon values with empty `entries` and `groups` sets
        // because icon data and entry/group data live in separate parts of the XML file.
        // We perform a single pass here to reconstruct all back-references from the icon
        // fields that were already set on each entry and group during xml_to_db_handle.
        let entry_ids: Vec<crate::db::EntryId> = db.entries.keys().copied().collect();
        for entry_id in entry_ids {
            // current version
            if let Some(crate::db::Icon::Custom(icon_id)) =
                db.entries.get(&entry_id).and_then(|e| e.icon.as_ref())
            {
                if let Some(icon) = db.custom_icons.get_mut(icon_id) {
                    icon.entries.insert((entry_id, None));
                }
            }

            // historical versions
            if let Some(entry) = db.entries.get(&entry_id) {
                let history_len = entry.history.as_ref().map_or(0, |h| h.entries.len());

                for i in 0..history_len {
                    #[allow(clippy::indexing_slicing)] // We just checked that the index is in bounds
                    if let Some(crate::db::Icon::Custom(icon_id)) =
                        entry.history.as_ref().and_then(|h| h.entries[i].icon.as_ref())
                    {
                        if let Some(icon) = db.custom_icons.get_mut(icon_id) {
                            icon.entries.insert((entry_id, Some(i)));
                        }
                    }
                }
            }
        }

        let group_ids: Vec<crate::db::GroupId> = db.groups.keys().copied().collect();
        for group_id in group_ids {
            if let Some(crate::db::Icon::Custom(icon_id)) =
                db.groups.get(&group_id).and_then(|g| g.icon.as_ref())
            {
                if let Some(icon) = db.custom_icons.get_mut(icon_id) {
                    icon.groups.insert(group_id);
                }
            }
        }

        Ok(db)
    }

    /// Convert from database representation to XML representation.
    #[cfg(feature = "save_kdbx4")]
    fn db_to_xml(db: &crate::db::Database, inner_cipher: &mut dyn Cipher) -> Result<Self, CryptographyError> {
        use crate::format::xml_db::meta::Icon;

        let group = Group::db_to_xml(db.root(), inner_cipher)?;

        let mut meta: Meta = db.meta.clone().into();
        meta.custom_icons.get_or_insert_default().icons =
            db.custom_icons.values().cloned().map(Icon::from).collect();

        let deleted_objects = if db.deleted_objects.is_empty() {
            None
        } else {
            Some(DeletedObjects {
                objects: db
                    .deleted_objects
                    .iter()
                    .map(|(uuid, deletion_time)| DeletedObject {
                        uuid: UUID(*uuid),
                        deletion_time: deletion_time.map(Timestamp::new_iso8601),
                    })
                    .collect(),
            })
        };

        Ok(KeePassFile {
            meta,
            root: Root {
                group,
                deleted_objects,
            },
        })
    }
}

/// A UUID deserialized from a Base64 string.
#[allow(clippy::upper_case_acronyms)] // Keep the name consistent with KeePass XML schema
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct UUID(Uuid);

impl<'de> Deserialize<'de> for UUID {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let input = String::deserialize(deserializer)?;

        let v = base64_engine::STANDARD
            .decode(input)
            .map_err(serde::de::Error::custom)?;

        let uuid = Uuid::from_slice(&v).map_err(serde::de::Error::custom)?;
        Ok(UUID(uuid))
    }
}

impl Serialize for UUID {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let b64 = base64_engine::STANDARD.encode(self.0.as_bytes());
        serializer.serialize_str(&b64)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Root {
    #[serde(rename = "Group")]
    pub group: Group,

    #[serde(default, rename = "DeletedObjects")]
    pub deleted_objects: Option<DeletedObjects>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeletedObjects {
    #[serde(default, rename = "DeletedObject")]
    pub objects: Vec<DeletedObject>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeletedObject {
    #[serde(rename = "UUID")]
    uuid: UUID,

    #[serde(default, with = "cs_opt_string")]
    deletion_time: Option<Timestamp>,
}

#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {

    use super::*;

    #[derive(Serialize, Deserialize)]
    struct Test<T>(T);

    #[test]
    fn test_deserialize_uuid() {
        let uuid_str = "AAECAwQFBgcICQoLDA0ODw==";
        let uuid: UUID = quick_xml::de::from_str(uuid_str).unwrap();
        assert_eq!(
            uuid.0.as_bytes(),
            &[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]
        );
    }

    #[test]
    fn test_serialize_uuid() {
        let uuid = UUID(Uuid::from_bytes([
            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
        ]));
        let serialized = quick_xml::se::to_string(&Test(uuid)).unwrap();
        assert_eq!(serialized, "<Test>AAECAwQFBgcICQoLDA0ODw==</Test>");
    }
}
