const std = @import("std");
const llvm = @import("llvm_api.zig");
const c = llvm.c;

/// One `#jni_main #jni_class("...")` declaration's Java-source emission.
/// Populated by lowering and surfaced to the sx Android bundler in
/// `library/modules/platform/bundle.sx` via `BuildConfig.jni_main_*`,
/// which writes a `.java` file under `<stage>/java/<pkg>/<Cls>.java`,
/// compiles via `javac`, dexes via `d8`, and bundles the resulting
/// `classes.dex` into the APK.
pub const JniMainEmission = struct {
    /// foreign_path of the source decl (e.g. "co/swipelab/sxmain/SxApp").
    /// Splits into package + class name for `<stage>/java/<pkg>/<Class>.java`.
    foreign_path: []const u8,
    /// Pre-rendered Java source bytes (from `jni_java_emit.emitJavaSource`).
    java_source: []const u8,
};

pub const TargetConfig = struct {
    /// Target triple (e.g. "aarch64-apple-darwin"). Null = host default.
    triple: ?[*:0]const u8 = null,
    /// CPU name (e.g. "generic", "apple-m1"). Null = "generic".
    cpu: ?[*:0]const u8 = null,
    /// CPU features string (e.g. "+avx2"). Null = "".
    features: ?[*:0]const u8 = null,
    /// Optimization level.
    opt_level: OptLevel = .default,
    /// Library search paths (-L flags).
    lib_paths: []const []const u8 = &.{},
    /// Framework search paths (-F flags). Apple-only.
    framework_paths: []const []const u8 = &.{},
    /// Output path override.
    output_path: ?[]const u8 = null,
    /// Linker command (null = "cc" on Unix, "link.exe" on Windows).
    linker: ?[]const u8 = null,
    /// Sysroot for cross-compilation (passed as --sysroot to linker).
    sysroot: ?[]const u8 = null,
    /// Extra flags passed through to the linker (e.g. Emscripten -s flags).
    extra_link_flags: []const []const u8 = &.{},
    /// Custom WASM shell template path (overrides the built-in template).
    wasm_shell_path: ?[]const u8 = null,
    /// Path to a `.app` bundle directory to produce (iOS/macOS). When set, the
    /// linker output is moved into the bundle alongside a generated Info.plist
    /// and ad-hoc signed for simulator runs.
    bundle_path: ?[]const u8 = null,
    /// CFBundleIdentifier for the bundle (e.g. "co.swipelab.sxhello").
    /// Required when `bundle_path` is set. On Android, doubles as the
    /// AndroidManifest package="..." attribute.
    bundle_id: ?[]const u8 = null,
    /// Path to a `.apk` file to produce (Android). When set, the linked
    /// `.so` is wrapped into a debug-signed APK ready for `adb install`.
    apk_path: ?[]const u8 = null,
    /// Custom AndroidManifest.xml path. When null, a minimal NativeActivity
    /// manifest is generated from `bundle_id`.
    manifest_path: ?[]const u8 = null,
    /// Debug keystore for APK signing. Defaults to `~/.android/debug.keystore`.
    keystore_path: ?[]const u8 = null,
    /// Codesigning identity (e.g. `"Apple Development: Alex (TEAMID)"` or a
    /// SHA-1 fingerprint from `security find-identity -p codesigning`).
    /// When null, ad-hoc signs with `-` (sufficient for simulator, not device).
    codesign_identity: ?[]const u8 = null,
    /// Path to a `.mobileprovision` to embed as `embedded.mobileprovision`.
    /// Required for real-device builds.
    provisioning_profile: ?[]const u8 = null,
    /// Path to an entitlements plist. When null and `provisioning_profile`
    /// is set, the entitlements are auto-extracted from the profile.
    entitlements_path: ?[]const u8 = null,
    /// True when emitting an ahead-of-time binary (`sx build`), false for
    /// in-process JIT (`sx run`). Used by emit_llvm to gate code that only
    /// makes sense for a standalone executable — e.g. the macOS bundle
    /// `chdir` shouldn't run in JIT mode because it would mutate the host
    /// sx process's CWD.
    is_aot: bool = false,
    /// Keep the DWARF-bearing object file after linking (`--emit-obj`) so a
    /// debugger can step the binary: macOS resolves via the debug map → the
    /// `.o`; Linux carries DWARF in the binary directly. Implies `-O0` unless
    /// `--opt` is given explicitly (DWARF is only emitted at opt none/less).
    /// The object is kept at `.sx-tmp/main.o` (its link-time path, so the
    /// debug map resolves when lldb is run from the project root).
    emit_obj: bool = false,

    pub const OptLevel = enum {
        none,
        less,
        default,
        aggressive,

        pub fn toLLVM(self: OptLevel) c.LLVMCodeGenOptLevel {
            return switch (self) {
                .none => c.LLVMCodeGenLevelNone,
                .less => c.LLVMCodeGenLevelLess,
                .default => c.LLVMCodeGenLevelDefault,
                .aggressive => c.LLVMCodeGenLevelAggressive,
            };
        }
    };

    /// Check if target triple indicates aarch64/arm64 (runtime check, not comptime).
    pub fn isAarch64(self: TargetConfig) bool {
        return self.tripleHasPrefix("aarch64", "arm64");
    }

    /// Check if target triple indicates x86_64/x86-64.
    pub fn isX86_64(self: TargetConfig) bool {
        return self.tripleHasPrefix("x86_64", "x86-64");
    }

    /// Check if target triple indicates Windows (contains "windows" or "win32").
    pub fn isWindows(self: TargetConfig) bool {
        return self.tripleContains("windows") or self.tripleContains("win32");
    }

    /// Check if target triple indicates WebAssembly (wasm32 or wasm64).
    pub fn isWasm(self: TargetConfig) bool {
        return self.tripleHasPrefix("wasm32", "wasm64");
    }

    /// Check if target triple indicates wasm32 specifically (4-byte pointers, i32 size_t).
    pub fn isWasm32(self: TargetConfig) bool {
        return self.tripleHasPrefix("wasm32", "wasm32");
    }

    /// Check if target triple indicates wasm64 specifically (8-byte pointers, i64 size_t).
    pub fn isWasm64(self: TargetConfig) bool {
        return self.tripleHasPrefix("wasm64", "wasm64");
    }

    /// Check if target triple indicates macOS/Darwin (does not match iOS).
    pub fn isMacOS(self: TargetConfig) bool {
        if (self.isIOS()) return false;
        return self.tripleContains("darwin") or self.tripleContains("macos");
    }

    /// Check if target triple indicates iOS (device or simulator).
    pub fn isIOS(self: TargetConfig) bool {
        return self.tripleContains("-apple-ios");
    }

    /// Check if target triple indicates the iOS Simulator.
    pub fn isIOSSimulator(self: TargetConfig) bool {
        return self.isIOS() and self.tripleContains("simulator");
    }

    /// Check if target triple indicates a real iOS device (not Simulator).
    pub fn isIOSDevice(self: TargetConfig) bool {
        return self.isIOS() and !self.tripleContains("simulator");
    }

    /// Check if target triple indicates Linux (NOT Android — Android uses
    /// "linux-android" too but isAndroid() must take precedence).
    pub fn isLinux(self: TargetConfig) bool {
        if (self.isAndroid()) return false;
        return self.tripleContains("linux");
    }

    /// Check if target triple indicates Android (e.g. aarch64-linux-android21).
    pub fn isAndroid(self: TargetConfig) bool {
        return self.tripleContains("android");
    }

    /// Check if target triple indicates Emscripten (contains "emscripten").
    pub fn isEmscripten(self: TargetConfig) bool {
        return self.tripleContains("emscripten");
    }

    fn tripleHasPrefix(self: TargetConfig, prefix1: []const u8, prefix2: []const u8) bool {
        if (self.triple) |t| {
            const span = std.mem.span(t);
            return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2);
        }
        const dt = c.LLVMGetDefaultTargetTriple();
        defer c.LLVMDisposeMessage(dt);
        const span = std.mem.span(dt);
        return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2);
    }

    fn tripleContains(self: TargetConfig, needle: []const u8) bool {
        if (self.triple) |t| {
            return std.mem.indexOf(u8, std.mem.span(t), needle) != null;
        }
        const dt = c.LLVMGetDefaultTargetTriple();
        defer c.LLVMDisposeMessage(dt);
        return std.mem.indexOf(u8, std.mem.span(dt), needle) != null;
    }

    pub fn getCpu(self: TargetConfig) [*:0]const u8 {
        return self.cpu orelse "generic";
    }

    pub fn getFeatures(self: TargetConfig) [*:0]const u8 {
        return self.features orelse "";
    }

    pub fn getLinker(self: TargetConfig) []const u8 {
        return self.linker orelse "cc";
    }
};

/// Execute a precompiled object file in-process using LLVM's ORC JIT.
/// Takes ownership of obj_buf. Returns the exit code from main().
pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
    // Create LLJIT with default builder (no custom TM needed — .o is precompiled)
    var jit: c.LLVMOrcLLJITRef = null;
    var err = c.LLVMOrcCreateLLJIT(&jit, null);
    if (err != null) {
        const msg = c.LLVMGetErrorMessage(err);
        defer c.LLVMDisposeErrorMessage(msg);
        std.debug.print("JIT error: {s}\n", .{std.mem.span(msg)});
        return error.CompileError;
    }
    defer _ = c.LLVMOrcDisposeLLJIT(jit);

    // Add process symbols so JIT can find libc (printf, etc.)
    const jd = c.LLVMOrcLLJITGetMainJITDylib(jit);
    const prefix = c.LLVMOrcLLJITGetGlobalPrefix(jit);
    var gen: c.LLVMOrcDefinitionGeneratorRef = null;
    err = c.LLVMOrcCreateDynamicLibrarySearchGeneratorForProcess(&gen, prefix, null, null);
    if (err != null) {
        const msg = c.LLVMGetErrorMessage(err);
        defer c.LLVMDisposeErrorMessage(msg);
        std.debug.print("JIT symbol gen error: {s}\n", .{std.mem.span(msg)});
        return error.CompileError;
    }
    c.LLVMOrcJITDylibAddGenerator(jd, gen);

    // Add precompiled object file (transfers ownership of obj_buf)
    err = c.LLVMOrcLLJITAddObjectFile(jit, jd, obj_buf);
    if (err != null) {
        const msg = c.LLVMGetErrorMessage(err);
        defer c.LLVMDisposeErrorMessage(msg);
        std.debug.print("JIT add object error: {s}\n", .{std.mem.span(msg)});
        return error.CompileError;
    }

    // Look up the "main" function
    var main_addr: c.LLVMOrcExecutorAddress = 0;
    err = c.LLVMOrcLLJITLookup(jit, &main_addr, "main");
    if (err != null) {
        const msg = c.LLVMGetErrorMessage(err);
        defer c.LLVMDisposeErrorMessage(msg);
        std.debug.print("JIT lookup error: {s}\n", .{std.mem.span(msg)});
        return error.CompileError;
    }

    // Cast to function pointer and call. The exit code is main's integer
    // return truncated to u8 — matching the OS truncation an AOT binary's
    // exit status already gets, so JIT and AOT agree (e.g. 1105 -> 81,
    // -1 -> 255, 256 -> 0). Bit-cast i32 -> u32 first so negatives wrap
    // as two's-complement low byte rather than being clamped.
    const main_fn: *const fn () callconv(.c) i32 = @ptrFromInt(main_addr);
    const result = main_fn();
    return @truncate(@as(u32, @bitCast(result)));
}

// Android APK bundling (createApk, compileJniMainSources,
// buildAndroidManifest, buildJniMainManifest, ensureDebugKeystore,
// libNameFromSoBasename + helpers) has moved to
// `library/modules/platform/bundle.sx`. `src/main.zig` invokes it
// post-link via the BuildOptions callback registered from sx code.
// `--apk <path>` on the CLI is a transitional alias that feeds
// `bundle_path` so the auto-fallback to `platform.bundle.bundle_main`
// fires; programs that opt in via `set_post_link_callback` reach the
// sx bundler directly.


/// Discover the Android NDK root. Honors $ANDROID_NDK_HOME / $ANDROID_NDK_ROOT,
/// otherwise picks the highest-versioned NDK under $HOME/Library/Android/sdk/ndk
/// (the SDK Manager default install location on macOS). Caller owns the slice.
pub fn discoverAndroidNdk(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
    if (std.c.getenv("ANDROID_NDK_HOME")) |env| {
        return try allocator.dupe(u8, std.mem.span(env));
    }
    if (std.c.getenv("ANDROID_NDK_ROOT")) |env| {
        return try allocator.dupe(u8, std.mem.span(env));
    }
    const home_env = std.c.getenv("HOME") orelse {
        std.debug.print("error: cannot locate Android NDK \u{2014} set $ANDROID_NDK_HOME\n", .{});
        return error.NdkNotFound;
    };
    const home = std.mem.span(home_env);
    const ndk_root = try std.fmt.allocPrint(allocator, "{s}/Library/Android/sdk/ndk", .{home});
    var dir = std.Io.Dir.openDir(.cwd(), io, ndk_root, .{ .iterate = true }) catch {
        std.debug.print("error: no NDK at {s} \u{2014} install via Android Studio or set $ANDROID_NDK_HOME\n", .{ndk_root});
        return error.NdkNotFound;
    };
    defer dir.close(io);
    var best: ?[]const u8 = null;
    var it = dir.iterate();
    while (it.next(io) catch null) |entry| {
        if (entry.kind != .directory) continue;
        if (best == null or std.mem.order(u8, entry.name, best.?) == .gt) {
            best = try allocator.dupe(u8, entry.name);
        }
    }
    const version = best orelse {
        std.debug.print("error: no NDK versions under {s}\n", .{ndk_root});
        return error.NdkNotFound;
    };
    return try std.fmt.allocPrint(allocator, "{s}/{s}", .{ ndk_root, version });
}

/// Run `xcrun --sdk <sdk_name> --show-sdk-path` and return the trimmed path.
/// Caller owns the returned slice.
pub fn discoverAppleSdk(allocator: std.mem.Allocator, io: std.Io, sdk_name: []const u8) ![]const u8 {
    const r = std.process.run(allocator, io, .{
        .argv = &.{ "xcrun", "--sdk", sdk_name, "--show-sdk-path" },
    }) catch |e| {
        std.debug.print("error: failed to run xcrun: {} \u{2014} install Xcode Command Line Tools (xcode-select --install)\n", .{e});
        return error.SdkNotFound;
    };
    defer allocator.free(r.stderr);
    errdefer allocator.free(r.stdout);
    if (r.term != .exited or r.term.exited != 0) {
        std.debug.print("error: xcrun --sdk {s} --show-sdk-path failed\n", .{sdk_name});
        allocator.free(r.stdout);
        return error.SdkNotFound;
    }
    const trimmed = std.mem.trimEnd(u8, r.stdout, " \t\r\n");
    const out = try allocator.dupe(u8, trimmed);
    allocator.free(r.stdout);
    return out;
}

pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, frameworks: []const []const u8, target_config: TargetConfig, has_jni_main: bool) !void {
    var argv = std.ArrayList([]const u8).empty;

    if (target_config.isIOS()) {
        // iOS: clang driver with -isysroot pointing at the iOS SDK.
        // -l libraries are generally wrong for iOS (Apple ships system code
        // as frameworks); user-declared #library still pass through.
        const linker = target_config.linker orelse "clang";
        try argv.append(allocator, linker);
        if (target_config.triple) |t| {
            try argv.append(allocator, "-target");
            try argv.append(allocator, std.mem.span(t));
        }
        const sdk_path = if (target_config.sysroot) |sr|
            try allocator.dupe(u8, sr)
        else blk: {
            const sdk_name: []const u8 = if (target_config.isIOSSimulator()) "iphonesimulator" else "iphoneos";
            break :blk try discoverAppleSdk(allocator, io, sdk_name);
        };
        try argv.append(allocator, "-isysroot");
        try argv.append(allocator, sdk_path);
        const min_flag: []const u8 = if (target_config.isIOSSimulator()) "-mios-simulator-version-min=14.0" else "-mios-version-min=14.0";
        try argv.append(allocator, min_flag);
        // Embedded framework load path: bundle/Frameworks at runtime.
        try argv.append(allocator, "-Wl,-rpath,@executable_path/Frameworks");
        try argv.append(allocator, output_obj);
        try argv.append(allocator, "-o");
        try argv.append(allocator, output_bin);
        for (extra_objects) |eo| try argv.append(allocator, eo);
        for (target_config.lib_paths) |lp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
        }
        for (target_config.framework_paths) |fp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-F{s}", .{fp}));
        }
        for (libraries) |lib| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
        }
        for (frameworks) |fw| {
            try argv.append(allocator, "-framework");
            try argv.append(allocator, fw);
        }
        for (target_config.extra_link_flags) |flag| {
            var it = std.mem.tokenizeScalar(u8, flag, ' ');
            while (it.next()) |part| try argv.append(allocator, part);
        }
    } else if (target_config.isAndroid()) {
        // Android: NDK clang. Produces a shared library (.so).
        //
        // Two entry shapes:
        //
        //   - **#jni_main path (`has_jni_main = true`)** — the Java side
        //     drives lifecycle (the bundled classes.dex declares an
        //     Activity that overrides `onCreate` etc.). The .so just
        //     provides JNI implementations bound at load time via the
        //     `JNI_OnLoad` synthesized in slice R.3. No native_app_glue
        //     is needed: there's no `ANativeActivity_onCreate` to host,
        //     no `android_main` event loop to run.
        //
        //   - **Legacy NativeActivity path (`has_jni_main = false`)** —
        //     native_app_glue.c is compiled and linked alongside the sx
        //     code; the glue owns `ANativeActivity_onCreate` and forwards
        //     into the user's `android_main` on a worker thread. The
        //     `-u ANativeActivity_onCreate` keeps the glue's symbol from
        //     being stripped (nothing in our .o references it).
        //
        // The `libraries` parameter (collected from `#library` directives)
        // and `frameworks` parameter (Apple-only by definition) are
        // intentionally ignored here. On Android, users opt into specific
        // libs via `opts.add_link_flag("-l<name>")` in their build.sx —
        // the platform-specific link surface should be expressed in build
        // options rather than auto-inherited from every imported module
        // (most of which assume Apple targets).
        const ndk_root = if (target_config.sysroot) |sr|
            try allocator.dupe(u8, sr)
        else
            try discoverAndroidNdk(allocator, io);
        const host_tag: []const u8 = if (@import("builtin").os.tag == .macos) "darwin-x86_64" else "linux-x86_64";
        const clang = try std.fmt.allocPrint(allocator, "{s}/toolchains/llvm/prebuilt/{s}/bin/clang", .{ ndk_root, host_tag });

        const glue_obj_opt: ?[]const u8 = if (has_jni_main) null else blk: {
            const glue_src = try std.fmt.allocPrint(allocator, "{s}/sources/android/native_app_glue/android_native_app_glue.c", .{ndk_root});
            const glue_obj = try std.fmt.allocPrint(allocator, "{s}.glue.o", .{output_obj});
            var glue_argv = std.ArrayList([]const u8).empty;
            try glue_argv.appendSlice(allocator, &.{ clang, "-c", "-fPIC" });
            if (target_config.triple) |t| {
                try glue_argv.append(allocator, "-target");
                try glue_argv.append(allocator, std.mem.span(t));
            }
            try glue_argv.appendSlice(allocator, &.{ glue_src, "-o", glue_obj });
            const glue_slice = try glue_argv.toOwnedSlice(allocator);
            var glue_child = std.process.spawn(io, .{ .argv = glue_slice }) catch return error.LinkError;
            const glue_term = glue_child.wait(io) catch return error.LinkError;
            if (glue_term != .exited or glue_term.exited != 0) return error.LinkError;
            break :blk glue_obj;
        };

        try argv.append(allocator, clang);
        if (target_config.triple) |t| {
            try argv.append(allocator, "-target");
            try argv.append(allocator, std.mem.span(t));
        }
        try argv.append(allocator, "-shared");
        try argv.append(allocator, "-fPIC");
        if (!has_jni_main) {
            try argv.appendSlice(allocator, &.{ "-u", "ANativeActivity_onCreate" });
        }
        try argv.append(allocator, output_obj);
        if (glue_obj_opt) |go| try argv.append(allocator, go);
        for (extra_objects) |eo| try argv.append(allocator, eo);
        try argv.append(allocator, "-o");
        try argv.append(allocator, output_bin);
        for (target_config.lib_paths) |lp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
        }
        // Default libs available on every Android runtime; linker drops
        // unreferenced ones automatically. `#library` directives are
        // intentionally NOT auto-emitted here (most assume Apple targets);
        // users opt in per-target via `opts.add_link_flag("-l...")` in
        // their build.sx.
        try argv.appendSlice(allocator, &.{ "-llog", "-landroid", "-lEGL", "-lGLESv3", "-lm", "-ldl" });
        for (target_config.extra_link_flags) |flag| {
            var it = std.mem.tokenizeScalar(u8, flag, ' ');
            while (it.next()) |part| try argv.append(allocator, part);
        }
    } else if (target_config.isEmscripten()) {
        // Emscripten: use emcc as the linker/driver
        const linker = target_config.linker orelse "emcc";
        try argv.appendSlice(allocator, &.{ linker, output_obj, "-o", output_bin });
        for (extra_objects) |eo| try argv.append(allocator, eo);

        if (target_config.sysroot) |sr| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr}));
        }

        for (target_config.lib_paths) |lp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
        }
        // Skip -l flags for Emscripten: libraries like SDL3 are provided via
        // -sUSE_SDL=3, not -lSDL3. User provides everything via --lflags.

        // wasm64: automatically add -sMEMORY64 for the linker
        if (target_config.isWasm64()) {
            try argv.append(allocator, "-sMEMORY64");
        }

        // HTML shell template: use custom path if set, otherwise write built-in template to temp file
        if (std.mem.endsWith(u8, output_bin, ".html")) {
            if (target_config.wasm_shell_path) |custom_shell| {
                try argv.appendSlice(allocator, &.{ "--shell-file", custom_shell });
            } else {
                const shell_html = @embedFile("wasm_shell.html");
                const shell_path = try std.fmt.allocPrint(allocator, "{s}.shell.html", .{output_obj});
                std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = shell_path, .data = shell_html }) catch {};
                try argv.appendSlice(allocator, &.{ "--shell-file", shell_path });
            }
        }

        // Extra linker flags (e.g. -sUSE_SDL=3, -sUSE_WEBGL2=1, --preload-file assets)
        // Split space-separated flags into individual argv entries.
        for (target_config.extra_link_flags) |flag| {
            var it = std.mem.tokenizeScalar(u8, flag, ' ');
            while (it.next()) |part| {
                try argv.append(allocator, part);
            }
        }
    } else if (target_config.isWindows()) {
        // Windows: MSVC-style linker flags
        const linker = target_config.linker orelse "link.exe";
        try argv.appendSlice(allocator, &.{ linker, output_obj });
        for (extra_objects) |eo| try argv.append(allocator, eo);
        try argv.append(allocator, try std.fmt.allocPrint(allocator, "/OUT:{s}", .{output_bin}));

        for (target_config.lib_paths) |lp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "/LIBPATH:{s}", .{lp}));
        }
        for (libraries) |lib| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "{s}.lib", .{lib}));
        }
    } else {
        // Unix: cc-style linker flags
        try argv.appendSlice(allocator, &.{ target_config.getLinker(), output_obj, "-o", output_bin });
        for (extra_objects) |eo| try argv.append(allocator, eo);

        if (target_config.sysroot) |sr| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr}));
        }

        // User-supplied library paths first
        for (target_config.lib_paths) |lp| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
        }

        // Auto-detect host OS library paths when linking foreign libraries
        if (libraries.len > 0 and target_config.triple == null) {
            for (host_lib_paths) |path| {
                try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{path}));
            }
        }

        for (libraries) |lib| {
            try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
        }

        // Frameworks: only meaningful on Apple targets; silently ignored elsewhere.
        if (target_config.isMacOS()) {
            for (frameworks) |fw| {
                try argv.append(allocator, "-framework");
                try argv.append(allocator, fw);
            }
        }

        // Extra linker flags — split space-separated flags into individual argv entries.
        for (target_config.extra_link_flags) |flag| {
            var it = std.mem.tokenizeScalar(u8, flag, ' ');
            while (it.next()) |part| {
                try argv.append(allocator, part);
            }
        }
    }

    const argv_slice = try argv.toOwnedSlice(allocator);
    if (std.c.getenv("SX_DEBUG_LINK") != null) {
        std.debug.print("[sx] link argv:", .{});
        for (argv_slice) |a| std.debug.print(" {s}", .{a});
        std.debug.print("\n", .{});
    }
    var child = std.process.spawn(io, .{
        .argv = argv_slice,
    }) catch return error.LinkError;
    const result = child.wait(io) catch return error.LinkError;
    if (result != .exited) return error.LinkError;
    if (result.exited != 0) return error.LinkError;
}

// Apple .app bundling (createBundle, embedFramework, extractEntitlements,
// buildInfoPlist, codesign) has moved to
// `library/modules/platform/bundle.sx`. `src/main.zig` invokes it
// post-link via the BuildOptions callback registered from sx code.

/// After emcc produces HTML output, inject cache-busting hashes into the
/// generated <script> tag and add Module.locateFile for .wasm/.data files.
pub fn postProcessWasmHtml(allocator: std.mem.Allocator, io: std.Io, html_path: []const u8) void {
    const base = if (std.mem.endsWith(u8, html_path, ".html"))
        html_path[0 .. html_path.len - 5]
    else
        return;

    // Hash build output contents (.js + .wasm + optional .data)
    var hash: u64 = 0;
    const exts = [_][]const u8{ ".js", ".wasm", ".data" };
    for (exts) |ext| {
        const path = std.fmt.allocPrint(allocator, "{s}{s}", .{ base, ext }) catch continue;
        if (std.Io.Dir.readFileAlloc(.cwd(), io, path, allocator, .limited(64 * 1024 * 1024))) |data| {
            hash = std.hash.Wyhash.hash(hash, data);
        } else |_| {}
    }

    const hash_hex = std.fmt.allocPrint(allocator, "{x:0>8}", .{@as(u32, @truncate(hash))}) catch return;

    // Read the final HTML produced by emcc
    const html = std.Io.Dir.readFileAlloc(.cwd(), io, html_path, allocator, .limited(10 * 1024 * 1024)) catch return;

    var out = std.ArrayList(u8).empty;
    var pos: usize = 0;
    var injected_locateFile = false;

    // Find emcc's generated script tag: <script ...src="*.js"></script>
    // Inject ?v=HASH into the src and prepend a Module.locateFile script.
    while (std.mem.indexOfPos(u8, html, pos, "src=\"")) |src_start| {
        const val_start = src_start + 5; // past src="
        const val_end = std.mem.indexOfPos(u8, html, val_start, "\"") orelse break;
        const src_val = html[val_start..val_end];

        if (!std.mem.endsWith(u8, src_val, ".js")) {
            // Not a .js src — skip past this attribute and keep searching
            pos = val_end + 1;
            continue;
        }

        // Find the opening < of this tag to inject locateFile before it
        const tag_start = if (std.mem.lastIndexOf(u8, html[pos..src_start], "<")) |off| pos + off else src_start;

        // Copy everything up to the tag start
        out.appendSlice(allocator, html[pos..tag_start]) catch return;

        // Inject Module.locateFile once, before the first .js script tag
        if (!injected_locateFile) {
            out.appendSlice(allocator, "<script>Module.locateFile=function(p){return p+'?v=") catch return;
            out.appendSlice(allocator, hash_hex) catch return;
            out.appendSlice(allocator, "'}</script>\n") catch return;
            injected_locateFile = true;
        }

        // Copy tag up to the closing quote of src, inserting ?v=HASH
        out.appendSlice(allocator, html[tag_start..val_end]) catch return;
        out.appendSlice(allocator, "?v=") catch return;
        out.appendSlice(allocator, hash_hex) catch return;

        pos = val_end;
    }
    // Copy remaining HTML
    out.appendSlice(allocator, html[pos..]) catch return;

    const final = out.toOwnedSlice(allocator) catch return;
    std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = html_path, .data = final }) catch {};
}

/// Common library paths for the host OS, computed at comptime.
pub const host_lib_paths = blk: {
    const builtin = @import("builtin");
    var paths: []const []const u8 = &.{};
    if (builtin.os.tag == .macos) {
        if (builtin.cpu.arch == .aarch64) {
            // Apple Silicon Homebrew
            paths = &.{ "/opt/homebrew/lib", "/usr/local/lib" };
        } else {
            // Intel Mac Homebrew
            paths = &.{"/usr/local/lib"};
        }
    } else if (builtin.os.tag == .linux) {
        paths = &.{ "/usr/local/lib", "/usr/lib" };
    }
    break :blk paths;
};
