use std::rc::Rc;

use anyhow::{Context, Result};
use inflections::Inflect;
use itertools::Itertools;
use oas3::spec::{ObjectSchema, Schema, SchemaType, Spec};

use super::{
  ConversionOutput,
  inline_resolver::InlineTypeResolver,
  union_types::{FlattenedUnion, variants_to_cache_key},
};
use crate::{
  generator::{
    ast::{RustPrimitive, TypeRef},
    converter::ConverterContext,
    naming::{constants::VARIANT_KIND_SUFFIX, identifiers::strip_parent_prefix, inference::CommonVariantName},
  },
  utils::{
    SchemaExt, SchemaInspect, SchemaRefName, SchemaResolveExt, extract_union_fingerprint, parse_schema_ref_path,
    variant_is_nullable,
  },
};

#[derive(Clone, Debug)]
pub(crate) struct TypeResolver {
  context: Rc<ConverterContext>,
  inline_resolver: InlineTypeResolver,
}

impl TypeResolver {
  /// Creates a new type resolver with an inline resolver for anonymous schemas.
  pub(crate) fn new(context: Rc<ConverterContext>) -> Self {
    let inline_resolver = InlineTypeResolver::new(context.clone());
    Self {
      context,
      inline_resolver,
    }
  }

  /// Returns the OpenAPI specification from the schema registry.
  fn spec(&self) -> &Spec {
    self.context.graph().spec()
  }

  /// Resolves a schema reference to its underlying schema.
  pub(crate) fn resolve(&self, schema_ref: &Schema) -> Result<ObjectSchema> {
    schema_ref
      .resolve_object(self.spec())
      .context("Schema resolution failed")
  }

  /// Resolves a schema to its Rust type reference.
  pub(crate) fn resolve_type(&self, schema: &ObjectSchema) -> Result<TypeRef> {
    if let Some(type_ref) = self.context.cache().get_type_ref(schema)? {
      return Ok(type_ref);
    }

    let type_ref = self.resolve_type_uncached(schema)?;

    if schema.is_primitive() {
      let _ = self
        .context
        .cache
        .borrow_mut()
        .register_type_ref(schema, type_ref.clone());
    }

    Ok(type_ref)
  }

  /// Resolves a schema to a type reference without cache lookup.
  ///
  /// Checks for title-based type references, union types, primitive types,
  /// nullable wrappers, and const values in that order.
  fn resolve_type_uncached(&self, schema: &ObjectSchema) -> Result<TypeRef> {
    if let Some(type_ref) = self.try_type_ref_by_title(schema) {
      return Ok(type_ref);
    }

    let union_variants = if !schema.one_of.is_empty() {
      Some(&schema.one_of)
    } else if !schema.any_of.is_empty() {
      Some(&schema.any_of)
    } else {
      None
    };

    if let Some(variants) = union_variants
      && let Some(type_ref) = self.try_union(variants)?
    {
      return Ok(type_ref);
    }

    if let Some(typ) = schema.single_type() {
      return self.primitive(typ, schema);
    }

    if let Some(non_null) = schema.non_null_type() {
      return Ok(self.primitive(non_null, schema)?.with_option());
    }

    if let Some(ref const_value) = schema.const_value {
      return Ok(const_value.into());
    }

    Ok(TypeRef::new(RustPrimitive::Value))
  }

  /// Resolves a property schema to its Rust type reference with inline type tracking.
  pub(crate) fn resolve_property(
    &self,
    parent_name: &str,
    property_name: &str,
    schema: &ObjectSchema,
    schema_ref: &Schema,
  ) -> Result<ConversionOutput<TypeRef>> {
    if let Some(ref_path) = schema_ref.ref_path() {
      return self.resolve_ref(ref_path, schema);
    }

    if schema.all_of.len() == 1
      && let Some(type_ref) = self.try_union(&schema.all_of)?
    {
      return Ok(ConversionOutput::new(type_ref));
    }

    if schema.is_inline_object() {
      return self
        .inline_resolver
        .resolve_inline_struct(parent_name, property_name, schema);
    }

    if schema.has_enum_values() {
      return self.inline_enum(parent_name, property_name, schema);
    }

    if schema.has_union() {
      return self.inline_union(parent_name, property_name, schema);
    }

    if schema.is_array()
      && let Some(result) = self.try_inline_array(parent_name, property_name, schema)?
    {
      return Ok(result);
    }

    Ok(ConversionOutput::new(self.resolve_type(schema)?))
  }

  /// Resolves a `$ref` path to a type reference.
  ///
  /// For primitive types at ref targets, returns the primitive directly.
  /// For wrapper unions (nullable ref), unwraps to the inner type.
  /// Otherwise, returns a reference to the named schema type.
  fn resolve_ref(&self, ref_path: &str, schema: &ObjectSchema) -> Result<ConversionOutput<TypeRef>> {
    let ref_name =
      parse_schema_ref_path(ref_path).ok_or_else(|| anyhow::anyhow!("Invalid reference path: {ref_path}"))?;

    if schema.is_primitive() && !schema.has_inline_union_array_items(self.spec()) {
      return Ok(ConversionOutput::new(self.resolve_type(schema)?));
    }

    let Some(variants) = schema.union_variants_with_kind() else {
      return Ok(ConversionOutput::new(self.context.graph().type_ref(&ref_name)));
    };

    if schema.has_null_variant(self.spec())
      && let Some(non_null) = schema.find_non_null_variant(self.spec())
    {
      let resolved = self.resolve(non_null)?;
      if resolved.has_inline_union_array_items(self.spec()) {
        return Ok(ConversionOutput::new(
          self.context.graph().type_ref(&ref_name).with_option(),
        ));
      }
    }

    if self.is_wrapper_union(schema)?
      && let Some(type_ref) = self.try_union(variants)?
    {
      return Ok(ConversionOutput::new(type_ref));
    }

    Ok(ConversionOutput::new(self.context.graph().type_ref(&ref_name)))
  }

  /// Creates an inline enum type from a schema with enum values.
  ///
  /// Single-value enums are treated as constants rather than generating
  /// a separate enum type.
  pub(crate) fn inline_enum(
    &self,
    parent_name: &str,
    property_name: &str,
    schema: &ObjectSchema,
  ) -> Result<ConversionOutput<TypeRef>> {
    if schema.enum_values.len() == 1 {
      return Ok(ConversionOutput::new(self.resolve_type(schema)?));
    }

    let enum_values = variants_to_cache_key(&schema.extract_enum_entries(self.spec()));
    self
      .inline_resolver
      .resolve_inline_enum(parent_name, property_name, schema, &enum_values)
  }

  /// Creates a named struct type from an inline object schema.
  ///
  /// Delegates to the inline resolver for cache-aware type generation.
  pub(crate) fn inline_struct_from_schema(
    &self,
    schema: &ObjectSchema,
    base_name: &str,
  ) -> Result<ConversionOutput<TypeRef>> {
    self.inline_resolver.resolve_inline_struct_with_name(schema, base_name)
  }

  /// Creates a named union enum from an inline `oneOf`/`anyOf` schema.
  ///
  /// First attempts to recognize nullable unions (single non-null variant)
  /// and inline arrays. Otherwise generates a new enum type with a name
  /// derived from parent and property names.
  pub(crate) fn inline_union(
    &self,
    parent_name: &str,
    property_name: &str,
    schema: &ObjectSchema,
  ) -> Result<ConversionOutput<TypeRef>> {
    let Some(variants) = schema.union_variants_with_kind() else {
      anyhow::bail!("Expected oneOf or anyOf variants for inline union");
    };

    if let Some(type_ref) = self.try_nullable_union(schema)? {
      return Ok(ConversionOutput::new(type_ref));
    }

    if let Some(result) = self.try_inline_array(parent_name, property_name, schema)? {
      return Ok(result);
    }

    let property_pascal = property_name.to_pascal_case();
    let suffix = format!("{property_pascal}{VARIANT_KIND_SUFFIX}");
    let base_name = CommonVariantName::union_name_or(variants, &suffix, || format!("{parent_name}{property_pascal}"));

    self.union_type(schema, variants, &base_name)
  }

  /// Creates a union type reference via the inline resolver.
  fn union_type(
    &self,
    schema: &ObjectSchema,
    variants: &[Schema],
    base_name: &str,
  ) -> Result<ConversionOutput<TypeRef>> {
    let refs = extract_union_fingerprint(variants);
    self.inline_resolver.resolve_inline_union(schema, &refs, base_name)
  }

  /// Attempts to resolve an array schema with inline item definitions.
  ///
  /// Returns `None` if the schema is not an array or has no inline items.
  /// For arrays with inline object, union, or enum items, generates the
  /// item type and wraps it in `Vec<T>` or `IndexSet<T>` (for unique items).
  pub(crate) fn try_inline_array(
    &self,
    parent_name: &str,
    property_name: &str,
    schema: &ObjectSchema,
  ) -> Result<Option<ConversionOutput<TypeRef>>> {
    let Some(items) = schema.inline_array_items(self.spec()) else {
      return Ok(None);
    };

    let unique = self.preserve_unique_items(schema);
    let singular = cruet::to_singular(property_name).to_pascal_case();

    let result = if items.is_inline_object() {
      let base = format!("{parent_name}{}", strip_parent_prefix(parent_name, &singular));
      self.inline_struct_from_schema(&items, &base)?
    } else if items.has_union() {
      self.inline_union_array_item(&items, &singular)?
    } else if items.has_enum_values() {
      self.inline_enum(parent_name, &singular, &items)?
    } else {
      return Ok(None);
    };

    let vec_type = TypeRef {
      boxed: false,
      ..result.result
    }
    .with_vec()
    .with_unique_items(unique);

    Ok(Some(ConversionOutput::with_inline_types(vec_type, result.inline_types)))
  }

  /// Creates a union type for array items containing `oneOf`/`anyOf`.
  fn inline_union_array_item(&self, items: &ObjectSchema, singular: &str) -> Result<ConversionOutput<TypeRef>> {
    let Some(variants) = items.union_variants_with_kind() else {
      anyhow::bail!("expected oneOf or anyOf variants for inline union array item");
    };
    let kind_name = format!("{singular}{VARIANT_KIND_SUFFIX}");
    let name = CommonVariantName::union_name_or(variants, &kind_name, || kind_name.clone());

    let final_name = {
      let cache = self.context.cache();
      if cache.name_conflicts_with_different_schema(&name, items)? {
        cache.make_unique_name(&name)
      } else {
        name
      }
    };

    self.union_type(items, variants, &final_name)
  }

  /// Attempts to resolve a schema by its `title` to a named schema reference.
  ///
  /// Returns `None` if the schema has no title, has an explicit type, or
  /// the title doesn't match a known schema name.
  fn try_type_ref_by_title(&self, schema: &ObjectSchema) -> Option<TypeRef> {
    if schema.schema_type.is_some() {
      return None;
    }
    let title = schema.title.as_ref()?;
    self
      .context
      .graph()
      .contains(title)
      .then(|| self.context.graph().type_ref(title))
  }

  /// Attempts to recognize a nullable union (exactly one non-null variant).
  ///
  /// For unions like `oneOf: [{ $ref: "#/.../Foo" }, { type: null }]`,
  /// returns `Option<Foo>` instead of generating a separate enum type.
  pub(crate) fn try_nullable_union(&self, schema: &ObjectSchema) -> Result<Option<TypeRef>> {
    let Some(variants) = schema.union_variants_with_kind() else {
      return Ok(None);
    };
    self.try_nullable_variants(variants)
  }

  /// Attempts to recognize a nullable pattern from a variants slice.
  ///
  /// Returns `Some(Option<T>)` when the variants contain exactly two
  /// entries: one null and one non-null. The non-null variant is resolved
  /// to its type and wrapped in `Option`. Returns `None` if the pattern
  /// doesn't match or a new enum type must be generated.
  fn try_nullable_variants(&self, variants: &[Schema]) -> Result<Option<TypeRef>> {
    if variants.len() != 2 {
      return Ok(None);
    }

    let spec = self.spec();

    let Ok(variant) = variants.iter().filter(|v| !variant_is_nullable(v, spec)).exactly_one() else {
      return Ok(None);
    };

    if let Some(ref_name) = variant.schema_ref_name() {
      return Ok(Some(self.context.graph().type_ref(&ref_name).with_option()));
    }

    let resolved = self.resolve(variant)?;

    if resolved.is_array() && self.has_union_items(&resolved) {
      return Ok(None);
    }

    Ok(Some(self.resolve_type(&resolved)?.with_option()))
  }

  /// Returns `true` if the array schema has items containing a union.
  fn has_union_items(&self, schema: &ObjectSchema) -> bool {
    let Some(items) = schema.items.as_deref() else {
      return false;
    };
    self.resolve(items).is_ok_and(|items| items.has_union())
  }

  /// Attempts to resolve a union to an existing type or simple form.
  ///
  /// Returns `Some` if the union matches a known type by refs, is a
  /// nullable wrapper, or can be simplified to a single ref or primitive.
  /// Returns `None` if a new enum type must be generated.
  pub(crate) fn try_union(&self, variants: &[Schema]) -> Result<Option<TypeRef>> {
    let refs = extract_union_fingerprint(variants);

    if refs.len() > 1
      && let Some(name) = self.context.cache().find_union(&refs).map(String::from)
    {
      return Ok(Some(self.context.graph().type_ref(&name)));
    }

    if let Some(nullable) = self.try_nullable_variants(variants)? {
      return Ok(Some(nullable));
    }

    self.union_fallback(variants)
  }

  /// Attempts fallback union simplification when direct lookup fails.
  ///
  /// Handles cases like single-ref unions with null variants, unions
  /// containing only primitives, or single-variant unions.
  fn union_fallback(&self, variants: &[Schema]) -> Result<Option<TypeRef>> {
    let mut first_ref: Option<String> = None;

    for variant in variants {
      if let Some(name) = variant.schema_ref_name() {
        if first_ref.is_some() {
          return Ok(None);
        }
        first_ref = Some(name);
        continue;
      }

      let Ok(resolved) = self.resolve(variant) else {
        continue;
      };

      if resolved.is_null() {
        continue;
      }

      if let Some(type_ref) = self.try_simple_fallback_type(&resolved)? {
        return Ok(Some(type_ref));
      }
    }

    Ok(first_ref.map(|name| self.context.graph().type_ref(&name)))
  }

  /// Attempts to extract a simple type from a resolved schema.
  ///
  /// Recognizes arrays, strings, single-variant unions, and title references.
  fn try_simple_fallback_type(&self, schema: &ObjectSchema) -> Result<Option<TypeRef>> {
    if schema.is_array() {
      let item = self.array_item_type(schema)?;
      let unique = self.preserve_unique_items(schema);
      return Ok(Some(
        TypeRef::new(item.to_rust_type()).with_vec().with_unique_items(unique),
      ));
    }

    if schema.is_string() {
      return Ok(Some(TypeRef::new(RustPrimitive::String)));
    }

    if let [single_variant] = schema.one_of.as_slice()
      && let Some(name) = single_variant.schema_ref_name()
    {
      return Ok(Some(self.context.graph().type_ref(&name)));
    }

    if let Some(ref title) = schema.title
      && self.context.graph().get(title).is_some()
    {
      return Ok(Some(self.context.graph().type_ref(title)));
    }

    Ok(None)
  }

  /// Converts an OpenAPI schema type to a Rust primitive type reference.
  ///
  /// Applies format-specific mappings (e.g., `format: date-time` → `DateTime`).
  /// Object types without properties become `serde_json::Value`.
  fn primitive(&self, typ: SchemaType, schema: &ObjectSchema) -> Result<TypeRef> {
    match typ {
      SchemaType::String | SchemaType::Number | SchemaType::Integer => {
        Ok(TypeRef::new(Self::format_or_default(typ, schema)))
      }
      SchemaType::Boolean => Ok(TypeRef::new(RustPrimitive::Bool)),
      SchemaType::Object => {
        if let Some(map) = self.try_map_type(schema)? {
          return Ok(map);
        }
        Ok(TypeRef::new(RustPrimitive::Value))
      }
      SchemaType::Null => Ok(TypeRef::new(RustPrimitive::Unit).with_option()),
      SchemaType::Array => {
        let item = self.array_item_type(schema)?;
        let unique = self.preserve_unique_items(schema);
        Ok(TypeRef::new(item.to_rust_type()).with_vec().with_unique_items(unique))
      }
    }
  }

  /// Returns the Rust primitive for a schema type, applying format overrides.
  fn format_or_default(typ: SchemaType, schema: &ObjectSchema) -> RustPrimitive {
    let default = match typ {
      SchemaType::String => RustPrimitive::String,
      SchemaType::Number => RustPrimitive::F64,
      SchemaType::Integer => RustPrimitive::I64,
      _ => return RustPrimitive::Value,
    };
    schema
      .format
      .as_ref()
      .and_then(|f| RustPrimitive::from_format(f))
      .unwrap_or(default)
  }

  /// Attempts to recognize an object schema as a map type.
  ///
  /// Returns `Some` only if `additionalProperties` is set and `properties`
  /// is empty (pure map type rather than a struct with extra fields). The
  /// concrete map type follows
  /// [`CollectionTypePolicy`](crate::generator::CollectionTypePolicy):
  /// `indexmap::IndexMap` when ordered, `std::collections::HashMap` when hashed.
  fn try_map_type(&self, schema: &ObjectSchema) -> Result<Option<TypeRef>> {
    let Some(ref additional) = schema.additional_properties else {
      return Ok(None);
    };

    if matches!(additional, Schema::Boolean(b) if !b.0) {
      return Ok(None);
    }

    if !schema.properties.is_empty() {
      return Ok(None);
    }

    let value = self.additional_properties_type(additional)?;
    Ok(Some(TypeRef::new(format!(
      "{}<String, {}>",
      self.context.config().map_type_path(),
      value.to_rust_type()
    ))))
  }

  /// Resolves whether an array schema's `uniqueItems` flag should propagate to
  /// the generated type. With the ordered policy this returns the schema flag
  /// directly; with the hashed policy it returns `false` so the array stays a
  /// `Vec<T>` regardless of the spec.
  fn preserve_unique_items(&self, schema: &ObjectSchema) -> bool {
    self.context.config().ordered_collections() && schema.unique_items.unwrap_or(false)
  }

  /// Resolves the value type for `additionalProperties`.
  ///
  /// Boolean `true` maps to `serde_json::Value`; schema references
  /// and inline schemas are resolved to their respective types.
  pub(crate) fn additional_properties_type(&self, additional: &Schema) -> Result<TypeRef> {
    if matches!(additional, Schema::Boolean(_)) {
      return Ok(TypeRef::new(RustPrimitive::Value));
    }

    if let Some(ref_path) = additional.ref_path() {
      let name = parse_schema_ref_path(ref_path).ok_or_else(|| anyhow::anyhow!("Invalid reference: {ref_path}"))?;
      return Ok(self.context.graph().type_ref(&name));
    }

    let resolved = self.resolve(additional)?;
    if resolved.is_empty_object() {
      return Ok(TypeRef::new(RustPrimitive::Value));
    }
    self.resolve_type(&resolved)
  }

  /// Resolves the item type for an array schema.
  ///
  /// Handles both ref and inline item definitions. Unboxes the result
  /// since array items don't need boxing for cycle breaking.
  fn array_item_type(&self, schema: &ObjectSchema) -> Result<TypeRef> {
    let Some(items_ref) = schema.items.as_deref() else {
      return Ok(TypeRef::new(RustPrimitive::Value));
    };

    let items = self.resolve(items_ref)?;

    let type_ref = if let Some(ref_path) = items_ref.ref_path() {
      self.resolve_ref(ref_path, &items)?.result
    } else {
      self.resolve_type(&items)?
    };

    Ok(TypeRef {
      boxed: false,
      ..type_ref
    })
  }

  /// Returns `true` if the union is a simple nullable wrapper.
  ///
  /// A wrapper union has exactly one non-null variant that is either a
  /// `$ref` or a primitive type. Such unions are unwrapped to `Option<T>`
  /// rather than generating a separate enum.
  pub(crate) fn is_wrapper_union(&self, schema: &ObjectSchema) -> Result<bool> {
    let Some(variant) = schema.single_non_null_variant(self.spec()) else {
      return Ok(false);
    };

    if variant.schema_ref_name().is_some() {
      return Ok(true);
    }

    let resolved = self.resolve(variant)?;

    Ok(
      !resolved.has_union()
        && resolved.additional_properties.is_none()
        && !(resolved.is_array() && self.has_union_items(&resolved))
        && (resolved.is_primitive() || resolved.enum_values.len() > 1),
    )
  }

  /// Attempts to flatten a nested union structure.
  ///
  /// For patterns like `oneOf: [{ oneOf: [...] }, null]`, promotes the inner union's variants to the outer level.
  /// Returns `None` if the schema is not a nested union pattern.
  pub(crate) fn try_flatten_nested_union(&self, outer: &ObjectSchema) -> Result<Option<FlattenedUnion>> {
    let Some(variant) = outer.single_non_null_variant(self.spec()) else {
      return Ok(None);
    };

    if variant.schema_ref_name().is_some() {
      return Ok(None);
    }

    let inner = self.resolve(variant)?;

    if inner.has_union() {
      let Some(variants) = inner.union_variants_with_kind() else {
        return Ok(None);
      };

      return Ok(Some(
        FlattenedUnion::builder()
          .variants(variants.to_owned())
          .maybe_description(outer.description.clone().or_else(|| inner.description.clone()))
          .maybe_discriminator(inner.discriminator.clone().or_else(|| outer.discriminator.clone()))
          .is_one_of(!inner.one_of.is_empty())
          .build(),
      ));
    }

    if let Some(items) = inner.inline_array_items(self.spec())
      && items.has_union()
      && let Some(variants) = items.union_variants_with_kind()
    {
      Ok(Some(
        FlattenedUnion::builder()
          .variants(variants.to_owned())
          .maybe_description(outer.description.clone().or_else(|| items.description.clone()))
          .maybe_discriminator(items.discriminator.clone())
          .is_one_of(!items.one_of.is_empty())
          .build(),
      ))
    } else {
      Ok(None)
    }
  }
}
