//! # omni-schema-core
//!
//! Core types and traits for the omni-schema library.
//!
//! This crate provides the foundational types, traits, and schema generators
//! that power the omni-schema derive macros. It can also be used directly
//! for programmatic schema generation.
//!
//! ## Core Components
//!
//! - [`Schema`] - The main trait implemented by types that can generate schemas
//! - [`SchemaType`] - Intermediate representation of type information
//! - [`SchemaDefinition`] - Complete type definition with metadata
//! - [`SchemaRegistry`] - Collection of types for batch export
//!
//! ## Format Generators
//!
//! Each output format has its own generator module (enabled via feature flags):
//!
//! - `json_schema` - JSON Schema (draft 2020-12)
//! - `openapi` - OpenAPI 3.1 components
//! - `graphql` - GraphQL SDL type definitions
//! - `protobuf` - Protocol Buffers (.proto files)
//! - `typescript` - TypeScript type definitions
//! - `avro` - Apache Avro schema

pub mod attributes;
pub mod error;
pub mod formats;
pub mod registry;
pub mod types;

pub use attributes::{
    EnumAttribute, FieldAttribute, SchemaAttributes, TypeAttribute, VariantAttribute,
};
pub use error::{SchemaError, SchemaResult};
pub use registry::SchemaRegistry;
pub use types::{
    EnumRepresentation, EnumVariant, FieldDefinition, PrimitiveType, SchemaDefinition, SchemaType,
    StructField, TypeReference,
};

#[doc(hidden)]
pub mod __private {
    pub use indexmap::IndexMap;
    pub use serde_json::json;
}

/// The main trait for types that can generate schema definitions.
///
/// This trait is typically derived using `#[derive(Schema)]` from `omni-schema-derive`,
/// but can also be implemented manually for custom types.
pub trait Schema: Sized {
    /// Returns the schema definition for this type.
    fn schema_definition() -> SchemaDefinition;

    /// Returns the type name as used in schemas.
    fn schema_name() -> &'static str;

    /// Returns the type reference for this schema.
    fn type_reference() -> TypeReference {
        TypeReference::Named(Self::schema_name().to_string())
    }

    /// Generates a JSON Schema representation.
    #[cfg(feature = "json-schema")]
    fn json_schema() -> String {
        formats::json_schema::generate(&Self::schema_definition())
    }

    /// Generates a JSON Schema as a serde_json::Value.
    #[cfg(feature = "json-schema")]
    fn json_schema_value() -> serde_json::Value {
        formats::json_schema::generate_value(&Self::schema_definition())
    }

    /// Generates an OpenAPI 3.1 component schema.
    #[cfg(feature = "openapi")]
    fn openapi_schema() -> String {
        formats::openapi::generate(&Self::schema_definition())
    }

    /// Generates a GraphQL SDL type definition.
    #[cfg(feature = "graphql")]
    fn graphql_sdl() -> String {
        formats::graphql::generate(&Self::schema_definition())
    }

    /// Generates a Protocol Buffers message definition.
    #[cfg(feature = "protobuf")]
    fn proto_definition() -> String {
        formats::protobuf::generate(&Self::schema_definition())
    }

    /// Generates a TypeScript type definition.
    #[cfg(feature = "typescript")]
    fn typescript_type() -> String {
        formats::typescript::generate(&Self::schema_definition())
    }

    /// Generates an Avro schema.
    #[cfg(feature = "avro")]
    fn avro_schema() -> String {
        formats::avro::generate(&Self::schema_definition())
    }
}

/// Trait for types that can be flattened into their parent struct.
pub trait FlattenableSchema: Schema {
    /// Returns the fields that should be inlined into the parent.
    fn flattened_fields() -> Vec<FieldDefinition>;
}

/// Trait for providing custom schema implementations for external types.
pub trait ExternalSchema {
    /// The external type this schema is for.
    type Target;

    /// Returns the schema definition for the external type.
    fn schema_definition() -> SchemaDefinition;

    /// Returns the type name for the external type.
    fn schema_name() -> &'static str {
        std::any::type_name::<Self::Target>()
            .rsplit("::")
            .next()
            .unwrap_or("Unknown")
    }
}

macro_rules! impl_schema_for_primitive {
    ($ty:ty, $prim:expr, $name:expr) => {
        impl Schema for $ty {
            fn schema_definition() -> SchemaDefinition {
                SchemaDefinition::new($name, SchemaType::Primitive($prim))
            }

            fn schema_name() -> &'static str {
                $name
            }
        }
    };
}

impl_schema_for_primitive!(bool, PrimitiveType::Bool, "bool");
impl_schema_for_primitive!(i8, PrimitiveType::I8, "i8");
impl_schema_for_primitive!(i16, PrimitiveType::I16, "i16");
impl_schema_for_primitive!(i32, PrimitiveType::I32, "i32");
impl_schema_for_primitive!(i64, PrimitiveType::I64, "i64");
impl_schema_for_primitive!(i128, PrimitiveType::I128, "i128");
impl_schema_for_primitive!(isize, PrimitiveType::Isize, "isize");
impl_schema_for_primitive!(u8, PrimitiveType::U8, "u8");
impl_schema_for_primitive!(u16, PrimitiveType::U16, "u16");
impl_schema_for_primitive!(u32, PrimitiveType::U32, "u32");
impl_schema_for_primitive!(u64, PrimitiveType::U64, "u64");
impl_schema_for_primitive!(u128, PrimitiveType::U128, "u128");
impl_schema_for_primitive!(usize, PrimitiveType::Usize, "usize");
impl_schema_for_primitive!(f32, PrimitiveType::F32, "f32");
impl_schema_for_primitive!(f64, PrimitiveType::F64, "f64");
impl_schema_for_primitive!(char, PrimitiveType::Char, "char");
impl_schema_for_primitive!(String, PrimitiveType::String, "String");
impl_schema_for_primitive!(&str, PrimitiveType::String, "str");

impl<T: Schema> Schema for Option<T> {
    fn schema_definition() -> SchemaDefinition {
        let inner = T::schema_definition();
        SchemaDefinition::new(
            "Option".to_string(),
            SchemaType::Option(Box::new(inner.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "Option"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Option(Box::new(T::type_reference()))
    }
}

impl<T: Schema> Schema for Vec<T> {
    fn schema_definition() -> SchemaDefinition {
        let inner = T::schema_definition();
        SchemaDefinition::new(
            "Vec".to_string(),
            SchemaType::Array(Box::new(inner.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "Vec"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Array(Box::new(T::type_reference()))
    }
}

impl<T: Schema> Schema for std::collections::HashSet<T> {
    fn schema_definition() -> SchemaDefinition {
        let inner = T::schema_definition();
        SchemaDefinition::new(
            "HashSet".to_string(),
            SchemaType::Set(Box::new(inner.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "HashSet"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Set(Box::new(T::type_reference()))
    }
}

impl<T: Schema> Schema for std::collections::BTreeSet<T> {
    fn schema_definition() -> SchemaDefinition {
        let inner = T::schema_definition();
        SchemaDefinition::new(
            "BTreeSet".to_string(),
            SchemaType::Set(Box::new(inner.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "BTreeSet"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Set(Box::new(T::type_reference()))
    }
}

impl<K: Schema, V: Schema> Schema for std::collections::HashMap<K, V> {
    fn schema_definition() -> SchemaDefinition {
        let value = V::schema_definition();
        SchemaDefinition::new(
            "HashMap".to_string(),
            SchemaType::Map(Box::new(value.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "HashMap"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Map(Box::new(V::type_reference()))
    }
}

impl<K: Schema, V: Schema> Schema for std::collections::BTreeMap<K, V> {
    fn schema_definition() -> SchemaDefinition {
        let value = V::schema_definition();
        SchemaDefinition::new(
            "BTreeMap".to_string(),
            SchemaType::Map(Box::new(value.schema_type)),
        )
    }

    fn schema_name() -> &'static str {
        "BTreeMap"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Map(Box::new(V::type_reference()))
    }
}

impl<T: Schema> Schema for Box<T> {
    fn schema_definition() -> SchemaDefinition {
        T::schema_definition()
    }

    fn schema_name() -> &'static str {
        T::schema_name()
    }

    fn type_reference() -> TypeReference {
        T::type_reference()
    }
}

macro_rules! impl_schema_for_tuple {
    ($($T:ident),+) => {
        impl<$($T: Schema),+> Schema for ($($T,)+) {
            fn schema_definition() -> SchemaDefinition {
                let types = vec![
                    $(Box::new($T::schema_definition().schema_type)),+
                ];
                SchemaDefinition::new(
                    "Tuple",
                    SchemaType::Tuple(types),
                )
            }

            fn schema_name() -> &'static str {
                "Tuple"
            }

            fn type_reference() -> TypeReference {
                TypeReference::Tuple(vec![$($T::type_reference()),+])
            }
        }
    };
}

impl_schema_for_tuple!(T0);
impl_schema_for_tuple!(T0, T1);
impl_schema_for_tuple!(T0, T1, T2);
impl_schema_for_tuple!(T0, T1, T2, T3);
impl_schema_for_tuple!(T0, T1, T2, T3, T4);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
impl_schema_for_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);

impl Schema for () {
    fn schema_definition() -> SchemaDefinition {
        SchemaDefinition::new("Unit", SchemaType::Unit)
    }

    fn schema_name() -> &'static str {
        "()"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Unit
    }
}

#[cfg(feature = "uuid-support")]
impl Schema for uuid::Uuid {
    fn schema_definition() -> SchemaDefinition {
        SchemaDefinition::new("Uuid", SchemaType::Primitive(PrimitiveType::String))
            .with_format("uuid")
            .with_description("A universally unique identifier (UUID)")
    }

    fn schema_name() -> &'static str {
        "Uuid"
    }
}

#[cfg(feature = "chrono-support")]
mod chrono_impls {
    use super::*;

    impl<Tz: chrono::TimeZone> Schema for chrono::DateTime<Tz> {
        fn schema_definition() -> SchemaDefinition {
            SchemaDefinition::new("DateTime", SchemaType::Primitive(PrimitiveType::String))
                .with_format("date-time")
                .with_description("An RFC 3339 date-time string")
        }

        fn schema_name() -> &'static str {
            "DateTime"
        }
    }

    impl Schema for chrono::NaiveDate {
        fn schema_definition() -> SchemaDefinition {
            SchemaDefinition::new("Date", SchemaType::Primitive(PrimitiveType::String))
                .with_format("date")
                .with_description("A date in YYYY-MM-DD format")
        }

        fn schema_name() -> &'static str {
            "Date"
        }
    }

    impl Schema for chrono::NaiveTime {
        fn schema_definition() -> SchemaDefinition {
            SchemaDefinition::new("Time", SchemaType::Primitive(PrimitiveType::String))
                .with_format("time")
                .with_description("A time in HH:MM:SS format")
        }

        fn schema_name() -> &'static str {
            "Time"
        }
    }

    impl Schema for chrono::NaiveDateTime {
        fn schema_definition() -> SchemaDefinition {
            SchemaDefinition::new("NaiveDateTime", SchemaType::Primitive(PrimitiveType::String))
                .with_format("date-time")
                .with_description("A date-time without timezone information")
        }

        fn schema_name() -> &'static str {
            "NaiveDateTime"
        }
    }

    impl Schema for chrono::Duration {
        fn schema_definition() -> SchemaDefinition {
            SchemaDefinition::new("Duration", SchemaType::Primitive(PrimitiveType::String))
                .with_format("duration")
                .with_description("An ISO 8601 duration string")
        }

        fn schema_name() -> &'static str {
            "Duration"
        }
    }
}

#[cfg(feature = "url-support")]
impl Schema for url::Url {
    fn schema_definition() -> SchemaDefinition {
        SchemaDefinition::new("Url", SchemaType::Primitive(PrimitiveType::String))
            .with_format("uri")
            .with_description("A valid URL")
    }

    fn schema_name() -> &'static str {
        "Url"
    }
}

impl Schema for serde_json::Value {
    fn schema_definition() -> SchemaDefinition {
        SchemaDefinition::new("JsonValue", SchemaType::Any)
            .with_description("Any valid JSON value")
    }

    fn schema_name() -> &'static str {
        "JsonValue"
    }

    fn type_reference() -> TypeReference {
        TypeReference::Any
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_primitive_schema() {
        let def = String::schema_definition();
        assert_eq!(def.name, "String");
        assert!(matches!(
            def.schema_type,
            SchemaType::Primitive(PrimitiveType::String)
        ));
    }

    #[test]
    fn test_option_schema() {
        let def = Option::<String>::schema_definition();
        assert!(matches!(def.schema_type, SchemaType::Option(_)));
    }

    #[test]
    fn test_vec_schema() {
        let def = Vec::<i32>::schema_definition();
        assert!(matches!(def.schema_type, SchemaType::Array(_)));
    }

    #[test]
    fn test_hashmap_schema() {
        let def = std::collections::HashMap::<String, i32>::schema_definition();
        assert!(matches!(def.schema_type, SchemaType::Map(_)));
    }
}
