use crate::model::{
    ElementId, GlobalType, GlobalTypeProperty, GlobalTypeValue, InputProperty, IntegerEnumElement,
    NumberEnumElement, OutputProperty, Provider, Ref, StringEnumElement,
};
use crate::utils::fix_description;
use anyhow::{Context, Result, anyhow};
use convert_case::{Case, Casing};
use pulumi_gestalt_serde_constant_string::generate_string_const;
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet, HashSet};

type PulumiMap<T> = BTreeMap<String, T>;

#[derive(Deserialize, Debug)]
pub(crate) enum TypeEnum {
    #[serde(alias = "boolean")]
    Boolean,
    #[serde(alias = "integer")]
    Integer,
    #[serde(alias = "number")]
    Number,
    #[serde(alias = "string")]
    String,
    #[serde(alias = "array")]
    Array,
    #[serde(alias = "object")]
    Object,
    #[serde(untagged)]
    Other(String),
}

#[derive(Deserialize, Debug)]
struct Type {
    #[serde(rename = "type")]
    type_: Option<TypeEnum>,
    description: Option<String>,
    #[serde(rename = "$ref")]
    ref_: Option<String>,
    items: Option<Box<Type>>,
    #[serde(rename = "additionalProperties")]
    additional_properties: Option<Box<Type>>,
    #[serde(rename = "oneOf")]
    one_of: Option<Vec<OneOfType>>,
    #[serde(rename = "const")]
    const_: Option<String>,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum OneOfType {
    Ref(OneOfTypeRef),
    Primitive(OneOfTypePrimitive),
    Array(OneOfTypeArray),
    Object(OneOfTypeObject),
}

#[derive(Deserialize, Debug)]
struct OneOfTypeRef {
    #[serde(rename = "$ref")]
    ref_: String,
}

#[derive(Deserialize, Debug)]
struct OneOfTypePrimitive {
    #[serde(rename = "type")]
    type_: OneOfTypePrimitiveType,
}

#[derive(Deserialize, Debug)]
struct OneOfTypeArray {
    #[serde(rename = "type")]
    #[allow(dead_code)]
    type_: ArrayConstant,
    items: OneOfTypePrimitive,
}

#[derive(Deserialize, Debug)]
struct OneOfTypeObject {
    #[serde(rename = "type")]
    #[allow(dead_code)]
    type_: ObjectConstant,
    #[serde(rename = "additionalProperties")]
    additional_properties: OneOfTypePrimitive,
}

generate_string_const!(ObjectConstant, "object");
generate_string_const!(ArrayConstant, "array");

#[derive(Deserialize, Debug)]
enum OneOfTypePrimitiveType {
    #[serde(rename = "string")]
    String,
}

#[derive(Deserialize, Debug)]
struct Property {
    #[serde(flatten)]
    r#type: Type,
}

#[derive(Deserialize, Debug)]
struct ObjectType {
    description: Option<String>,
    r#type: Option<TypeEnum>,
    #[serde(default)]
    properties: PulumiMap<Property>,
    #[serde(default)]
    required: BTreeSet<String>,
    #[serde(rename = "enum")]
    enum_: Option<ObjectTypeEnum>,
}

#[derive(Deserialize, Debug)]
struct Resource {
    #[serde(flatten)]
    object_type: ObjectType,
    #[serde(default, rename = "inputProperties")]
    input_properties: PulumiMap<Property>,
    #[serde(default, rename = "requiredInputs")]
    required_inputs: BTreeSet<String>,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum ObjectTypeEnum {
    String(Vec<StringEnumValue>),
    Integer(Vec<IntegerEnumValue>),
    Number(Vec<NumberEnumValue>),
}

#[derive(Deserialize, Debug)]
struct IntegerEnumValue {
    name: String,
    description: Option<String>,
    value: i64,
}

#[derive(Deserialize, Debug)]
struct NumberEnumValue {
    name: String,
    description: Option<String>,
    value: f64,
}

#[derive(Deserialize, Debug)]
struct StringEnumValue {
    name: Option<String>,
    description: Option<String>,
    value: Option<String>,
}

#[derive(Deserialize, Debug)]
struct ComplexType {
    #[serde(flatten)]
    object_type: ObjectType,
}

#[derive(Deserialize, Debug)]
struct Function {
    description: Option<String>,
    inputs: Option<ObjectType>,
    outputs: Option<ObjectType>,
}

#[derive(Deserialize, Debug)]
pub(crate) struct Package {
    name: String,
    #[serde(rename = "displayName")]
    display_name: Option<String>,
    #[serde(rename = "pluginDownloadURL")]
    plugin_download_url: Option<String>,
    #[serde(default)]
    resources: PulumiMap<Resource>,
    #[serde(default)]
    provider: Option<Resource>,
    version: Option<String>,
    #[serde(default)]
    types: PulumiMap<ComplexType>,
    #[serde(default)]
    functions: PulumiMap<Function>,
}

// fn complex_type_mapper(complex_type: ComplexType) -> Result<crate::model::Type> {
//     //TODO: Enums
//     object_type_mapper(complex_type.object_type)
// }
//
// fn object_type_mapper(object_type: ObjectType) -> Result<crate::model::Type> {
//
//
// }

//TODO: Fix formatting
fn new_type_mapper(type_: &Type) -> Result<crate::model::Type> {
    (match type_ {
        Type { ref_: Some(r), .. } => Ok(crate::model::Type::Ref(
            Ref::new(r).context(format!("Cannot convert ref fo type {type_:?}"))?,
        )),
        Type {
            type_: Some(TypeEnum::Boolean),
            ..
        } => Ok(crate::model::Type::Boolean),
        Type {
            type_: Some(TypeEnum::Integer),
            ..
        } => Ok(crate::model::Type::Integer),
        Type {
            type_: Some(TypeEnum::Number),
            ..
        } => Ok(crate::model::Type::Number),
        Type {
            type_: Some(TypeEnum::String),
            const_: Some(const_),
            ..
        } => Ok(crate::model::Type::ConstString(const_.clone())),
        Type {
            type_: Some(TypeEnum::String),
            ..
        } => Ok(crate::model::Type::String),
        Type {
            type_: Some(TypeEnum::Array),
            items: Some(items),
            ..
        } => Ok(crate::model::Type::Array(Box::new(new_type_mapper(items)?))),
        Type {
            type_: Some(TypeEnum::Array),
            ..
        } => Err(anyhow!("Array does not have 'items' field")),
        Type {
            type_: Some(TypeEnum::Object),
            additional_properties: Some(property),
            ..
        } => Ok(crate::model::Type::Object(Box::new(new_type_mapper(
            property,
        )?))),
        Type {
            type_: Some(TypeEnum::Object),
            ..
        } => Err(anyhow!("Object does not have 'additionalProperties' field")),
        Type {
            one_of: Some(one_of),
            ..
        } => create_discriminated_union(one_of),
        Type {
            type_: None,
            ref_: None,
            ..
        } => Err(anyhow!("'type' and 'ref' fields cannot be empty")),
        Type {
            type_: Some(TypeEnum::Other(other)),
            ref_: None,
            one_of: None,
            ..
        } => Err(anyhow!("Unknown type: [{}]", other)),
    })
    .context(format!("Cannot handle type: [{type_:?}]"))
}

fn create_discriminated_union(one_of: &[OneOfType]) -> Result<crate::model::Type> {
    Ok(crate::model::Type::DiscriminatedUnion(
        one_of
            .iter()
            .map(|r| match r {
                OneOfType::Ref(r) => crate::model::Type::Ref(
                    Ref::new(&r.ref_)
                        .context(format!("Cannot convert ref fo type {r:?}"))
                        .unwrap(),
                ),
                OneOfType::Primitive(primitive) => match primitive.type_ {
                    OneOfTypePrimitiveType::String => crate::model::Type::String,
                },
                OneOfType::Array(arr) => match arr.items.type_ {
                    OneOfTypePrimitiveType::String => {
                        crate::model::Type::Array(Box::new(crate::model::Type::String))
                    }
                },
                OneOfType::Object(obj) => match obj.additional_properties.type_ {
                    OneOfTypePrimitiveType::String => {
                        crate::model::Type::Object(Box::new(crate::model::Type::String))
                    }
                },
            })
            .collect(),
    ))
}

fn resource_to_model(
    resource_name: &str,
    resource: &Resource,
) -> Result<(ElementId, crate::model::Resource)> {
    let element_id = ElementId::new(resource_name)?;
    Ok((
        element_id.clone(),
        crate::model::Resource {
            element_id: element_id.clone(),
            // name: resource_name.clone(),
            description: fix_description(resource.object_type.description.clone(), &element_id),
            input_properties: convert_input_properties(
                &resource.input_properties,
                &resource.required_inputs,
            )?,
            output_properties: convert_output_property_object_type(
                &element_id,
                &resource.object_type,
            )?,
        },
    ))
}

fn function_to_model(
    function_name: &str,
    function: &Function,
) -> Result<(ElementId, crate::model::Function)> {
    let element_id = ElementId::new(function_name)?;
    Ok((
        element_id.clone(),
        crate::model::Function {
            element_id: element_id.clone(),
            description: fix_description(function.description.clone(), &element_id),
            input_properties: match &function.inputs {
                None => vec![],
                Some(input) => convert_input_property_object_type(input)?,
            },
            output_properties: match &function.outputs {
                None => vec![],
                Some(output) => convert_output_property_object_type(&element_id, output)?,
            },
        },
    ))
}

fn provider_to_model(provider: Option<&Resource>) -> Result<Provider> {
    let Some(provider) = provider else {
        return Ok(Provider::default());
    };

    Ok(Provider {
        description: provider.object_type.description.clone(),
        input_properties: convert_input_properties(
            &provider.input_properties,
            &provider.required_inputs,
        )?,
        output_properties: convert_output_property_object_type_without_forced(
            &provider.object_type,
        )?,
    })
}

fn convert_input_properties(
    properties: &PulumiMap<Property>,
    required: &BTreeSet<String>,
) -> Result<Vec<InputProperty>> {
    properties
        .iter()
        .map(|(input_name, input_property)| {
            let mut type_ = new_type_mapper(&input_property.r#type)
                .context(format!("Cannot handle [{input_name}] type"))?;
            if !required.contains(input_name) {
                type_ = crate::model::Type::Option(Box::new(type_));
            }
            Ok(InputProperty {
                name: input_name.clone(),
                r#type: type_,
                description: input_property.r#type.description.clone(),
            })
        })
        .collect::<Result<Vec<_>>>()
}

fn convert_input_property_object_type(object_type: &ObjectType) -> Result<Vec<InputProperty>> {
    convert_input_properties(&object_type.properties, &object_type.required)
}

fn convert_output_property_object_type(
    element_id: &ElementId,
    object_type: &ObjectType,
) -> Result<Vec<OutputProperty>> {
    let forced_options = invalid_required_complextype_required_fields();
    object_type
        .properties
        .iter()
        .map(|(output_name, output_property)| {
            let mut type_ = new_type_mapper(&output_property.r#type)
                .context(format!("Cannot handle [{output_name}] type"))?;
            if !object_type.required.contains(output_name)
                || forced_options.contains(&(element_id.clone(), output_name.clone()))
            {
                type_ = crate::model::Type::Option(Box::new(type_));
            }
            Ok(OutputProperty {
                name: output_name.clone(),
                r#type: type_,
                description: output_property.r#type.description.clone(),
            })
        })
        .collect::<Result<Vec<_>>>()
}

fn convert_output_property_object_type_without_forced(
    object_type: &ObjectType,
) -> Result<Vec<OutputProperty>> {
    object_type
        .properties
        .iter()
        .map(|(output_name, output_property)| {
            let mut type_ = new_type_mapper(&output_property.r#type)
                .context(format!("Cannot handle [{output_name}] type"))?;
            if !object_type.required.contains(output_name) {
                type_ = crate::model::Type::Option(Box::new(type_));
            }
            Ok(OutputProperty {
                name: output_name.clone(),
                r#type: type_,
                description: output_property.r#type.description.clone(),
            })
        })
        .collect::<Result<Vec<_>>>()
}

pub(crate) fn to_model(package: &Package) -> Result<crate::model::Package> {
    let resources = package
        .resources
        .iter()
        .map(|(resource_name, resource)| resource_to_model(resource_name, resource))
        .collect::<Result<BTreeMap<_, _>>>()
        .context("Cannot handle resources")?;

    let provider =
        provider_to_model(package.provider.as_ref()).context("Cannot handle provider")?;

    let functions = package
        .functions
        .iter()
        .map(|(function_name, function)| function_to_model(function_name, function))
        .collect::<Result<BTreeMap<_, _>>>()
        .context("Cannot handle functions")?;

    let types = package
        .types
        .iter()
        .map(|(type_name, type_)| {
            //TODO: Enums, support non objects
            let r = convert_to_global_type(type_name, &type_)?;
            Ok((r.element_id.clone(), r))
        })
        .collect::<Result<BTreeMap<_, _>>>()
        .context("Cannot handle types")?;
    Ok(crate::model::Package::new(
        package.name.clone(),
        package.display_name.clone(),
        package.plugin_download_url.clone(),
        package.version.clone().unwrap_or("0.0.1".to_string()),
        provider,
        resources,
        functions,
        types,
    ))
}

fn convert_to_global_type(type_name: &String, type_: &&ComplexType) -> Result<GlobalType> {
    let element_id = ElementId::new(type_name)?;
    let tpe = match &type_.object_type {
        ObjectType { r#type: None, .. } => Err(anyhow!("Unknown complex type")),
        ObjectType {
            r#type: Some(TypeEnum::Object),
            ..
        } => Ok(GlobalTypeValue::Object(
            type_.object_type.description.clone(),
            convert_output_property_object_type(&element_id, &type_.object_type)?
                .iter()
                .map(|p| GlobalTypeProperty {
                    name: p.name.clone(),
                    r#type: p.r#type.clone(),
                    description: p.description.clone(),
                })
                .collect(),
        )),
        ObjectType {
            r#type: Some(TypeEnum::Array),
            ..
        } => Err(anyhow!("Array not supported")),
        ObjectType {
            r#type: Some(TypeEnum::Boolean),
            ..
        } => Err(anyhow!("Boolean not supported")),

        ObjectType {
            r#type: Some(TypeEnum::Integer),
            enum_: Some(ObjectTypeEnum::Integer(enum_cases)),
            description,
            ..
        } => Ok(create_integer_enum(description, enum_cases)),
        ObjectType {
            r#type: Some(TypeEnum::Integer),
            enum_: Some(e),
            ..
        } => Err(anyhow!("Invalid integer enum combination {:?}", e)),
        ObjectType {
            r#type: Some(TypeEnum::Integer),
            ..
        } => Err(anyhow!("Invalid integer without enum")),

        ObjectType {
            r#type: Some(TypeEnum::Number),
            enum_: Some(ObjectTypeEnum::Number(enum_cases)),
            description,
            ..
        } => Ok(create_number_enum(description, enum_cases)),
        ObjectType {
            r#type: Some(TypeEnum::Number),
            enum_: Some(ObjectTypeEnum::Integer(enum_cases)),
            description,
            ..
        } => Ok(create_number_integer_enum(description, enum_cases)),

        ObjectType {
            r#type: Some(TypeEnum::Number),
            enum_: Some(e),
            ..
        } => Err(anyhow!("Invalid number enum combination {:?}", e)),
        ObjectType {
            r#type: Some(TypeEnum::Number),
            ..
        } => Err(anyhow!("Invalid number without enum")),

        ObjectType {
            r#type: Some(TypeEnum::String),
            enum_: Some(ObjectTypeEnum::String(enum_cases)),
            description,
            ..
        } => Ok(create_string_enum(description, enum_cases)),
        ObjectType {
            r#type: Some(TypeEnum::String),
            enum_: Some(e),
            ..
        } => Err(anyhow!("Invalid string enum combination {:?}", e)),
        ObjectType {
            r#type: Some(TypeEnum::String),
            ..
        } => Err(anyhow!("Invalid string without enum")),
        ObjectType {
            r#type: Some(TypeEnum::Other(other)),
            ..
        } => Err(anyhow!("Unknown type: [{}]", other)),
    }
    .context(format!("Cannot convert type [{type_name}]"))?;
    Ok(GlobalType {
        element_id: element_id.clone(),
        value: tpe,
    })
    // Ok((element_id, tpe))
}

fn create_number_integer_enum(
    description: &Option<String>,
    enum_values: &[IntegerEnumValue],
) -> GlobalTypeValue {
    GlobalTypeValue::NumberEnum(
        description.clone(),
        enum_values
            .iter()
            .map(|enum_value| NumberEnumElement {
                name: enum_value.name.clone(),
                value: enum_value.value as f64,
                description: enum_value.description.clone(),
            })
            .collect(),
    )
}

fn create_string_enum(
    description: &Option<String>,
    enum_values: &[StringEnumValue],
) -> GlobalTypeValue {
    GlobalTypeValue::StringEnum(
        description.clone(),
        enum_values
            .iter()
            .map(|enum_value| {
                let (name, value) = match (&enum_value.name, &enum_value.value) {
                    (Some(name), Some(value)) => (name, value),
                    (Some(name), None) => (name, name),
                    (None, Some(value)) => (value, value),
                    (None, None) => {
                        panic!("Invalid enum value: {enum_value:?}")
                    }
                };

                StringEnumElement {
                    name: name.clone(),
                    value: value.clone(),
                    description: enum_value.description.clone(),
                }
            })
            .collect(),
    )
}

fn create_integer_enum(
    description: &Option<String>,
    enum_values: &[IntegerEnumValue],
) -> GlobalTypeValue {
    GlobalTypeValue::IntegerEnum(
        description.clone(),
        enum_values
            .iter()
            .map(|enum_value| IntegerEnumElement {
                name: enum_value.name.to_case(Case::UpperCamel),
                value: enum_value.value,
                description: enum_value.description.clone(),
            })
            .collect(),
    )
}

fn create_number_enum(
    description: &Option<String>,
    enum_values: &[NumberEnumValue],
) -> GlobalTypeValue {
    GlobalTypeValue::NumberEnum(
        description.clone(),
        enum_values
            .iter()
            .map(|enum_value| NumberEnumElement {
                name: enum_value.name.to_case(Case::UpperCamel),
                value: enum_value.value,
                description: enum_value.description.clone(),
            })
            .collect(),
    )
}

fn invalid_required_complextype_required_fields() -> HashSet<(ElementId, String)> {
    HashSet::from([
        // https://github.com/pulumi/pulumi-docker/issues/1052
        (
            ElementId::new("docker:index/container:Container").unwrap(),
            "containerLogs".to_string(),
        ),
        (
            ElementId::new("docker:index/container:Container").unwrap(),
            "healthcheck".to_string(),
        ),
        //
    ])
}

#[cfg(test)]
mod test {
    use crate::filter::filter_package;
    use crate::model::{ElementId, Provider};
    use crate::schema::to_model;
    use anyhow::Result;
    use serde_json::json;

    #[test]
    fn resource_with_invalid_id_fails() -> Result<()> {
        let json = json!({
            "name": "test",
            "version": "0.0.0",
            "resources": {
                "invalid": {
                    "description": "test resource",
                }
            }
        });

        let err = to_model(&serde_json::from_value(json)?).unwrap_err();

        let chain: Vec<_> = anyhow::Chain::new(err.as_ref())
            .map(|e| e.to_string())
            .collect();

        assert_eq!(
            vec![
                "Cannot handle resources",
                "Cannot generate element id from [invalid]",
            ],
            chain
        );

        Ok(())

        // assert!(err
        //     .to_string()
        //     .contains("Cannot generate element id from [invalid]"));
        //
        // Ok(())
    }

    #[test]
    fn object_without_additionalproperties_fails() -> Result<()> {
        let json = json!({
            "name": "test",
            "version": "0.0.0-DEV",
            "resources": {
                "test:index:test_resource": {
                    "description": "test resource",
                    "inputProperties": {
                        "test_input": {
                            "type": "object"
                        }
                    },
                }
            }
        });

        let err = to_model(&serde_json::from_value(json)?).unwrap_err();

        let chain: Vec<_> = anyhow::Chain::new(err.as_ref())
            .map(|e| e.to_string())
            .collect();

        assert_eq!(
            vec![
                "Cannot handle resources",
                "Cannot handle [test_input] type",
                "Cannot handle type: [Type { type_: Some(Object), description: None, ref_: None, items: None, additional_properties: None, one_of: None, const_: None }]",
                "Object does not have 'additionalProperties' field",
            ],
            chain
        );

        Ok(())
    }

    #[test]
    fn array_without_items_fails() -> Result<()> {
        let json = json!({
            "name": "test",
            "version": "0.0.0-DEV",
            "resources": {
                "test:index:test_resource": {
                    "description": "test resource",
                    "inputProperties": {
                        "test_input": {
                            "type": "array"
                        }
                    },
                }
            }
        });

        let err = to_model(&serde_json::from_value(json)?).unwrap_err();

        let chain: Vec<_> = anyhow::Chain::new(err.as_ref())
            .map(|e| e.to_string())
            .collect();

        assert_eq!(
            vec![
                "Cannot handle resources",
                "Cannot handle [test_input] type",
                "Cannot handle type: [Type { type_: Some(Array), description: None, ref_: None, items: None, additional_properties: None, one_of: None, const_: None }]",
                "Array does not have 'items' field",
            ],
            chain
        );

        Ok(())
    }

    #[test]
    fn provider_is_mapped_to_model() -> Result<()> {
        let json = json!({
            "name": "aws",
            "version": "0.0.0-DEV",
            "provider": {
                "description": "provider",
                "inputProperties": {
                    "region": {
                        "type": "string"
                    }
                },
                "requiredInputs": ["region"],
                "properties": {
                    "region": {
                        "type": "string"
                    }
                },
                "required": ["region"]
            }
        });

        let package = to_model(&serde_json::from_value(json)?)?;
        let provider = package.provider;

        assert_eq!(provider.description, Some("provider".to_string()));
        assert_eq!(provider.input_properties.len(), 1);
        assert_eq!(provider.output_properties.len(), 1);
        assert!(!package.resource_name_map.contains_key("aws:providers:aws"));

        Ok(())
    }

    #[test]
    fn provider_is_created_even_if_not_in_schema() -> Result<()> {
        let json = json!({
            "name": "aws",
            "version": "0.0.0-DEV",
        });

        let package = to_model(&serde_json::from_value(json)?)?;

        assert_eq!(
            package.provider,
            Provider {
                description: None,
                input_properties: vec![],
                output_properties: vec![],
            }
        );

        Ok(())
    }

    #[test]
    fn provider_is_not_filtered_out() -> Result<()> {
        let json = json!({
            "name": "aws",
            "version": "0.0.0-DEV",
            "resources": {
                "aws:s3/bucket:Bucket": {
                    "properties": {},
                    "inputProperties": {}
                }
            },
            "provider": {
                "properties": {},
                "inputProperties": {}
            }
        });

        let mut package = to_model(&serde_json::from_value(json)?)?;
        filter_package(&mut package, &["ec2"]);

        assert!(
            !package
                .resources
                .contains_key(&ElementId::new("aws:s3/bucket:Bucket")?)
        );
        assert!(package.provider.input_properties.is_empty());
        assert!(package.provider.output_properties.is_empty());

        Ok(())
    }

    #[test]
    fn provider_types_are_kept() -> Result<()> {
        let json = json!({
            "name": "aws",
            "version": "0.0.0-DEV",
            "provider": {
                "inputProperties": {
                    "some_type": {
                        "$ref": "#/types/aws:index:SomeType"
                    }
                },
                "properties": {},
            },
            "types": {
                "aws:index:SomeType": {
                    "type": "object",
                    "properties": {
                        "foo": { "type": "string" }
                    }
                }
            }
        });

        let mut package = to_model(&serde_json::from_value(json)?)?;
        filter_package(&mut package, &["ec2"]);

        assert!(
            package
                .types
                .contains_key(&ElementId::new("aws:index:SomeType")?)
        );
        Ok(())
    }
}
