mod operations;
mod schemas;

pub use operations::{
    PARAM_LOCATIONS, generate_operations, generate_operations_tokens, group_by_tag,
    location_arg_name, location_suffix, multipart_body_struct_name, status_code_variant_name,
};
pub use schemas::{generate_types, generate_types_tokens};

pub use crate::config::Config;

use heck::{ToPascalCase, ToSnakeCase};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};

use crate::ir::{PrimitiveType, TypeRef};

/// Metadata about the OpenAPI spec being generated from.
pub struct SpecInfo {
    pub title: String,
    pub version: String,
    pub orator_version: String,
}

pub const RUST_KEYWORDS: &[&str] = &[
    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
    "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
    "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
    "unsafe", "use", "where", "while", "yield",
];

pub fn to_snake_ident(s: &str) -> Ident {
    let mut snake = s.to_snake_case();
    if snake.is_empty() {
        snake = "value".to_string();
    } else if snake.starts_with(|c: char| c.is_ascii_digit()) {
        snake = format!("_{snake}");
    }
    if RUST_KEYWORDS.contains(&snake.as_str()) {
        format_ident!("r#{}", snake)
    } else {
        Ident::new(&snake, Span::call_site())
    }
}

pub fn to_pascal_ident(s: &str) -> Ident {
    let pascal = s.to_pascal_case();
    Ident::new(&pascal, Span::call_site())
}

pub fn type_ref_to_tokens(type_ref: &TypeRef, module_prefix: Option<&TokenStream>) -> TokenStream {
    match type_ref {
        TypeRef::Named(name) => {
            let ident = to_pascal_ident(name);
            if let Some(prefix) = module_prefix {
                quote! { #prefix :: #ident }
            } else {
                quote! { #ident }
            }
        }
        TypeRef::Primitive(p) => primitive_to_tokens(p),
        TypeRef::Array(inner) => {
            let inner_tokens = type_ref_to_tokens(inner, module_prefix);
            quote! { Vec<#inner_tokens> }
        }
        TypeRef::Tuple(items) => {
            let item_tokens = items
                .iter()
                .map(|item| type_ref_to_tokens(item, module_prefix));
            quote! { ( #( #item_tokens, )* ) }
        }
        TypeRef::Option(inner) => {
            let inner_tokens = type_ref_to_tokens(inner, module_prefix);
            quote! { Option<#inner_tokens> }
        }
        TypeRef::Map(inner) => {
            let inner_tokens = type_ref_to_tokens(inner, module_prefix);
            quote! { std::collections::HashMap<String, #inner_tokens> }
        }
        TypeRef::Boxed(inner) => {
            let inner_tokens = type_ref_to_tokens(inner, module_prefix);
            quote! { Box<#inner_tokens> }
        }
        TypeRef::Any => quote! { orator_axum::serde_json::Value },
    }
}

pub fn primitive_to_tokens(p: &PrimitiveType) -> TokenStream {
    match p {
        PrimitiveType::String => quote! { String },
        PrimitiveType::Bool => quote! { bool },
        PrimitiveType::I32 => quote! { i32 },
        PrimitiveType::I64 => quote! { i64 },
        PrimitiveType::F32 => quote! { f32 },
        PrimitiveType::F64 => quote! { f64 },
        PrimitiveType::Bytes => quote! { orator_axum::bytes::Bytes },
        PrimitiveType::Date => quote! { orator_axum::chrono::NaiveDate },
        PrimitiveType::DateTime => {
            quote! { orator_axum::chrono::DateTime<orator_axum::chrono::Utc> }
        }
        PrimitiveType::Uuid => quote! { orator_axum::uuid::Uuid },
    }
}

/// Generate the banner comment prepended to every generated file.
pub fn generated_file_banner(spec_info: &SpecInfo) -> String {
    let orator_version = &spec_info.orator_version;
    format!(
        "// Generated by orator v{orator_version}\n\
         // Do not edit manually.\n\
         // {} | v{}\n\n",
        spec_info.title, spec_info.version,
    )
}

/// Emit file-level `#![allow(...)]` attributes to suppress expected lints in generated code.
pub fn generated_file_preamble() -> TokenStream {
    quote! {
        #![allow(dead_code, unused_imports)]
    }
}

pub fn generate_doc_comment(description: &Option<String>) -> TokenStream {
    match description {
        Some(desc) => {
            let doc = format!(" {desc}");
            quote! { #[doc = #doc] }
        }
        None => quote! {},
    }
}
