pub fn Server(comptime H: type) type {
    const AppCtxType = switch (@typeInfo(H)) {
        .@"struct" => H,
        .pointer => |ptr| ptr.child,
        .void => void,
        else => @compileError("Server app context must be a struct, pointer to struct, or void, got: " ++ @tagName(@typeInfo(H))),
    };

    return struct {
        const Self = @This();

        pub const Meta = ServerMeta;
        pub const version = module_config.version;

        allocator: std.mem.Allocator,
        meta: ServerMeta,
        handler: HandlerType,
        server: httpz.Server(*HandlerType),
        config: AppConfig,
        app_ctx: H,
        io: std.Io,

        _is_listening: bool = false,

        const HandlerType = Handler(AppCtxType);

        pub fn init(io: std.Io, allocator: std.mem.Allocator, config: AppConfig, app_ctx: H) !*Self {
            const self = try allocator.create(Self);
            errdefer allocator.destroy(self);

            self.allocator = allocator;
            self.meta = zx_app.meta;
            self.app_ctx = app_ctx;
            self.io = io;

            // Get pointer to app context for handler initialization
            // When H is void, pass undefined; when H is pointer, use directly; when H is value, get pointer from self
            const app_ctx_ptr: *AppCtxType = if (H == void)
                undefined
            else if (@typeInfo(H) == .pointer)
                app_ctx
            else
                &self.app_ctx;

            self.config = config;
            self.handler = try HandlerType.init(self.io, allocator, &self.meta, config, app_ctx_ptr);
            errdefer self.handler.deinit();
            self.server = try httpz.Server(*HandlerType).init(self.io, allocator, mapStruct(httpz.Config, config.server), &self.handler);

            // -- Routing -- //
            var router = try self.server.router(.{});

            // Static assets
            router.get("/assets/*", HandlerType.assets, .{});
            router.get("/*", HandlerType.public, .{});

            // Routes
            inline for (&zx_app.routes) |*route| {
                // Check if this is an API-only route (no page)
                const is_api_only = route.page == null;

                if (!is_api_only) {
                    // Page routes
                    var method_found = false;
                    var get_method_found = false;
                    if (route.page_opts) |pg_opts| {
                        inline for (pg_opts.methods) |method| {
                            method_found = true;
                            switch (method) {
                                .GET => {
                                    get_method_found = true;
                                    router.get(route.path, HandlerType.page, .{ .data = route });
                                },
                                .POST => router.post(route.path, HandlerType.page, .{ .data = route }),
                                .PUT => router.put(route.path, HandlerType.page, .{ .data = route }),
                                .DELETE => router.delete(route.path, HandlerType.page, .{ .data = route }),
                                .PATCH => router.patch(route.path, HandlerType.page, .{ .data = route }),
                                .OPTIONS => router.options(route.path, HandlerType.page, .{ .data = route }),
                                .HEAD => router.head(route.path, HandlerType.page, .{ .data = route }),
                                .CONNECT => router.connect(route.path, HandlerType.page, .{ .data = route }),
                                .TRACE => router.trace(route.path, HandlerType.page, .{ .data = route }),
                                .ALL => router.all(route.path, HandlerType.page, .{ .data = route }),
                            }
                        }
                    }

                    if (!method_found or !get_method_found) {
                        router.get(route.path, HandlerType.page, .{ .data = route });
                    }
                }

                // API routes
                if (route.route) |handlers| {
                    if (handlers.get) |_| router.get(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.post) |_| router.post(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.put) |_| router.put(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.delete) |_| router.delete(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.patch) |_| router.patch(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.head) |_| router.head(route.path, HandlerType.api, .{ .data = route });
                    if (handlers.options) |_| router.options(route.path, HandlerType.api, .{ .data = route });

                    if (handlers.handler) |_| {
                        if (handlers.get == null and is_api_only) router.get(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.post == null) router.post(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.put == null) router.put(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.delete == null) router.delete(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.patch == null) router.patch(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.head == null) router.head(route.path, HandlerType.api, .{ .data = route });
                        if (handlers.options == null) router.options(route.path, HandlerType.api, .{ .data = route });
                    }

                    if (handlers.custom_methods) |custom_methods| {
                        inline for (custom_methods) |custom| {
                            router.method(custom.method, route.path, HandlerType.api, .{ .data = route });
                        }
                    }
                }
            }

            // Introspect the app, this will exit the program in some cases like --introspect flag
            try self.introspect();

            return self;
        }

        pub fn deinit(self: *Self) void {
            const allocator = self.allocator;

            if (self._is_listening) {
                self.server.stop();
                self._is_listening = false;
            }
            self.server.deinit();
            self.handler.deinit();
            allocator.destroy(self);
        }

        pub fn stop(self: *Self) void {
            if (self._is_listening) {
                self.server.stop();
                self._is_listening = false;
            }
        }

        pub fn start(self: *Self) !void {
            if (self._is_listening) return;
            self._is_listening = true;

            // When running under the dev proxy, bind to the inner port on
            // loopback only - the proxy owns the user-facing port.
            if (envVar("ZIEX_INNER_PORT")) |port_str| {
                if (std.fmt.parseInt(u16, port_str, 10)) |inner_port| {
                    setServerAddress(&self.server.config, "127.0.0.1", inner_port);
                } else |_| {}
            }

            self.server.listen() catch |err| {
                self._is_listening = false;

                switch (err) {
                    error.AddressInUse => {
                        const is_dev = self.meta.cli_command == .dev;
                        const port = serverPort(&self.server.config).?;
                        var max_retries: u8 = 10;

                        if (is_dev) while (max_retries > 0) : (max_retries -= 1) {
                            const new_port = port + 1;
                            self.infoWithCrossedOutPort(port);
                            std.debug.print("{s}Port {d} is already in use, {s}trying with port {d}...{s}\n\n", .{ colors.yellow, port, colors.reset_all, new_port, colors.reset_all });
                            std.debug.print("To kill the port, run:\n  {s}kill -9 $(lsof -t -i:{d}){s}\n\n", .{ colors.dim, port, colors.reset_all });
                            setServerAddress(&self.server.config, "127.0.0.1", new_port);

                            var retry_config = self.config;
                            retry_config.server.port = new_port;
                            self.server.deinit();
                            var retry_server = try init(self.io, self.allocator, retry_config, self.app_ctx);
                            defer retry_server.deinit();

                            retry_server.info();
                            return retry_server.start();
                        } else {
                            std.debug.print("{s}Failed to find available port after {d} retries{s}\n", .{ colors.bold, max_retries, colors.reset_all });
                        };

                        if (!is_dev) {
                            self.infoWithCrossedOutPort(port);
                            std.debug.print("{s}Port {d} is already in use{s}\n", .{ colors.red, port, colors.reset_all });
                        }

                        std.debug.print("\nTo kill the port, run:\n  {s}kill -9 $(lsof -t -i:{d}){s}\n\n", .{ colors.dim, port, colors.reset_all });
                    },
                    else => return err,
                }
            };
        }

        /// Print the server info to the console
        /// ZX - v{version} | http://localhost:{port}
        pub fn info(self: *Self) void {
            const display_port: u16 = if (envVar("ZIEX_OUTER_PORT")) |s|
                std.fmt.parseInt(u16, s, 10) catch serverPort(&self.server.config).?
            else
                serverPort(&self.server.config).?;
            std.debug.print("{s}ZX{s} {s}- v{s}{s} | http://localhost:{d}\n", .{ colors.bold, colors.reset_all, colors.dim, Self.version, colors.reset_all, display_port });
        }

        /// Print the info line with the address/port part crossed out
        fn infoWithCrossedOutPort(_: *Self, port: u16) void {
            std.debug.print(
                "{s}{s}{s}ZX{s} {s}- v{s}{s} {s} | {s}http://localhost:{d}{s}\n",
                .{
                    colors.move_up,
                    colors.reset,
                    colors.bold,
                    colors.reset_all,
                    colors.dim,
                    Self.version,
                    colors.reset_all,
                    colors.dim,
                    colors.strikethrough,
                    port,
                    colors.reset_all,
                },
            );
        }

        fn introspect(self: *Self) !void {
            const port = (if (zx_options.server_port != null) zx_options.server_port else serverPort(&self.server.config)) orelse Constant.default_port;
            const address = zx_options.server_address orelse self.config.server.address orelse Constant.default_address;

            if (zx_options.server_rootdir) |rootdir| {
                self.meta.rootdir = rootdir;
            }

            self.meta.cli_command = std.meta.stringToEnum(ServerMeta.CliCommand, zx_options.cli_command) orelse return error.InvalidCliCommand;

            // Overriding or setting default configs
            setServerAddress(&self.server.config, address, port);
            self.server.config.request.max_form_count = self.server.config.request.max_form_count orelse Constant.default_max_form_count;
            self.server.config.request.max_multiform_count = self.server.config.request.max_multiform_count orelse Constant.default_max_multiform_count;

            const introspect_requested = zx_options.introspect or runtimeIntrospectRequested();
            if (introspect_requested) {
                var aw = std.Io.Writer.Allocating.init(self.allocator);
                defer aw.deinit();

                var serilizable_meta = try SerilizableAppMeta.init(self.allocator, &self.meta, self.config.server);
                defer serilizable_meta.deinit(self.allocator);
                try serilizable_meta.serialize(&aw.writer);

                try std.Io.File.stdout().writeStreamingAll(self.io, aw.written());
                try std.Io.File.stdout().writeStreamingAll(self.io, "\n");
                std.process.exit(0);
            }

            // TODO: move this to DevServer.zig
            // Dev-only routes under /.well-known/_zx/
            if (self.meta.cli_command == .dev) {
                var router = try self.server.router(.{});
                var zx_routes = router.group("/.well-known/_zx", .{});

                zx_routes.all("/devtool", HandlerType.devtool, .{});
            }
        }
    };
}

fn runtimeIntrospectRequested() bool {
    return envVar("ZIEX_INTROSPECT") != null;
}

pub const SerilizableAppMeta = struct {
    pub const Route = struct {
        path: []const u8,
        kind: []const u8 = "Page",
        methods: []const []const u8 = &.{},
        has_notfound: bool = false,
        is_dynamic: bool = false,
    };
    pub const Config = struct {
        server: AppConfig.ServerConfig,
    };

    binpath: ?[]const u8 = null,
    rootdir: ?[]const u8 = null,
    routes: []const Route,
    config: SerilizableAppMeta.Config,
    version: []const u8,
    cli_command: ?ServerMeta.CliCommand = null,

    pub fn init(allocator: std.mem.Allocator, meta: *ServerMeta, config: AppConfig.ServerConfig) !SerilizableAppMeta {
        var routes = try allocator.alloc(Route, meta.routes.len);

        for (meta.routes, 0..) |route, i| {
            const is_dynamic = std.mem.indexOf(u8, route.path, ":") != null;
            const kind, const methods = try getRouteKindAndMethods(allocator, route);
            routes[i] = Route{
                .path = try allocator.dupe(u8, route.path),
                .kind = kind,
                .methods = methods,
                .has_notfound = route.notfound != null,
                .is_dynamic = is_dynamic,
            };
        }

        const version = try allocator.dupe(u8, module_config.version);
        const rootdir = if (meta.rootdir) |rd| try allocator.dupe(u8, rd) else null;

        return SerilizableAppMeta{
            .routes = routes,
            .config = SerilizableAppMeta.Config{
                .server = config,
            },
            .version = version,
            .rootdir = rootdir,
            .cli_command = meta.cli_command,
        };
    }

    pub fn deinit(self: *SerilizableAppMeta, allocator: std.mem.Allocator) void {
        for (self.routes) |route| {
            allocator.free(route.path);
            allocator.free(route.methods);
        }
        allocator.free(self.routes);

        allocator.free(self.version);
        if (self.rootdir) |rootdir| allocator.free(rootdir);
        // if (self.binpath) |binpath| allocator.free(binpath);
    }

    pub fn serialize(self: *const SerilizableAppMeta, writer: anytype) !void {
        try std.zon.stringify.serialize(self, .{
            .whitespace = true,
            .emit_default_optional_fields = true,
        }, writer);
    }
    pub fn serializeRoutes(self: SerilizableAppMeta, writer: anytype) !void {
        try zx.util.zxon.serialize(self.routes, writer, .{});
    }

    fn getRouteKindAndMethods(allocator: std.mem.Allocator, route: ServerMeta.Route) !struct { []const u8, []const []const u8 } {
        var methods = std.ArrayList([]const u8).empty;
        defer methods.deinit(allocator);

        if (route.route) |handlers| {
            if (handlers.handler != null) try methods.append(allocator, "ANY");
            if (handlers.get != null) try methods.append(allocator, "GET");
            if (handlers.post != null) try methods.append(allocator, "POST");
            if (handlers.put != null) try methods.append(allocator, "PUT");
            if (handlers.delete != null) try methods.append(allocator, "DELETE");
            if (handlers.patch != null) try methods.append(allocator, "PATCH");
            if (handlers.head != null) try methods.append(allocator, "HEAD");
            if (handlers.options != null) try methods.append(allocator, "OPTIONS");

            if (handlers.custom_methods) |custom_methods| {
                for (custom_methods) |custom| {
                    try methods.append(allocator, custom.method);
                }
            }

            return .{ "Route", try methods.toOwnedSlice(allocator) };
        }

        if (route.page_opts) |page_opts| {
            for (page_opts.methods) |method| {
                try methods.append(allocator, @tagName(method));
            }
        }

        if (methods.items.len == 0) {
            try methods.append(allocator, "GET");
        }

        return .{ "Page", try methods.toOwnedSlice(allocator) };
    }
};

pub const ServerMeta = struct {
    pub const StdInput = struct {
        const Header = struct {
            name: []const u8,
            value: []const u8,
        };
        url: []const u8,
        method: zx.server.Request.Method,
        headers: []const Header,
        body: []const u8,
    };

    /// Route handler function type for API routes
    pub const RouteHandler = *const fn (ctx: zx.RouteContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!void;

    /// Socket message handler function type for WebSocket connections
    /// Called for each message received from the client
    pub const SocketHandler = *const fn (
        socket: zx.Socket,
        message: []const u8,
        message_type: zx.SocketMessageType,
        upgrade_data: ?[]const u8,
        allocator: std.mem.Allocator,
        arena: std.mem.Allocator,
    ) anyerror!void;

    /// Socket open handler function type (optional)
    /// Called once when the WebSocket connection is established
    pub const SocketOpenHandler = *const fn (
        socket: zx.Socket,
        upgrade_data: ?[]const u8,
        allocator: std.mem.Allocator,
        arena: std.mem.Allocator,
    ) anyerror!void;

    /// Socket close handler function type (optional)
    /// Called once when the WebSocket connection is closed
    pub const SocketCloseHandler = *const fn (
        socket: zx.Socket,
        upgrade_data: ?[]const u8,
        allocator: std.mem.Allocator,
    ) void;

    /// Custom method entry for non-standard HTTP methods
    pub const CustomMethod = struct {
        method: []const u8,
        handler: RouteHandler,
    };

    /// Struct containing all HTTP method handlers for an API route
    pub const RouteHandlers = struct {
        handler: ?RouteHandler = null, // Catch-all undefined standard HTTP method
        get: ?RouteHandler = null,
        post: ?RouteHandler = null,
        put: ?RouteHandler = null,
        delete: ?RouteHandler = null,
        patch: ?RouteHandler = null,
        head: ?RouteHandler = null,
        options: ?RouteHandler = null,
        custom_methods: ?[]const CustomMethod = null, // Arbitrary uppercase methods
        socket: ?SocketHandler = null,
        socket_open: ?SocketOpenHandler = null,
        socket_close: ?SocketCloseHandler = null,
    };

    // Standard HTTP methods to exclude from custom detection
    fn isStandardMethod(name: []const u8) bool {
        const standard_methods = [_][]const u8{ "Route", "Socket", "SocketOpen", "SocketClose", "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS" };
        for (standard_methods) |std_method| {
            if (std.mem.eql(u8, name, std_method)) return true;
        }
        return false;
    }

    fn isAllUppercase(name: []const u8) bool {
        if (name.len == 0) return false;
        for (name) |c| if (!std.ascii.isUpper(c)) return false;
        return true;
    }

    /// Comptime function to build RouteHandlers from a route module
    /// Optionally takes a page module to validate for method conflicts
    pub fn route(comptime T: type, comptime PageModule: ?type) RouteHandlers {
        @setEvalBranchQuota(20_000);

        // Validate for method conflicts when page module is provided
        if (PageModule) |P| {
            const page_methods = if (@hasDecl(P, "options") and @hasField(@TypeOf(P.options), "methods"))
                P.options.methods
            else
                &[_]zx.PageOptions.Method{.GET};

            // Check for specific method conflicts
            inline for (page_methods) |method| {
                const method_name = @tagName(method);
                if (@hasDecl(T, method_name)) {
                    @compileError("route.zig cannot define " ++ method_name ++ " handler when page.zx handles it. Remove the method from route.zig or page_opts.methods.");
                }
            }

            // Check for Route() catch-all conflict when page handles non-GET methods
            // Route() would intercept methods that page.zx should handle
            if (@hasDecl(T, "Route")) {
                inline for (page_methods) |method| {
                    if (method != .GET) {
                        @compileError("route.zig cannot define Route() catch-all handler when page.zx handles " ++ @tagName(method) ++ ". Use specific method handlers (POST, PUT, etc.) in route.zig instead.");
                    }
                }
            }
        }

        // Count custom methods first
        comptime var custom_count: usize = 0;
        const decls = @typeInfo(T).@"struct".decls;
        for (decls) |decl| {
            if (!isStandardMethod(decl.name) and isAllUppercase(decl.name)) {
                const field = @field(T, decl.name);
                const FieldType = @TypeOf(field);
                if (@typeInfo(FieldType) == .@"fn") {
                    custom_count += 1;
                }
            }
        }

        // Build custom methods array as const
        const custom_methods = comptime blk: {
            var methods: [custom_count]CustomMethod = undefined;
            var idx: usize = 0;
            for (decls) |decl| {
                if (!isStandardMethod(decl.name) and isAllUppercase(decl.name)) {
                    const field = @field(T, decl.name);
                    const FieldType = @TypeOf(field);
                    if (@typeInfo(FieldType) == .@"fn") {
                        methods[idx] = .{
                            .method = decl.name,
                            .handler = wrapRoute(field),
                        };
                        idx += 1;
                    }
                }
            }
            break :blk methods;
        };

        return .{
            .handler = if (@hasDecl(T, "Route")) wrapRoute(T.Route) else null,
            .get = if (@hasDecl(T, "GET")) wrapRoute(T.GET) else null,
            .post = if (@hasDecl(T, "POST")) wrapRoute(T.POST) else null,
            .put = if (@hasDecl(T, "PUT")) wrapRoute(T.PUT) else null,
            .delete = if (@hasDecl(T, "DELETE")) wrapRoute(T.DELETE) else null,
            .patch = if (@hasDecl(T, "PATCH")) wrapRoute(T.PATCH) else null,
            .head = if (@hasDecl(T, "HEAD")) wrapRoute(T.HEAD) else null,
            .options = if (@hasDecl(T, "OPTIONS")) wrapRoute(T.OPTIONS) else null,
            .custom_methods = if (custom_count > 0) &custom_methods else null,
            .socket = if (@hasDecl(T, "Socket")) wrapSocket(T.Socket) else null,
            .socket_open = if (@hasDecl(T, "SocketOpen")) wrapSocketOpen(T.SocketOpen) else null,
            .socket_close = if (@hasDecl(T, "SocketClose")) wrapSocketClose(T.SocketClose) else null,
        };
    }

    /// Wrapper to allow socket message handlers to return void or !void
    /// Supports both SocketContext (simple) and SocketCtx(T) (with custom data)
    fn wrapSocket(comptime socketFn: anytype) SocketHandler {
        const FnInfo = @typeInfo(@TypeOf(socketFn)).@"fn";
        const R = FnInfo.return_type.?;
        const CtxType = FnInfo.params[0].type.?;
        const DataType = @TypeOf(@as(CtxType, undefined).data);

        return struct {
            fn wrapper(socket: zx.Socket, message: []const u8, message_type: zx.SocketMessageType, upgrade_data: ?[]const u8, allocator: std.mem.Allocator, arena: std.mem.Allocator) anyerror!void {
                const data: DataType = if (upgrade_data) |bytes|
                    std.mem.bytesToValue(DataType, bytes[0..@sizeOf(DataType)])
                else
                    std.mem.zeroes(DataType);

                const ctx = CtxType{
                    .socket = socket,
                    .message = message,
                    .message_type = message_type,
                    .data = data,
                    .allocator = allocator,
                    .arena = arena,
                };
                if (R == void) {
                    socketFn(ctx);
                } else {
                    try socketFn(ctx);
                }
            }
        }.wrapper;
    }

    /// Wrapper for SocketOpen handlers
    fn wrapSocketOpen(comptime socketOpenFn: anytype) SocketOpenHandler {
        const FnInfo = @typeInfo(@TypeOf(socketOpenFn)).@"fn";
        const R = FnInfo.return_type.?;
        const CtxType = FnInfo.params[0].type.?;
        const DataType = @TypeOf(@as(CtxType, undefined).data);

        return struct {
            fn wrapper(socket: zx.Socket, upgrade_data: ?[]const u8, allocator: std.mem.Allocator, arena: std.mem.Allocator) anyerror!void {
                const data: DataType = if (upgrade_data) |bytes|
                    std.mem.bytesToValue(DataType, bytes[0..@sizeOf(DataType)])
                else
                    std.mem.zeroes(DataType);

                const ctx = CtxType{
                    .socket = socket,
                    .data = data,
                    .allocator = allocator,
                    .arena = arena,
                };
                if (R == void) {
                    socketOpenFn(ctx);
                } else {
                    try socketOpenFn(ctx);
                }
            }
        }.wrapper;
    }

    /// Wrapper for SocketClose handlers
    fn wrapSocketClose(comptime socketCloseFn: anytype) SocketCloseHandler {
        const CtxType = @typeInfo(@TypeOf(socketCloseFn)).@"fn".params[0].type.?;
        const DataType = @TypeOf(@as(CtxType, undefined).data);

        return struct {
            fn wrapper(socket: zx.Socket, upgrade_data: ?[]const u8, allocator: std.mem.Allocator) void {
                const data: DataType = if (upgrade_data) |bytes|
                    std.mem.bytesToValue(DataType, bytes[0..@sizeOf(DataType)])
                else
                    std.mem.zeroes(DataType);

                const ctx = CtxType{
                    .socket = socket,
                    .data = data,
                    .allocator = allocator,
                    .arena = allocator,
                };
                socketCloseFn(ctx);
            }
        }.wrapper;
    }

    fn injectApp(comptime AppType: type, app_ptr: ?*const anyopaque) AppType {
        if (AppType == void) return {};

        if (app_ptr == null) {
            if (comptime @typeInfo(AppType) == .optional) return null;
            @panic("Missing app context for non-optional app parameter");
        }
        const ptr = app_ptr.?;

        if (@typeInfo(AppType) == .pointer) {
            const addr = @intFromPtr(ptr);
            const typed: AppType = @ptrFromInt(addr);
            return typed;
        } else {
            const loose: *align(1) const AppType = @ptrCast(ptr);
            return loose.*;
        }
    }

    fn injectState(comptime StateType: type, state_ptr: ?*const anyopaque) StateType {
        if (StateType == void) return {};

        if (state_ptr == null) {
            if (comptime @typeInfo(StateType) == .optional) return null;
            if (comptime @typeInfo(StateType) == .pointer) @panic("Missing proxy state for non-optional state parameter");
            return std.mem.zeroes(StateType);
        }
        const ptr = state_ptr.?;

        if (@typeInfo(StateType) == .pointer) {
            const addr = @intFromPtr(ptr);
            const typed: StateType = @ptrFromInt(addr);
            return typed;
        } else {
            const loose: *align(1) const StateType = @ptrCast(ptr);
            return loose.*;
        }
    }

    /// Wrapper to allow routes to return void or !void.
    /// Route function shape: `(ctx: zx.RouteContext, app?: App, state?: State)` - positional.
    fn wrapRoute(comptime routeFn: anytype) RouteHandler {
        const FnInfo = @typeInfo(@TypeOf(routeFn)).@"fn";
        const R = FnInfo.return_type.?;
        const n_params = FnInfo.params.len;

        return struct {
            fn wrapper(ctx: zx.RouteContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!void {
                if (n_params == 1) {
                    if (R == void) routeFn(ctx) else try routeFn(ctx);
                    return;
                }
                if (n_params == 2) {
                    const app = injectApp(FnInfo.params[1].type.?, app_ptr);
                    if (R == void) routeFn(ctx, app) else try routeFn(ctx, app);
                    return;
                }
                if (n_params == 3) {
                    const app = injectApp(FnInfo.params[1].type.?, app_ptr);
                    const state = injectState(FnInfo.params[2].type.?, state_ptr);
                    if (R == void) routeFn(ctx, app, state) else try routeFn(ctx, app, state);
                    return;
                }
                @compileError("Route function must have 1-3 parameters: (ctx, app?, state?)");
            }
        }.wrapper;
    }

    /// Proxy handler function type - called before page/route handlers
    pub const ProxyHandler = *const fn (ctx: *zx.ProxyContext) anyerror!void;

    /// Wrapper to allow proxy handlers to return void or !void
    fn wrapProxy(comptime proxyFn: anytype) ProxyHandler {
        const R = @typeInfo(@TypeOf(proxyFn)).@"fn".return_type.?;
        return struct {
            fn wrapper(ctx: *zx.ProxyContext) anyerror!void {
                if (R == void) {
                    proxyFn(ctx);
                } else {
                    try proxyFn(ctx);
                }
            }
        }.wrapper;
    }

    /// Comptime function to extract global Proxy handler from a proxy module (cascades to child routes)
    pub fn proxy(comptime T: type) ?ProxyHandler {
        if (@hasDecl(T, "Proxy")) {
            return wrapProxy(T.Proxy);
        }
        return null;
    }

    /// Comptime function to extract PageProxy handler from a proxy module (does NOT cascade)
    pub fn pageProxy(comptime T: type) ?ProxyHandler {
        if (@hasDecl(T, "PageProxy")) {
            return wrapProxy(T.PageProxy);
        }
        return null;
    }

    /// Comptime function to extract RouteProxy handler from a proxy module (does NOT cascade)
    pub fn routeProxy(comptime T: type) ?ProxyHandler {
        if (@hasDecl(T, "RouteProxy")) {
            return wrapProxy(T.RouteProxy);
        }
        return null;
    }

    /// Page handler function type
    pub const PageHandler = *const fn (ctx: zx.PageContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!Component;

    /// Layout handler function type
    pub const LayoutHandler = *const fn (ctx: zx.LayoutContext, component: Component, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) Component;

    /// Comptime function to wrap a page module's Page function.
    /// The app context and state are read from type-erased pointers in ctx and cast to the appropriate types.
    pub fn page(comptime T: type) PageHandler {
        const is_md = @hasDecl(T, "_zx_md");
        const pageFn = if (is_md) T._zx_md else T.Page;

        const FnType = @TypeOf(pageFn);
        const fn_info = @typeInfo(FnType).@"fn";
        const R = fn_info.return_type.?;
        const n_params = fn_info.params.len;

        return struct {
            fn wrapper(ctx: zx.PageContext, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) anyerror!Component {
                if (is_md) {
                    // TODO: figure out a better design for page.mdzx file, we need a better way to use PageContext in md file without magic
                    const CtxType = fn_info.params[0].type.?;
                    const CCtxType = @typeInfo(CtxType).pointer.child;
                    const allocator = ctx.arena;
                    const cctx = (allocator.create(CCtxType) catch @panic("OOM"));
                    cctx.allocator = allocator;
                    if (@hasDecl(T, "page_ctx")) T.page_ctx = ctx;
                    return pageFn(cctx);
                }

                if (n_params == 1) {
                    if (R == Component) return pageFn(ctx) else return try pageFn(ctx);
                }
                if (n_params == 2) {
                    const app = injectApp(fn_info.params[1].type.?, app_ptr);
                    if (R == Component) return pageFn(ctx, app) else return try pageFn(ctx, app);
                }
                if (n_params == 3) {
                    const app = injectApp(fn_info.params[1].type.?, app_ptr);
                    const state = injectState(fn_info.params[2].type.?, state_ptr);
                    if (R == Component) return pageFn(ctx, app, state) else return try pageFn(ctx, app, state);
                }
                @compileError("Page function must have 1-3 parameters: (ctx, app?, state?)");
            }
        }.wrapper;
    }

    /// Comptime function to wrap a layout module's Layout function.
    /// Layout signature (positional): `(ctx: zx.LayoutContext, children: Component, app?, state?)`
    pub fn layout(comptime T: type) LayoutHandler {
        const layoutFn = T.Layout;
        const FnType = @TypeOf(layoutFn);
        const fn_info = @typeInfo(FnType).@"fn";
        const n_params = fn_info.params.len;

        return struct {
            fn wrapper(ctx: zx.LayoutContext, component: Component, app_ptr: ?*const anyopaque, state_ptr: ?*const anyopaque) Component {
                if (n_params == 2) {
                    return layoutFn(ctx, component);
                }
                if (n_params == 3) {
                    const app = injectApp(fn_info.params[2].type.?, app_ptr);
                    return layoutFn(ctx, component, app);
                }
                if (n_params == 4) {
                    const app = injectApp(fn_info.params[2].type.?, app_ptr);
                    const state = injectState(fn_info.params[3].type.?, state_ptr);
                    return layoutFn(ctx, component, app, state);
                }
                @compileError("Layout function must have 2-4 parameters: (ctx, children, app?, state?)");
            }
        }.wrapper;
    }

    pub const Route = struct {
        path: []const u8,
        page: ?PageHandler = null,
        layout: ?LayoutHandler = null,
        notfound: ?*const fn (ctx: zx.NotFoundContext) Component = null,
        @"error": ?*const fn (ctx: zx.ErrorContext) Component = null,
        page_opts: ?zx.PageOptions = null,
        layout_opts: ?zx.LayoutOptions = null,
        notfound_opts: ?zx.NotFoundOptions = null,
        error_opts: ?zx.ErrorOptions = null,
        route: ?RouteHandlers = null,
        route_opts: ?zx.RouteOptions = null,
        proxy: ?ProxyHandler = null,
        page_proxy: ?ProxyHandler = null,
        route_proxy: ?ProxyHandler = null,
    };
    pub const CliCommand = enum { dev, serve, @"export", @"--" };

    routes: []const Route,
    rootdir: ?[]const u8,
    base_path: ?[]const u8 = null,
    cli_command: ?CliCommand = null,
};

/// Comptime-map any struct into a target struct type by matching field names.
/// Used to convert our AppConfig.ServerConfig into httpz.Config without manually
/// listing each field. Only fields present in both are copied; nested structs
/// recurse. This keeps AppConfig decoupled from httpz (so wasm/wasi builds work).
pub fn mapStruct(comptime T: type, src: anytype) T {
    var out: T = .{};
    const S = @TypeOf(src);
    inline for (@typeInfo(T).@"struct".fields) |f| {
        if (comptime T == httpz.Config and std.mem.eql(u8, f.name, "address")) {
            // handled after the loop because our app config stores host/port
            // separately while httpz expects a tagged union
        } else if (@hasField(S, f.name)) {
            const sv = @field(src, f.name);
            switch (@typeInfo(f.type)) {
                .@"struct" => @field(out, f.name) = mapStruct(f.type, sv),
                else => @field(out, f.name) = sv,
            }
        }
    }
    if (comptime T == httpz.Config) out.address = mapServerAddress(src);
    return out;
}

fn mapServerAddress(src: AppConfig.ServerConfig) httpz.Config.Address {
    if (src.unix_path) |unix_path| return .{ .unix = unix_path };
    const port = src.port orelse Constant.default_port;
    const address = src.address orelse Constant.default_address;

    if (std.mem.eql(u8, address, "localhost")) return httpz.Config.Address.localhost(port);
    if (std.mem.eql(u8, address, "0.0.0.0")) return httpz.Config.Address.all(port);

    return .{ .ip = std.Io.net.IpAddress.parse(address, port) catch .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } } };
}

fn serverPort(config: *const httpz.Config) ?u16 {
    return switch (config.address) {
        .ip => |ip| ip.getPort(),
        .unix => null,
    };
}

fn setServerAddress(config: *httpz.Config, address: []const u8, port: u16) void {
    config.address = if (std.mem.eql(u8, address, "localhost"))
        httpz.Config.Address.localhost(port)
    else if (std.mem.eql(u8, address, "0.0.0.0"))
        httpz.Config.Address.all(port)
    else
        .{ .ip = std.Io.net.IpAddress.parse(address, port) catch .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } } };
}

// TODO: read from passed through env from root init
fn envVar(name_z: [*:0]const u8) ?[]const u8 {
    const value = std.c.getenv(name_z) orelse return null;
    return std.mem.span(value);
}

const std = @import("std");
const builtin = @import("builtin");
const httpz = @import("httpz");
const cachez = zx.Cache.cachez;
const zx = @import("../../root.zig");
const zx_app = @import("app");
const module_config = @import("zx_info");
const zx_options = @import("zx_options");
const Constant = @import("../../constant.zig");
const Handler = @import("handler.zig").Handler;
const AppConfig = @import("../../AppConfig.zig");
const CacheConfig = AppConfig.CacheConfig;

const Allocator = std.mem.Allocator;
const Component = zx.Component;
const log = std.log.scoped(.app);

const colors = struct {
    const move_up = "\x1b[1A";
    const reset = "\r";
    const bold = "\x1b[1m";
    const dim = "\x1b[2m";
    const strikethrough = "\x1b[9m";
    const reset_all = "\x1b[0m";
    const yellow = "\x1b[33m";
    const red = "\x1b[31m";
    const blink = "\x1b[5m";
};
