    const std = @import("std");
    const zap = @import("zap.zig");
    const auth = @import("http_auth.zig");

    const Endpoint = @This();

    // zap types

    const Request = zap.Request;
    const ListenerSettings = zap.HttpListenerSettings;
    const HttpListener = zap.HttpListener;

    /// Type of the request function callbacks.
    pub const RequestFn = *const fn (self: *Endpoint, r: Request) void;

    /// Settings to initialize an Endpoint
    pub const Settings = struct {
        /// path / slug of the endpoint
        path: []const u8,
        /// callback to GET request handler
        get: ?RequestFn = null,
        /// callback to POST request handler
        post: ?RequestFn = null,
        /// callback to PUT request handler
        put: ?RequestFn = null,
        /// callback to DELETE request handler
        delete: ?RequestFn = null,
        /// callback to PATCH request handler
        patch: ?RequestFn = null,
        /// callback to OPTIONS request handler
        options: ?RequestFn = null,
        /// Only applicable to Authenticating Endpoint: handler for unauthorized requests
        unauthorized: ?RequestFn = null,
    };

    settings: Settings,

    /// Initialize the endpoint.
    /// Set only the callbacks you need. Requests of HTTP methods without a
    /// provided callback will be ignored.
    pub fn init(s: Settings) Endpoint {
        return .{
            .settings = .{
                .path = s.path,
                .get = s.get orelse &nop,
                .post = s.post orelse &nop,
                .put = s.put orelse &nop,
                .delete = s.delete orelse &nop,
                .patch = s.patch orelse &nop,
                .options = s.options orelse &nop,
                .unauthorized = s.unauthorized orelse &nop,
            },
        };
    }

    // no operation. Dummy handler function for ignoring unset request types.

    fn nop(self: *Endpoint, r: Request) void {
        _ = self;
        _ = r;
    }

    /// The global request handler for this Endpoint, called by the listener.
    pub fn onRequest(self: *Endpoint, r: zap.Request) void {
        switch (r.methodAsEnum()) {
            .GET => self.settings.get.?(self, r),
            .POST => self.settings.post.?(self, r),
            .PUT => self.settings.put.?(self, r),
            .DELETE => self.settings.delete.?(self, r),
            .PATCH => self.settings.patch.?(self, r),
            .OPTIONS => self.settings.options.?(self, r),
            else => return,
        }
    }

    /// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint
    /// is available via the `endpoint()` function.
    pub fn Authenticating(comptime Authenticator: type) type {
        return struct {
            authenticator: *Authenticator,
            ep: *Endpoint,
            auth_endpoint: Endpoint,
            const Self = @This();

            /// Init the authenticating endpoint. Pass in a pointer to the endpoint
            /// you want to wrap, and the Authenticator that takes care of authenticating
            /// requests.
            pub fn init(e: *Endpoint, authenticator: *Authenticator) Self {
                return .{
                    .authenticator = authenticator,
                    .ep = e,
                    .auth_endpoint = Endpoint.init(.{
                        .path = e.settings.path,
                        // we override only the set ones. the other ones

                        // are set to null anyway -> will be nopped out

                        .get = if (e.settings.get != null) get else null,
                        .post = if (e.settings.post != null) post else null,
                        .put = if (e.settings.put != null) put else null,
                        .delete = if (e.settings.delete != null) delete else null,
                        .patch = if (e.settings.patch != null) patch else null,
                        .options = if (e.settings.options != null) options else null,
                        .unauthorized = e.settings.unauthorized,
                    }),
                };
            }

            /// Get the auth endpoint struct of type Endpoint so it can be stored in the listener.
            /// When the listener calls the auth_endpoint, onRequest will have
            /// access to all of this via fieldParentPtr
            pub fn endpoint(self: *Self) *Endpoint {
                return &self.auth_endpoint;
            }

            /// GET: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates GET requests using the Authenticator.
            pub fn get(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.get.?(authEp.ep, r),
                    .Handled => {},
                }
            }

            /// POST: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates POST requests using the Authenticator.
            pub fn post(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.post.?(authEp.ep, r),
                    .Handled => {},
                }
            }

            /// PUT: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates PUT requests using the Authenticator.
            pub fn put(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.put.?(authEp.ep, r),
                    .Handled => {},
                }
            }

            /// DELETE: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates DELETE requests using the Authenticator.
            pub fn delete(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.delete.?(authEp.ep, r),
                    .Handled => {},
                }
            }

            /// PATCH: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates PATCH requests using the Authenticator.
            pub fn patch(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.patch.?(authEp.ep, r),
                    .Handled => {},
                }
            }

            /// OPTIONS: here, the auth_endpoint will be passed in as endpoint.
            /// Authenticates OPTIONS requests using the Authenticator.
            pub fn options(e: *Endpoint, r: zap.Request) void {
                const authEp: *Self = @fieldParentPtr(Self, "auth_endpoint", e);
                switch (authEp.authenticator.authenticateRequest(&r)) {
                    .AuthFailed => {
                        if (e.settings.unauthorized) |unauthorized| {
                            unauthorized(authEp.ep, r);
                            return;
                        } else {
                            r.setStatus(.unauthorized);
                            r.sendBody("UNAUTHORIZED") catch return;
                            return;
                        }
                    },
                    .AuthOK => authEp.ep.settings.put.?(authEp.ep, r),
                    .Handled => {},
                }
            }
        };
    }

    pub const EndpointListenerError = error{
        /// Since we use .startsWith to check for matching paths, you cannot use
        /// endpoint paths that overlap at the beginning. --> When trying to register
        /// an endpoint whose path would shadow an already registered one, you will
        /// receive this error.
        EndpointPathShadowError,
    };

    /// The listener with endpoint support
    ///
    /// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!!
    pub const Listener = struct {
        listener: HttpListener,
        allocator: std.mem.Allocator,

        const Self = @This();

        /// Internal static struct of member endpoints
        var endpoints: std.ArrayList(*Endpoint) = undefined;

        /// Internal, static request handler callback. Will be set to the optional,
        /// user-defined request callback that only gets called if no endpoints match
        /// a request.
        var on_request: ?zap.HttpRequestFn = null;

        /// Initialize a new endpoint listener. Note, if you pass an `on_request`
        /// callback in the provided ListenerSettings, this request callback will be
        /// called every time a request arrives that no endpoint matches.
        pub fn init(a: std.mem.Allocator, l: ListenerSettings) Self {
            endpoints = std.ArrayList(*Endpoint).init(a);

            // take copy of listener settings before modifying the callback field

            var ls = l;

            // override the settings with our internal, actul callback function

            // so that "we" will be called on request

            ls.on_request = Listener.onRequest;

            // store the settings-provided request callback for later use

            on_request = l.on_request;
            return .{
                .listener = HttpListener.init(ls),
                .allocator = a,
            };
        }

        /// De-init the listener and free its resources.
        /// Registered endpoints will not be de-initialized automatically; just removed
        /// from the internal map.
        pub fn deinit(self: *Self) void {
            _ = self;
            endpoints.deinit();
        }

        /// Call this to start listening. After this, no more endpoints can be
        /// registered.
        pub fn listen(self: *Self) !void {
            try self.listener.listen();
        }

        /// Register an endpoint with this listener.
        /// NOTE: endpoint paths are matched with startsWith -> so use endpoints with distinctly starting names!!
        /// If you try to register an endpoint whose path would shadow an already registered one, you will
        /// receive an EndpointPathShadowError.
        pub fn register(self: *Self, e: *Endpoint) !void {
            _ = self;
            for (endpoints.items) |other| {
                if (std.mem.startsWith(
                    u8,
                    other.settings.path,
                    e.settings.path,
                ) or std.mem.startsWith(
                    u8,
                    e.settings.path,
                    other.settings.path,
                )) {
                    return EndpointListenerError.EndpointPathShadowError;
                }
            }
            try endpoints.append(e);
        }

        fn onRequest(r: Request) void {
            if (r.path) |p| {
                for (endpoints.items) |e| {
                    if (std.mem.startsWith(u8, p, e.settings.path)) {
                        e.onRequest(r);
                        return;
                    }
                }
            }
            // if set, call the user-provided default callback

            if (on_request) |foo| {
                foo(r);
            }
        }
    };