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

    /// Authentication Scheme enum: Basic or Bearer.
    pub const AuthScheme = enum {
        Basic,
        Bearer,

        pub fn str(self: AuthScheme) []const u8 {
            return switch (self) {
                .Basic => "Basic ",
                .Bearer => "Bearer ",
            };
        }

        pub fn headerFieldStrFio(self: AuthScheme) []const u8 {
            return switch (self) {
                .Basic => "authentication",
                .Bearer => "authorization",
            };
        }

        pub fn headerFieldStrHeader(self: AuthScheme) [:0]const u8 {
            return switch (self) {
                .Basic => "Authentication",
                .Bearer => "Authorization",
            };
        }
    };

    /// Used internally: check for presence of the requested auth header.
    pub fn checkAuthHeader(scheme: AuthScheme, auth_header: []const u8) bool {
        return switch (scheme) {
            .Basic => |b| std.mem.startsWith(u8, auth_header, b.str()) and auth_header.len > b.str().len,
            .Bearer => |b| std.mem.startsWith(u8, auth_header, b.str()) and auth_header.len > b.str().len,
        };
    }

    /// Used internally: return the requested auth header.
    pub fn extractAuthHeader(scheme: AuthScheme, r: *const zap.Request) ?[]const u8 {
        return switch (scheme) {
            .Basic => |b| r.getHeader(b.headerFieldStrFio()),
            .Bearer => |b| r.getHeader(b.headerFieldStrFio()),
        };
    }

    /// Decoding Strategy for Basic Authentication
    const BasicAuthStrategy = enum {
        /// decode into user and pass, then check pass
        UserPass,
        /// just look up the encoded user:pass token
        Token68,
    };

    /// Authentication result
    pub const AuthResult = enum {
        /// authentication / authorization was successful
        AuthOK,
        /// authentication / authorization failed
        AuthFailed,
        /// The authenticator handled the request that didn't pass authentication /
        /// authorization.
        /// This is used to implement authenticators that redirect to a login
        /// page. An Authenticating endpoint will not do the default, which is trying
        /// to call the `unauthorized` callback if one exists orelse ignore the request.
        Handled,
    };

    /// HTTP Basic Authentication RFC 7617.
    /// "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
    /// user-pass strings: "$username:$password" -> base64
    ///
    /// Notes:
    ///   - we only look at the Authentication header
    ///   - we ignore the required realm parameter
    ///   - we ignore the optional charset parameter
    ///
    /// Errors:
    /// WWW-Authenticate: Basic realm="this"
    ///
    /// Lookup : any kind of map that implements get([]const u8) -> []const u8
    pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
        return struct {
            allocator: std.mem.Allocator,
            realm: ?[]const u8,
            lookup: *Lookup,

            const Self = @This();

            /// Creates a BasicAuth. `lookup` must implement `.get([]const u8) -> []const u8`
            /// different implementations can
            ///   - either decode, lookup and compare passwords
            ///   - or just check for existence of the base64-encoded user:pass combination
            /// if realm is provided (not null), a copy of it is taken -> call deinit() to clean up
            pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self {
                return .{
                    .allocator = allocator,
                    .lookup = lookup,
                    .realm = if (realm) |the_realm| try allocator.dupe(u8, the_realm) else null,
                };
            }

            /// Deinit the authenticator.
            pub fn deinit(self: *Self) void {
                if (self.realm) |the_realm| {
                    self.allocator.free(the_realm);
                }
            }

            /// Use this to decode the auth_header into user:pass, lookup pass in lookup.
            /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
            pub fn authenticateUserPass(self: *Self, auth_header: []const u8) AuthResult {
                zap.debug("AuthenticateUserPass\n", .{});
                const encoded = auth_header[AuthScheme.Basic.str().len..];
                const decoder = std.base64.standard.Decoder;
                var buffer: [0x100]u8 = undefined;
                if (decoder.calcSizeForSlice(encoded)) |decoded_size| {
                    if (decoded_size >= buffer.len) {
                        zap.debug(
                            "ERROR: UserPassAuth: decoded_size {d} >= buffer.len {d}\n",
                            .{ decoded_size, buffer.len },
                        );
                        return .AuthFailed;
                    }
                    const decoded = buffer[0..decoded_size];
                    decoder.decode(decoded, encoded) catch |err| {
                        zap.debug(
                            "ERROR: UserPassAuth: unable to decode `{s}`: {any}\n",
                            .{ encoded, err },
                        );
                        return .AuthFailed;
                    };
                    // we have decoded

                    // we can split

                    var it = std.mem.split(u8, decoded, ":");
                    const user = it.next();
                    const pass = it.next();
                    if (user == null or pass == null) {
                        zap.debug(
                            "ERROR: UserPassAuth: user {any} or pass {any} is null\n",
                            .{ user, pass },
                        );
                        return .AuthFailed;
                    }
                    // now, do the lookup

                    const actual_pw = self.lookup.*.get(user.?);
                    if (actual_pw) |pw| {
                        const ret = std.mem.eql(u8, pass.?, pw);
                        zap.debug(
                            "INFO: UserPassAuth for user `{s}`: `{s}` == pass `{s}` = {}\n",
                            .{ user.?, pw, pass.?, ret },
                        );
                        return if (ret) .AuthOK else .AuthFailed;
                    } else {
                        zap.debug(
                            "ERROR: UserPassAuth: user `{s}` not found in map of size {d}!\n",
                            .{ user.?, self.lookup.*.count() },
                        );
                        return .AuthFailed;
                    }
                } else |err| {
                    // can't calc slice size --> fallthrough to return false

                    zap.debug(
                        "ERROR: UserPassAuth: cannot calc slize size for encoded `{s}`: {any} \n",
                        .{ encoded, err },
                    );
                    return .AuthFailed;
                }
                zap.debug("UNREACHABLE\n", .{});
                return .AuthFailed;
            }

            /// Use this to just look up if the base64-encoded auth_header exists in lookup.
            /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
            pub fn authenticateToken68(self: *Self, auth_header: []const u8) AuthResult {
                const token = auth_header[AuthScheme.Basic.str().len..];
                return if (self.lookup.*.contains(token)) .AuthOK else .AuthFailed;
            }

            /// dispatch based on kind (.UserPass / .Token689) and try to authenticate based on the header.
            /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
            pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
                zap.debug("AUTHENTICATE\n", .{});
                switch (kind) {
                    .UserPass => return self.authenticateUserPass(auth_header),
                    .Token68 => return self.authenticateToken68(auth_header),
                }
            }

            /// The zap authentication request handler.
            ///
            /// Tries to extract the authentication header and perform the authentication.
            /// If no authentication header is found, an authorization header is tried.
            pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
                zap.debug("AUTHENTICATE REQUEST\n", .{});
                if (extractAuthHeader(.Basic, r)) |auth_header| {
                    zap.debug("Authentication Header found!\n", .{});
                    return self.authenticate(auth_header);
                } else {
                    // try with .Authorization

                    if (extractAuthHeader(.Bearer, r)) |auth_header| {
                        zap.debug("Authorization Header found!\n", .{});
                        return self.authenticate(auth_header);
                    }
                }
                zap.debug("NO fitting Auth Header found!\n", .{});
                return .AuthFailed;
            }
        };
    }

    /// HTTP bearer authentication for a single token
    /// RFC 6750
    /// "Authentication: Bearer TOKEN"
    /// `Bearer` is case-sensitive
    ///   - we don't support form-encoded `access_token` body parameter
    ///   - we don't support URI query parameter `access_token`
    ///
    /// Errors:
    /// HTTP/1.1 401 Unauthorized
    /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..."
    pub const BearerSingle = struct {
        allocator: std.mem.Allocator,
        token: []const u8,
        realm: ?[]const u8,

        const Self = @This();

        /// Creates a Single-Token Bearer Authenticator.
        /// Takes a copy of the token.
        /// If realm is provided (not null), a copy is taken call deinit() to clean up.
        pub fn init(allocator: std.mem.Allocator, token: []const u8, realm: ?[]const u8) !Self {
            return .{
                .allocator = allocator,
                .token = try allocator.dupe(u8, token),
                .realm = if (realm) |the_realm| try allocator.dupe(u8, the_realm) else null,
            };
        }

        /// Try to authenticate based on the header.
        /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
        pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
            if (checkAuthHeader(.Bearer, auth_header) == false) {
                return .AuthFailed;
            }
            const token = auth_header[AuthScheme.Bearer.str().len..];
            return if (std.mem.eql(u8, token, self.token)) .AuthOK else .AuthFailed;
        }

        /// The zap authentication request handler.
        ///
        /// Tries to extract the authentication header and perform the authentication.
        pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
            if (extractAuthHeader(.Bearer, r)) |auth_header| {
                return self.authenticate(auth_header);
            }
            return .AuthFailed;
        }

        /// Deinits the authenticator.
        pub fn deinit(self: *Self) void {
            if (self.realm) |the_realm| {
                self.allocator.free(the_realm);
            }
            self.allocator.free(self.token);
        }
    };

    /// HTTP bearer authentication for multiple tokens
    /// RFC 6750
    /// "Authentication: Bearer TOKEN"
    /// `Bearer` is case-sensitive
    ///   - we don't support form-encoded `access_token` body parameter
    ///   - we don't support URI query parameter `access_token`
    ///
    /// Errors:
    /// HTTP/1.1 401 Unauthorized
    /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..."
    pub fn BearerMulti(comptime Lookup: type) type {
        return struct {
            allocator: std.mem.Allocator,
            lookup: *Lookup,
            realm: ?[]const u8,

            const Self = @This();

            /// Creates a Multi Token Bearer Authenticator. `lookup` must implement
            /// `.get([]const u8) -> []const u8` to look up tokens.
            /// If realm is provided (not null), a copy of it is taken -> call deinit() to clean up.
            pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self {
                return .{
                    .allocator = allocator,
                    .lookup = lookup,
                    .realm = if (realm) |the_realm| try allocator.dupe(u8, the_realm) else null,
                };
            }

            /// Deinit the authenticator. Only required if a realm was provided at
            /// init() time.
            pub fn deinit(self: *Self) void {
                if (self.realm) |the_realm| {
                    self.allocator.free(the_realm);
                }
            }

            /// Try to authenticate based on the header.
            /// Note: usually, you don't want to use this; you'd go for `authenticateRequest()`.
            pub fn authenticate(self: *Self, auth_header: []const u8) AuthResult {
                if (checkAuthHeader(.Bearer, auth_header) == false) {
                    return .AuthFailed;
                }
                const token = auth_header[AuthScheme.Bearer.str().len..];
                return if (self.lookup.*.contains(token)) .AuthOK else .AuthFailed;
            }

            /// The zap authentication request handler.
            ///
            /// Tries to extract the authentication header and perform the authentication.
            pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
                if (extractAuthHeader(.Bearer, r)) |auth_header| {
                    return self.authenticate(auth_header);
                }
                return .AuthFailed;
            }
        };
    }

    /// Settings to initialize a UserPassSession authenticator.
    pub const UserPassSessionArgs = struct {
        /// username body parameter
        usernameParam: []const u8,
        /// password body parameter
        passwordParam: []const u8,
        /// redirect to this page if auth fails
        loginPage: []const u8,
        /// name of the auth cookie
        cookieName: []const u8,
        /// cookie max age in seconds; 0 -> session cookie
        cookieMaxAge: u8 = 0,
        /// redirect status code, defaults to 302 found
        redirectCode: zap.StatusCode = .found,
    };

    /// UserPassSession supports the following use case:
    ///
    /// - checks every request: is it going to the login page? -> let the request through.
    /// - else:
    ///   - checks every request for a session token in a cookie
    ///   - if there is no token, it checks for correct username and password body params
    ///     - if username and password are present and correct, it will create a session token,
    ///       create a response cookie containing the token, and carry on with the request
    ///     - else it will redirect to the login page
    ///   - if the session token is present and correct: it will let the request through
    ///   - else: it will redirect to the login page
    ///
    /// Please note the implications of this simple approach: IF YOU REUSE "username"
    /// and "password" body params for anything else in your application, then the
    /// mechanisms described above will still kick in. For that reason: please know what
    /// you're doing.
    ///
    /// See UserPassSessionArgs:
    /// - username & password param names can be defined by you
    /// - session cookie name and max-age can be defined by you
    /// - login page and redirect code (.302) can be defined by you
    ///
    /// Comptime Parameters:
    ///
    /// - `Lookup` must implement .get([]const u8) -> []const u8 for user password retrieval
    /// - `lockedPwLookups` : if true, accessing the provided Lookup instance will be protected
    ///    by a Mutex. You can access the mutex yourself via the `passwordLookupLock`.
    ///
    /// Note: In order to be quick, you can set lockedTokenLookups to false.
    ///       -> we generate it on init() and leave it static
    ///       -> there is no way to 100% log out apart from re-starting the server
    ///       -> because: we send a cookie to the browser that invalidates the session cookie
    ///       -> another browser program with the page still open would still be able to use
    ///       -> the session. Which is kindof OK, but not as cool as erasing the token
    ///       -> on the server side which immediately block all other browsers as well.
    pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) type {
        return struct {
            allocator: std.mem.Allocator,
            lookup: *Lookup,
            settings: UserPassSessionArgs,

            // TODO: cookie store per user?

            sessionTokens: SessionTokenMap,
            passwordLookupLock: std.Thread.Mutex = .{},
            tokenLookupLock: std.Thread.Mutex = .{},

            const Self = @This();
            const SessionTokenMap = std.StringHashMap(void);
            const Hash = std.crypto.hash.sha2.Sha256;

            const Token = [Hash.digest_length * 2]u8;

            /// Construct this authenticator. See above and related types for more
            /// information.
            pub fn init(
                allocator: std.mem.Allocator,
                lookup: *Lookup,
                args: UserPassSessionArgs,
            ) !Self {
                const ret: Self = .{
                    .allocator = allocator,
                    .settings = .{
                        .usernameParam = try allocator.dupe(u8, args.usernameParam),
                        .passwordParam = try allocator.dupe(u8, args.passwordParam),
                        .loginPage = try allocator.dupe(u8, args.loginPage),
                        .cookieName = try allocator.dupe(u8, args.cookieName),
                        .cookieMaxAge = args.cookieMaxAge,
                        .redirectCode = args.redirectCode,
                    },
                    .lookup = lookup,
                    .sessionTokens = SessionTokenMap.init(allocator),
                };

                return ret;
            }

            /// De-init this authenticator.
            pub fn deinit(self: *Self) void {
                self.allocator.free(self.settings.usernameParam);
                self.allocator.free(self.settings.passwordParam);
                self.allocator.free(self.settings.loginPage);
                self.allocator.free(self.settings.cookieName);

                // clean up the session tokens: the key strings are duped

                var key_it = self.sessionTokens.keyIterator();
                while (key_it.next()) |key_ptr| {
                    self.allocator.free(key_ptr.*);
                }
                self.sessionTokens.deinit();
            }

            /// Check for session token cookie, remove the token from the valid tokens
            pub fn logout(self: *Self, r: *const zap.Request) void {
                // we  erase the list of valid tokens server-side (later) and set the

                // cookie to "invalid" on the client side.

                if (r.setCookie(.{
                    .name = self.settings.cookieName,
                    .value = "invalid",
                    .max_age_s = -1,
                })) {
                    zap.debug("logout ok\n", .{});
                } else |err| {
                    zap.debug("logout cookie setting failed: {any}\n", .{err});
                }

                r.parseCookies(false);

                // check for session cookie

                if (r.getCookieStr(self.allocator, self.settings.cookieName, false)) |maybe_cookie| {
                    if (maybe_cookie) |cookie| {
                        defer cookie.deinit();
                        self.tokenLookupLock.lock();
                        defer self.tokenLookupLock.unlock();
                        if (self.sessionTokens.getKeyPtr(cookie.str)) |keyPtr| {
                            const keySlice = keyPtr.*;
                            // if cookie is a valid session, remove it!

                            _ = self.sessionTokens.remove(cookie.str);
                            // only now can we let go of the cookie str slice that

                            // was used as the key

                            self.allocator.free(keySlice);
                        }
                    }
                } else |err| {
                    zap.debug("unreachable: UserPassSession.logout: {any}", .{err});
                }
            }

            fn _internal_authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
                // if we're requesting the login page, let the request through

                if (r.path) |p| {
                    if (std.mem.startsWith(u8, p, self.settings.loginPage)) {
                        return .AuthOK;
                    }
                }

                // parse body

                r.parseBody() catch {
                    // zap.debug("warning: parseBody() failed in UserPassSession: {any}", .{err});

                    // this is not an error in case of e.g. gets with querystrings

                };

                r.parseCookies(false);

                // check for session cookie

                if (r.getCookieStr(self.allocator, self.settings.cookieName, false)) |maybe_cookie| {
                    if (maybe_cookie) |cookie| {
                        defer cookie.deinit();
                        // locked or unlocked token lookup

                        self.tokenLookupLock.lock();
                        defer self.tokenLookupLock.unlock();
                        if (self.sessionTokens.contains(cookie.str)) {
                            // cookie is a valid session!

                            zap.debug("Auth: COOKIE IS OK!!!!: {s}\n", .{cookie.str});
                            return .AuthOK;
                        } else {
                            zap.debug("Auth: COOKIE IS BAD!!!!: {s}\n", .{cookie.str});
                            // this is not necessarily a bad thing. it could be a

                            // stale cookie from a previous session. So let's check

                            // if username and password are being sent and correct.

                        }
                    }
                } else |err| {
                    zap.debug("unreachable: could not check for cookie in UserPassSession: {any}", .{err});
                }

                // get params of username and password

                if (r.getParamStr(self.allocator, self.settings.usernameParam, false)) |maybe_username| {
                    if (maybe_username) |*username| {
                        defer username.deinit();
                        if (r.getParamStr(self.allocator, self.settings.passwordParam, false)) |maybe_pw| {
                            if (maybe_pw) |*pw| {
                                defer pw.deinit();

                                // now check

                                const correct_pw_optional = brk: {
                                    if (lockedPwLookups) {
                                        self.passwordLookupLock.lock();
                                        defer self.passwordLookupLock.unlock();
                                        break :brk self.lookup.*.get(username.str);
                                    } else {
                                        break :brk self.lookup.*.get(username.str);
                                    }
                                };
                                if (correct_pw_optional) |correct_pw| {
                                    if (std.mem.eql(u8, pw.str, correct_pw)) {
                                        // create session token

                                        if (self.createAndStoreSessionToken(username.str, pw.str)) |token| {
                                            defer self.allocator.free(token);
                                            // now set the cookie header

                                            if (r.setCookie(.{
                                                .name = self.settings.cookieName,
                                                .value = token,
                                                .max_age_s = self.settings.cookieMaxAge,
                                            })) {
                                                return .AuthOK;
                                            } else |err| {
                                                zap.debug("could not set session token: {any}", .{err});
                                            }
                                        } else |err| {
                                            zap.debug("could not create session token: {any}", .{err});
                                        }
                                        // errors with token don't mean the auth itself wasn't OK

                                        return .AuthOK;
                                    }
                                }
                            }
                        } else |err| {
                            zap.debug("getParamSt() for password failed in UserPassSession: {any}", .{err});
                            return .AuthFailed;
                        }
                    }
                } else |err| {
                    zap.debug("getParamSt() for user failed in UserPassSession: {any}", .{err});
                    return .AuthFailed;
                }
                return .AuthFailed;
            }

            /// The zap authentication request handler.
            ///
            /// See above for how it works.
            pub fn authenticateRequest(self: *Self, r: *const zap.Request) AuthResult {
                switch (self._internal_authenticateRequest(r)) {
                    .AuthOK => {
                        // username and pass are ok -> created token, set header, caller can continue

                        return .AuthOK;
                    },
                    // this does not happen, just for completeness

                    .Handled => return .Handled,
                    // auth failed -> redirect

                    .AuthFailed => {
                        // we need to redirect and return .Handled

                        self.redirect(r) catch |err| {
                            // we just give up

                            zap.debug("redirect() failed in UserPassSession: {any}", .{err});
                        };
                        return .Handled;
                    },
                }
            }

            fn redirect(self: *Self, r: *const zap.Request) !void {
                try r.redirectTo(self.settings.loginPage, self.settings.redirectCode);
            }

            fn createSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 {
                var hasher = Hash.init(.{});
                hasher.update(username);
                hasher.update(password);
                var buf: [16]u8 = undefined;
                const time_nano = std.time.nanoTimestamp();
                const timestampHex = try std.fmt.bufPrint(&buf, "{0x}", .{time_nano});
                hasher.update(timestampHex);

                var digest: [Hash.digest_length]u8 = undefined;
                hasher.final(&digest);
                const token: Token = std.fmt.bytesToHex(digest, .lower);
                const token_str = try self.allocator.dupe(u8, token[0..token.len]);
                return token_str;
            }

            fn createAndStoreSessionToken(self: *Self, username: []const u8, password: []const u8) ![]const u8 {
                const token = try self.createSessionToken(username, password);
                self.tokenLookupLock.lock();
                defer self.tokenLookupLock.unlock();

                if (!self.sessionTokens.contains(token)) {
                    try self.sessionTokens.put(try self.allocator.dupe(u8, token), {});
                }
                return token;
            }
        };
    }