usingnamespace @import("global.zig");
usingnamespace @import("./http.zig");

const std = @import("std");
const lex = @import("js_lexer.zig");
const logger = @import("logger.zig");
const alloc = @import("alloc.zig");
const options = @import("options.zig");
const js_parser = @import("js_parser.zig");
const json_parser = @import("json_parser.zig");
const js_printer = @import("js_printer.zig");
const js_ast = @import("js_ast.zig");
const linker = @import("linker.zig");
usingnamespace @import("ast/base.zig");
usingnamespace @import("defines.zig");
const panicky = @import("panic_handler.zig");
const sync = @import("./sync.zig");
const Api = @import("api/schema.zig").Api;
const resolve_path = @import("./resolver/resolve_path.zig");
const configureTransformOptionsForBun = @import("./javascript/jsc/config.zig").configureTransformOptionsForBun;
const clap = @import("clap");

const bundler = @import("bundler.zig");
const DotEnv = @import("./env_loader.zig");

const fs = @import("fs.zig");
const Router = @import("./router.zig");

const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle;

const BunCommand = @import("./cli/bun_command.zig").BunCommand;
const DevCommand = @import("./cli/dev_command.zig").DevCommand;
const DiscordCommand = @import("./cli/discord_command.zig").DiscordCommand;
const BuildCommand = @import("./cli/build_command.zig").BuildCommand;
const CreateCommand = @import("./cli/create_command.zig").CreateCommand;
const CreateListExamplesCommand = @import("./cli/create_command.zig").CreateListExamplesCommand;
const RunCommand = @import("./cli/run_command.zig").RunCommand;

var start_time: i128 = undefined;

pub const Cli = struct {
    var wait_group: sync.WaitGroup = undefined;
    pub fn startTransform(allocator: *std.mem.Allocator, args: Api.TransformOptions, log: *logger.Log) anyerror!void {}
    pub fn start(allocator: *std.mem.Allocator, stdout: anytype, stderr: anytype, comptime MainPanicHandler: type) anyerror!void {
        start_time = std.time.nanoTimestamp();
        var log = try allocator.create(logger.Log);
        log.* = logger.Log.init(allocator);
        var panicker = MainPanicHandler.init(log);
        MainPanicHandler.Singleton = &panicker;

        Command.start(allocator, log) catch |err| {
            switch (err) {
                error.MissingEntryPoint => {
                    Output.prettyErrorln("<r><red>MissingEntryPoint<r> what do you want to bundle?\n\n<d>Example:\n\n<r>  <b><cyan>bun bun --use next<r>\n\n  <b><cyan>bun bun ./src/index.ts ./src/file2.ts<r>\n", .{});
                    Output.flush();
                    std.os.exit(1);
                },
                else => {
                    return err;
                },
            }
        };
    }
};

const LoaderMatcher = strings.ExactSizeMatcher(4);
const ColonListType = @import("./cli/colon_list_type.zig").ColonListType;
pub const LoaderColonList = ColonListType(Api.Loader, Arguments.loader_resolver);
pub const DefineColonList = ColonListType(string, Arguments.noop_resolver);

pub const Arguments = struct {
    pub fn loader_resolver(in: string) !Api.Loader {
        const Matcher = strings.ExactSizeMatcher(4);
        switch (Matcher.match(in)) {
            Matcher.case("jsx") => return Api.Loader.jsx,
            Matcher.case("js") => return Api.Loader.js,
            Matcher.case("ts") => return Api.Loader.ts,
            Matcher.case("tsx") => return Api.Loader.tsx,
            Matcher.case("css") => return Api.Loader.css,
            Matcher.case("file") => return Api.Loader.file,
            Matcher.case("json") => return Api.Loader.json,
            else => {
                return error.InvalidLoader;
            },
        }
    }

    pub fn noop_resolver(in: string) !string {
        return in;
    }

    pub fn fileReadError(err: anyerror, stderr: anytype, filename: string, kind: string) noreturn {
        stderr.writer().print("Error reading file \"{s}\" for {s}: {s}", .{ filename, kind, @errorName(err) }) catch {};
        std.process.exit(1);
    }

    pub fn readFile(
        allocator: *std.mem.Allocator,
        cwd: string,
        filename: string,
    ) ![]u8 {
        var paths = [_]string{ cwd, filename };
        const outpath = try std.fs.path.resolve(allocator, &paths);
        defer allocator.free(outpath);
        var file = try std.fs.openFileAbsolute(outpath, std.fs.File.OpenFlags{ .read = true, .write = false });
        defer file.close();
        const stats = try file.stat();
        return try file.readToEndAlloc(allocator, stats.size);
    }

    pub fn resolve_jsx_runtime(str: string) !Api.JsxRuntime {
        if (strings.eqlComptime(str, "automatic")) {
            return Api.JsxRuntime.automatic;
        } else if (strings.eqlComptime(str, "fallback")) {
            return Api.JsxRuntime.classic;
        } else {
            return error.InvalidJSXRuntime;
        }
    }

    pub const ParamType = clap.Param(clap.Help);

    const params: [26]ParamType = brk: {
        @setEvalBranchQuota(9999);
        break :brk [_]ParamType{
            clap.parseParam("--use <STR>                       Choose a framework, e.g. \"--use next\". It checks first for a package named \"bun-framework-packagename\" and then \"packagename\".") catch unreachable,
            clap.parseParam("--bunfile <STR>                   Use a .bun file (default: node_modules.bun)") catch unreachable,
            clap.parseParam("--server-bunfile <STR>            Use a .server.bun file (default: node_modules.server.bun)") catch unreachable,
            clap.parseParam("--cwd <STR>                       Absolute path to resolve files & entry points from. This just changes the process' cwd.") catch unreachable,
            clap.parseParam("--disable-react-fast-refresh      Disable React Fast Refresh") catch unreachable,
            clap.parseParam("--disable-hmr                     Disable Hot Module Reloading (disables fast refresh too)") catch unreachable,
            clap.parseParam("--extension-order <STR>...        defaults to: .tsx,.ts,.jsx,.js,.json ") catch unreachable,
            clap.parseParam("--jsx-factory <STR>               Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
            clap.parseParam("--jsx-fragment <STR>              Changes the function called when compiling JSX fragments using the classic JSX runtime") catch unreachable,
            clap.parseParam("--jsx-import-source <STR>         Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable,
            clap.parseParam("--jsx-production                  Use jsx instead of jsxDEV (default) for the automatic runtime") catch unreachable,
            clap.parseParam("--jsx-runtime <STR>               \"automatic\" (default) or \"classic\"") catch unreachable,
            clap.parseParam("--main-fields <STR>...            Main fields to lookup in package.json. Defaults to --platform dependent") catch unreachable,
            clap.parseParam("--no-summary                      Don't print a summary (when generating .bun") catch unreachable,
            clap.parseParam("--version                         Print version and exit") catch unreachable,
            clap.parseParam("--origin <STR>                    Rewrite import paths to start with --origin. Default: \"/\"") catch unreachable,
            clap.parseParam("--port <STR>                      Port to serve Bun's dev server on. Default: \"/3000\"") catch unreachable,
            clap.parseParam("--platform <STR>                  \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable,
            // clap.parseParam("--production                      [not implemented] generate production code") catch unreachable,
            clap.parseParam("--public-dir <STR>                Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable,
            clap.parseParam("--tsconfig-override <STR>         Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable,
            clap.parseParam("-d, --define <STR>...             Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\". Values are parsed as JSON.") catch unreachable,
            clap.parseParam("-e, --external <STR>...           Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable,
            clap.parseParam("-h, --help                        Display this help and exit.              ") catch unreachable,
            clap.parseParam("-i, --inject <STR>...             Inject module at the top of every file") catch unreachable,
            clap.parseParam("-l, --loader <STR>...             Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx, ts, css") catch unreachable,
            // clap.parseParam("-o, --outdir <STR>                Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable,
            // clap.parseParam("-r, --resolve <STR>               Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable,
            // clap.parseParam("-r, --resolve <STR>               Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable,

            clap.parseParam("<POS>...                          ") catch unreachable,
        };
    };

    fn printVersionAndExit() noreturn {
        @setCold(true);
        Output.writer().writeAll(Global.package_json_version) catch {};
        Output.flush();
        std.os.exit(0);
    }

    pub fn parse(allocator: *std.mem.Allocator, comptime cmd: Command.Tag) !Api.TransformOptions {
        var diag = clap.Diagnostic{};

        var args = clap.parse(clap.Help, &params, .{
            .diagnostic = &diag,
            .allocator = allocator,
        }) catch |err| {
            // Report useful error and exit
            diag.report(Output.errorWriter(), err) catch {};
            return err;
        };

        if (args.flag("--version")) {
            printVersionAndExit();
        }

        var cwd_paths = [_]string{args.option("--cwd") orelse try std.process.getCwdAlloc(allocator)};
        var cwd = try std.fs.path.resolve(allocator, &cwd_paths);

        var defines_tuple = try DefineColonList.resolve(allocator, args.options("--define"));
        var loader_tuple = try LoaderColonList.resolve(allocator, args.options("--define"));
        var externals = std.mem.zeroes([][]u8);
        if (args.options("--external").len > 0) {
            externals = try allocator.alloc([]u8, args.options("--external").len);
            for (args.options("--external")) |external, i| {
                externals[i] = constStrToU8(external);
            }
        }

        var opts = Api.TransformOptions{
            .tsconfig_override = if (args.option("--tsconfig-override")) |ts| (Arguments.readFile(allocator, cwd, ts) catch |err| fileReadError(err, Output.errorStream(), ts, "tsconfig.json")) else null,
            .external = externals,
            .absolute_working_dir = cwd,
            .origin = args.option("--origin"),
            .define = .{
                .keys = defines_tuple.keys,
                .values = defines_tuple.values,
            },
            .loaders = .{
                .extensions = loader_tuple.keys,
                .loaders = loader_tuple.values,
            },
            .port = if (args.option("--port")) |port_str| std.fmt.parseInt(u16, port_str, 10) catch return error.InvalidPort else null,

            .serve = cmd == .DevCommand or (FeatureFlags.dev_only and cmd == .AutoCommand),
            .main_fields = args.options("--main-fields"),
            .generate_node_module_bundle = cmd == .BunCommand,
            .inject = args.options("--inject"),
            .extension_order = args.options("--extension-order"),
            .entry_points = undefined,
            .no_summary = args.flag("--no-summary"),
            .disable_hmr = args.flag("--disable-hmr"),
        };

        if (opts.port != null and opts.origin == null) {
            opts.origin = try std.fmt.allocPrint(allocator, "http://localhost:{d}/", .{opts.port.?});
        }

        const print_help = args.flag("--help");
        if (print_help) {
            clap.help(Output.writer(), &params) catch {};
            Output.prettyln("\n-------\n\n", .{});
            Output.flush();
            HelpCommand.printWithReason(.explicit);
            Output.flush();
            std.os.exit(0);
        }

        // var output_dir = args.option("--outdir");
        var output_dir: ?string = null;

        var define_keys = defines_tuple.keys;
        var define_values = defines_tuple.values;
        var loader_keys = loader_tuple.keys;
        var loader_values = loader_tuple.values;
        var entry_points = args.positionals();

        switch (comptime cmd) {
            .BunCommand => {
                if (entry_points.len > 0 and (strings.eqlComptime(
                    entry_points[0],
                    "bun",
                ))) {
                    entry_points = entry_points[1..];
                }
            },
            .DevCommand => {
                if (entry_points.len > 0 and (strings.eqlComptime(
                    entry_points[0],
                    "dev",
                ) or strings.eqlComptime(
                    entry_points[0],
                    "d",
                ))) {
                    entry_points = entry_points[1..];
                }
            },
            .BuildCommand => {
                if (entry_points.len > 0 and (strings.eqlComptime(
                    entry_points[0],
                    "build",
                ) or strings.eqlComptime(
                    entry_points[0],
                    "b",
                ))) {
                    entry_points = entry_points[1..];
                }
            },
            .RunCommand => {
                if (entry_points.len > 0 and (strings.eqlComptime(
                    entry_points[0],
                    "run",
                ) or strings.eqlComptime(
                    entry_points[0],
                    "r",
                ))) {
                    entry_points = entry_points[1..];
                }
            },
            else => {},
        }

        const production = false; //args.flag("--production");

        var write = entry_points.len > 1 or output_dir != null;
        if (write and output_dir == null) {
            var _paths = [_]string{ cwd, "out" };
            output_dir = try std.fs.path.resolve(allocator, &_paths);
        }
        opts.write = write;
        opts.entry_points = entry_points;

        var jsx_factory = args.option("--jsx-factory");
        var jsx_fragment = args.option("--jsx-fragment");
        var jsx_import_source = args.option("--jsx-import-source");
        var jsx_runtime = args.option("--jsx-runtime");
        var jsx_production = args.flag("--jsx-production") or production;
        const react_fast_refresh = switch (comptime cmd) {
            .BunCommand, .DevCommand => !(args.flag("--disable-react-fast-refresh") or jsx_production),
            else => true,
        };

        opts.node_modules_bundle_path = args.option("--bunfile") orelse brk: {
            const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.bun");

            break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null;
        };

        opts.node_modules_bundle_path_server = args.option("--server-bunfile") orelse brk: {
            const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.server.bun");

            break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null;
        };

        switch (comptime cmd) {
            .AutoCommand, .DevCommand, .BuildCommand, .BunCommand => {
                if (args.option("--public-dir")) |public_dir| {
                    if (public_dir.len > 0) {
                        opts.router = Api.RouteConfig{ .extensions = &.{}, .dir = &.{}, .static_dir = public_dir };
                    }
                }
            },
            else => {},
        }

        const ResolveMatcher = strings.ExactSizeMatcher(8);

        opts.resolve = Api.ResolveMode.lazy;

        switch (comptime cmd) {
            .BuildCommand => {
                // if (args.option("--resolve")) |_resolve| {
                //     switch (ResolveMatcher.match(_resolve)) {
                //         ResolveMatcher.case("disable") => {
                //             opts.resolve = Api.ResolveMode.disable;
                //         },
                //         ResolveMatcher.case("bundle") => {
                //             opts.resolve = Api.ResolveMode.bundle;
                //         },
                //         ResolveMatcher.case("dev") => {
                //             opts.resolve = Api.ResolveMode.dev;
                //         },
                //         ResolveMatcher.case("lazy") => {
                //             opts.resolve = Api.ResolveMode.lazy;
                //         },
                //         else => {
                //             diag.name.long = "--resolve";
                //             diag.arg = _resolve;
                //             try diag.report(Output.errorWriter(), error.InvalidResolveOption);
                //             std.process.exit(1);
                //         },
                //     }
                // }
            },
            else => {},
        }

        const PlatformMatcher = strings.ExactSizeMatcher(8);

        if (args.option("--platform")) |_platform| {
            switch (PlatformMatcher.match(_platform)) {
                PlatformMatcher.case("browser") => {
                    opts.platform = Api.Platform.browser;
                },
                PlatformMatcher.case("node") => {
                    opts.platform = Api.Platform.node;
                },
                else => {
                    diag.name.long = "--platform";
                    diag.arg = _platform;
                    try diag.report(Output.errorWriter(), error.InvalidPlatform);
                    std.process.exit(1);
                },
            }
        }

        if (jsx_factory != null or
            jsx_fragment != null or
            jsx_import_source != null or
            jsx_runtime != null or
            jsx_production or !react_fast_refresh)
        {
            var default_factory = "".*;
            var default_fragment = "".*;
            var default_import_source = "".*;
            opts.jsx = Api.Jsx{
                .factory = constStrToU8(jsx_factory orelse &default_factory),
                .fragment = constStrToU8(jsx_fragment orelse &default_fragment),
                .import_source = constStrToU8(jsx_import_source orelse &default_import_source),
                .runtime = if (jsx_runtime != null) try resolve_jsx_runtime(jsx_runtime.?) else Api.JsxRuntime.automatic,
                .development = !jsx_production,
                .react_fast_refresh = react_fast_refresh,
            };
        }

        if (args.option("--use")) |entry| {
            opts.framework = Api.FrameworkConfig{
                .package = entry,
                .development = !production,
            };
        }

        if (cmd == .BunCommand or !FeatureFlags.dev_only) {
            if (entry_points.len == 0 and opts.framework == null and opts.node_modules_bundle_path == null) {
                return error.MissingEntryPoint;
            }
        }

        opts.output_dir = output_dir;
        return opts;
    }
};

const AutoCommand = struct {
    pub fn exec(allocator: *std.mem.Allocator) !void {
        try HelpCommand.execWithReason(allocator, .invalid_command);
    }
};
const InitCommand = struct {
    pub fn exec(allocator: *std.mem.Allocator) !void {}
};
const HelpCommand = struct {
    pub fn exec(allocator: *std.mem.Allocator) !void {
        @setCold(true);
        execWithReason(allocator, .explicit);
    }

    pub const Reason = enum {
        explicit,
        invalid_command,
    };

    pub fn printWithReason(comptime reason: Reason) void {
        var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
        const cwd = std.os.getcwd(&cwd_buf) catch unreachable;
        const dirname = std.fs.path.basename(cwd);
        if (FeatureFlags.dev_only) {
            const fmt =
                \\> <r> <b><green>dev     <r><d>  ./a.ts ./b.jsx<r>        Start a Bun Dev Server
                \\> <r> <b><magenta>bun     <r><d>  ./a.ts ./b.jsx<r>        Bundle dependencies of input files into a <r><magenta>.bun<r>
                \\> <r> <b><cyan>create     <r><d>  next ./app<r>          Start a new project from a template <d>(shorthand: c)<r>
                \\> <r> <b><blue>discord <r>                        Open Bun's Discord server
                \\> <r> <b><d>help      <r>                      Print this help menu
                \\
            ;

            switch (reason) {
                .explicit => Output.pretty("<r><b><magenta>Bun<r>: a fast bundler & transpiler for web software.\n\n" ++ fmt, .{}),
                .invalid_command => Output.prettyError("<r><red>Uh-oh<r> not sure what to do with that command.\n\n" ++ fmt, .{}),
            }
        } else {
            const fmt =
                \\> <r> <b><white>init<r>                           Setup Bun in \"{s}\"
                \\> <r> <b><green>dev    <r><d>  ./a.ts ./b.jsx<r>        Start a Bun Dev Server
                \\<d>*<r> <b><cyan>build  <r><d>  ./a.ts ./b.jsx<r>        Make JavaScript-like code runnable & bundle CSS
                \\> <r> <b><cyan>create<r><d> next<r>        Use a template from https://github.com/jarred-sumner/bun/tree/main/examples<r>
                \\> <r> <b><magenta>bun    <r><d>  ./a.ts ./b.jsx<r>        Bundle dependencies of input files into a <r><magenta>.bun<r>
                \\> <r> <green>run    <r><d>  ./a.ts        <r>        Run a JavaScript-like file with Bun.js
                \\> <r> <b><blue>discord<r>                        Open Bun's Discord server
                \\> <r> <b><d>help      <r>                     Print this help menu
                \\
            ;

            switch (reason) {
                .explicit => Output.pretty("<r><b><magenta>Bun<r>: a fast bundler & transpiler for web software.\n\n" ++ fmt, .{dirname}),
                .invalid_command => Output.prettyError("<r><red>Uh-oh<r> not sure what to do with that command.\n\n" ++ fmt, .{dirname}),
            }
        }

        Output.flush();
    }
    pub fn execWithReason(allocator: *std.mem.Allocator, comptime reason: Reason) void {
        @setCold(true);
        printWithReason(reason);

        if (reason == .invalid_command) {
            std.process.exit(1);
        }
    }
};

pub const PrintBundleCommand = struct {
    pub fn exec(ctx: Command.Context) !void {
        const entry_point = ctx.args.entry_points[0];
        var out_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
        var stdout = std.io.getStdOut();

        var input = try std.fs.openFileAbsolute(try std.os.realpath(ctx.args.entry_points[0], &out_buffer), .{ .read = true });
        const params = comptime [_]Arguments.ParamType{
            clap.parseParam("--summary  Peek inside the .bun") catch unreachable,
        };

        var jsBundleArgs = clap.parse(clap.Help, &params, .{ .allocator = ctx.allocator }) catch |err| {
            try NodeModuleBundle.printBundle(std.fs.File, input, @TypeOf(stdout), stdout);
            return;
        };

        if (jsBundleArgs.flag("--summary")) {
            NodeModuleBundle.printSummaryFromDisk(std.fs.File, input, @TypeOf(stdout), stdout, ctx.allocator) catch {};
            return;
        }

        try NodeModuleBundle.printBundle(std.fs.File, input, @TypeOf(stdout), stdout);
    }
};

pub const Command = struct {
    pub const Context = struct {
        start_time: i128,
        args: Api.TransformOptions = std.mem.zeroes(Api.TransformOptions),
        log: *logger.Log,
        allocator: *std.mem.Allocator,

        pub fn create(allocator: *std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {
            return Command.Context{
                .args = if (comptime command != Command.Tag.CreateCommand)
                    try Arguments.parse(allocator, command)
                else
                    std.mem.zeroes(Api.TransformOptions),
                .log = log,
                .start_time = start_time,
                .allocator = allocator,
            };
        }
    };

    pub fn which(allocator: *std.mem.Allocator) Tag {
        var args_iter = std.process.args();
        // first one is the executable name
        const skipped = args_iter.skip();

        if (!skipped) {
            return .AutoCommand;
        }

        var next_arg = (args_iter.next(allocator) orelse return .AutoCommand) catch unreachable;
        while (next_arg[0] == '-') {
            next_arg = (args_iter.next(allocator) orelse return .AutoCommand) catch unreachable;
        }

        const first_arg_name = std.mem.span(next_arg);
        const RootCommandMatcher = strings.ExactSizeMatcher(8);

        if (comptime FeatureFlags.dev_only) {
            return switch (RootCommandMatcher.match(first_arg_name)) {
                RootCommandMatcher.case("init") => .InitCommand,
                RootCommandMatcher.case("bun") => .BunCommand,
                RootCommandMatcher.case("discord") => .DiscordCommand,
                RootCommandMatcher.case("c"), RootCommandMatcher.case("create") => .CreateCommand,

                RootCommandMatcher.case("b"), RootCommandMatcher.case("build") => .BuildCommand,
                RootCommandMatcher.case("r"), RootCommandMatcher.case("run") => .RunCommand,
                RootCommandMatcher.case("d"), RootCommandMatcher.case("dev") => .DevCommand,

                RootCommandMatcher.case("help") => .HelpCommand,
                else => .AutoCommand,
            };
        } else {
            return switch (RootCommandMatcher.match(first_arg_name)) {
                RootCommandMatcher.case("init") => .InitCommand,
                RootCommandMatcher.case("bun") => .BunCommand,
                RootCommandMatcher.case("discord") => .DiscordCommand,
                RootCommandMatcher.case("d"), RootCommandMatcher.case("dev") => .DevCommand,
                RootCommandMatcher.case("c"), RootCommandMatcher.case("create") => .CreateCommand,

                RootCommandMatcher.case("help") => .HelpCommand,
                else => .AutoCommand,
            };
        }
    }

    pub fn start(allocator: *std.mem.Allocator, log: *logger.Log) !void {
        const tag = which(allocator);
        switch (tag) {
            .DiscordCommand => return try DiscordCommand.exec(allocator),
            .HelpCommand => return try HelpCommand.exec(allocator),
            .InitCommand => return try InitCommand.exec(allocator),
            else => {},
        }

        switch (tag) {
            .BunCommand => {
                const ctx = try Command.Context.create(allocator, log, .BunCommand);

                try BunCommand.exec(ctx);
            },
            .DevCommand => {
                const ctx = try Command.Context.create(allocator, log, .DevCommand);

                try DevCommand.exec(ctx);
            },
            .BuildCommand => {
                const ctx = try Command.Context.create(allocator, log, .BuildCommand);

                try BuildCommand.exec(ctx);
            },
            .CreateCommand => {
                const ctx = try Command.Context.create(allocator, log, .CreateCommand);
                var positionals: [2]string = undefined;
                var positional_i: usize = 0;

                var args = try std.process.argsAlloc(allocator);

                if (args.len > 2) {
                    var remainder = args[2..];
                    var remainder_i: usize = 0;
                    var i: usize = 0;
                    while (remainder_i < remainder.len and positional_i < positionals.len) : (remainder_i += 1) {
                        var slice = std.mem.trim(u8, std.mem.span(remainder[remainder_i]), " \t\n;");
                        if (slice.len > 0) {
                            positionals[positional_i] = slice;
                            positional_i += 1;
                        }
                    }
                }
                var positionals_ = positionals[0..positional_i];

                switch (positionals_.len) {
                    0...1 => try CreateListExamplesCommand.exec(ctx),
                    else => try CreateCommand.exec(ctx, positionals_),
                }
            },
            .RunCommand => {
                const ctx = try Command.Context.create(allocator, log, .RunCommand);

                try RunCommand.exec(ctx);
            },
            .AutoCommand => {
                var ctx = Command.Context.create(allocator, log, .AutoCommand) catch |e| {
                    switch (e) {
                        error.MissingEntryPoint => {
                            HelpCommand.execWithReason(allocator, .explicit);
                            return;
                        },
                        else => {
                            return e;
                        },
                    }
                };

                if (ctx.args.entry_points.len == 1 and
                    std.mem.endsWith(u8, ctx.args.entry_points[0], ".bun"))
                {
                    try PrintBundleCommand.exec(ctx);
                    return;
                }

                if (FeatureFlags.dev_only) {
                    try DevCommand.exec(ctx);
                } else {
                    try BuildCommand.exec(ctx);
                }
            },
            else => unreachable,
        }
    }

    pub const Tag = enum {
        InitCommand,
        BunCommand,
        DevCommand,
        DiscordCommand,
        BuildCommand,
        RunCommand,
        AutoCommand,
        HelpCommand,
        CreateCommand,
    };
};
