const std = @import("std");
const LazyPath = std.Build.LazyPath;
pub const ZxInitOptions = @import("init/ZxInitOptions.zig");

pub fn init(b: *std.Build, exe: *std.Build.Step.Compile, options: ZxInitOptions) !Build {
    const target = exe.root_module.resolved_target;
    const optimize = exe.root_module.optimize;
    const build_zig = @import("../../build.zig");
    const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none });

    const zx_dep = b.dependencyFromBuildZig(build_zig, .{
        .optimize = optimize,
        .target = target,
    });

    const zx_host_dep = b.dependencyFromBuildZig(build_zig, .{
        .optimize = optimize,
        // No target = host target, so zx CLI can execute during build
    });

    const zx_wasm_dep = b.dependencyFromBuildZig(build_zig, .{
        .optimize = optimize,
        .target = wasm_target,
    });

    const zx_module = zx_dep.module("zx");
    const zx_wasm_module = zx_wasm_dep.module("zx_wasm");
    const zx_exe = zx_host_dep.artifact("zx");

    var opts: InitInnerOptions = .{
        .site_path = b.path("app"),
        .cli_path = null,
        .site_outdir = null,
        .steps = .default,
        .copy_embedded_sources = false,
        .client = options.client,
    };

    if (options.app) |site_opts| {
        opts.site_path = site_opts.path;
        opts.copy_embedded_sources = site_opts.copy_embedded_sources;
    }

    if (options.cli) |cli_opts| {
        opts.cli_path = cli_opts.path;

        if (cli_opts.steps) |cli_steps| {
            opts.steps = cli_steps;
        }
    }

    return initInner(b, exe, zx_exe, zx_module, zx_wasm_module, opts);
}

const InitInnerOptions = struct {
    site_path: LazyPath,
    cli_path: ?LazyPath,
    site_outdir: ?LazyPath,
    steps: ZxInitOptions.CliOptions.Steps,
    copy_embedded_sources: bool,
    edge_path: ?LazyPath = null,
    client: ?ZxInitOptions.ClientOptions = null,
};

fn getZxRun(b: *std.Build, zx_exe: *std.Build.Step.Compile, opts: InitInnerOptions) *std.Build.Step.Run {
    if (opts.cli_path) |cli_path| {
        const run = b.addSystemCommand(&.{});
        run.addFileArg(cli_path);
        return run;
    }

    return b.addRunArtifact(zx_exe);
}

fn getTranspileOutdir(transpile_cmd: *std.Build.Step.Run, opts: InitInnerOptions) std.Build.LazyPath {
    if (opts.site_outdir) |site_outdir| {
        transpile_cmd.addDirectoryArg(site_outdir);
        return site_outdir;
    }

    // if user didn't provide a path, they don't want to keep transpiled output
    // this will put the transpiled output in .zig-cache/o/{HASH}/app
    return transpile_cmd.addOutputDirectoryArg("app");
}

pub fn initInner(
    b: *std.Build,
    exe: *std.Build.Step.Compile,
    zx_exe: *std.Build.Step.Compile,
    zx_module: *std.Build.Module,
    zx_wasm_module: *std.Build.Module,
    opts: InitInnerOptions,
) !Build {
    // const target = exe.root_module.resolved_target;
    const optimize = exe.root_module.optimize;

    // --- ZX Transpilation ---
    const transpile_cmd = getZxRun(b, zx_exe, opts);
    transpile_cmd.setName("zx transpile");
    transpile_cmd.addArg("transpile");
    transpile_cmd.addDirectoryArg(opts.site_path);
    transpile_cmd.addArg("--outdir");
    const transpile_outdir = getTranspileOutdir(transpile_cmd, opts);
    if (opts.copy_embedded_sources) {
        transpile_cmd.addArg("--copy-embedded-sources");
    }
    transpile_cmd.expectExitCode(0);

    // --- ZX Options --- //
    const zx_options = b.addOptions();
    zx_options.addOption(bool, "client_enabled", opts.client != null);
    zx_options.addOption(?[]const u8, "jsglue_href", if (opts.client) |client_opts| client_opts.jsglue_href else null);
    zx_module.addOptions("zx_options", zx_options);

    // --- ZX File Cache Invalidator ---
    watch: {
        const site_path = opts.site_path.getPath3(b, &transpile_cmd.step);
        var site_dir = site_path.root_dir.handle.openDir(site_path.subPathOrDot(), .{ .iterate = true }) catch break :watch;
        var itd = try site_dir.walk(transpile_cmd.step.owner.allocator);
        defer itd.deinit();
        while (try itd.next()) |entry| {
            switch (entry.kind) {
                .directory => {},
                .file => {
                    const entry_path = try site_path.join(transpile_cmd.step.owner.allocator, entry.path);
                    transpile_cmd.addFileInput(b.path(entry_path.sub_path));
                },
                else => continue,
            }
        }
    }

    // --- ZX Site Main Executable --- //
    var imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
    var import_it = exe.root_module.import_table.iterator();
    while (import_it.next()) |entry| {
        try imports.append(.{ .name = entry.key_ptr.*, .module = entry.value_ptr.* });
    }

    // Copy all imports from the original zx_module
    var zx_import_it = zx_module.import_table.iterator();
    while (zx_import_it.next()) |entry| {
        if (!std.mem.eql(u8, entry.key_ptr.*, "zx_meta")) {
            try imports.append(.{ .name = entry.key_ptr.*, .module = entry.value_ptr.* });
        }
    }

    // Create a site-specific zx module with the generated meta
    const site_zx_module = b.createModule(.{
        .root_source_file = zx_module.root_source_file,
        .target = zx_module.resolved_target,
        .optimize = zx_module.optimize,
        .imports = imports.items,
    });

    // Build imports for zx_meta (needs access to zx module and all other imports)
    var meta_imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
    for (imports.items) |import| {
        try meta_imports.append(import);
    }
    try meta_imports.append(.{ .name = "zx", .module = site_zx_module });

    // Add project-specific zx_meta to the site module
    site_zx_module.addAnonymousImport("zx_meta", .{
        .root_source_file = transpile_outdir.path(b, "meta.zig"),
        .imports = meta_imports.items,
    });

    exe.root_module.addImport("zx", site_zx_module);

    exe.step.dependOn(&transpile_cmd.step);
    b.installArtifact(exe);

    // --- ZX WASM Main Executable --- //
    const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none });
    var wasm_exe_opt: ?*std.Build.Step.Compile = null;
    var wasm_zx_meta_module: ?*std.Build.Module = null;
    if (opts.client != null) {
        wasm_exe_opt = b.addExecutable(.{
            .name = b.fmt("main", .{}),
            .root_module = b.createModule(.{
                .root_source_file = exe.root_module.root_source_file,
                .target = wasm_target,
                .optimize = optimize,
            }),
        });
        const wasm_exe = wasm_exe_opt.?;

        wasm_exe.entry = .disabled;
        wasm_exe.export_memory = true;
        wasm_exe.rdynamic = true;

        // Create a site-specific wasm module (same approach as server module)
        var wasm_imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
        var wasm_import_it = zx_wasm_module.import_table.iterator();
        while (wasm_import_it.next()) |entry| {
            if (!std.mem.eql(u8, entry.key_ptr.*, "zx_meta")) {
                try wasm_imports.append(.{ .name = entry.key_ptr.*, .module = entry.value_ptr.* });
            }
        }

        const site_wasm_module = b.createModule(.{
            .root_source_file = zx_wasm_module.root_source_file,
            .target = wasm_target,
            .optimize = zx_wasm_module.optimize,
            .imports = wasm_imports.items,
        });

        // Build imports for wasm zx_meta
        var wasm_meta_imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
        for (wasm_imports.items) |import| {
            try wasm_meta_imports.append(import);
        }
        try wasm_meta_imports.append(.{ .name = "zx", .module = site_wasm_module });

        wasm_zx_meta_module = b.addModule("zx_meta", .{
            .root_source_file = transpile_outdir.path(b, "meta.zig"),
            .imports = wasm_meta_imports.items,
        });

        site_wasm_module.addImport("zx_meta", wasm_zx_meta_module.?);
        wasm_exe.root_module.addImport("zx", site_wasm_module);
        wasm_exe.step.dependOn(&transpile_cmd.step);

        // --- CMD: ZX Post Transpile --- //
        const post_transpile_cmd = getZxRun(b, zx_exe, opts);
        post_transpile_cmd.addArgs(&.{"transpile"});
        post_transpile_cmd.addFileArg(wasm_exe.getEmittedBin());
        post_transpile_cmd.addArgs(&.{ "--copy-only", "--outdir" });
        post_transpile_cmd.addDirectoryArg(transpile_outdir.path(b, "assets"));
        post_transpile_cmd.expectExitCode(0);
        post_transpile_cmd.step.dependOn(&transpile_cmd.step);
        post_transpile_cmd.step.dependOn(&wasm_exe.step);
        b.default_step.dependOn(&post_transpile_cmd.step);
    }

    // --- ZX WASM Edge Executable --- //
    var wasm_edge_exe_opt: ?*std.Build.Step.Compile = null;
    if (opts.client != null and opts.edge_path != null) {
        wasm_edge_exe_opt = b.addExecutable(.{
            .name = b.fmt("edge", .{}),
            .root_module = b.createModule(.{
                .root_source_file = opts.edge_path,
                .target = wasm_target,
                .optimize = optimize,
            }),
        });
        const wasm_edge_exe = wasm_edge_exe_opt.?;

        wasm_edge_exe.entry = .disabled;
        wasm_edge_exe.export_memory = true;
        wasm_edge_exe.rdynamic = true;
        // Create a site-specific wasm module (same approach as server module)
        var wasm_imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
        var wasm_import_it = zx_wasm_module.import_table.iterator();
        while (wasm_import_it.next()) |entry| {
            if (!std.mem.eql(u8, entry.key_ptr.*, "zx_meta")) {
                try wasm_imports.append(.{ .name = entry.key_ptr.*, .module = entry.value_ptr.* });
            }
        }

        const site_wasm_module = b.createModule(.{
            .root_source_file = zx_wasm_module.root_source_file,
            .target = wasm_target,
            .optimize = zx_wasm_module.optimize,
            .imports = wasm_imports.items,
        });

        // Build imports for wasm zx_meta
        var wasm_meta_imports = std.array_list.Managed(std.Build.Module.Import).init(b.allocator);
        for (wasm_imports.items) |import| {
            try wasm_meta_imports.append(import);
        }
        try wasm_meta_imports.append(.{ .name = "zx", .module = site_wasm_module });

        site_wasm_module.addAnonymousImport("zx_meta", .{
            .root_source_file = transpile_outdir.path(b, "meta.zig"),
            .imports = wasm_meta_imports.items,
        });

        wasm_edge_exe.root_module.addImport("zx", site_wasm_module);
        wasm_edge_exe.step.dependOn(&transpile_cmd.step);

        // --- CMD: ZX Post Transpile --- //
        const post_transpile_cmd = getZxRun(b, zx_exe, opts);
        post_transpile_cmd.addArgs(&.{"transpile"});
        post_transpile_cmd.addFileArg(wasm_edge_exe.getEmittedBin());
        post_transpile_cmd.addArgs(&.{ "--copy-only", "--outdir" });
        post_transpile_cmd.addDirectoryArg(transpile_outdir.path(b, "assets"));
        post_transpile_cmd.expectExitCode(0);
        post_transpile_cmd.step.dependOn(&transpile_cmd.step);
        post_transpile_cmd.step.dependOn(&wasm_edge_exe.step);
        b.default_step.dependOn(&post_transpile_cmd.step);
    }

    // --- Steps: ZX (Root of ZX CLI) --- //
    {
        const zx_step = b.step(
            "zx",
            b.fmt("ZX CLI - \x1b[2m{s}\x1b[0m", .{"zig build zx -- <args>"}),
        );
        const zx_cmd = b.addRunArtifact(zx_exe);
        zx_step.dependOn(&zx_cmd.step);
        if (b.args) |args| zx_cmd.addArgs(args);
    }

    // --- Steps: Serve --- //
    if (opts.steps.serve) |serve_step_name| {
        const serve_step = b.step(serve_step_name, "Run the Ziex app with production behavior");
        const serve_cmd = b.addRunArtifact(exe);
        serve_cmd.step.dependOn(b.getInstallStep());
        serve_cmd.step.dependOn(&transpile_cmd.step);
        serve_step.dependOn(&serve_cmd.step);
        if (b.args) |args| serve_cmd.addArgs(args);
    }

    // --- Steps: Dev --- //
    if (opts.steps.dev) |dev_step_name| {
        const dev_cmd = getZxRun(b, zx_exe, opts);
        dev_cmd.addArgs(&.{
            "dev",
            // "--binpath",
        });
        // dev_cmd.addFileArg(exe.getEmittedBin());
        const dev_step = b.step(dev_step_name, "Run the Ziex app in development mode");
        dev_step.dependOn(&dev_cmd.step);
        if (b.args) |args| dev_cmd.addArgs(args);
    }

    // --- Steps: Export --- //
    if (opts.steps.@"export") |export_step_name| {
        const export_cmd = getZxRun(b, zx_exe, opts);
        export_cmd.addArgs(&.{"export"});
        const export_step = b.step(export_step_name, "Export the Ziex app for static hosting");
        export_step.dependOn(&export_cmd.step);
        if (b.args) |args| export_cmd.addArgs(args);
    }

    // --- Steps: Bundle --- //
    if (opts.steps.bundle) |bundle_step_name| {
        const bundle_cmd = getZxRun(b, zx_exe, opts);
        bundle_cmd.addArgs(&.{"bundle"});
        const bundle_step = b.step(bundle_step_name, "Bundle the Ziex app for production deployment");
        bundle_step.dependOn(&bundle_cmd.step);
        if (b.args) |args| bundle_cmd.addArgs(args);
    }

    return .{
        .build = b,
        .cmd = .{
            .transpile = transpile_cmd,
        },
        .outdir = transpile_outdir,
        .assetsdir = transpile_outdir.path(b, "assets"),
        .zx = .{
            .exe = zx_exe,
        },
        .server = .{
            .exe = exe,
        },
        .client = if (wasm_zx_meta_module != null) .{
            .exe = wasm_exe_opt.?,
            .root_module = wasm_zx_meta_module.?,
        } else null,
    };
}

pub const Build = struct {
    pub const BuildClient = struct {
        exe: *std.Build.Step.Compile,
        root_module: *std.Build.Module,
    };

    pub const BuildServer = struct {
        exe: *std.Build.Step.Compile,
    };

    pub const BuildZiex = struct {
        exe: *std.Build.Step.Compile,
    };

    pub const BuildCommand = struct {
        transpile: *std.Build.Step.Run,
    };

    build: *std.Build,

    cmd: BuildCommand,

    outdir: LazyPath,
    assetsdir: LazyPath,

    zx: BuildZiex,

    server: BuildServer,
    client: ?BuildClient,

    pub fn addPlugin(b: *Build, plugin: ZxInitOptions.PluginOptions) void {
        for (plugin.steps) |*step| {
            switch (step.*) {
                .command => {
                    var run = step.command.run;

                    // TODO: Fails when used with remote package, was added to supress the output of TW Plugin
                    // But it tries to check for that before the plugin is run, so it fails.
                    // _ = run.captureStdErr();
                    // run.captured_stderr = null;
                    run.setName(plugin.name);

                    const transpile_cmd = b.cmd.transpile;
                    const exe = b.server.exe;
                    switch (step.command.type) {
                        .before_transpile => transpile_cmd.step.dependOn(&run.step),
                        .after_transpile => {
                            run.step.dependOn(&transpile_cmd.step);
                            exe.step.dependOn(&run.step);
                        },
                    }
                },
            }
        }
    }
};
