use boltffi_ffi_rules::naming;
use indexmap::IndexMap;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use syn::{
    Attribute, Fields, FnArg, ImplItem, Item, ItemEnum, ItemImpl, ItemStruct, ItemTrait, Type,
};
use walkdir::WalkDir;

use crate::model::{
    BuiltinId, CallbackTrait, Class, ClosureSignature as MClosureSignature, Constructor,
    ConstructorParam, CustomType, Enumeration, Function, Method, Module, Parameter, Primitive,
    Receiver, Record, RecordField, ReturnType, StreamMethod, StreamMode, TraitMethod,
    TraitMethodParam, Type as MType, Variant,
};

mod compiler_type_resolution;

pub enum PendingKind {
    Record,
    Enum,
    Class,
    Callback,
}

pub enum TypeShape {
    Pending(PendingKind),
    Record {
        fields: Vec<RecordField>,
        is_repr_c: bool,
        is_error: bool,
        constructors: Vec<Constructor>,
        methods: Vec<Method>,
    },
    Enum {
        variants: Vec<Variant>,
        is_error: bool,
        repr_type: Option<Primitive>,
        constructors: Vec<Constructor>,
        methods: Vec<Method>,
    },
    Class {
        constructors: Vec<Constructor>,
        methods: Vec<Method>,
        streams: Vec<StreamMethod>,
    },
    Custom {
        repr: MType,
    },
}

pub struct TypeMeta {
    pub doc: Option<String>,
    pub shape: TypeShape,
}

#[derive(Default)]
pub struct TypeRegistry {
    types: IndexMap<String, TypeMeta>,
    custom_type_names_by_remote_key: HashMap<CustomTypeLookupKey, String>,
}

impl TypeRegistry {
    pub fn is_enum(&self, name: &str) -> bool {
        self.types.get(name).is_some_and(|meta| {
            matches!(
                meta.shape,
                TypeShape::Pending(PendingKind::Enum) | TypeShape::Enum { .. }
            )
        })
    }

    pub fn contains(&self, name: &str) -> bool {
        self.types.contains_key(name)
    }

    pub fn doc(&self, name: &str) -> Option<&str> {
        self.types.get(name).and_then(|meta| meta.doc.as_deref())
    }

    pub fn register(&mut self, name: String, meta: TypeMeta) {
        self.types.insert(name, meta);
    }

    pub fn fill(&mut self, name: &str, shape: TypeShape) {
        if let Some(meta) = self.types.get_mut(name) {
            meta.shape = shape;
        }
    }

    pub fn fill_record_fields(
        &mut self,
        name: &str,
        fields: Vec<RecordField>,
        is_repr_c: bool,
        is_error: bool,
    ) {
        let Some(meta) = self.types.get_mut(name) else {
            return;
        };
        match &mut meta.shape {
            TypeShape::Record {
                fields: existing_fields,
                is_repr_c: existing_repr_c,
                is_error: existing_is_error,
                ..
            } => {
                *existing_fields = fields;
                *existing_repr_c = is_repr_c;
                *existing_is_error = is_error;
            }
            _ => {
                meta.shape = TypeShape::Record {
                    fields,
                    is_repr_c,
                    is_error,
                    constructors: Vec::new(),
                    methods: Vec::new(),
                };
            }
        }
    }

    pub fn set_doc(&mut self, name: &str, doc: String) {
        if let Some(meta) = self.types.get_mut(name) {
            meta.doc = Some(doc);
        }
    }

    pub fn is_record(&self, name: &str) -> bool {
        self.types.get(name).is_some_and(|meta| {
            matches!(
                meta.shape,
                TypeShape::Pending(PendingKind::Record) | TypeShape::Record { .. }
            )
        })
    }

    pub fn merge_record_impl(
        &mut self,
        name: &str,
        constructors: Vec<Constructor>,
        methods: Vec<Method>,
    ) {
        let Some(meta) = self.types.get_mut(name) else {
            return;
        };
        match &mut meta.shape {
            TypeShape::Record {
                constructors: existing_ctors,
                methods: existing_methods,
                ..
            } => {
                existing_ctors.extend(constructors);
                existing_methods.extend(methods);
            }
            TypeShape::Pending(PendingKind::Record) => {
                meta.shape = TypeShape::Record {
                    fields: Vec::new(),
                    is_repr_c: true,
                    is_error: false,
                    constructors,
                    methods,
                };
            }
            _ => {}
        }
    }

    pub fn merge_enum_impl(
        &mut self,
        name: &str,
        constructors: Vec<Constructor>,
        methods: Vec<Method>,
    ) {
        let Some(meta) = self.types.get_mut(name) else {
            return;
        };
        match &mut meta.shape {
            TypeShape::Enum {
                constructors: existing_ctors,
                methods: existing_methods,
                ..
            } => {
                existing_ctors.extend(constructors);
                existing_methods.extend(methods);
            }
            TypeShape::Pending(PendingKind::Enum) => {
                meta.shape = TypeShape::Enum {
                    variants: Vec::new(),
                    is_error: false,
                    repr_type: None,
                    constructors,
                    methods,
                };
            }
            _ => {}
        }
    }

    pub fn drain(self) -> impl Iterator<Item = (String, TypeMeta)> {
        self.types.into_iter()
    }

    pub fn classify_named_type(&self, name: &str) -> Option<MType> {
        let meta = self.types.get(name)?;
        Some(match &meta.shape {
            TypeShape::Pending(PendingKind::Record) | TypeShape::Record { .. } => {
                MType::Record(name.to_string())
            }
            TypeShape::Pending(PendingKind::Enum) | TypeShape::Enum { .. } => {
                MType::Enum(name.to_string())
            }
            TypeShape::Pending(PendingKind::Class) | TypeShape::Class { .. } => {
                MType::Object(name.to_string())
            }
            TypeShape::Pending(PendingKind::Callback) => MType::BoxedTrait(name.to_string()),
            TypeShape::Custom { repr } => MType::Custom {
                name: name.to_string(),
                repr: Box::new(repr.clone()),
            },
        })
    }

    pub fn classify_type_spelling(&self, spelling: &str) -> Option<MType> {
        self.classify_named_type(spelling).or_else(|| {
            syn::parse_str::<Type>(spelling)
                .ok()
                .and_then(|rust_type| self.classify_custom_remote_type(&rust_type))
        })
    }

    pub fn classify_custom_remote_type(&self, rust_type: &Type) -> Option<MType> {
        let custom_type_name = CustomTypeLookupKey::lookup_keys_for_rust_type(rust_type)
            .into_iter()
            .find_map(|lookup_key| self.custom_type_names_by_remote_key.get(&lookup_key))?;
        self.classify_named_type(custom_type_name)
    }

    pub fn register_custom_remote_type(
        &mut self,
        custom_type_name: &str,
        rust_type: &Type,
    ) -> Result<(), String> {
        let normalized_lookup_key = CustomTypeLookupKey::Normalized(CustomTypeNormalizedSpelling(
            normalize_type(rust_type),
        ));

        if let Some(existing_custom_type_name) = self
            .custom_type_names_by_remote_key
            .get(&normalized_lookup_key)
            .filter(|existing_custom_type_name| {
                existing_custom_type_name.as_str() != custom_type_name
            })
        {
            return Err(format!(
                "custom_type!: remote type already registered by `{}`",
                existing_custom_type_name
            ));
        }

        self.custom_type_names_by_remote_key
            .insert(normalized_lookup_key, custom_type_name.to_string());
        self.custom_type_names_by_remote_key
            .entry(CustomTypeLookupKey::Shape(CustomTypeShapeKey(
                type_shape_key(rust_type),
            )))
            .or_insert_with(|| custom_type_name.to_string());
        Ok(())
    }

    pub fn register_callback(&mut self, name: String) {
        self.types.insert(
            name,
            TypeMeta {
                shape: TypeShape::Pending(PendingKind::Callback),
                doc: None,
            },
        );
    }

    pub fn has_callback(&self, name: &str) -> bool {
        self.types
            .get(name)
            .is_some_and(|m| matches!(m.shape, TypeShape::Pending(PendingKind::Callback)))
    }
}

#[derive(Debug, Clone, Default)]
struct AliasResolver {
    use_aliases: HashMap<String, Vec<String>>,
    type_aliases: HashMap<String, Vec<String>>,
}

impl AliasResolver {
    fn from_items(items: &[Item]) -> Self {
        let mut resolver = Self::default();

        items
            .iter()
            .filter_map(|item| match item {
                Item::Use(item_use) => Some(&item_use.tree),
                _ => None,
            })
            .for_each(|tree| resolver.collect_use_tree(Vec::new(), tree));

        items
            .iter()
            .filter_map(|item| match item {
                Item::Type(item_type) => Some(item_type),
                _ => None,
            })
            .filter_map(|item_type| {
                let target = match item_type.ty.as_ref() {
                    Type::Path(type_path) => Some(Self::segments_from_path(type_path)),
                    _ => None,
                }?;
                Some((item_type.ident.to_string(), target))
            })
            .for_each(|(alias, target)| {
                resolver.type_aliases.insert(alias, target);
            });

        resolver
    }

    fn with_global(mut self, global: &HashMap<String, Vec<String>>) -> Self {
        global
            .iter()
            .filter(|&(name, _target)| !self.type_aliases.contains_key(name))
            .map(|(name, target)| (name.clone(), target.clone()))
            .collect::<Vec<_>>()
            .into_iter()
            .for_each(|(name, target)| {
                self.type_aliases.insert(name, target);
            });
        self
    }

    fn resolve_type_spelling<'a>(&self, spelling: &'a str) -> Cow<'a, str> {
        let stripped = spelling.trim().trim_start_matches("::");
        let parts: Vec<String> = stripped
            .split("::")
            .filter(|p| !p.is_empty())
            .map(|p| p.to_string())
            .collect();

        let resolved = self.resolve_segments(parts);
        let resolved_spelling = resolved
            .iter()
            .map(String::as_str)
            .collect::<Vec<_>>()
            .join("::");

        if resolved_spelling != stripped {
            Cow::Owned(resolved_spelling)
        } else {
            Cow::Borrowed(stripped)
        }
    }

    fn resolve_segments(&self, segments: Vec<String>) -> Vec<String> {
        let expanded = self.expand_use_aliases(segments);

        expanded
            .last()
            .and_then(|last| self.type_aliases.get(last))
            .cloned()
            .unwrap_or(expanded)
    }

    fn expand_use_aliases(&self, segments: Vec<String>) -> Vec<String> {
        self.expand_use_aliases_with_visited(segments, &mut HashSet::new())
    }

    fn expand_use_aliases_with_visited(
        &self,
        segments: Vec<String>,
        visited: &mut HashSet<Vec<String>>,
    ) -> Vec<String> {
        if !visited.insert(segments.clone()) {
            return segments;
        }

        match self.next_use_alias_segments(&segments) {
            Some(next_segments) if visited.contains(&next_segments) => segments,
            Some(next_segments) => self.expand_use_aliases_with_visited(next_segments, visited),
            None => segments,
        }
    }

    fn next_use_alias_segments(&self, segments: &[String]) -> Option<Vec<String>> {
        let first = segments.first()?;
        let replacement = self.use_aliases.get(first)?;
        let next_segments = replacement
            .iter()
            .cloned()
            .chain(segments.iter().skip(1).cloned())
            .collect::<Vec<_>>();
        (next_segments != segments).then_some(next_segments)
    }

    fn segments_from_path(type_path: &syn::TypePath) -> Vec<String> {
        type_path
            .path
            .segments
            .iter()
            .map(|seg| seg.ident.to_string())
            .collect()
    }

    fn collect_use_tree(&mut self, prefix: Vec<String>, tree: &syn::UseTree) {
        match tree {
            syn::UseTree::Path(path) => {
                let mut next_prefix = prefix;
                next_prefix.push(path.ident.to_string());
                self.collect_use_tree(next_prefix, &path.tree);
            }
            syn::UseTree::Name(name) => {
                let mut target = prefix;
                target.push(name.ident.to_string());
                self.use_aliases.insert(name.ident.to_string(), target);
            }
            syn::UseTree::Rename(rename) => {
                let mut target = prefix;
                target.push(rename.ident.to_string());
                self.use_aliases.insert(rename.rename.to_string(), target);
            }
            syn::UseTree::Group(group) => group
                .items
                .iter()
                .for_each(|item| self.collect_use_tree(prefix.clone(), item)),
            syn::UseTree::Glob(_) => {}
        }
    }
}

pub struct SourceScanner {
    module_name: String,
    type_registry: TypeRegistry,
    functions: Vec<Function>,
    callback_traits: Vec<CallbackTrait>,
    alias_resolver: AliasResolver,
    global_aliases: HashMap<String, Vec<String>>,
    compiler_canonical_types: HashMap<String, String>,
    integer_constants: HashMap<String, i128>,
    source_root: Option<PathBuf>,
    target_pointer_width_bits: Option<u8>,
}

impl SourceScanner {
    pub fn new(module_name: impl Into<String>) -> Self {
        Self::new_with_pointer_width(module_name, parse_target_pointer_width())
    }

    pub fn new_with_pointer_width(
        module_name: impl Into<String>,
        target_pointer_width_bits: Option<u8>,
    ) -> Self {
        Self {
            module_name: module_name.into(),
            type_registry: TypeRegistry::default(),
            functions: Vec::new(),
            callback_traits: Vec::new(),
            alias_resolver: AliasResolver::default(),
            global_aliases: HashMap::new(),
            compiler_canonical_types: HashMap::new(),
            integer_constants: HashMap::new(),
            source_root: None,
            target_pointer_width_bits,
        }
    }

    pub fn scan_directory(&mut self, crate_path: &Path, dir: &Path) -> Result<(), String> {
        let mut files: Vec<_> = WalkDir::new(dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
            .map(|e| e.path().to_path_buf())
            .collect();
        files.sort();

        self.source_root = Some(dir.to_path_buf());
        self.global_aliases = Self::collect_global_aliases(&files)?;
        self.integer_constants =
            collect_integer_constants_from_files(dir, &files, self.target_pointer_width_bits)?;
        let compiler_targets = Self::collect_compiler_type_targets(&files, &self.global_aliases)?;
        self.compiler_canonical_types =
            compiler_type_resolution::resolve(crate_path, &self.module_name, compiler_targets)?;
        files
            .iter()
            .try_for_each(|path| self.collect_type_names(path))?;
        files
            .iter()
            .try_for_each(|path| self.collect_custom_types(path))?;
        files.iter().try_for_each(|path| self.scan_file(path))?;
        Ok(())
    }

    fn collect_compiler_type_targets(
        files: &[std::path::PathBuf],
        global_aliases: &HashMap<String, Vec<String>>,
    ) -> Result<Vec<String>, String> {
        let mut targets = Vec::<String>::new();
        let mut seen = HashSet::<String>::new();

        files.iter().try_for_each(|path| {
            let content = fs::read_to_string(path)
                .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

            let syntax = syn::parse_file(&content)
                .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;

            let alias_resolver =
                AliasResolver::from_items(&syntax.items).with_global(global_aliases);
            syntax.items.iter().for_each(|item| {
                Self::collect_item_type_targets(item, &alias_resolver, &mut targets, &mut seen)
            });

            Ok::<(), String>(())
        })?;

        Ok(targets)
    }

    fn collect_item_type_targets(
        item: &Item,
        alias_resolver: &AliasResolver,
        out: &mut Vec<String>,
        seen: &mut HashSet<String>,
    ) {
        match item {
            Item::Struct(item_struct) => {
                let is_record = has_attribute(&item_struct.attrs, "ffi_record")
                    || has_attribute(&item_struct.attrs, "data")
                    || has_attribute(&item_struct.attrs, "error")
                    || has_repr_c(&item_struct.attrs)
                    || (has_attribute(&item_struct.attrs, "derive")
                        && has_ffi_type_derive(&item_struct.attrs));
                if is_record {
                    item_struct.fields.iter().for_each(|field| {
                        Self::collect_type_targets(&field.ty, alias_resolver, out, seen)
                    });
                }
            }
            Item::Enum(item_enum) => {
                let is_error = has_attribute(&item_enum.attrs, "error");
                let is_data_enum = has_repr_int(&item_enum.attrs)
                    || has_attribute(&item_enum.attrs, "data")
                    || is_error;
                if is_data_enum {
                    item_enum
                        .variants
                        .iter()
                        .flat_map(|variant| variant.fields.iter())
                        .for_each(|field| {
                            Self::collect_type_targets(&field.ty, alias_resolver, out, seen)
                        });
                }
            }
            Item::Impl(item_impl) => {
                let is_exported = has_attribute(&item_impl.attrs, "ffi_class")
                    || has_attribute(&item_impl.attrs, "export")
                    || has_data_impl_attribute(&item_impl.attrs);
                if is_exported {
                    item_impl
                        .items
                        .iter()
                        .filter_map(|impl_item| match impl_item {
                            ImplItem::Fn(method) => Some(method),
                            _ => None,
                        })
                        .filter(|method| matches!(method.vis, syn::Visibility::Public(_)))
                        .filter(|method| !method.attrs.iter().any(|a| a.path().is_ident("skip")))
                        .for_each(|method| {
                            method
                                .sig
                                .inputs
                                .iter()
                                .filter_map(|arg| match arg {
                                    FnArg::Typed(pat_type) => Some(pat_type.ty.as_ref()),
                                    FnArg::Receiver(_) => None,
                                })
                                .for_each(|ty| {
                                    Self::collect_type_targets(ty, alias_resolver, out, seen)
                                });

                            match &method.sig.output {
                                syn::ReturnType::Default => {}
                                syn::ReturnType::Type(_, ty) => {
                                    Self::collect_type_targets(
                                        ty.as_ref(),
                                        alias_resolver,
                                        out,
                                        seen,
                                    );
                                }
                            }
                        });
                }
            }
            Item::Trait(item_trait) => {
                let is_exported = has_attribute(&item_trait.attrs, "ffi_trait")
                    || has_attribute(&item_trait.attrs, "export");
                if is_exported {
                    item_trait
                        .items
                        .iter()
                        .filter_map(|trait_item| match trait_item {
                            syn::TraitItem::Fn(method) => Some(method),
                            _ => None,
                        })
                        .for_each(|method| {
                            method
                                .sig
                                .inputs
                                .iter()
                                .filter_map(|arg| match arg {
                                    FnArg::Typed(pat_type) => Some(pat_type.ty.as_ref()),
                                    FnArg::Receiver(_) => None,
                                })
                                .for_each(|ty| {
                                    Self::collect_type_targets(ty, alias_resolver, out, seen)
                                });

                            match &method.sig.output {
                                syn::ReturnType::Default => {}
                                syn::ReturnType::Type(_, ty) => {
                                    Self::collect_type_targets(
                                        ty.as_ref(),
                                        alias_resolver,
                                        out,
                                        seen,
                                    );
                                }
                            }
                        });
                }
            }
            Item::Fn(item_fn) => {
                let is_exported = has_attribute(&item_fn.attrs, "ffi_export")
                    || has_attribute(&item_fn.attrs, "export");
                if is_exported {
                    item_fn
                        .sig
                        .inputs
                        .iter()
                        .filter_map(|arg| match arg {
                            FnArg::Typed(pat_type) => Some(pat_type.ty.as_ref()),
                            FnArg::Receiver(_) => None,
                        })
                        .for_each(|ty| Self::collect_type_targets(ty, alias_resolver, out, seen));

                    match &item_fn.sig.output {
                        syn::ReturnType::Default => {}
                        syn::ReturnType::Type(_, ty) => {
                            Self::collect_type_targets(ty.as_ref(), alias_resolver, out, seen);
                        }
                    }
                }
            }
            _ => {}
        }
    }

    fn collect_type_targets(
        ty: &Type,
        alias_resolver: &AliasResolver,
        out: &mut Vec<String>,
        seen: &mut HashSet<String>,
    ) {
        match ty {
            Type::Path(type_path) => {
                let all_segments_plain = type_path
                    .path
                    .segments
                    .iter()
                    .all(|seg| matches!(seg.arguments, syn::PathArguments::None));

                type_path
                    .path
                    .segments
                    .iter()
                    .filter_map(|seg| match &seg.arguments {
                        syn::PathArguments::AngleBracketed(args) => Some(args),
                        _ => None,
                    })
                    .flat_map(|args| args.args.iter())
                    .filter_map(|arg| match arg {
                        syn::GenericArgument::Type(inner_ty) => Some(inner_ty),
                        _ => None,
                    })
                    .for_each(|inner_ty| {
                        Self::collect_type_targets(inner_ty, alias_resolver, out, seen)
                    });

                type_path
                    .path
                    .segments
                    .iter()
                    .filter_map(|seg| match &seg.arguments {
                        syn::PathArguments::Parenthesized(args) => Some(args),
                        _ => None,
                    })
                    .for_each(|args| {
                        args.inputs.iter().for_each(|inner_ty| {
                            Self::collect_type_targets(inner_ty, alias_resolver, out, seen)
                        });
                        match &args.output {
                            syn::ReturnType::Default => {}
                            syn::ReturnType::Type(_, out_ty) => {
                                Self::collect_type_targets(
                                    out_ty.as_ref(),
                                    alias_resolver,
                                    out,
                                    seen,
                                );
                            }
                        }
                    });

                if all_segments_plain {
                    let path_str = type_path
                        .path
                        .segments
                        .iter()
                        .map(|seg| seg.ident.to_string())
                        .collect::<Vec<_>>()
                        .join("::");

                    let resolved = alias_resolver.resolve_type_spelling(&path_str).into_owned();
                    if resolved.starts_with("crate::") && seen.insert(resolved.clone()) {
                        out.push(resolved);
                    }
                }
            }
            Type::Reference(type_ref) => {
                Self::collect_type_targets(type_ref.elem.as_ref(), alias_resolver, out, seen);
            }
            Type::Slice(slice) => {
                Self::collect_type_targets(slice.elem.as_ref(), alias_resolver, out, seen);
            }
            Type::Array(array) => {
                Self::collect_type_targets(array.elem.as_ref(), alias_resolver, out, seen);
            }
            Type::Tuple(tuple) => tuple.elems.iter().for_each(|inner_ty| {
                Self::collect_type_targets(inner_ty, alias_resolver, out, seen)
            }),
            Type::Group(group) => {
                Self::collect_type_targets(group.elem.as_ref(), alias_resolver, out, seen);
            }
            Type::Paren(paren) => {
                Self::collect_type_targets(paren.elem.as_ref(), alias_resolver, out, seen);
            }
            _ => {}
        }
    }

    fn collect_global_aliases(
        files: &[std::path::PathBuf],
    ) -> Result<HashMap<String, Vec<String>>, String> {
        let mut aliases = HashMap::<String, Vec<String>>::new();
        let mut conflicts = HashSet::<String>::new();

        files.iter().try_for_each(|path| {
            let content = fs::read_to_string(path)
                .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

            let syntax = syn::parse_file(&content)
                .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;

            let local = AliasResolver::from_items(&syntax.items);
            syntax
                .items
                .iter()
                .filter_map(|item| match item {
                    Item::Type(item_type) => Some(item_type),
                    _ => None,
                })
                .filter_map(|item_type| {
                    let target_path = match item_type.ty.as_ref() {
                        Type::Path(type_path) => {
                            Some(AliasResolver::segments_from_path(type_path).join("::"))
                        }
                        _ => None,
                    }?;

                    let resolved = local.resolve_type_spelling(&target_path).into_owned();
                    let segments = resolved
                        .split("::")
                        .filter(|p| !p.is_empty())
                        .map(|p| p.to_string())
                        .collect::<Vec<_>>();
                    Some((item_type.ident.to_string(), segments))
                })
                .for_each(|(alias_name, target)| match aliases.get(&alias_name) {
                    None => {
                        aliases.insert(alias_name, target);
                    }
                    Some(existing) if *existing == target => {}
                    Some(_) => {
                        conflicts.insert(alias_name);
                    }
                });

            Ok::<(), String>(())
        })?;

        conflicts.iter().for_each(|name| {
            aliases.remove(name);
        });

        Ok(aliases)
    }

    fn collect_custom_types(&mut self, path: &Path) -> Result<(), String> {
        let content = fs::read_to_string(path)
            .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

        let syntax = syn::parse_file(&content)
            .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;

        let alias_resolver =
            AliasResolver::from_items(&syntax.items).with_global(&self.global_aliases);
        syntax
            .items
            .iter()
            .filter_map(|item| match item {
                Item::Macro(item_macro)
                    if item_macro
                        .mac
                        .path
                        .segments
                        .last()
                        .is_some_and(|segment| segment.ident == "custom_type") =>
                {
                    Some(item_macro)
                }
                _ => None,
            })
            .try_for_each(|item_macro| {
                self.collect_custom_type_macro(item_macro, &alias_resolver)
            })?;
        syntax
            .items
            .iter()
            .filter_map(|item| match item {
                Item::Impl(item_impl) if has_attribute(&item_impl.attrs, "custom_ffi") => {
                    Some(item_impl)
                }
                _ => None,
            })
            .try_for_each(|item_impl| self.collect_custom_type(item_impl, &alias_resolver))
    }

    fn collect_custom_type_macro(
        &mut self,
        item_macro: &syn::ItemMacro,
        alias_resolver: &AliasResolver,
    ) -> Result<(), String> {
        let spec: CustomTypeMacroSpec = syn::parse2(item_macro.mac.tokens.clone())
            .map_err(|e| format!("custom_type!: failed to parse: {e}"))?;

        let name = spec.name.to_string();
        if self.type_registry.contains(&name) {
            return Err(format!(
                "custom_type!: `{}` conflicts with an existing record/enum/class name",
                name
            ));
        }

        let repr_syn_type = &spec.repr;
        let repr = rust_type_to_ffi_type(
            repr_syn_type,
            &self.type_registry,
            alias_resolver,
            &self.compiler_canonical_types,
            None,
        )
        .ok_or_else(|| {
            format!(
                "custom_type!: `{}` has an unsupported repr type: {}",
                name,
                quote::quote!(#repr_syn_type)
            )
        })?;

        self.type_registry.register(
            name.clone(),
            TypeMeta {
                doc: extract_doc_string(&item_macro.attrs),
                shape: TypeShape::Custom { repr: repr.clone() },
            },
        );
        self.type_registry
            .register_custom_remote_type(&name, &spec.remote)?;
        Ok(())
    }

    fn collect_custom_type(
        &mut self,
        item_impl: &ItemImpl,
        alias_resolver: &AliasResolver,
    ) -> Result<(), String> {
        let Some(name) = impl_self_type_ident(item_impl) else {
            return Err("custom_ffi: unsupported self type".to_string());
        };

        if self.type_registry.contains(&name) {
            return Err(format!(
                "custom_ffi: `{}` conflicts with an existing record/enum/class name",
                name
            ));
        }

        let repr_syn_type = item_impl
            .items
            .iter()
            .filter_map(|item| match item {
                ImplItem::Type(assoc) if assoc.ident == "FfiRepr" => Some(&assoc.ty),
                _ => None,
            })
            .next()
            .ok_or_else(|| format!("custom_ffi: `{}` is missing `type FfiRepr = ...;`", name))?;

        let repr = rust_type_to_ffi_type(
            repr_syn_type,
            &self.type_registry,
            alias_resolver,
            &self.compiler_canonical_types,
            None,
        )
        .ok_or_else(|| {
            format!(
                "custom_ffi: `{}` has an unsupported FfiRepr type: {}",
                name,
                quote::quote!(#repr_syn_type)
            )
        })?;

        self.type_registry.register(
            name.clone(),
            TypeMeta {
                doc: extract_doc_string(&item_impl.attrs),
                shape: TypeShape::Custom { repr: repr.clone() },
            },
        );

        Ok(())
    }

    fn collect_type_names(&mut self, path: &Path) -> Result<(), String> {
        let content = fs::read_to_string(path)
            .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

        let syntax = syn::parse_file(&content)
            .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;

        for item in &syntax.items {
            match item {
                Item::Struct(item_struct) => {
                    if has_attribute(&item_struct.attrs, "ffi_record")
                        || has_attribute(&item_struct.attrs, "data")
                        || has_attribute(&item_struct.attrs, "error")
                        || has_repr_c(&item_struct.attrs)
                        || (has_attribute(&item_struct.attrs, "derive")
                            && has_ffi_type_derive(&item_struct.attrs))
                    {
                        self.type_registry.register(
                            item_struct.ident.to_string(),
                            TypeMeta {
                                doc: extract_doc_string(&item_struct.attrs),
                                shape: TypeShape::Pending(PendingKind::Record),
                            },
                        );
                    }
                }
                Item::Enum(item_enum) => {
                    if has_repr_int(&item_enum.attrs)
                        || has_attribute(&item_enum.attrs, "data")
                        || has_attribute(&item_enum.attrs, "error")
                    {
                        self.type_registry.register(
                            item_enum.ident.to_string(),
                            TypeMeta {
                                doc: extract_doc_string(&item_enum.attrs),
                                shape: TypeShape::Pending(PendingKind::Enum),
                            },
                        );
                    }
                }
                Item::Impl(item_impl) => {
                    if (has_attribute(&item_impl.attrs, "ffi_class")
                        || has_attribute(&item_impl.attrs, "export"))
                        && !has_data_impl_attribute(&item_impl.attrs)
                        && let Type::Path(type_path) = item_impl.self_ty.as_ref()
                        && let Some(seg) = type_path.path.segments.last()
                    {
                        self.type_registry.register(
                            seg.ident.to_string(),
                            TypeMeta {
                                doc: None,
                                shape: TypeShape::Pending(PendingKind::Class),
                            },
                        );
                    }
                }
                _ => {}
            }
        }

        Ok(())
    }

    pub fn scan_file(&mut self, path: &Path) -> Result<(), String> {
        let content = fs::read_to_string(path)
            .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

        let syntax = syn::parse_file(&content)
            .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;

        self.alias_resolver =
            AliasResolver::from_items(&syntax.items).with_global(&self.global_aliases);
        let file_module_path = self
            .source_root
            .as_ref()
            .map(|source_root| module_path_for_source_file(source_root, path))
            .transpose()?
            .unwrap_or_default();
        syntax
            .items
            .iter()
            .try_for_each(|item| self.process_item(item, &file_module_path))?;

        Ok(())
    }

    fn process_item(&mut self, item: &Item, file_module_path: &[String]) -> Result<(), String> {
        match item {
            Item::Struct(item_struct) => {
                if let Some(doc) = extract_doc_string(&item_struct.attrs) {
                    self.type_registry
                        .set_doc(&item_struct.ident.to_string(), doc);
                }
                if has_attribute(&item_struct.attrs, "ffi_record")
                    || has_attribute(&item_struct.attrs, "data")
                    || has_attribute(&item_struct.attrs, "error")
                    || has_repr_c(&item_struct.attrs)
                    || (has_attribute(&item_struct.attrs, "derive")
                        && has_ffi_type_derive(&item_struct.attrs))
                {
                    self.process_record(item_struct);
                }
            }
            Item::Impl(item_impl) => {
                if has_data_impl_attribute(&item_impl.attrs) {
                    self.process_value_type_impl(item_impl);
                } else if has_attribute(&item_impl.attrs, "ffi_class")
                    || has_attribute(&item_impl.attrs, "export")
                {
                    self.process_class(item_impl);
                }
            }
            Item::Trait(item_trait) => {
                if has_attribute(&item_trait.attrs, "ffi_trait")
                    || has_attribute(&item_trait.attrs, "export")
                {
                    self.process_callback_trait(item_trait);
                }
            }
            Item::Fn(item_fn) => {
                if has_attribute(&item_fn.attrs, "ffi_export")
                    || has_attribute(&item_fn.attrs, "export")
                {
                    self.process_function(item_fn);
                }
            }
            Item::Enum(item_enum) => {
                let is_error = has_attribute(&item_enum.attrs, "error");
                if has_repr_int(&item_enum.attrs)
                    || has_attribute(&item_enum.attrs, "data")
                    || is_error
                {
                    self.process_enum(item_enum, is_error, file_module_path)?;
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn resolve_typed_params(
        &self,
        inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
        self_type: Option<&str>,
    ) -> Option<Vec<(String, MType)>> {
        let typed: Vec<_> = inputs
            .iter()
            .filter_map(|arg| match arg {
                FnArg::Typed(pat_type) => Some(pat_type),
                _ => None,
            })
            .collect();

        let resolved: Vec<(String, MType)> = typed
            .iter()
            .filter_map(|pat_type| {
                let name = match &*pat_type.pat {
                    syn::Pat::Ident(ident) => ident.ident.to_string(),
                    _ => return None,
                };
                let ty = rust_type_to_ffi_type(
                    &pat_type.ty,
                    &self.type_registry,
                    &self.alias_resolver,
                    &self.compiler_canonical_types,
                    self_type,
                )?;
                Some((name, ty))
            })
            .collect();

        (resolved.len() == typed.len()).then_some(resolved)
    }

    fn resolve_output(&self, output: &syn::ReturnType, self_type: Option<&str>) -> Option<MType> {
        match output {
            syn::ReturnType::Default => None,
            syn::ReturnType::Type(_, ty) => rust_type_to_ffi_type(
                ty,
                &self.type_registry,
                &self.alias_resolver,
                &self.compiler_canonical_types,
                self_type,
            ),
        }
    }

    fn extract_receiver(sig: &syn::Signature) -> Receiver {
        sig.inputs
            .first()
            .and_then(|arg| match arg {
                syn::FnArg::Receiver(r) => Some(if r.mutability.is_some() {
                    Receiver::RefMut
                } else {
                    Receiver::Ref
                }),
                _ => None,
            })
            .unwrap_or(Receiver::None)
    }

    fn process_record(&mut self, item_struct: &ItemStruct) {
        let name = item_struct.ident.to_string();
        let fields = match &item_struct.fields {
            Fields::Named(named) => named
                .named
                .iter()
                .filter_map(|f| {
                    let field_name = f.ident.as_ref()?.to_string();
                    let field_type = rust_type_to_ffi_type(
                        &f.ty,
                        &self.type_registry,
                        &self.alias_resolver,
                        &self.compiler_canonical_types,
                        None,
                    )?;
                    let mut record_field = RecordField::new(&field_name, field_type);
                    if let Some(doc) = extract_doc_string(&f.attrs) {
                        record_field = record_field.with_doc(doc);
                    }
                    if let Some(default) = extract_default_value(&f.attrs) {
                        record_field = record_field.with_default(default);
                    }
                    Some(record_field)
                })
                .collect(),
            _ => Vec::new(),
        };

        let has_data = has_attribute(&item_struct.attrs, "data");
        let is_error = has_attribute(&item_struct.attrs, "error");
        let has_any_repr = item_struct.attrs.iter().any(|a| a.path().is_ident("repr"));
        let is_repr_c = if (has_data || is_error) && !has_any_repr {
            true
        } else {
            has_repr_c(&item_struct.attrs)
        };
        self.type_registry
            .fill_record_fields(&name, fields, is_repr_c, is_error);
    }

    fn process_enum(
        &mut self,
        item_enum: &ItemEnum,
        is_error: bool,
        file_module_path: &[String],
    ) -> Result<(), String> {
        let name = item_enum.ident.to_string();
        let repr_type = extract_repr_int(&item_enum.attrs);
        let mut next_discriminant: i128 = 0;

        let variants = item_enum
            .variants
            .iter()
            .map(|v| -> Result<Variant, String> {
                let variant_name = v.ident.to_string();
                let discriminant = match v.discriminant.as_ref() {
                    Some((_, expr)) => parse_discriminant_expr(
                        expr,
                        &self.integer_constants,
                        file_module_path,
                        self.target_pointer_width_bits,
                    )
                    .ok_or_else(|| {
                        format!(
                            "failed to evaluate discriminant for enum `{}` variant `{}`",
                            name, variant_name
                        )
                    })?,
                    None => next_discriminant,
                };
                next_discriminant = discriminant.checked_add(1).ok_or_else(|| {
                    format!(
                        "discriminant overflow for enum `{}` variant `{}`",
                        name, variant_name
                    )
                })?;

                let fields: Vec<RecordField> = match &v.fields {
                    Fields::Named(named) => named
                        .named
                        .iter()
                        .filter_map(|f| {
                            let field_name = f.ident.as_ref()?.to_string();
                            let field_type = rust_type_to_ffi_type(
                                &f.ty,
                                &self.type_registry,
                                &self.alias_resolver,
                                &self.compiler_canonical_types,
                                None,
                            )?;
                            let mut record_field = RecordField::new(&field_name, field_type);
                            if let Some(doc) = extract_doc_string(&f.attrs) {
                                record_field = record_field.with_doc(doc);
                            }
                            Some(record_field)
                        })
                        .collect(),
                    Fields::Unnamed(unnamed) => unnamed
                        .unnamed
                        .iter()
                        .enumerate()
                        .filter_map(|(i, f)| {
                            let field_type = rust_type_to_ffi_type(
                                &f.ty,
                                &self.type_registry,
                                &self.alias_resolver,
                                &self.compiler_canonical_types,
                                None,
                            )?;
                            Some(RecordField::new(format!("value_{i}"), field_type))
                        })
                        .collect(),
                    Fields::Unit => Vec::new(),
                };

                let variant = Variant::new(&variant_name)
                    .with_discriminant(discriminant)
                    .maybe_doc(extract_doc_string(&v.attrs));
                Ok(fields
                    .into_iter()
                    .fold(variant, |v, field| v.with_field(field)))
            })
            .collect::<Result<Vec<_>, _>>()?;

        self.type_registry.fill(
            &name,
            TypeShape::Enum {
                variants,
                is_error,
                repr_type,
                constructors: Vec::new(),
                methods: Vec::new(),
            },
        );
        Ok(())
    }

    fn process_function(&mut self, item_fn: &syn::ItemFn) {
        let sig = &item_fn.sig;
        let Some(params) = self.resolve_typed_params(&sig.inputs, None) else {
            return;
        };
        let output = self.resolve_output(&sig.output, None);
        if matches!(sig.output, syn::ReturnType::Type(..)) && output.is_none() {
            return;
        }

        let function = params
            .into_iter()
            .fold(Function::new(sig.ident.to_string()), |f, (name, ty)| {
                f.with_param(Parameter::new(&name, ty))
            })
            .maybe_doc(extract_doc_string(&item_fn.attrs))
            .maybe_return(output.map(ReturnType::from_output))
            .maybe_async(sig.asyncness.is_some());

        self.functions.push(function);
    }

    fn process_callback_trait(&mut self, item_trait: &ItemTrait) {
        let name = item_trait.ident.to_string();

        let callback = item_trait
            .items
            .iter()
            .filter_map(|item| match item {
                syn::TraitItem::Fn(method) => self.build_trait_method(method),
                _ => None,
            })
            .fold(CallbackTrait::new(&name), |ct, m| ct.with_method(m))
            .maybe_doc(extract_doc_string(&item_trait.attrs));

        self.type_registry.register_callback(name);
        self.callback_traits.push(callback);
    }

    fn build_trait_method(&self, method: &syn::TraitItemFn) -> Option<TraitMethod> {
        let sig = &method.sig;
        let params = self.resolve_typed_params(&sig.inputs, None)?;
        let output = self.resolve_output(&sig.output, None);

        Some(
            params
                .into_iter()
                .fold(TraitMethod::new(sig.ident.to_string()), |tm, (name, ty)| {
                    tm.with_param(TraitMethodParam::new(&name, ty))
                })
                .maybe_doc(extract_doc_string(&method.attrs))
                .maybe_return(output.map(ReturnType::from_output))
                .maybe_async(sig.asyncness.is_some()),
        )
    }

    fn process_class(&mut self, item_impl: &ItemImpl) {
        let Some(class_name) = impl_self_type_ident(item_impl) else {
            return;
        };

        let mut constructors = Vec::new();
        let mut methods = Vec::new();
        let mut streams = Vec::new();

        item_impl
            .items
            .iter()
            .filter_map(|item| match item {
                ImplItem::Fn(method) => Some(method),
                _ => None,
            })
            .filter(|method| matches!(method.vis, syn::Visibility::Public(_)))
            .filter(|method| !has_attribute(&method.attrs, "skip"))
            .for_each(|method| {
                if has_attribute(&method.attrs, "ffi_stream") {
                    if let Some(stream) = self.build_stream(method) {
                        streams.push(stream);
                    }
                    return;
                }

                if self.is_constructor(method, &class_name) {
                    if let Some(ctor) = self.build_constructor(method, &class_name) {
                        constructors.push(ctor);
                    }
                    return;
                }

                if let Some(built_method) = self.build_method(method, &class_name) {
                    methods.push(built_method);
                }
            });

        self.type_registry.fill(
            &class_name,
            TypeShape::Class {
                constructors,
                methods,
                streams,
            },
        );
    }

    fn process_value_type_impl(&mut self, item_impl: &ItemImpl) {
        let Some(type_name) = impl_self_type_ident(item_impl) else {
            return;
        };

        let is_record = self.type_registry.is_record(&type_name);
        let is_enum = self.type_registry.is_enum(&type_name);

        if !is_record && !is_enum {
            return;
        }

        let mut constructors = Vec::new();
        let mut methods = Vec::new();

        item_impl
            .items
            .iter()
            .filter_map(|item| match item {
                ImplItem::Fn(method) => Some(method),
                _ => None,
            })
            .filter(|method| matches!(method.vis, syn::Visibility::Public(_)))
            .filter(|method| !has_attribute(&method.attrs, "skip"))
            .for_each(|method| {
                if self.is_constructor(method, &type_name) {
                    if let Some(ctor) = self.build_constructor(method, &type_name) {
                        constructors.push(ctor);
                    }
                    return;
                }

                if let Some(built_method) = self.build_method(method, &type_name) {
                    methods.push(built_method);
                }
            });

        if is_record {
            self.type_registry
                .merge_record_impl(&type_name, constructors, methods);
        } else {
            self.type_registry
                .merge_enum_impl(&type_name, constructors, methods);
        }
    }

    fn build_method(&self, method: &syn::ImplItemFn, self_type_name: &str) -> Option<Method> {
        let sig = &method.sig;
        let receiver = Self::extract_receiver(sig);
        let params = self.resolve_typed_params(&sig.inputs, Some(self_type_name))?;
        let output = self.resolve_output(&sig.output, Some(self_type_name));

        Some(
            params
                .into_iter()
                .fold(
                    Method::new(sig.ident.to_string(), receiver),
                    |m, (name, ty)| m.with_param(Parameter::new(&name, ty)),
                )
                .maybe_doc(extract_doc_string(&method.attrs))
                .maybe_return(output.map(ReturnType::from_output))
                .maybe_async(sig.asyncness.is_some()),
        )
    }

    fn build_stream(&self, method: &syn::ImplItemFn) -> Option<StreamMethod> {
        let name = method.sig.ident.to_string();

        let (item_type, mode) = extract_stream_attr(
            &method.attrs,
            &self.type_registry,
            &self.alias_resolver,
            &self.compiler_canonical_types,
        )?;

        Some(
            StreamMethod::new(&name, item_type)
                .with_mode(mode)
                .maybe_doc(extract_doc_string(&method.attrs)),
        )
    }

    fn is_constructor(&self, method: &syn::ImplItemFn, class_name: &str) -> bool {
        let has_self_receiver = method
            .sig
            .inputs
            .iter()
            .any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
        if has_self_receiver {
            return false;
        }

        match &method.sig.output {
            syn::ReturnType::Default => false,
            syn::ReturnType::Type(_, ty) => {
                return_type_is_self(ty.as_ref(), class_name)
                    || return_type_is_result_self(ty.as_ref(), class_name)
                    || return_type_is_option_self(ty.as_ref(), class_name)
            }
        }
    }

    fn build_constructor(
        &self,
        method: &syn::ImplItemFn,
        self_type_name: &str,
    ) -> Option<Constructor> {
        let sig = &method.sig;
        let is_fallible = match &sig.output {
            syn::ReturnType::Default => false,
            syn::ReturnType::Type(_, ty) => return_type_is_result_self(ty.as_ref(), self_type_name),
        };
        let is_optional = match &sig.output {
            syn::ReturnType::Default => false,
            syn::ReturnType::Type(_, ty) => return_type_is_option_self(ty.as_ref(), self_type_name),
        };
        let params = self.resolve_typed_params(&sig.inputs, Some(self_type_name))?;

        Some(
            params
                .into_iter()
                .fold(
                    Constructor::new()
                        .with_name(sig.ident.to_string())
                        .with_fallible(is_fallible)
                        .with_optional(is_optional),
                    |c, (name, ty)| c.with_param(ConstructorParam::new(&name, ty)),
                )
                .maybe_doc(extract_doc_string(&method.attrs)),
        )
    }

    pub fn into_module(self) -> Module {
        let mut module = Module::new(&self.module_name);

        for (name, entry) in self.type_registry.drain() {
            match entry.shape {
                TypeShape::Record {
                    fields,
                    is_repr_c,
                    is_error,
                    constructors,
                    methods,
                } => {
                    let record = fields
                        .into_iter()
                        .fold(Record::new(&name), |r, f| r.with_field(f))
                        .with_repr_c(is_repr_c)
                        .maybe_doc(entry.doc);
                    let record = constructors
                        .into_iter()
                        .fold(record, |r, ctor| r.with_constructor(ctor));
                    let record = methods.into_iter().fold(record, |r, m| r.with_method(m));
                    let record = if is_error { record.as_error() } else { record };
                    module = module.with_record(record);
                }
                TypeShape::Enum {
                    variants,
                    is_error,
                    repr_type,
                    constructors,
                    methods,
                } => {
                    let mut enumeration = variants
                        .into_iter()
                        .fold(Enumeration::new(&name), |e, v| e.with_variant(v))
                        .maybe_doc(entry.doc);
                    if is_error {
                        enumeration = enumeration.as_error();
                    }
                    enumeration.repr_type = repr_type;
                    let enumeration = constructors
                        .into_iter()
                        .fold(enumeration, |e, ctor| e.with_constructor(ctor));
                    let enumeration = methods
                        .into_iter()
                        .fold(enumeration, |e, m| e.with_method(m));
                    module = module.with_enum(enumeration);
                }
                TypeShape::Class {
                    constructors,
                    methods,
                    streams,
                } => {
                    let class = constructors
                        .into_iter()
                        .fold(Class::new(&name), |c, ctor| c.with_constructor(ctor));
                    let class = methods.into_iter().fold(class, |c, m| c.with_method(m));
                    let class = streams
                        .into_iter()
                        .fold(class, |c, s| c.with_stream(s))
                        .maybe_doc(entry.doc);
                    module = module.with_class(class);
                }
                TypeShape::Custom { repr } => {
                    module = module.with_custom_type(CustomType::new(name, repr));
                }
                TypeShape::Pending(_) => {}
            }
        }

        for function in self.functions {
            module = module.with_function(function);
        }
        for callback in self.callback_traits {
            module = module.with_callback_trait(callback);
        }

        module.collect_derived_types();
        module
    }
}

struct CustomTypeMacroSpec {
    name: syn::Ident,
    remote: syn::Type,
    repr: syn::Type,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CustomTypeNormalizedSpelling(String);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CustomTypeShapeKey(String);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CustomTypeLookupKey {
    Normalized(CustomTypeNormalizedSpelling),
    Shape(CustomTypeShapeKey),
}

impl CustomTypeLookupKey {
    fn lookup_keys_for_rust_type(rust_type: &Type) -> [Self; 2] {
        [
            Self::Normalized(CustomTypeNormalizedSpelling(normalize_type(rust_type))),
            Self::Shape(CustomTypeShapeKey(type_shape_key(rust_type))),
        ]
    }
}

impl syn::parse::Parse for CustomTypeMacroSpec {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let _visibility: syn::Visibility = input.parse()?;
        let name: syn::Ident = input.parse()?;
        input.parse::<syn::Token![,]>()?;

        let mut remote: Option<syn::Type> = None;
        let mut repr: Option<syn::Type> = None;
        while !input.is_empty() {
            let key: syn::Ident = input.parse()?;
            input.parse::<syn::Token![=]>()?;
            match key.to_string().as_str() {
                "remote" => {
                    remote = Some(input.parse()?);
                }
                "repr" => {
                    repr = Some(input.parse()?);
                }
                "error" => {
                    let _: syn::Type = input.parse()?;
                }
                "into_ffi" | "try_from_ffi" => {
                    let _: syn::Expr = input.parse()?;
                }
                _ => {
                    let _: syn::Expr = input.parse()?;
                }
            }

            if input.peek(syn::Token![,]) {
                input.parse::<syn::Token![,]>()?;
            }
        }

        let remote = remote.ok_or_else(|| input.error("custom_type!: missing `remote = ...`"))?;
        let repr = repr.ok_or_else(|| input.error("custom_type!: missing `repr = ...`"))?;

        Ok(Self { name, remote, repr })
    }
}

fn normalize_type(rust_type: &Type) -> String {
    quote::quote!(#rust_type).to_string().replace(' ', "")
}

fn type_shape_key(rust_type: &Type) -> String {
    match rust_type {
        Type::Reference(reference) => type_shape_key(reference.elem.as_ref()),
        Type::Path(type_path) => type_path
            .path
            .segments
            .last()
            .map(shape_key_for_segment)
            .unwrap_or_else(|| normalize_type(rust_type)),
        _ => normalize_type(rust_type),
    }
}

fn shape_key_for_segment(path_segment: &syn::PathSegment) -> String {
    let segment_name = path_segment.ident.to_string();
    let generic_shape_keys = match &path_segment.arguments {
        syn::PathArguments::AngleBracketed(arguments) => arguments
            .args
            .iter()
            .filter_map(|argument| match argument {
                syn::GenericArgument::Type(rust_type) => Some(type_shape_key(rust_type)),
                _ => None,
            })
            .collect::<Vec<_>>(),
        _ => Vec::new(),
    };

    if generic_shape_keys.is_empty() {
        segment_name
    } else {
        format!("{}<{}>", segment_name, generic_shape_keys.join(","))
    }
}

fn has_attribute(attrs: &[Attribute], name: &str) -> bool {
    attrs.iter().any(|attr| {
        attr.path().is_ident(name)
            || attr
                .path()
                .segments
                .last()
                .is_some_and(|segment| segment.ident == name)
    })
}

fn has_data_impl_attribute(attrs: &[Attribute]) -> bool {
    attrs.iter().any(|attr| {
        let is_data = attr.path().is_ident("data")
            || attr
                .path()
                .segments
                .last()
                .is_some_and(|segment| segment.ident == "data");
        if !is_data {
            return false;
        }
        matches!(&attr.meta, syn::Meta::List(list) if list.tokens.to_string().trim() == "impl")
    })
}

fn extract_default_value(attrs: &[Attribute]) -> Option<String> {
    attrs.iter().find_map(|attr| {
        let path = attr.path();
        let is_boltffi_default = path.segments.len() == 2
            && path.segments[0].ident == "boltffi"
            && path.segments[1].ident == "default";
        if !is_boltffi_default {
            return None;
        }
        let tokens = match &attr.meta {
            syn::Meta::List(list) => list.tokens.to_string(),
            _ => return None,
        };
        Some(tokens.trim().to_string())
    })
}

fn extract_doc_string(attrs: &[Attribute]) -> Option<String> {
    let lines: Vec<String> = attrs
        .iter()
        .filter(|attr| attr.path().is_ident("doc"))
        .filter_map(|attr| match &attr.meta {
            syn::Meta::NameValue(nv) => {
                if let syn::Expr::Lit(syn::ExprLit {
                    lit: syn::Lit::Str(s),
                    ..
                }) = &nv.value
                {
                    Some(s.value())
                } else {
                    None
                }
            }
            _ => None,
        })
        .collect();

    if lines.is_empty() {
        return None;
    }

    let joined = lines
        .iter()
        .map(|line| line.strip_prefix(' ').unwrap_or(line))
        .collect::<Vec<_>>()
        .join("\n");

    let trimmed = joined.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

fn return_type_is_self(ty: &Type, class_name: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };
    type_path
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident == "Self" || segment.ident == class_name)
}

fn return_type_is_result_self(ty: &Type, class_name: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };
    let Some(result_segment) = type_path
        .path
        .segments
        .last()
        .filter(|segment| segment.ident == "Result")
    else {
        return false;
    };
    let syn::PathArguments::AngleBracketed(args) = &result_segment.arguments else {
        return false;
    };
    let Some(syn::GenericArgument::Type(Type::Path(ok_ty))) = args.args.first() else {
        return false;
    };
    ok_ty
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident == "Self" || segment.ident == class_name)
}

fn return_type_is_option_self(ty: &Type, class_name: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };
    let Some(option_segment) = type_path
        .path
        .segments
        .last()
        .filter(|segment| segment.ident == "Option")
    else {
        return false;
    };
    let syn::PathArguments::AngleBracketed(args) = &option_segment.arguments else {
        return false;
    };
    let Some(syn::GenericArgument::Type(Type::Path(inner_ty))) = args.args.first() else {
        return false;
    };
    inner_ty
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident == "Self" || segment.ident == class_name)
}

fn impl_self_type_ident(item_impl: &ItemImpl) -> Option<String> {
    match item_impl.self_ty.as_ref() {
        Type::Path(type_path) => type_path
            .path
            .segments
            .last()
            .map(|segment| segment.ident.to_string()),
        Type::Group(group) => match group.elem.as_ref() {
            Type::Path(type_path) => type_path
                .path
                .segments
                .last()
                .map(|segment| segment.ident.to_string()),
            _ => None,
        },
        _ => None,
    }
}

fn has_repr_c(attrs: &[Attribute]) -> bool {
    attrs.iter().any(|attr| {
        if !attr.path().is_ident("repr") {
            return false;
        }
        let Ok(meta) = attr.meta.require_list() else {
            return false;
        };
        meta.tokens.to_string().contains('C')
    })
}

fn has_repr_int(attrs: &[Attribute]) -> bool {
    extract_repr_int(attrs).is_some()
}

fn extract_repr_int(attrs: &[Attribute]) -> Option<Primitive> {
    attrs.iter().find_map(|attr| {
        if !attr.path().is_ident("repr") {
            return None;
        }
        let idents = attr
            .parse_args_with(
                syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated,
            )
            .ok()?;
        idents
            .iter()
            .find_map(|ident| ident.to_string().parse().ok())
    })
}

fn module_path_for_source_file(
    source_root: &Path,
    file_path: &Path,
) -> Result<Vec<String>, String> {
    let relative_path = file_path.strip_prefix(source_root).map_err(|error| {
        format!(
            "Failed to resolve module path for {}: {}",
            file_path.display(),
            error
        )
    })?;
    let relative_components = relative_path
        .components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>();
    let (file_name, directory_components) = relative_components
        .split_last()
        .ok_or_else(|| format!("Failed to resolve module path for {}", file_path.display()))?;
    let mut module_path = directory_components.to_vec();
    let file_stem = Path::new(file_name)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .ok_or_else(|| format!("Failed to resolve module path for {}", file_path.display()))?;
    if !matches!(file_stem, "lib" | "main" | "mod") {
        module_path.push(file_stem.to_string());
    }
    Ok(module_path)
}

fn collect_integer_constant_candidates(
    items: &[Item],
    module_path: &[String],
    out: &mut Vec<(String, syn::Expr)>,
) {
    items.iter().for_each(|item| match item {
        Item::Const(item_const) => {
            let key = module_path
                .iter()
                .cloned()
                .chain(std::iter::once(item_const.ident.to_string()))
                .collect::<Vec<_>>()
                .join("::");
            out.push((key, (*item_const.expr).clone()));
        }
        Item::Mod(item_mod) => {
            if let Some((_, inner_items)) = &item_mod.content {
                let mut nested_path = module_path.to_vec();
                nested_path.push(item_mod.ident.to_string());
                collect_integer_constant_candidates(inner_items, &nested_path, out);
            }
        }
        _ => {}
    });
}

fn constant_parent_module_path(constant_key: &str) -> Vec<String> {
    let mut segments = constant_key
        .split("::")
        .map(|segment| segment.to_string())
        .collect::<Vec<_>>();
    segments.pop();
    segments
}

fn resolve_integer_constants(
    mut candidates: Vec<(String, syn::Expr)>,
    target_pointer_width_bits: Option<u8>,
) -> HashMap<String, i128> {
    let mut resolved = HashMap::<String, i128>::new();
    loop {
        let unresolved_before = candidates.len();
        candidates = candidates
            .into_iter()
            .filter_map(|(key, expr)| {
                if resolved.contains_key(&key) {
                    return None;
                }
                let current_module_path = constant_parent_module_path(&key);
                if let Some(value) = parse_discriminant_expr(
                    &expr,
                    &resolved,
                    current_module_path.as_slice(),
                    target_pointer_width_bits,
                ) {
                    resolved.insert(key, value);
                    None
                } else {
                    Some((key, expr))
                }
            })
            .collect();
        if candidates.is_empty() || candidates.len() == unresolved_before {
            break;
        }
    }
    resolved
}

fn collect_integer_constants(
    items: &[Item],
    module_path: &[String],
    target_pointer_width_bits: Option<u8>,
) -> HashMap<String, i128> {
    let mut candidates = Vec::<(String, syn::Expr)>::new();
    collect_integer_constant_candidates(items, module_path, &mut candidates);
    resolve_integer_constants(candidates, target_pointer_width_bits)
}

fn collect_integer_constants_from_files(
    source_root: &Path,
    files: &[PathBuf],
    target_pointer_width_bits: Option<u8>,
) -> Result<HashMap<String, i128>, String> {
    let mut candidates = Vec::<(String, syn::Expr)>::new();
    files.iter().try_for_each(|file_path| {
        let module_path = module_path_for_source_file(source_root, file_path)?;
        let content = fs::read_to_string(file_path)
            .map_err(|error| format!("Failed to read {}: {}", file_path.display(), error))?;
        let syntax = syn::parse_file(&content)
            .map_err(|error| format!("Failed to parse {}: {}", file_path.display(), error))?;
        collect_integer_constant_candidates(&syntax.items, module_path.as_slice(), &mut candidates);
        Ok::<(), String>(())
    })?;
    Ok(resolve_integer_constants(
        candidates,
        target_pointer_width_bits,
    ))
}

fn parse_discriminant_expr(
    expr: &syn::Expr,
    constants: &HashMap<String, i128>,
    current_module_path: &[String],
    target_pointer_width_bits: Option<u8>,
) -> Option<i128> {
    fn parse_integer_literal(literal: &syn::LitInt) -> Option<i128> {
        if let Ok(value) = literal.base10_parse::<i128>() {
            return Some(value);
        }
        literal
            .base10_parse::<u128>()
            .ok()
            .filter(|value| *value <= u64::MAX as u128)
            .map(|value| value as i128)
    }

    fn parse_negated_integer_literal(literal: &syn::LitInt) -> Option<i128> {
        let unsigned = literal.base10_parse::<u128>().ok()?;
        if unsigned == (i64::MAX as u128) + 1 {
            return Some(i64::MIN as i128);
        }
        i128::try_from(unsigned).ok()?.checked_neg()
    }

    fn cast_target_name(cast_type: &syn::Type) -> Option<String> {
        match cast_type {
            syn::Type::Path(type_path) if type_path.qself.is_none() => type_path
                .path
                .segments
                .last()
                .map(|segment| segment.ident.to_string()),
            syn::Type::Group(group) => cast_target_name(group.elem.as_ref()),
            syn::Type::Paren(paren) => cast_target_name(paren.elem.as_ref()),
            _ => None,
        }
    }

    fn apply_integer_cast(
        value: i128,
        cast_type: &syn::Type,
        target_pointer_width_bits: Option<u8>,
    ) -> Option<i128> {
        match cast_target_name(cast_type)?.as_str() {
            "i8" => Some((value as i8) as i128),
            "u8" => Some((value as u8) as i128),
            "i16" => Some((value as i16) as i128),
            "u16" => Some((value as u16) as i128),
            "i32" => Some((value as i32) as i128),
            "u32" => Some((value as u32) as i128),
            "i64" => Some((value as i64) as i128),
            "u64" => Some((value as u64) as i128),
            "isize" => match target_pointer_width_bits {
                Some(32) => Some((value as i32) as i128),
                Some(64) => Some((value as i64) as i128),
                _ => None,
            },
            "usize" => match target_pointer_width_bits {
                Some(32) => Some((value as u32) as i128),
                Some(64) => Some((value as u64) as i128),
                _ => None,
            },
            _ => None,
        }
    }

    fn path_key(path: &[String]) -> Option<String> {
        (!path.is_empty()).then(|| path.join("::"))
    }

    fn path_segments(path: &syn::Path) -> Vec<String> {
        path.segments
            .iter()
            .map(|segment| segment.ident.to_string())
            .collect::<Vec<_>>()
    }

    fn const_lookup_keys(path: &syn::Path, current_module_path: &[String]) -> Vec<String> {
        let segments = path_segments(path);
        if segments.is_empty() {
            return Vec::new();
        }
        if path.leading_colon.is_some() {
            return path_key(segments.as_slice()).into_iter().collect();
        }

        match segments.first().map(String::as_str) {
            Some("crate") => {
                let absolute_path = segments.into_iter().skip(1).collect::<Vec<_>>();
                path_key(absolute_path.as_slice()).into_iter().collect()
            }
            Some("self") => {
                let resolved_path = current_module_path
                    .iter()
                    .cloned()
                    .chain(segments.into_iter().skip(1))
                    .collect::<Vec<_>>();
                path_key(resolved_path.as_slice()).into_iter().collect()
            }
            Some("super") => {
                let super_depth = segments
                    .iter()
                    .take_while(|segment| segment.as_str() == "super")
                    .count();
                if super_depth > current_module_path.len() {
                    return Vec::new();
                }
                let mut resolved_path = current_module_path
                    .iter()
                    .take(current_module_path.len() - super_depth)
                    .cloned()
                    .collect::<Vec<_>>();
                resolved_path.extend(segments.iter().skip(super_depth).cloned());
                let mut lookup_keys = path_key(resolved_path.as_slice())
                    .into_iter()
                    .collect::<Vec<_>>();
                let fallback_path = segments.into_iter().skip(super_depth).collect::<Vec<_>>();
                if let Some(fallback_key) = path_key(fallback_path.as_slice())
                    && lookup_keys.first() != Some(&fallback_key)
                {
                    lookup_keys.push(fallback_key);
                }
                lookup_keys
            }
            _ => {
                let relative_path = current_module_path
                    .iter()
                    .cloned()
                    .chain(segments.iter().cloned())
                    .collect::<Vec<_>>();
                let mut lookup_keys = path_key(relative_path.as_slice())
                    .into_iter()
                    .collect::<Vec<_>>();
                if let Some(absolute_key) = path_key(segments.as_slice())
                    && lookup_keys.first() != Some(&absolute_key)
                {
                    lookup_keys.push(absolute_key);
                }
                lookup_keys
            }
        }
    }

    match expr {
        syn::Expr::Lit(lit) => {
            if let syn::Lit::Int(int_lit) = &lit.lit {
                parse_integer_literal(int_lit)
            } else {
                None
            }
        }
        syn::Expr::Unary(unary) => {
            if matches!(unary.op, syn::UnOp::Neg(_))
                && let syn::Expr::Lit(lit) = unary.expr.as_ref()
                && let syn::Lit::Int(int_lit) = &lit.lit
            {
                return parse_negated_integer_literal(int_lit);
            }
            parse_discriminant_expr(
                &unary.expr,
                constants,
                current_module_path,
                target_pointer_width_bits,
            )
            .and_then(|value| match unary.op {
                syn::UnOp::Neg(_) => value.checked_neg(),
                syn::UnOp::Not(_) => Some(!value),
                _ => None,
            })
        }
        syn::Expr::Binary(binary) => {
            let left = parse_discriminant_expr(
                &binary.left,
                constants,
                current_module_path,
                target_pointer_width_bits,
            )?;
            let right = parse_discriminant_expr(
                &binary.right,
                constants,
                current_module_path,
                target_pointer_width_bits,
            )?;
            match binary.op {
                syn::BinOp::Add(_) => left.checked_add(right),
                syn::BinOp::Sub(_) => left.checked_sub(right),
                syn::BinOp::Mul(_) => left.checked_mul(right),
                syn::BinOp::Div(_) => (right != 0).then(|| left / right),
                syn::BinOp::Rem(_) => (right != 0).then(|| left % right),
                syn::BinOp::BitXor(_) => Some(left ^ right),
                syn::BinOp::BitAnd(_) => Some(left & right),
                syn::BinOp::BitOr(_) => Some(left | right),
                syn::BinOp::Shl(_) => u32::try_from(right)
                    .ok()
                    .and_then(|shift| left.checked_shl(shift)),
                syn::BinOp::Shr(_) => u32::try_from(right)
                    .ok()
                    .and_then(|shift| left.checked_shr(shift)),
                _ => None,
            }
        }
        syn::Expr::Paren(paren) => parse_discriminant_expr(
            &paren.expr,
            constants,
            current_module_path,
            target_pointer_width_bits,
        ),
        syn::Expr::Group(group) => parse_discriminant_expr(
            &group.expr,
            constants,
            current_module_path,
            target_pointer_width_bits,
        ),
        syn::Expr::Path(path) => const_lookup_keys(&path.path, current_module_path)
            .into_iter()
            .find_map(|key| constants.get(&key).copied()),
        syn::Expr::Cast(cast) => parse_discriminant_expr(
            &cast.expr,
            constants,
            current_module_path,
            target_pointer_width_bits,
        )
        .and_then(|value| apply_integer_cast(value, cast.ty.as_ref(), target_pointer_width_bits)),
        _ => None,
    }
}

fn parse_target_pointer_width() -> Option<u8> {
    std::env::var("BOLTFFI_TARGET_POINTER_WIDTH")
        .ok()
        .or_else(|| std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").ok())
        .and_then(|value| value.parse::<u8>().ok())
        .filter(|width| matches!(width, 32 | 64))
}

fn has_ffi_type_derive(attrs: &[Attribute]) -> bool {
    attrs.iter().any(|attr| {
        if !attr.path().is_ident("derive") {
            return false;
        }
        let Ok(meta) = attr.meta.require_list() else {
            return false;
        };
        meta.tokens.to_string().contains("FfiType")
    })
}

fn extract_stream_attr(
    attrs: &[Attribute],
    registry: &TypeRegistry,
    alias_resolver: &AliasResolver,
    compiler_canonical_types: &HashMap<String, String>,
) -> Option<(MType, StreamMode)> {
    for attr in attrs {
        if !attr.path().is_ident("ffi_stream") {
            continue;
        }

        let Ok(meta) = attr.meta.require_list() else {
            continue;
        };

        let tokens = meta.tokens.to_string();
        let item_type =
            extract_item_type(&tokens, registry, alias_resolver, compiler_canonical_types)?;
        let mode = extract_stream_mode(&tokens);

        return Some((item_type, mode));
    }
    None
}

fn extract_item_type(
    tokens: &str,
    registry: &TypeRegistry,
    alias_resolver: &AliasResolver,
    compiler_canonical_types: &HashMap<String, String>,
) -> Option<MType> {
    let item_start = tokens.find("item")? + 4;
    let rest = &tokens[item_start..];
    let eq_pos = rest.find('=')?;
    let after_eq = rest[eq_pos + 1..].trim();

    let type_end = after_eq
        .find(',')
        .unwrap_or(after_eq.find(')').unwrap_or(after_eq.len()));
    let type_str = after_eq[..type_end].trim();

    string_to_ffi_type(type_str, registry, alias_resolver, compiler_canonical_types)
}

fn extract_stream_mode(tokens: &str) -> StreamMode {
    if tokens.contains("mode") {
        if tokens.contains("\"batch\"") {
            StreamMode::Batch
        } else if tokens.contains("\"callback\"") {
            StreamMode::Callback
        } else {
            StreamMode::Async
        }
    } else {
        StreamMode::Async
    }
}

fn rust_type_to_ffi_type(
    ty: &Type,
    registry: &TypeRegistry,
    alias_resolver: &AliasResolver,
    compiler_canonical_types: &HashMap<String, String>,
    self_type_name: Option<&str>,
) -> Option<MType> {
    if let Some(custom_type) = registry.classify_custom_remote_type(ty) {
        return Some(custom_type);
    }

    match ty {
        Type::Path(type_path) => {
            let last_segment = type_path.path.segments.last()?;
            let ident = last_segment.ident.to_string();

            if ident == "Self" {
                return self_type_name.and_then(|name| {
                    registry
                        .classify_named_type(name)
                        .or_else(|| Some(MType::Object(name.to_string())))
                });
            }

            if ident == "Box"
                && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
                && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
                    args.args.first()
                && let Some(syn::TypeParamBound::Trait(trait_bound)) = trait_obj.bounds.first()
                && let Some(seg) = trait_bound.path.segments.last()
            {
                return Some(MType::BoxedTrait(seg.ident.to_string()));
            }

            if ident == "Arc"
                && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
                && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
                    args.args.first()
                && let Some(syn::TypeParamBound::Trait(trait_bound)) = trait_obj.bounds.first()
                && let Some(seg) = trait_bound.path.segments.last()
            {
                return Some(MType::BoxedTrait(seg.ident.to_string()));
            }

            if (ident == "Arc" || ident == "Box")
                && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
                && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
            {
                return rust_type_to_ffi_type(
                    inner_ty,
                    registry,
                    alias_resolver,
                    compiler_canonical_types,
                    self_type_name,
                );
            }

            if ident == "Vec" {
                if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
                    && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
                {
                    let inner = rust_type_to_ffi_type(
                        inner_ty,
                        registry,
                        alias_resolver,
                        compiler_canonical_types,
                        self_type_name,
                    )?;
                    return Some(MType::Vec(Box::new(inner)));
                }
                return None;
            }

            if ident == "Option" {
                if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
                    && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
                {
                    let inner = rust_type_to_ffi_type(
                        inner_ty,
                        registry,
                        alias_resolver,
                        compiler_canonical_types,
                        self_type_name,
                    )?;
                    return Some(MType::Option(Box::new(inner)));
                }
                return None;
            }

            if ident == "Result" {
                if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
                    let mut args_iter = args.args.iter();
                    if let Some(syn::GenericArgument::Type(ok_ty)) = args_iter.next() {
                        let ok = rust_type_to_ffi_type(
                            ok_ty,
                            registry,
                            alias_resolver,
                            compiler_canonical_types,
                            self_type_name,
                        )?;
                        let err = args_iter
                            .next()
                            .and_then(|arg| {
                                if let syn::GenericArgument::Type(err_ty) = arg {
                                    rust_type_to_ffi_type(
                                        err_ty,
                                        registry,
                                        alias_resolver,
                                        compiler_canonical_types,
                                        self_type_name,
                                    )
                                } else {
                                    None
                                }
                            })
                            .unwrap_or(MType::String);
                        return Some(MType::Result {
                            ok: Box::new(ok),
                            err: Box::new(err),
                        });
                    }
                }
                return None;
            }

            let path_str = type_path
                .path
                .segments
                .iter()
                .map(|s| s.ident.to_string())
                .collect::<Vec<_>>()
                .join("::");

            string_to_ffi_type(
                &path_str,
                registry,
                alias_resolver,
                compiler_canonical_types,
            )
        }
        Type::Reference(type_ref) => {
            if let Type::Path(inner) = &*type_ref.elem {
                let ident = inner.path.segments.last()?.ident.to_string();
                if ident == "str" {
                    return Some(MType::String);
                }
            }
            if let Type::Slice(slice) = &*type_ref.elem {
                let inner = rust_type_to_ffi_type(
                    &slice.elem,
                    registry,
                    alias_resolver,
                    compiler_canonical_types,
                    self_type_name,
                )?;
                return if type_ref.mutability.is_some() {
                    Some(MType::MutSlice(Box::new(inner)))
                } else {
                    Some(MType::Slice(Box::new(inner)))
                };
            }
            rust_type_to_ffi_type(
                &type_ref.elem,
                registry,
                alias_resolver,
                compiler_canonical_types,
                self_type_name,
            )
        }
        Type::Slice(slice) => {
            let inner = rust_type_to_ffi_type(
                &slice.elem,
                registry,
                alias_resolver,
                compiler_canonical_types,
                self_type_name,
            )?;
            Some(MType::Slice(Box::new(inner)))
        }
        Type::ImplTrait(impl_trait) => {
            for bound in &impl_trait.bounds {
                if let syn::TypeParamBound::Trait(trait_bound) = bound {
                    let trait_name = trait_bound
                        .path
                        .segments
                        .last()
                        .map(|s| s.ident.to_string())?;

                    if (trait_name == "FnMut" || trait_name == "Fn" || trait_name == "FnOnce")
                        && let syn::PathArguments::Parenthesized(args) =
                            &trait_bound.path.segments.last()?.arguments
                    {
                        let params: Vec<MType> = args
                            .inputs
                            .iter()
                            .filter_map(|t| {
                                rust_type_to_ffi_type(
                                    t,
                                    registry,
                                    alias_resolver,
                                    compiler_canonical_types,
                                    self_type_name,
                                )
                            })
                            .collect();

                        let returns = match &args.output {
                            syn::ReturnType::Default => MType::Void,
                            syn::ReturnType::Type(_, ty) => rust_type_to_ffi_type(
                                ty,
                                registry,
                                alias_resolver,
                                compiler_canonical_types,
                                self_type_name,
                            )
                            .unwrap_or(MType::Void),
                        };

                        return Some(MType::Closure(MClosureSignature::new(params, returns)));
                    }

                    if registry.has_callback(&trait_name) {
                        return Some(MType::BoxedTrait(trait_name));
                    }
                }
            }
            None
        }
        Type::TraitObject(trait_obj) => {
            if let Some(syn::TypeParamBound::Trait(trait_bound)) = trait_obj.bounds.first()
                && let Some(seg) = trait_bound.path.segments.last()
            {
                return Some(MType::BoxedTrait(seg.ident.to_string()));
            }
            None
        }
        _ => None,
    }
}

fn string_to_ffi_type(
    s: &str,
    registry: &TypeRegistry,
    alias_resolver: &AliasResolver,
    compiler_canonical_types: &HashMap<String, String>,
) -> Option<MType> {
    let trimmed = s.trim();
    match trimmed {
        "i8" => Some(MType::Primitive(Primitive::I8)),
        "i16" => Some(MType::Primitive(Primitive::I16)),
        "i32" => Some(MType::Primitive(Primitive::I32)),
        "i64" => Some(MType::Primitive(Primitive::I64)),
        "u8" => Some(MType::Primitive(Primitive::U8)),
        "u16" => Some(MType::Primitive(Primitive::U16)),
        "u32" => Some(MType::Primitive(Primitive::U32)),
        "u64" => Some(MType::Primitive(Primitive::U64)),
        "f32" => Some(MType::Primitive(Primitive::F32)),
        "f64" => Some(MType::Primitive(Primitive::F64)),
        "bool" => Some(MType::Primitive(Primitive::Bool)),
        "usize" => Some(MType::Primitive(Primitive::USize)),
        "isize" => Some(MType::Primitive(Primitive::ISize)),
        "String" | "str" | "std::string::String" | "alloc::string::String" => Some(MType::String),
        s if s.starts_with("Vec<") => {
            let inner = &s[4..s.len() - 1];
            Some(MType::Vec(Box::new(string_to_ffi_type(
                inner,
                registry,
                alias_resolver,
                compiler_canonical_types,
            )?)))
        }
        s if s.starts_with("Option<") => {
            let inner = &s[7..s.len() - 1];
            Some(MType::Option(Box::new(string_to_ffi_type(
                inner,
                registry,
                alias_resolver,
                compiler_canonical_types,
            )?)))
        }
        s if s.starts_with("Result<") => {
            let inner = &s[7..s.len() - 1];
            let parts: Vec<&str> = inner.splitn(2, ',').map(|p| p.trim()).collect();
            let ok = string_to_ffi_type(
                parts.first()?,
                registry,
                alias_resolver,
                compiler_canonical_types,
            )?;
            let err = parts
                .get(1)
                .and_then(|e| {
                    string_to_ffi_type(e, registry, alias_resolver, compiler_canonical_types)
                })
                .unwrap_or(MType::String);
            Some(MType::Result {
                ok: Box::new(ok),
                err: Box::new(err),
            })
        }
        s => {
            if let Some(ty) = registry.classify_type_spelling(s) {
                return Some(ty);
            }

            let resolved = alias_resolver.resolve_type_spelling(s);
            let canonical = compiler_canonical_types
                .get(resolved.as_ref())
                .map(String::as_str)
                .unwrap_or(resolved.as_ref());

            BuiltinId::from_rust_path(canonical)
                .map(MType::Builtin)
                .or_else(|| registry.classify_type_spelling(canonical))
                .or_else(|| {
                    canonical
                        .rsplit("::")
                        .next()
                        .and_then(|name| registry.classify_type_spelling(name))
                })
        }
    }
}

fn validate_no_symbol_collisions(module: &Module) -> Result<(), String> {
    let mut symbols: HashMap<String, String> = HashMap::new();

    let mut check = |symbol: String, origin: String| -> Result<(), String> {
        if let Some(existing) = symbols.get(&symbol) {
            return Err(format!(
                "FFI symbol collision: '{}' is produced by both {} and {}. Rename one to avoid the conflict.",
                symbol, existing, origin
            ));
        }
        symbols.insert(symbol, origin);
        Ok(())
    };

    for func in &module.functions {
        let symbol = naming::function_ffi_name(&func.name).to_string();
        check(symbol, format!("fn {}()", func.name))?;
    }

    for record in &module.records {
        for ctor in &record.constructors {
            let symbol = if ctor.name == "new" {
                naming::class_ffi_new(&record.name).to_string()
            } else {
                naming::method_ffi_name(&record.name, &ctor.name).to_string()
            };
            check(symbol, format!("{}::{}()", record.name, ctor.name))?;
        }
        for method in &record.methods {
            let symbol = naming::method_ffi_name(&record.name, &method.name).to_string();
            check(symbol, format!("{}::{}()", record.name, method.name))?;
        }
    }

    for enumeration in &module.enums {
        for ctor in &enumeration.constructors {
            let symbol = if ctor.name == "new" {
                naming::class_ffi_new(&enumeration.name).to_string()
            } else {
                naming::method_ffi_name(&enumeration.name, &ctor.name).to_string()
            };
            check(symbol, format!("{}::{}()", enumeration.name, ctor.name))?;
        }
        for method in &enumeration.methods {
            let symbol = naming::method_ffi_name(&enumeration.name, &method.name).to_string();
            check(symbol, format!("{}::{}()", enumeration.name, method.name))?;
        }
    }

    for class in &module.classes {
        for ctor in &class.constructors {
            let symbol = if ctor.name == "new" {
                naming::class_ffi_new(&class.name).to_string()
            } else {
                naming::method_ffi_name(&class.name, &ctor.name).to_string()
            };
            check(symbol, format!("{}::{}()", class.name, ctor.name))?;
        }
        for method in &class.methods {
            let symbol = naming::method_ffi_name(&class.name, &method.name).to_string();
            check(symbol, format!("{}::{}()", class.name, method.name))?;
        }
    }

    Ok(())
}

pub fn scan_crate(crate_path: &Path, module_name: &str) -> Result<Module, String> {
    scan_crate_with_pointer_width(crate_path, module_name, None)
}

pub fn scan_crate_with_pointer_width(
    crate_path: &Path,
    module_name: &str,
    target_pointer_width_bits: Option<u8>,
) -> Result<Module, String> {
    let src_path = crate_path.join("src");
    let mut scanner = SourceScanner::new_with_pointer_width(
        module_name,
        target_pointer_width_bits.or_else(parse_target_pointer_width),
    );
    scanner.scan_directory(crate_path, &src_path)?;
    let module = scanner.into_module();
    validate_no_symbol_collisions(&module)?;
    Ok(module)
}

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

    #[test]
    fn extract_doc_from_single_line() {
        let source: syn::File = syn::parse_quote! {
            /// A point in 2D space.
            struct Point;
        };
        let attrs = match &source.items[0] {
            Item::Struct(s) => &s.attrs,
            _ => panic!("expected struct"),
        };
        assert_eq!(
            extract_doc_string(attrs),
            Some("A point in 2D space.".to_string())
        );
    }

    #[test]
    fn extract_doc_from_multiple_lines() {
        let source: syn::File = syn::parse_quote! {
            /// First line.
            /// Second line.
            /// Third line.
            struct Widget;
        };
        let attrs = match &source.items[0] {
            Item::Struct(s) => &s.attrs,
            _ => panic!("expected struct"),
        };
        assert_eq!(
            extract_doc_string(attrs),
            Some("First line.\nSecond line.\nThird line.".to_string())
        );
    }

    #[test]
    fn extract_doc_returns_none_for_undocumented() {
        let source: syn::File = syn::parse_quote! {
            struct Bare;
        };
        let attrs = match &source.items[0] {
            Item::Struct(s) => &s.attrs,
            _ => panic!("expected struct"),
        };
        assert_eq!(extract_doc_string(attrs), None);
    }

    #[test]
    fn extract_doc_trims_empty_lines() {
        let source: syn::File = syn::parse_quote! {
            ///
            /// Actual content.
            ///
            struct Padded;
        };
        let attrs = match &source.items[0] {
            Item::Struct(s) => &s.attrs,
            _ => panic!("expected struct"),
        };
        let doc = extract_doc_string(attrs).unwrap();
        assert_eq!(doc, "Actual content.");
    }

    #[test]
    fn alias_resolution_stops_at_cycle_without_falling_back() {
        let resolver = AliasResolver {
            use_aliases: HashMap::from([
                ("Foo".to_string(), vec!["Bar".to_string()]),
                ("Bar".to_string(), vec!["Foo".to_string()]),
            ]),
            type_aliases: HashMap::new(),
        };

        assert_eq!(
            resolver.resolve_segments(vec!["Foo".to_string()]),
            vec!["Bar".to_string()]
        );
    }

    fn pending(kind: PendingKind) -> TypeMeta {
        TypeMeta {
            doc: None,
            shape: TypeShape::Pending(kind),
        }
    }

    fn pending_with_doc(kind: PendingKind, doc: &str) -> TypeMeta {
        TypeMeta {
            doc: Some(doc.to_string()),
            shape: TypeShape::Pending(kind),
        }
    }

    #[test]
    fn type_registry_single_map_classify() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));
        reg.register("Color".into(), pending(PendingKind::Enum));
        reg.register("Sensor".into(), pending(PendingKind::Class));
        reg.register(
            "UtcDateTime".into(),
            TypeMeta {
                doc: None,
                shape: TypeShape::Custom {
                    repr: MType::Primitive(Primitive::I64),
                },
            },
        );

        assert!(matches!(
            reg.classify_named_type("Point"),
            Some(MType::Record(_))
        ));
        assert!(matches!(
            reg.classify_named_type("Color"),
            Some(MType::Enum(_))
        ));
        assert!(matches!(
            reg.classify_named_type("Sensor"),
            Some(MType::Object(_))
        ));
        assert!(matches!(
            reg.classify_named_type("UtcDateTime"),
            Some(MType::Custom { .. })
        ));
        assert!(reg.classify_named_type("Unknown").is_none());
    }

    #[test]
    fn type_registry_is_enum() {
        let mut reg = TypeRegistry::default();
        reg.register("Status".into(), pending(PendingKind::Enum));
        reg.register("Point".into(), pending(PendingKind::Record));

        assert!(reg.is_enum("Status"));
        assert!(!reg.is_enum("Point"));
        assert!(!reg.is_enum("Missing"));
    }

    #[test]
    fn type_registry_contains() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));

        assert!(reg.contains("Point"));
        assert!(!reg.contains("Nope"));
    }

    #[test]
    fn type_registry_doc_at_registration() {
        let mut reg = TypeRegistry::default();
        reg.register(
            "Sensor".into(),
            pending_with_doc(PendingKind::Class, "A hardware sensor."),
        );

        assert_eq!(reg.doc("Sensor"), Some("A hardware sensor."));
    }

    #[test]
    fn type_registry_set_doc_after_registration() {
        let mut reg = TypeRegistry::default();
        reg.register("Sensor".into(), pending(PendingKind::Class));
        reg.set_doc("Sensor", "A hardware sensor.".into());

        assert_eq!(reg.doc("Sensor"), Some("A hardware sensor."));
    }

    #[test]
    fn type_registry_set_doc_ignores_unregistered() {
        let mut reg = TypeRegistry::default();
        reg.set_doc("Ghost", "spooky".into());

        assert!(reg.doc("Ghost").is_none());
    }

    #[test]
    fn type_registry_custom_type_classifies_correctly() {
        let mut reg = TypeRegistry::default();
        reg.register(
            "Timestamp".into(),
            TypeMeta {
                doc: None,
                shape: TypeShape::Custom {
                    repr: MType::Primitive(Primitive::I64),
                },
            },
        );

        assert!(matches!(
            reg.classify_named_type("Timestamp"),
            Some(MType::Custom { .. })
        ));
        assert!(!reg.is_enum("Timestamp"));
    }

    #[test]
    fn type_registry_custom_type_classifies_remote_generic_type() {
        let mut reg = TypeRegistry::default();
        reg.register(
            "UtcDateTime".into(),
            TypeMeta {
                doc: None,
                shape: TypeShape::Custom {
                    repr: MType::Primitive(Primitive::I64),
                },
            },
        );
        reg.register_custom_remote_type("UtcDateTime", &syn::parse_quote!(DateTime<Utc>))
            .unwrap();

        assert!(matches!(
            reg.classify_type_spelling("DateTime<Utc>"),
            Some(MType::Custom { .. })
        ));
        assert!(matches!(
            reg.classify_type_spelling("chrono::DateTime<chrono::Utc>"),
            Some(MType::Custom { .. })
        ));
    }

    #[test]
    fn scan_demo_crate_includes_datetime_custom_type_exports() {
        let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap()
            .to_path_buf();
        let demo_crate_path = repo_root.join("examples").join("demo");
        let module = scan_crate(&demo_crate_path, "demo").unwrap();

        assert!(
            module
                .functions
                .iter()
                .any(|function| function.name == "echo_datetime"),
            "expected echo_datetime to be exported"
        );
        assert!(
            module
                .functions
                .iter()
                .any(|function| function.name == "datetime_to_millis"),
            "expected datetime_to_millis to be exported"
        );
    }

    #[test]
    fn scan_demo_crate_preserves_callback_return_type() {
        let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap()
            .to_path_buf();
        let demo_crate_path = repo_root.join("examples").join("demo");
        let module = scan_crate(&demo_crate_path, "demo").unwrap();

        let function = module
            .functions
            .iter()
            .find(|function| function.name == "make_incrementing_callback")
            .expect("expected make_incrementing_callback to be exported");

        assert!(matches!(
            function.returns,
            crate::model::ReturnType::Value(crate::model::Type::BoxedTrait(ref name))
                if name == "ValueCallback"
        ));
    }

    #[test]
    fn type_registry_fill_replaces_pending() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));
        reg.fill(
            "Point",
            TypeShape::Record {
                fields: vec![RecordField::new("x", MType::Primitive(Primitive::F64))],
                is_repr_c: true,
                is_error: false,
                constructors: Vec::new(),
                methods: Vec::new(),
            },
        );

        assert!(matches!(
            reg.classify_named_type("Point"),
            Some(MType::Record(_))
        ));
        assert!(matches!(
            reg.types.get("Point").unwrap().shape,
            TypeShape::Record { ref fields, .. } if fields.len() == 1
        ));
    }

    #[test]
    fn type_registry_filled_enum_still_is_enum() {
        let mut reg = TypeRegistry::default();
        reg.register("Color".into(), pending(PendingKind::Enum));
        reg.fill(
            "Color",
            TypeShape::Enum {
                variants: vec![],
                is_error: false,
                repr_type: None,
                constructors: vec![],
                methods: vec![],
            },
        );

        assert!(reg.is_enum("Color"));
    }

    #[test]
    fn merge_record_impl_upgrades_pending_to_filled_shape() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));

        let ctor = Constructor::new().with_name("origin");
        let method = Method::new("magnitude", Receiver::Ref);
        reg.merge_record_impl("Point", vec![ctor], vec![method]);

        match &reg.types.get("Point").unwrap().shape {
            TypeShape::Record {
                fields,
                constructors,
                methods,
                ..
            } => {
                assert!(fields.is_empty());
                assert_eq!(constructors.len(), 1);
                assert_eq!(constructors[0].name, "origin");
                assert_eq!(methods.len(), 1);
                assert_eq!(methods[0].name, "magnitude");
            }
            other => panic!("expected Record, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn fill_record_fields_preserves_existing_methods() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));

        let ctor = Constructor::new().with_name("origin");
        let method = Method::new("magnitude", Receiver::Ref);
        reg.merge_record_impl("Point", vec![ctor], vec![method]);

        reg.fill_record_fields(
            "Point",
            vec![RecordField::new("x", MType::Primitive(Primitive::F64))],
            true,
            false,
        );

        match &reg.types.get("Point").unwrap().shape {
            TypeShape::Record {
                fields,
                is_repr_c,
                constructors,
                methods,
                ..
            } => {
                assert_eq!(fields.len(), 1);
                assert_eq!(fields[0].name, "x");
                assert!(*is_repr_c);
                assert_eq!(constructors.len(), 1);
                assert_eq!(methods.len(), 1);
            }
            other => panic!("expected Record, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn fill_record_fields_creates_fresh_shape_from_pending() {
        let mut reg = TypeRegistry::default();
        reg.register("Point".into(), pending(PendingKind::Record));

        reg.fill_record_fields(
            "Point",
            vec![RecordField::new("x", MType::Primitive(Primitive::F64))],
            false,
            false,
        );

        match &reg.types.get("Point").unwrap().shape {
            TypeShape::Record {
                fields,
                is_repr_c,
                constructors,
                methods,
                ..
            } => {
                assert_eq!(fields.len(), 1);
                assert!(!*is_repr_c);
                assert!(constructors.is_empty());
                assert!(methods.is_empty());
            }
            other => panic!("expected Record, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn extract_doc_from_struct_field() {
        let source: syn::File = syn::parse_quote! {
            struct Location {
                /// Unique identifier for this location.
                id: i64,
                lat: f64,
            }
        };
        let fields = match &source.items[0] {
            Item::Struct(s) => match &s.fields {
                Fields::Named(named) => &named.named,
                _ => panic!("expected named fields"),
            },
            _ => panic!("expected struct"),
        };
        assert_eq!(
            extract_doc_string(&fields[0].attrs),
            Some("Unique identifier for this location.".to_string())
        );
        assert_eq!(extract_doc_string(&fields[1].attrs), None);
    }

    #[test]
    fn extract_doc_from_enum_variant() {
        let source: syn::File = syn::parse_quote! {
            enum Direction {
                /// Pointing north.
                North,
                South,
            }
        };
        let variants = match &source.items[0] {
            Item::Enum(e) => &e.variants,
            _ => panic!("expected enum"),
        };
        assert_eq!(
            extract_doc_string(&variants[0].attrs),
            Some("Pointing north.".to_string())
        );
        assert_eq!(extract_doc_string(&variants[1].attrs), None);
    }

    #[test]
    fn extract_doc_from_enum_variant_field() {
        let source: syn::File = syn::parse_quote! {
            enum Status {
                Failed {
                    /// Error code describing the failure.
                    error_code: i32,
                    retry_count: i32,
                },
            }
        };
        let fields = match &source.items[0] {
            Item::Enum(e) => match &e.variants[0].fields {
                Fields::Named(named) => &named.named,
                _ => panic!("expected named fields"),
            },
            _ => panic!("expected enum"),
        };
        assert_eq!(
            extract_doc_string(&fields[0].attrs),
            Some("Error code describing the failure.".to_string())
        );
        assert_eq!(extract_doc_string(&fields[1].attrs), None);
    }

    #[test]
    fn parse_discriminant_expr_handles_const_refs_and_bit_ops() {
        let source: syn::File = syn::parse_quote! {
            const FLAG: i64 = 1 << 4;
            const MASK: i64 = FLAG | 0b11;
        };
        let constants = collect_integer_constants(&source.items, &[], None);
        let expr: syn::Expr = syn::parse_quote!(MASK + 1);

        assert_eq!(
            parse_discriminant_expr(&expr, &constants, &[], None),
            Some(20)
        );
    }

    #[test]
    fn discriminant_progression_tracks_explicit_expr_values() {
        let source: syn::File = syn::parse_quote! {
            const START: i64 = 10;
            enum Mode {
                A = START,
                B,
                C = 1 << 3,
                D,
            }
        };
        let constants = collect_integer_constants(&source.items, &[], None);
        let item_enum = match &source.items[1] {
            Item::Enum(item_enum) => item_enum,
            _ => panic!("expected enum"),
        };

        let discriminants = item_enum
            .variants
            .iter()
            .scan(0_i128, |next_discriminant, variant| {
                let value = match variant.discriminant.as_ref() {
                    Some((_, expr)) => parse_discriminant_expr(expr, &constants, &[], None),
                    None => Some(*next_discriminant),
                }?;
                *next_discriminant = value + 1;
                Some(value)
            })
            .collect::<Vec<_>>();

        assert_eq!(discriminants, vec![10, 11, 8, 9]);
    }

    #[test]
    fn collect_integer_constants_resolves_forward_references() {
        let source: syn::File = syn::parse_quote! {
            const A: i64 = B;
            const B: i64 = 3;
        };
        let constants = collect_integer_constants(&source.items, &[], None);

        assert_eq!(constants.get("A").copied(), Some(3));
        assert_eq!(constants.get("B").copied(), Some(3));
    }

    #[test]
    fn parse_discriminant_expr_prefers_full_qualified_const_path() {
        let constants = HashMap::from([
            ("BAR".to_string(), 1_i128),
            ("foo::BAR".to_string(), 9_i128),
        ]);

        let qualified: syn::Expr = syn::parse_quote!(foo::BAR);
        let unqualified: syn::Expr = syn::parse_quote!(BAR);
        let crate_qualified: syn::Expr = syn::parse_quote!(crate::foo::BAR);

        assert_eq!(
            parse_discriminant_expr(&qualified, &constants, &[], None),
            Some(9)
        );
        assert_eq!(
            parse_discriminant_expr(&unqualified, &constants, &[], None),
            Some(1)
        );
        assert_eq!(
            parse_discriminant_expr(&crate_qualified, &constants, &[], None),
            Some(9)
        );
    }

    #[test]
    fn parse_discriminant_expr_resolves_super_path_from_current_module() {
        let constants = HashMap::from([
            ("foo::FLAG".to_string(), 9_i128),
            ("foo::bar::FLAG".to_string(), 1_i128),
        ]);
        let expression: syn::Expr = syn::parse_quote!(super::FLAG);
        let current_module_path = vec!["foo".to_string(), "bar".to_string()];

        assert_eq!(
            parse_discriminant_expr(
                &expression,
                &constants,
                current_module_path.as_slice(),
                None
            ),
            Some(9)
        );
    }

    #[test]
    fn parse_discriminant_expr_applies_integer_cast_semantics() {
        let expression: syn::Expr = syn::parse_quote!((-1i16 as u8));

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], None),
            Some(255)
        );
    }

    #[test]
    fn parse_discriminant_expr_accepts_u64_max_literal() {
        let expression: syn::Expr = syn::parse_quote!(18446744073709551615);

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], None),
            Some(u64::MAX as i128)
        );
    }

    #[test]
    fn parse_discriminant_expr_accepts_i64_min_literal_form() {
        let expression: syn::Expr = syn::parse_quote!(-9223372036854775808);

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], None),
            Some(i64::MIN as i128)
        );
    }

    #[test]
    fn parse_discriminant_expr_truncates_i64_cast() {
        let expression: syn::Expr = syn::parse_quote!(18446744073709551615 as i64);

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], None),
            Some(-1)
        );
    }

    #[test]
    fn parse_discriminant_expr_requires_pointer_width_for_usize_casts() {
        let expression: syn::Expr = syn::parse_quote!(7 as usize);

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], None),
            None
        );
    }

    #[test]
    fn parse_discriminant_expr_applies_target_pointer_width_to_usize_casts() {
        let expression: syn::Expr = syn::parse_quote!(4294967297 as usize);

        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], Some(32)),
            Some(1)
        );
        assert_eq!(
            parse_discriminant_expr(&expression, &HashMap::new(), &[], Some(64)),
            Some(4294967297)
        );
    }

    #[test]
    fn collect_integer_constants_from_files_collects_module_scoped_constants() {
        let unique_suffix = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let temp_root = std::env::temp_dir().join(format!(
            "boltffi_scan_constants_{}_{}",
            std::process::id(),
            unique_suffix
        ));
        let source_root = temp_root.join("src");

        fs::create_dir_all(&source_root).expect("create source root");
        fs::write(source_root.join("lib.rs"), "pub mod constants;").expect("write lib.rs");
        fs::write(source_root.join("constants.rs"), "pub const TAG: i64 = 7;")
            .expect("write constants.rs");

        let files = vec![source_root.join("lib.rs"), source_root.join("constants.rs")];
        let constants = collect_integer_constants_from_files(&source_root, files.as_slice(), None)
            .expect("collect constants");
        let expression: syn::Expr = syn::parse_quote!(crate::constants::TAG);

        assert_eq!(constants.get("constants::TAG").copied(), Some(7));
        assert_eq!(
            parse_discriminant_expr(&expression, &constants, &[], None),
            Some(7)
        );

        fs::remove_dir_all(temp_root).expect("cleanup temp root");
    }

    fn scan_temp_crate(source: &str) -> Module {
        let unique_suffix = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let temp_root = std::env::temp_dir().join(format!(
            "boltffi_scan_record_methods_{}_{}",
            std::process::id(),
            unique_suffix
        ));
        let src_dir = temp_root.join("src");
        fs::create_dir_all(&src_dir).expect("create src dir");
        fs::write(src_dir.join("lib.rs"), source).expect("write lib.rs");

        let module =
            scan_crate_with_pointer_width(&temp_root, "testlib", None).expect("scan failed");
        fs::remove_dir_all(&temp_root).expect("cleanup");
        module
    }

    fn scan_temp_crate_multi(files: &[(&str, &str)]) -> Module {
        let unique_suffix = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let temp_root = std::env::temp_dir().join(format!(
            "boltffi_scan_record_methods_{}_{}",
            std::process::id(),
            unique_suffix
        ));
        let src_dir = temp_root.join("src");
        fs::create_dir_all(&src_dir).expect("create src dir");

        files.iter().for_each(|(name, content)| {
            fs::write(src_dir.join(name), content).expect("write file");
        });

        let module =
            scan_crate_with_pointer_width(&temp_root, "testlib", None).expect("scan failed");
        fs::remove_dir_all(&temp_root).expect("cleanup");
        module
    }

    #[test]
    fn error_structs_are_scanned_as_records() {
        let source = r#"
            use boltffi::*;

            #[error]
            pub struct AppError {
                pub code: i32,
                pub message: String,
            }

            #[export]
            pub fn may_fail(valid: bool) -> Result<String, AppError> {
                if valid {
                    Ok("ok".to_string())
                } else {
                    Err(AppError {
                        code: 400,
                        message: "bad".to_string(),
                    })
                }
            }
        "#;

        let module = scan_temp_crate(source);
        let error_record = module
            .records
            .iter()
            .find(|record| record.name == "AppError")
            .expect("AppError should be scanned as a record");

        assert!(error_record.is_error);
        assert_eq!(error_record.fields.len(), 2);
        assert_eq!(module.functions.len(), 1);
    }

    #[test]
    fn record_impl_scanned() {
        let source = r#"
            #[boltffi::data]
            pub struct Point {
                pub x: f64,
                pub y: f64,
            }

            #[boltffi::data(impl)]
            impl Point {
                pub fn origin() -> Self {
                    Self { x: 0.0, y: 0.0 }
                }

                pub fn magnitude(&self) -> f64 {
                    (self.x * self.x + self.y * self.y).sqrt()
                }
            }
        "#;

        let module = scan_temp_crate(source);

        let record = module.find_record("Point").expect("Point not found");
        assert_eq!(record.fields.len(), 2);
        assert_eq!(record.constructors.len(), 1);
        assert_eq!(record.constructors[0].name, "origin");
        assert_eq!(record.methods.len(), 1);
        assert_eq!(record.methods[0].name, "magnitude");
    }

    #[test]
    fn record_impl_cross_file_impl_before_struct() {
        let module = scan_temp_crate_multi(&[
            ("lib.rs", "pub mod record_impl;\npub mod record_struct;\n"),
            (
                "record_impl.rs",
                r#"
                        use super::record_struct::Point;

                        #[boltffi::data(impl)]
                        impl Point {
                            pub fn origin() -> Self {
                                Self { x: 0.0, y: 0.0 }
                            }

                            pub fn magnitude(&self) -> f64 {
                                (self.x * self.x + self.y * self.y).sqrt()
                            }
                        }
                    "#,
            ),
            (
                "record_struct.rs",
                r#"
                        #[boltffi::data]
                        pub struct Point {
                            pub x: f64,
                            pub y: f64,
                        }
                    "#,
            ),
        ]);

        let record = module.find_record("Point").expect("Point not found");
        assert_eq!(record.fields.len(), 2);
        assert_eq!(record.constructors.len(), 1);
        assert_eq!(record.constructors[0].name, "origin");
        assert_eq!(record.methods.len(), 1);
        assert_eq!(record.methods[0].name, "magnitude");
    }

    #[test]
    fn symbol_collision_between_method_and_function_is_detected() {
        let mut module = Module::new("test");
        module.functions.push(Function::new("point_distance"));
        module
            .records
            .push(Record::new("Point").with_method(Method::new("distance", Receiver::Ref)));

        let result = validate_no_symbol_collisions(&module);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            err.contains("FFI symbol collision"),
            "error should mention collision: {}",
            err
        );
        assert!(
            err.contains("Point::distance()"),
            "error should mention the method: {}",
            err
        );
        assert!(
            err.contains("fn point_distance()"),
            "error should mention the function: {}",
            err
        );
    }

    #[test]
    fn no_collision_when_names_differ() {
        let mut module = Module::new("test");
        module.functions.push(Function::new("echo_point"));
        module
            .records
            .push(Record::new("Point").with_method(Method::new("distance", Receiver::Ref)));

        assert!(validate_no_symbol_collisions(&module).is_ok());
    }
}
