use std::collections::HashMap;
use std::fmt;

#[cfg(feature = "serde")]
use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "serde")]
use serde_json::Value;

use super::credential::Credential;
use super::ursa_cl::Nonce;
use crate::identifiers::cred_def::CredentialDefinitionId;
use crate::identifiers::rev_reg::RevocationRegistryId;
use crate::identifiers::schema::SchemaId;
use crate::utils::{qualifiable, Qualifiable};
use crate::{Validatable, ValidationError};
use indy_utils::did::DidValue;
use indy_utils::invalid;
use indy_utils::wql::Query;

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct PresentationRequestPayload {
    pub nonce: Nonce,
    pub name: String,
    pub version: String,
    #[cfg_attr(feature = "serde", serde(default))]
    pub requested_attributes: HashMap<String, AttributeInfo>,
    #[cfg_attr(feature = "serde", serde(default))]
    pub requested_predicates: HashMap<String, PredicateInfo>,
    pub non_revoked: Option<NonRevocedInterval>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PresentationRequest {
    PresentationRequestV1(PresentationRequestPayload),
    PresentationRequestV2(PresentationRequestPayload),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PresentationRequestVersion {
    V1,
    V2,
}

impl PresentationRequest {
    pub fn value<'a>(&'a self) -> &'a PresentationRequestPayload {
        match self {
            PresentationRequest::PresentationRequestV1(req) => req,
            PresentationRequest::PresentationRequestV2(req) => req,
        }
    }

    pub fn version(&self) -> PresentationRequestVersion {
        match self {
            PresentationRequest::PresentationRequestV1(_) => PresentationRequestVersion::V1,
            PresentationRequest::PresentationRequestV2(_) => PresentationRequestVersion::V2,
        }
    }
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for PresentationRequest {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct Helper {
            ver: Option<String>,
            nonce: String,
        }

        let v = Value::deserialize(deserializer)?;

        let helper = Helper::deserialize(&v).map_err(de::Error::custom)?;
        let nonce_cleaned = helper.nonce.replace(" ", "").replace("_", "");

        let req = match helper.ver {
            Some(version) => match version.as_ref() {
                "1.0" => {
                    let request =
                        PresentationRequestPayload::deserialize(v).map_err(de::Error::custom)?;
                    PresentationRequest::PresentationRequestV1(request)
                }
                "2.0" => {
                    let request =
                        PresentationRequestPayload::deserialize(v).map_err(de::Error::custom)?;
                    PresentationRequest::PresentationRequestV2(request)
                }
                _ => return Err(de::Error::unknown_variant(&version, &["2.0"])),
            },
            None => {
                let request =
                    PresentationRequestPayload::deserialize(v).map_err(de::Error::custom)?;
                PresentationRequest::PresentationRequestV1(request)
            }
        };
        let nonce_parsed = match &req {
            PresentationRequest::PresentationRequestV1(payload) => &payload.nonce,
            PresentationRequest::PresentationRequestV2(payload) => &payload.nonce,
        };
        if nonce_cleaned.as_str() != &**nonce_parsed {
            Err(de::Error::custom(format!(
                "Invalid nonce provided: {}",
                nonce_cleaned
            )))
        } else {
            Ok(req)
        }
    }
}

#[cfg(feature = "serde")]
impl Serialize for PresentationRequest {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let value = match self {
            PresentationRequest::PresentationRequestV1(v1) => {
                let mut value = ::serde_json::to_value(v1).map_err(ser::Error::custom)?;
                value
                    .as_object_mut()
                    .unwrap()
                    .insert("ver".into(), Value::from("1.0"));
                value
            }
            PresentationRequest::PresentationRequestV2(v2) => {
                let mut value = ::serde_json::to_value(v2).map_err(ser::Error::custom)?;
                value
                    .as_object_mut()
                    .unwrap()
                    .insert("ver".into(), Value::from("2.0"));
                value
            }
        };

        value.serialize(serializer)
    }
}

#[allow(unused)]
pub type PresentationRequestExtraQuery = HashMap<String, Query>;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct NonRevocedInterval {
    pub from: Option<u64>,
    pub to: Option<u64>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct AttributeInfo {
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub name: Option<String>,
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub names: Option<Vec<String>>,
    pub restrictions: Option<Query>,
    pub non_revoked: Option<NonRevocedInterval>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct PredicateInfo {
    pub name: String,
    pub p_type: PredicateTypes,
    pub p_value: i32,
    pub restrictions: Option<Query>,
    pub non_revoked: Option<NonRevocedInterval>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum PredicateTypes {
    #[cfg_attr(feature = "serde", serde(rename = ">="))]
    GE,
    #[cfg_attr(feature = "serde", serde(rename = "<="))]
    LE,
    #[cfg_attr(feature = "serde", serde(rename = ">"))]
    GT,
    #[cfg_attr(feature = "serde", serde(rename = "<"))]
    LT,
}

impl fmt::Display for PredicateTypes {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            PredicateTypes::GE => write!(f, "GE"),
            PredicateTypes::GT => write!(f, "GT"),
            PredicateTypes::LE => write!(f, "LE"),
            PredicateTypes::LT => write!(f, "LT"),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct RequestedAttributeInfo {
    pub attr_referent: String,
    pub attr_info: AttributeInfo,
    pub revealed: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct RequestedPredicateInfo {
    pub predicate_referent: String,
    pub predicate_info: PredicateInfo,
}

impl Validatable for PresentationRequest {
    fn validate(&self) -> Result<(), ValidationError> {
        let value = self.value();
        let version = self.version();

        if value.requested_attributes.is_empty() && value.requested_predicates.is_empty() {
            return Err(invalid!("Presentation request validation failed: both `requested_attributes` and `requested_predicates` are empty"));
        }

        for (_, requested_attribute) in value.requested_attributes.iter() {
            let has_name = !requested_attribute
                .name
                .as_ref()
                .map(String::is_empty)
                .unwrap_or(true);
            let has_names = !requested_attribute
                .names
                .as_ref()
                .map(Vec::is_empty)
                .unwrap_or(true);
            if !has_name && !has_names {
                return Err(invalid!(
                    "Presentation request validation failed: there is empty requested attribute: {:?}",
                    requested_attribute
                ));
            }

            if has_name && has_names {
                return Err(invalid!("Presentation request validation failed: there is a requested attribute with both name and names: {:?}", requested_attribute));
            }

            if let Some(ref restrictions) = requested_attribute.restrictions {
                _process_operator(&restrictions, &version)?;
            }
        }

        for (_, requested_predicate) in value.requested_predicates.iter() {
            if requested_predicate.name.is_empty() {
                return Err(invalid!(
                    "Presentation request validation failed: there is empty requested attribute: {:?}",
                    requested_predicate
                ));
            }
            if let Some(ref restrictions) = requested_predicate.restrictions {
                _process_operator(&restrictions, &version)?;
            }
        }

        Ok(())
    }
}

impl PresentationRequest {
    #[allow(unused)]
    pub fn to_unqualified(self) -> PresentationRequest {
        let convert = |request: &mut PresentationRequestPayload| {
            for (_, requested_attribute) in request.requested_attributes.iter_mut() {
                requested_attribute.restrictions = requested_attribute
                    .restrictions
                    .as_mut()
                    .map(|ref mut restrictions| _convert_query_to_unqualified(&restrictions));
            }
            for (_, requested_predicate) in request.requested_predicates.iter_mut() {
                requested_predicate.restrictions = requested_predicate
                    .restrictions
                    .as_mut()
                    .map(|ref mut restrictions| _convert_query_to_unqualified(&restrictions));
            }
        };

        match self {
            PresentationRequest::PresentationRequestV2(mut request) => {
                convert(&mut request);
                PresentationRequest::PresentationRequestV2(request)
            }
            PresentationRequest::PresentationRequestV1(mut request) => {
                convert(&mut request);
                PresentationRequest::PresentationRequestV1(request)
            }
        }
    }
}

fn _convert_query_to_unqualified(query: &Query) -> Query {
    match query {
        Query::Eq(tag_name, ref tag_value) => Query::Eq(
            tag_name.to_string(),
            _convert_value_to_unqualified(tag_name, tag_value),
        ),
        Query::Neq(ref tag_name, ref tag_value) => Query::Neq(
            tag_name.to_string(),
            _convert_value_to_unqualified(tag_name, tag_value),
        ),
        Query::In(ref tag_name, ref tag_values) => Query::In(
            tag_name.to_string(),
            tag_values
                .iter()
                .map(|tag_value| _convert_value_to_unqualified(tag_name, tag_value))
                .collect::<Vec<String>>(),
        ),
        Query::And(ref queries) => Query::And(
            queries
                .iter()
                .map(|query| _convert_query_to_unqualified(query))
                .collect::<Vec<Query>>(),
        ),
        Query::Or(ref queries) => Query::Or(
            queries
                .iter()
                .map(|query| _convert_query_to_unqualified(query))
                .collect::<Vec<Query>>(),
        ),
        Query::Not(ref query) => _convert_query_to_unqualified(query),
        query => query.clone(),
    }
}

fn _convert_value_to_unqualified(tag_name: &str, tag_value: &str) -> String {
    match tag_name {
        "issuer_did" | "schema_issuer_did" => DidValue(tag_value.to_string()).to_unqualified().0,
        "schema_id" => SchemaId(tag_value.to_string()).to_unqualified().0,
        "cred_def_id" => {
            CredentialDefinitionId(tag_value.to_string())
                .to_unqualified()
                .0
        }
        "rev_reg_id" => {
            RevocationRegistryId(tag_value.to_string())
                .to_unqualified()
                .0
        }
        _ => tag_value.to_string(),
    }
}

fn _process_operator(
    restriction_op: &Query,
    version: &PresentationRequestVersion,
) -> Result<(), ValidationError> {
    match restriction_op {
        Query::Eq(ref tag_name, ref tag_value)
        | Query::Neq(ref tag_name, ref tag_value)
        | Query::Gt(ref tag_name, ref tag_value)
        | Query::Gte(ref tag_name, ref tag_value)
        | Query::Lt(ref tag_name, ref tag_value)
        | Query::Lte(ref tag_name, ref tag_value)
        | Query::Like(ref tag_name, ref tag_value) => {
            _check_restriction(tag_name, tag_value, version)
        }
        Query::In(ref tag_name, ref tag_values) => {
            tag_values
                .iter()
                .map(|tag_value| _check_restriction(tag_name, tag_value, version))
                .collect::<Result<Vec<()>, ValidationError>>()?;
            Ok(())
        }
        Query::And(ref operators) | Query::Or(ref operators) => {
            operators
                .iter()
                .map(|operator| _process_operator(operator, version))
                .collect::<Result<Vec<()>, ValidationError>>()?;
            Ok(())
        }
        Query::Not(ref operator) => _process_operator(operator, version),
    }
}

fn _check_restriction(
    tag_name: &str,
    tag_value: &str,
    version: &PresentationRequestVersion,
) -> Result<(), ValidationError> {
    if *version == PresentationRequestVersion::V1
        && Credential::QUALIFIABLE_TAGS.contains(&tag_name)
        && qualifiable::is_fully_qualified(tag_value)
    {
        return Err(invalid!("Presentation request validation failed: fully qualified identifiers can not be used for presentation request of the first version. \
                    Please, set \"ver\":\"2.0\" to use fully qualified identifiers."));
    }
    Ok(())
}

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

    #[cfg(feature = "serde")]
    mod invalid_nonce {
        use super::*;

        #[test]
        fn presentation_request_valid_nonce() {
            let req_json = json!({
                "nonce": "123456",
                "name": "name",
                "version": "2.0",
                "requested_attributes": {},
                "requested_predicates": {},
            })
            .to_string();

            let req: PresentationRequest = serde_json::from_str(&req_json).unwrap();
            let payload = match req {
                PresentationRequest::PresentationRequestV1(p) => p,
                PresentationRequest::PresentationRequestV2(p) => p,
            };

            assert_eq!(&*payload.nonce, "123456");
        }

        #[test]
        fn presentation_request_invalid_nonce() {
            let req_json = json!({
                "nonce": "123abc",
                "name": "name",
                "version": "2.0",
                "requested_attributes": {},
                "requested_predicates": {},
            })
            .to_string();

            serde_json::from_str::<PresentationRequest>(&req_json).unwrap_err();
        }
    }

    mod to_unqualified {
        use super::*;

        const DID_QUALIFIED: &str = "did:sov:NcYxiDXkpYi6ov5FcYDi1e";
        const DID_UNQUALIFIED: &str = "NcYxiDXkpYi6ov5FcYDi1e";
        const SCHEMA_ID_QUALIFIED: &str = "schema:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0";
        const SCHEMA_ID_UNQUALIFIED: &str = "NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0";
        const CRED_DEF_ID_QUALIFIED: &str = "creddef:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:schema:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag";
        const CRED_DEF_ID_UNQUALIFIED: &str =
            "NcYxiDXkpYi6ov5FcYDi1e:3:CL:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag";
        const REV_REG_ID_QUALIFIED: &str = "revreg:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:4:creddef:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:schema:sov:did:sov:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag:CL_ACCUM:TAG_1";
        const REV_REG_ID_UNQUALIFIED: &str = "NcYxiDXkpYi6ov5FcYDi1e:4:NcYxiDXkpYi6ov5FcYDi1e:3:CL:NcYxiDXkpYi6ov5FcYDi1e:2:gvt:1.0:tag:CL_ACCUM:TAG_1";

        #[test]
        fn presentation_request_to_unqualified() {
            let mut requested_attributes: HashMap<String, AttributeInfo> = HashMap::new();
            requested_attributes.insert(
                "attr1_referent".to_string(),
                AttributeInfo {
                    name: Some("name".to_string()),
                    names: None,
                    restrictions: Some(Query::And(vec![
                        Query::Eq("issuer_did".to_string(), DID_QUALIFIED.to_string()),
                        Query::Eq("schema_id".to_string(), SCHEMA_ID_QUALIFIED.to_string()),
                        Query::Eq("cred_def_id".to_string(), CRED_DEF_ID_QUALIFIED.to_string()),
                    ])),
                    non_revoked: None,
                },
            );

            let mut requested_predicates: HashMap<String, PredicateInfo> = HashMap::new();
            requested_predicates.insert(
                "predicate1_referent".to_string(),
                PredicateInfo {
                    name: "age".to_string(),
                    p_type: PredicateTypes::GE,
                    p_value: 0,
                    restrictions: Some(Query::And(vec![
                        Query::Eq("schema_issuer_did".to_string(), DID_QUALIFIED.to_string()),
                        Query::Eq("rev_reg_id".to_string(), REV_REG_ID_QUALIFIED.to_string()),
                    ])),
                    non_revoked: None,
                },
            );

            let request = PresentationRequest::PresentationRequestV2(PresentationRequestPayload {
                nonce: Nonce::from_dec("112233445566").unwrap(), //Nonce::new().unwrap(),
                name: "presentation_request_to_unqualified".to_string(),
                version: "1.0".to_string(),
                requested_attributes,
                requested_predicates,
                non_revoked: None,
            });

            let mut expected_requested_attributes: HashMap<String, AttributeInfo> = HashMap::new();
            expected_requested_attributes.insert(
                "attr1_referent".to_string(),
                AttributeInfo {
                    name: Some("name".to_string()),
                    names: None,
                    restrictions: Some(Query::And(vec![
                        Query::Eq("issuer_did".to_string(), DID_UNQUALIFIED.to_string()),
                        Query::Eq("schema_id".to_string(), SCHEMA_ID_UNQUALIFIED.to_string()),
                        Query::Eq(
                            "cred_def_id".to_string(),
                            CRED_DEF_ID_UNQUALIFIED.to_string(),
                        ),
                    ])),
                    non_revoked: None,
                },
            );

            let mut expected_requested_predicates: HashMap<String, PredicateInfo> = HashMap::new();
            expected_requested_predicates.insert(
                "predicate1_referent".to_string(),
                PredicateInfo {
                    name: "age".to_string(),
                    p_type: PredicateTypes::GE,
                    p_value: 0,
                    restrictions: Some(Query::And(vec![
                        Query::Eq("schema_issuer_did".to_string(), DID_UNQUALIFIED.to_string()),
                        Query::Eq("rev_reg_id".to_string(), REV_REG_ID_UNQUALIFIED.to_string()),
                    ])),
                    non_revoked: None,
                },
            );

            let request = request.to_unqualified();
            assert_eq!(
                expected_requested_attributes,
                request.value().requested_attributes
            );
            assert_eq!(
                expected_requested_predicates,
                request.value().requested_predicates
            );
            assert_eq!(PresentationRequestVersion::V2, request.version());
        }
    }
}
