//! OSchema macro for generating Rust types from .oschema files.
//!
//! This module provides the `include_oschema!` macro that reads an .oschema file
//! at compile time and generates corresponding Rust struct/enum definitions.
//!
//! # Usage
//!
//! ```ignore
//! // Simple - Rust structs only
//! include_oschema!("task.oschema");
//!
//! // With namespace - enables Rhai function generation
//! include_oschema!("task.oschema", namespace = "myproject");
//!
//! // Explicit Rhai control
//! include_oschema!("task.oschema", namespace = "myproject", rhai = true);
//! include_oschema!("config.oschema", namespace = "myproject", rhai = false);
//! ```

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::path::PathBuf;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitBool, LitStr, Token, parse_macro_input};

/// Configuration for include_oschema! macro.
#[derive(Default)]
struct OSchemaConfig {
    /// Path to the .oschema file (required).
    path: String,
    /// Namespace for Rhai functions (optional).
    /// If set, enables Rhai function generation.
    namespace: Option<String>,
    /// Whether to generate Rhai bindings.
    /// Default: true if namespace is set, false otherwise.
    rhai: Option<bool>,
    /// Output directory for .generated.rs file (optional).
    /// If not set, outputs next to the .oschema file.
    output_dir: Option<String>,
}

impl Parse for OSchemaConfig {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let path_lit: LitStr = input.parse()?;
        let mut config = OSchemaConfig {
            path: path_lit.value(),
            namespace: None,
            rhai: None,
            output_dir: None,
        };

        // Parse optional key=value pairs
        while input.peek(Token![,]) {
            input.parse::<Token![,]>()?;

            // Check for end of input after comma
            if input.is_empty() {
                break;
            }

            let key: Ident = input.parse()?;
            input.parse::<Token![=]>()?;

            match key.to_string().as_str() {
                "namespace" => {
                    let value: LitStr = input.parse()?;
                    config.namespace = Some(value.value());
                }
                "rhai" => {
                    let value: LitBool = input.parse()?;
                    config.rhai = Some(value.value);
                }
                "output_dir" => {
                    let value: LitStr = input.parse()?;
                    config.output_dir = Some(value.value());
                }
                other => {
                    return Err(syn::Error::new(
                        key.span(),
                        format!(
                            "unknown option '{}'. Expected 'namespace', 'rhai', or 'output_dir'",
                            other
                        ),
                    ));
                }
            }
        }

        Ok(config)
    }
}

impl OSchemaConfig {
    /// Whether Rhai bindings should be generated.
    #[allow(dead_code)]
    fn should_generate_rhai(&self) -> bool {
        // Explicit setting takes priority
        if let Some(rhai) = self.rhai {
            return rhai;
        }
        // Default: generate Rhai if namespace is set
        self.namespace.is_some()
    }
}

/// Implementation of the `include_oschema!` macro.
///
/// Reads an .oschema file and generates Rust types at compile time.
/// Also writes the generated code to a `.generated.rs` file next to the schema
/// for easy inspection during development.
pub fn impl_include_oschema(input: TokenStream) -> TokenStream {
    let config = parse_macro_input!(input as OSchemaConfig);
    let schema_path = &config.path;

    // Resolve path relative to CARGO_MANIFEST_DIR
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");

    let full_path = PathBuf::from(&manifest_dir).join(schema_path);

    // Read the schema file
    let schema_content = match std::fs::read_to_string(&full_path) {
        Ok(content) => content,
        Err(e) => {
            let error_msg = format!(
                "Failed to read oschema file '{}': {}",
                full_path.display(),
                e
            );
            return syn::Error::new(proc_macro2::Span::call_site(), error_msg)
                .to_compile_error()
                .into();
        }
    };

    // Parse and generate Rust code
    match generate_rust_from_oschema(&schema_content, schema_path, &full_path, &config) {
        Ok(tokens) => tokens.into(),
        Err(e) => syn::Error::new(proc_macro2::Span::call_site(), e)
            .to_compile_error()
            .into(),
    }
}

/// Parse oschema and generate Rust code.
///
/// This is a self-contained parser that doesn't depend on herolib_osis
/// (since proc-macro crates have restrictions on dependencies).
///
/// Also writes the generated code to a `.generated.rs` file for inspection.
fn generate_rust_from_oschema(
    schema: &str,
    source_path: &str,
    full_path: &PathBuf,
    config: &OSchemaConfig,
) -> Result<TokenStream2, String> {
    // Track already included files to prevent circular includes
    let mut included_files = std::collections::HashSet::new();
    included_files.insert(
        full_path
            .canonicalize()
            .unwrap_or_else(|_| full_path.clone()),
    );

    // Parse with include support
    let parsed = parse_oschema_with_includes(schema, full_path, &mut included_files)?;
    let tokens = generate_rust_code(&parsed, source_path, config, schema)?;

    // Write generated code to .generated.rs file for inspection
    let generated_path = if let Some(ref output_dir) = config.output_dir {
        // Use custom output directory
        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
        let output_path = PathBuf::from(&manifest_dir).join(output_dir);
        let file_stem = full_path.file_stem().unwrap_or_default();
        output_path.join(format!("{}.generated.rs", file_stem.to_string_lossy()))
    } else {
        // Default: next to the .oschema file
        full_path.with_extension("generated.rs")
    };

    // Format the tokens nicely
    let formatted_code = format_generated_code(&tokens);

    let generated_code = format!(
        r#"// =============================================================================
// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
// =============================================================================
//
// Generated from: {}
// Generator: herolib_derive::include_oschema!
//
// This file is for inspection only. The actual code is generated at compile
// time by the include_oschema! macro.
//
// =============================================================================

#![allow(dead_code)]
#![allow(unused_imports)]

use serde::{{Deserialize, Serialize}};
use herolib_osis::OTime;

{}
"#,
        source_path, formatted_code
    );

    // Write file (ignore errors - this is just for development convenience)
    let _ = std::fs::write(&generated_path, &generated_code);

    Ok(tokens)
}

// ============================================================================
// Simple OSchema Parser (self-contained for proc-macro)
// ============================================================================

#[derive(Debug, Clone)]
enum TypeDef {
    /// String enum: Status = "active" | "disabled"
    StringEnum {
        name: String,
        variants: Vec<String>,
        comments: Vec<String>,
    },
    /// Struct: User = { id: u64, name: str }
    Struct {
        name: String,
        fields: Vec<Field>,
        comments: Vec<String>,
    },
    /// Service definition: service User { ... }
    Service {
        name: String,
        version: Option<String>,
        description: Option<String>,
        methods: Vec<ServiceMethod>,
        comments: Vec<String>,
    },
}

/// A method in a service definition.
#[derive(Debug, Clone)]
struct ServiceMethod {
    /// Method name (e.g., "get_user")
    name: String,
    /// Method parameters
    params: Vec<ServiceParam>,
    /// Return type (None for void)
    return_type: Option<FieldType>,
    /// Error types that can be returned
    errors: Vec<String>,
    /// Comment/description
    comment: Option<String>,
}

/// A parameter in a service method.
#[derive(Debug, Clone)]
struct ServiceParam {
    /// Parameter name
    name: String,
    /// Parameter type
    ty: FieldType,
    /// Whether the parameter is optional
    optional: bool,
}

#[derive(Debug, Clone)]
struct Field {
    name: String,
    ty: FieldType,
    optional: bool,
    indexed: bool,
    comment: Option<String>,
    default_value: Option<DefaultValue>,
}

#[derive(Debug, Clone)]
enum DefaultValue {
    String(String),
    Integer(i64),
    Float(f64),
    Boolean(bool),
    List(Vec<DefaultValue>),
}

#[derive(Debug, Clone)]
enum FieldType {
    Primitive(String), // u64, str, bool, time, etc.
    Named(String),     // Reference to another type
    List(Box<FieldType>),
    Map(Box<FieldType>, Box<FieldType>),
}

fn parse_string_enum_variants(input: &str) -> Result<Vec<String>, String> {
    let mut variants = Vec::new();

    for part in input.split('|') {
        let trimmed = part.trim();
        if trimmed.starts_with('"') && trimmed.ends_with('"') {
            variants.push(trimmed[1..trimmed.len() - 1].to_string());
        }
    }

    Ok(variants)
}

/// Parse an include directive line.
/// Returns Some(path) if line is `include "path"`, None otherwise.
fn parse_include_directive(line: &str) -> Option<String> {
    let trimmed = line.trim();
    if !trimmed.starts_with("include ") {
        return None;
    }

    let rest = trimmed["include ".len()..].trim();

    // Extract quoted path
    if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
        Some(rest[1..rest.len() - 1].to_string())
    } else {
        None
    }
}

/// Parse oschema with support for include directives.
/// Recursively processes included files and merges all definitions.
fn parse_oschema_with_includes(
    input: &str,
    current_file: &PathBuf,
    included_files: &mut std::collections::HashSet<PathBuf>,
) -> Result<Vec<TypeDef>, String> {
    let mut all_definitions = Vec::new();
    let current_dir = current_file
        .parent()
        .ok_or_else(|| format!("Cannot get parent directory of {:?}", current_file))?;

    // First pass: process include directives
    for line in input.lines() {
        let trimmed = line.trim();

        // Check for include directive
        if let Some(include_path) = parse_include_directive(trimmed) {
            let included_file = current_dir.join(&include_path);

            // Canonicalize for comparison
            let canonical = included_file
                .canonicalize()
                .unwrap_or_else(|_| included_file.clone());

            // Skip if already included (prevents circular includes)
            if included_files.contains(&canonical) {
                continue;
            }
            included_files.insert(canonical.clone());

            // Read and parse the included file
            let included_content = std::fs::read_to_string(&included_file).map_err(|e| {
                format!(
                    "Failed to read included file '{}': {}",
                    included_file.display(),
                    e
                )
            })?;

            // Recursively parse the included file
            let included_defs =
                parse_oschema_with_includes(&included_content, &included_file, included_files)?;
            all_definitions.extend(included_defs);
        }
    }

    // Second pass: parse the current file's definitions (excluding includes)
    let current_defs = parse_oschema_complete_no_includes(input)?;
    all_definitions.extend(current_defs);

    Ok(all_definitions)
}

/// State for parsing service blocks
struct ServiceParseState {
    name: String,
    version: Option<String>,
    description: Option<String>,
    methods: Vec<ServiceMethod>,
    comments: Vec<String>,
    current_method_comment: Option<String>,
    /// For accumulating multi-line method definitions
    method_buffer: Option<String>,
}

/// More complete parser that handles multiline structs and services (without include processing).
fn parse_oschema_complete_no_includes(input: &str) -> Result<Vec<TypeDef>, String> {
    let mut definitions = Vec::new();
    let mut current_comments = Vec::new();
    let mut in_struct: Option<(String, Vec<Field>, Vec<String>)> = None;
    let mut in_service: Option<ServiceParseState> = None;

    for line in input.lines() {
        let trimmed = line.trim();

        // Skip include directives (handled separately)
        if trimmed.starts_with("include ") {
            continue;
        }

        // Skip empty lines (but not inside structs/services)
        if trimmed.is_empty() {
            if in_struct.is_none() && in_service.is_none() {
                continue;
            }
        }

        // Handle end of struct or service
        if trimmed == "}" {
            if let Some((name, fields, comments)) = in_struct.take() {
                definitions.push(TypeDef::Struct {
                    name,
                    fields,
                    comments,
                });
            }
            if let Some(svc) = in_service.take() {
                definitions.push(TypeDef::Service {
                    name: svc.name,
                    version: svc.version,
                    description: svc.description,
                    methods: svc.methods,
                    comments: svc.comments,
                });
            }
            continue;
        }

        // If we're inside a struct, parse fields
        if let Some((_, ref mut fields, _)) = in_struct {
            if !trimmed.is_empty() && !trimmed.starts_with('#') {
                if let Some(field) = parse_field(trimmed)? {
                    fields.push(field);
                }
            }
            continue;
        }

        // If we're inside a service, parse methods and metadata
        if let Some(ref mut svc) = in_service {
            // Skip empty lines (but not if we're accumulating a multi-line method)
            if trimmed.is_empty() && svc.method_buffer.is_none() {
                continue;
            }

            // If we're accumulating a multi-line method definition
            if let Some(ref mut buffer) = svc.method_buffer {
                // Skip empty lines and comments while accumulating
                if trimmed.is_empty() || trimmed.starts_with('#') {
                    continue;
                }

                // Check if this line ends the method (contains closing paren)
                if trimmed.contains(')') {
                    // This line contains the closing paren - append and parse
                    // Add comma separator for previous params if needed
                    if !buffer.ends_with('(') && !buffer.ends_with(',') {
                        buffer.push(',');
                    }
                    buffer.push(' ');
                    buffer.push_str(trimmed);

                    // Try to parse the complete method
                    let complete_line = buffer.clone();
                    let comment = svc.current_method_comment.take();
                    svc.method_buffer = None;

                    if let Some(method) = parse_service_method(&complete_line, comment)? {
                        svc.methods.push(method);
                    }
                } else {
                    // This is a parameter line - add with comma separator
                    if !buffer.ends_with('(') && !buffer.ends_with(',') {
                        buffer.push(',');
                    }
                    buffer.push(' ');
                    buffer.push_str(trimmed);
                }
                continue;
            }

            // Collect method comments
            if trimmed.starts_with('#') {
                svc.current_method_comment = Some(trimmed[1..].trim().to_string());
                continue;
            }

            // Parse service metadata: version: "1.0.0"
            if trimmed.starts_with("version:") {
                if let Some(v) = extract_quoted_value(trimmed) {
                    svc.version = Some(v);
                }
                continue;
            }

            // Parse service metadata: description: "..."
            if trimmed.starts_with("description:") {
                if let Some(d) = extract_quoted_value(trimmed) {
                    svc.description = Some(d);
                }
                continue;
            }

            // Skip error declarations (they follow method definitions)
            if trimmed.starts_with("error:") {
                // Add errors to the last method
                if let Some(last_method) = svc.methods.last_mut() {
                    let errors_part = trimmed.strip_prefix("error:").unwrap().trim();
                    for err in errors_part.split('|') {
                        let err_name = err.trim();
                        if !err_name.is_empty() {
                            last_method.errors.push(err_name.to_string());
                        }
                    }
                }
                continue;
            }

            // Check if this is a method definition start
            if trimmed.contains('(') {
                // Check if it's a complete single-line method (has closing paren)
                if trimmed.contains(')') {
                    // Single-line method - parse immediately
                    if let Some(method) =
                        parse_service_method(trimmed, svc.current_method_comment.take())?
                    {
                        svc.methods.push(method);
                    }
                } else {
                    // Multi-line method - start accumulating
                    svc.method_buffer = Some(trimmed.to_string());
                }
                continue;
            }

            // Skip separator lines (like ─────────)
            if trimmed
                .chars()
                .all(|c| c == '─' || c == '-' || c == '=' || c.is_whitespace())
            {
                continue;
            }

            continue;
        }

        // Collect comments
        if trimmed.starts_with('#') {
            current_comments.push(trimmed[1..].trim().to_string());
            continue;
        }

        // Check for service definition: service Name {
        if trimmed.starts_with("service ") {
            if let Some(rest) = trimmed.strip_prefix("service ") {
                let rest = rest.trim();
                if rest.ends_with('{') {
                    let name = rest.trim_end_matches('{').trim().to_string();
                    in_service = Some(ServiceParseState {
                        name,
                        version: None,
                        description: None,
                        methods: Vec::new(),
                        comments: std::mem::take(&mut current_comments),
                        current_method_comment: None,
                        method_buffer: None,
                    });
                    continue;
                }
            }
        }

        // Parse type definition start
        let parts: Vec<&str> = trimmed.splitn(2, '=').collect();
        if parts.len() != 2 {
            continue;
        }

        let name = parts[0].trim().to_string();
        let rest = parts[1].trim();

        // String enum
        if rest.starts_with('"') {
            let variants = parse_string_enum_variants(rest)?;
            definitions.push(TypeDef::StringEnum {
                name,
                variants,
                comments: std::mem::take(&mut current_comments),
            });
            continue;
        }

        // Struct start
        if rest.starts_with('{') {
            in_struct = Some((name, Vec::new(), std::mem::take(&mut current_comments)));
            continue;
        }

        current_comments.clear();
    }

    Ok(definitions)
}

/// Extract a quoted string value from a line like: key: "value"
fn extract_quoted_value(line: &str) -> Option<String> {
    let start = line.find('"')?;
    let rest = &line[start + 1..];
    let end = rest.find('"')?;
    Some(rest[..end].to_string())
}

/// Parse a service method definition.
/// Format: method_name(param1: type, param2?: type) -> ReturnType
/// Or: method_name(param1: type)  (no return type = void)
fn parse_service_method(
    line: &str,
    comment: Option<String>,
) -> Result<Option<ServiceMethod>, String> {
    // Find method name (before the parenthesis)
    let paren_start = match line.find('(') {
        Some(idx) => idx,
        None => return Ok(None), // Not a method definition
    };

    let method_name = line[..paren_start].trim().to_string();
    if method_name.is_empty() {
        return Ok(None);
    }

    // Find the closing parenthesis
    let paren_end = match line.find(')') {
        Some(idx) => idx,
        None => return Err(format!("Missing closing parenthesis in method: {}", line)),
    };

    // Parse parameters
    let params_str = &line[paren_start + 1..paren_end];
    let params = parse_service_params(params_str)?;

    // Parse return type (after ->)
    let return_type = if let Some(arrow_idx) = line.find("->") {
        let return_str = line[arrow_idx + 2..].trim();
        // Remove any trailing comments
        let return_str = return_str.split('#').next().unwrap_or(return_str).trim();
        if return_str.is_empty() {
            None
        } else {
            Some(parse_field_type(return_str)?)
        }
    } else {
        None
    };

    Ok(Some(ServiceMethod {
        name: method_name,
        params,
        return_type,
        errors: Vec::new(), // Errors are parsed separately
        comment,
    }))
}

/// Parse service method parameters.
/// Format: param1: type, param2?: type
fn parse_service_params(params_str: &str) -> Result<Vec<ServiceParam>, String> {
    let mut params = Vec::new();

    if params_str.trim().is_empty() {
        return Ok(params);
    }

    // Split by comma, but be careful with nested types like {str:any}
    let mut current = String::new();
    let mut brace_depth = 0;
    let mut bracket_depth = 0;

    for c in params_str.chars() {
        match c {
            '{' => {
                brace_depth += 1;
                current.push(c);
            }
            '}' => {
                brace_depth -= 1;
                current.push(c);
            }
            '[' => {
                bracket_depth += 1;
                current.push(c);
            }
            ']' => {
                bracket_depth -= 1;
                current.push(c);
            }
            ',' if brace_depth == 0 && bracket_depth == 0 => {
                if !current.trim().is_empty() {
                    if let Some(param) = parse_single_param(current.trim())? {
                        params.push(param);
                    }
                }
                current.clear();
            }
            _ => current.push(c),
        }
    }

    // Don't forget the last parameter
    if !current.trim().is_empty() {
        if let Some(param) = parse_single_param(current.trim())? {
            params.push(param);
        }
    }

    Ok(params)
}

/// Parse a single parameter: name: type or name?: type
fn parse_single_param(param_str: &str) -> Result<Option<ServiceParam>, String> {
    // Split by colon
    let parts: Vec<&str> = param_str.splitn(2, ':').collect();
    if parts.len() != 2 {
        return Ok(None);
    }

    let name_part = parts[0].trim();
    let type_part = parts[1].trim();

    // Check for optional marker
    let (name, optional) = if name_part.ends_with('?') {
        (name_part.trim_end_matches('?').to_string(), true)
    } else {
        (name_part.to_string(), false)
    };

    let ty = parse_field_type(type_part)?;

    Ok(Some(ServiceParam { name, ty, optional }))
}

fn parse_field(line: &str) -> Result<Option<Field>, String> {
    // Format: name: type  # comment [index]
    // or:     name?: type  # comment [index] (optional)
    // or:     name: type = default  # comment [index] (with default)

    // Extract comment first
    let (field_part, raw_comment) = if let Some(idx) = line.find('#') {
        (&line[..idx], Some(line[idx + 1..].trim().to_string()))
    } else {
        (line, None)
    };

    // Check for [index] marker in comment and extract clean comment
    let (comment, indexed) = if let Some(ref c) = raw_comment {
        if c.contains("[index]") {
            // Remove [index] from comment
            let clean = c.replace("[index]", "").trim().to_string();
            let clean_comment = if clean.is_empty() { None } else { Some(clean) };
            (clean_comment, true)
        } else {
            (raw_comment, false)
        }
    } else {
        (None, false)
    };

    let field_part = field_part.trim();
    if field_part.is_empty() {
        return Ok(None);
    }

    // Split name and type (and potentially default)
    let parts: Vec<&str> = field_part.splitn(2, ':').collect();
    if parts.len() != 2 {
        return Ok(None);
    }

    let mut name = parts[0].trim().to_string();
    let type_and_default = parts[1].trim();

    // Check for optional (?)
    let optional = name.ends_with('?');
    if optional {
        name = name[..name.len() - 1].to_string();
    }

    // Split type and default value (if present)
    let (type_str, default_value) = if let Some(eq_idx) = type_and_default.find('=') {
        let type_str = type_and_default[..eq_idx].trim();
        let default_str = type_and_default[eq_idx + 1..].trim();
        let default = parse_default_value(default_str)?;
        (type_str, Some(default))
    } else {
        (type_and_default, None)
    };

    let ty = parse_field_type(type_str)?;

    Ok(Some(Field {
        name,
        ty,
        optional,
        indexed,
        comment,
        default_value,
    }))
}

fn parse_default_value(s: &str) -> Result<DefaultValue, String> {
    let s = s.trim();

    // List literal: ["a", "b"] or [1, 2, 3]
    if s.starts_with('[') && s.ends_with(']') {
        let inner = &s[1..s.len() - 1].trim();
        if inner.is_empty() {
            return Ok(DefaultValue::List(Vec::new()));
        }

        let mut elements = Vec::new();
        let mut current = String::new();
        let mut in_string = false;
        let mut depth = 0;

        for ch in inner.chars() {
            match ch {
                '"' => {
                    in_string = !in_string;
                    current.push(ch);
                }
                '[' | '(' | '{' if !in_string => {
                    depth += 1;
                    current.push(ch);
                }
                ']' | ')' | '}' if !in_string => {
                    depth -= 1;
                    current.push(ch);
                }
                ',' if !in_string && depth == 0 => {
                    let trimmed = current.trim();
                    if !trimmed.is_empty() {
                        elements.push(parse_default_value(trimmed)?);
                    }
                    current.clear();
                }
                _ => {
                    current.push(ch);
                }
            }
        }

        let trimmed = current.trim();
        if !trimmed.is_empty() {
            elements.push(parse_default_value(trimmed)?);
        }

        return Ok(DefaultValue::List(elements));
    }

    // String literal
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        return Ok(DefaultValue::String(s[1..s.len() - 1].to_string()));
    }

    // Boolean
    if s == "true" {
        return Ok(DefaultValue::Boolean(true));
    }
    if s == "false" {
        return Ok(DefaultValue::Boolean(false));
    }

    // Try to parse as integer
    if let Ok(i) = s.parse::<i64>() {
        return Ok(DefaultValue::Integer(i));
    }

    // Try to parse as float
    if let Ok(f) = s.parse::<f64>() {
        return Ok(DefaultValue::Float(f));
    }

    Err(format!("Invalid default value: {}", s))
}

fn parse_field_type(type_str: &str) -> Result<FieldType, String> {
    let type_str = type_str.trim();

    // List: [type]
    if type_str.starts_with('[') && type_str.ends_with(']') {
        let inner = &type_str[1..type_str.len() - 1];
        let inner_type = parse_field_type(inner)?;
        return Ok(FieldType::List(Box::new(inner_type)));
    }

    // Map: {key:value}
    if type_str.starts_with('{') && type_str.ends_with('}') {
        let inner = &type_str[1..type_str.len() - 1];
        let parts: Vec<&str> = inner.splitn(2, ':').collect();
        if parts.len() == 2 {
            let key_type = parse_field_type(parts[0])?;
            let value_type = parse_field_type(parts[1])?;
            return Ok(FieldType::Map(Box::new(key_type), Box::new(value_type)));
        }
    }

    // Primitive or named type
    Ok(match type_str {
        "str" | "string" => FieldType::Primitive("String".to_string()),
        "bool" => FieldType::Primitive("bool".to_string()),
        "u8" => FieldType::Primitive("u8".to_string()),
        "u16" => FieldType::Primitive("u16".to_string()),
        "u32" => FieldType::Primitive("u32".to_string()),
        "u64" => FieldType::Primitive("u64".to_string()),
        "i8" => FieldType::Primitive("i8".to_string()),
        "i16" => FieldType::Primitive("i16".to_string()),
        "i32" => FieldType::Primitive("i32".to_string()),
        "i64" => FieldType::Primitive("i64".to_string()),
        "f32" => FieldType::Primitive("f32".to_string()),
        "f64" => FieldType::Primitive("f64".to_string()),
        "otime" | "time" => FieldType::Primitive("OTime".to_string()),
        "ocur" => FieldType::Primitive("OCur".to_string()),
        "olocation" => FieldType::Primitive("OLocation".to_string()),
        "oaddress" => FieldType::Primitive("OAddress".to_string()),
        "any" => FieldType::Primitive("serde_json::Value".to_string()),
        other => FieldType::Named(other.to_string()),
    })
}

// ============================================================================
// Rust Code Generation
// ============================================================================

/// Generate a JSON Schema string at compile time from field definitions.
/// Includes $defs for all referenced enum types (modern JSON Schema style).
fn generate_json_schema_string(
    name: &str,
    fields: &[Field],
    enums: &[(&str, &[String])],
    structs: &[(&str, &[Field])],
    all_type_names: &[String],
) -> String {
    let mut properties = Vec::new();
    let mut required = Vec::new();

    for field in fields {
        let field_name = to_snake_case(&field.name);
        let mut field_schema = field_type_to_json_schema(&field.ty, all_type_names, field.optional);

        // Add default value if present
        if let Some(ref default) = field.default_value {
            let default_json = match default {
                DefaultValue::String(s) => {
                    format!(r#""{}""#, s.replace('\\', "\\\\").replace('"', "\\\""))
                }
                DefaultValue::Integer(i) => i.to_string(),
                DefaultValue::Float(f) => f.to_string(),
                DefaultValue::Boolean(b) => b.to_string(),
                DefaultValue::List(elements) => {
                    let elem_strs: Vec<String> = elements
                        .iter()
                        .map(|e| match e {
                            DefaultValue::String(s) => {
                                format!(r#""{}""#, s.replace('\\', "\\\\").replace('"', "\\\""))
                            }
                            DefaultValue::Integer(i) => i.to_string(),
                            DefaultValue::Float(f) => f.to_string(),
                            DefaultValue::Boolean(b) => b.to_string(),
                            DefaultValue::List(_) => "[]".to_string(),
                        })
                        .collect();
                    format!("[{}]", elem_strs.join(", "))
                }
            };
            // Insert default before closing brace
            if field_schema.ends_with('}') {
                field_schema.insert_str(
                    field_schema.len() - 1,
                    &format!(r#", "default": {}"#, default_json),
                );
            }
        }

        properties.push(format!(r#""{}": {}"#, field_name, field_schema));
        if !field.optional {
            required.push(format!(r#""{}""#, field_name));
        }
    }

    // Build $defs for all types (enums and structs)
    let mut defs: Vec<String> = Vec::new();

    // Add enum definitions
    for (enum_name, variants) in enums {
        let enum_values: Vec<String> = variants.iter().map(|v| format!(r#""{}""#, v)).collect();
        defs.push(format!(
            r#""{}": {{"type": "string", "enum": [{}], "description": "{} enumeration"}}"#,
            enum_name,
            enum_values.join(", "),
            enum_name
        ));
    }

    // Add struct definitions (excluding the main struct itself)
    for (struct_name, struct_fields) in structs {
        if *struct_name == name {
            continue; // Skip the main struct
        }
        let struct_schema = generate_struct_schema(struct_fields, all_type_names);
        defs.push(format!(r#""{}": {}"#, struct_name, struct_schema));
    }

    if defs.is_empty() {
        format!(
            r#"{{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "{}", "type": "object", "properties": {{{}}}, "required": [{}]}}"#,
            name,
            properties.join(", "),
            required.join(", ")
        )
    } else {
        format!(
            r#"{{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "{}", "type": "object", "properties": {{{}}}, "required": [{}], "$defs": {{{}}}}}"#,
            name,
            properties.join(", "),
            required.join(", "),
            defs.join(", ")
        )
    }
}

/// Generate a JSON schema object for a struct (used in $defs).
fn generate_struct_schema(fields: &[Field], all_type_names: &[String]) -> String {
    let mut properties = Vec::new();
    let mut required = Vec::new();

    for field in fields {
        let field_name = to_snake_case(&field.name);
        let mut field_schema = field_type_to_json_schema(&field.ty, all_type_names, field.optional);

        // Add default value if present
        if let Some(ref default) = field.default_value {
            let default_json = match default {
                DefaultValue::String(s) => {
                    format!(r#""{}""#, s.replace('\\', "\\\\").replace('"', "\\\""))
                }
                DefaultValue::Integer(i) => i.to_string(),
                DefaultValue::Float(f) => f.to_string(),
                DefaultValue::Boolean(b) => b.to_string(),
                DefaultValue::List(elements) => {
                    let elem_strs: Vec<String> = elements
                        .iter()
                        .map(|e| match e {
                            DefaultValue::String(s) => {
                                format!(r#""{}""#, s.replace('\\', "\\\\").replace('"', "\\\""))
                            }
                            DefaultValue::Integer(i) => i.to_string(),
                            DefaultValue::Float(f) => f.to_string(),
                            DefaultValue::Boolean(b) => b.to_string(),
                            DefaultValue::List(_) => "[]".to_string(),
                        })
                        .collect();
                    format!("[{}]", elem_strs.join(", "))
                }
            };
            // Insert default before closing brace
            if field_schema.ends_with('}') {
                field_schema.insert_str(
                    field_schema.len() - 1,
                    &format!(r#", "default": {}"#, default_json),
                );
            }
        }

        properties.push(format!(r#""{}": {}"#, field_name, field_schema));
        if !field.optional {
            required.push(format!(r#""{}""#, field_name));
        }
    }

    format!(
        r#"{{"type": "object", "properties": {{{}}}, "required": [{}]}}"#,
        properties.join(", "),
        required.join(", ")
    )
}

/// Convert a FieldType to a JSON Schema string.
/// Uses type arrays for nullable fields (better tooling support than oneOf).
/// Uses $ref to $defs for all known types (enums and structs).
fn field_type_to_json_schema(ty: &FieldType, all_type_names: &[String], nullable: bool) -> String {
    let base_schema = match ty {
        FieldType::Primitive(p) => match p.as_str() {
            "String" => {
                if nullable {
                    r#"{"type": ["string", "null"]}"#.to_string()
                } else {
                    r#"{"type": "string"}"#.to_string()
                }
            }
            "bool" => {
                if nullable {
                    r#"{"type": ["boolean", "null"]}"#.to_string()
                } else {
                    r#"{"type": "boolean"}"#.to_string()
                }
            }
            "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" => {
                if nullable {
                    r#"{"type": ["integer", "null"]}"#.to_string()
                } else {
                    r#"{"type": "integer"}"#.to_string()
                }
            }
            "f32" | "f64" => {
                if nullable {
                    r#"{"type": ["number", "null"]}"#.to_string()
                } else {
                    r#"{"type": "number"}"#.to_string()
                }
            }
            "OTime" => {
                if nullable {
                    r#"{"type": ["string", "null"], "format": "date-time"}"#.to_string()
                } else {
                    r#"{"type": "string", "format": "date-time"}"#.to_string()
                }
            }
            "OCur" => {
                if nullable {
                    r#"{"type": ["string", "null"], "description": "Currency value"}"#.to_string()
                } else {
                    r#"{"type": "string", "description": "Currency value"}"#.to_string()
                }
            }
            "OLocation" => {
                if nullable {
                    r#"{"type": ["object", "null"], "description": "Geographic location"}"#
                        .to_string()
                } else {
                    r#"{"type": "object", "description": "Geographic location"}"#.to_string()
                }
            }
            "OAddress" => {
                if nullable {
                    r#"{"type": ["object", "null"], "description": "Address"}"#.to_string()
                } else {
                    r#"{"type": "object", "description": "Address"}"#.to_string()
                }
            }
            "serde_json::Value" => r#"{"type": "object"}"#.to_string(),
            _ => {
                if nullable {
                    r#"{"type": ["object", "null"]}"#.to_string()
                } else {
                    r#"{"type": "object"}"#.to_string()
                }
            }
        },
        FieldType::List(inner) => {
            let inner_schema = field_type_to_json_schema(inner, all_type_names, false);
            if nullable {
                format!(
                    r#"{{"type": ["array", "null"], "items": {}}}"#,
                    inner_schema
                )
            } else {
                format!(r#"{{"type": "array", "items": {}}}"#, inner_schema)
            }
        }
        FieldType::Map(_key, value) => {
            let value_schema = field_type_to_json_schema(value, all_type_names, false);
            if nullable {
                format!(
                    r#"{{"type": ["object", "null"], "additionalProperties": {}}}"#,
                    value_schema
                )
            } else {
                format!(
                    r#"{{"type": "object", "additionalProperties": {}}}"#,
                    value_schema
                )
            }
        }
        FieldType::Named(type_name) => {
            // Check if this is a known type (enum or struct) - if so, use $ref to $defs
            if all_type_names.iter().any(|name| name == type_name) {
                if nullable {
                    // For nullable refs, use oneOf since $ref can't combine with type
                    format!(
                        r##"{{"oneOf": [{{"$ref": "#/$defs/{}"}}, {{"type": "null"}}]}}"##,
                        type_name
                    )
                } else {
                    format!(r##"{{"$ref": "#/$defs/{}"}}"##, type_name)
                }
            } else {
                // Unknown type - just mark as object with description
                if nullable {
                    format!(
                        r#"{{"type": ["object", "null"], "description": "{} object"}}"#,
                        type_name
                    )
                } else {
                    format!(
                        r#"{{"type": "object", "description": "{} object"}}"#,
                        type_name
                    )
                }
            }
        }
    };
    base_schema
}

fn generate_rust_code(
    definitions: &[TypeDef],
    _source_path: &str,
    config: &OSchemaConfig,
    oschema_source: &str,
) -> Result<TokenStream2, String> {
    // Definitions are already fully parsed with includes resolved

    // Collect all enum definitions for schema generation
    let enum_defs: Vec<(String, Vec<String>)> = definitions
        .iter()
        .filter_map(|def| match def {
            TypeDef::StringEnum { name, variants, .. } => Some((name.clone(), variants.clone())),
            _ => None,
        })
        .collect();

    // Collect all struct definitions for schema generation
    let struct_defs: Vec<(String, Vec<Field>)> = definitions
        .iter()
        .filter_map(|def| match def {
            TypeDef::Struct { name, fields, .. } => Some((name.clone(), fields.clone())),
            _ => None,
        })
        .collect();

    // Combine into all_types for $ref resolution (skip services as they're not types)
    let all_type_names: Vec<String> = definitions
        .iter()
        .filter_map(|def| match def {
            TypeDef::StringEnum { name, .. } => Some(name.clone()),
            TypeDef::Struct { name, .. } => Some(name.clone()),
            TypeDef::Service { .. } => None,
        })
        .collect();

    // Collect service definitions and link them to their object types
    let service_defs: Vec<&TypeDef> = definitions
        .iter()
        .filter(|def| matches!(def, TypeDef::Service { .. }))
        .collect();

    let mut tokens = TokenStream2::new();

    for def in definitions {
        match def {
            TypeDef::StringEnum {
                name,
                variants,
                comments,
            } => {
                tokens.extend(generate_string_enum(name, variants, comments));
            }
            TypeDef::Struct {
                name,
                fields,
                comments,
            } => {
                // Check if there's a matching service for this struct
                let matching_service = service_defs.iter().find(|s| {
                    if let TypeDef::Service { name: svc_name, .. } = s {
                        svc_name == name
                    } else {
                        false
                    }
                });

                tokens.extend(generate_struct(
                    name,
                    fields,
                    comments,
                    config,
                    &enum_defs,
                    &struct_defs,
                    &all_type_names,
                    oschema_source,
                ));

                // Generate service trait and handler if matching service exists
                if let Some(TypeDef::Service {
                    name: svc_name,
                    version,
                    description,
                    methods,
                    comments: svc_comments,
                }) = matching_service
                {
                    tokens.extend(generate_service_trait(
                        svc_name,
                        version.as_deref(),
                        description.as_deref(),
                        methods,
                        svc_comments,
                    ));
                }
            }
            TypeDef::Service { .. } => {
                // Services are handled when processing their matching struct
            }
        }
    }

    Ok(tokens)
}

fn generate_string_enum(name: &str, variants: &[String], comments: &[String]) -> TokenStream2 {
    let name_ident = syn::Ident::new(name, proc_macro2::Span::call_site());

    let doc_comments: Vec<TokenStream2> = comments
        .iter()
        .map(|c| {
            quote! { #[doc = #c] }
        })
        .collect();

    let variant_tokens: Vec<TokenStream2> = variants
        .iter()
        .map(|v| {
            let variant_name = to_pascal_case(v);
            let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
            quote! {
                #[serde(rename = #v)]
                #variant_ident
            }
        })
        .collect();

    // Generate Default impl using first variant
    let default_impl = if !variants.is_empty() {
        let first_variant = to_pascal_case(&variants[0]);
        let first_ident = syn::Ident::new(&first_variant, proc_macro2::Span::call_site());
        Some(quote! {
            impl Default for #name_ident {
                fn default() -> Self {
                    Self::#first_ident
                }
            }
        })
    } else {
        None
    };

    quote! {
        #(#doc_comments)*
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
        pub enum #name_ident {
            #(#variant_tokens),*
        }

        #default_impl
    }
}

fn generate_struct(
    name: &str,
    fields: &[Field],
    comments: &[String],
    config: &OSchemaConfig,
    enum_defs: &[(String, Vec<String>)],
    struct_defs: &[(String, Vec<Field>)],
    all_type_names: &[String],
    oschema_source: &str,
) -> TokenStream2 {
    // Validate: enum, list, and map fields must not be optional
    for field in fields {
        if field.optional {
            match &field.ty {
                FieldType::Named(type_name) => {
                    if enum_defs.iter().any(|(name, _)| name == type_name) {
                        let error_msg = format!(
                            "Enum field '{}' in struct '{}' cannot be optional. Enums always have a default value (the first variant).",
                            field.name, name
                        );
                        return syn::Error::new(proc_macro2::Span::call_site(), error_msg)
                            .to_compile_error();
                    }
                }
                FieldType::List(_) => {
                    let error_msg = format!(
                        "List field '{}' in struct '{}' cannot be optional. Lists always default to empty Vec.",
                        field.name, name
                    );
                    return syn::Error::new(proc_macro2::Span::call_site(), error_msg)
                        .to_compile_error();
                }
                FieldType::Map(_, _) => {
                    let error_msg = format!(
                        "Map field '{}' in struct '{}' cannot be optional. Maps always default to empty HashMap.",
                        field.name, name
                    );
                    return syn::Error::new(proc_macro2::Span::call_site(), error_msg)
                        .to_compile_error();
                }
                _ => {}
            }
        }
    }

    let name_ident = syn::Ident::new(name, proc_macro2::Span::call_site());

    let doc_comments: Vec<TokenStream2> = comments
        .iter()
        .map(|c| {
            quote! { #[doc = #c] }
        })
        .collect();

    let field_tokens: Vec<TokenStream2> = fields
        .iter()
        .map(|f| {
            let field_name = to_snake_case(&f.name);
            let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());
            let field_type = field_type_to_tokens(&f.ty);

            let field_doc = f.comment.as_ref().map(|c| {
                quote! { #[doc = #c] }
            });

            let serde_skip = if f.optional {
                Some(quote! { #[serde(skip_serializing_if = "Option::is_none")] })
            } else {
                None
            };

            let final_type = if f.optional {
                quote! { Option<#field_type> }
            } else {
                field_type
            };

            quote! {
                #field_doc
                #serde_skip
                pub #field_ident: #final_type
            }
        })
        .collect();

    // Check if struct has sid and name fields (OSIS-compatible)
    let has_sid = fields.iter().any(|f| f.name == "sid");
    let has_name = fields.iter().any(|f| f.name == "name");
    let sid_is_optional = fields.iter().any(|f| f.name == "sid" && f.optional);
    let name_is_optional = fields.iter().any(|f| f.name == "name" && f.optional);

    // Generate builder methods (with_fieldname) for each field
    let builder_methods: Vec<TokenStream2> = fields
        .iter()
        .map(|f| {
            let field_name = to_snake_case(&f.name);
            let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());
            let method_name = format!("with_{}", field_name);
            let method_ident = syn::Ident::new(&method_name, proc_macro2::Span::call_site());
            let field_type = field_type_to_tokens(&f.ty);

            let doc = format!("Set the {} field.", field_name);

            if f.optional {
                // For optional fields, wrap in Some()
                match &f.ty {
                    FieldType::Primitive(p) if p == "String" => {
                        quote! {
                            #[doc = #doc]
                            pub fn #method_ident(mut self, value: &str) -> Self {
                                self.#field_ident = Some(value.to_string());
                                self
                            }
                        }
                    }
                    FieldType::List(inner) => {
                        let inner_type = field_type_to_tokens(inner);
                        quote! {
                            #[doc = #doc]
                            pub fn #method_ident(mut self, value: Vec<#inner_type>) -> Self {
                                self.#field_ident = Some(value);
                                self
                            }
                        }
                    }
                    _ => {
                        quote! {
                            #[doc = #doc]
                            pub fn #method_ident(mut self, value: #field_type) -> Self {
                                self.#field_ident = Some(value);
                                self
                            }
                        }
                    }
                }
            } else {
                // For required fields
                match &f.ty {
                    FieldType::Primitive(p) if p == "String" => {
                        quote! {
                            #[doc = #doc]
                            pub fn #method_ident(mut self, value: &str) -> Self {
                                self.#field_ident = value.to_string();
                                self
                            }
                        }
                    }
                    FieldType::List(_) => {
                        // For Vec fields, accept Vec<&str> and convert
                        if matches!(&f.ty, FieldType::List(inner) if matches!(inner.as_ref(), FieldType::Primitive(p) if p == "String")) {
                            quote! {
                                #[doc = #doc]
                                pub fn #method_ident(mut self, value: Vec<&str>) -> Self {
                                    self.#field_ident = value.into_iter().map(String::from).collect();
                                    self
                                }
                            }
                        } else {
                            quote! {
                                #[doc = #doc]
                                pub fn #method_ident(mut self, value: #field_type) -> Self {
                                    self.#field_ident = value;
                                    self
                                }
                            }
                        }
                    }
                    _ => {
                        quote! {
                            #[doc = #doc]
                            pub fn #method_ident(mut self, value: #field_type) -> Self {
                                self.#field_ident = value;
                                self
                            }
                        }
                    }
                }
            }
        })
        .collect();

    // Generate Default impl for OSIS-compatible structs
    let default_impl = if has_sid {
        let default_fields: Vec<TokenStream2> = fields
            .iter()
            .map(|f| {
                let field_name = to_snake_case(&f.name);
                let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());

                let default_value = if let Some(ref default) = f.default_value {
                    // Use the provided default value
                    match default {
                        DefaultValue::String(s) => {
                            // Check if this is an enum type
                            if let FieldType::Named(type_name) = &f.ty {
                                // Check if this named type is an enum
                                if enum_defs.iter().any(|(name, _)| name == type_name) {
                                    // Convert string to enum variant
                                    let variant_name = to_pascal_case(s);
                                    let type_ident =
                                        syn::Ident::new(type_name, proc_macro2::Span::call_site());
                                    let variant_ident = syn::Ident::new(
                                        &variant_name,
                                        proc_macro2::Span::call_site(),
                                    );
                                    if f.optional {
                                        quote! { Some(#type_ident::#variant_ident) }
                                    } else {
                                        quote! { #type_ident::#variant_ident }
                                    }
                                } else {
                                    // Regular string default
                                    if f.optional {
                                        quote! { Some(#s.to_string()) }
                                    } else {
                                        quote! { #s.to_string() }
                                    }
                                }
                            } else {
                                // Regular string default
                                if f.optional {
                                    quote! { Some(#s.to_string()) }
                                } else {
                                    quote! { #s.to_string() }
                                }
                            }
                        }
                        DefaultValue::Integer(i) => {
                            // Cast integer to the correct type
                            let value = match &f.ty {
                                FieldType::Primitive(p) => match p.as_str() {
                                    "u8" => quote! { #i as u8 },
                                    "u16" => quote! { #i as u16 },
                                    "u32" => quote! { #i as u32 },
                                    "u64" => quote! { #i as u64 },
                                    "i8" => quote! { #i as i8 },
                                    "i16" => quote! { #i as i16 },
                                    "i32" => quote! { #i as i32 },
                                    "i64" => quote! { #i },
                                    _ => quote! { #i },
                                },
                                _ => quote! { #i },
                            };
                            if f.optional {
                                quote! { Some(#value) }
                            } else {
                                value
                            }
                        }
                        DefaultValue::Float(fl) => {
                            // Cast float to the correct type
                            let value = match &f.ty {
                                FieldType::Primitive(p) => match p.as_str() {
                                    "f32" => quote! { #fl as f32 },
                                    "f64" => quote! { #fl },
                                    _ => quote! { #fl },
                                },
                                _ => quote! { #fl },
                            };
                            if f.optional {
                                quote! { Some(#value) }
                            } else {
                                value
                            }
                        }
                        DefaultValue::Boolean(b) => {
                            if f.optional {
                                quote! { Some(#b) }
                            } else {
                                quote! { #b }
                            }
                        }
                        DefaultValue::List(elements) => {
                            // Generate vec![] with elements
                            let element_tokens: Vec<proc_macro2::TokenStream> = elements
                                .iter()
                                .map(|elem| {
                                    match elem {
                                        DefaultValue::String(s) => quote! { #s.to_string() },
                                        DefaultValue::Integer(i) => {
                                            // Cast based on list element type
                                            if let FieldType::List(inner) = &f.ty {
                                                if let FieldType::Primitive(p) = inner.as_ref() {
                                                    match p.as_str() {
                                                        "u8" => quote! { #i as u8 },
                                                        "u16" => quote! { #i as u16 },
                                                        "u32" => quote! { #i as u32 },
                                                        "u64" => quote! { #i as u64 },
                                                        "i8" => quote! { #i as i8 },
                                                        "i16" => quote! { #i as i16 },
                                                        "i32" => quote! { #i as i32 },
                                                        "i64" => quote! { #i },
                                                        _ => quote! { #i },
                                                    }
                                                } else {
                                                    quote! { #i }
                                                }
                                            } else {
                                                quote! { #i }
                                            }
                                        }
                                        DefaultValue::Float(fl) => {
                                            if let FieldType::List(inner) = &f.ty {
                                                if let FieldType::Primitive(p) = inner.as_ref() {
                                                    match p.as_str() {
                                                        "f32" => quote! { #fl as f32 },
                                                        "f64" => quote! { #fl },
                                                        _ => quote! { #fl },
                                                    }
                                                } else {
                                                    quote! { #fl }
                                                }
                                            } else {
                                                quote! { #fl }
                                            }
                                        }
                                        DefaultValue::Boolean(b) => quote! { #b },
                                        DefaultValue::List(_) => {
                                            // Nested lists not supported yet
                                            quote! { vec![] }
                                        }
                                    }
                                })
                                .collect();

                            if f.optional {
                                quote! { Some(vec![#(#element_tokens),*]) }
                            } else {
                                quote! { vec![#(#element_tokens),*] }
                            }
                        }
                    }
                } else if f.optional {
                    quote! { None }
                } else {
                    match &f.ty {
                        FieldType::Primitive(p) => match p.as_str() {
                            "String" => quote! { String::new() },
                            "bool" => quote! { false },
                            "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" => {
                                quote! { 0 }
                            }
                            "f32" | "f64" => quote! { 0.0 },
                            "OTime" => quote! { OTime::now() },
                            _ => quote! { Default::default() },
                        },
                        FieldType::List(_) => quote! { Vec::new() },
                        FieldType::Map(_, _) => quote! { std::collections::HashMap::new() },
                        FieldType::Named(type_name) => {
                            // Check if this is an enum type
                            if let Some((_, variants)) =
                                enum_defs.iter().find(|(name, _)| name == type_name)
                            {
                                // Use first variant as default
                                if let Some(first_variant) = variants.first() {
                                    let variant_name = to_pascal_case(first_variant);
                                    let type_ident =
                                        syn::Ident::new(type_name, proc_macro2::Span::call_site());
                                    let variant_ident = syn::Ident::new(
                                        &variant_name,
                                        proc_macro2::Span::call_site(),
                                    );
                                    quote! { #type_ident::#variant_ident }
                                } else {
                                    quote! { Default::default() }
                                }
                            } else {
                                quote! { Default::default() }
                            }
                        }
                    }
                };

                quote! { #field_ident: #default_value }
            })
            .collect();

        Some(quote! {
            impl Default for #name_ident {
                fn default() -> Self {
                    Self {
                        #(#default_fields),*
                    }
                }
            }
        })
    } else {
        None
    };

    let struct_tokens = quote! {
        #(#doc_comments)*
        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
        pub struct #name_ident {
            #(#field_tokens),*
        }

        #default_impl

        impl #name_ident {
            #(#builder_methods)*
        }
    };

    // Generate OSIS wrapper if struct has sid field
    if has_sid {
        // Collect indexed fields for search functionality
        let indexed_fields: Vec<&Field> = fields.iter().filter(|f| f.indexed).collect();
        let osis_tokens = generate_osis_struct(
            name,
            has_name,
            name_is_optional,
            sid_is_optional,
            fields,
            &indexed_fields,
            config,
            enum_defs,
            struct_defs,
            all_type_names,
            oschema_source,
        );
        quote! {
            #struct_tokens
            #osis_tokens
        }
    } else {
        struct_tokens
    }
}

/// Generate the {Name}OSIS struct with CRUD methods.
fn generate_osis_struct(
    name: &str,
    has_name: bool,
    name_is_optional: bool,
    sid_is_optional: bool,
    all_fields: &[Field],
    indexed_fields: &[&Field],
    _config: &OSchemaConfig,
    enum_defs: &[(String, Vec<String>)],
    struct_defs: &[(String, Vec<Field>)],
    all_type_names: &[String],
    oschema_source: &str,
) -> TokenStream2 {
    let name_ident = syn::Ident::new(name, proc_macro2::Span::call_site());
    let osis_name = format!("{}OSIS", name);
    let osis_ident = syn::Ident::new(&osis_name, proc_macro2::Span::call_site());

    // Generate JSON schema at compile time, including all type definitions
    let enum_refs: Vec<(&str, &[String])> = enum_defs
        .iter()
        .map(|(n, v)| (n.as_str(), v.as_slice()))
        .collect();
    let struct_refs: Vec<(&str, &[Field])> = struct_defs
        .iter()
        .map(|(n, f)| (n.as_str(), f.as_slice()))
        .collect();
    let json_schema =
        generate_json_schema_string(name, all_fields, &enum_refs, &struct_refs, all_type_names);

    let doc_comment = format!("OSIS storage wrapper for {}.", name);
    let new_doc = format!("Create a new {} with a generated SID.", name);
    let set_doc = format!("Store a {} to the filesystem as OTOML.", name);
    let get_doc = format!("Load a {} from the filesystem by SID.", name);
    let delete_doc = format!("Delete a {} from the filesystem by SID.", name);
    let exists_doc = format!("Check if a {} exists by SID.", name);
    let list_doc = "List all SIDs, optionally filtered by name prefix.";

    // Generate name() helper based on whether struct has name field and if it's optional
    let get_name_impl = if has_name {
        if name_is_optional {
            // name: Option<String> - use as_deref()
            quote! {
                fn get_name(obj: &#name_ident) -> Option<&str> {
                    obj.name.as_deref()
                }
            }
        } else {
            // name: String - return Some(&name)
            quote! {
                fn get_name(obj: &#name_ident) -> Option<&str> {
                    Some(&obj.name)
                }
            }
        }
    } else {
        quote! {
            fn get_name(_obj: &#name_ident) -> Option<&str> {
                None
            }
        }
    };

    // Generate index-related code if there are indexed fields
    let has_index = !indexed_fields.is_empty();

    // Generate field names for indexing (must be before index_init)
    let indexed_field_names: Vec<String> = indexed_fields
        .iter()
        .map(|f| to_snake_case(&f.name))
        .collect();

    let index_struct_field = if has_index {
        quote! {
            index: Option<std::sync::Arc<herolib_osis::index::OsisIndex>>,
        }
    } else {
        quote! {}
    };

    let index_init = if has_index {
        quote! {
            let index_path = format!("{}/{}", path_index, type_name);
            let field_names: &[&str] = &[#(#indexed_field_names),*];
            let index = herolib_osis::index::OsisIndex::open_or_create(&index_path, field_names)
                .map_err(|e| herolib_osis::db::DbError::Index(e.to_string()))?;
            let index = Some(std::sync::Arc::new(index));
        }
    } else {
        quote! {}
    };

    let index_field_init = if has_index {
        quote! { index, }
    } else {
        quote! {}
    };

    // Generate index update in set() - note: index uses the sid from the object
    let index_on_set = if has_index {
        let field_extracts: Vec<TokenStream2> = indexed_fields
            .iter()
            .map(|f| {
                let field_name = to_snake_case(&f.name);
                let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());
                if f.optional {
                    quote! {
                        (#field_name.to_string(), obj.#field_ident.clone().unwrap_or_default())
                    }
                } else {
                    quote! {
                        (#field_name.to_string(), obj.#field_ident.clone())
                    }
                }
            })
            .collect();

        // Use sid_str converted from sid
        quote! {
            // Update search index
            if let Some(ref index) = self.index {
                let sid_str = sid.as_str();
                let fields: Vec<(String, String)> = vec![
                    #(#field_extracts),*
                ];
                let _ = index.index_document(&sid_str, &fields);
            }
        }
    } else {
        quote! {}
    };

    // Generate index removal in delete()
    let index_on_delete = if has_index {
        quote! {
            // Remove from search index
            if let Some(ref index) = self.index {
                let sid_str = sid.as_str();
                let _ = index.delete_document(&sid_str);
            }
        }
    } else {
        quote! {}
    };

    // Generate find() method if indexed
    let find_method = if has_index {
        quote! {
            /// Search for objects matching the query.
            /// Returns a list of matching SIDs.
            pub fn find(&self, query: &str) -> Result<Vec<String>, String> {
                match &self.index {
                    Some(index) => index.search(query).map_err(|e| e.to_string()),
                    None => Err("Index not available".to_string()),
                }
            }
        }
    } else {
        quote! {}
    };

    // Generate rebuild_index() method if indexed
    // Note: We use &sid from the loop instead of obj.sid to avoid optional field issues
    let rebuild_index_method = if has_index {
        let field_extracts: Vec<TokenStream2> = indexed_fields
            .iter()
            .map(|f| {
                let field_name = to_snake_case(&f.name);
                let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());
                if f.optional {
                    quote! {
                        (#field_name.to_string(), obj.#field_ident.clone().unwrap_or_default())
                    }
                } else {
                    quote! {
                        (#field_name.to_string(), obj.#field_ident.clone())
                    }
                }
            })
            .collect();

        quote! {
            /// Rebuild the search index from all stored objects.
            pub fn rebuild_index(&self) -> Result<(), String> {
                let index = match &self.index {
                    Some(i) => i,
                    None => return Err("Index not available".to_string()),
                };

                // Clear existing index
                index.clear().map_err(|e| e.to_string())?;

                // Re-index all objects
                let sids = self.list(None)?;
                for sid in sids {
                    if let Ok(obj) = self.get(&sid) {
                        let fields: Vec<(String, String)> = vec![
                            #(#field_extracts),*
                        ];
                        // Use sid from list() instead of obj.sid to handle optional sid field
                        index.index_document(&sid, &fields).map_err(|e| e.to_string())?;
                    }
                }

                Ok(())
            }

            /// Get the list of indexed field names.
            pub fn indexed_fields(&self) -> Vec<&'static str> {
                vec![#(#indexed_field_names),*]
            }
        }
    } else {
        quote! {}
    };

    // Generate create() method based on whether sid is optional
    let create_method = if sid_is_optional {
        quote! {
            #[doc = #new_doc]
            pub fn create(&self) -> Result<#name_ident, String> {
                let sid = self.mint_sid(0)?;
                let mut obj = #name_ident::default();
                obj.sid = Some(sid);
                Ok(obj)
            }
        }
    } else {
        quote! {
            #[doc = #new_doc]
            pub fn create(&self) -> Result<#name_ident, String> {
                let sid = self.mint_sid(0)?;
                let mut obj = #name_ident::default();
                obj.sid = sid;
                Ok(obj)
            }
        }
    };

    // Generate ensure_sid() helper - extracts or generates SID, returns the string value
    let ensure_sid_method = if sid_is_optional {
        quote! {
            /// Ensure the object has a valid SID. If empty or None, generates a new one.
            /// Returns the SID string value and updates the object if needed.
            fn ensure_sid(&self, obj: &mut #name_ident) -> Result<String, String> {
                let needs_sid = match &obj.sid {
                    None => true,
                    Some(s) if s.is_empty() || s == "0" => true,
                    _ => false,
                };

                if needs_sid {
                    let new_sid = self.mint_sid(0)?;
                    obj.sid = Some(new_sid.clone());
                    Ok(new_sid)
                } else {
                    Ok(obj.sid.clone().unwrap())
                }
            }
        }
    } else {
        quote! {
            /// Ensure the object has a valid SID. If empty or "0", generates a new one.
            /// Returns the SID string value and updates the object if needed.
            fn ensure_sid(&self, obj: &mut #name_ident) -> Result<String, String> {
                if obj.sid.is_empty() || obj.sid == "0" {
                    let new_sid = self.mint_sid(0)?;
                    obj.sid = new_sid.clone();
                    Ok(new_sid)
                } else {
                    Ok(obj.sid.clone())
                }
            }
        }
    };

    // Generate set() method - stores object to filesystem
    let set_method = quote! {
        #[doc = #set_doc]
        pub fn set(&self, obj: &mut #name_ident) -> Result<String, String> {
            let sid_str = self.ensure_sid(obj)?;
            let sid = self.parse_sid(&sid_str)?;
            let name = Self::get_name(obj);
            let path = self.get_path(&sid, name);

            // Clean up old file if name changed
            if let Some(existing_path) = self.find_path_by_sid(&sid)? {
                if existing_path != path {
                    std::fs::remove_file(&existing_path).ok();
                }
            }

            let content = herolib_osis::dump_otoml(obj)
                .map_err(|e| e.to_string())?;
            std::fs::write(&path, content).map_err(|e| e.to_string())?;

            #index_on_set

            Ok(sid_str)
        }
    };

    quote! {
        #[doc = #doc_comment]
        #[derive(Clone)]
        pub struct #osis_ident {
            storage: std::sync::Arc<herolib_osis::db::FsStorage>,
            #index_struct_field
        }

        impl #osis_ident {
            /// Create a new OSIS storage instance.
            ///
            /// # Arguments
            /// * `path_db` - Root directory for data storage (supports `~` for home).
            ///               Objects are stored at: `{path_db}/{type}/{name}_{sid}.otoml`
            /// * `path_index` - Root directory for search indexes.
            ///                  Indexes are stored at: `{path_index}/{type}/.index`
            /// * `user_id` - Contributor ID for SID generation (0-998)
            pub fn new(path_db: &str, path_index: &str, user_id: u16) -> Result<Self, herolib_osis::db::DbError> {
                let type_name = stringify!(#name_ident).to_lowercase();
                let storage = std::sync::Arc::new(herolib_osis::db::FsStorage::new(path_db, &type_name, user_id)?);
                #index_init
                Ok(Self { storage, #index_field_init })
            }

            #create_method

            #ensure_sid_method

            #set_method

            #[doc = #get_doc]
            pub fn get(&self, sid: &str) -> Result<#name_ident, String> {
                let sid = self.parse_sid(sid)?;
                let path = self.find_path_by_sid(&sid)?
                    .ok_or_else(|| format!("{} not found: {}", stringify!(#name_ident), sid.as_str()))?;
                let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
                herolib_osis::load_otoml(&content).map_err(|e| e.to_string())
            }

            #[doc = #delete_doc]
            pub fn delete(&self, sid: &str) -> Result<(), String> {
                let sid = self.parse_sid(sid)?;
                #index_on_delete
                if let Some(path) = self.find_path_by_sid(&sid)? {
                    std::fs::remove_file(&path).map_err(|e| e.to_string())?;
                }
                Ok(())
            }

            #[doc = #exists_doc]
            pub fn exists(&self, sid: &str) -> Result<bool, String> {
                let sid = self.parse_sid(sid)?;
                Ok(self.find_path_by_sid(&sid)?.is_some())
            }

            #[doc = #list_doc]
            pub fn list(&self, name_filter: Option<&str>) -> Result<Vec<String>, String> {
                self.storage.list(name_filter)
                    .map(|sids| sids.into_iter().map(|s| s.as_str()).collect())
                    .map_err(|e| e.to_string())
            }

            /// Get the storage directory path.
            pub fn storage_path(&self) -> String {
                format!("{}/{}", self.storage.path_db(), self.storage.obj_type())
            }

            /// Mint a new SID for the given contributor ID.
            /// Use contributor 0 as the default user.
            pub fn mint_sid(&self, contributor_id: u16) -> Result<String, String> {
                // We need interior mutability here since mint modifies the space
                // The storage uses RefCell internally for this
                self.storage.mint_sid(contributor_id)
                    .map(|sid| sid.as_str())
                    .map_err(|e| e.to_string())
            }

            #find_method

            #rebuild_index_method

            // Private helper methods

            fn parse_sid(&self, sid: &str) -> Result<herolib_osis::sid::SmartId, String> {
                herolib_osis::sid::SmartId::parse(sid)
                    .map_err(|e| format!("Invalid SID '{}': {}", sid, e))
            }

            #get_name_impl

            fn get_path(&self, sid: &herolib_osis::sid::SmartId, name: Option<&str>) -> String {
                let dir = self.storage_path();
                match name {
                    Some(n) => {
                        let fixed_name = herolib_core::text::name_fix(n);
                        format!("{}/{}_{}.otoml", dir, fixed_name, sid.as_str())
                    }
                    None => format!("{}/{}.otoml", dir, sid.as_str()),
                }
            }

            fn find_path_by_sid(&self, sid: &herolib_osis::sid::SmartId) -> Result<Option<String>, String> {
                let dir = self.storage_path();
                let dir_path = std::path::Path::new(&dir);

                if !dir_path.exists() {
                    return Ok(None);
                }

                let sid_str = sid.as_str();

                // Check unnamed file
                let unnamed_path = format!("{}/{}.otoml", dir, sid_str);
                if std::path::Path::new(&unnamed_path).exists() {
                    return Ok(Some(unnamed_path));
                }

                // Search named files
                let suffix = format!("_{}.otoml", sid_str);
                for entry in std::fs::read_dir(dir_path).map_err(|e| e.to_string())? {
                    let entry = entry.map_err(|e| e.to_string())?;
                    let file_name = entry.file_name();
                    let name_str = file_name.to_string_lossy();

                    if name_str.ends_with(&suffix) {
                        return Ok(Some(entry.path().to_string_lossy().to_string()));
                    }
                }

                Ok(None)
            }
        }

        // Auto-implement OsisRpcHandler trait for RPC support
        impl herolib_osis::rpc::OsisRpcHandler for #osis_ident {
            fn type_name(&self) -> &str {
                stringify!(#name_ident).to_lowercase().leak()
            }

            fn handle_new(&mut self) -> herolib_osis::rpc::Result<String> {
                let obj = self.create()
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))?;
                herolib_osis::dump_otoml(&obj)
                    .map_err(|e| herolib_osis::rpc::RpcError::Serialization(e.to_string()))
            }

            fn handle_get(&self, sid: &str) -> herolib_osis::rpc::Result<String> {
                let obj = self.get(sid)
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))?;
                herolib_osis::dump_otoml(&obj)
                    .map_err(|e| herolib_osis::rpc::RpcError::Serialization(e.to_string()))
            }

            fn handle_set(&self, otoml: &str) -> herolib_osis::rpc::Result<String> {
                let mut obj: #name_ident = herolib_osis::load_otoml(otoml)
                    .map_err(|e| herolib_osis::rpc::RpcError::Deserialization(e.to_string()))?;
                // set() now handles SID generation if needed and returns the SID
                let sid = self.set(&mut obj)
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))?;
                Ok(sid)
            }

            fn handle_delete(&self, sid: &str) -> herolib_osis::rpc::Result<()> {
                self.delete(sid)
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))
            }

            fn handle_list(&self) -> herolib_osis::rpc::Result<String> {
                let sids = self.list(None)
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))?;
                Ok(sids.join("\n"))
            }

            fn handle_find(&self, query: &str) -> herolib_osis::rpc::Result<String> {
                let sids = self.find(query)
                    .map_err(|e| herolib_osis::rpc::RpcError::Storage(e))?;
                Ok(sids.join("\n"))
            }

            fn custom_method_names(&self) -> &[&str] {
                // Delegate to OsisCustomRpcHooks trait
                <Self as herolib_osis::rpc::OsisCustomRpcHooks>::custom_method_names_hook(self)
            }

            fn handle_custom(&self, method: &str, data: &str) -> herolib_osis::rpc::Result<String> {
                // Delegate to OsisCustomRpcHooks trait
                <Self as herolib_osis::rpc::OsisCustomRpcHooks>::handle_custom_hook(self, method, data)
            }

            fn json_schema(&self) -> Option<String> {
                Some(#json_schema.to_string())
            }

            fn oschema_source(&self) -> Option<&'static str> {
                Some(#oschema_source)
            }
        }

        // Note: OsisCustomRpcHooks is NOT auto-implemented here.
        // The trait has default methods, so callers must either:
        // 1. Use osis_rpc_impl!(TypeOSIS {}) to get an empty default implementation
        // 2. Use osis_rpc_impl! with #[osis_rpc] methods for custom RPC methods
        //
        // This avoids conflicts when osis_rpc_impl! is used alongside include_oschema!

        // =========================================================================
        // Rhai integration - auto-generated namespaced functions
        // =========================================================================
        //
        // This generates a registration function that can be called to register
        // namespaced OSIS functions for this type with a Rhai engine.
        //
        // Usage:
        //   TaskOSIS::register_rhai_local(&mut engine, "myproject", "/tmp/data", 1);
        //
        // This registers functions like:
        //   osis_myproject_task_new()   -> Map
        //   osis_myproject_task_get(sid) -> Map
        //   osis_myproject_task_set(obj)
        //   osis_myproject_task_delete(sid)
        //   osis_myproject_task_list() -> Array
        //   osis_myproject_task_find(query) -> Array
        //   osis_myproject_task_exists(sid) -> bool
        //
        // For RPC mode:
        //   TaskOSIS::register_rhai_http(&mut engine, "myproject", "http://localhost:8080/osis");
        //   TaskOSIS::register_rhai_socket(&mut engine, "myproject", "/tmp/osis.sock");
        //   TaskOSIS::register_rhai_redis(&mut engine, "myproject", "redis://localhost:6379", "ctx");

        #[cfg(feature = "rhai")]
        impl #osis_ident {
            /// Register Rhai functions for local filesystem storage.
            ///
            /// This registers namespaced functions like `osis_{namespace}_{type}_new()`.
            ///
            /// # Arguments
            /// * `engine` - The Rhai engine to register functions with
            /// * `namespace` - Namespace for function names (e.g., "myproject")
            /// * `path_db` - Base path for data storage
            /// * `path_index` - Base path for index storage
            /// * `user_id` - User/contributor ID for SID generation
            pub fn register_rhai_local(
                engine: &mut rhai::Engine,
                namespace: &str,
                path_db: &str,
                path_index: &str,
                user_id: u16,
            ) -> Result<(), String> {
                use std::sync::{Arc, RwLock};
                use rhai::{Dynamic, Map, EvalAltResult};

                let type_name = stringify!(#name_ident).to_lowercase();
                let storage = Arc::new(RwLock::new(
                    #osis_ident::new(path_db, path_index, user_id)
                        .map_err(|e| e.to_string())?
                ));

                // Function name prefix: osis_{namespace}_{type}_
                let prefix = format!("osis_{}_{}", namespace, type_name);

                // osis_{ns}_{type}_new() -> Map
                let new_name = format!("{}_new", prefix);
                let s = storage.clone();
                engine.register_fn(&new_name, move || -> Result<Map, Box<EvalAltResult>> {
                    let mut storage = s.write().map_err(|e| e.to_string())?;
                    let obj = storage.create().map_err(|e| e.to_string())?;
                    let json = serde_json::to_value(&obj).map_err(|e| e.to_string())?;
                    Ok(herolib_osis::rhai::json_to_rhai_map(&json))
                });

                // osis_{ns}_{type}_get(sid) -> Map
                let get_name = format!("{}_get", prefix);
                let s = storage.clone();
                engine.register_fn(&get_name, move |sid: &str| -> Result<Map, Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    let obj = storage.get(sid).map_err(|e| e.to_string())?;
                    let json = serde_json::to_value(&obj).map_err(|e| e.to_string())?;
                    Ok(herolib_osis::rhai::json_to_rhai_map(&json))
                });

                // osis_{ns}_{type}_set(obj)
                let set_name = format!("{}_set", prefix);
                let s = storage.clone();
                engine.register_fn(&set_name, move |data: Map| -> Result<(), Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    let json = herolib_osis::rhai::rhai_map_to_json(&data);
                    let mut obj: #name_ident = serde_json::from_value(json).map_err(|e| e.to_string())?;
                    storage.set(&mut obj).map_err(|e| e.to_string().into())?;
                    Ok(())
                });

                // osis_{ns}_{type}_delete(sid)
                let delete_name = format!("{}_delete", prefix);
                let s = storage.clone();
                engine.register_fn(&delete_name, move |sid: &str| -> Result<(), Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    storage.delete(sid).map_err(|e| e.to_string().into())
                });

                // osis_{ns}_{type}_exists(sid) -> bool
                let exists_name = format!("{}_exists", prefix);
                let s = storage.clone();
                engine.register_fn(&exists_name, move |sid: &str| -> Result<bool, Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    storage.exists(sid).map_err(|e| e.to_string().into())
                });

                // osis_{ns}_{type}_list() -> Array
                let list_name = format!("{}_list", prefix);
                let s = storage.clone();
                engine.register_fn(&list_name, move || -> Result<rhai::Array, Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    let sids = storage.list(None).map_err(|e| e.to_string())?;
                    Ok(sids.into_iter().map(Dynamic::from).collect())
                });

                // osis_{ns}_{type}_find(query) -> Array
                let find_name = format!("{}_find", prefix);
                let s = storage.clone();
                engine.register_fn(&find_name, move |query: &str| -> Result<rhai::Array, Box<EvalAltResult>> {
                    let storage = s.read().map_err(|e| e.to_string())?;
                    let sids = storage.find(query).map_err(|e| e.to_string())?;
                    Ok(sids.into_iter().map(Dynamic::from).collect())
                });

                Ok(())
            }

            /// Register Rhai functions for HTTP RPC access.
            #[cfg(feature = "rpc")]
            pub fn register_rhai_http(
                engine: &mut rhai::Engine,
                namespace: &str,
                base_url: &str,
            ) -> Result<(), String> {
                use std::sync::Arc;
                use rhai::{Dynamic, Map, EvalAltResult};

                let type_name = stringify!(#name_ident).to_lowercase();
                let client = Arc::new(herolib_osis::rpc::OsisRpcClient::http(base_url, &type_name));
                let prefix = format!("osis_{}_{}", namespace, type_name);

                Self::register_rhai_rpc_client(engine, &prefix, client)
            }

            /// Register Rhai functions for Unix socket RPC access.
            #[cfg(feature = "rpc")]
            pub fn register_rhai_socket(
                engine: &mut rhai::Engine,
                namespace: &str,
                socket_path: &str,
            ) -> Result<(), String> {
                use std::sync::Arc;

                let type_name = stringify!(#name_ident).to_lowercase();
                let client = Arc::new(herolib_osis::rpc::OsisRpcClient::socket(socket_path, &type_name));
                let prefix = format!("osis_{}_{}", namespace, type_name);

                Self::register_rhai_rpc_client(engine, &prefix, client)
            }

            /// Register Rhai functions for Redis queue RPC access.
            #[cfg(feature = "rpc")]
            pub fn register_rhai_redis(
                engine: &mut rhai::Engine,
                namespace: &str,
                redis_url: &str,
                context: &str,
            ) -> Result<(), String> {
                use std::sync::Arc;

                let type_name = stringify!(#name_ident).to_lowercase();
                let client = Arc::new(herolib_osis::rpc::OsisRpcClient::redis(redis_url, context, &type_name));
                let prefix = format!("osis_{}_{}", namespace, type_name);

                Self::register_rhai_rpc_client(engine, &prefix, client)
            }

            #[cfg(feature = "rpc")]
            fn register_rhai_rpc_client(
                engine: &mut rhai::Engine,
                prefix: &str,
                client: std::sync::Arc<herolib_osis::rpc::OsisRpcClient>,
            ) -> Result<(), String> {
                use rhai::{Dynamic, Map, EvalAltResult};

                // osis_{ns}_{type}_new() -> Map
                let new_name = format!("{}_new", prefix);
                let c = client.clone();
                engine.register_fn(&new_name, move || -> Result<Map, Box<EvalAltResult>> {
                    let otoml = c.new_obj().map_err(|e| e.to_string())?;
                    let json: serde_json::Value = herolib_osis::load_otoml(&otoml).map_err(|e| e.to_string())?;
                    Ok(herolib_osis::rhai::json_to_rhai_map(&json))
                });

                // osis_{ns}_{type}_get(sid) -> Map
                let get_name = format!("{}_get", prefix);
                let c = client.clone();
                engine.register_fn(&get_name, move |sid: &str| -> Result<Map, Box<EvalAltResult>> {
                    let otoml = c.get(sid).map_err(|e| e.to_string())?;
                    let json: serde_json::Value = herolib_osis::load_otoml(&otoml).map_err(|e| e.to_string())?;
                    Ok(herolib_osis::rhai::json_to_rhai_map(&json))
                });

                // osis_{ns}_{type}_set(obj)
                let set_name = format!("{}_set", prefix);
                let c = client.clone();
                engine.register_fn(&set_name, move |data: Map| -> Result<(), Box<EvalAltResult>> {
                    let json = herolib_osis::rhai::rhai_map_to_json(&data);
                    let otoml = herolib_osis::dump_otoml(&json).map_err(|e| e.to_string())?;
                    c.set(&otoml).map_err(|e| e.to_string())?;
                    Ok(())
                });

                // osis_{ns}_{type}_delete(sid)
                let delete_name = format!("{}_delete", prefix);
                let c = client.clone();
                engine.register_fn(&delete_name, move |sid: &str| -> Result<(), Box<EvalAltResult>> {
                    c.delete(sid).map_err(|e| e.to_string().into())
                });

                // osis_{ns}_{type}_exists(sid) -> bool (via get)
                let exists_name = format!("{}_exists", prefix);
                let c = client.clone();
                engine.register_fn(&exists_name, move |sid: &str| -> bool {
                    c.get(sid).is_ok()
                });

                // osis_{ns}_{type}_list() -> Array
                let list_name = format!("{}_list", prefix);
                let c = client.clone();
                engine.register_fn(&list_name, move || -> Result<rhai::Array, Box<EvalAltResult>> {
                    let sids = c.list().map_err(|e| e.to_string())?;
                    Ok(sids.into_iter().map(Dynamic::from).collect())
                });

                // osis_{ns}_{type}_find(query) -> Array
                let find_name = format!("{}_find", prefix);
                let c = client.clone();
                engine.register_fn(&find_name, move |query: &str| -> Result<rhai::Array, Box<EvalAltResult>> {
                    let sids = c.find(query).map_err(|e| e.to_string())?;
                    Ok(sids.into_iter().map(Dynamic::from).collect())
                });

                Ok(())
            }
        }
    }
}

// ============================================================================
// Service Trait Generation
// ============================================================================

/// Generate a service trait and default implementation for custom RPC methods.
///
/// For a service like:
/// ```text
/// service User {
///     get_user(id: u64) -> User
///     create_user(name: str, email: str) -> User
/// }
/// ```
///
/// This generates:
/// 1. A trait `UserService` with method signatures
/// 2. A default empty implementation for `UserOSIS`
/// 3. Integration with the RPC handler via `OsisCustomRpcHooks`
fn generate_service_trait(
    name: &str,
    version: Option<&str>,
    description: Option<&str>,
    methods: &[ServiceMethod],
    comments: &[String],
) -> TokenStream2 {
    let trait_name = format!("{}Service", name);
    let trait_ident = syn::Ident::new(&trait_name, proc_macro2::Span::call_site());
    let osis_name = format!("{}OSIS", name);
    let osis_ident = syn::Ident::new(&osis_name, proc_macro2::Span::call_site());

    // Build doc comments for the trait
    let mut trait_docs = Vec::new();
    for comment in comments {
        trait_docs.push(quote! { #[doc = #comment] });
    }
    if let Some(desc) = description {
        trait_docs.push(quote! { #[doc = ""] });
        trait_docs.push(quote! { #[doc = #desc] });
    }
    if let Some(ver) = version {
        let ver_doc = format!("Version: {}", ver);
        trait_docs.push(quote! { #[doc = ""] });
        trait_docs.push(quote! { #[doc = #ver_doc] });
    }

    // Generate method signatures for the trait
    let method_signatures: Vec<TokenStream2> = methods
        .iter()
        .map(|method| {
            let method_name = syn::Ident::new(&method.name, proc_macro2::Span::call_site());

            // Generate method parameters
            let params: Vec<TokenStream2> = method
                .params
                .iter()
                .map(|p| {
                    let param_name = syn::Ident::new(&p.name, proc_macro2::Span::call_site());
                    let param_type = field_type_to_tokens(&p.ty);
                    if p.optional {
                        quote! { #param_name: Option<#param_type> }
                    } else {
                        quote! { #param_name: #param_type }
                    }
                })
                .collect();

            // Generate return type
            let return_type = match &method.return_type {
                Some(ty) => {
                    let ty_tokens = field_type_to_tokens(ty);
                    quote! { Result<#ty_tokens, String> }
                }
                None => quote! { Result<(), String> },
            };

            // Generate method doc comment
            let method_doc = method
                .comment
                .as_ref()
                .map(|c| quote! { #[doc = #c] })
                .unwrap_or_else(|| quote! {});

            // Generate error doc if present
            let error_docs: Vec<TokenStream2> = if !method.errors.is_empty() {
                let errors_str = format!("Errors: {}", method.errors.join(", "));
                vec![quote! { #[doc = ""] }, quote! { #[doc = #errors_str] }]
            } else {
                vec![]
            };

            quote! {
                #method_doc
                #(#error_docs)*
                fn #method_name(&self, #(#params),*) -> #return_type;
            }
        })
        .collect();

    // Generate default implementations that return "not implemented"
    let default_impls: Vec<TokenStream2> = methods
        .iter()
        .map(|method| {
            let method_name = syn::Ident::new(&method.name, proc_macro2::Span::call_site());
            let method_name_str = &method.name;

            // Generate method parameters (same as above)
            let params: Vec<TokenStream2> = method
                .params
                .iter()
                .map(|p| {
                    let param_name = syn::Ident::new(&p.name, proc_macro2::Span::call_site());
                    let param_type = field_type_to_tokens(&p.ty);
                    if p.optional {
                        quote! { #param_name: Option<#param_type> }
                    } else {
                        quote! { #param_name: #param_type }
                    }
                })
                .collect();

            // Suppress unused variable warnings
            let unused_params: Vec<TokenStream2> = method
                .params
                .iter()
                .map(|p| {
                    let param_name = syn::Ident::new(&p.name, proc_macro2::Span::call_site());
                    quote! { let _ = &#param_name; }
                })
                .collect();

            // Generate return type
            let return_type = match &method.return_type {
                Some(ty) => {
                    let ty_tokens = field_type_to_tokens(ty);
                    quote! { Result<#ty_tokens, String> }
                }
                None => quote! { Result<(), String> },
            };

            quote! {
                fn #method_name(&self, #(#params),*) -> #return_type {
                    #(#unused_params)*
                    Err(format!("Method '{}' not implemented", #method_name_str))
                }
            }
        })
        .collect();

    // Generate method names for custom_method_names (must be 'static)
    let method_names: Vec<String> = methods.iter().map(|m| m.name.clone()).collect();

    // Generate match arms for handle_custom dispatch
    let dispatch_arms: Vec<TokenStream2> = methods
        .iter()
        .map(|method| {
            let method_name_str = &method.name;
            let method_ident = syn::Ident::new(&method.name, proc_macro2::Span::call_site());

            // For now, we parse params from JSON and call the method
            // Generate parameter extraction from JSON
            let param_extractions: Vec<TokenStream2> = method
                .params
                .iter()
                .map(|p| {
                    let param_name = syn::Ident::new(&p.name, proc_macro2::Span::call_site());
                    let param_name_str = &p.name;
                    let param_type = field_type_to_tokens(&p.ty);

                    if p.optional {
                        quote! {
                            let #param_name: Option<#param_type> = params.get(#param_name_str)
                                .and_then(|v| serde_json::from_value(v.clone()).ok());
                        }
                    } else {
                        quote! {
                            let #param_name: #param_type = {
                                let val = params.get(#param_name_str)
                                    .ok_or_else(|| format!("Missing required parameter: {}", #param_name_str))?
                                    .clone();
                                serde_json::from_value(val)
                                    .map_err(|e| format!("Invalid parameter '{}': {}", #param_name_str, e))?
                            };
                        }
                    }
                })
                .collect();

            let param_names: Vec<syn::Ident> = method
                .params
                .iter()
                .map(|p| syn::Ident::new(&p.name, proc_macro2::Span::call_site()))
                .collect();

            // Generate result handling based on return type
            let result_handling = if method.return_type.is_some() {
                quote! {
                    let result = <Self as #trait_ident>::#method_ident(self, #(#param_names),*)?;
                    serde_json::to_string(&result)
                        .map_err(|e| format!("Failed to serialize result: {}", e))
                }
            } else {
                quote! {
                    <Self as #trait_ident>::#method_ident(self, #(#param_names),*)?;
                    Ok("null".to_string())
                }
            };

            // Wrap the match arm in a closure to isolate the ? operator
            quote! {
                #method_name_str => {
                    (|| -> Result<String, String> {
                        #(#param_extractions)*
                        #result_handling
                    })()
                }
            }
        })
        .collect();

    quote! {
        // =========================================================================
        // Service Trait
        // =========================================================================

        #(#trait_docs)*
        pub trait #trait_ident {
            #(#method_signatures)*
        }

        // Default implementation with "not implemented" stubs
        impl #trait_ident for #osis_ident {
            #(#default_impls)*
        }

        // Integration with OsisCustomRpcHooks for RPC dispatch
        impl herolib_osis::rpc::OsisCustomRpcHooks for #osis_ident {
            fn custom_method_names_hook(&self) -> &'static [&'static str] {
                &[#(#method_names),*]
            }

            fn handle_custom_hook(&self, method: &str, data: &str) -> herolib_osis::rpc::Result<String> {
                use herolib_osis::rpc::RpcError;

                // Parse input data as JSON object with params
                let params: serde_json::Map<String, serde_json::Value> = if data.is_empty() || data == "null" {
                    serde_json::Map::new()
                } else {
                    serde_json::from_str(data)
                        .map_err(|e| RpcError::Deserialization(format!("Invalid JSON params: {}", e)))?
                };

                let result: Result<String, String> = match method {
                    #(#dispatch_arms)*
                    _ => Err(format!("Unknown method: {}", method)),
                };

                result.map_err(|e| RpcError::Storage(e))
            }
        }
    }
}

fn field_type_to_tokens(ty: &FieldType) -> TokenStream2 {
    match ty {
        FieldType::Primitive(p) => {
            // Handle special case for serde_json::Value which contains ::
            if p == "serde_json::Value" {
                quote! { serde_json::Value }
            } else {
                let ident = syn::Ident::new(p, proc_macro2::Span::call_site());
                quote! { #ident }
            }
        }
        FieldType::Named(n) => {
            let ident = syn::Ident::new(n, proc_macro2::Span::call_site());
            quote! { #ident }
        }
        FieldType::List(inner) => {
            let inner_tokens = field_type_to_tokens(inner);
            quote! { Vec<#inner_tokens> }
        }
        FieldType::Map(key, value) => {
            let key_tokens = field_type_to_tokens(key);
            let value_tokens = field_type_to_tokens(value);
            quote! { std::collections::HashMap<#key_tokens, #value_tokens> }
        }
    }
}

fn to_pascal_case(s: &str) -> String {
    let mut result = String::new();
    let mut capitalize_next = true;

    for ch in s.chars() {
        if ch == '_' || ch == '-' || ch == ' ' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(ch.to_ascii_uppercase());
            capitalize_next = false;
        } else {
            result.push(ch);
        }
    }

    result
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    let mut prev_lowercase = false;

    for ch in s.chars() {
        if ch.is_uppercase() {
            if prev_lowercase {
                result.push('_');
            }
            result.push(ch.to_ascii_lowercase());
            prev_lowercase = false;
        } else {
            result.push(ch);
            prev_lowercase = ch.is_lowercase();
        }
    }

    result
}

/// Format generated code for readable output in the .generated.rs file.
fn format_generated_code(tokens: &TokenStream2) -> String {
    let code = tokens.to_string();

    // Parse tokens into a syn File and use prettyplease to format
    match syn::parse_file(&code) {
        Ok(file) => prettyplease::unparse(&file),
        Err(_) => {
            // Fallback: return unformatted code
            code
        }
    }
}
