// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const builtin = @import("builtin");

const lightpanda_version = std.SemanticVersion.parse(@import("build.zig.zon").version) catch unreachable;
const min_zig_version = std.SemanticVersion.parse(@import("build.zig.zon").minimum_zig_version) catch unreachable;

const Build = blk: {
    if (builtin.zig_version.order(min_zig_version) == .lt) {
        const message = std.fmt.comptimePrint(
            \\Zig version is too old:
            \\  current Zig version: {f}
            \\  minimum Zig version: {f}
        , .{ builtin.zig_version, min_zig_version });
        @compileError(message);
    } else {
        break :blk std.Build;
    }
};

pub fn build(b: *Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
    const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
    const wpt_extensions = b.option(bool, "wpt_extensions", "Extend WebAPI with WPT driver behavior") orelse false;

    const version = resolveVersion(b);
    var stdout = std.fs.File.stdout().writer(&.{});
    try stdout.interface.print("Lightpanda {f}\n", .{version});

    const version_string = b.fmt("{f}", .{version});
    const version_encoded = std.mem.replaceOwned(u8, b.allocator, version_string, "+", "%2B") catch @panic("OOM");

    var opts = b.addOptions();
    opts.addOption([]const u8, "version", version_string);
    opts.addOption([]const u8, "version_encoded", version_encoded);
    opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
    opts.addOption(bool, "wpt_extensions", wpt_extensions);

    const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
    const enable_asan = b.option(bool, "asan", "Enable Address Sanitizer") orelse false;
    const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");

    const lightpanda_module = blk: {
        const mod = b.addModule("lightpanda", .{
            .root_source_file = b.path("src/lightpanda.zig"),
            .target = target,
            .optimize = optimize,
            .link_libc = true,
            .link_libcpp = true,
            .sanitize_c = enable_csan,
            .sanitize_thread = enable_tsan,
        });
        mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
        mod.addImport("build_config", opts.createModule());

        // Format check
        const fmt_step = b.step("fmt", "Check code formatting");
        const fmt = b.addFmt(.{
            .paths = &.{ "src", "build.zig", "build.zig.zon" },
            .check = true,
        });
        fmt_step.dependOn(&fmt.step);

        // Set default behavior
        b.default_step.dependOn(fmt_step);

        try linkV8(b, mod, enable_asan, enable_tsan, prebuilt_v8_path);
        try linkCurl(b, mod, enable_tsan);
        try linkHtml5Ever(b, mod);
        linkZenai(b, mod);
        linkLinenoise(b, mod);

        break :blk mod;
    };

    linkSqlite(b, lightpanda_module, enable_csan, enable_tsan);

    // Check compilation
    const check = b.step("check", "Check if lightpanda compiles");

    const check_lib = b.addLibrary(.{
        .name = "lightpanda_check",
        .root_module = lightpanda_module,
    });
    check.dependOn(&check_lib.step);

    // Extras (snapshot_creator, legacy_test) are off the default install to
    // avoid paying for three exe compiles on every edit. Build explicitly
    // with `zig build extras`.
    const extras_step = b.step("extras", "Build snapshot_creator and legacy_test");

    {
        // browser
        const exe = b.addExecutable(.{
            .name = "lightpanda",
            .use_llvm = true,
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/main.zig"),
                .target = target,
                .optimize = optimize,
                .sanitize_c = enable_csan,
                .sanitize_thread = enable_tsan,
                .imports = &.{
                    .{ .name = "lightpanda", .module = lightpanda_module },
                },
            }),
        });
        b.installArtifact(exe);

        const exe_check = b.addLibrary(.{
            .name = "lightpanda_exe_check",
            .root_module = exe.root_module,
        });
        check.dependOn(&exe_check.step);

        const run_cmd = b.addRunArtifact(exe);
        if (b.args) |args| {
            run_cmd.addArgs(args);
        }
        const run_step = b.step("run", "Run the app");
        run_step.dependOn(&run_cmd.step);

        const version_info_step = b.step("version", "Print the resolved version information");
        const version_info_run = b.addRunArtifact(exe);
        version_info_run.addArg("version");
        version_info_step.dependOn(&version_info_run.step);
    }

    {
        // snapshot creator
        const exe = b.addExecutable(.{
            .name = "lightpanda-snapshot-creator",
            .use_llvm = true,
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/main_snapshot_creator.zig"),
                .target = target,
                .optimize = optimize,
                .imports = &.{
                    .{ .name = "lightpanda", .module = lightpanda_module },
                },
            }),
        });
        extras_step.dependOn(&b.addInstallArtifact(exe, .{}).step);

        const exe_check = b.addLibrary(.{
            .name = "snapshot_creator_check",
            .root_module = exe.root_module,
        });
        check.dependOn(&exe_check.step);

        const run_cmd = b.addRunArtifact(exe);
        if (b.args) |args| {
            run_cmd.addArgs(args);
        }
        const run_step = b.step("snapshot_creator", "Generate a v8 snapshot");
        run_step.dependOn(&run_cmd.step);
    }

    {
        // test
        const tests = b.addTest(.{
            .root_module = lightpanda_module,
            .use_llvm = true,
            .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
        });
        const run_tests = b.addRunArtifact(tests);
        const test_step = b.step("test", "Run unit tests");
        test_step.dependOn(&run_tests.step);
    }

    {
        // browser
        const exe = b.addExecutable(.{
            .name = "legacy_test",
            .use_llvm = true,
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/main_legacy_test.zig"),
                .target = target,
                .optimize = optimize,
                .sanitize_c = enable_csan,
                .sanitize_thread = enable_tsan,
                .imports = &.{
                    .{ .name = "lightpanda", .module = lightpanda_module },
                },
            }),
        });
        extras_step.dependOn(&b.addInstallArtifact(exe, .{}).step);

        const exe_check = b.addLibrary(.{
            .name = "legacy_test_check",
            .root_module = exe.root_module,
        });
        check.dependOn(&exe_check.step);

        const run_cmd = b.addRunArtifact(exe);
        if (b.args) |args| {
            run_cmd.addArgs(args);
        }
        const run_step = b.step("legacy_test", "Run the app");
        run_step.dependOn(&run_cmd.step);
    }
}

fn linkV8(
    b: *Build,
    mod: *Build.Module,
    is_asan: bool,
    is_tsan: bool,
    prebuilt_v8_path: ?[]const u8,
) !void {
    const target = mod.resolved_target.?;

    const dep = b.dependency("v8", .{
        .target = target,
        .optimize = mod.optimize.?,
        .is_asan = is_asan,
        .is_tsan = is_tsan,
        .inspector_subtype = false,
        .v8_enable_sandbox = is_tsan,
        .cache_root = b.pathFromRoot(".lp-cache"),
        .prebuilt_v8_path = prebuilt_v8_path,
    });
    mod.addImport("v8", dep.module("v8"));
}

fn linkHtml5Ever(b: *Build, mod: *Build.Module) !void {
    const is_debug = if (mod.optimize.? == .Debug) true else false;

    const exec_cargo = b.addSystemCommand(&.{
        "cargo",           "build",
        "--profile",       if (is_debug) "dev" else "release",
        "--manifest-path", "src/html5ever/Cargo.toml",
    });

    // Track Rust sources so edits invalidate the cargo step's cache.
    // Without this, Zig keys the step on argv only and won't re-run cargo
    // when lib.rs/Cargo.toml change.
    for ([_][]const u8{
        "src/html5ever/Cargo.toml",
        "src/html5ever/Cargo.lock",
        "src/html5ever/lib.rs",
        "src/html5ever/sink.rs",
        "src/html5ever/types.rs",
    }) |path| {
        exec_cargo.addFileInput(b.path(path));
    }

    // TODO: We can prefer `--artifact-dir` once it become stable.
    const out_dir = exec_cargo.addPrefixedOutputDirectoryArg("--target-dir=", "html5ever");

    const html5ever_step = b.step("html5ever", "Install html5ever dependency (requires cargo)");
    html5ever_step.dependOn(&exec_cargo.step);

    const obj = out_dir.path(b, if (is_debug) "debug" else "release").path(b, "liblitefetch_html5ever.a");
    mod.addObjectFile(obj);
}

fn linkSqlite(b: *Build, mod: *Build.Module, enable_csan: ?std.zig.SanitizeC, is_tsan: bool) void {
    const dep = b.dependency("sqlite3", .{
        .target = mod.resolved_target.?,
        .optimize = mod.optimize.?,
    });

    const lib = dep.artifact("sqlite3");
    lib.root_module.sanitize_c = enable_csan;
    lib.root_module.sanitize_thread = is_tsan;

    const macros = [_]struct { []const u8, []const u8 }{
        .{ "SQLITE_DEFAULT_FILE_PERMISSIONS", "0600" },
        .{ "SQLITE_DEFAULT_MEMSTATUS", "0" },
        .{ "SQLITE_DEFAULT_WAL_SYNCHRONOUS", "1" },
        .{ "SQLITE_DQS", "0" },
        .{ "SQLITE_ENABLE_API_ARMOR", "1" },
        .{ "SQLITE_ENABLE_UNLOCK_NOTIFY", "1" },
        .{ "SQLITE_TEMP_STORE", "3" },
        .{ "SQLITE_THREADSAFE", "1" },
        .{ "SQLITE_UNTESTABLE", "1" },
        .{ "SQLITE_USE_ALLOCA", "1" },
        .{ "SQLITE_OMIT_AUTHORIZATION", "1" },
        .{ "SQLITE_OMIT_AUTOMATIC_INDEX", "1" },
        .{ "SQLITE_OMIT_AUTORESET", "1" },
        .{ "SQLITE_OMIT_AUTOVACUUM", "1" },
        .{ "SQLITE_OMIT_BETWEEN_OPTIMIZATION", "1" },
        .{ "SQLITE_OMIT_CASE_SENSITIVE_LIKE_PRAGMA", "1" },
        .{ "SQLITE_OMIT_COMPLETE", "1" },
        .{ "SQLITE_OMIT_DECLTYPE", "1" },
        .{ "SQLITE_OMIT_DEPRECATED", "1" },
        .{ "SQLITE_OMIT_DESERIALIZE", "1" },
        .{ "SQLITE_OMIT_GET_TABLE", "1" },
        .{ "SQLITE_OMIT_INCRBLOB", "1" },
        .{ "SQLITE_OMIT_JSON", "1" },
        .{ "SQLITE_OMIT_LIKE_OPTIMIZATION", "1" },
        .{ "SQLITE_OMIT_LOAD_EXTENSION", "1" },
        .{ "SQLITE_OMIT_PROGRESS_CALLBACK", "1" },
        .{ "SQLITE_OMIT_SHARED_CACHE", "1" },
        .{ "SQLITE_OMIT_TCL_VARIABLE", "1" },
        .{ "SQLITE_OMIT_TEMPDB", "1" },
        .{ "SQLITE_OMIT_TRACE", "1" },
        .{ "SQLITE_OMIT_UTF16", "1" },
        .{ "SQLITE_OMIT_XFER_OPT", "1" },
    };
    for (macros) |m| {
        lib.root_module.addCMacro(m[0], m[1]);
    }

    mod.linkLibrary(lib);
}

fn linkCurl(b: *Build, mod: *Build.Module, is_tsan: bool) !void {
    const target = mod.resolved_target.?;

    const curl = buildCurl(b, target, mod.optimize.?, is_tsan);
    mod.linkLibrary(curl);

    const zlib = buildZlib(b, target, mod.optimize.?, is_tsan);
    curl.root_module.linkLibrary(zlib);

    const brotli = buildBrotli(b, target, mod.optimize.?, is_tsan);
    for (brotli) |lib| curl.root_module.linkLibrary(lib);

    const nghttp2 = buildNghttp2(b, target, mod.optimize.?, is_tsan);
    curl.root_module.linkLibrary(nghttp2);

    const boringssl = buildBoringSsl(b, target, mod.optimize.?);
    for (boringssl) |lib| curl.root_module.linkLibrary(lib);

    const libidn2 = buildLibidn2(b, target, mod.optimize.?, is_tsan);
    curl.root_module.linkLibrary(libidn2);
    // Also expose libidn2 to the consuming module so src/sys/idna.zig's
    // @cImport of <idn2.h> resolves. Without this, lightpanda_module only
    // sees idn2.h transitively if a system libidn2 happens to be installed.
    mod.linkLibrary(libidn2);

    switch (target.result.os.tag) {
        .macos => {
            // needed for proxying on mac
            mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
            mod.linkFramework("CoreFoundation", .{});
            mod.linkFramework("SystemConfiguration", .{});
        },
        else => {},
    }
}

fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Step.Compile {
    const dep = b.dependency("zlib", .{});

    const mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_thread = is_tsan,
    });

    const lib = b.addLibrary(.{ .name = "z", .root_module = mod });
    lib.installHeadersDirectory(dep.path(""), "", .{});
    lib.addCSourceFiles(.{
        .root = dep.path(""),
        .flags = &.{
            "-DHAVE_SYS_TYPES_H",
            "-DHAVE_STDINT_H",
            "-DHAVE_STDDEF_H",
            "-DHAVE_UNISTD_H",
        },
        .files = &.{
            "adler32.c", "compress.c", "crc32.c",
            "deflate.c", "gzclose.c",  "gzlib.c",
            "gzread.c",  "gzwrite.c",  "infback.c",
            "inffast.c", "inflate.c",  "inftrees.c",
            "trees.c",   "uncompr.c",  "zutil.c",
        },
    });

    return lib;
}

fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) [3]*Build.Step.Compile {
    const dep = b.dependency("brotli", .{});

    const mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_thread = is_tsan,
    });
    mod.addIncludePath(dep.path("c/include"));

    const brotlicmn = b.addLibrary(.{ .name = "brotlicommon", .root_module = mod });
    const brotlidec = b.addLibrary(.{ .name = "brotlidec", .root_module = mod });
    const brotlienc = b.addLibrary(.{ .name = "brotlienc", .root_module = mod });

    brotlicmn.installHeadersDirectory(dep.path("c/include/brotli"), "brotli", .{});
    brotlicmn.addCSourceFiles(.{
        .root = dep.path("c/common"),
        .files = &.{
            "transform.c",  "shared_dictionary.c", "platform.c",
            "dictionary.c", "context.c",           "constants.c",
        },
    });
    brotlidec.addCSourceFiles(.{
        .root = dep.path("c/dec"),
        .files = &.{
            "bit_reader.c", "decode.c", "huffman.c",
            "prefix.c",     "state.c",  "static_init.c",
        },
    });
    brotlienc.addCSourceFiles(.{
        .root = dep.path("c/enc"),
        .files = &.{
            "backward_references.c",        "backward_references_hq.c", "bit_cost.c",
            "block_splitter.c",             "brotli_bit_stream.c",      "cluster.c",
            "command.c",                    "compound_dictionary.c",    "compress_fragment.c",
            "compress_fragment_two_pass.c", "dictionary_hash.c",        "encode.c",
            "encoder_dict.c",               "entropy_encode.c",         "fast_log.c",
            "histogram.c",                  "literal_cost.c",           "memory.c",
            "metablock.c",                  "static_dict.c",            "static_dict_lut.c",
            "static_init.c",                "utf8_util.c",
        },
    });

    return .{ brotlicmn, brotlidec, brotlienc };
}

fn buildBoringSsl(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) [2]*Build.Step.Compile {
    const dep = b.dependency("boringssl-zig", .{
        .target = target,
        .optimize = optimize,
        .force_pic = true,
    });

    const ssl = dep.artifact("ssl");
    ssl.bundle_ubsan_rt = false;

    const crypto = dep.artifact("crypto");
    crypto.bundle_ubsan_rt = false;

    return .{ ssl, crypto };
}

fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Step.Compile {
    const dep = b.dependency("nghttp2", .{});

    const mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_thread = is_tsan,
    });
    mod.addIncludePath(dep.path("lib/includes"));

    const config = b.addConfigHeader(.{
        .include_path = "nghttp2ver.h",
        .style = .{ .cmake = dep.path("lib/includes/nghttp2/nghttp2ver.h.in") },
    }, .{
        .PACKAGE_VERSION = "1.68.90",
        .PACKAGE_VERSION_NUM = 0x016890,
    });
    mod.addConfigHeader(config);

    const lib = b.addLibrary(.{ .name = "nghttp2", .root_module = mod });

    lib.installConfigHeader(config);
    lib.installHeadersDirectory(dep.path("lib/includes/nghttp2"), "nghttp2", .{});
    lib.addCSourceFiles(.{
        .root = dep.path("lib"),
        .flags = &.{
            "-DNGHTTP2_STATICLIB",
            "-DHAVE_TIME_H",
            "-DHAVE_ARPA_INET_H",
            "-DHAVE_NETINET_IN_H",
        },
        .files = &.{
            "sfparse.c",                 "nghttp2_alpn.c",   "nghttp2_buf.c",
            "nghttp2_callbacks.c",       "nghttp2_debug.c",  "nghttp2_extpri.c",
            "nghttp2_frame.c",           "nghttp2_hd.c",     "nghttp2_hd_huffman.c",
            "nghttp2_hd_huffman_data.c", "nghttp2_helper.c", "nghttp2_http.c",
            "nghttp2_map.c",             "nghttp2_mem.c",    "nghttp2_option.c",
            "nghttp2_outbound_item.c",   "nghttp2_pq.c",     "nghttp2_priority_spec.c",
            "nghttp2_queue.c",           "nghttp2_rcbuf.c",  "nghttp2_session.c",
            "nghttp2_stream.c",          "nghttp2_submit.c", "nghttp2_version.c",
            "nghttp2_ratelim.c",         "nghttp2_time.c",
        },
    });

    return lib;
}

fn buildLibidn2(
    b: *Build,
    target: Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
    is_tsan: bool,
) *Build.Step.Compile {
    const dep = b.dependency("libidn2", .{});

    const os = target.result.os.tag;
    const is_darwin = os.isDarwin();

    const mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_thread = is_tsan,
    });

    // libidn2's autoconf+gnulib stack expects a config.h with hundreds of
    // HAVE_*/_GL_ATTRIBUTE_* defines — including ~800 lines of attribute-
    // detection macros emitted from gnulib-common.m4 via AH_VERBATIM. We
    // vendor a single autoconf-generated config.h rather than try to
    // reproduce that machinery in the Zig build system.
    mod.addIncludePath(b.path("vendor/libidn2"));

    // Substitute the gnulib-style .in.h templates. All @VAR@ in them are
    // either DLL-visibility markers (empty for static POSIX) or
    // HAVE_UNISTRING_WOE32DLL_H (0).
    inline for (.{ "unitypes", "unistr", "uniconv", "unictype", "uninorm" }) |name| {
        mod.addConfigHeader(renderUnistringHeader(b, dep, name));
    }

    mod.addIncludePath(dep.path("lib"));
    mod.addIncludePath(dep.path("unistring"));
    // gl/ holds gnulib helpers — only malloca and version-etc headers are
    // referenced from the sources we compile; we don't need the full gl/ shim
    // layer (system header replacements).
    mod.addIncludePath(dep.path("gl"));

    const lib = b.addLibrary(.{ .name = "idn2", .root_module = mod });
    lib.installHeader(dep.path("lib/idn2.h"), "idn2.h");

    if (is_darwin) {
        // unistring's striconveh.c calls real iconv_*, which on macOS lives
        // in libiconv (separate from libSystem). On glibc Linux iconv is in
        // libc itself; on musl it would also need a separate -liconv.
        mod.linkSystemLibrary("iconv", .{});

        // libidn2's lib/lookup.c calls strchrnul() without including
        // <string.h>; the prototype is declared in vendor/libidn2/config.h
        // alongside the existing strverscmp shim. macOS libc lacked the
        // symbol entirely before 15.4 — provide it here so the link
        // succeeds. Mirrors how gl/strverscmp.c is wired up below.
        lib.addCSourceFile(.{
            .file = b.path("vendor/libidn2/darwin/strchrnul.c"),
            .flags = &.{},
        });
    }

    lib.addCSourceFiles(.{
        .root = dep.path("lib"),
        .flags = &.{ "-DHAVE_CONFIG_H", "-DIDN2_STATIC" },
        .files = &.{
            "bidi.c",     "context.c",  "data.c",   "decode.c",
            "error.c",    "free.c",     "idna.c",   "lookup.c",
            "punycode.c", "register.c", "tables.c", "tr46map.c",
            "version.c",
        },
    });
    lib.addCSourceFiles(.{
        .root = dep.path("gl"),
        .flags = &.{"-DHAVE_CONFIG_H"},
        // malloca.c provides striconveha's stack-or-heap allocator; strverscmp
        // is a glibc extension absent on macOS that lib/version.c needs.
        .files = &.{ "malloca.c", "strverscmp.c" },
    });
    lib.addCSourceFiles(.{
        .root = dep.path("unistring"),
        .flags = &.{"-DHAVE_CONFIG_H"},
        .files = &.{
            "c-ctype.c",                    "c-strcasecmp.c",                    "c-strncasecmp.c",
            "free.c",                       "iconv.c",                           "iconv_close.c",
            "iconv_open.c",                 "localcharset.c",                    "stdlib.c",
            "striconveh.c",                 "striconveha.c",                     "unistd.c",
            "uniconv/u8-conv-from-enc.c",   "uniconv/u8-strconv-from-enc.c",     "uniconv/u8-strconv-from-locale.c",
            "uniconv/u8-strconv-to-enc.c",  "uniconv/u8-strconv-to-locale.c",    "unictype/bidi_of.c",
            "unictype/categ_M.c",           "unictype/categ_none.c",             "unictype/categ_of.c",
            "unictype/categ_test.c",        "unictype/combiningclass.c",         "unictype/joiningtype_of.c",
            "unictype/scripts.c",           "uninorm/canonical-decomposition.c", "uninorm/composition.c",
            "uninorm/decompose-internal.c", "uninorm/decomposition-table.c",     "uninorm/nfc.c",
            "uninorm/nfd.c",                "uninorm/u32-normalize.c",           "unistr/u32-cmp.c",
            "unistr/u32-cpy-alloc.c",       "unistr/u32-cpy.c",                  "unistr/u32-mbtouc-unsafe.c",
            "unistr/u32-strlen.c",          "unistr/u32-to-u8.c",                "unistr/u32-uctomb.c",
            "unistr/u8-check.c",            "unistr/u8-mblen.c",                 "unistr/u8-mbtouc.c",
            "unistr/u8-mbtouc-aux.c",       "unistr/u8-mbtouc-unsafe.c",         "unistr/u8-mbtouc-unsafe-aux.c",
            "unistr/u8-mbtoucr.c",          "unistr/u8-prev.c",                  "unistr/u8-strlen.c",
            "unistr/u8-to-u32.c",           "unistr/u8-uctomb.c",                "unistr/u8-uctomb-aux.c",
        },
    });

    return lib;
}

/// Process one of unistring's `.in.h` template headers into a real `.h`.
/// All `@VAR@` substitutions in these headers are either DLL-visibility markers
/// (empty for static POSIX builds) or `HAVE_UNISTRING_WOE32DLL_H` (0).
fn renderUnistringHeader(b: *Build, dep: *Build.Dependency, name: []const u8) *Build.Step.ConfigHeader {
    const in_rel = b.fmt("unistring/{s}.in.h", .{name});
    const out_name = b.fmt("{s}.h", .{name});
    const lazy = dep.path(in_rel);
    const path = lazy.getPath3(b, null);

    const file = path.root_dir.handle.openFile(path.sub_path, .{}) catch |e| {
        std.debug.panic("openFile {s}: {s}", .{ path.sub_path, @errorName(e) });
    };
    defer file.close();
    const contents = file.readToEndAlloc(b.allocator, 4 << 20) catch @panic("OOM");

    const ch = b.addConfigHeader(.{
        .include_path = out_name,
        .style = .{ .autoconf_at = lazy },
    }, .{});

    var seen = std.StringHashMap(void).init(b.allocator);
    var i: usize = 0;
    while (std.mem.indexOfScalarPos(u8, contents, i, '@')) |s| {
        const a = s + 1;
        const e = std.mem.indexOfScalarPos(u8, contents, a, '@') orelse break;
        const var_name = contents[a..e];
        if (!isAtConfigName(var_name)) {
            // Stray '@' (e.g. an email address in a comment); advance past it
            // alone so we don't mis-pair with a later '@'.
            i = s + 1;
            continue;
        }
        const owned = b.allocator.dupe(u8, var_name) catch @panic("OOM");
        const gop = seen.getOrPut(owned) catch @panic("OOM");
        if (!gop.found_existing) {
            if (std.mem.eql(u8, var_name, "HAVE_UNISTRING_WOE32DLL_H")) {
                ch.addValue(owned, c_int, 0);
            } else {
                ch.addValue(owned, []const u8, "");
            }
        }
        i = e + 1;
    }
    return ch;
}

fn isAtConfigName(s: []const u8) bool {
    if (s.len == 0) return false;
    for (s, 0..) |c, idx| {
        const ok = switch (c) {
            'A'...'Z', '_' => true,
            '0'...'9' => idx > 0,
            else => false,
        };
        if (!ok) return false;
    }
    return true;
}

fn buildCurl(
    b: *Build,
    target: Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
    is_tsan: bool,
) *Build.Step.Compile {
    const dep = b.dependency("curl", .{});

    const mod = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_thread = is_tsan,
    });
    mod.addIncludePath(dep.path("lib"));
    mod.addIncludePath(dep.path("include"));

    const os = target.result.os.tag;
    const abi = target.result.abi;

    const is_gnu = abi.isGnu();
    const is_ios = os == .ios;
    const is_android = abi.isAndroid();
    const is_linux = os == .linux;
    const is_darwin = os.isDarwin();
    const is_windows = os == .windows;
    const is_netbsd = os == .netbsd;
    const is_openbsd = os == .openbsd;
    const is_freebsd = os == .freebsd;

    const byte_size = struct {
        fn it(b2: *std.Build, target2: Build.ResolvedTarget, name: []const u8, comptime ctype: std.Target.CType) []const u8 {
            const size = target2.result.cTypeByteSize(ctype);
            return std.fmt.allocPrint(b2.allocator, "#define SIZEOF_{s} {d}", .{ name, size }) catch @panic("OOM");
        }
    }.it;

    const config = .{
        .HAVE_LIBZ = true,
        .HAVE_BROTLI = true,
        .USE_NGHTTP2 = true,

        .USE_OPENSSL = true,
        .OPENSSL_IS_BORINGSSL = true,
        .CURL_CA_PATH = null,
        .CURL_CA_BUNDLE = null,
        .CURL_CA_FALLBACK = false,
        .CURL_CA_SEARCH_SAFE = false,
        .CURL_DEFAULT_SSL_BACKEND = "openssl",

        .CURL_DISABLE_AWS = true,
        .CURL_DISABLE_DICT = true,
        .CURL_DISABLE_DOH = true,
        .CURL_DISABLE_FILE = true,
        .CURL_DISABLE_FTP = true,
        .CURL_DISABLE_GOPHER = true,
        .CURL_DISABLE_KERBEROS_AUTH = true,
        .CURL_DISABLE_IMAP = true,
        .CURL_DISABLE_IPFS = true,
        .CURL_DISABLE_LDAP = true,
        .CURL_DISABLE_LDAPS = true,
        .CURL_DISABLE_MQTT = true,
        .CURL_DISABLE_NTLM = true,
        .CURL_DISABLE_PROGRESS_METER = true,
        .CURL_DISABLE_POP3 = true,
        .CURL_DISABLE_RTSP = true,
        .CURL_DISABLE_SMB = true,
        .CURL_DISABLE_SMTP = true,
        .CURL_DISABLE_TELNET = true,
        .CURL_DISABLE_TFTP = true,
        .CURL_DISABLE_WEBSOCKETS = false, // Enable WebSocket support

        .ssize_t = null,
        ._FILE_OFFSET_BITS = 64,

        .USE_IPV6 = true,
        // Route IDN hostnames through libidn2 (vendored, see buildLibidn2).
        // Without this, libcurl ships UTF-8 host bytes to SNI/cert validation
        // and breaks for non-ASCII hostnames like räksmörgås.se.
        .HAVE_LIBIDN2 = true,
        .HAVE_IDN2_H = true,
        .CURL_OS = switch (os) {
            .linux => if (is_android) "\"android\"" else "\"linux\"",
            else => std.fmt.allocPrint(b.allocator, "\"{s}\"", .{@tagName(os)}) catch @panic("OOM"),
        },

        // Adjusts the sizes of variables
        .SIZEOF_INT_CODE = byte_size(b, target, "INT", .int),
        .SIZEOF_LONG_CODE = byte_size(b, target, "LONG", .long),
        .SIZEOF_LONG_LONG_CODE = byte_size(b, target, "LONG_LONG", .longlong),

        .SIZEOF_OFF_T_CODE = byte_size(b, target, "OFF_T", .longlong),
        .SIZEOF_CURL_OFF_T_CODE = byte_size(b, target, "CURL_OFF_T", .longlong),
        .SIZEOF_CURL_SOCKET_T_CODE = byte_size(b, target, "CURL_SOCKET_T", .int),

        .SIZEOF_SIZE_T_CODE = byte_size(b, target, "SIZE_T", .longlong),
        .SIZEOF_TIME_T_CODE = byte_size(b, target, "TIME_T", .longlong),

        // headers availability
        .HAVE_ARPA_INET_H = !is_windows,
        .HAVE_DIRENT_H = true,
        .HAVE_FCNTL_H = true,
        .HAVE_IFADDRS_H = !is_windows,
        .HAVE_IO_H = is_windows,
        .HAVE_LIBGEN_H = true,
        .HAVE_LINUX_TCP_H = is_linux and is_gnu,
        .HAVE_LOCALE_H = true,
        .HAVE_NETDB_H = !is_windows,
        .HAVE_NETINET_IN6_H = is_android,
        .HAVE_NETINET_IN_H = !is_windows,
        .HAVE_NETINET_TCP_H = !is_windows,
        .HAVE_NETINET_UDP_H = !is_windows,
        .HAVE_NET_IF_H = !is_windows,
        .HAVE_POLL_H = !is_windows,
        .HAVE_PWD_H = !is_windows,
        .HAVE_STDATOMIC_H = true,
        .HAVE_STDBOOL_H = true,
        .HAVE_STDDEF_H = true,
        .HAVE_STDINT_H = true,
        .HAVE_STRINGS_H = true,
        .HAVE_STROPTS_H = false,
        .HAVE_SYS_EVENTFD_H = is_linux or is_freebsd or is_netbsd,
        .HAVE_SYS_FILIO_H = !is_linux and !is_windows,
        .HAVE_SYS_IOCTL_H = !is_windows,
        .HAVE_SYS_PARAM_H = true,
        .HAVE_SYS_POLL_H = !is_windows,
        .HAVE_SYS_RESOURCE_H = !is_windows,
        .HAVE_SYS_SELECT_H = !is_windows,
        .HAVE_SYS_SOCKIO_H = !is_linux and !is_windows,
        .HAVE_SYS_TYPES_H = true,
        .HAVE_SYS_UN_H = !is_windows,
        .HAVE_SYS_UTIME_H = is_windows,
        .HAVE_TERMIOS_H = !is_windows,
        .HAVE_TERMIO_H = is_linux,
        .HAVE_UNISTD_H = true,
        .HAVE_UTIME_H = true,
        .STDC_HEADERS = true,

        // general environment
        .CURL_KRB5_VERSION = null,
        .HAVE_ALARM = !is_windows,
        .HAVE_ARC4RANDOM = is_android,
        .HAVE_ATOMIC = true,
        .HAVE_BOOL_T = true,
        .HAVE_BUILTIN_AVAILABLE = true,
        .HAVE_CLOCK_GETTIME_MONOTONIC = !is_darwin and !is_windows,
        .HAVE_CLOCK_GETTIME_MONOTONIC_RAW = is_linux,
        .HAVE_FILE_OFFSET_BITS = true,
        .HAVE_GETEUID = !is_windows,
        .HAVE_GETPPID = !is_windows,
        .HAVE_GETTIMEOFDAY = true,
        .HAVE_GLIBC_STRERROR_R = is_gnu,
        .HAVE_GMTIME_R = !is_windows,
        .HAVE_LOCALTIME_R = !is_windows,
        .HAVE_LONGLONG = !is_windows,
        .HAVE_MACH_ABSOLUTE_TIME = is_darwin,
        .HAVE_MEMRCHR = !is_darwin and !is_windows,
        .HAVE_POSIX_STRERROR_R = !is_gnu and !is_windows,
        .HAVE_PTHREAD_H = !is_windows,
        .HAVE_SETLOCALE = true,
        .HAVE_SETRLIMIT = !is_windows,
        .HAVE_SIGACTION = !is_windows,
        .HAVE_SIGINTERRUPT = !is_windows,
        .HAVE_SIGNAL = true,
        .HAVE_SIGSETJMP = !is_windows,
        .HAVE_SIZEOF_SA_FAMILY_T = false,
        .HAVE_SIZEOF_SUSECONDS_T = false,
        .HAVE_SNPRINTF = true,
        .HAVE_STRCASECMP = !is_windows,
        .HAVE_STRCMPI = false,
        .HAVE_STRDUP = true,
        .HAVE_STRERROR_R = !is_windows,
        .HAVE_STRICMP = false,
        .HAVE_STRUCT_TIMEVAL = true,
        .HAVE_TIME_T_UNSIGNED = false,
        .HAVE_UTIME = true,
        .HAVE_UTIMES = !is_windows,
        .HAVE_WRITABLE_ARGV = !is_windows,
        .HAVE__SETMODE = is_windows,
        .USE_THREADS_POSIX = !is_windows,

        // filesystem, network
        .HAVE_ACCEPT4 = is_linux or is_freebsd or is_netbsd or is_openbsd,
        .HAVE_BASENAME = true,
        .HAVE_CLOSESOCKET = is_windows,
        .HAVE_DECL_FSEEKO = !is_windows,
        .HAVE_EVENTFD = is_linux or is_freebsd or is_netbsd,
        .HAVE_FCNTL = !is_windows,
        .HAVE_FCNTL_O_NONBLOCK = !is_windows,
        .HAVE_FNMATCH = !is_windows,
        .HAVE_FREEADDRINFO = true,
        .HAVE_FSEEKO = !is_windows,
        .HAVE_FSETXATTR = is_darwin or is_linux or is_netbsd,
        .HAVE_FSETXATTR_5 = is_linux or is_netbsd,
        .HAVE_FSETXATTR_6 = is_darwin,
        .HAVE_FTRUNCATE = true,
        .HAVE_GETADDRINFO = true,
        .HAVE_GETADDRINFO_THREADSAFE = is_linux or is_freebsd or is_netbsd,
        .HAVE_GETHOSTBYNAME_R = is_linux or is_freebsd,
        .HAVE_GETHOSTBYNAME_R_3 = false,
        .HAVE_GETHOSTBYNAME_R_3_REENTRANT = false,
        .HAVE_GETHOSTBYNAME_R_5 = false,
        .HAVE_GETHOSTBYNAME_R_5_REENTRANT = false,
        .HAVE_GETHOSTBYNAME_R_6 = is_linux,
        .HAVE_GETHOSTBYNAME_R_6_REENTRANT = is_linux,
        .HAVE_GETHOSTNAME = true,
        .HAVE_GETIFADDRS = if (is_windows) false else !is_android or target.result.os.versionRange().linux.android >= 24,
        .HAVE_GETPASS_R = is_netbsd,
        .HAVE_GETPEERNAME = true,
        .HAVE_GETPWUID = !is_windows,
        .HAVE_GETPWUID_R = !is_windows,
        .HAVE_GETRLIMIT = !is_windows,
        .HAVE_GETSOCKNAME = true,
        .HAVE_IF_NAMETOINDEX = !is_windows,
        .HAVE_INET_NTOP = !is_windows,
        .HAVE_INET_PTON = !is_windows,
        .HAVE_IOCTLSOCKET = is_windows,
        .HAVE_IOCTLSOCKET_CAMEL = false,
        .HAVE_IOCTLSOCKET_CAMEL_FIONBIO = false,
        .HAVE_IOCTLSOCKET_FIONBIO = is_windows,
        .HAVE_IOCTL_FIONBIO = !is_windows,
        .HAVE_IOCTL_SIOCGIFADDR = !is_windows,
        .HAVE_MSG_NOSIGNAL = !is_windows,
        .HAVE_OPENDIR = true,
        .HAVE_PIPE = !is_windows,
        .HAVE_PIPE2 = is_linux or is_freebsd or is_netbsd or is_openbsd,
        .HAVE_POLL = !is_windows,
        .HAVE_REALPATH = !is_windows,
        .HAVE_RECV = true,
        .HAVE_SA_FAMILY_T = !is_windows,
        .HAVE_SCHED_YIELD = !is_windows,
        .HAVE_SELECT = true,
        .HAVE_SEND = true,
        .HAVE_SENDMMSG = !is_darwin and !is_windows,
        .HAVE_SENDMSG = !is_windows,
        .HAVE_SETMODE = !is_linux,
        .HAVE_SETSOCKOPT_SO_NONBLOCK = false,
        .HAVE_SOCKADDR_IN6_SIN6_ADDR = !is_windows,
        .HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID = true,
        .HAVE_SOCKET = true,
        .HAVE_SOCKETPAIR = !is_windows,
        .HAVE_STRUCT_SOCKADDR_STORAGE = true,
        .HAVE_SUSECONDS_T = is_android or is_ios,
        .USE_UNIX_SOCKETS = !is_windows,
    };

    const curl_config = b.addConfigHeader(.{
        .include_path = "curl_config.h",
        .style = .{ .cmake = dep.path("lib/curl_config-cmake.h.in") },
    }, .{
        .CURL_EXTERN_SYMBOL = "__attribute__ ((__visibility__ (\"default\"))",
    });
    curl_config.addValues(config);

    const lib = b.addLibrary(.{ .name = "curl", .root_module = mod });
    lib.addConfigHeader(curl_config);
    lib.installHeadersDirectory(dep.path("include/curl"), "curl", .{});
    lib.addCSourceFiles(.{
        .root = dep.path("lib"),
        .flags = &.{
            "-D_GNU_SOURCE",
            "-DHAVE_CONFIG_H",
            "-DCURL_STATICLIB",
            "-DBUILDING_LIBCURL",
        },
        .files = &.{
            // You can include all files from lib, libcurl uses #ifdef-guards to exclude code for disabled functions
            "altsvc.c",              "amigaos.c",              "asyn-ares.c",
            "asyn-base.c",           "asyn-thrdd.c",           "bufq.c",
            "bufref.c",              "cf-h1-proxy.c",          "cf-h2-proxy.c",
            "cf-haproxy.c",          "cf-https-connect.c",     "cf-ip-happy.c",
            "cf-socket.c",           "cfilters.c",             "conncache.c",
            "connect.c",             "content_encoding.c",     "cookie.c",
            "cshutdn.c",             "curl_addrinfo.c",        "curl_endian.c",
            "curl_fnmatch.c",        "curl_fopen.c",           "curl_get_line.c",
            "curl_gethostname.c",    "curl_gssapi.c",          "curl_memrchr.c",
            "curl_ntlm_core.c",      "curl_range.c",           "curl_rtmp.c",
            "curl_sasl.c",           "curl_sha512_256.c",      "curl_share.c",
            "curl_sspi.c",           "curl_threads.c",         "curl_trc.c",
            "curlx/base64.c",        "curlx/dynbuf.c",         "curlx/fopen.c",
            "curlx/inet_ntop.c",     "curlx/inet_pton.c",      "curlx/multibyte.c",
            "curlx/nonblock.c",      "curlx/strcopy.c",        "curlx/strerr.c",
            "curlx/strparse.c",      "curlx/timediff.c",       "curlx/timeval.c",
            "curlx/version_win32.c", "curlx/wait.c",           "curlx/warnless.c",
            "curlx/winapi.c",        "cw-out.c",               "cw-pause.c",
            "dict.c",                "dllmain.c",              "doh.c",
            "dynhds.c",              "easy.c",                 "easygetopt.c",
            "easyoptions.c",         "escape.c",               "fake_addrinfo.c",
            "file.c",                "fileinfo.c",             "formdata.c",
            "ftp.c",                 "ftplistparser.c",        "getenv.c",
            "getinfo.c",             "gopher.c",               "hash.c",
            "headers.c",             "hmac.c",                 "hostip.c",
            "hostip4.c",             "hostip6.c",              "hsts.c",
            "http.c",                "http1.c",                "http2.c",
            "http_aws_sigv4.c",      "http_chunks.c",          "http_digest.c",
            "http_negotiate.c",      "http_ntlm.c",            "http_proxy.c",
            "httpsrr.c",             "idn.c",                  "if2ip.c",
            "imap.c",                "ldap.c",                 "llist.c",
            "macos.c",               "md4.c",                  "md5.c",
            "memdebug.c",            "mime.c",                 "mprintf.c",
            "mqtt.c",                "multi.c",                "multi_ev.c",
            "multi_ntfy.c",          "netrc.c",                "noproxy.c",
            "openldap.c",            "parsedate.c",            "pingpong.c",
            "pop3.c",                "progress.c",             "psl.c",
            "rand.c",                "ratelimit.c",            "request.c",
            "rtsp.c",                "select.c",               "sendf.c",
            "setopt.c",              "sha256.c",               "slist.c",
            "smb.c",                 "smtp.c",                 "socketpair.c",
            "socks.c",               "socks_gssapi.c",         "socks_sspi.c",
            "splay.c",               "strcase.c",              "strdup.c",
            "strequal.c",            "strerror.c",             "system_win32.c",
            "telnet.c",              "tftp.c",                 "transfer.c",
            "uint-bset.c",           "uint-hash.c",            "uint-spbset.c",
            "uint-table.c",          "url.c",                  "urlapi.c",
            "vauth/cleartext.c",     "vauth/cram.c",           "vauth/digest.c",
            "vauth/digest_sspi.c",   "vauth/gsasl.c",          "vauth/krb5_gssapi.c",
            "vauth/krb5_sspi.c",     "vauth/ntlm.c",           "vauth/ntlm_sspi.c",
            "vauth/oauth2.c",        "vauth/spnego_gssapi.c",  "vauth/spnego_sspi.c",
            "vauth/vauth.c",         "version.c",              "vquic/curl_ngtcp2.c",
            "vquic/curl_osslq.c",    "vquic/curl_quiche.c",    "vquic/vquic-tls.c",
            "vquic/vquic.c",         "vssh/libssh.c",          "vssh/libssh2.c",
            "vssh/vssh.c",           "vtls/apple.c",           "vtls/cipher_suite.c",
            "vtls/gtls.c",           "vtls/hostcheck.c",       "vtls/keylog.c",
            "vtls/mbedtls.c",        "vtls/openssl.c",         "vtls/rustls.c",
            "vtls/schannel.c",       "vtls/schannel_verify.c", "vtls/vtls.c",
            "vtls/vtls_scache.c",    "vtls/vtls_spack.c",      "vtls/wolfssl.c",
            "vtls/x509asn1.c",       "ws.c",
        },
    });

    return lib;
}

fn linkZenai(b: *Build, mod: *Build.Module) void {
    const dep = b.dependency("zenai", .{});
    mod.addImport("zenai", dep.module("zenai"));
}

fn linkLinenoise(b: *Build, mod: *Build.Module) void {
    const dep = b.dependency("linenoise", .{});
    mod.addIncludePath(dep.path(""));
    mod.addCSourceFile(.{
        .file = dep.path("linenoise.c"),
    });
}

/// Resolves the semantic version of the build.
///
/// The base version is read from `build.zig.zon`. This can be overridden
/// using the `-Dversion` command-line flag:
/// - If the flag contains a full semantic version (e.g., `1.2.3`), it replaces
///   the base version entirely.
/// - If the flag contains a simple string (e.g., `nightly`), it replaces only
///   the pre-release tag of the base version (e.g., `1.0.0-dev` -> `1.0.0-nightly`).
///
/// For versions that have a pre-release tag and no explicit build metadata,
/// this function automatically enriches the version with the git commit count
/// and short hash (e.g., `1.0.0-dev.5243+dbe45229`).
fn resolveVersion(b: *std.Build) std.SemanticVersion {
    const opt_version = b.option([]const u8, "version", "Override the version of this build");

    const version = if (opt_version) |v|
        std.SemanticVersion.parse(v) catch blk: {
            var fallback = lightpanda_version;
            fallback.pre = v;
            break :blk fallback;
        }
    else
        lightpanda_version;

    // Only enrich versions that have a pre-release field and no explicit build metadata.
    if (version.pre == null or version.build != null) return version;

    // For dev/nightly versions, calculate the commit count and hash
    const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return version;
    const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");

    const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return version;
    const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");

    return .{
        .major = version.major,
        .minor = version.minor,
        .patch = version.patch,
        .pre = b.fmt("{s}.{s}", .{ version.pre.?, commit_count }),
        .build = commit_hash,
    };
}

/// Helper function to run git commands and return stdout
fn runGit(b: *std.Build, args: []const []const u8) ![]const u8 {
    var code: u8 = undefined;
    const dir = b.pathFromRoot(".");
    var command: std.ArrayList([]const u8) = .empty;
    defer command.deinit(b.allocator);
    try command.appendSlice(b.allocator, &.{ "git", "-C", dir });
    try command.appendSlice(b.allocator, args);
    return b.runAllowFail(command.items, &code, .Ignore);
}
