use std::borrow::Cow;

use proc_macro2::TokenStream;
use proc_macro_error::abort;
use quote::{quote, ToTokens};
use syn::{
    parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Field, Generics, Ident,
};

use crate::{
    component::{
        self,
        features::{
            self, AdditionalProperties, AllowReserved, Example, ExclusiveMaximum, ExclusiveMinimum,
            Explode, Format, Inline, MaxItems, MaxLength, Maximum, MinItems, MinLength, Minimum,
            MultipleOf, Names, Nullable, Pattern, ReadOnly, Rename, RenameAll, SchemaWith, Style,
            WriteOnly, XmlAttr,
        },
        FieldRename,
    },
    doc_comment::CommentAttributes,
    Array, Required, ResultExt,
};

use super::{
    features::{
        impl_into_inner, impl_merge, parse_features, pop_feature, pop_feature_as_inner, Feature,
        FeaturesExt, IntoInner, Merge, ToTokensExt,
    },
    serde::{self, SerdeContainer, SerdeValue},
    ComponentSchema, TypeTree,
};

impl_merge!(IntoParamsFeatures, FieldFeatures);

/// Container attribute `#[into_params(...)]`.
pub struct IntoParamsFeatures(Vec<Feature>);

impl Parse for IntoParamsFeatures {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self(parse_features!(
            input as Style,
            features::ParameterIn,
            Names,
            RenameAll
        )))
    }
}

impl_into_inner!(IntoParamsFeatures);

#[cfg_attr(feature = "debug", derive(Debug))]
pub struct IntoParams {
    /// Attributes tagged on the whole struct or enum.
    pub attrs: Vec<Attribute>,
    /// Generics required to complete the definition.
    pub generics: Generics,
    /// Data within the struct or enum.
    pub data: Data,
    /// Name of the struct or enum.
    pub ident: Ident,
}

impl ToTokens for IntoParams {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let ident = &self.ident;
        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();

        let mut into_params_features = self
            .attrs
            .iter()
            .filter(|attr| attr.path().is_ident("into_params"))
            .map(|attribute| {
                attribute
                    .parse_args::<IntoParamsFeatures>()
                    .unwrap_or_abort()
                    .into_inner()
            })
            .reduce(|acc, item| acc.merge(item));
        let serde_container = serde::parse_container(&self.attrs);

        // #[param] is only supported over fields
        if self.attrs.iter().any(|attr| attr.path().is_ident("param")) {
            abort! {
                ident,
                "found `param` attribute in unsupported context";
                help = "Did you mean `into_params`?",
            }
        }

        let names = into_params_features.as_mut().and_then(|features| {
            features
                .pop_by(|feature| matches!(feature, Feature::IntoParamsNames(_)))
                .and_then(|feature| match feature {
                    Feature::IntoParamsNames(names) => Some(names.into_values()),
                    _ => None,
                })
        });

        let style = pop_feature!(into_params_features => Feature::Style(_));
        let parameter_in = pop_feature!(into_params_features => Feature::ParameterIn(_));
        let rename_all = pop_feature!(into_params_features => Feature::RenameAll(_));

        let params = self
            .get_struct_fields(&names.as_ref())
            .enumerate()
            .filter_map(|(index, field)| {
                let field_params = serde::parse_value(&field.attrs);
                if matches!(&field_params, Some(params) if !params.skip) {
                    Some((index, field, field_params))
                } else {
                    None
                }
            })
            .map(|(index, field, field_serde_params)| {
                Param {
                    field,
                    field_serde_params,
                    container_attributes: FieldParamContainerAttributes {
                        rename_all: rename_all.as_ref().and_then(|feature| {
                            match feature {
                                Feature::RenameAll(rename_all) => Some(rename_all),
                                _ => None
                            }
                        }),
                        style: &style,
                        parameter_in: &parameter_in,
                        name: names.as_ref()
                            .map(|names| names.get(index).unwrap_or_else(|| abort!(
                                ident,
                                "There is no name specified in the names(...) container attribute for tuple struct field {}",
                                index
                            ))),
                    },
                    serde_container: serde_container.as_ref(),
                }
            })
            .collect::<Array<Param>>();

        tokens.extend(quote! {
            impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause {
                fn into_params(parameter_in_provider: impl Fn() -> Option<utoipa::openapi::path::ParameterIn>) -> Vec<utoipa::openapi::path::Parameter> {
                    #params.to_vec()
                }
            }
        });
    }
}

impl IntoParams {
    fn get_struct_fields(
        &self,
        field_names: &Option<&Vec<String>>,
    ) -> impl Iterator<Item = &Field> {
        let ident = &self.ident;
        let abort = |note: &str| {
            abort! {
                ident,
                "unsupported data type, expected struct with named fields `struct {} {{...}}` or unnamed fields `struct {}(...)`",
                ident.to_string(),
                ident.to_string();
                note = note
            }
        };

        match &self.data {
            Data::Struct(data_struct) => match &data_struct.fields {
                syn::Fields::Named(named_fields) => {
                    if field_names.is_some() {
                        abort! {ident, "`#[into_params(names(...))]` is not supported attribute on a struct with named fields"}
                    }
                    named_fields.named.iter()
                }
                syn::Fields::Unnamed(unnamed_fields) => {
                    self.validate_unnamed_field_names(&unnamed_fields.unnamed, field_names);
                    unnamed_fields.unnamed.iter()
                }
                _ => abort("Unit type struct is not supported"),
            },
            _ => abort("Only struct type is supported"),
        }
    }

    fn validate_unnamed_field_names(
        &self,
        unnamed_fields: &Punctuated<Field, Comma>,
        field_names: &Option<&Vec<String>>,
    ) {
        let ident = &self.ident;
        match field_names {
            Some(names) => {
                if names.len() != unnamed_fields.len() {
                    abort! {
                        ident,
                        "declared names amount '{}' does not match to the unnamed fields amount '{}' in type: {}",
                            names.len(), unnamed_fields.len(), ident;
                        help = r#"Did you forget to add a field name to `#[into_params(names(... , "field_name"))]`"#;
                        help = "Or have you added extra name but haven't defined a type?"
                    }
                }
            }
            None => {
                abort! {
                    ident,
                    "struct with unnamed fields must have explicit name declarations.";
                    help = "Try defining `#[into_params(names(...))]` over your type: {}", ident,
                }
            }
        }
    }
}

#[cfg_attr(feature = "debug", derive(Debug))]
pub struct FieldParamContainerAttributes<'a> {
    /// See [`IntoParamsAttr::style`].
    style: &'a Option<Feature>,
    /// See [`IntoParamsAttr::names`]. The name that applies to this field.
    name: Option<&'a String>,
    /// See [`IntoParamsAttr::parameter_in`].
    parameter_in: &'a Option<Feature>,
    /// Custom rename all if serde attribute is not present.
    rename_all: Option<&'a RenameAll>,
}

struct FieldFeatures(Vec<Feature>);

impl_into_inner!(FieldFeatures);

impl Parse for FieldFeatures {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self(parse_features!(
            // param features
            input as component::features::ValueType,
            Rename,
            Style,
            AllowReserved,
            Example,
            Explode,
            SchemaWith,
            component::features::Required,
            // param schema features
            Inline,
            Format,
            component::features::Default,
            WriteOnly,
            ReadOnly,
            Nullable,
            XmlAttr,
            MultipleOf,
            Maximum,
            Minimum,
            ExclusiveMaximum,
            ExclusiveMinimum,
            MaxLength,
            MinLength,
            Pattern,
            MaxItems,
            MinItems,
            AdditionalProperties
        )))
    }
}

#[cfg_attr(feature = "debug", derive(Debug))]
struct Param<'a> {
    /// Field in the container used to create a single parameter.
    field: &'a Field,
    //// Field serde params parsed from field attributes.
    field_serde_params: Option<SerdeValue>,
    /// Attributes on the container which are relevant for this macro.
    container_attributes: FieldParamContainerAttributes<'a>,
    /// Either serde rename all rule or into_params rename all rule if provided.
    serde_container: Option<&'a SerdeContainer>,
}

impl Param<'_> {
    /// Resolve [`Param`] features and split features into two [`Vec`]s. Features are split by
    /// whether they should be rendered in [`Param`] itself or in [`Param`]s schema.
    ///
    /// Method returns a tuple containing two [`Vec`]s of [`Feature`].
    fn resolve_field_features(&self) -> (Vec<Feature>, Vec<Feature>) {
        let mut field_features = self
            .field
            .attrs
            .iter()
            .filter(|attribute| attribute.path().is_ident("param"))
            .map(|attribute| {
                attribute
                    .parse_args::<FieldFeatures>()
                    .unwrap_or_abort()
                    .into_inner()
            })
            .reduce(|acc, item| acc.merge(item))
            .unwrap_or_default();

        if let Some(ref style) = self.container_attributes.style {
            if !field_features
                .iter()
                .any(|feature| matches!(&feature, Feature::Style(_)))
            {
                field_features.push(style.clone()); // could try to use cow to avoid cloning
            };
        }

        field_features.into_iter().fold(
            (Vec::<Feature>::new(), Vec::<Feature>::new()),
            |(mut schema_features, mut param_features), feature| {
                match feature {
                    Feature::Inline(_)
                    | Feature::Format(_)
                    | Feature::Default(_)
                    | Feature::WriteOnly(_)
                    | Feature::ReadOnly(_)
                    | Feature::Nullable(_)
                    | Feature::XmlAttr(_)
                    | Feature::MultipleOf(_)
                    | Feature::Maximum(_)
                    | Feature::Minimum(_)
                    | Feature::ExclusiveMaximum(_)
                    | Feature::ExclusiveMinimum(_)
                    | Feature::MaxLength(_)
                    | Feature::MinLength(_)
                    | Feature::Pattern(_)
                    | Feature::MaxItems(_)
                    | Feature::MinItems(_)
                    | Feature::AdditionalProperties(_) => {
                        schema_features.push(feature);
                    }
                    _ => {
                        param_features.push(feature);
                    }
                };

                (schema_features, param_features)
            },
        )
    }
}

impl ToTokens for Param<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let field = self.field;
        let field_serde_params = &self.field_serde_params;
        let ident = &field.ident;
        let mut name = &*ident
            .as_ref()
            .map(|ident| ident.to_string())
            .or_else(|| self.container_attributes.name.cloned())
            .unwrap_or_else(|| abort!(
                field, "No name specified for unnamed field.";
                help = "Try adding #[into_params(names(...))] container attribute to specify the name for this field"
            ));

        if name.starts_with("r#") {
            name = &name[2..];
        }

        let (schema_features, mut param_features) = self.resolve_field_features();

        let rename = param_features
            .pop_rename_feature()
            .map(|rename| rename.into_value());
        let rename_to = field_serde_params
            .as_ref()
            .and_then(|field_param_serde| field_param_serde.rename.as_deref().map(Cow::Borrowed))
            .or_else(|| rename.map(Cow::Owned));
        let rename_all = self
            .serde_container
            .as_ref()
            .and_then(|serde_container| serde_container.rename_all.as_ref())
            .or_else(|| {
                self.container_attributes
                    .rename_all
                    .map(|rename_all| rename_all.as_rename_rule())
            });
        let name = super::rename::<FieldRename>(name, rename_to, rename_all)
            .unwrap_or(Cow::Borrowed(name));
        let type_tree = TypeTree::from_type(&field.ty);

        tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new()
            .name(#name)
        });
        tokens.extend(
            if let Some(ref parameter_in) = self.container_attributes.parameter_in {
                parameter_in.into_token_stream()
            } else {
                quote! {
                    .parameter_in(parameter_in_provider().unwrap_or_default())
                }
            },
        );

        if let Some(deprecated) = super::get_deprecated(&field.attrs) {
            tokens.extend(quote! { .deprecated(Some(#deprecated)) });
        }

        let schema_with = pop_feature!(param_features => Feature::SchemaWith(_));
        if let Some(schema_with) = schema_with {
            tokens.extend(quote! { .schema(Some(#schema_with)).build() });
        } else {
            let description =
                CommentAttributes::from_attributes(&field.attrs).as_formatted_string();
            if !description.is_empty() {
                tokens.extend(quote! { .description(Some(#description))})
            }

            let value_type = param_features.pop_value_type_feature();
            let component = value_type
                .as_ref()
                .map(|value_type| value_type.as_type_tree())
                .unwrap_or(type_tree);

            let required = pop_feature_as_inner!(param_features => Feature::Required(_v))
                .as_ref()
                .map(super::features::Required::is_true)
                .unwrap_or(false);

            let non_required = (component.is_option() && !required)
                || !component::is_required(field_serde_params.as_ref(), self.serde_container);
            let required: Required = (!non_required).into();

            tokens.extend(quote! {
                .required(#required)
            });
            tokens.extend(param_features.to_token_stream());

            let schema = ComponentSchema::new(component::ComponentSchemaProps {
                type_tree: &component,
                features: Some(schema_features),
                description: None,
                deprecated: None,
                object_name: "",
            });

            tokens.extend(quote! { .schema(Some(#schema)).build() });
        }
    }
}
