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

use clap::Parser;
use cosmian_kms_client::{
    KmsClient,
    cosmian_kmip::kmip_2_1::{
        kmip_operations::{GetAttributes, GetAttributesResponse},
        kmip_types::{AttributeReference, Tag, UniqueIdentifier},
    },
    reexport::cosmian_kms_client_utils::attributes_utils::{CLinkType, parse_selected_attributes},
    write_bytes_to_file,
};
use cosmian_logger::{debug, trace};
use serde_json::Value;

use crate::{
    actions::kms::{console, labels::ATTRIBUTE_ID, shared::get_key_uid},
    error::result::KmsCliResult,
};

/// Get the KMIP object attributes and tags.
///
/// When using tags to retrieve the object, rather than the object id,
/// an error is returned if multiple objects matching the tags are found.
#[derive(Parser, Debug, Default)]
#[clap(verbatim_doc_comment)]
pub struct GetAttributesAction {
    /// The unique identifier of the cryptographic object.
    /// If not specified, tags should be specified
    #[clap(long = ATTRIBUTE_ID, short = 'i', group = "id-tags")]
    pub id: Option<String>,

    /// Tag to use to retrieve the key when no key id is specified.
    /// To specify multiple tags, use the option multiple times.
    #[clap(long = "tag", short = 't', value_name = "TAG", group = "id-tags")]
    pub tags: Option<Vec<String>>,

    /// The KMIP attribute to retrieve.
    /// To specify multiple attributes, use the option multiple times.
    /// If not specified, all possible attributes are returned.
    /// To retrieve the tags, use `Tag` as an attribute value.
    #[clap(
        long = "attribute",
        short = 'a',
        value_name = "ATTRIBUTE",
        verbatim_doc_comment
    )]
    pub attribute_tags: Vec<Tag>,

    /// Filter on retrieved links. Only if KMIP tag `LinkType` is used in `attribute` parameter.
    /// To specify multiple attributes, use the option multiple times.
    /// If not specified, all possible link types are returned.
    #[clap(
        long = "link-type",
        short = 'l',
        value_name = "LINK_TYPE",
        verbatim_doc_comment
    )]
    pub attribute_link_types: Vec<CLinkType>,

    /// An optional file where to export the attributes.
    /// The attributes will be in JSON TTLV format.
    #[clap(long = "output-file", short = 'o', verbatim_doc_comment)]
    pub output_file: Option<PathBuf>,
}

impl GetAttributesAction {
    /// Get the KMIP object attributes and tags.
    ///
    /// When using tags to retrieve the object, rather than the object id,
    /// an error is returned if multiple objects matching the tags are found.
    ///
    /// # Errors
    ///
    /// This function can return an error if:
    ///
    /// - The `--id` or one or more `--tag` options is not specified.
    /// - There is an error serializing the tags to a string.
    /// - There is an error performing the Get Attributes request.
    /// - There is an error serializing the attributes to JSON.
    /// - There is an error writing the attributes to the output file.
    /// - There is an error writing to the console.
    pub async fn run(&self, kms_rest_client: KmsClient) -> KmsCliResult<HashMap<String, Value>> {
        trace!("{self:?}");
        let id = get_key_uid(self.id.as_ref(), self.tags.as_ref(), ATTRIBUTE_ID)?;

        let (unique_identifier, results) = get_attributes(
            &kms_rest_client,
            &id,
            &self.attribute_tags,
            &self.attribute_link_types,
        )
        .await?;

        if let Some(output_file) = &self.output_file {
            let json = serde_json::to_string_pretty(&results)?;
            debug!("GetAttributes response for {unique_identifier}: {}", json);
            write_bytes_to_file(json.as_bytes(), output_file)?;
            let stdout = format!(
                "The attributes for {unique_identifier} were exported to {}",
                output_file.display()
            );
            console::Stdout::new(&stdout).write()?;
        } else {
            let mut stdout = console::Stdout::new(&format!("Attributes for {unique_identifier}"));
            stdout.set_unique_identifier(&unique_identifier);
            stdout.set_attributes(results.clone());
            stdout.write()?;
        }
        Ok(results)
    }
}

pub(crate) async fn get_attributes(
    kms_rest_client: &KmsClient,
    id_or_tags: &str,
    attribute_tags: &[Tag],
    attribute_link_types: &[CLinkType],
) -> KmsCliResult<(UniqueIdentifier, HashMap<String, Value>)> {
    let mut references: Vec<AttributeReference> = Vec::with_capacity(attribute_tags.len());
    for tag in attribute_tags {
        references.push(AttributeReference::Standard(*tag));
    }

    // perform the Get Attributes request
    let GetAttributesResponse {
        unique_identifier,
        attributes,
    } = kms_rest_client
        .get_attributes(GetAttributes {
            unique_identifier: Some(UniqueIdentifier::TextString(id_or_tags.to_owned())),
            attribute_reference: if references.is_empty() {
                None
            } else {
                Some(references)
            },
        })
        .await?;

    debug!("GetAttributes response for {unique_identifier}: {attributes}",);

    let results = parse_selected_attributes(&attributes, attribute_tags, attribute_link_types)?;
    Ok((unique_identifier, results))
}
