use crate::{
    algorithms::ptd::Type,
    http::{Client, CONTENT_TYPE_JSON},
    standards::{indieauth::AccessToken, micropub::convert_error},
    traits::as_string_or_list,
};
use serde::{Deserialize, Serialize};

use super::{
    extension::{self, PostStatus, Visibility},
    paging,
};
use microformats::types::Item;
use secrecy::ExposeSecret;
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Query {
    /// Pagination options to modify where in the list to look
    #[serde(flatten)]
    pub pagination: paging::Query,

    /// A representation of the base querying components.
    #[serde(flatten)]
    pub kind: QueryKind,
}

#[derive(Debug, Clone, Default, Eq)]
pub struct MatchingPropertyValuesMap(HashMap<String, Vec<serde_json::Value>>);

impl std::ops::Deref for MatchingPropertyValuesMap {
    type Target = HashMap<String, Vec<serde_json::Value>>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::hash::Hash for MatchingPropertyValuesMap {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        serde_json::to_string(&self.0)
            .unwrap_or_default()
            .hash(state);
    }
}

impl PartialEq for MatchingPropertyValuesMap {
    fn eq(&self, other: &Self) -> bool {
        self.0.eq(&other.0)
    }
}

const KNOWN_SOURCE_QUERY_PARAMS: &[&str] = &[
    "url",
    "destination",
    "post_type",
    "post-status",
    "post_status",
    "visibility",
    "audience",
    "syndicate_to",
    "syndicate-to",
    "category",
    "exists",
    "not_exists",
    "not-exists",
    "filter",
    "limit",
    "offset",
    "before",
    "after",
    "order",
    "q",
    "h",
    "type",
    "channel",
    "mp-channel",
    "mime_type",
    "mime-type",
];

fn is_known_query_param(key: &str) -> bool {
    KNOWN_SOURCE_QUERY_PARAMS
        .iter()
        .any(|&known| key == known || key == known.replace('_', "-"))
}

struct PropertyValueVisitor;

impl<'de> serde::de::Visitor<'de> for PropertyValueVisitor {
    type Value = MatchingPropertyValuesMap;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("expecting a map of property filters (with 'property-' prefix or shorthand)")
    }

    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    where
        A: serde::de::MapAccess<'de>,
    {
        let mut props = HashMap::<String, Vec<serde_json::Value>>::default();
        
        while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
            let (property_name, should_capture) = if key.starts_with("property-") {
                (key.replace("property-", ""), true)
            } else if !is_known_query_param(&key) {
                (key, true)
            } else {
                (key, false)
            };
            
            if !should_capture {
                continue;
            }
            
            let values = value
                .as_array()
                .cloned()
                .unwrap_or_else(|| vec![value.clone()]);
            if let Some(list) = props.get_mut(&property_name) {
                list.extend(values);
            } else {
                props.insert(property_name, values);
            }
        }

        Ok(MatchingPropertyValuesMap(HashMap::from_iter(
            props
                .iter()
                .map(|(name, values)| (name.to_owned(), values.to_vec()))
                .collect::<Vec<_>>()
                .into_iter()
                .rev(),
        )))
    }
}

impl From<HashMap<String, Vec<serde_json::Value>>> for MatchingPropertyValuesMap {
    fn from(v: HashMap<String, Vec<serde_json::Value>>) -> Self {
        Self(v)
    }
}

impl<'de> serde::Deserialize<'de> for MatchingPropertyValuesMap {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        deserializer.deserialize_map(PropertyValueVisitor)
    }
}

impl serde::Serialize for MatchingPropertyValuesMap {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(self.0.len()))?;
        for (k, v) in &self.0 {
            serde::ser::SerializeMap::serialize_entry(&mut map, &format!("property-{}", k), &v)?;
        }
        serde::ser::SerializeMap::end(map)
    }
}

impl TryFrom<serde_json::Value> for MatchingPropertyValuesMap {
    type Error = serde_json::Error;

    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
        serde_json::from_value(value)
    }
}

impl FromIterator<(String, Vec<serde_json::Value>)> for MatchingPropertyValuesMap {
    fn from_iter<T: IntoIterator<Item = (String, Vec<serde_json::Value>)>>(iter: T) -> Self {
        Self(HashMap::from_iter(iter))
    }
}

impl IntoIterator for MatchingPropertyValuesMap {
    type Item = (String, Vec<serde_json::Value>);
    type IntoIter = std::collections::hash_map::IntoIter<String, Vec<serde_json::Value>>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

#[test]
fn matching_property_values_map() {
    assert_eq!(
        serde_json::from_str::<MatchingPropertyValuesMap>(
            r#"
    {
        "property-foo": null,
        "property-bar": [3, "jump"]
    }
    "#
        )
        .map_err(|e| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::from_iter([
            ("foo".to_string(), vec![serde_json::Value::Null]),
            (
                "bar".to_string(),
                vec![
                    serde_json::Value::Number(3.into()),
                    serde_json::Value::String("jump".to_string())
                ]
            )
        ])))
        .map_err(|e: serde_json::Error| e.to_string()),
        "deserializing from JSON with property- prefix"
    );

    assert_eq!(
        serde_qs::from_str::<MatchingPropertyValuesMap>(
            "property-foo=kick&property-bar[]=jump&property-bar[]=high"
        )
        .map_err(|e: serde_qs::Error| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::from_iter([
            (
                "foo".to_string(),
                vec![serde_json::Value::String("kick".to_string())]
            ),
            (
                "bar".to_string(),
                vec![
                    serde_json::Value::String("jump".to_string()),
                    serde_json::Value::String("high".to_string()),
                ]
            )
        ]))),
        "deserializing from query string with property- prefix"
    );
}

#[test]
fn matching_property_values_map_shorthand() {
    assert_eq!(
        serde_json::from_str::<MatchingPropertyValuesMap>(
            r#"{ "rsvp": "yes" }"#
        )
        .map_err(|e| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::from_iter([
            ("rsvp".to_string(), vec![serde_json::Value::String("yes".to_string())])
        ]))),
        "deserializing shorthand property from JSON"
    );

    assert_eq!(
        serde_qs::from_str::<MatchingPropertyValuesMap>("rsvp=yes")
            .map_err(|e: serde_qs::Error| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::from_iter([
            ("rsvp".to_string(), vec![serde_json::Value::String("yes".to_string())])
        ]))),
        "deserializing shorthand property from query string"
    );

    assert_eq!(
        serde_json::from_str::<MatchingPropertyValuesMap>(
            r#"{ "property-foo": "bar", "rsvp": "yes" }"#
        )
        .map_err(|e| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::from_iter([
            ("foo".to_string(), vec![serde_json::Value::String("bar".to_string())]),
            ("rsvp".to_string(), vec![serde_json::Value::String("yes".to_string())])
        ]))),
        "deserializing mixed explicit and shorthand properties from JSON"
    );

    assert_eq!(
        serde_json::from_str::<MatchingPropertyValuesMap>(
            r#"{ "url": "https://example.com" }"#
        )
        .map_err(|e| e.to_string()),
        Ok(MatchingPropertyValuesMap(HashMap::new())),
        "known query params should not be captured as property filters"
    );
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub struct SourceQuery {
    /// Properties to match in this query.
    #[serde(flatten)]
    pub matching_properties: MatchingPropertyValuesMap,

    /// The URL of the resource being discovered.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub url: Option<url::Url>,

    /// Represents the destination of this item when filtering this query.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub destination: Option<url::Url>,

    /// Represents one or many post types to ask for when filtering this query.
    #[serde(
        with = "as_string_or_list",
        skip_serializing_if = "Vec::is_empty",
        default
    )]
    pub post_type: Vec<crate::algorithms::ptd::Type>,

    /// Represents the expected post status of the items returned when filtering this query.
    #[serde(
        default,
        skip_serializing_if = "SourceQuery::is_non_serializable_status"
    )]
    pub post_status: Option<PostStatus>,

    /// Provides a list of the associated audiences to look for when filtering this query.
    #[serde(
        with = "as_string_or_list",
        default,
        skip_serializing_if = "Vec::is_empty"
    )]
    pub audience: Vec<String>,

    /// Represents the expected visibilities of the items returned when filtering this query.
    #[serde(
        default,
        skip_serializing_if = "SourceQuery::is_non_serializable_visibility"
    )]
    pub visibility: Option<Visibility>,

    /// Represents one or many channels to ask for when filtering this query.
    #[serde(
        skip_serializing_if = "Vec::is_empty",
        default,
        with = "as_string_or_list"
    )]
    #[cfg(feature = "experimental_channels")]
    pub channel: Vec<extension::channel::Form>,

    /// Represents one or many targets used for syndication when filtering this query.
    #[serde(
        skip_serializing_if = "Vec::is_empty",
        default,
        with = "as_string_or_list"
    )]
    pub syndicate_to: Vec<String>,

    /// Represents one or many categories when filtering this query.
    #[serde(
        skip_serializing_if = "Vec::is_empty",
        default,
        with = "as_string_or_list"
    )]
    pub category: Vec<String>,

    /// Represents one or many properties to check for the presence of.
    #[serde(
        skip_serializing_if = "Vec::is_empty",
        default,
        with = "as_string_or_list"
    )]
    pub exists: Vec<String>,

    /// Represents one or many properties to check for the lack of a presence of.
    #[serde(
        skip_serializing_if = "Vec::is_empty",
        default,
        with = "as_string_or_list"
    )]
    pub not_exists: Vec<String>,

    /// Represents a value to filter against.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub filter: Option<String>,

    /// Filter media by MIME type (e.g., "image", "image/jpeg", "image/*").
    ///
    /// Supports:
    /// - Full MIME type: "image/jpeg"
    /// - Type only: "image" (matches any subtype)
    /// - Wildcard: "image/*" or "*/*"
    ///
    /// See <https://github.com/indieweb/micropub-extensions/issues/45>
    #[cfg(feature = "experimental_media_query")]
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub mime_type: Option<extension::MimeType>,
}

impl SourceQuery {
    pub fn post_status_is_published_by_default() -> PostStatus {
        PostStatus::Published
    }

    pub fn visibility_is_public_by_default() -> Visibility {
        Visibility::Public
    }

    pub fn is_non_serializable_visibility(visibility: &Option<Visibility>) -> bool {
        visibility.is_none() || *visibility != Some(Visibility::Public)
    }

    pub fn is_default_post_type(post_types: &Vec<Type>) -> bool {
        *post_types == vec![Type::default()]
    }

    pub fn is_non_serializable_status(post_status: &Option<PostStatus>) -> bool {
        post_status.is_none() || *post_status != Some(PostStatus::default())
    }
}

#[derive(Serialize, Deserialize, Debug, Eq)]
#[serde(rename_all = "kebab-case", tag = "q", deny_unknown_fields)]
#[non_exhaustive]
pub enum QueryKind {
    /// Pulls the configuration of the Micropub server.
    #[serde(rename = "config")]
    Configuration,

    /// Represents a call to `?q=source` for post information
    Source(Box<SourceQuery>),

    /// Represents a call to `?q=channel` to fetch channels.
    /// To learn about channels, view <https://github.com/indieweb/micropub-extensions/issues/40>
    #[cfg(feature = "experimental_channels")]
    Channel,

    /// Represents a call to `?q=syndicate-to` to fetch syndication targets.
    #[serde(rename_all = "kebab-case")]
    #[cfg(feature = "experimental_syndication")]
    SyndicateTo {
        /// Represents a single or many post types to ask for when filtering this query.
        #[serde(with = "as_string_or_list", skip_serializing_if = "Vec::is_empty")]
        post_type: Vec<Type>,
    },

    /// Represents a call to `?q=rel` to fetch relationship links.
    #[serde(alias = "rel", rename_all = "kebab-case")]
    #[cfg(feature = "experimental_relation")]
    Relation {
        /// The relations to explicitly look for.
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        rel: Vec<String>,

        /// The identity to look up relationships for.
        url: url::Url,
    },

    /// Represents a call to `?q=category` to fetch tags.
    Category,
}

impl PartialEq for QueryKind {
    fn eq(&self, other: &Self) -> bool {
        use std::mem::discriminant;
        discriminant(self) == discriminant(other)
    }
}

impl Query {
    /// 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<Response, crate::Error> {
        let mut url = endpoint.clone();
        let self_string = serde_qs::to_string(&self).map_err(crate::Error::Qs)?;

        if url.query().is_none() {
            url.set_query(Some(self_string.as_str()));
        } else {
            url.query_pairs_mut()
                .extend_pairs(url::form_urlencoded::parse(self_string.as_bytes()));
        }

        tracing::trace!(
            endpoint = format!("{endpoint}"),
            query = self_string,
            "Sending request to Micropub server"
        );

        let req = http::Request::get(url.as_str())
            .header(http::header::ACCEPT, CONTENT_TYPE_JSON)
            .header(
                http::header::AUTHORIZATION,
                format!("Bearer {}", access_token.expose_secret()),
            )
            .body(crate::http::Body::Empty)?;

        let resp = client.send_request(req).await?;

        tracing::debug!(?resp, "Received Micropub query response");

        crate::http::from_json_value(resp)
            .and_then(|response: Response| Result::<Response, crate::Error>::from(response))
            .map_err(convert_error)
    }
}

/// A representation of what can be returned from a configuration request.
///
/// View <https://micropub.spec.indieweb.org/#configuration> for more information.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ConfigurationResponse {
    /// A list of the supported queries available to the querying client.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub q: Vec<String>,

    /// A list of channels available for use to the querying client.
    #[cfg(feature = "experimental_channels")]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub channels: Vec<extension::channel::Form>,

    /// The media endpoint suggested for use with this Micropub server.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media_endpoint: Option<url::Url>,

    /// The supported post types this Micropub server recognizes.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub post_types: Vec<PostTypeInfo>,

    /// The available syndication targets from the remote Micropub server.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[cfg(feature = "experimental_syndication")]
    pub syndicate_to: Vec<extension::syndication::Target>,

    /// The categories available to the querying client.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub category: Vec<String>,

    /// The list of Micropub extensions supported by this server.
    ///
    /// Applications can populate this field using `ExtensionRegistry::new().extensions()`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub extensions: Vec<String>,
}


/// Information about a supported post type.
///
/// Can be either a simple type identifier or an extended object with metadata.
/// This allows backward compatibility with simple type lists while supporting
/// extended metadata when available.
///
/// View <https://micropub.spec.indieweb.org/#supported-vocabulary> for more information.
#[derive(PartialEq, Debug, Clone)]
pub enum PostTypeInfo {
    /// Simple post type identifier (backward compatible)
    Simple(Type),
    /// Extended post type with metadata
    Extended {
        /// The post type
        r#type: Type,
        /// Human-readable name for the post type
        name: String,
        /// Optional list of properties this post type supports
        properties: Option<Vec<String>>,
    },
}

impl Serialize for PostTypeInfo {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            PostTypeInfo::Simple(t) => t.serialize(serializer),
            PostTypeInfo::Extended {
                r#type,
                name,
                properties,
            } => {
                use serde::ser::SerializeMap;
                let mut map =
                    serializer.serialize_map(Some(if properties.is_some() { 3 } else { 2 }))?;
                map.serialize_entry("type", r#type)?;
                map.serialize_entry("name", name)?;
                if let Some(props) = properties {
                    map.serialize_entry("properties", props)?;
                }
                map.end()
            }
        }
    }
}

impl<'de> Deserialize<'de> for PostTypeInfo {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::Error;
        let value = serde_json::Value::deserialize(deserializer)?;

        // Try to deserialize as simple Type first (backward compatibility)
        if let Ok(t) = serde_json::from_value::<Type>(value.clone()) {
            return Ok(PostTypeInfo::Simple(t));
        }

        // Otherwise, try as extended object
        if let Some(obj) = value.as_object() {
            let r#type = obj
                .get("type")
                .ok_or_else(|| D::Error::missing_field("type"))
                .and_then(|v| serde_json::from_value(v.clone()).map_err(D::Error::custom))?;
            let name = obj
                .get("name")
                .ok_or_else(|| D::Error::missing_field("name"))
                .and_then(|v| serde_json::from_value(v.clone()).map_err(D::Error::custom))?;
            let properties = obj
                .get("properties")
                .map(|v| serde_json::from_value(v.clone()))
                .transpose()
                .map_err(D::Error::custom)?;

            return Ok(PostTypeInfo::Extended {
                r#type,
                name,
                properties,
            });
        }

        Err(D::Error::custom(
            "expected string or object for PostTypeInfo",
        ))
    }
}

impl From<Type> for PostTypeInfo {
    fn from(t: Type) -> Self {
        PostTypeInfo::Simple(t)
    }
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct SourceResponse {
    /// The hinted post type.
    #[serde(
        default,
        rename = "post-type",
        with = "as_string_or_list",
        skip_serializing_if = "Vec::is_empty"
    )]
    pub post_type: Vec<Type>,

    /// The [Microformats][microformats] item returned in this request.
    #[serde(flatten)]
    pub item: Item,
}


/// Response containing a list of posts from a source query.
///
/// Returned when querying for multiple posts (e.g., `?q=source` without a specific URL).
/// View <https://micropub.spec.indieweb.org/#source-content> for more information.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct SourceListResponse {
    /// The list of items matching the query
    pub items: Vec<SourceResponse>,

    /// Pagination information for navigating the list
    #[serde(flatten, skip_serializing_if = "paging::Fields::is_empty")]
    pub paging: paging::Fields,
}


#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct CategoryResponse {
    pub categories: Vec<String>,

    #[serde(flatten, skip_serializing_if = "paging::Fields::is_empty")]
    pagination: paging::Fields,
}

/// Response containing syndication targets from a syndicate-to query.
///
/// View <https://micropub.spec.indieweb.org/#syndication-targets> for more information.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg(feature = "experimental_syndication")]
pub struct SyndicationTargetsResponse {
    /// The list of syndication targets available to the querying client.
    pub syndicate_to: Vec<extension::syndication::Target>,
}

/// Represents the response for a query.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum Response {
    Source(SourceResponse),
    SourceList(SourceListResponse),
    #[cfg(feature = "experimental_channels")]
    Channel(extension::channel::QueryResponse),
    Category(CategoryResponse),
    #[cfg(feature = "experimental_syndication")]
    SyndicateTo(SyndicationTargetsResponse),
    Configuration(ConfigurationResponse),
    Paginated(paging::Response),
    Error(super::Error),
}

impl From<Response> for Result<Response, crate::Error> {
    fn from(value: Response) -> Self {
        if let Response::Error(e) = value {
            Err(crate::Error::Micropub(e))
        } else {
            Ok(value)
        }
    }
}

#[cfg(test)]
mod test;
