use super::known_properties::is_known_property;
use secrecy::ExposeSecret;
use std::collections::HashMap;

use http::StatusCode;

use crate::standards::indieauth::AccessToken;
use serde::Deserialize;

use crate::{
    algorithms::Properties,
    http::{from_json_value, Client},
    standards::micropub::Parameters,
};

#[cfg(feature = "experimental_batch")]
mod batch;
#[cfg(feature = "experimental_batch")]
pub use batch::{BatchActionResponse, BatchActionResult};


#[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)]
pub struct CreationProperties {
    pub r#type: microformats::types::Class,

    #[serde(flatten)]
    pub parameters: Parameters,

    #[serde(flatten)]
    pub extra_fields: Properties,
}

impl Default for CreationProperties {
    fn default() -> Self {
        Self {
            r#type: microformats::types::Class::Known(microformats::types::KnownClass::Entry),
            parameters: Default::default(),
            extra_fields: Default::default(),
        }
    }
}

impl<'de> serde::Deserialize<'de> for CreationProperties {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        struct V {}

        impl V {
            fn extract_parameters<E>(properties: &mut Properties) -> Result<Parameters, E>
            where
                E: serde::de::Error,
            {
                let matching_parameters = properties
                    .0
                    .iter()
                    .filter(|(key, _)| {
                        is_known_property(key)
                    })
                    .map(|(k, v)| (k.to_owned(), v.to_owned()))
                    .collect::<serde_json::Map<String, serde_json::Value>>();

                matching_parameters.keys().for_each(|key| {
                    properties.remove_entry(key);
                });
                let props: Properties =
                    serde_json::from_value(serde_json::Value::Object(matching_parameters))
                        .map_err(serde::de::Error::custom)?;

                props.try_into().map_err(serde::de::Error::custom)
            }

            fn parse<E>(mut root_properties: Properties) -> Result<CreationProperties, E>
            where
                E: serde::de::Error,
            {
                let (type_value, parameters, extra_fields) =
                    if let Some(class_value) = root_properties.remove("h") {
                        let parameters = Self::extract_parameters(&mut root_properties)?;
                        root_properties.values_mut().for_each(|value| {
                            if !value.is_array() {
                                *value = serde_json::Value::Array(vec![value.clone()]);
                            }
                        });

                        (class_value, parameters, root_properties)
                    } else if let Some(class_value) = root_properties.remove("type") {
                        let mut properties: Properties = root_properties
                            .remove(PROPERTY_PROPERTIES)
                            .ok_or_else(|| serde::de::Error::missing_field(".properties"))
                            .and_then(serde_json::from_value)
                            .map_err(serde::de::Error::custom)?;

                        let parameters = Self::extract_parameters(&mut properties)?;

                        (class_value, parameters, properties)
                    } else {
                        return Err(serde::de::Error::unknown_variant(
                            "encoding",
                            &["json (via 'type')", "form (via 'h')"],
                        ));
                    };

                let r#type = Self::extract_class_name(type_value)?;

                Ok(CreationProperties {
                    r#type,
                    parameters,
                    extra_fields,
                })
            }

            fn extract_class_name<E>(
                value: serde_json::Value,
            ) -> Result<microformats::types::Class, E>
            where
                E: serde::de::Error,
            {
                let specific_value = if let serde_json::Value::Array(values) = value {
                    values
                        .into_iter()
                        .find(|v| v.is_string())
                        .unwrap_or_default()
                } else {
                    value
                };

                serde_json::from_value(specific_value).map_err(serde::de::Error::custom)
            }
        }

        impl<'de> serde::de::Visitor<'de> for V {
            type Value = CreationProperties;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a map of values representing either a form-encoded or JSON-encoded Micropub creation payload")
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::SeqAccess<'de>,
                A::Error: serde::de::Error,
            {
                while let Some(v) = seq.next_element()? {
                    if let Ok(vz) = V::parse::<A::Error>(v) {
                        return Ok(vz);
                    }
                }

                Err(serde::de::Error::custom(
                    "Failed to parse the sequence of values into a valid CreationProperties",
                ))
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                let mut root_properties = Properties::default();
                while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
                    if let Some(original_value) = root_properties.get_mut(&key) {
                        if let serde_json::Value::Array(values) = original_value {
                            values.push(value);
                        } else {
                            *original_value =
                                serde_json::Value::Array(vec![original_value.clone(), value]);
                        }
                    } else {
                        root_properties.insert(key, value);
                    }
                }
                V::parse(root_properties)
            }

            fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
            where
                D: serde::de::Deserializer<'de>,
            {
                let root_properties = Properties::deserialize(deserializer)?;
                V::parse(root_properties)
            }
        }

        deserializer.deserialize_map(V {})
    }
}

#[test]
fn creation_properties_from_json() {
    let creation_properties = CreationProperties {
        parameters: Parameters {
            category: vec!["foo".into(), "bar".into()],
            status: Some(crate::standards::micropub::extension::PostStatus::Drafted),
            ..Default::default()
        },
        extra_fields: Properties::try_from(serde_json::json!({
            "content": ["hello world"]
        }))
        .expect("failed to build extra fields of creation properties"),
        r#type: microformats::types::Class::Known(microformats::types::KnownClass::Entry),
    };

    assert_eq!(
        Ok(creation_properties),
        serde_json::from_value(serde_json::json!({
            "type": ["h-entry"],
            "properties": {
                "content": ["hello world"],
                "category": ["foo", "bar"],
                "post-status": "draft"
            }
        }))
        .map_err(|e| e.to_string())
    );
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum UpdateOperation {
    Replace,
    Add,
    Delete,
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum OperationValue {
    WholeProperties(Vec<String>),
    SpecificValues(HashMap<String, Vec<microformats::types::PropertyValue>>),
}

/// Represents all of the actions known to be taken by a Micropub server.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub enum Action {
    /// Represented by <https://micropub.spec.indieweb.org/#create>.
    Create {
        properties: Box<CreationProperties>,
        files: HashMap<String, Vec<u8>>,
    },
    /// <https://micropub.spec.indieweb.org/#update>.
    Update {
        url: url::Url,
        operation: (UpdateOperation, OperationValue),
    },
    /// <https://micropub.spec.indieweb.org/#delete-p-1>
    Delete(url::Url),
    /// <https://micropub.spec.indieweb.org/#delete-p-7>
    Undelete(url::Url),
    /// Batch operation to execute multiple actions in a single request.
    ///
    /// See <https://github.com/indieweb/micropub-extensions/issues/52>
    #[cfg(feature = "experimental_batch")]
    Batch {
        actions: Vec<Action>,
    },
}

fn parse_location_into_url(
    header_value: Option<http::HeaderValue>,
) -> Result<Option<url::Url>, crate::Error> {
    if let Some(location_value) = header_value {
        String::from_utf8(location_value.as_bytes().to_vec())
            .map_err(crate::Error::FromUTF8)?
            .parse()
            .map_err(crate::Error::Url)
            .map(Option::Some)
    } else {
        Ok(None)
    }
}

fn obtain_url_from_location(
    header_value: Option<http::HeaderValue>,
) -> Result<url::Url, crate::Error> {
    parse_location_into_url(header_value)?
        .ok_or_else(|| super::Error::missing_header("location").into())
}

fn handle_manipulation_response(
    response: http::Response<crate::http::Body>,
) -> Result<(Option<serde_json::Value>, Option<url::Url>), crate::Error> {
    let expected_codes = [StatusCode::OK, StatusCode::CREATED, StatusCode::NO_CONTENT];
    if !expected_codes.contains(&response.status()) {
        return Err(
            super::Error::unexpected_status_code(response.status(), &expected_codes).into(),
        );
    }

    let location = if response.status() == StatusCode::CREATED {
        Some(obtain_url_from_location(
            response.headers().get(http::header::LOCATION).cloned(),
        )?)
    } else {
        None
    };

    let changes = if response.status() == StatusCode::OK {
        Some(serde_json::from_slice(response.body().as_bytes())?)
    } else {
        None
    };

    Ok((changes, location))
}

impl Action {
    /// FIXME: Check if files are present and switch to a form-encoded request.
    #[tracing::instrument]
    fn prepare_request(
        &self,
        access_token: &AccessToken,
        endpoint: &url::Url,
    ) -> Result<http::Request<crate::http::Body>, crate::Error> {
        let (content_type, bytes) = if matches!(self, Self::Create { files, .. } if files.is_empty())
        {
            ("application/json; charset=utf-8", serde_json::to_vec(self)?)
        } else {
            ("multipart/form-data; charset=utf-8", Vec::default())
        };

        http::Request::post(endpoint.as_str())
            .header(http::header::ACCEPT, "application/json; charset=utf-8")
            .header(http::header::CONTENT_TYPE, content_type)
            .header(
                http::header::AUTHORIZATION,
                format!("Bearer {}", access_token.expose_secret()),
            )
            .body(crate::http::Body::Bytes(bytes))
            .map_err(|e| e.into())
    }

    /// Sends this request over to the requested Micropub server.
    #[tracing::instrument(skip(client))]
    pub async fn send(
        &self,
        client: &impl Client,
        endpoint: &url::Url,
        access_token: &AccessToken,
    ) -> Result<ActionResponse, crate::Error> {
        tracing::trace!(endpoint = endpoint.to_string(), "Sending request");

        let req = self.prepare_request(access_token, endpoint)?;

        client
            .send_request(req)
            .await
            .map_err(super::convert_error)
            .and_then(|response: http::Response<crate::http::Body>| {
                match self {
                    Action::Create { .. } => {
                        let location = obtain_url_from_location(
                            response.headers().get(http::header::LOCATION).cloned(),
                        )?;

                        let sync = match response.status() {
                            StatusCode::CREATED => true,
                            StatusCode::ACCEPTED => false,
                            _ => {
                                return Err(super::Error::unexpected_status_code(
                                    response.status(),
                                    &[StatusCode::CREATED, StatusCode::ACCEPTED],
                                )
                                .into());
                            }
                        };

                        // MAY have a Link value.
                        // FIXME: Parse the returned header value.
                        let rel = microformats::types::Relations::default();

                        Ok(ActionResponse::Created {
                            sync,
                            location,
                            rel,
                        })
                    }
                    Action::Delete(_) => {
                        let expected_codes = [StatusCode::OK, StatusCode::NO_CONTENT];
                        if !expected_codes.contains(&response.status()) {
                            return Err(super::Error::unexpected_status_code(
                                response.status(),
                                &expected_codes,
                            )
                            .into());
                        }

                        let changes = if response.status() == StatusCode::OK {
                            Some(from_json_value(response)?)
                        } else {
                            None
                        };

                        Ok(ActionResponse::Deleted(changes))
                    }

                    Action::Update { .. } => {
                        let (changes, location) = handle_manipulation_response(response)?;
                        Ok(ActionResponse::Updated { changes, location })
                    }
                    Action::Undelete(_) => {
                        let (changes, location) = handle_manipulation_response(response)?;
                        Ok(ActionResponse::Undeleted { changes, location })
                    }
                    #[cfg(feature = "experimental_batch")]
                    Action::Batch { .. } => {
                        let expected_codes = [StatusCode::OK, StatusCode::CREATED, StatusCode::ACCEPTED];
                        if !expected_codes.contains(&response.status()) {
                            return Err(super::Error::unexpected_status_code(
                                response.status(),
                                &expected_codes,
                            )
                            .into());
                        }

                        let batch_response: BatchActionResponse = from_json_value(response)?;
                        Ok(ActionResponse::Batch(batch_response))
                    }
                }
            })
    }

    /// Converts this action into its JSON form.
    pub fn into_json(&self) -> serde_json::Value {
        match self {
            Action::Create { properties, .. } => {
                let mut all_properties: serde_json::Map<String, serde_json::Value> =
                    serde_json::to_value(properties.parameters.clone())
                        .map(|v| v.as_object().cloned().unwrap_or_default())
                        .unwrap_or_default();
                all_properties.extend(properties.extra_fields.0.clone());
                serde_json::json!({
                    "type": [properties.r#type],
                    "properties": all_properties
                })
            }
            Action::Update {
                url: updated_url,
                operation,
            } => {
                let mut all_properties: serde_json::Map<String, serde_json::Value> =
                    Default::default();
                all_properties.insert("action".to_string(), "update".into());
                all_properties.insert("url".to_string(), updated_url.to_string().into());

                let (operation, operation_values) = operation;

                let operational_key = match *operation {
                    UpdateOperation::Replace => "replace".to_string(),
                    UpdateOperation::Add => "add".to_string(),
                    UpdateOperation::Delete => "delete".to_string(),
                };

                all_properties.insert(operational_key, serde_json::json!(operation_values));

                serde_json::json!(all_properties)
            }
            Action::Delete(deleted_url) => serde_json::json!({
                "action": "delete",
                "url": deleted_url
            }),
            Action::Undelete(undeleted_url) => serde_json::json!({
                "action": "undelete",
                "url": undeleted_url
            }),
            #[cfg(feature = "experimental_batch")]
            Action::Batch { actions } => {
                let actions_json: Vec<serde_json::Value> = actions
                    .iter()
                    .map(|a| a.into_json())
                    .collect();
                serde_json::json!({
                    "action": "batch",
                    "actions": actions_json
                })
            }
        }
    }
}

const PROPERTY_ACTION: &str = "action";
const PROPERTY_PROPERTIES: &str = "properties";
const PROPERTY_URL: &str = "url";
const PROPERTY_ADD: &str = "add";
const PROPERTY_DELETE: &str = "delete";
const PROPERTY_REPLACE: &str = "replace";

impl<'de> serde::Deserialize<'de> for Action {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        let mut params: serde_json::Map<String, serde_json::Value> =
            serde_json::Map::deserialize(deserializer)?;
        let action_value = params.remove(PROPERTY_ACTION).unwrap_or_default();

        if action_value == serde_json::Value::String("update".into()) {
            let url_value = params
                .remove(PROPERTY_URL)
                .ok_or_else(|| serde::de::Error::missing_field("url"))
                .and_then(|uv| serde_json::from_value(uv).map_err(serde::de::Error::custom))?;
            let delete_value = params.remove(PROPERTY_DELETE);
            let add_value = params.remove(PROPERTY_ADD);
            let replace_value = params.remove(PROPERTY_REPLACE);

            let operation = if let Some(value) = replace_value {
                serde_json::from_value(value)
                    .map(|rv: OperationValue| (UpdateOperation::Replace, rv))
                    .map_err(serde::de::Error::custom)
            } else if let Some(value) = add_value {
                serde_json::from_value(value)
                    .map(|rv: OperationValue| (UpdateOperation::Add, rv))
                    .map_err(serde::de::Error::custom)
            } else if let Some(value) = delete_value {
                serde_json::from_value(value)
                    .map(|rv: OperationValue| (UpdateOperation::Delete, rv))
                    .map_err(serde::de::Error::custom)
            } else {
                serde_json::from_value(serde_json::Value::Object(params))
                    .map(|rv| (UpdateOperation::Replace, rv))
                    .map_err(serde::de::Error::custom)
            };

            operation.map(|operation| Self::Update {
                url: url_value,
                operation,
            })
        } else if action_value == serde_json::Value::String("delete".into()) {
            params
                .remove(PROPERTY_URL)
                .ok_or_else(|| serde::de::Error::missing_field("url"))
                .and_then(|uv| serde_json::from_value(uv).map_err(serde::de::Error::custom))
                .map(Self::Delete)
        } else if action_value == serde_json::Value::String("undelete".into()) {
            params
                .remove(PROPERTY_URL)
                .ok_or_else(|| serde::de::Error::missing_field("url"))
                .and_then(|uv| serde_json::from_value(uv).map_err(serde::de::Error::custom))
                .map(Self::Undelete)
        } else if action_value == serde_json::Value::String("batch".into()) {
            #[cfg(feature = "experimental_batch")]
            {
                let actions_value = params
                    .remove("actions")
                    .ok_or_else(|| serde::de::Error::missing_field("actions"))?;
                let actions: Vec<Action> = serde_json::from_value(actions_value)
                    .map_err(serde::de::Error::custom)?;
                Ok(Self::Batch { actions })
            }
            #[cfg(not(feature = "experimental_batch"))]
            {
                Err(serde::de::Error::custom(
                    "batch actions require the 'experimental_batch' feature to be enabled",
                ))
            }
        } else {
            let properties = serde_json::from_value(serde_json::Value::Object(params))
                .map_err(serde::de::Error::custom)?;
            Ok(Self::Create {
                properties,
                files: Default::default(),
            })
        }
    }
}

#[test]
fn action_representation_into_json_create() {
    let creation_properties = CreationProperties {
        parameters: Parameters {
            category: vec!["foo".into(), "bar".into()],
            status: Some(crate::standards::micropub::extension::PostStatus::Drafted),
            ..Default::default()
        },
        extra_fields: Properties::try_from(serde_json::json!({
            "content": ["hello world"]
        }))
        .expect("failed to build extra fields of creation properties"),
        r#type: microformats::types::Class::Known(microformats::types::KnownClass::Entry),
    };

    assert_eq!(
        Action::Create {
            properties: Box::new(creation_properties),
            files: Default::default(),
        }
        .into_json(),
        serde_json::json!({
            "type": ["h-entry"],
            "properties": {
                "category": ["foo", "bar"],
                "content": ["hello world"],
                "status": "draft"
            }
        }),
        "converts object into payload JSON"
    )
}

#[test]
fn action_representation_into_json_update_delete() {
    let object = Action::Update {
        url: "http://example.com".parse().unwrap(),
        operation: (
            UpdateOperation::Delete,
            OperationValue::WholeProperties(vec!["content".to_string()]),
        ),
    };

    assert_eq!(
        object.into_json(),
        serde_json::json!({
            "action": "update",
            "url": "http://example.com/",
            "delete": ["content"]
        }),
        "converts object into payload JSON for a update-delete operation"
    )
}

#[test]
fn action_representation_into_json_delete() {
    assert_eq!(
        Action::Delete("http://example.com".parse().unwrap()).into_json(),
        serde_json::json!({
            "action": "delete",
            "url": "http://example.com/",
        }),
        "converts object into payload JSON for a delete operation"
    );
}

#[test]
fn action_representation_into_json_undelete() {
    assert_eq!(
        Action::Undelete("http://example.com".parse().unwrap()).into_json(),
        serde_json::json!({
            "action": "undelete",
            "url": "http://example.com/",
        }),
        "converts object into payload JSON for a undelete operation"
    );
}

#[test]
fn action_representation_create() {
    crate::test::Client::default();
    let creation_properties = CreationProperties {
        parameters: Parameters {
            category: vec!["foo".into(), "bar".into()],
            ..Default::default()
        },
        extra_fields: Properties::try_from(serde_json::json!({
            "content": ["hello world"],
            "photo": ["https://photos.example.com/592829482876343254.jpg"]
        }))
        .expect("failed to build extra fields of creation properties"),
        r#type: microformats::types::Class::Known(microformats::types::KnownClass::Entry),
    };

    assert_eq!(
        serde_json::from_value(serde_json::json!({
          "type": ["h-entry"],
          "properties": {
            "content": ["hello world"],
            "category": ["foo","bar"],
            "photo": ["https://photos.example.com/592829482876343254.jpg"]
          }
        }))
        .map_err(|e| e.to_string()),
        Ok(Action::Create {
            properties: Box::new(creation_properties.clone()),
            files: Default::default()
        }),
        "converts create payload from JSON"
    );

    assert_eq!(
        serde_qs::from_str("h=entry&content=hello+world&category[]=foo&category[]=bar&photo=https://photos.example.com/592829482876343254.jpg")
            .map_err(|e| e.to_string()),
        Ok(Action::Create {
            properties: Box::new(creation_properties.clone()),
            files: Default::default()
        }),
        "converts create payload from form encoded payload"
    );
}

#[test]
fn action_representation_delete() {
    crate::test::Client::default();
    assert_eq!(
        serde_json::from_value(
            serde_json::json!({"action": "delete", "url": "http://example.com"})
        )
        .map_err(|e| e.to_string()),
        Ok(Action::Delete("http://example.com".parse().unwrap())),
        "converts delete payload from JSON"
    );
    assert_eq!(
        serde_json::from_value(
            serde_json::json!({"action": "undelete", "url": "http://example.com"})
        )
        .map_err(|e| e.to_string()),
        Ok(Action::Undelete("http://example.com".parse().unwrap())),
        "converts undelete payload from JSON"
    )
}

#[test]
fn action_representation_update_replace() {
    crate::test::Client::default();

    assert_eq!(
        serde_json::from_value(serde_json::json!({
            "action": "update",
            "url": "http://example.com",
            "replace": {
                "category": ["tag1", "tag2"]
            }
        }))
        .map_err(|e| e.to_string()),
        Ok(Action::Update {
            url: "http://example.com".parse().unwrap(),
            operation: (
                UpdateOperation::Replace,
                serde_json::from_value(serde_json::json!({
                    "category": ["tag1", "tag2"]
                }))
                .unwrap()
            )
        }),
        "converts upload payload for replace from JSON"
    );
}

#[test]
fn action_representation_update_delete() {
    crate::test::Client::default();
    assert_eq!(
        serde_json::from_value(serde_json::json!({
            "action": "update",
            "url": "http://example.com",
            "delete": {
                "category": ["tag1", "tag2"]
            }
        }))
        .map_err(|e| e.to_string()),
        Ok(Action::Update {
            url: "http://example.com".parse().unwrap(),
            operation: (
                UpdateOperation::Delete,
                serde_json::from_value(serde_json::json!({
                    "category": ["tag1", "tag2"]
                }))
                .unwrap()
            )
        }),
        "converts upload payload from delete from JSON"
    );
}

#[test]
fn action_representation_update_replace_whole() {
    crate::test::Client::default();
    assert_eq!(
        serde_json::from_value(serde_json::json!({
            "action": "update",
            "url": "http://example.com",
            "add": ["category"]
        }))
        .map_err(|e| e.to_string()),
        Ok(Action::Update {
            url: "http://example.com".parse().unwrap(),
            operation: (
                UpdateOperation::Add,
                serde_json::from_value(serde_json::json!(["category"])).unwrap()
            )
        }),
        "converts upload payload from add from JSON"
    );
}

/// Represents a collection of the known response sttes of a Micropub server.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ActionResponse {
    // The creation response.
    // <https://micropub.spec.indieweb.org/#x3-3-6-response>
    Created {
        sync: bool,
        location: url::Url,
        rel: microformats::types::Relations,
    },

    // The updating response.
    // <https://micropub.spec.indieweb.org/#x3-4-4-response>
    Updated {
        changes: Option<serde_json::Value>,
        location: Option<url::Url>,
    },

    // The deletion response.
    // <https://micropub.spec.indieweb.org/#x3-5-1-response>
    Deleted(Option<serde_json::Value>),

    // The undeletion response.
    // <https://micropub.spec.indieweb.org/#x3-5-1-response>
    Undeleted {
        location: Option<url::Url>,
        changes: Option<serde_json::Value>,
    },

    /// The batch operation response.
    /// Contains individual results for each action in the batch.
    ///
    /// See <https://github.com/indieweb/micropub-extensions/issues/52>
    #[cfg(feature = "experimental_batch")]
    Batch(BatchActionResponse),
}

mod test;
