use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};

use crate::ast::*;
use crate::diagnostic::Diagnostic;
use crate::identity::{ModuleId, PackageId, PackageItemId};
use crate::interface::{PackageExportGraph, PackageInterface, PackageInterfaceGraph};
use crate::span::Span;
use crate::symbol::SymbolTable;

pub fn load_flattened_program_from_entry(path: &Path) -> Result<Program, Vec<Diagnostic>> {
    Ok(load_flattened_from_entry(path)?.program)
}

pub fn import_paths_from_entry(path: &Path) -> Result<Vec<String>, Vec<Diagnostic>> {
    let (entry_program, manifest) = parse_entry_program(path)?;
    if entry_program.package.is_none() {
        return Ok(Vec::new());
    }

    let mut loader = PackageLoader::new(path.to_path_buf(), entry_program, manifest);
    loader.load_entry_import_paths()
}

pub fn entry_package_path_from_entry(path: &Path) -> Result<Option<String>, Vec<Diagnostic>> {
    let (entry_program, _) = parse_entry_program(path)?;
    Ok(entry_program.package.map(|package| package.path))
}

pub fn source_fingerprint_input_from_entry(path: &Path) -> Result<String, Vec<Diagnostic>> {
    let (entry_program, manifest) = parse_entry_program(path)?;
    if entry_program.package.is_none() {
        let source = fs::read_to_string(path).map_err(|error| {
            vec![Diagnostic::new(
                "PK002",
                format!("failed to read {}: {error}", path.display()),
                Span::default(),
            )]
        })?;
        return Ok(format!("script\t{}\n{source}\n", source.len()));
    }

    let mut loader = PackageLoader::new(path.to_path_buf(), entry_program, manifest);
    loader.entry_package_source_fingerprint_input()
}

pub fn load_flattened_from_entry(path: &Path) -> Result<LoadedFlattenedProgram, Vec<Diagnostic>> {
    let (entry_program, manifest) = parse_entry_program(path)?;
    if entry_program.package.is_none() {
        return Ok(LoadedFlattenedProgram {
            program: entry_program,
            package_graph: PackageSymbolGraph::default(),
            package_exports: PackageExportGraph::default(),
        });
    }

    let mut loader = PackageLoader::new(path.to_path_buf(), entry_program, manifest);
    loader.load_and_flatten()
}

pub fn load_package_graph_from_entry(path: &Path) -> Result<LoadedPackageGraph, Vec<Diagnostic>> {
    let (entry_program, manifest) = parse_entry_program(path)?;
    if entry_program.package.is_none() {
        return Err(vec![
            Diagnostic::new(
                "PK001",
                "package graph loading requires a package-mode entrypoint",
                Span::default(),
            )
            .with_suggestion("use a file that starts with `package`, or check scripts directly"),
        ]);
    }

    let mut loader = PackageLoader::new(path.to_path_buf(), entry_program, manifest);
    loader.load_unflattened_graph()
}

pub fn load_package_graph_from_entry_against_interfaces(
    path: &Path,
    interfaces: &PackageInterfaceGraph,
    interface_symbols: &SymbolTable,
) -> Result<LoadedPackageGraph, Vec<Diagnostic>> {
    let (entry_program, manifest) = parse_entry_program(path)?;
    if entry_program.package.is_none() {
        return Err(vec![
            Diagnostic::new(
                "PK001",
                "package graph loading requires a package-mode entrypoint",
                Span::default(),
            )
            .with_suggestion("use a file that starts with `package`, or check scripts directly"),
        ]);
    }

    let mut loader = PackageLoader::new(path.to_path_buf(), entry_program, manifest);
    loader.load_unflattened_graph_against_interfaces(interfaces, interface_symbols)
}

fn parse_entry_program(path: &Path) -> Result<(Program, Option<ProjectManifest>), Vec<Diagnostic>> {
    let entry_source = match fs::read_to_string(path) {
        Ok(source) => source,
        Err(error) => {
            return Err(vec![Diagnostic::new(
                "PK002",
                format!("failed to read {}: {error}", path.display()),
                Span::default(),
            )]);
        }
    };
    let entry_tokens = crate::lexer::lex(&entry_source)?;
    let manifest = discover_manifest(path)?;
    let entry_program = if let Some(manifest) = &manifest {
        let inferred_package = infer_manifest_package_path(path, manifest)?;
        let program =
            crate::parser::parse_inferred_package(entry_tokens, inferred_package.clone())?;
        if let Some(package) = &program.package
            && package.path != inferred_package
        {
            return Err(vec![Diagnostic::new(
                "PK006",
                format!(
                    "file {} declares package `{}` but manifest layout expects `{inferred_package}`",
                    path.display(),
                    package.path
                ),
                package.span,
            )]);
        }
        program
    } else {
        crate::parser::parse(entry_tokens)?
    };
    Ok((entry_program, manifest))
}

#[derive(Clone, Debug)]
pub struct LoadedFlattenedProgram {
    pub program: Program,
    pub package_graph: PackageSymbolGraph,
    pub package_exports: PackageExportGraph,
}

#[derive(Clone, Debug)]
pub struct LoadedPackageGraph {
    pub packages: Vec<LoadedPackage>,
    pub package_graph: PackageSymbolGraph,
    pub package_exports: PackageExportGraph,
    pub interfaces: Option<LoadedPackageInterfaces>,
    pub entry_package: PackageId,
    pub entry_module: ModuleId,
}

impl LoadedPackageGraph {
    pub fn is_loaded_interface_package_path(&self, path: &str) -> bool {
        let Some(interfaces) = &self.interfaces else {
            return false;
        };
        if self
            .package_graph
            .package(self.entry_package)
            .is_some_and(|package| package.path == path)
        {
            return false;
        }
        interfaces.graph.package_by_path(path).is_some()
    }

    pub fn entry_program(&self) -> Option<&Program> {
        let entry_package = self.package_graph.package(self.entry_package)?;
        let entry_module = self.package_graph.module(self.entry_module)?;
        self.packages
            .iter()
            .find(|package| package.path == entry_package.path)?
            .files
            .iter()
            .find(|file| file.module_path == entry_module.path)
            .map(|file| &file.program)
    }
}

#[derive(Clone, Debug)]
pub struct LoadedPackageInterfaces {
    pub graph: PackageInterfaceGraph,
    pub symbols: SymbolTable,
}

#[derive(Clone, Debug)]
pub struct LoadedPackage {
    pub path: String,
    pub files: Vec<LoadedPackageFile>,
}

#[derive(Clone, Debug)]
pub struct LoadedPackageFile {
    pub module_path: String,
    pub source: String,
    pub program: Program,
}

#[derive(Clone, Debug, Default)]
pub struct PackageSymbolGraph {
    pub packages: Vec<PackageInfo>,
    pub modules: Vec<PackageModuleInfo>,
    pub items: Vec<PackageItemInfo>,
}

impl PackageSymbolGraph {
    pub fn package(&self, id: PackageId) -> Option<&PackageInfo> {
        self.packages.get(id.as_u32() as usize)
    }

    pub fn item(&self, id: PackageItemId) -> Option<&PackageItemInfo> {
        self.items.get(id.as_u32() as usize)
    }

    pub fn module(&self, id: ModuleId) -> Option<&PackageModuleInfo> {
        self.modules.get(id.as_u32() as usize)
    }

    pub fn module_id(&self, package: PackageId, path: &str) -> Option<ModuleId> {
        self.modules
            .iter()
            .find(|module| module.package == package && module.path == path)
            .map(|module| module.id)
    }

    pub fn package_id(&self, path: &str) -> Option<PackageId> {
        self.packages
            .iter()
            .find(|package| package.path == path)
            .map(|package| package.id)
    }

    pub fn item_id(
        &self,
        package: PackageId,
        name: &str,
        kind: PackageItemKind,
    ) -> Option<PackageItemId> {
        self.items
            .iter()
            .find(|item| item.package == package && item.name == name && item.kind == kind)
            .map(|item| item.id)
    }

    pub fn item_id_in_module(
        &self,
        module: ModuleId,
        name: &str,
        kind: PackageItemKind,
    ) -> Option<PackageItemId> {
        self.items
            .iter()
            .find(|item| item.module == module && item.name == name && item.kind == kind)
            .map(|item| item.id)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageInfo {
    pub id: PackageId,
    pub path: String,
    pub modules: Vec<ModuleId>,
    pub imports: Vec<PackageImportInfo>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageModuleInfo {
    pub id: ModuleId,
    pub package: PackageId,
    pub path: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageImportInfo {
    pub alias: String,
    pub package: PackageId,
    pub path: String,
    pub span: Span,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageItemInfo {
    pub id: PackageItemId,
    pub package: PackageId,
    pub module: ModuleId,
    pub name: String,
    pub kind: PackageItemKind,
    pub visibility: Visibility,
    pub span: Span,
    pub mangled_name: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PackageItemKind {
    Record,
    Enum,
    Function,
}

pub fn validate_loaded_package_graph(loaded: &LoadedPackageGraph) -> Vec<Diagnostic> {
    let mut checker = PackageAwareChecker::new(loaded);
    checker.check();
    checker.diagnostics
}

struct PackageAwareChecker<'a> {
    loaded: &'a LoadedPackageGraph,
    diagnostics: Vec<Diagnostic>,
    imports: HashMap<String, String>,
    current_package: PackageId,
    current_module: ModuleId,
    scopes: Vec<HashSet<String>>,
}

impl<'a> PackageAwareChecker<'a> {
    fn new(loaded: &'a LoadedPackageGraph) -> Self {
        Self {
            loaded,
            diagnostics: Vec::new(),
            imports: HashMap::new(),
            current_package: PackageId::new(0),
            current_module: ModuleId::new(0),
            scopes: Vec::new(),
        }
    }

    fn check(&mut self) {
        for package in &self.loaded.packages {
            let Some(package_id) = self.loaded.package_graph.package_id(&package.path) else {
                continue;
            };
            for file in &package.files {
                let Some(module_id) = self
                    .loaded
                    .package_graph
                    .module_id(package_id, &file.module_path)
                else {
                    continue;
                };
                self.current_package = package_id;
                self.current_module = module_id;
                self.imports = file_import_aliases(&file.program.imports, &mut self.diagnostics);
                for statement in &file.program.statements {
                    self.check_top_level_stmt(statement);
                }
            }
        }
    }

    fn check_top_level_stmt(&mut self, statement: &Stmt) {
        match statement {
            Stmt::RecordDecl(record) => {
                if matches!(record.visibility, Visibility::Public | Visibility::Package) {
                    for field in &record.fields {
                        self.validate_visible_type_with_params(
                            &field.type_name,
                            record.visibility,
                            field.span,
                            &record.type_params,
                        );
                    }
                }
                self.scan_record_decl(record);
            }
            Stmt::EnumDecl(enumeration) => {
                if matches!(
                    enumeration.visibility,
                    Visibility::Public | Visibility::Package
                ) {
                    for variant in &enumeration.variants {
                        if let Some(payload) = &variant.payload {
                            self.validate_visible_type_with_params(
                                payload,
                                enumeration.visibility,
                                variant.span,
                                &enumeration.type_params,
                            );
                        }
                    }
                }
                self.scan_enum_decl(enumeration);
            }
            Stmt::FuncDecl(function) => {
                if function.visibility == Visibility::Public {
                    let has_full_signature = function
                        .params
                        .iter()
                        .all(|param| param.type_name.is_some())
                        && function.return_type.is_some();
                    if !has_full_signature {
                        self.diagnostics.push(
                            Diagnostic::new(
                                "PK011",
                                "public functions must annotate every parameter and the return type",
                                function.span,
                            )
                            .with_suggestion(
                                "add parameter type annotations and an explicit return type",
                            ),
                        );
                    }
                }
                if matches!(
                    function.visibility,
                    Visibility::Public | Visibility::Package
                ) {
                    for param in &function.params {
                        if let Some(type_name) = &param.type_name {
                            self.validate_visible_type_with_params(
                                type_name,
                                function.visibility,
                                param.span,
                                &function.type_params,
                            );
                        }
                    }
                    if let Some(type_name) = &function.return_type {
                        self.validate_visible_type_with_params(
                            type_name,
                            function.visibility,
                            function.span,
                            &function.type_params,
                        );
                    }
                }
                self.scan_func_decl(function);
            }
            _ => self.scan_stmt(statement),
        }
    }

    fn scan_stmt(&mut self, statement: &Stmt) {
        match statement {
            Stmt::Assign(stmt) => {
                if let Some(type_name) = &stmt.type_name {
                    self.scan_type_expr(type_name, stmt.span);
                }
                self.scan_expr(&stmt.value);
                self.insert_local(stmt.name.clone());
            }
            Stmt::RecordDecl(record) => self.scan_record_decl(record),
            Stmt::EnumDecl(enumeration) => self.scan_enum_decl(enumeration),
            Stmt::FuncDecl(function) => self.scan_func_decl(function),
            Stmt::If(stmt) => {
                self.scan_expr(&stmt.condition);
                self.scan_block(&stmt.then_branch);
                if let Some(else_branch) = &stmt.else_branch {
                    self.scan_block(else_branch);
                }
            }
            Stmt::While(stmt) => {
                self.scan_expr(&stmt.condition);
                self.scan_block(&stmt.body);
            }
            Stmt::Expr(stmt) => self.scan_expr(&stmt.expr),
        }
    }

    fn scan_record_decl(&mut self, record: &RecordDecl) {
        for field in &record.fields {
            self.scan_type_expr(&field.type_name, field.span);
        }
    }

    fn scan_enum_decl(&mut self, enumeration: &EnumDecl) {
        for variant in &enumeration.variants {
            if let Some(payload) = &variant.payload {
                self.scan_type_expr(payload, variant.span);
            }
        }
    }

    fn scan_func_decl(&mut self, function: &FuncDecl) {
        for param in &function.params {
            if let Some(type_name) = &param.type_name {
                self.scan_type_expr(type_name, param.span);
            }
        }
        if let Some(type_name) = &function.return_type {
            self.scan_type_expr(type_name, function.span);
        }

        self.push_scope();
        for param in &function.params {
            self.insert_local(param.name.clone());
        }
        self.predeclare_nested_functions(&function.body.statements);
        for statement in &function.body.statements {
            self.scan_stmt(statement);
        }
        self.scan_expr(&function.body.expr);
        self.pop_scope();
    }

    fn scan_block(&mut self, block: &Block) {
        self.push_scope();
        self.predeclare_nested_functions(&block.statements);
        for statement in &block.statements {
            self.scan_stmt(statement);
        }
        self.pop_scope();
    }

    fn scan_value_block(&mut self, block: &ValueBlock) {
        self.push_scope();
        self.predeclare_nested_functions(&block.statements);
        for statement in &block.statements {
            self.scan_stmt(statement);
        }
        self.scan_expr(&block.expr);
        self.pop_scope();
    }

    fn scan_expr(&mut self, expr: &Expr) {
        match expr {
            Expr::Int(_) | Expr::Bool(_) | Expr::String(_) | Expr::Unit(_) => {}
            Expr::Ident(expr) => {
                if !self.lookup_local(&expr.name) {
                    self.check_value_name(&expr.name, expr.span);
                }
            }
            Expr::ListLit(expr) => {
                for item in &expr.items {
                    self.scan_expr(item);
                }
            }
            Expr::Index(expr) => {
                self.scan_expr(&expr.base);
                self.scan_expr(&expr.index);
            }
            Expr::RecordLit(expr) => {
                self.check_type_name(&expr.type_name, expr.span);
                for field in &expr.fields {
                    self.scan_expr(&field.value);
                }
            }
            Expr::Field(expr) => self.scan_expr(&expr.base),
            Expr::RecordUpdate(expr) => {
                self.scan_expr(&expr.base);
                for field in &expr.fields {
                    self.scan_expr(&field.value);
                }
            }
            Expr::Unary(expr) => self.scan_expr(&expr.expr),
            Expr::Binary(expr) => {
                self.scan_expr(&expr.left);
                self.scan_expr(&expr.right);
            }
            Expr::Call(expr) => {
                self.scan_expr(&expr.callee);
                for arg in &expr.args {
                    self.scan_expr(arg);
                }
            }
            Expr::Try(expr) => self.scan_expr(&expr.expr),
            Expr::If(expr) => {
                self.scan_expr(&expr.condition);
                self.scan_value_block(&expr.then_branch);
                self.scan_value_block(&expr.else_branch);
            }
            Expr::Match(expr) => {
                self.scan_expr(&expr.value);
                for arm in &expr.arms {
                    let MatchPattern::Variant(pattern) = &arm.pattern;
                    self.check_type_name(&pattern.enum_name, pattern.span);
                    self.push_scope();
                    if let Some(binding) = &pattern.binding {
                        self.insert_local(binding.clone());
                    }
                    self.scan_expr(&arm.value);
                    self.pop_scope();
                }
            }
            Expr::Fn(expr) => {
                for param in &expr.params {
                    if let Some(type_name) = &param.type_name {
                        self.scan_type_expr(type_name, param.span);
                    }
                }
                if let Some(type_name) = &expr.return_type {
                    self.scan_type_expr(type_name, expr.span);
                }
                self.push_scope();
                for param in &expr.params {
                    self.insert_local(param.name.clone());
                }
                self.scan_value_block(&expr.body);
                self.pop_scope();
            }
        }
    }

    fn scan_type_expr(&mut self, type_expr: &TypeExpr, span: Span) {
        match type_expr {
            TypeExpr::Int | TypeExpr::Bool | TypeExpr::String | TypeExpr::Unit => {}
            TypeExpr::Named(name) => self.check_type_name(name, span),
            TypeExpr::Generic(generic) => {
                if !is_known_generic_type_name(&generic.name) {
                    self.check_type_name(&generic.name, span);
                }
                for arg in &generic.args {
                    self.scan_type_expr(arg, span);
                }
            }
            TypeExpr::Function(function) => {
                for param in &function.params {
                    self.scan_type_expr(param, span);
                }
                self.scan_type_expr(&function.ret, span);
            }
        }
    }

    fn validate_visible_type_with_params(
        &mut self,
        type_expr: &TypeExpr,
        api_visibility: Visibility,
        span: Span,
        type_params: &[String],
    ) {
        match type_expr {
            TypeExpr::Int | TypeExpr::Bool | TypeExpr::String | TypeExpr::Unit => {}
            TypeExpr::Named(name) => {
                if !type_params.iter().any(|param| param == name) {
                    self.validate_visible_type_name(name, api_visibility, span);
                }
            }
            TypeExpr::Generic(generic) => {
                if !is_known_generic_type_name(&generic.name)
                    && !type_params.iter().any(|param| param == &generic.name)
                {
                    self.validate_visible_type_name(&generic.name, api_visibility, span);
                }
                for arg in &generic.args {
                    self.validate_visible_type_with_params(arg, api_visibility, span, type_params);
                }
            }
            TypeExpr::Function(function) => {
                for param in &function.params {
                    self.validate_visible_type_with_params(
                        param,
                        api_visibility,
                        span,
                        type_params,
                    );
                }
                self.validate_visible_type_with_params(
                    &function.ret,
                    api_visibility,
                    span,
                    type_params,
                );
            }
        }
    }

    fn validate_visible_type_name(&mut self, name: &str, api_visibility: Visibility, span: Span) {
        if let Some((alias, item)) = split_qualified_name(name) {
            let _ = self.resolve_imported_type_item(alias, item, span);
            return;
        }

        for kind in [PackageItemKind::Record, PackageItemKind::Enum] {
            if let Some(item) = self.visible_same_package_item(name, kind)
                && !visibility_can_expose(item.visibility, api_visibility)
            {
                let api = visibility_label(api_visibility);
                let item_visibility = visibility_label(item.visibility);
                self.diagnostics.push(
                    Diagnostic::new(
                        "PK012",
                        format!(
                            "{api} API may not expose {item_visibility} {} `{name}`",
                            package_item_kind_label(kind)
                        ),
                        span,
                    )
                    .with_related(
                        format!(
                            "{} `{name}` is declared here",
                            package_item_kind_label(kind)
                        ),
                        item.span,
                    ),
                );
                return;
            }
        }
    }

    fn check_type_name(&mut self, name: &str, span: Span) {
        if let Some((alias, item)) = split_qualified_name(name) {
            let _ = self.resolve_imported_type_item(alias, item, span);
            return;
        }
        if self
            .visible_same_package_item(name, PackageItemKind::Record)
            .is_some()
            || self
                .visible_same_package_item(name, PackageItemKind::Enum)
                .is_some()
        {
            return;
        }
        if let Some(item) = self
            .inaccessible_same_package_item(name, PackageItemKind::Record)
            .cloned()
        {
            self.push_inaccessible_same_package_diagnostic(name, &item, "record", span);
        } else if let Some(item) = self
            .inaccessible_same_package_item(name, PackageItemKind::Enum)
            .cloned()
        {
            self.push_inaccessible_same_package_diagnostic(name, &item, "enum", span);
        }
    }

    fn check_value_name(&mut self, name: &str, span: Span) {
        if name.contains("::") && is_builtin_name(name) {
            return;
        }
        if let Some((enum_name, _variant_name)) = split_variant_name(name) {
            if let Some((alias, item)) = split_qualified_name(enum_name) {
                let _ = self.resolve_imported_item(alias, item, PackageItemKind::Enum, span);
                return;
            }
            if self
                .visible_same_package_item(enum_name, PackageItemKind::Enum)
                .is_some()
            {
                return;
            }
            if let Some(item) = self
                .inaccessible_same_package_item(enum_name, PackageItemKind::Enum)
                .cloned()
            {
                self.push_inaccessible_same_package_diagnostic(enum_name, &item, "enum", span);
                return;
            }
        }
        if let Some((alias, item)) = split_qualified_name(name) {
            let _ = self.resolve_imported_item(alias, item, PackageItemKind::Function, span);
            return;
        }
        if self
            .visible_same_package_item(name, PackageItemKind::Function)
            .is_some()
        {
            return;
        }
        if let Some(item) = self
            .inaccessible_same_package_item(name, PackageItemKind::Function)
            .cloned()
        {
            self.push_inaccessible_same_package_diagnostic(name, &item, "function", span);
        }
    }

    fn resolve_imported_type_item(&mut self, alias: &str, item: &str, span: Span) -> bool {
        let Some(package_id) = self.imported_package(alias, span) else {
            return false;
        };
        if self
            .loaded
            .package_exports
            .record_by_name(package_id, item)
            .is_some()
            || self
                .loaded
                .package_exports
                .enum_by_name(package_id, item)
                .is_some()
        {
            return true;
        }
        if let Some(record) = self
            .package_item(package_id, item, PackageItemKind::Record)
            .cloned()
        {
            self.push_missing_export_diagnostic(&record, "record", span);
        } else if let Some(enumeration) = self
            .package_item(package_id, item, PackageItemKind::Enum)
            .cloned()
        {
            self.push_missing_export_diagnostic(&enumeration, "enum", span);
        } else {
            self.push_missing_export_name_diagnostic(package_id, item, "type", span);
        }
        false
    }

    fn resolve_imported_item(
        &mut self,
        alias: &str,
        item: &str,
        kind: PackageItemKind,
        span: Span,
    ) -> bool {
        let Some(package_id) = self.imported_package(alias, span) else {
            return false;
        };
        let exported = match kind {
            PackageItemKind::Record => self.loaded.package_exports.record_by_name(package_id, item),
            PackageItemKind::Enum => self.loaded.package_exports.enum_by_name(package_id, item),
            PackageItemKind::Function => self
                .loaded
                .package_exports
                .function_by_name(package_id, item),
        };
        if exported.is_some() {
            return true;
        }
        if let Some(declaration) = self.package_item(package_id, item, kind).cloned() {
            self.push_missing_export_diagnostic(&declaration, package_item_kind_label(kind), span);
        } else {
            self.push_missing_export_name_diagnostic(
                package_id,
                item,
                package_item_kind_label(kind),
                span,
            );
        }
        false
    }

    fn imported_package(&mut self, alias: &str, span: Span) -> Option<PackageId> {
        let Some(package_path) = self.imports.get(alias).cloned() else {
            self.diagnostics.push(Diagnostic::new(
                "PK009",
                format!("unknown import alias `{alias}`"),
                span,
            ));
            return None;
        };
        let Some(package_id) = self.loaded.package_graph.package_id(&package_path) else {
            self.diagnostics.push(Diagnostic::new(
                "PK010",
                format!("unknown imported package `{package_path}`"),
                span,
            ));
            return None;
        };
        Some(package_id)
    }

    fn visible_same_package_item(
        &self,
        name: &str,
        kind: PackageItemKind,
    ) -> Option<&PackageItemInfo> {
        self.loaded
            .package_graph
            .items
            .iter()
            .find(|item| {
                item.package == self.current_package
                    && item.module == self.current_module
                    && item.name == name
                    && item.kind == kind
            })
            .or_else(|| {
                self.loaded.package_graph.items.iter().find(|item| {
                    item.package == self.current_package
                        && item.name == name
                        && item.kind == kind
                        && matches!(item.visibility, Visibility::Package | Visibility::Public)
                })
            })
    }

    fn inaccessible_same_package_item(
        &self,
        name: &str,
        kind: PackageItemKind,
    ) -> Option<&PackageItemInfo> {
        self.loaded.package_graph.items.iter().find(|item| {
            item.package == self.current_package
                && item.module != self.current_module
                && item.name == name
                && item.kind == kind
                && item.visibility == Visibility::Private
        })
    }

    fn package_item(
        &self,
        package_id: PackageId,
        name: &str,
        kind: PackageItemKind,
    ) -> Option<&PackageItemInfo> {
        self.loaded
            .package_graph
            .items
            .iter()
            .find(|item| item.package == package_id && item.name == name && item.kind == kind)
    }

    fn push_missing_export_diagnostic(
        &mut self,
        declaration: &PackageItemInfo,
        kind: &str,
        span: Span,
    ) {
        let package_path = self
            .loaded
            .package_graph
            .package(declaration.package)
            .map(|package| package.path.as_str())
            .unwrap_or("<unknown>");
        self.diagnostics.push(
            Diagnostic::new(
                "PK010",
                format!(
                    "package `{package_path}` does not export {kind} `{}`",
                    declaration.name
                ),
                span,
            )
            .with_related(
                format!(
                    "{kind} `{}` is declared here but is not public",
                    declaration.name
                ),
                declaration.span,
            )
            .with_suggestion(format!(
                "mark the {kind} declaration as `pub` to export it from the package"
            )),
        );
    }

    fn push_missing_export_name_diagnostic(
        &mut self,
        package_id: PackageId,
        item: &str,
        kind: &str,
        span: Span,
    ) {
        let package_path = self
            .loaded
            .package_graph
            .package(package_id)
            .map(|package| package.path.as_str())
            .unwrap_or("<unknown>");
        self.diagnostics.push(Diagnostic::new(
            "PK010",
            format!("package `{package_path}` does not export {kind} `{item}`"),
            span,
        ));
    }

    fn push_inaccessible_same_package_diagnostic(
        &mut self,
        name: &str,
        item: &PackageItemInfo,
        kind: &str,
        span: Span,
    ) {
        let module = self
            .loaded
            .package_graph
            .module(self.current_module)
            .map(|module| module.path.as_str())
            .unwrap_or("<unknown>");
        let private_module = self
            .loaded
            .package_graph
            .module(item.module)
            .map(|module| module.path.as_str())
            .unwrap_or("<unknown>");
        self.diagnostics.push(
            Diagnostic::new(
                "PK015",
                format!("{kind} `{name}` is not visible from module `{module}`"),
                span,
            )
            .with_related(
                format!("{kind} `{name}` is module-private to `{private_module}`"),
                item.span,
            )
            .with_suggestion("mark the declaration as `pkg` to share it within the package"),
        );
    }

    fn predeclare_nested_functions(&mut self, statements: &[Stmt]) {
        for statement in statements {
            if let Stmt::FuncDecl(function) = statement {
                self.insert_local(function.name.clone());
            }
        }
    }

    fn push_scope(&mut self) {
        self.scopes.push(HashSet::new());
    }

    fn pop_scope(&mut self) {
        self.scopes.pop();
    }

    fn insert_local(&mut self, name: String) {
        if let Some(scope) = self.scopes.last_mut() {
            scope.insert(name);
        }
    }

    fn lookup_local(&self, name: &str) -> bool {
        self.scopes.iter().rev().any(|scope| scope.contains(name))
    }
}

struct ParsedFile {
    program: Program,
    module_path: String,
    source: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct SourceFingerprintFile {
    pub module_path: String,
    pub source: String,
}

#[derive(Clone, Debug)]
struct ProjectManifest {
    source_root: PathBuf,
    name: String,
}

struct PackageData {
    files: Vec<ParsedFile>,
    records: HashMap<String, Vec<PackageItemDecl>>,
    enums: HashMap<String, Vec<PackageItemDecl>>,
    functions: HashMap<String, Vec<PackageItemDecl>>,
}

#[derive(Clone, Debug)]
struct PackageItemDecl {
    visibility: Visibility,
    module_path: String,
    span: Span,
}

struct PackageItemDeclInput<'a> {
    name: &'a str,
    visibility: Visibility,
    module_path: &'a str,
    span: Span,
    kind: PackageItemKind,
    package_path: &'a str,
}

struct PackageLoader {
    entry_file: PathBuf,
    source_root: PathBuf,
    entry_package: String,
    manifest: Option<ProjectManifest>,
    packages: HashMap<String, PackageData>,
    loading: HashSet<String>,
    diagnostics: Vec<Diagnostic>,
}

impl PackageLoader {
    fn new(entry_file: PathBuf, entry_program: Program, manifest: Option<ProjectManifest>) -> Self {
        let entry_package = entry_program
            .package
            .as_ref()
            .expect("checked package mode")
            .path
            .clone();
        let source_root = manifest
            .as_ref()
            .map(|manifest| manifest.source_root.clone())
            .unwrap_or_else(|| {
                infer_source_root(&entry_file, &entry_package)
                    .unwrap_or_else(|_| entry_file.clone())
            });
        Self {
            entry_file,
            source_root,
            entry_package,
            manifest,
            packages: HashMap::new(),
            loading: HashSet::new(),
            diagnostics: Vec::new(),
        }
    }

    fn load_and_flatten(&mut self) -> Result<LoadedFlattenedProgram, Vec<Diagnostic>> {
        if let Err(diagnostic) = self.ensure_source_root() {
            self.diagnostics.push(diagnostic);
            return Err(std::mem::take(&mut self.diagnostics));
        }

        self.load_package(self.entry_package.clone());

        if !self.diagnostics.is_empty() {
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let package_paths = self.sorted_package_paths();
        let package_graph = self.build_symbol_graph(&package_paths);
        let package_exports = PackageExportGraph::from_symbol_graph(&package_graph);
        self.flatten_packages(&package_paths, package_graph, package_exports)
    }

    fn load_unflattened_graph(&mut self) -> Result<LoadedPackageGraph, Vec<Diagnostic>> {
        if let Err(diagnostic) = self.ensure_source_root() {
            self.diagnostics.push(diagnostic);
            return Err(std::mem::take(&mut self.diagnostics));
        }

        self.load_package(self.entry_package.clone());

        if !self.diagnostics.is_empty() {
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let package_paths = self.sorted_package_paths();
        let package_graph = self.build_symbol_graph(&package_paths);
        let package_exports = PackageExportGraph::from_symbol_graph(&package_graph);
        let packages = self.loaded_packages_from_paths(&package_paths);
        let (entry_package, entry_module) = self.entry_ids(&package_graph);

        Ok(LoadedPackageGraph {
            packages,
            package_graph,
            package_exports,
            interfaces: None,
            entry_package,
            entry_module,
        })
    }

    fn load_unflattened_graph_against_interfaces(
        &mut self,
        interfaces: &PackageInterfaceGraph,
        interface_symbols: &SymbolTable,
    ) -> Result<LoadedPackageGraph, Vec<Diagnostic>> {
        if let Err(diagnostic) = self.ensure_source_root() {
            self.diagnostics.push(diagnostic);
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let entry_package = self.entry_package.clone();
        let files = self.load_package_files(&entry_package);
        let entry_data = collect_package_data(&entry_package, files, &mut self.diagnostics);
        let imported_paths = package_import_paths(&entry_data);
        self.packages.insert(entry_package.clone(), entry_data);

        for import_path in &imported_paths {
            if interfaces.package_by_path(import_path).is_none() {
                self.diagnostics.push(
                    Diagnostic::new(
                        "PK016",
                        format!("missing loaded package interface for `{import_path}`"),
                        Span::default(),
                    )
                    .with_suggestion("load or regenerate the package interface before checking"),
                );
            }
        }

        if !self.diagnostics.is_empty() {
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let package_paths = self.sorted_package_paths();
        let package_graph = self.build_symbol_graph_against_interfaces(&package_paths, interfaces);
        let package_exports = PackageExportGraph::from_interfaces(interfaces, &package_graph);
        let packages = self.loaded_packages_from_paths(&package_paths);
        let (entry_package, entry_module) = self.entry_ids(&package_graph);

        Ok(LoadedPackageGraph {
            packages,
            package_graph,
            package_exports,
            interfaces: Some(LoadedPackageInterfaces {
                graph: interfaces.clone(),
                symbols: interface_symbols.clone(),
            }),
            entry_package,
            entry_module,
        })
    }

    fn loaded_packages_from_paths(&self, package_paths: &[String]) -> Vec<LoadedPackage> {
        package_paths
            .iter()
            .filter_map(|package_path| {
                let package = self.packages.get(package_path)?;
                Some(LoadedPackage {
                    path: package_path.clone(),
                    files: package
                        .files
                        .iter()
                        .map(|file| LoadedPackageFile {
                            module_path: file.module_path.clone(),
                            source: file.source.clone(),
                            program: file.program.clone(),
                        })
                        .collect(),
                })
            })
            .collect()
    }

    fn entry_ids(&self, package_graph: &PackageSymbolGraph) -> (PackageId, ModuleId) {
        let package = package_graph
            .package_id(&self.entry_package)
            .expect("entry package should exist in loaded package graph");
        let module_path = module_path_for_file(&self.entry_file);
        let module = package_graph
            .module_id(package, &module_path)
            .expect("entry module should exist in loaded package graph");
        (package, module)
    }

    fn load_entry_import_paths(&mut self) -> Result<Vec<String>, Vec<Diagnostic>> {
        if let Err(diagnostic) = self.ensure_source_root() {
            self.diagnostics.push(diagnostic);
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let entry_package = self.entry_package.clone();
        let files = self.load_package_files(&entry_package);
        let entry_data = collect_package_data(&entry_package, files, &mut self.diagnostics);
        if self.diagnostics.is_empty() {
            Ok(package_import_paths(&entry_data))
        } else {
            Err(std::mem::take(&mut self.diagnostics))
        }
    }

    fn entry_package_source_fingerprint_input(&mut self) -> Result<String, Vec<Diagnostic>> {
        if let Err(diagnostic) = self.ensure_source_root() {
            self.diagnostics.push(diagnostic);
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let entry_package = self.entry_package.clone();
        let files = self.load_package_source_files(&entry_package);
        if !self.diagnostics.is_empty() {
            return Err(std::mem::take(&mut self.diagnostics));
        }

        let mut input = format!("package\t{entry_package}\n");
        for file in files {
            input.push_str(&format!(
                "file\t{}\t{}\n{}\n",
                file.module_path,
                file.source.len(),
                file.source
            ));
        }
        Ok(input)
    }

    fn ensure_source_root(&mut self) -> Result<(), Diagnostic> {
        if self.manifest.is_none() {
            self.source_root = infer_source_root(&self.entry_file, &self.entry_package)?;
        }
        Ok(())
    }

    fn flatten_packages(
        &mut self,
        package_paths: &[String],
        package_graph: PackageSymbolGraph,
        package_exports: PackageExportGraph,
    ) -> Result<LoadedFlattenedProgram, Vec<Diagnostic>> {
        let mut statements = Vec::new();
        for package_path in package_paths {
            let Some(package) = self.packages.get(package_path) else {
                continue;
            };
            for file in &package.files {
                let import_aliases =
                    file_import_aliases(&file.program.imports, &mut self.diagnostics);
                let mut rewriter = PackageRewriter {
                    diagnostics: &mut self.diagnostics,
                    current_package: package_path.clone(),
                    current_module: file.module_path.clone(),
                    entry_package: self.entry_package.clone(),
                    imports: import_aliases,
                    current_package_data: package,
                    packages: &self.packages,
                    package_graph: &package_graph,
                    package_exports: &package_exports,
                    scopes: Vec::new(),
                };
                for statement in &file.program.statements {
                    statements.push(rewriter.rewrite_top_level_stmt(statement));
                }
            }
        }

        if self.diagnostics.is_empty() {
            let mut program = Program {
                package: None,
                imports: Vec::new(),
                statements,
            };
            renumber_node_ids(&mut program);
            Ok(LoadedFlattenedProgram {
                program,
                package_graph,
                package_exports,
            })
        } else {
            Err(std::mem::take(&mut self.diagnostics))
        }
    }

    fn load_package(&mut self, package_path: String) {
        if self.packages.contains_key(&package_path) {
            return;
        }
        if !self.loading.insert(package_path.clone()) {
            self.diagnostics.push(Diagnostic::new(
                "PK008",
                format!("import cycle detected at package `{package_path}`"),
                Span::default(),
            ));
            return;
        }

        let files = self.load_package_files(&package_path);
        for file in &files {
            for import in &file.program.imports {
                self.load_package(import.path.clone());
            }
        }

        let package_data = collect_package_data(&package_path, files, &mut self.diagnostics);
        self.packages.insert(package_path.clone(), package_data);
        self.loading.remove(&package_path);
    }

    fn load_package_files(&mut self, package_path: &str) -> Vec<ParsedFile> {
        if let Some(files) = crate::std_package::virtual_package_files(package_path) {
            return self.load_virtual_package_files(package_path, files);
        }

        let package_dir = self.package_dir(package_path);
        let read_dir = match fs::read_dir(&package_dir) {
            Ok(read_dir) => read_dir,
            Err(error) => {
                self.diagnostics.push(Diagnostic::new(
                    "PK002",
                    format!(
                        "failed to read package directory {}: {error}",
                        package_dir.display()
                    ),
                    Span::default(),
                ));
                return Vec::new();
            }
        };

        let mut file_paths: Vec<PathBuf> = read_dir
            .filter_map(|entry| entry.ok().map(|entry| entry.path()))
            .filter(|path| path.extension().is_some_and(|ext| ext == "muga"))
            .collect();
        file_paths.sort();

        if file_paths.is_empty() {
            self.diagnostics.push(Diagnostic::new(
                "PK004",
                format!("package `{package_path}` does not contain any `.muga` files"),
                Span::default(),
            ));
            return Vec::new();
        }

        let mut files = Vec::new();
        for file_path in file_paths {
            let source = match fs::read_to_string(&file_path) {
                Ok(source) => source,
                Err(error) => {
                    self.diagnostics.push(Diagnostic::new(
                        "PK002",
                        format!("failed to read {}: {error}", file_path.display()),
                        Span::default(),
                    ));
                    continue;
                }
            };
            let program = match self.parse_package_file(&source, package_path) {
                Ok(program) => program,
                Err(diagnostics) => {
                    self.diagnostics.extend(diagnostics);
                    continue;
                }
            };
            match &program.package {
                Some(package) if package.path == package_path => {}
                Some(package) => {
                    self.diagnostics.push(Diagnostic::new(
                        "PK006",
                        format!(
                            "file {} declares package `{}` but directory expects `{package_path}`",
                            file_path.display(),
                            package.path
                        ),
                        package.span,
                    ));
                    continue;
                }
                None => {
                    self.diagnostics.push(Diagnostic::new(
                        "PK005",
                        format!(
                            "package directory file {} must begin with `package {package_path}`",
                            file_path.display()
                        ),
                        Span::default(),
                    ));
                    continue;
                }
            }
            let module_path = module_path_for_file(&file_path);
            files.push(ParsedFile {
                program,
                module_path,
                source,
            });
        }
        files
    }

    fn load_virtual_package_files(
        &mut self,
        package_path: &str,
        files: &[crate::std_package::VirtualPackageFile],
    ) -> Vec<ParsedFile> {
        let mut parsed = Vec::with_capacity(files.len());
        for file in files {
            let source = file.source.trim_start().to_string();
            let program = match self.parse_package_file(&source, package_path) {
                Ok(program) => program,
                Err(diagnostics) => {
                    self.diagnostics.extend(diagnostics);
                    continue;
                }
            };
            parsed.push(ParsedFile {
                program,
                module_path: file.module_path.to_string(),
                source,
            });
        }
        parsed
    }

    fn load_package_source_files(&mut self, package_path: &str) -> Vec<SourceFingerprintFile> {
        let package_dir = self.package_dir(package_path);
        let read_dir = match fs::read_dir(&package_dir) {
            Ok(read_dir) => read_dir,
            Err(error) => {
                self.diagnostics.push(Diagnostic::new(
                    "PK002",
                    format!(
                        "failed to read package directory {}: {error}",
                        package_dir.display()
                    ),
                    Span::default(),
                ));
                return Vec::new();
            }
        };

        let mut file_paths: Vec<PathBuf> = read_dir
            .filter_map(|entry| entry.ok().map(|entry| entry.path()))
            .filter(|path| path.extension().is_some_and(|ext| ext == "muga"))
            .collect();
        file_paths.sort();

        if file_paths.is_empty() {
            self.diagnostics.push(Diagnostic::new(
                "PK004",
                format!("package `{package_path}` does not contain any `.muga` files"),
                Span::default(),
            ));
            return Vec::new();
        }

        let mut files = Vec::new();
        for file_path in file_paths {
            let source = match fs::read_to_string(&file_path) {
                Ok(source) => source,
                Err(error) => {
                    self.diagnostics.push(Diagnostic::new(
                        "PK002",
                        format!("failed to read {}: {error}", file_path.display()),
                        Span::default(),
                    ));
                    continue;
                }
            };
            files.push(SourceFingerprintFile {
                module_path: module_path_for_file(&file_path),
                source,
            });
        }
        files
    }

    fn parse_package_file(
        &self,
        source: &str,
        package_path: &str,
    ) -> Result<Program, Vec<Diagnostic>> {
        let tokens = crate::lexer::lex(source)?;
        if self.manifest.is_some() {
            crate::parser::parse_inferred_package(tokens, package_path.to_string())
        } else {
            crate::parser::parse(tokens)
        }
    }

    fn package_dir(&self, package_path: &str) -> PathBuf {
        if let Some(manifest) = &self.manifest {
            if package_path == manifest.name {
                return self.source_root.clone();
            }
            if let Some(rest) = package_path.strip_prefix(&(manifest.name.clone() + "::")) {
                let mut path = self.source_root.clone();
                for segment in split_package_path(rest) {
                    path.push(segment);
                }
                return path;
            }
        }

        let mut path = self.source_root.clone();
        for segment in split_package_path(package_path) {
            path.push(segment);
        }
        path
    }

    fn sorted_package_paths(&self) -> Vec<String> {
        let mut package_paths: Vec<String> = self.packages.keys().cloned().collect();
        package_paths.sort();
        package_paths
    }

    fn build_symbol_graph(&self, package_paths: &[String]) -> PackageSymbolGraph {
        let package_ids: HashMap<&str, PackageId> = package_paths
            .iter()
            .enumerate()
            .map(|(index, path)| (path.as_str(), PackageId::new(index as u32)))
            .collect();

        let mut packages = Vec::with_capacity(package_paths.len());
        let mut modules = Vec::new();
        let mut items = Vec::new();

        for package_path in package_paths {
            let Some(package) = self.packages.get(package_path) else {
                continue;
            };
            let package_id = package_ids[package_path.as_str()];
            let mut package_modules = Vec::new();
            let mut file_modules = HashMap::new();
            for file in &package.files {
                let id = ModuleId::new(modules.len() as u32);
                package_modules.push(id);
                file_modules.insert(file.module_path.as_str(), id);
                modules.push(PackageModuleInfo {
                    id,
                    package: package_id,
                    path: file.module_path.clone(),
                });
            }
            let mut imports = Vec::new();
            for file in &package.files {
                for import in &file.program.imports {
                    if let Some(imported_package) = package_ids.get(import.path.as_str()) {
                        imports.push(PackageImportInfo {
                            alias: import.alias.clone(),
                            package: *imported_package,
                            path: import.path.clone(),
                            span: import.span,
                        });
                    }
                }
            }
            packages.push(PackageInfo {
                id: package_id,
                path: package_path.clone(),
                modules: package_modules,
                imports,
            });

            for file in &package.files {
                let module_id = file_modules[file.module_path.as_str()];
                for statement in &file.program.statements {
                    match statement {
                        Stmt::RecordDecl(record) => {
                            let id = PackageItemId::new(items.len() as u32);
                            items.push(PackageItemInfo {
                                id,
                                package: package_id,
                                module: module_id,
                                name: record.name.clone(),
                                kind: PackageItemKind::Record,
                                visibility: record.visibility,
                                span: record.span,
                                mangled_name: mangle_record_name_for_visibility(
                                    package_path,
                                    &file.module_path,
                                    &record.name,
                                    record.visibility,
                                ),
                            });
                        }
                        Stmt::EnumDecl(enumeration) => {
                            let id = PackageItemId::new(items.len() as u32);
                            items.push(PackageItemInfo {
                                id,
                                package: package_id,
                                module: module_id,
                                name: enumeration.name.clone(),
                                kind: PackageItemKind::Enum,
                                visibility: enumeration.visibility,
                                span: enumeration.span,
                                mangled_name: mangle_enum_name_for_visibility(
                                    package_path,
                                    &file.module_path,
                                    &enumeration.name,
                                    enumeration.visibility,
                                ),
                            });
                        }
                        Stmt::FuncDecl(func) => {
                            let id = PackageItemId::new(items.len() as u32);
                            items.push(PackageItemInfo {
                                id,
                                package: package_id,
                                module: module_id,
                                name: func.name.clone(),
                                kind: PackageItemKind::Function,
                                visibility: func.visibility,
                                span: func.span,
                                mangled_name: mangle_function_name_for_visibility(
                                    package_path,
                                    &file.module_path,
                                    &func.name,
                                    func.visibility,
                                    &self.entry_package,
                                ),
                            });
                        }
                        _ => {}
                    }
                }
            }
        }

        PackageSymbolGraph {
            packages,
            modules,
            items,
        }
    }

    fn build_symbol_graph_against_interfaces(
        &mut self,
        package_paths: &[String],
        interfaces: &PackageInterfaceGraph,
    ) -> PackageSymbolGraph {
        let mut package_ids: HashMap<String, PackageId> = interfaces
            .packages
            .iter()
            .map(|interface| (interface.path.clone(), interface.package))
            .collect();
        let mut next_package_id = interfaces
            .packages
            .iter()
            .map(|interface| interface.package.as_u32())
            .max()
            .map_or(0, |id| id + 1);
        for package_path in package_paths {
            package_ids.entry(package_path.clone()).or_insert_with(|| {
                let id = PackageId::new(next_package_id);
                next_package_id += 1;
                id
            });
        }

        let max_package_id = package_ids
            .values()
            .map(|id| id.as_u32())
            .max()
            .unwrap_or(0) as usize;
        let max_item_id = interfaces
            .packages
            .iter()
            .flat_map(|interface| {
                interface
                    .records
                    .iter()
                    .map(|record| record.item)
                    .chain(interface.enums.iter().map(|enumeration| enumeration.item))
                    .chain(interface.functions.iter().map(|function| function.item))
            })
            .map(|id| id.as_u32())
            .max()
            .map_or(0, |id| id + 1) as usize;

        let mut package_slots: Vec<Option<PackageInfo>> = vec![None; max_package_id + 1];
        let mut modules = Vec::new();
        let mut item_slots: Vec<Option<PackageItemInfo>> = vec![None; max_item_id];

        for package_path in package_paths {
            let Some(package) = self.packages.get(package_path) else {
                continue;
            };
            let package_id = package_ids[package_path];
            let mut package_modules = Vec::new();
            let mut file_modules = HashMap::new();
            for file in &package.files {
                let id = ModuleId::new(modules.len() as u32);
                package_modules.push(id);
                file_modules.insert(file.module_path.as_str(), id);
                modules.push(PackageModuleInfo {
                    id,
                    package: package_id,
                    path: file.module_path.clone(),
                });
            }
            let mut imports = Vec::new();
            for file in &package.files {
                for import in &file.program.imports {
                    if let Some(imported_package) = package_ids.get(&import.path) {
                        imports.push(PackageImportInfo {
                            alias: import.alias.clone(),
                            package: *imported_package,
                            path: import.path.clone(),
                            span: import.span,
                        });
                    }
                }
            }
            package_slots[package_id.as_u32() as usize] = Some(PackageInfo {
                id: package_id,
                path: package_path.clone(),
                modules: package_modules,
                imports,
            });

            let package_interface = interfaces.package_by_path(package_path);
            for file in &package.files {
                let module_id = file_modules[file.module_path.as_str()];
                for statement in &file.program.statements {
                    match statement {
                        Stmt::RecordDecl(record) => {
                            let id = interface_item_id(
                                package_interface,
                                &record.name,
                                PackageItemKind::Record,
                                record.visibility,
                            )
                            .unwrap_or_else(|| allocate_package_item_id(&mut item_slots));
                            insert_package_graph_item(
                                &mut item_slots,
                                PackageItemInfo {
                                    id,
                                    package: package_id,
                                    module: module_id,
                                    name: record.name.clone(),
                                    kind: PackageItemKind::Record,
                                    visibility: record.visibility,
                                    span: record.span,
                                    mangled_name: mangle_record_name_for_visibility(
                                        package_path,
                                        &file.module_path,
                                        &record.name,
                                        record.visibility,
                                    ),
                                },
                                &mut self.diagnostics,
                            );
                        }
                        Stmt::EnumDecl(enumeration) => {
                            let id = interface_item_id(
                                package_interface,
                                &enumeration.name,
                                PackageItemKind::Enum,
                                enumeration.visibility,
                            )
                            .unwrap_or_else(|| allocate_package_item_id(&mut item_slots));
                            insert_package_graph_item(
                                &mut item_slots,
                                PackageItemInfo {
                                    id,
                                    package: package_id,
                                    module: module_id,
                                    name: enumeration.name.clone(),
                                    kind: PackageItemKind::Enum,
                                    visibility: enumeration.visibility,
                                    span: enumeration.span,
                                    mangled_name: mangle_enum_name_for_visibility(
                                        package_path,
                                        &file.module_path,
                                        &enumeration.name,
                                        enumeration.visibility,
                                    ),
                                },
                                &mut self.diagnostics,
                            );
                        }
                        Stmt::FuncDecl(func) => {
                            let id = interface_item_id(
                                package_interface,
                                &func.name,
                                PackageItemKind::Function,
                                func.visibility,
                            )
                            .unwrap_or_else(|| allocate_package_item_id(&mut item_slots));
                            insert_package_graph_item(
                                &mut item_slots,
                                PackageItemInfo {
                                    id,
                                    package: package_id,
                                    module: module_id,
                                    name: func.name.clone(),
                                    kind: PackageItemKind::Function,
                                    visibility: func.visibility,
                                    span: func.span,
                                    mangled_name: mangle_function_name_for_visibility(
                                        package_path,
                                        &file.module_path,
                                        &func.name,
                                        func.visibility,
                                        &self.entry_package,
                                    ),
                                },
                                &mut self.diagnostics,
                            );
                        }
                        _ => {}
                    }
                }
            }
        }

        for interface in &interfaces.packages {
            if self.packages.contains_key(&interface.path) {
                continue;
            }
            let package_id = package_ids[&interface.path];
            let module_id = ModuleId::new(modules.len() as u32);
            modules.push(PackageModuleInfo {
                id: module_id,
                package: package_id,
                path: INTERFACE_MODULE.to_string(),
            });
            let imports = interface
                .dependencies
                .iter()
                .filter_map(|dependency| {
                    package_ids
                        .get(dependency)
                        .map(|package| PackageImportInfo {
                            alias: dependency
                                .rsplit("::")
                                .next()
                                .unwrap_or(dependency)
                                .to_string(),
                            package: *package,
                            path: dependency.clone(),
                            span: Span::default(),
                        })
                })
                .collect();
            package_slots[package_id.as_u32() as usize] = Some(PackageInfo {
                id: package_id,
                path: interface.path.clone(),
                modules: vec![module_id],
                imports,
            });
            for record in &interface.records {
                insert_package_graph_item(
                    &mut item_slots,
                    PackageItemInfo {
                        id: record.item,
                        package: package_id,
                        module: module_id,
                        name: record.name.clone(),
                        kind: PackageItemKind::Record,
                        visibility: Visibility::Public,
                        span: record.span,
                        mangled_name: mangle_record_name_for_visibility(
                            &interface.path,
                            INTERFACE_MODULE,
                            &record.name,
                            Visibility::Public,
                        ),
                    },
                    &mut self.diagnostics,
                );
            }
            for enumeration in &interface.enums {
                insert_package_graph_item(
                    &mut item_slots,
                    PackageItemInfo {
                        id: enumeration.item,
                        package: package_id,
                        module: module_id,
                        name: enumeration.name.clone(),
                        kind: PackageItemKind::Enum,
                        visibility: Visibility::Public,
                        span: enumeration.span,
                        mangled_name: mangle_enum_name_for_visibility(
                            &interface.path,
                            INTERFACE_MODULE,
                            &enumeration.name,
                            Visibility::Public,
                        ),
                    },
                    &mut self.diagnostics,
                );
            }
            for function in &interface.functions {
                insert_package_graph_item(
                    &mut item_slots,
                    PackageItemInfo {
                        id: function.item,
                        package: package_id,
                        module: module_id,
                        name: function.name.clone(),
                        kind: PackageItemKind::Function,
                        visibility: Visibility::Public,
                        span: function.span,
                        mangled_name: mangle_function_name_for_visibility(
                            &interface.path,
                            INTERFACE_MODULE,
                            &function.name,
                            Visibility::Public,
                            &self.entry_package,
                        ),
                    },
                    &mut self.diagnostics,
                );
            }
        }

        let fallback_package = package_ids
            .values()
            .copied()
            .next()
            .unwrap_or_else(|| PackageId::new(0));
        let fallback_module = modules
            .first()
            .map(|module| module.id)
            .unwrap_or(ModuleId::new(0));
        let packages = package_slots
            .into_iter()
            .enumerate()
            .map(|(index, package)| {
                package.unwrap_or_else(|| PackageInfo {
                    id: PackageId::new(index as u32),
                    path: format!("<interface-gap-{index}>"),
                    modules: Vec::new(),
                    imports: Vec::new(),
                })
            })
            .collect();
        let items = item_slots
            .into_iter()
            .enumerate()
            .map(|(index, item)| {
                item.unwrap_or_else(|| PackageItemInfo {
                    id: PackageItemId::new(index as u32),
                    package: fallback_package,
                    module: fallback_module,
                    name: format!("<interface-gap-{index}>"),
                    kind: PackageItemKind::Function,
                    visibility: Visibility::Private,
                    span: Span::default(),
                    mangled_name: format!("__muga_interface_gap_{index}"),
                })
            })
            .collect();

        PackageSymbolGraph {
            packages,
            modules,
            items,
        }
    }
}

struct PackageRewriter<'a> {
    diagnostics: &'a mut Vec<Diagnostic>,
    current_package: String,
    current_module: String,
    entry_package: String,
    imports: HashMap<String, String>,
    current_package_data: &'a PackageData,
    packages: &'a HashMap<String, PackageData>,
    package_graph: &'a PackageSymbolGraph,
    package_exports: &'a PackageExportGraph,
    scopes: Vec<HashSet<String>>,
}

impl<'a> PackageRewriter<'a> {
    fn rewrite_top_level_stmt(&mut self, statement: &Stmt) -> Stmt {
        match statement {
            Stmt::RecordDecl(record) => Stmt::RecordDecl(self.rewrite_record_decl(record)),
            Stmt::EnumDecl(enumeration) => Stmt::EnumDecl(self.rewrite_enum_decl(enumeration)),
            Stmt::FuncDecl(func) => Stmt::FuncDecl(self.rewrite_func_decl(func, true)),
            _ => statement.clone(),
        }
    }

    fn rewrite_record_decl(&mut self, record: &RecordDecl) -> RecordDecl {
        if record.visibility == Visibility::Public || record.visibility == Visibility::Package {
            for field in &record.fields {
                self.validate_visible_type_with_params(
                    &field.type_name,
                    record.visibility,
                    field.span,
                    &record.type_params,
                );
            }
        }

        RecordDecl {
            id: record.id,
            package_item: self.package_item_id(&record.name, PackageItemKind::Record),
            name: mangle_record_name_for_visibility(
                &self.current_package,
                &self.current_module,
                &record.name,
                record.visibility,
            ),
            visibility: Visibility::Private,
            type_params: record.type_params.clone(),
            fields: record
                .fields
                .iter()
                .map(|field| RecordFieldDecl {
                    name: field.name.clone(),
                    type_name: self.rewrite_type_expr_with_params(
                        &field.type_name,
                        field.span,
                        &record.type_params,
                    ),
                    span: field.span,
                })
                .collect(),
            span: record.span,
        }
    }

    fn rewrite_enum_decl(&mut self, enumeration: &EnumDecl) -> EnumDecl {
        if enumeration.visibility == Visibility::Public
            || enumeration.visibility == Visibility::Package
        {
            for variant in &enumeration.variants {
                if let Some(payload) = &variant.payload {
                    self.validate_visible_type_with_params(
                        payload,
                        enumeration.visibility,
                        variant.span,
                        &enumeration.type_params,
                    );
                }
            }
        }

        EnumDecl {
            id: enumeration.id,
            package_item: self.package_item_id(&enumeration.name, PackageItemKind::Enum),
            name: mangle_enum_name_for_visibility(
                &self.current_package,
                &self.current_module,
                &enumeration.name,
                enumeration.visibility,
            ),
            visibility: Visibility::Private,
            type_params: enumeration.type_params.clone(),
            variants: enumeration
                .variants
                .iter()
                .map(|variant| EnumVariantDecl {
                    name: variant.name.clone(),
                    payload: variant.payload.as_ref().map(|payload| {
                        self.rewrite_type_expr_with_params(
                            payload,
                            variant.span,
                            &enumeration.type_params,
                        )
                    }),
                    span: variant.span,
                })
                .collect(),
            span: enumeration.span,
        }
    }

    fn rewrite_func_decl(&mut self, func: &FuncDecl, top_level: bool) -> FuncDecl {
        if top_level && func.visibility == Visibility::Public {
            let has_full_signature = func.params.iter().all(|param| param.type_name.is_some())
                && func.return_type.is_some();
            if !has_full_signature {
                self.diagnostics.push(
                    Diagnostic::new(
                        "PK011",
                        "public functions must annotate every parameter and the return type",
                        func.span,
                    )
                    .with_suggestion("add parameter type annotations and an explicit return type"),
                );
            }
            for param in &func.params {
                if let Some(type_name) = &param.type_name {
                    self.validate_visible_type_with_params(
                        type_name,
                        Visibility::Public,
                        param.span,
                        &func.type_params,
                    );
                }
            }
            if let Some(type_name) = &func.return_type {
                self.validate_visible_type_with_params(
                    type_name,
                    Visibility::Public,
                    func.span,
                    &func.type_params,
                );
            }
        } else if top_level && func.visibility == Visibility::Package {
            for param in &func.params {
                if let Some(type_name) = &param.type_name {
                    self.validate_visible_type_with_params(
                        type_name,
                        Visibility::Package,
                        param.span,
                        &func.type_params,
                    );
                }
            }
            if let Some(type_name) = &func.return_type {
                self.validate_visible_type_with_params(
                    type_name,
                    Visibility::Package,
                    func.span,
                    &func.type_params,
                );
            }
        }

        let mut params = Vec::with_capacity(func.params.len());
        self.push_scope();
        for param in &func.params {
            self.insert_local(param.name.clone());
            params.push(Param {
                name: param.name.clone(),
                type_name: param.type_name.as_ref().map(|type_name| {
                    self.rewrite_type_expr_with_params(type_name, param.span, &func.type_params)
                }),
                span: param.span,
            });
        }

        let body = self.rewrite_value_block(&func.body);
        self.pop_scope();

        FuncDecl {
            id: func.id,
            package_item: if top_level {
                self.package_item_id(&func.name, PackageItemKind::Function)
            } else {
                None
            },
            name: if top_level {
                mangle_function_name_for_visibility(
                    &self.current_package,
                    &self.current_module,
                    &func.name,
                    func.visibility,
                    &self.entry_package,
                )
            } else {
                func.name.clone()
            },
            visibility: Visibility::Private,
            type_params: func.type_params.clone(),
            params,
            return_type: func.return_type.as_ref().map(|type_name| {
                self.rewrite_type_expr_with_params(type_name, func.span, &func.type_params)
            }),
            body,
            span: func.span,
        }
    }

    fn package_item_id(&self, name: &str, kind: PackageItemKind) -> Option<PackageItemId> {
        let package = self.package_graph.package_id(&self.current_package)?;
        let module = self
            .package_graph
            .module_id(package, &self.current_module)?;
        self.package_graph.item_id_in_module(module, name, kind)
    }

    fn rewrite_stmt(&mut self, statement: &Stmt) -> Stmt {
        match statement {
            Stmt::Assign(stmt) => {
                let value = self.rewrite_expr(&stmt.value);
                self.insert_local(stmt.name.clone());
                Stmt::Assign(AssignStmt {
                    id: stmt.id,
                    mutable: stmt.mutable,
                    name: stmt.name.clone(),
                    type_name: stmt
                        .type_name
                        .as_ref()
                        .map(|type_name| self.rewrite_type_expr(type_name, stmt.span)),
                    value,
                    span: stmt.span,
                })
            }
            Stmt::RecordDecl(record) => Stmt::RecordDecl(self.rewrite_record_decl(record)),
            Stmt::EnumDecl(enumeration) => Stmt::EnumDecl(self.rewrite_enum_decl(enumeration)),
            Stmt::FuncDecl(func) => Stmt::FuncDecl(self.rewrite_func_decl(func, false)),
            Stmt::If(stmt) => Stmt::If(IfStmt {
                id: stmt.id,
                condition: self.rewrite_expr(&stmt.condition),
                then_branch: self.rewrite_block(&stmt.then_branch),
                else_branch: stmt
                    .else_branch
                    .as_ref()
                    .map(|block| self.rewrite_block(block)),
                span: stmt.span,
            }),
            Stmt::While(stmt) => Stmt::While(WhileStmt {
                id: stmt.id,
                condition: self.rewrite_expr(&stmt.condition),
                body: self.rewrite_block(&stmt.body),
                span: stmt.span,
            }),
            Stmt::Expr(stmt) => Stmt::Expr(ExprStmt {
                id: stmt.id,
                expr: self.rewrite_expr(&stmt.expr),
                span: stmt.span,
            }),
        }
    }

    fn rewrite_block(&mut self, block: &Block) -> Block {
        self.push_scope();
        self.predeclare_nested_functions(&block.statements);
        let statements = block
            .statements
            .iter()
            .map(|statement| self.rewrite_stmt(statement))
            .collect();
        self.pop_scope();
        Block {
            statements,
            span: block.span,
        }
    }

    fn rewrite_value_block(&mut self, block: &ValueBlock) -> ValueBlock {
        self.push_scope();
        self.predeclare_nested_functions(&block.statements);
        let statements = block
            .statements
            .iter()
            .map(|statement| self.rewrite_stmt(statement))
            .collect();
        let expr = Box::new(self.rewrite_expr(&block.expr));
        self.pop_scope();
        ValueBlock {
            statements,
            expr,
            span: block.span,
        }
    }

    fn rewrite_expr(&mut self, expr: &Expr) -> Expr {
        match expr {
            Expr::Int(_) | Expr::Bool(_) | Expr::String(_) | Expr::Unit(_) => expr.clone(),
            Expr::Ident(expr) => Expr::Ident(IdentExpr {
                id: expr.id,
                name: self.rewrite_value_name(&expr.name, expr.span),
                span: expr.span,
            }),
            Expr::ListLit(expr) => Expr::ListLit(ListLitExpr {
                id: expr.id,
                items: expr
                    .items
                    .iter()
                    .map(|item| self.rewrite_expr(item))
                    .collect(),
                span: expr.span,
            }),
            Expr::Index(expr) => Expr::Index(IndexExpr {
                id: expr.id,
                base: Box::new(self.rewrite_expr(&expr.base)),
                index: Box::new(self.rewrite_expr(&expr.index)),
                span: expr.span,
            }),
            Expr::RecordLit(expr) => Expr::RecordLit(RecordLitExpr {
                id: expr.id,
                type_name: self.rewrite_type_name(&expr.type_name, expr.span),
                fields: expr
                    .fields
                    .iter()
                    .map(|field| RecordFieldInit {
                        name: field.name.clone(),
                        value: self.rewrite_expr(&field.value),
                        span: field.span,
                    })
                    .collect(),
                span: expr.span,
            }),
            Expr::Field(expr) => Expr::Field(FieldExpr {
                id: expr.id,
                base: Box::new(self.rewrite_expr(&expr.base)),
                field: expr.field.clone(),
                span: expr.span,
            }),
            Expr::RecordUpdate(expr) => Expr::RecordUpdate(RecordUpdateExpr {
                id: expr.id,
                base: Box::new(self.rewrite_expr(&expr.base)),
                fields: expr
                    .fields
                    .iter()
                    .map(|field| RecordFieldInit {
                        name: field.name.clone(),
                        value: self.rewrite_expr(&field.value),
                        span: field.span,
                    })
                    .collect(),
                span: expr.span,
            }),
            Expr::Unary(expr) => Expr::Unary(UnaryExpr {
                id: expr.id,
                op: expr.op,
                expr: Box::new(self.rewrite_expr(&expr.expr)),
                span: expr.span,
            }),
            Expr::Binary(expr) => Expr::Binary(BinaryExpr {
                id: expr.id,
                op: expr.op,
                left: Box::new(self.rewrite_expr(&expr.left)),
                right: Box::new(self.rewrite_expr(&expr.right)),
                span: expr.span,
            }),
            Expr::Call(expr) => Expr::Call(CallExpr {
                id: expr.id,
                callee: Box::new(self.rewrite_expr(&expr.callee)),
                args: expr.args.iter().map(|arg| self.rewrite_expr(arg)).collect(),
                origin: expr.origin,
                span: expr.span,
            }),
            Expr::Try(expr) => Expr::Try(TryExpr {
                id: expr.id,
                expr: Box::new(self.rewrite_expr(&expr.expr)),
                span: expr.span,
            }),
            Expr::If(expr) => Expr::If(IfExpr {
                id: expr.id,
                condition: Box::new(self.rewrite_expr(&expr.condition)),
                then_branch: self.rewrite_value_block(&expr.then_branch),
                else_branch: self.rewrite_value_block(&expr.else_branch),
                span: expr.span,
            }),
            Expr::Match(expr) => Expr::Match(self.rewrite_match_expr(expr)),
            Expr::Fn(expr) => Expr::Fn(self.rewrite_fn_expr(expr)),
        }
    }

    fn rewrite_match_expr(&mut self, expr: &MatchExpr) -> MatchExpr {
        let value = Box::new(self.rewrite_expr(&expr.value));
        let arms = expr
            .arms
            .iter()
            .map(|arm| {
                self.push_scope();
                let MatchPattern::Variant(pattern) = &arm.pattern;
                if let Some(binding) = &pattern.binding {
                    self.insert_local(binding.clone());
                }
                let value = self.rewrite_expr(&arm.value);
                self.pop_scope();
                MatchArm {
                    pattern: MatchPattern::Variant(EnumVariantPattern {
                        enum_name: self.rewrite_type_name(&pattern.enum_name, pattern.span),
                        variant_name: pattern.variant_name.clone(),
                        binding: pattern.binding.clone(),
                        span: pattern.span,
                    }),
                    value,
                    span: arm.span,
                }
            })
            .collect();
        MatchExpr {
            id: expr.id,
            value,
            arms,
            span: expr.span,
        }
    }

    fn rewrite_fn_expr(&mut self, expr: &FnExpr) -> FnExpr {
        let mut params = Vec::with_capacity(expr.params.len());
        self.push_scope();
        for param in &expr.params {
            self.insert_local(param.name.clone());
            params.push(Param {
                name: param.name.clone(),
                type_name: param
                    .type_name
                    .as_ref()
                    .map(|type_name| self.rewrite_type_expr(type_name, param.span)),
                span: param.span,
            });
        }
        let body = self.rewrite_value_block(&expr.body);
        self.pop_scope();
        FnExpr {
            id: expr.id,
            params,
            return_type: expr
                .return_type
                .as_ref()
                .map(|type_name| self.rewrite_type_expr(type_name, expr.span)),
            body,
            span: expr.span,
        }
    }

    fn rewrite_type_expr(&mut self, type_expr: &TypeExpr, span: Span) -> TypeExpr {
        self.rewrite_type_expr_with_params(type_expr, span, &[])
    }

    fn rewrite_type_expr_with_params(
        &mut self,
        type_expr: &TypeExpr,
        span: Span,
        type_params: &[String],
    ) -> TypeExpr {
        match type_expr {
            TypeExpr::Int => TypeExpr::Int,
            TypeExpr::Bool => TypeExpr::Bool,
            TypeExpr::String => TypeExpr::String,
            TypeExpr::Unit => TypeExpr::Unit,
            TypeExpr::Named(name) if type_params.iter().any(|param| param == name) => {
                TypeExpr::Named(name.clone())
            }
            TypeExpr::Named(name) => TypeExpr::Named(self.rewrite_type_name(name, span)),
            TypeExpr::Generic(generic) => TypeExpr::Generic(GenericTypeExpr {
                name: if type_params.iter().any(|param| param == &generic.name) {
                    generic.name.clone()
                } else {
                    self.rewrite_type_name(&generic.name, span)
                },
                args: generic
                    .args
                    .iter()
                    .map(|arg| self.rewrite_type_expr_with_params(arg, span, type_params))
                    .collect(),
            }),
            TypeExpr::Function(function) => TypeExpr::Function(FunctionTypeExpr {
                params: function
                    .params
                    .iter()
                    .map(|param| self.rewrite_type_expr_with_params(param, span, type_params))
                    .collect(),
                ret: Box::new(self.rewrite_type_expr_with_params(&function.ret, span, type_params)),
            }),
        }
    }

    fn rewrite_type_name(&mut self, name: &str, span: Span) -> String {
        if let Some((alias, item)) = split_qualified_name(name) {
            return self.resolve_imported_type_item(alias, item, span);
        }
        if let Some(item) = resolve_package_item(
            &self.current_package_data.records,
            name,
            &self.current_module,
        ) {
            return mangle_record_name_for_visibility(
                &self.current_package,
                &item.module_path,
                name,
                item.visibility,
            );
        }
        if let Some(item) =
            resolve_package_item(&self.current_package_data.enums, name, &self.current_module)
        {
            return mangle_enum_name_for_visibility(
                &self.current_package,
                &item.module_path,
                name,
                item.visibility,
            );
        }
        if let Some(item) = inaccessible_package_item(&self.current_package_data.records, name) {
            self.diagnostics.push(
                Diagnostic::new(
                    "PK015",
                    format!(
                        "record `{name}` is not visible from module `{}`",
                        self.current_module
                    ),
                    span,
                )
                .with_related(
                    format!(
                        "record `{name}` is module-private to `{}`",
                        item.module_path
                    ),
                    item.span,
                )
                .with_suggestion("mark the declaration as `pkg` to share it within the package"),
            );
        }
        if let Some(item) = inaccessible_package_item(&self.current_package_data.enums, name) {
            self.diagnostics.push(
                Diagnostic::new(
                    "PK015",
                    format!(
                        "enum `{name}` is not visible from module `{}`",
                        self.current_module
                    ),
                    span,
                )
                .with_related(
                    format!("enum `{name}` is module-private to `{}`", item.module_path),
                    item.span,
                )
                .with_suggestion("mark the declaration as `pkg` to share it within the package"),
            );
        }
        name.to_string()
    }

    fn rewrite_value_name(&mut self, name: &str, span: Span) -> String {
        if name.contains("::") && is_builtin_name(name) {
            return name.to_string();
        }
        if let Some((enum_name, variant_name)) = split_variant_name(name) {
            if is_mangled_item_name(enum_name) {
                return name.to_string();
            }
            if let Some((alias, item)) = split_qualified_name(enum_name) {
                let resolved =
                    self.resolve_imported_item(alias, item, ImportedItemKind::Enum, span);
                return format!("{resolved}::{variant_name}");
            }
            if let Some(item) = resolve_package_item(
                &self.current_package_data.enums,
                enum_name,
                &self.current_module,
            ) {
                let resolved = mangle_enum_name_for_visibility(
                    &self.current_package,
                    &item.module_path,
                    enum_name,
                    item.visibility,
                );
                return format!("{resolved}::{variant_name}");
            }
        }
        if let Some((alias, item)) = split_qualified_name(name) {
            return self.resolve_imported_item(alias, item, ImportedItemKind::Function, span);
        }
        if self.lookup_local(name) {
            return name.to_string();
        }
        if let Some(item) = resolve_package_item(
            &self.current_package_data.functions,
            name,
            &self.current_module,
        ) {
            return mangle_function_name_for_visibility(
                &self.current_package,
                &item.module_path,
                name,
                item.visibility,
                &self.entry_package,
            );
        }
        if is_builtin_name(name) {
            return name.to_string();
        }
        if let Some(item) = inaccessible_package_item(&self.current_package_data.functions, name) {
            self.diagnostics.push(
                Diagnostic::new(
                    "PK015",
                    format!(
                        "function `{name}` is not visible from module `{}`",
                        self.current_module
                    ),
                    span,
                )
                .with_related(
                    format!(
                        "function `{name}` is module-private to `{}`",
                        item.module_path
                    ),
                    item.span,
                )
                .with_suggestion("mark the declaration as `pkg` to share it within the package"),
            );
        }
        name.to_string()
    }

    fn validate_visible_type_with_params(
        &mut self,
        type_expr: &TypeExpr,
        api_visibility: Visibility,
        span: Span,
        type_params: &[String],
    ) {
        match type_expr {
            TypeExpr::Int | TypeExpr::Bool | TypeExpr::String | TypeExpr::Unit => {}
            TypeExpr::Named(name) => {
                if type_params.iter().any(|param| param == name) {
                    return;
                }
                if let Some((alias, item)) = split_qualified_name(name) {
                    let _ = self.resolve_imported_type_item(alias, item, span);
                    return;
                }
                if let Some(item) = resolve_package_item(
                    &self.current_package_data.records,
                    name,
                    &self.current_module,
                ) && !visibility_can_expose(item.visibility, api_visibility)
                {
                    let api = visibility_label(api_visibility);
                    let item_visibility = visibility_label(item.visibility);
                    self.diagnostics.push(
                        Diagnostic::new(
                            "PK012",
                            format!("{api} API may not expose {item_visibility} record `{name}`"),
                            span,
                        )
                        .with_related(format!("record `{name}` is declared here"), item.span),
                    );
                }
                if let Some(item) = resolve_package_item(
                    &self.current_package_data.enums,
                    name,
                    &self.current_module,
                ) && !visibility_can_expose(item.visibility, api_visibility)
                {
                    let api = visibility_label(api_visibility);
                    let item_visibility = visibility_label(item.visibility);
                    self.diagnostics.push(
                        Diagnostic::new(
                            "PK012",
                            format!("{api} API may not expose {item_visibility} enum `{name}`"),
                            span,
                        )
                        .with_related(format!("enum `{name}` is declared here"), item.span),
                    );
                }
            }
            TypeExpr::Generic(generic) => {
                if !type_params.iter().any(|param| param == &generic.name)
                    && !is_known_generic_type_name(&generic.name)
                {
                    self.validate_visible_type_with_params(
                        &TypeExpr::Named(generic.name.clone()),
                        api_visibility,
                        span,
                        type_params,
                    );
                }
                for arg in &generic.args {
                    self.validate_visible_type_with_params(arg, api_visibility, span, type_params);
                }
            }
            TypeExpr::Function(function) => {
                for param in &function.params {
                    self.validate_visible_type_with_params(
                        param,
                        api_visibility,
                        span,
                        type_params,
                    );
                }
                self.validate_visible_type_with_params(
                    &function.ret,
                    api_visibility,
                    span,
                    type_params,
                );
            }
        }
    }

    fn resolve_imported_item(
        &mut self,
        alias: &str,
        item: &str,
        kind: ImportedItemKind,
        span: Span,
    ) -> String {
        let Some(package_path) = self.imports.get(alias) else {
            self.diagnostics.push(Diagnostic::new(
                "PK009",
                format!("unknown import alias `{alias}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };
        let Some(package) = self.packages.get(package_path) else {
            self.diagnostics.push(Diagnostic::new(
                "PK010",
                format!("unknown imported package `{package_path}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };
        let Some(package_id) = self.package_graph.package_id(package_path) else {
            self.diagnostics.push(Diagnostic::new(
                "PK010",
                format!("unknown imported package `{package_path}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };

        match kind {
            ImportedItemKind::Enum => {
                if let Some(export) = self.package_exports.enum_by_name(package_id, item) {
                    export.mangled_name.clone()
                } else {
                    let diagnostic = missing_export_diagnostic(
                        package_path,
                        item,
                        "enum",
                        package_item_decl(&package.enums, item),
                        span,
                    );
                    self.diagnostics.push(diagnostic);
                    format!("{alias}::{item}")
                }
            }
            ImportedItemKind::Function => {
                if let Some(export) = self.package_exports.function_by_name(package_id, item) {
                    export.mangled_name.clone()
                } else {
                    let diagnostic = missing_export_diagnostic(
                        package_path,
                        item,
                        "function",
                        package_item_decl(&package.functions, item),
                        span,
                    );
                    self.diagnostics.push(diagnostic);
                    format!("{alias}::{item}")
                }
            }
        }
    }

    fn resolve_imported_type_item(&mut self, alias: &str, item: &str, span: Span) -> String {
        let Some(package_path) = self.imports.get(alias) else {
            self.diagnostics.push(Diagnostic::new(
                "PK009",
                format!("unknown import alias `{alias}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };
        let Some(package) = self.packages.get(package_path) else {
            self.diagnostics.push(Diagnostic::new(
                "PK010",
                format!("unknown imported package `{package_path}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };
        let Some(package_id) = self.package_graph.package_id(package_path) else {
            self.diagnostics.push(Diagnostic::new(
                "PK010",
                format!("unknown imported package `{package_path}`"),
                span,
            ));
            return format!("{alias}::{item}");
        };

        if let Some(export) = self.package_exports.record_by_name(package_id, item) {
            return export.mangled_name.clone();
        }
        if package_item_decl(&package.records, item).is_some() {
            self.diagnostics.push(missing_export_diagnostic(
                package_path,
                item,
                "record",
                package_item_decl(&package.records, item),
                span,
            ));
            return format!("{alias}::{item}");
        }
        if let Some(export) = self.package_exports.enum_by_name(package_id, item) {
            return export.mangled_name.clone();
        }
        self.diagnostics.push(missing_export_diagnostic(
            package_path,
            item,
            "enum",
            package_item_decl(&package.enums, item),
            span,
        ));
        format!("{alias}::{item}")
    }

    fn predeclare_nested_functions(&mut self, statements: &[Stmt]) {
        for statement in statements {
            if let Stmt::FuncDecl(func) = statement {
                self.insert_local(func.name.clone());
            }
        }
    }

    fn push_scope(&mut self) {
        self.scopes.push(HashSet::new());
    }

    fn pop_scope(&mut self) {
        self.scopes.pop();
    }

    fn insert_local(&mut self, name: String) {
        if let Some(scope) = self.scopes.last_mut() {
            scope.insert(name);
        }
    }

    fn lookup_local(&self, name: &str) -> bool {
        self.scopes.iter().rev().any(|scope| scope.contains(name))
    }
}

#[derive(Clone, Copy)]
enum ImportedItemKind {
    Enum,
    Function,
}

fn collect_package_data(
    package_path: &str,
    files: Vec<ParsedFile>,
    diagnostics: &mut Vec<Diagnostic>,
) -> PackageData {
    let mut records: HashMap<String, Vec<PackageItemDecl>> = HashMap::new();
    let mut enums: HashMap<String, Vec<PackageItemDecl>> = HashMap::new();
    let mut functions: HashMap<String, Vec<PackageItemDecl>> = HashMap::new();

    for file in &files {
        for statement in &file.program.statements {
            match statement {
                Stmt::RecordDecl(record) => {
                    insert_package_item_decl(
                        &mut records,
                        PackageItemDeclInput {
                            name: &record.name,
                            visibility: record.visibility,
                            module_path: &file.module_path,
                            span: record.span,
                            kind: PackageItemKind::Record,
                            package_path,
                        },
                        diagnostics,
                    );
                }
                Stmt::EnumDecl(enumeration) => {
                    insert_package_item_decl(
                        &mut enums,
                        PackageItemDeclInput {
                            name: &enumeration.name,
                            visibility: enumeration.visibility,
                            module_path: &file.module_path,
                            span: enumeration.span,
                            kind: PackageItemKind::Enum,
                            package_path,
                        },
                        diagnostics,
                    );
                }
                Stmt::FuncDecl(func) => {
                    insert_package_item_decl(
                        &mut functions,
                        PackageItemDeclInput {
                            name: &func.name,
                            visibility: func.visibility,
                            module_path: &file.module_path,
                            span: func.span,
                            kind: PackageItemKind::Function,
                            package_path,
                        },
                        diagnostics,
                    );
                }
                _ => {}
            }
        }
    }

    PackageData {
        files,
        records,
        enums,
        functions,
    }
}

fn package_import_paths(package: &PackageData) -> Vec<String> {
    let mut paths: Vec<String> = package
        .files
        .iter()
        .flat_map(|file| {
            file.program
                .imports
                .iter()
                .map(|import| import.path.clone())
        })
        .collect::<HashSet<_>>()
        .into_iter()
        .collect();
    paths.sort();
    paths
}

const INTERFACE_MODULE: &str = "<interface>";

fn interface_item_id(
    package_interface: Option<&PackageInterface>,
    name: &str,
    kind: PackageItemKind,
    visibility: Visibility,
) -> Option<PackageItemId> {
    if visibility != Visibility::Public {
        return None;
    }
    let interface = package_interface?;
    match kind {
        PackageItemKind::Record => interface
            .records
            .iter()
            .find(|record| record.name == name)
            .map(|record| record.item),
        PackageItemKind::Enum => interface
            .enums
            .iter()
            .find(|enumeration| enumeration.name == name)
            .map(|enumeration| enumeration.item),
        PackageItemKind::Function => interface
            .functions
            .iter()
            .find(|function| function.name == name)
            .map(|function| function.item),
    }
}

fn allocate_package_item_id(items: &mut Vec<Option<PackageItemInfo>>) -> PackageItemId {
    let id = PackageItemId::new(items.len() as u32);
    items.push(None);
    id
}

fn insert_package_graph_item(
    items: &mut Vec<Option<PackageItemInfo>>,
    item: PackageItemInfo,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let index = item.id.as_u32() as usize;
    if index >= items.len() {
        items.resize(index + 1, None);
    }
    if items[index].is_some() {
        diagnostics.push(
            Diagnostic::new(
                "PK017",
                format!("duplicate package interface item identity {:?}", item.id),
                item.span,
            )
            .with_suggestion("regenerate the package interface"),
        );
    } else {
        items[index] = Some(item);
    }
}

fn insert_package_item_decl(
    items: &mut HashMap<String, Vec<PackageItemDecl>>,
    decl: PackageItemDeclInput<'_>,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let existing = items.entry(decl.name.to_string()).or_default();
    let duplicate = existing.iter().find(|item| {
        item.module_path == decl.module_path
            || (decl.visibility != Visibility::Private && item.visibility != Visibility::Private)
    });

    if let Some(previous) = duplicate {
        let kind_name = match decl.kind {
            PackageItemKind::Record => "record",
            PackageItemKind::Enum => "enum",
            PackageItemKind::Function => "function",
        };
        diagnostics.push(
            Diagnostic::new(
                "PK013",
                format!(
                    "duplicate top-level {kind_name} `{}` in package `{}`",
                    decl.name, decl.package_path
                ),
                decl.span,
            )
            .with_related(
                format!("previous `{}` declaration is here", decl.name),
                previous.span,
            ),
        );
    }

    existing.push(PackageItemDecl {
        visibility: decl.visibility,
        module_path: decl.module_path.to_string(),
        span: decl.span,
    });
}

fn resolve_package_item<'a>(
    items: &'a HashMap<String, Vec<PackageItemDecl>>,
    name: &str,
    current_module: &str,
) -> Option<&'a PackageItemDecl> {
    let candidates = items.get(name)?;
    candidates
        .iter()
        .find(|item| item.module_path == current_module)
        .or_else(|| {
            candidates.iter().find(|item| {
                item.visibility == Visibility::Package || item.visibility == Visibility::Public
            })
        })
}

fn inaccessible_package_item<'a>(
    items: &'a HashMap<String, Vec<PackageItemDecl>>,
    name: &str,
) -> Option<&'a PackageItemDecl> {
    items
        .get(name)?
        .iter()
        .find(|item| item.visibility == Visibility::Private)
}

fn package_item_decl<'a>(
    items: &'a HashMap<String, Vec<PackageItemDecl>>,
    name: &str,
) -> Option<&'a PackageItemDecl> {
    items.get(name)?.first()
}

fn missing_export_diagnostic(
    package_path: &str,
    item: &str,
    kind: &str,
    declaration: Option<&PackageItemDecl>,
    span: Span,
) -> Diagnostic {
    let mut diagnostic = Diagnostic::new(
        "PK010",
        format!("package `{package_path}` does not export {kind} `{item}`"),
        span,
    );
    if let Some(declaration) = declaration {
        diagnostic = diagnostic
            .with_related(
                format!("{kind} `{item}` is declared here but is not public"),
                declaration.span,
            )
            .with_suggestion(format!(
                "mark the {kind} declaration as `pub` to export it from the package"
            ));
    }
    diagnostic
}

fn visibility_can_expose(item_visibility: Visibility, api_visibility: Visibility) -> bool {
    match api_visibility {
        Visibility::Public => item_visibility == Visibility::Public,
        Visibility::Package => {
            item_visibility == Visibility::Package || item_visibility == Visibility::Public
        }
        Visibility::Private => true,
    }
}

fn visibility_label(visibility: Visibility) -> &'static str {
    match visibility {
        Visibility::Private => "module-private",
        Visibility::Package => "package-visible",
        Visibility::Public => "public",
    }
}

fn package_item_kind_label(kind: PackageItemKind) -> &'static str {
    match kind {
        PackageItemKind::Record => "record",
        PackageItemKind::Enum => "enum",
        PackageItemKind::Function => "function",
    }
}

fn is_known_generic_type_name(name: &str) -> bool {
    matches!(name, "List" | "Map")
        || name == crate::known_enum::OPTION_NAME
        || name == crate::known_enum::RESULT_NAME
}

fn infer_source_root(entry_file: &Path, package_path: &str) -> Result<PathBuf, Diagnostic> {
    let package_segments = split_package_path(package_path);
    let Some(dir) = entry_file.parent() else {
        return Err(Diagnostic::new(
            "PK003",
            "entry file must live inside a package directory",
            Span::default(),
        ));
    };

    let dir_segments: Vec<String> = dir
        .iter()
        .map(|segment| segment.to_string_lossy().into_owned())
        .collect();
    if dir_segments.len() < package_segments.len()
        || dir_segments[dir_segments.len() - package_segments.len()..] != package_segments
    {
        return Err(Diagnostic::new(
            "PK003",
            format!(
                "package path `{package_path}` must match the directory layout of {}",
                entry_file.display()
            ),
            Span::default(),
        ));
    }

    let mut root = dir.to_path_buf();
    for _ in 0..package_segments.len() {
        root = root.parent().map(Path::to_path_buf).ok_or_else(|| {
            Diagnostic::new(
                "PK003",
                format!(
                    "package path `{package_path}` must match the directory layout of {}",
                    entry_file.display()
                ),
                Span::default(),
            )
        })?;
    }
    Ok(root)
}

fn discover_manifest(entry_file: &Path) -> Result<Option<ProjectManifest>, Vec<Diagnostic>> {
    let mut current = entry_file.parent();
    while let Some(dir) = current {
        let manifest_path = dir.join("muga.toml");
        if manifest_path.is_file() {
            return parse_manifest(&manifest_path).map(Some);
        }
        current = dir.parent();
    }
    Ok(None)
}

fn parse_manifest(path: &Path) -> Result<ProjectManifest, Vec<Diagnostic>> {
    let source = fs::read_to_string(path).map_err(|error| {
        vec![Diagnostic::new(
            "PK002",
            format!("failed to read {}: {error}", path.display()),
            Span::default(),
        )]
    })?;

    let mut in_package = false;
    let mut name = None;
    let mut source_dir = "src".to_string();

    for raw_line in source.lines() {
        let line = raw_line.split('#').next().unwrap_or("").trim();
        if line.is_empty() {
            continue;
        }
        if line.starts_with('[') && line.ends_with(']') {
            in_package = line == "[package]";
            continue;
        }
        if !in_package {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim();
        let Some(value) = parse_manifest_string(value.trim()) else {
            return Err(vec![Diagnostic::new(
                "PK014",
                format!(
                    "manifest field `{key}` in {} must be a string",
                    path.display()
                ),
                Span::default(),
            )]);
        };
        match key {
            "name" => name = Some(value),
            "source" => source_dir = value,
            _ => {}
        }
    }

    let Some(name) = name else {
        return Err(vec![Diagnostic::new(
            "PK014",
            format!("manifest {} must define [package] name", path.display()),
            Span::default(),
        )]);
    };
    if !is_valid_package_path(&name) {
        return Err(vec![Diagnostic::new(
            "PK014",
            format!("manifest package name `{name}` is not a valid package path"),
            Span::default(),
        )]);
    }

    let root = path.parent().map(Path::to_path_buf).unwrap_or_default();
    let source_root = if Path::new(&source_dir).is_absolute() {
        PathBuf::from(source_dir)
    } else {
        root.join(source_dir)
    };

    Ok(ProjectManifest { source_root, name })
}

fn parse_manifest_string(value: &str) -> Option<String> {
    value
        .strip_prefix('"')
        .and_then(|value| value.strip_suffix('"'))
        .map(ToString::to_string)
}

fn infer_manifest_package_path(
    entry_file: &Path,
    manifest: &ProjectManifest,
) -> Result<String, Vec<Diagnostic>> {
    let Some(package_dir) = entry_file.parent() else {
        return Err(vec![Diagnostic::new(
            "PK003",
            "entry file must live inside a package directory",
            Span::default(),
        )]);
    };
    let relative = package_dir
        .strip_prefix(&manifest.source_root)
        .map_err(|_| {
            vec![Diagnostic::new(
                "PK003",
                format!(
                    "entry file {} must live under manifest source root {}",
                    entry_file.display(),
                    manifest.source_root.display()
                ),
                Span::default(),
            )]
        })?;

    let mut segments = vec![manifest.name.clone()];
    for component in relative {
        let Some(segment) = component.to_str() else {
            return Err(vec![Diagnostic::new(
                "PK003",
                format!(
                    "package path for {} contains non-UTF-8 segment",
                    entry_file.display()
                ),
                Span::default(),
            )]);
        };
        if segment.is_empty() {
            continue;
        }
        if !is_valid_package_segment(segment) {
            return Err(vec![Diagnostic::new(
                "PK003",
                format!(
                    "directory segment `{segment}` in {} is not a valid package segment",
                    entry_file.display()
                ),
                Span::default(),
            )]);
        }
        segments.push(segment.to_string());
    }

    Ok(segments.join("::"))
}

fn is_valid_package_path(path: &str) -> bool {
    !path.is_empty() && path.split("::").all(is_valid_package_segment)
}

fn is_valid_package_segment(segment: &str) -> bool {
    let mut chars = segment.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    (first.is_ascii_alphabetic() || first == '_')
        && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}

fn split_package_path(path: &str) -> Vec<String> {
    path.split("::").map(ToString::to_string).collect()
}

fn module_path_for_file(path: &Path) -> String {
    path.file_name()
        .map(|name| name.to_string_lossy().into_owned())
        .unwrap_or_else(|| path.to_string_lossy().into_owned())
}

fn split_qualified_name(name: &str) -> Option<(&str, &str)> {
    let mut parts = name.split("::");
    let first = parts.next()?;
    let second = parts.next()?;
    if parts.next().is_some() {
        None
    } else {
        Some((first, second))
    }
}

fn split_variant_name(name: &str) -> Option<(&str, &str)> {
    name.rsplit_once("::")
}

fn file_import_aliases(
    imports: &[ImportDecl],
    diagnostics: &mut Vec<Diagnostic>,
) -> HashMap<String, String> {
    let mut aliases = HashMap::new();
    for import in imports {
        if let Some((previous, previous_span)) = aliases.get(&import.alias) {
            diagnostics.push(
                Diagnostic::new(
                    "PK007",
                    format!(
                        "duplicate import alias `{}` for `{}` and `{}`",
                        import.alias, previous, import.path
                    ),
                    import.span,
                )
                .with_related("previous import using this alias is here", *previous_span)
                .with_suggestion("use `as` to give one import a distinct local alias"),
            );
        } else {
            aliases.insert(import.alias.clone(), (import.path.clone(), import.span));
        }
    }
    aliases
        .into_iter()
        .map(|(alias, (path, _))| (alias, path))
        .collect()
}

fn mangle_function_name(package_path: &str, name: &str, entry_package: &str) -> String {
    if package_path == entry_package && name == "main" {
        "main".to_string()
    } else {
        format!("__muga_pkg__{}__{}", package_path.replace("::", "__"), name)
    }
}

fn mangle_function_name_for_visibility(
    package_path: &str,
    module_path: &str,
    name: &str,
    visibility: Visibility,
    entry_package: &str,
) -> String {
    if package_path == entry_package && name == "main" {
        return "main".to_string();
    }
    match visibility {
        Visibility::Private => mangle_module_item_name(package_path, module_path, name),
        Visibility::Package | Visibility::Public => {
            mangle_function_name(package_path, name, entry_package)
        }
    }
}

fn mangle_record_name(package_path: &str, name: &str) -> String {
    format!("__muga_pkg__{}__{}", package_path.replace("::", "__"), name)
}

fn mangle_record_name_for_visibility(
    package_path: &str,
    module_path: &str,
    name: &str,
    visibility: Visibility,
) -> String {
    match visibility {
        Visibility::Private => mangle_module_item_name(package_path, module_path, name),
        Visibility::Package | Visibility::Public => mangle_record_name(package_path, name),
    }
}

fn mangle_enum_name(package_path: &str, name: &str) -> String {
    format!("__muga_pkg__{}__{}", package_path.replace("::", "__"), name)
}

fn mangle_enum_name_for_visibility(
    package_path: &str,
    module_path: &str,
    name: &str,
    visibility: Visibility,
) -> String {
    match visibility {
        Visibility::Private => mangle_module_item_name(package_path, module_path, name),
        Visibility::Package | Visibility::Public => mangle_enum_name(package_path, name),
    }
}

fn mangle_module_item_name(package_path: &str, module_path: &str, name: &str) -> String {
    format!(
        "__muga_mod__{}__{}__{}",
        package_path.replace("::", "__"),
        sanitize_mangle_segment(module_path),
        name
    )
}

fn sanitize_mangle_segment(segment: &str) -> String {
    segment
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '_' {
                ch
            } else {
                '_'
            }
        })
        .collect()
}

fn is_builtin_name(name: &str) -> bool {
    crate::prelude::is_builtin_name(name)
}

fn is_mangled_item_name(name: &str) -> bool {
    name.starts_with("__muga_pkg__") || name.starts_with("__muga_mod__")
}
