use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use crate::core::config::{AdapterPattern, Language, ResolvedCrateConfig, resolve_output_dir};
use crate::core::ir::{ApiSurface, TypeRef};
use heck::AsSnakeCase;
use std::path::PathBuf;

use crate::backends::zig::trait_bridge::emit_trait_bridge;

mod errors;
mod functions;
mod helpers;
mod opaque_handles;
mod service_api;
mod types;

use errors::emit_error_set;
use functions::emit_function;
use helpers::emit_helpers;
use opaque_handles::{emit_opaque_constructor, emit_opaque_handle};
use types::{emit_enum, emit_type};

fn zig_module_name(crate_name: &str) -> String {
    crate_name.replace('-', "_")
}

pub struct ZigBackend;

fn type_references_excluded(ty: &TypeRef, exclude_types: &std::collections::HashSet<String>) -> bool {
    exclude_types.iter().any(|name| ty.references_named(name))
}

fn signature_references_excluded(
    params: &[crate::core::ir::ParamDef],
    return_type: &TypeRef,
    exclude_types: &std::collections::HashSet<String>,
) -> bool {
    type_references_excluded(return_type, exclude_types)
        || params
            .iter()
            .any(|param| type_references_excluded(&param.ty, exclude_types))
}

impl Backend for ZigBackend {
    fn name(&self) -> &str {
        "zig"
    }

    fn language(&self) -> Language {
        Language::Zig
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities {
            supports_async: false,
            supports_classes: true,
            supports_enums: true,
            supports_option: true,
            supports_result: true,
            supports_callbacks: false,
            supports_streaming: false,
            supports_service_api: true,
        }
    }

    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let api = api.with_deduped_functions();
        let module_name = zig_module_name(&config.name);
        let header = config.ffi_header_name();
        let prefix = config.ffi_prefix();

        let mut exclude_functions: std::collections::HashSet<String> = config
            .zig
            .as_ref()
            .map(|c| c.exclude_functions.iter().cloned().collect())
            .unwrap_or_default();
        let mut exclude_types: std::collections::HashSet<String> = config
            .ffi
            .as_ref()
            .map(|c| c.exclude_types.iter().cloned().collect())
            .unwrap_or_default();
        if let Some(zig) = &config.zig {
            exclude_types.extend(zig.exclude_types.iter().cloned());
        }
        if let Some(ffi) = &config.ffi {
            exclude_functions.extend(ffi.exclude_functions.iter().cloned());
        }
        // Extend exclude_types with service-bound types flagged binding_excluded
        // (service owners and handler-contract types are emitted via service_api path)
        exclude_types.extend(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.clone()));
        // Mirror the FFI backend's `contains('<')` filter for workspace-declared opaque
        // types with generic-parameter rust_paths — the FFI backend skips `_new`/`_free`
        // symbols for them, so Zig `extern fn` declarations against those symbols would
        // fail to link.
        exclude_types.extend(
            config
                .opaque_types
                .iter()
                .filter(|(_, path)| path.contains('<'))
                .map(|(name, _)| name.clone()),
        );

        let type_is_visible = |name: &str| !exclude_types.contains(name);
        let method_is_visible = |method: &crate::core::ir::MethodDef| {
            !signature_references_excluded(&method.params, &method.return_type, &exclude_types)
        };

        let api = if exclude_types.is_empty() {
            api
        } else {
            let mut filtered = api.clone();
            filtered.types.retain(|typ| type_is_visible(&typ.name));
            for typ in &mut filtered.types {
                typ.fields
                    .retain(|field| !type_references_excluded(&field.ty, &exclude_types));
                typ.methods.retain(method_is_visible);
            }
            filtered.enums.retain(|en| !exclude_types.contains(&en.name));
            filtered
                .functions
                .retain(|func| !signature_references_excluded(&func.params, &func.return_type, &exclude_types));
            filtered
        };

        // Host-native capsule (Language-passthrough) types configured under `[crates.zig.capsule_types]`.
        let zig_capsule_types: std::collections::HashMap<String, crate::core::config::HostCapsuleTypeConfig> =
            config.zig.as_ref().map(|c| c.capsule_types.clone()).unwrap_or_default();
        let uses_capsule = !zig_capsule_types.is_empty()
            && api.functions.iter().any(|f| {
                matches!(&f.return_type, crate::core::ir::TypeRef::Named(n) if zig_capsule_types.contains_key(n.as_str()))
            });

        let mut content = String::new();
        content.push_str("// Generated by alef. Do not edit by hand.\n");
        content.push('\n');
        content.push_str("const std = @import(\"std\");\n");
        // Emit a `const {module} = @import("{module}");` for the capsule host package.
        // Derive the module name from the capsule host_type (e.g. `?*const my_mod.Language`
        // → `my_mod`) so the import is fully config-driven — no tree-sitter literals.
        if uses_capsule {
            // Collect distinct import names from all capsule types used by capsule functions.
            let capsule_import_names: std::collections::BTreeSet<&str> = api
                .functions
                .iter()
                .filter_map(|f| {
                    if let crate::core::ir::TypeRef::Named(n) = &f.return_type {
                        zig_capsule_types.get(n.as_str())
                    } else {
                        None
                    }
                })
                .filter_map(|cap| crate::core::config::languages::zig_capsule_import_name(&cap.host_type))
                .collect();
            for import_name in capsule_import_names {
                content.push_str(&format!("const {import_name} = @import(\"{import_name}\");\n"));
            }
        }
        content.push_str(&crate::backends::zig::template_env::render(
            "c_import.jinja",
            minijinja::context! {
                header => header,
            },
        ));
        content.push('\n');

        // Emit helper wrappers for FFI error introspection and string ownership.
        // Pass the list of declared error-set type names so `_error_with_message`
        // can dispatch to the per-error `_from_ffi_msg_<name>` matchers emitted
        // alongside each error set below.
        let declared_error_names: Vec<String> = api.errors.iter().map(|e| e.name.clone()).collect();
        emit_helpers(&prefix, &declared_error_names, &mut content);
        content.push('\n');

        // Z1 fix: emit opaque pointer type aliases for every configured trait bridge
        // that declares a `type_alias`. These aliases are referenced in struct fields
        // (e.g. `visitor: ?VisitorHandle`) but the Zig backend previously never declared
        // them, causing "use of undeclared identifier" errors in Zig 0.16+.
        for bridge in &config.trait_bridges {
            if bridge.exclude_languages.iter().any(|lang| lang == "zig") {
                continue;
            }
            if let Some(alias) = &bridge.type_alias {
                content.push_str(&crate::backends::zig::template_env::render(
                    "trait_bridge_alias.jinja",
                    minijinja::context! {
                        alias => alias,
                    },
                ));
                content.push('\n');
            }
        }

        for error in &api.errors {
            emit_error_set(error, &mut content);
            content.push('\n');
        }

        for ty in api
            .types
            .iter()
            .filter(|t| !exclude_types.contains(&t.name) && !t.is_opaque && t.has_serde)
        {
            emit_type(ty, &mut content);
            content.push('\n');
        }

        for en in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
            emit_enum(en, &mut content);
            content.push('\n');
        }

        let declared_errors: Vec<String> = api.errors.iter().map(|e| e.name.clone()).collect();
        // Collect all top-level decl names so we can rename param names that would
        // shadow them. Zig 0.16 forbids parameter shadowing of file-scope decls.
        let mut top_level_names: std::collections::HashSet<String> = std::collections::HashSet::new();
        for f in &api.functions {
            top_level_names.insert(f.name.clone());
        }
        for ty in &api.types {
            top_level_names.insert(ty.name.clone());
        }
        for en in &api.enums {
            top_level_names.insert(en.name.clone());
        }
        // Set of struct (non-enum, non-trait) type names. Function parameters
        // typed as `Named(name)` where `name ∈ struct_names` are passed across
        // the FFI as opaque handles via JSON, since cbindgen emits them as
        // opaque types in the generated header.
        let struct_names: std::collections::HashSet<String> = api
            .types
            .iter()
            .filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
            .map(|t| t.name.clone())
            .collect();
        // Set of enum type names. Enum parameters are marshalled to i32 discriminants
        // using @intFromEnum() rather than being passed through JSON.
        let enum_names: std::collections::HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
        // For opaque handle types (is_opaque = true or has_serde = false), find the
        // creator function in api.functions that returns that type. The map stores:
        //   opaque_type_name -> (creator_fn_name, config_type_snake_case)
        // Used by emit_function to generate create+use+free patterns for handle params.
        let opaque_creator_map: std::collections::HashMap<String, (String, String)> = {
            let mut map = std::collections::HashMap::new();
            for opaque_ty in api
                .types
                .iter()
                .filter(|t| !t.is_trait && (t.is_opaque || !t.has_serde))
            {
                if let Some(creator) = api
                    .functions
                    .iter()
                    .find(|f| matches!(&f.return_type, crate::core::ir::TypeRef::Named(n) if n == &opaque_ty.name))
                {
                    if let Some(config_param) = creator.params.first() {
                        if let Some(config_name) = functions::opaque_type_name_inner(&config_param.ty) {
                            map.insert(
                                opaque_ty.name.clone(),
                                (creator.name.clone(), heck::AsSnakeCase(config_name).to_string()),
                            );
                        }
                    }
                }
            }
            map
        };

        // Functions matching `register_{trait_snake}` / `unregister_{trait_snake}` /
        // the configured `clear_fn` for any configured trait bridge are emitted by
        // `emit_trait_bridge` with a proper vtable signature. Skip the regular
        // C-FFI shim to avoid duplicate function definitions.
        let trait_bridge_fn_names: std::collections::HashSet<String> = config
            .trait_bridges
            .iter()
            .filter(|b| !b.exclude_languages.iter().any(|lang| lang == "zig"))
            .flat_map(|b| {
                let mut names = Vec::new();
                if let Some(trait_def) = api.types.iter().find(|t| t.name == b.trait_name && t.is_trait) {
                    let snake = heck::AsSnakeCase(&trait_def.name).to_string();
                    names.push(format!("register_{snake}"));
                    names.push(format!("unregister_{snake}"));
                }
                if let Some(clear_fn) = b.clear_fn.as_deref() {
                    names.push(clear_fn.to_string());
                }
                names
            })
            .collect();
        for f in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
            if trait_bridge_fn_names.contains(&f.name) {
                continue;
            }
            emit_function(
                f,
                &prefix,
                &declared_errors,
                &top_level_names,
                &struct_names,
                &opaque_creator_map,
                &zig_capsule_types,
                &mut content,
            );
            content.push('\n');
        }

        // Emit C-vtable structs and registration shims for every configured trait bridge.
        // Zig has no inheritance; trait bridges use an `extern struct` of function pointers.
        let error_type = config.error_type.as_deref().unwrap_or("error");
        for bridge_cfg in &config.trait_bridges {
            // Skip if "zig" is listed in exclude_languages for this bridge.
            if bridge_cfg.exclude_languages.iter().any(|lang| lang == "zig") {
                continue;
            }
            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
                // CRITICAL INVARIANT: emit_trait_bridge must be called unconditionally for every
                // discovered trait. This ensures that `make_{trait_snake}_vtable` comptime builders
                // are emitted, which e2e test fixtures depend on. See trait_bridge.rs line 663.
                emit_trait_bridge(&prefix, error_type, bridge_cfg, trait_def, &exclude_types, &mut content);
                content.push('\n');
            } else {
                let snake = AsSnakeCase(&bridge_cfg.trait_name).to_string();
                return Err(anyhow::anyhow!(
                    "zig backend: trait bridge '{}' has no trait definition in binding surface. \
                    Vtable builders (e.g., make_{}_vtable) will be undefined, breaking e2e tests. \
                    Check that the trait is not in exclude_types or marked binding_excluded.",
                    bridge_cfg.trait_name,
                    snake
                ));
            }
        }

        // Build a map of method_name -> item_type for Streaming adapters.
        // Used by emit_opaque_handle to emit iterator-based bodies instead of
        // the generic method wrapper (which would call the callback-based C symbol
        // with the wrong argument count).
        let streaming_item_types: std::collections::HashMap<String, String> = config
            .adapters
            .iter()
            .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
            .filter_map(|a| a.item_type.as_ref().map(|item| (a.name.clone(), item.clone())))
            .collect();

        // Collect trait-bridge `type_alias` names — these are emitted as
        // `pub const <Alias> = *anyopaque;` above and must not be re-emitted as
        // struct wrappers, since the bridge contract is a raw opaque pointer.
        let trait_bridge_type_aliases: std::collections::HashSet<String> = config
            .trait_bridges
            .iter()
            .filter(|b| !b.exclude_languages.iter().any(|lang| lang == "zig"))
            .filter_map(|b| b.type_alias.clone())
            .collect();

        // Emit Zig struct wrappers for all opaque handle types, including those
        // with no instance methods (e.g. a bare Language handle returned by
        // get_language()). Every opaque type that appears in a function signature
        // must be declared; omitting method-less types causes "use of undeclared
        // identifier" compile errors in Zig.
        for ty in api
            .types
            .iter()
            .filter(|t| !t.is_trait && (t.is_opaque || !t.has_serde))
            .filter(|t| !exclude_types.contains(&t.name))
            .filter(|t| !trait_bridge_type_aliases.contains(&t.name))
        {
            emit_opaque_handle(
                ty,
                &prefix,
                &declared_errors,
                &struct_names,
                &streaming_item_types,
                &enum_names,
                &mut content,
            );
            content.push('\n');
            // Client constructor — emit create_<type_snake> when configured.
            if let Some(ctor) = config.client_constructors.get(&ty.name) {
                emit_opaque_constructor(ty, &prefix, ctor, &top_level_names, &mut content);
                content.push('\n');
            }
        }

        let dir = resolve_output_dir(None, &config.name, "packages/zig/src");
        let path = PathBuf::from(dir).join(format!("{module_name}.zig"));

        Ok(vec![GeneratedFile {
            path,
            content,
            generated_header: false,
        }])
    }

    fn generate_scaffold(&self, _api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
        let module_name = zig_module_name(&config.name);
        let ffi_lib_name = config.ffi_lib_name();
        let ffi_crate_path = config.ffi_crate_path();
        let ffi_crate_root = ffi_crate_path.strip_prefix("../../").unwrap_or(&ffi_crate_path);
        let ffi_include_default = format!("../../{ffi_crate_root}/include");

        // Render build.zig from template with module and library names.
        let build_zig_content = crate::backends::zig::template_env::render(
            "build_zig.jinja",
            minijinja::context! {
                module_name => &module_name,
                ffi_lib_name => &ffi_lib_name,
                ffi_include_default => &ffi_include_default,
            },
        );

        let dir = resolve_output_dir(None, &config.name, "packages/zig");
        let dir_path = PathBuf::from(&dir);
        let build_zig_path = dir_path.join("build.zig");

        // Read FFI header from the workspace crate and emit it to packages/zig/include/.
        let header_name = config.ffi_header_name();
        let ffi_crate_header_path = PathBuf::from(ffi_crate_root)
            .join("include")
            .join(format!("{}.h", header_name));
        let ffi_header_content = std::fs::read_to_string(&ffi_crate_header_path).map_err(|e| {
            anyhow::anyhow!(
                "Failed to read FFI header from {}: {}",
                ffi_crate_header_path.display(),
                e
            )
        })?;

        let include_dir = dir_path.join("include");
        let include_header_path = include_dir.join(format!("{}.h", header_name));

        let mut files = vec![GeneratedFile {
            path: build_zig_path,
            content: build_zig_content,
            generated_header: true,
        }];

        // Emit the FFI header to the include/ directory (marked with generated header comment).
        files.push(GeneratedFile {
            path: include_header_path,
            content: format!(
                "// Generated by alef. Do not edit by hand.\n//\n// This header is copied from the FFI crate and must be present at link time.\n// For published distributions, include/ is bundled in the release tarball.\n\n{}",
                ffi_header_content
            ),
            generated_header: false, // Don't add duplicate header since we prepend manually
        });

        Ok(files)
    }

    fn generate_service_api(
        &self,
        api: &ApiSurface,
        config: &ResolvedCrateConfig,
    ) -> anyhow::Result<Vec<GeneratedFile>> {
        service_api::generate(api, config)
    }

    fn build_config(&self) -> Option<BuildConfig> {
        Some(BuildConfig {
            tool: "zig",
            crate_suffix: "",
            build_dep: BuildDependency::Ffi,
            post_build: vec![],
        })
    }
}
