pub(crate) mod cache;
pub(crate) mod common;
pub(crate) mod discriminator;
pub(crate) mod fields;
pub(crate) mod hashing;
pub(crate) mod inline_resolver;
pub(crate) mod methods;
pub(crate) mod operations;
pub(crate) mod parameters;
pub(crate) mod relaxed_enum;
pub(crate) mod requests;
pub(crate) mod responses;
pub(crate) mod structs;
pub(crate) mod type_resolver;
pub(crate) mod type_usage_recorder;
pub(crate) mod union_types;
pub(crate) mod unions;
pub(crate) mod value_enums;
pub(crate) mod variants;

use std::{
  cell::{Ref, RefCell, RefMut},
  collections::HashMap,
  rc::Rc,
  sync::Arc,
};

use anyhow::Result;
pub(crate) use common::ConversionOutput;
use oas3::spec::ObjectSchema;
pub(crate) use operations::{OperationsProcessor, build_server_trait};
pub(crate) use type_resolver::TypeResolver;
pub(crate) use type_usage_recorder::SerdeUsageRecorder;

use crate::{
  generator::{
    ast::{Documentation, EnumToken, RustType, TypeAliasDef, TypeAliasToken, TypeRef},
    converter::{
      cache::SharedSchemaCache,
      discriminator::DiscriminatorConverter,
      structs::StructConverter,
      unions::{EnumConverter, UnionConverter},
    },
    metrics::{GenerationStats, GenerationWarning},
    naming::{constants::DISCRIMINATED_BASE_SUFFIX, identifiers::to_rust_type_name},
    schema_registry::SchemaRegistry,
  },
  utils::{SchemaExt, SchemaSet},
};

/// Policy for handling enum variant name collisions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumCasePolicy {
  /// Append index suffix to colliding variants (e.g., `Value`, `Value1`).
  Preserve,
  /// Merge colliding variants and add serde aliases.
  #[default]
  Deduplicate,
}

/// Policy for generating enum helper constructors.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumHelperPolicy {
  /// Generate helper methods for creating enum variants.
  #[default]
  Generate,
  /// Disable helper method generation.
  Disable,
}

/// Policy for enum deserialization case sensitivity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumDeserializePolicy {
  /// Use standard case-sensitive deserialization.
  #[default]
  CaseSensitive,
  /// Generate custom case-insensitive deserializer.
  CaseInsensitive,
}

/// Policy for OData-specific schema support.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ODataPolicy {
  /// Disable OData-specific handling.
  #[default]
  Disabled,
  /// Enable OData support (makes `@odata.*` fields optional).
  Enabled,
}

/// Target for code generation (client vs server).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GenerationTarget {
  /// Generate HTTP client code.
  #[default]
  Client,
  /// Generate HTTP server code (axum).
  Server,
}

/// Policy for schema inclusion scope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SchemaScope {
  /// Generate only schemas referenced by operations.
  #[default]
  ReferencedOnly,
  /// Generate all schemas defined in the spec.
  All,
}

/// Policy for header constant inclusion scope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HeaderScope {
  /// Emit header constants only for headers used in operations.
  #[default]
  ReferencedOnly,
  /// Emit header constants for all header parameters in components.
  All,
}

/// Policy for the layout (ordering) of variants in generated enums.
///
/// Controls whether enum variants appear in OpenAPI spec declaration order
/// or are sorted alphabetically by Rust variant name. Sorted output stabilizes
/// generated code against spec re-orderings so that `[A, B]` and `[B, A]`
/// produce identical Rust source.
///
/// Applies to value enums (string `enum`), `oneOf`/`anyOf` union variants,
/// and discriminated enum variants. Response (HTTP status) enums are not
/// affected and continue to use status-code order.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumLayoutPolicy {
  /// Preserve enum variant order from the OpenAPI spec.
  #[default]
  Spec,
  /// Sort enum variants alphabetically by Rust variant name.
  Sorted,
}

/// Policy for the runtime collection types emitted in generated code.
///
/// Map types (`additionalProperties` and standalone object maps) and arrays
/// marked `uniqueItems: true` can either preserve JSON key/element order at
/// runtime via `indexmap`, or use the standard library equivalents.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CollectionTypePolicy {
  /// Emit `indexmap::IndexMap<String, T>` for map types and
  /// `indexmap::IndexSet<T>` for `uniqueItems` arrays, so the order of keys
  /// or elements received over the wire is retained when the generated type
  /// is re-serialized.
  #[default]
  Ordered,
  /// Emit `std::collections::HashMap<String, T>` for map types and
  /// `Vec<T>` for arrays (ignoring `uniqueItems` at the type level), matching
  /// the pre-0.26 generator output.
  Hashed,
}

/// Configuration for code generation.
///
/// Uses typed enums instead of booleans to make intent explicit at call sites
/// and prevent invalid combinations.
#[derive(Debug, Clone, Default, bon::Builder)]
pub struct CodegenConfig {
  #[builder(default)]
  pub enum_case: EnumCasePolicy,
  #[builder(default)]
  pub enum_helpers: EnumHelperPolicy,
  #[builder(default)]
  pub enum_deserialize: EnumDeserializePolicy,
  #[builder(default)]
  pub odata: ODataPolicy,
  #[builder(default)]
  pub target: GenerationTarget,
  #[builder(default)]
  pub schema_scope: SchemaScope,
  #[builder(default)]
  pub header_scope: HeaderScope,
  #[builder(default)]
  pub collection_types: CollectionTypePolicy,
  #[builder(default)]
  pub enum_layout: EnumLayoutPolicy,
  #[builder(default)]
  pub enable_builders: bool,
  #[builder(default)]
  pub customizations: HashMap<String, String>,
}

impl CodegenConfig {
  /// Returns `true` if enum variant name collisions should be resolved by appending
  /// numeric suffixes (e.g., `Value`, `Value1`) rather than merging with serde aliases.
  #[must_use]
  pub fn preserve_case_variants(&self) -> bool {
    self.enum_case == EnumCasePolicy::Preserve
  }

  /// Returns `true` if enums should generate custom deserializers that accept
  /// any casing of variant names (e.g., "FOO", "foo", "Foo" all deserialize to `Foo`).
  #[must_use]
  pub fn case_insensitive_enums(&self) -> bool {
    self.enum_deserialize == EnumDeserializePolicy::CaseInsensitive
  }

  /// Returns `true` if enum helper constructor methods should be omitted from generated code.
  #[must_use]
  pub fn no_helpers(&self) -> bool {
    self.enum_helpers == EnumHelperPolicy::Disable
  }

  /// Returns `true` if OData-specific handling is enabled, which makes `@odata.*` fields
  /// optional even when the schema marks them as required.
  #[must_use]
  pub fn odata_support(&self) -> bool {
    self.odata == ODataPolicy::Enabled
  }

  /// Returns `true` if all schemas should be generated, not just those referenced by operations.
  #[must_use]
  pub fn include_all_schemas(&self) -> bool {
    self.schema_scope == SchemaScope::All
  }

  /// Returns `true` if header constants should be emitted for all component-level headers,
  /// not just those used in operations.
  #[must_use]
  pub fn include_all_headers(&self) -> bool {
    self.header_scope == HeaderScope::All
  }

  /// Returns `true` if bon builder derives should be generated for schema structs
  /// and builder methods for request structs.
  #[must_use]
  pub fn enable_builders(&self) -> bool {
    self.enable_builders
  }

  /// Returns `true` when generated map and unique-array fields should emit
  /// `indexmap` types that preserve JSON key/element order at runtime.
  #[must_use]
  pub fn ordered_collections(&self) -> bool {
    self.collection_types == CollectionTypePolicy::Ordered
  }

  /// Returns `true` when enum variants should be sorted alphabetically by
  /// Rust variant name in generated code.
  #[must_use]
  pub fn sort_enum_variants(&self) -> bool {
    self.enum_layout == EnumLayoutPolicy::Sorted
  }

  /// Returns the fully qualified Rust path used for map-like fields
  /// (`additionalProperties` and standalone object maps).
  #[must_use]
  pub fn map_type_path(&self) -> &'static str {
    match self.collection_types {
      CollectionTypePolicy::Ordered => "indexmap::IndexMap",
      CollectionTypePolicy::Hashed => "std::collections::HashMap",
    }
  }
}

#[derive(Debug, Clone)]
pub(crate) struct ConverterContext {
  pub(crate) graph: Arc<SchemaRegistry>,
  pub(crate) config: CodegenConfig,
  pub(crate) cache: RefCell<SharedSchemaCache>,
  pub(crate) type_usage: RefCell<SerdeUsageRecorder>,
  pub(crate) reachable_schemas: Option<Arc<SchemaSet>>,
}

impl ConverterContext {
  /// Creates a new converter context with the given schema registry, configuration,
  /// type cache, and optional schema filter.
  ///
  /// The `reachable_schemas` parameter, when `Some`, restricts conversion to only
  /// schemas reachable from the filtered operations, enabling dead-code elimination.
  pub(crate) fn new(
    graph: Arc<SchemaRegistry>,
    config: CodegenConfig,
    cache: SharedSchemaCache,
    reachable_schemas: Option<Arc<SchemaSet>>,
  ) -> Self {
    Self {
      graph,
      config,
      cache: RefCell::new(cache),
      type_usage: RefCell::new(SerdeUsageRecorder::new()),
      reachable_schemas,
    }
  }

  /// Returns a reference to the schema registry containing all resolved schemas
  /// and their dependency relationships.
  pub(crate) fn graph(&self) -> &SchemaRegistry {
    &self.graph
  }

  /// Returns a reference to the code generation configuration.
  pub(crate) fn config(&self) -> &CodegenConfig {
    &self.config
  }

  /// Extracts the accumulated type usage data, replacing it with an empty recorder.
  ///
  /// Call this after conversion completes to retrieve which types are used in
  /// requests vs responses for serde mode optimization.
  pub(crate) fn take_type_usage(&self) -> SerdeUsageRecorder {
    self.type_usage.take()
  }

  /// Records that the named type appears in an HTTP request context (request body or parameter).
  ///
  /// Types used only in requests may derive `Serialize` without `Deserialize`.
  pub(crate) fn mark_request(&self, type_name: impl Into<EnumToken>) {
    self.type_usage.borrow_mut().mark_request(type_name);
  }

  /// Records that the named type appears in an HTTP response context.
  ///
  /// Types used only in responses may derive `Deserialize` without `Serialize`.
  pub(crate) fn mark_response(&self, type_name: impl Into<EnumToken>) {
    self.type_usage.borrow_mut().mark_response(type_name);
  }

  /// Records that multiple types appear in HTTP request contexts.
  ///
  /// More efficient than calling [`mark_request`](Self::mark_request) in a loop.
  pub(crate) fn mark_request_iter<I, T>(&self, types: I)
  where
    I: IntoIterator<Item = T>,
    T: Into<super::ast::EnumToken>,
  {
    self.type_usage.borrow_mut().mark_request_iter(types);
  }

  /// Extracts custom type names from a [`TypeRef`] and records them as response types.
  ///
  /// Handles nested types like `Box<CustomType>` by extracting the inner type name.
  pub(crate) fn mark_response_type_ref(&self, type_ref: &TypeRef) {
    self.type_usage.borrow_mut().mark_response_type_ref(type_ref);
  }

  /// Combines type usage data from another recorder into this context's recorder.
  ///
  /// Used when sub-converters track usage independently and results must be aggregated.
  pub(crate) fn merge_usage(&self, other: SerdeUsageRecorder) {
    self.type_usage.borrow_mut().merge(other);
  }

  pub(crate) fn cache(&self) -> Ref<'_, SharedSchemaCache> {
    self.cache.borrow()
  }

  pub(crate) fn cache_mut(&self) -> RefMut<'_, SharedSchemaCache> {
    self.cache.borrow_mut()
  }
}

/// Main entry point for converting OpenAPI schemas into Rust AST.
///
/// Orchestrates the one-way conversion pipeline from OpenAPI schemas to Rust types.
/// Uses `TypeResolver` for read-only type mapping and navigation, while managing
/// the conversion flow through specialized converters (`StructConverter`, `EnumConverter`, etc.).
#[derive(Debug, Clone)]
pub(crate) struct SchemaConverter {
  context: Rc<ConverterContext>,
  type_resolver: TypeResolver,
  struct_converter: StructConverter,
  enum_converter: EnumConverter,
  union_converter: UnionConverter,
  discriminator_converter: DiscriminatorConverter,
}

impl SchemaConverter {
  /// Creates a new schema converter with all specialized sub-converters.
  ///
  /// Each sub-converter receives a clone of the shared context `Rc`, enabling
  /// coordinated type caching and usage tracking across conversion operations.
  pub(crate) fn new(context: &Rc<ConverterContext>) -> Self {
    Self {
      type_resolver: TypeResolver::new(context.clone()),
      struct_converter: StructConverter::new(context.clone()),
      enum_converter: EnumConverter::new(context.clone()),
      union_converter: UnionConverter::new(context.clone()),
      discriminator_converter: DiscriminatorConverter::new(context.clone()),
      context: context.clone(),
    }
  }

  /// Returns a reference to the shared converter context.
  pub(crate) fn context(&self) -> &Rc<ConverterContext> {
    &self.context
  }

  /// Returns `true` if the schema registry contains a schema with the given name.
  pub(crate) fn contains(&self, name: &str) -> bool {
    self.context.cache().contains_schema_name(name)
  }

  /// Converts a named OpenAPI schema into one or more Rust type definitions.
  ///
  /// Routes the schema to the appropriate specialized converter based on its structure:
  /// - `allOf` schemas become merged structs
  /// - `oneOf`/`anyOf` schemas become tagged or untagged enums
  /// - Schemas with `enum` values become string enums
  /// - Schemas with `properties` become structs
  /// - Array schemas become `Vec<T>` type aliases
  /// - Primitive schemas become type aliases
  ///
  /// Returns multiple types when inline definitions are extracted (e.g., nested
  /// anonymous objects become separate struct definitions).
  pub(crate) fn convert_schema(&self, name: &str, schema: &ObjectSchema) -> Result<Vec<RustType>> {
    if schema.has_intersection() {
      return self.struct_converter.convert_all_of_schema(name);
    }

    if schema.union_variants_with_kind().is_some() {
      if schema.discriminator.is_none() && self.type_resolver.is_wrapper_union(schema)? {
        return self.convert_nullable_enum(name, schema);
      }

      if let Some(flattened) = self.type_resolver.try_flatten_nested_union(schema)? {
        return self
          .union_converter
          .convert_union(name, &flattened.into())
          .map(ConversionOutput::into_vec);
      }

      return self
        .union_converter
        .convert_union(name, schema)
        .map(ConversionOutput::into_vec);
    }

    if !schema.enum_values.is_empty() {
      return Ok(vec![self.enum_converter.convert_value_enum(name, schema)]);
    }

    if !schema.properties.is_empty() || schema.additional_properties.is_some() {
      let result = self.struct_converter.convert_struct(name, schema, None)?;
      return self.finalize_struct_types(name, schema, result.result, result.inline_types);
    }

    if let Some(output) = self.try_array_alias(name, schema)? {
      let alias = RustType::TypeAlias(TypeAliasDef {
        name: TypeAliasToken::from_raw(name),
        docs: Documentation::from_optional(schema.description.as_ref()),
        target: output.result,
      });
      let mut result = vec![alias];
      result.extend(output.inline_types);
      return Ok(result);
    }

    let type_ref = self.type_resolver.resolve_type(schema)?;
    Ok(vec![RustType::TypeAlias(TypeAliasDef {
      name: TypeAliasToken::from_raw(name),
      docs: Documentation::from_optional(schema.description.as_ref()),
      target: type_ref,
    })])
  }

  /// Builds a discriminated union enum for a base schema that defines discriminator mappings.
  ///
  /// The `fallback_type` specifies the struct type to use for the catch-all variant
  /// when the discriminator value doesn't match any known mapping.
  fn discriminated_enum(&self, name: &str, schema: &ObjectSchema, fallback_type: &str) -> Result<RustType> {
    self
      .discriminator_converter
      .build_base_discriminated_enum(name, schema, fallback_type)
  }

  /// Assembles the final type collection for a struct conversion, optionally prepending
  /// a discriminated enum wrapper.
  ///
  /// When the schema defines a discriminator with mappings, this creates a tagged union
  /// enum that wraps the base struct and its subtypes, enabling polymorphic deserialization.
  pub(crate) fn finalize_struct_types(
    &self,
    name: &str,
    schema: &ObjectSchema,
    main_type: RustType,
    inline_types: Vec<RustType>,
  ) -> Result<Vec<RustType>> {
    let discriminated_enum = schema
      .is_discriminated_base_type()
      .then(|| {
        let base_struct_name = match &main_type {
          RustType::Struct(def) => def.name.as_str().to_string(),
          _ => format!("{}{DISCRIMINATED_BASE_SUFFIX}", to_rust_type_name(name)),
        };
        self.discriminated_enum(name, schema, &base_struct_name)
      })
      .transpose()?;

    Ok(
      discriminated_enum
        .into_iter()
        .chain(std::iter::once(main_type))
        .chain(inline_types)
        .collect(),
    )
  }

  /// Converts a nullable enum schema to just the enum type.
  ///
  /// For nullable enums (`anyOf: [{ enum: [...] }, null]`), generates the enum
  /// directly with the schema name. The nullability is handled at the usage site
  /// (struct fields get `Option<EnumType>`).
  fn convert_nullable_enum(&self, name: &str, schema: &ObjectSchema) -> Result<Vec<RustType>> {
    let spec = self.context.graph().spec();
    let Some(non_null_variant) = schema.find_non_null_variant(spec) else {
      return Ok(vec![]);
    };

    let resolved = self.type_resolver.resolve(non_null_variant)?;
    if resolved.has_enum_values() && resolved.enum_values.len() > 1 {
      return Ok(vec![self.enum_converter.convert_value_enum(name, &resolved)]);
    }

    Ok(vec![])
  }

  /// Attempts to create a `Vec<T>` type reference for an array schema with inline items.
  ///
  /// Returns `None` if the schema is not an array type or has no inline item definition.
  /// Returns `Some` with the `Vec` type reference and any inline types extracted from
  /// the array items (e.g., anonymous structs or enums).
  fn try_array_alias(&self, name: &str, schema: &ObjectSchema) -> Result<Option<ConversionOutput<TypeRef>>> {
    if !schema.is_array() && !schema.is_nullable_array() {
      return Ok(None);
    }

    if let Some(output) = self.type_resolver.try_inline_array(name, name, schema)? {
      let type_ref = if schema.is_nullable_array() {
        output.result.with_option()
      } else {
        output.result
      };
      return Ok(Some(ConversionOutput::with_inline_types(type_ref, output.inline_types)));
    }

    Ok(None)
  }

  /// Converts all schemas from the OpenAPI spec to Rust types.
  ///
  /// Processes schemas in two phases: first registers all top-level schemas to enable
  /// deduplication, then converts each to Rust types in specification order.
  pub(crate) fn convert_all_schemas(
    &self,
    operation_reachable: Option<&SchemaSet>,
    stats: &mut GenerationStats,
  ) -> Vec<RustType> {
    let schemas = self.context.graph().schemas();

    let filtered = schemas
      .iter()
      .filter(|(name, _)| operation_reachable.is_none_or(|filter| filter.contains(name.as_str())))
      .collect::<Vec<_>>();

    let ordered = filtered;

    {
      let mut cache = self.context().cache_mut();
      for (name, schema) in &ordered {
        let _ = cache.register_top_level_schema(schema, name);
      }
    }

    ordered.into_iter().fold(vec![], |mut acc, (name, schema)| {
      match self.convert_schema(name, schema) {
        Ok(types) => acc.extend(types),
        Err(e) => stats.record_warning(GenerationWarning::SchemaConversionFailed {
          schema_name: name.clone(),
          error: e.to_string(),
        }),
      }
      acc
    })
  }
}

#[cfg(test)]
mod tests;
