const std = @import("std");
const lmdb = @import("lmdb");
const db = @import("db");
const http = @import("http");

// db {{{

const Db = struct {
    fn users(txn: lmdb.Txn) !db.Db(UserId, User) {
        return try db.Db(UserId, User).init(txn, "users");
    }
    fn user_ids(txn: lmdb.Txn) !db.Db(Username, UserId) {
        return try db.Db(Username, UserId).init(txn, "user_ids");
    }
    fn sessions(txn: lmdb.Txn) !db.Db(SessionToken, UserId) {
        return try db.Db(SessionToken, UserId).init(txn, "sessions");
    }
    fn posts(txn: lmdb.Txn) !db.Db(PostId, Post) {
        return try db.Db(PostId, Post).init(txn, "posts");
    }
};

// }}}

// content {{{

const User = struct {
    // TODO: choose sizes
    id: UserId,
    name: Username,
    display_name: DisplayName,
    description: UserDescription,
    password_hash: PasswordHash,

    posts: PostList,

    following: UserList,
    followers: UserList,

    post_lists: PostListList,
    feeds: UserListList,
};

const Post = struct {
    id: PostId,
    parent_id: ?PostId,
    quote_id: ?PostId,

    user_id: UserId,
    time: Timestamp,

    upvotes: u64 = 0,
    downvotes: u64 = 0,
    votes: VoteList,
    comments: PostList,
    quotes: PostList,

    text: PostText,
};

const SavedPostList = struct {
    name: Name,
    list: PostList,
};
const SavedUserList = struct {
    name: Name,
    list: UserList,
};

const Vote = struct {
    const Kind = enum { Up, Down };

    kind: Kind,
    time: Timestamp,
};

const Id = u64;
const Login = struct {
    user: User,
    session_token: SessionToken,
};
const Name = std.BoundedArray(u8, 32);
const UserId = enum(u64) { _ };
const PostId = enum(u64) { _ };
const Timestamp = i64;
const Username = std.BoundedArray(u8, 32);
const DisplayName = std.BoundedArray(u8, 64);
const UserDescription = std.BoundedArray(u8, 1024);
const PasswordHash = std.BoundedArray(u8, 128);
const SessionToken = u64;
const CookieValue = std.BoundedArray(u8, 128);
const PostText = std.BoundedArray(u8, 1024);
const PostList = db.Set(PostId);
const UserList = db.Set(UserId);
const VoteList = db.SetList(UserId, Vote);
const PostListList = db.List(SavedPostList);
const UserListList = db.List(SavedUserList);

fn parse_enum(comptime E: type, buf: []const u8, base: u8) !E {
    return @enumFromInt(try std.fmt.parseUnsigned(@typeInfo(E).@"enum".tag_type, buf, base));
}

// https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding
fn reencode(comptime T: type, text: []const u8) !T {
    var result = try T.init(0);

    const len = @min(text.len, 1024); // TODO: PostText length

    var idx: usize = 0;
    while (idx < len) : (idx += 1) {
        const c = text[idx];
        if (c == '+') {
            try result.append(' ');
        } else if (c == '%' and idx + 2 < text.len) {
            const allow = &[_]u8{ 0x26, 0x23, 0x3b, 0x0a };

            const escaped_value = std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16) catch continue;

            if (escaped_value == 0x0d) {
                try std.fmt.format(result.writer(), "<br />", .{});
            } else if (std.mem.indexOfScalar(u8, allow, escaped_value) != null) {
                try std.fmt.format(result.writer(), "{c}", .{escaped_value});
            } else {
                try std.fmt.format(result.writer(), "&#x{x};", .{escaped_value});
            }

            idx += 2;
        } else {
            try result.append(c);
        }
    }

    return result;
}

fn decode(text: []const u8) !std.BoundedArray(u8, 1024) {
    var result = try std.BoundedArray(u8, 1024).init(0);

    const max_len = @min(text.len, 1024); // TODO: PostText length

    var idx: usize = 0;
    var len: usize = 0;
    while (len < max_len and idx < text.len) : ({
        idx += 1;
        len += 1;
    }) {
        const c = text[idx];
        if (c == '+') {
            try result.append(' ');
        } else if (c == '%') {
            if (idx + 2 < text.len) {
                try std.fmt.format(result.writer(), "{c}", .{try std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16)});
            }
            idx += 2;
        } else {
            try result.append(c);
        }
    }

    return result;
}

const Chirp = struct {
    const PostsPerPage = 10;
    const UsersPerPage = 10;
    var HashBuffer = std.mem.zeroes([1024 * 1024 * 50]u8);

    pub fn hash_password(password: []const u8) !PasswordHash {
        var hash_buffer = try PasswordHash.init(128);

        // TODO: choose buffer size
        var alloc = std.heap.FixedBufferAllocator.init(&HashBuffer);

        // TODO: choose limits
        const result = try std.crypto.pwhash.argon2.strHash(password, .{
            .allocator = alloc.allocator(),
            .params = std.crypto.pwhash.argon2.Params.owasp_2id,
        }, hash_buffer.slice());

        try hash_buffer.resize(result.len);

        return hash_buffer;
    }

    pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
        var alloc = std.heap.FixedBufferAllocator.init(&HashBuffer);

        if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
            .allocator = alloc.allocator(),
        })) {
            return true;
        } else |err| {
            std.debug.print("verify error: {}\n", .{err});
            return false;
        }
    }

    pub fn register_user(env: lmdb.Env, username: []const u8, password: []const u8) !bool {
        const username_array = try Username.fromSlice(username);
        const display_name = try DisplayName.fromSlice(username);

        const txn = try env.txn();
        defer txn.commit() catch |err| {
            std.debug.print("error registering user: {}\n", .{err});
        };

        const users = try Db.users(txn);
        const user_ids = try Db.user_ids(txn);

        if (try user_ids.has(username_array)) {
            return false;
        } else {
            const user_id = try db.Prng.gen(users.dbi, UserId);

            try users.put(user_id, User{
                .id = user_id,
                .name = username_array,
                .display_name = display_name,
                .description = try UserDescription.init(0),
                .password_hash = try hash_password(password),
                .posts = try PostList.init(txn),
                .following = try UserList.init(txn),
                .followers = try UserList.init(txn),
                .post_lists = try PostListList.init(txn),
                .feeds = try UserListList.init(txn),
            });

            try user_ids.put(username_array, user_id);

            return true;
        }
    }

    pub fn login_user(
        env: lmdb.Env,
        username: []const u8,
        password: []const u8,
    ) !SessionToken {
        const username_array = try Username.fromSlice(username);

        const txn = try env.txn();
        defer txn.commit() catch {};

        const user_ids = try Db.user_ids(txn);
        const user_id = try user_ids.get(username_array);
        std.debug.print("user logging in, id: {}\n", .{user_id});

        const users = try Db.users(txn);
        const user = try users.get(user_id);

        if (verify_password(password, user.password_hash)) {
            const sessions = try Db.sessions(txn);
            const session_token = try db.Prng.gen(sessions.dbi, SessionToken);
            try sessions.put(session_token, user_id);
            return session_token;
        } else {
            return error.IncorrectPassword;
        }
    }

    fn logout_user(env: lmdb.Env, session_token: SessionToken) !void {
        const txn = try env.txn();
        defer txn.commit() catch {};

        const sessions = try Db.sessions(txn);
        try sessions.del(session_token);
    }

    fn append_post(env: lmdb.Env, user_id: UserId, post_list: PostList, parent_id: ?PostId, quote_id: ?PostId, text: []const u8) !PostId {
        var post_id: PostId = undefined;

        // TODO: do this in one commit

        var txn: lmdb.Txn = undefined;
        {
            // create post
            txn = try env.txn();
            defer txn.commit() catch {};

            const posts = try Db.posts(txn);
            post_id = try db.Prng.gen(posts.dbi, PostId);

            const decoded_text = try reencode(PostText, text);
            try posts.put(post_id, Post{
                .id = post_id,
                .parent_id = parent_id,
                .quote_id = quote_id,
                .user_id = user_id,
                .time = std.time.timestamp(),
                .votes = try VoteList.init(txn),
                .comments = try PostList.init(txn),
                .quotes = try PostList.init(txn),
                .text = decoded_text,
            });
        }

        {
            // append to user's posts
            txn = try env.txn();
            defer txn.commit() catch {};

            var posts_view = try post_list.open(txn);
            try posts_view.append(post_id);
        }

        if (quote_id != null) {
            txn = try env.txn();
            defer txn.commit() catch {};

            const posts = try Db.posts(txn);
            const quote_post = try posts.get(quote_id.?);
            var quotes = try quote_post.quotes.open(txn);
            try quotes.append(post_id);
        }

        return post_id;
    }

    fn post(env: lmdb.Env, user_id: UserId, text: []const u8) !void {
        var txn = try env.txn();
        const users = try Db.users(txn);
        const user = try users.get(user_id);
        txn.abort();

        const post_id = try append_post(env, user_id, user.posts, null, null, text);
        _ = post_id;
    }

    fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void {
        var txn = try env.txn();
        const users = try Db.users(txn);
        const user = try users.get(user_id);

        const posts = try Db.posts(txn);
        const parent_post = try posts.get(parent_post_id);
        txn.abort();

        const post_id = try append_post(env, user_id, parent_post.comments, parent_post_id, null, text);

        txn = try env.txn();
        var replies_view = try user.posts.open(txn);
        try replies_view.append(post_id);
        try txn.commit();
    }

    fn quote(env: lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void {
        var txn = try env.txn();
        const users = try Db.users(txn);
        const user = try users.get(user_id);
        txn.abort();

        const post_id = try append_post(env, user_id, user.posts, null, quote_post_id, text);
        _ = post_id;
    }

    fn vote(env: lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
        const txn = try env.txn();
        defer txn.commit() catch {};

        const posts = try Db.posts(txn);

        var p = try posts.get(post_id);
        var votes_view = try p.votes.open(txn);

        var add_vote = true;

        if (try votes_view.has(user_id)) {
            const old_vote = try votes_view.get(user_id);

            add_vote = old_vote.kind != kind;

            try votes_view.del(user_id);

            switch (old_vote.kind) {
                .Up => p.upvotes -= 1,
                .Down => p.downvotes -= 1,
            }
            try posts.put(post_id, p);
        }

        if (add_vote) {
            try votes_view.append(user_id, Vote{
                .kind = kind,
                .time = std.time.timestamp(),
            });

            if (kind == .Up) {
                p.upvotes += 1;
            } else {
                p.downvotes += 1;
            }
            try posts.put(post_id, p);
        }
    }

    fn follow(env: lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
        const txn = try env.txn();
        defer txn.commit() catch {};

        const users = try Db.users(txn);

        const user = try users.get(user_id);
        const user_to_follow = try users.get(user_id_to_follow);

        var user_following = try user.following.open(txn);
        var user_to_follow_followers = try user_to_follow.followers.open(txn);

        if ((user_following.has(user_id_to_follow) catch false) and (user_to_follow_followers.has(user_id) catch false)) {
            try user_following.del(user_id_to_follow);
            try user_to_follow_followers.del(user_id);
        } else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
            try user_following.append(user_id_to_follow);
            try user_to_follow_followers.append(user_id);
        } else {
            std.debug.print("Something went wrong when trying to unfollow\n", .{});
        }
    }

    fn get_session_user_id(env: lmdb.Env, session_token: SessionToken) !UserId {
        const txn = try env.txn();
        defer txn.abort();

        const sessions = try Db.sessions(txn);

        return try sessions.get(session_token);
    }

    fn get_user(env: lmdb.Env, user_id: UserId) !User {
        const txn = try env.txn();
        defer txn.abort();

        const users = try Db.users(txn);
        return try users.get(user_id);
    }
};

// }}}

// html {{{
pub fn Paginate(comptime T: type) type {
    return struct {
        const Self = @This();

        const IterateResult = T.Base.View.Iterator.Result;

        res: *http.Response,
        view: T.View,
        per_page: u64,

        it: T.Base.View.Iterator,
        starting_idx: ?T.Base.Key,
        count: u64 = 0,

        pub fn init(res: *http.Response, view: T.View, per_page: u64) !Self {
            var it = view.reverse_iterator();
            if (res.req.get_param("starting_at")) |starting_at_str| {
                it.idx = try parse_enum(T.Base.Key, starting_at_str, 16);
            }

            return .{
                .res = res,
                .view = view,
                .per_page = per_page,
                .it = it,
                .starting_idx = it.idx,
            };
        }
        pub fn next(self: *Self) ?IterateResult {
            if (self.it.next()) |kv| {
                if (self.count < self.per_page) {
                    self.count += 1;
                    return kv;
                }
            }
            return null;
        }
        pub fn write_navigation(self: *Self) !void {
            const next_idx = self.it.next();

            if (self.view.base.head.last != self.starting_idx) {
                var prev_it = self.view.iterator();
                prev_it.idx = self.starting_idx.?;
                var oldest_idx = self.starting_idx.?;

                var count: u64 = 0;
                while (prev_it.next()) |kv| {
                    oldest_idx = kv.key;

                    if (count > self.per_page) {
                        break;
                    } else {
                        count += 1;
                    }
                }

                try self.res.write("<a href=\"{s}?starting_at={x}\">Prev</a> ", .{ self.res.req.target, @intFromEnum(oldest_idx) });
            }

            if (next_idx) |kv| {
                try self.res.write("<a href=\"{s}?starting_at={x}\">Next</a>", .{ self.res.req.target, @intFromEnum(kv.key) });
            }
        }
    };
}
fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void {
    try res.write("<form action=\"{s}\" method=\"post\">", .{action});

    inline for (inputs) |input| {
        switch (@typeInfo(@TypeOf(input))) {
            .@"struct" => |s| {
                if (s.fields.len == 3) {
                    try res.write("<{s} ", .{input[0]});
                    try res.write(input[1], input[2]);
                    try res.write("></{s}>", .{input[0]});
                } else {
                    try res.write("<input ", .{});
                    try res.write(input[0], input[1]);
                    try res.write(" />", .{});
                }
            },
            else => {
                try res.write("<input ", .{});
                try res.write(input, .{});
                try res.write(" />", .{});
            },
        }
    }

    try res.write("</form>", .{});
}
// }}}

// write {{{
const TimeStr = std.BoundedArray(u8, 256);

// http://howardhinnant.github.io/date_algorithms.html
fn time_str(_t: i64) TimeStr {
    const t: u64 = @intCast(_t);
    var result = TimeStr.init(0) catch unreachable;

    const nD = @divFloor(t, std.time.s_per_day);
    const z: u64 = nD + 719468;
    const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
    const doe: u64 = z - era * 146097; // [0, 146096]
    const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
    const Y: u64 = yoe + era * 400;
    const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
    const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
    const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
    const M: u64 = if (mp < 10) mp + 3 else mp - 9;

    const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
    const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
    const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;

    std.fmt.format(result.writer(), "<time><span>{:0>4}-{:0>2}-{:0>2} {:0>2}:{:0>2}:{:0>2} UTC</span><script>document.currentScript.parentElement.innerHTML=new Date({}).toLocaleString()</script></time>", .{ Y, M, D, h, m, s, t * 1000 }) catch unreachable;

    return result;
}
fn write_header(res: *http.Response, logged_in: ?Login) !void {
    if (logged_in) |login| {
        try res.write(
            \\<a href="/">Home</a><br />
        , .{});
        try res.write(
            \\<a href="/user/{s}">Profile</a><br />
        , .{login.user.name.constSlice()});
        try res.write(
            \\<a href="/post">Post</a><br />
        , .{});
        try html_form(res, "/logout", .{
            \\type="submit" value="Logout"
        });
        try res.write("<br /><br />", .{});
    } else {
        try res.write(
            \\<a href="/">Home</a><br />
            \\<form action="/register" method="post">
            \\<input type="text" name="username" />
            \\<input type="password" name="password" />
            \\<input type="submit" value="Register" />
            \\</form><br />
            \\<form action="/login" method="post">
            \\<input type="text" name="username" />
            \\<input type="password" name="password" />
            \\<input type="submit" value="Login" />
            \\</form><br /><br />
        , .{});
    }
}
fn write_start(res: *http.Response) !void {
    try res.write(
        \\<!doctype html>
        \\<html>
        \\<head>
        \\<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>&#128035;</text></svg>">
        \\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
        \\<style>
        \\  form {
        \\    display: inline-block;
        \\  }
        \\  body {
        \\    margin:40px auto;max-width:650px;line-height:1.6;font-size:18px;color:#444;padding:0 10px;
        \\  }
        \\  h1,h2,h3{line-height:1.2}
        \\</style>
        \\</head>
        \\<body>
    , .{});
}
fn write_end(res: *http.Response) !void {
    try res.write("</body></html>", .{});
}
fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, options: struct { recurse: u8 = 0, show_comment_field: bool = false }) !void {
    const posts = try Db.posts(txn);
    const post = posts.get(post_id) catch {
        res.redirect("/") catch {};
        return;
    };
    const users = try Db.users(txn);
    const user = try users.get(post.user_id);

    try res.write(
        \\<div id="{x}">
        \\<span><a href="/user/{s}">{s}</a>
    , .{ @intFromEnum(post_id), user.name.constSlice(), user.display_name.constSlice() });
    if (post.parent_id) |id| {
        try res.write(" <a href=\"/post/{x}\">..</a>", .{@intFromEnum(id)});
    }
    try res.write(
        \\ {s}</span><br />
        \\<span>{s}</span><br />
    , .{ time_str(post.time).constSlice(), post.text.constSlice() });

    if (logged_in != null and post.user_id == logged_in.?.user.id) {
        // Votes
        try res.write(
            \\<small>
            \\<a href="/upvotes/{0x}">{1} Upvotes</a>
            \\<a href="/downvotes/{0x}">{2} Downvotes</a>
            \\</small>
            \\<br />
        , .{ @intFromEnum(post_id), post.upvotes, post.downvotes });
    }

    if (post.quote_id) |quote_id| {
        try res.write("<div style=\"border: 1px solid black;\">", .{});
        if (options.recurse > 0) {
            try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 });
        } else {
            try res.write("<a href=\"/post/{x}\">...</a>", .{@intFromEnum(post_id)});
        }
        try res.write("</div>", .{});
    }

    const comments_view = try post.comments.open(txn);
    const quotes_view = try post.quotes.open(txn);
    const votes_view = try post.votes.open(txn);

    // Votes
    const vote: ?Vote = if (logged_in != null and try votes_view.has(logged_in.?.user.id)) try votes_view.get(logged_in.?.user.id) else null;

    if (vote != null and vote.?.kind == .Up) {
        try html_form(res, "/upvote", .{
            .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
            .{ "type=\"submit\" value=\"&#x2B06; {}\"", .{post.upvotes} },
        });
    } else {
        try html_form(res, "/upvote", .{
            .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
            .{ "type=\"submit\" value=\"&#x21E7; {}\"", .{post.upvotes} },
        });
    }
    if (vote != null and vote.?.kind == .Down) {
        try html_form(res, "/downvote", .{
            .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
            .{ "type=\"submit\" value=\"&#x2B07; {}\"", .{post.downvotes} },
        });
    } else {
        try html_form(res, "/downvote", .{
            .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
            .{ "type=\"submit\" value=\"&#x21E9; {}\"", .{post.downvotes} },
        });
    }

    // Comment Count
    try res.write(
        \\<a href="/post/{x}">&#x1F4AD; {}</a> 
    , .{ @intFromEnum(post.id), comments_view.len() });

    // Quote
    try res.write(
        \\<a href="/quoted/{x}">&#x1F501; {}</a> 
    , .{ @intFromEnum(post.id), quotes_view.len() });

    // Save to List
    if (logged_in) |login| {
        const lists_view = try login.user.post_lists.open(txn);
        try res.write("<form action=\"/list_add\" method=\"post\">", .{});
        try res.write("<select name=\"list_id\">", .{});
        var it = lists_view.iterator();
        // TODO: mark lists that already contain post
        while (it.next()) |kv| {
            const name = kv.val.name;
            const id = kv.val.list.base.idx.?;
            const list_view = try kv.val.list.open(txn);
            try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(post_id) catch false) " *" else "" });
        }
        try res.write("</select>", .{});
        try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\" />", .{@intFromEnum(post_id)});
        try res.write("<input type=\"submit\" value=\"Save\" />", .{});
        try res.write("</form>", .{});
    }

    // Comment field
    // TODO: maybe always show comment field and prompt for login
    if (options.show_comment_field and logged_in != null) {
        try res.write("<br /><br />", .{});
        try html_form(res, "/comment", .{
            .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
            .{ "textarea", "type=\"text\" name=\"text\" placeholder=\"Text\"", .{} },
            "type=\"submit\" value=\"Comment\"",
        });
        try res.write("<br />", .{});
    }

    // Comments
    if (options.recurse > 0 and comments_view.len() > 0) {
        try res.write(
            \\<details{s}>
            \\<summary>Comments</summary>
        , .{if (options.recurse > 1) " open" else ""});
        try res.write("<div style=\"margin: 10px;\">", .{});
        var it = comments_view.iterator();
        var count: u8 = 0;
        while (it.next()) |comment_id| {
            try write_post(res, txn, logged_in, comment_id.key, .{ .recurse = options.recurse - 1 });
            try res.write("<br />", .{});
            if (options.recurse == 1) {
                count += 1;
                if (count >= 3) break;
            }
        }
        try res.write(
            \\</div>
            \\</details>
        , .{});
    }

    try res.write(
        \\</div>
    , .{});
}
fn write_profile(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
    const following = try user.following.open(txn);
    const followers = try user.followers.open(txn);

    try res.write(
        \\<h2 style="display: inline;"><a href="/user/{s}">{s}</a></h2>
    , .{
        user.name.constSlice(), user.display_name.constSlice(),
    });
    if (logged_in != null and user.id != logged_in.?.user.id) {
        const login = logged_in.?;

        // follow/unfollow
        if (try followers.has(login.user.id)) {
            try html_form(res, "/follow", .{
                .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
                \\type="submit" value="Unfollow"
            });
        } else {
            try html_form(res, "/follow", .{
                .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
                \\type="submit" value="Follow"
            });
        }

        // add to feed
        const feeds_view = try login.user.feeds.open(txn);
        try res.write("<form action=\"/feed_add\" method=\"post\">", .{});
        try res.write("<select name=\"feed_id\">", .{});
        var it = feeds_view.iterator();
        while (it.next()) |kv| {
            const name = kv.val.name;
            const id = kv.val.list.base.idx.?;
            const list_view = try kv.val.list.open(txn);
            try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(user.id) catch false) " *" else "" });
        }
        try res.write("</select>", .{});
        try res.write("<input type=\"hidden\" name=\"user_id\" value=\"{x}\" />", .{@intFromEnum(user.id)});
        try res.write("<input type=\"submit\" value=\"Add to feed\" />", .{});
        try res.write("</form>", .{});
    }
    try res.write(
        \\ <a href="/following/{s}">{} following</a>
        \\ <a href="/followers/{s}">{} followers</a>
        \\<br />
    , .{
        user.name.constSlice(), following.len(),
        user.name.constSlice(), followers.len(),
    });

    try res.write(
        \\<a href="/all/{0s}">All Posts</a>
        \\ <a href="/comments/{0s}">Comments</a>
        \\ <a href="/quotes/{0s}">Quotes</a><br />
    , .{
        user.name.constSlice(),
    });

    if (logged_in != null and user.id == logged_in.?.user.id) {
        try res.write(
            \\<a href="/lists">Lists</a>
            \\<a href="/feeds">Feeds</a>
            \\<a href="/edit">Edit</a><br />
            \\<br />
        , .{});
    }

    if (user.description.len > 0) {
        try res.write(
            \\<div style="padding-left: 5px; border-left: 1px solid grey;">
            // \\&#x00AB; {s} &#x00BB;
            \\<i>{s}</i>
            \\</div>
        , .{user.description.constSlice()});
    }

    try res.write("<br />", .{});
}
fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList, options: struct {
    show_posts: bool,
    show_quotes: bool,
    show_comments: bool,
}) !void {
    const posts_view = try post_list.open(txn);

    var paginate = try Paginate(PostList).init(res, posts_view, Chirp.PostsPerPage);

    while (paginate.next()) |post_id| {
        const posts = try Db.posts(txn);
        const post = try posts.get(post_id.key);
        if ((options.show_posts and (post.parent_id == null and post.quote_id == null)) or
            (options.show_quotes and (post.quote_id != null)) or
            (options.show_comments and (post.parent_id != null)))
        {
            try write_post(res, txn, logged_in, post_id.key, .{ .recurse = 1 });
            try res.write("<br />", .{});
        }
    }

    try paginate.write_navigation();
}
fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
    const users = try Db.users(txn);
    const posts = try Db.posts(txn);

    var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
    var prev_newest_post: ?Post = null;

    const following = try user_list.open(txn);
    if (following.len() == 0) {
        try res.write("Empty timeline (no users)", .{});
        return;
    }

    while (true) {
        var newest_post: ?Post = null;

        var following_it = following.iterator();
        while (following_it.next()) |following_id| {
            const followed_user = try users.get(following_id.key);
            const followed_posts = try followed_user.posts.open(txn);

            if (followed_posts.len() == 0) {
                continue;
            }

            var followed_posts_it = followed_posts.reverse_iterator();
            while (followed_posts_it.next()) |followed_post_id| {
                const p = try posts.get(followed_post_id.key);

                if ((prev_newest_post == null or p.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < p.time)) {
                    newest_post = p;
                    break;
                }
            }
        }
        if (newest_post) |post| {
            newest_post_ids.append(post.id) catch break;
            prev_newest_post = post;
        } else {
            break;
        }
    }

    for (newest_post_ids.constSlice()) |post_id| {
        try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
        try res.write("<br />", .{});
    }
}
fn write_user(res: *http.Response, txn: lmdb.Txn, user_id: UserId) !void {
    const users = try Db.users(txn);
    const user = try users.get(user_id);
    try res.write(
        \\<a href="/user/{s}">{s}</a>
    , .{ user.name.constSlice(), user.display_name.constSlice() });
}
fn write_votes(res: *http.Response, txn: lmdb.Txn, votes: VoteList, options: struct {
    show_upvotes: bool = true,
    show_downvotes: bool = true,
}) !void {
    const votes_view = try votes.open(txn);

    var paginate = try Paginate(VoteList).init(res, votes_view, Chirp.UsersPerPage);

    while (paginate.next()) |kv| {
        const user_id = kv.key;
        const vote = kv.val;

        if ((options.show_upvotes and vote.kind == .Up) or
            (options.show_downvotes and vote.kind == .Down))
        {
            try write_user(res, txn, user_id);
            try res.write(" <small>{s}</small><br />", .{time_str(vote.time).constSlice()});
        }
    }

    try paginate.write_navigation();
}
fn check_login(env: lmdb.Env, req: http.Request, res: *http.Response) !?Login {
    var result: ?Login = null;

    if (req.get_cookie("session_token")) |session_token_str| {
        var remove_session_token = true;

        if (std.fmt.parseUnsigned(SessionToken, session_token_str, 16) catch null) |session_token| {
            // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
            // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
            if (Chirp.get_session_user_id(env, session_token) catch null) |user_id| {
                const txn = try env.txn();
                defer txn.abort();
                const users = try Db.users(txn);

                result = .{
                    .user = try users.get(user_id),
                    .session_token = session_token,
                };

                remove_session_token = false;
            }
        }

        if (remove_session_token) {
            try res.add_header(
                "Set-Cookie",
                .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
            );
        }
    }

    return result;
}
// }}}

// GET {{{
const GET = struct {
    const Self = @This();

    txn: lmdb.Txn,
    req: http.Request,
    res: *http.Response,
    logged_in: ?Login,

    fn handle(self: Self) !bool {
        const ti = @typeInfo(Self);
        inline for (ti.@"struct".decls) |f_decl| {
            const has_arg = f_decl.name.len > 1 and f_decl.name[f_decl.name.len - 1] == '/';
            const match = if (has_arg) std.mem.startsWith(u8, self.req.target, f_decl.name) else std.mem.eql(u8, self.req.target, f_decl.name);

            if (match) {
                const f = @field(Self, f_decl.name);
                const fi = @typeInfo(@TypeOf(f));
                if (fi.@"fn".params.len == 1) {
                    try @call(.auto, f, .{self});
                } else {
                    const arg_type = fi.@"fn".params[1].type.?;
                    const arg_info = @typeInfo(arg_type);
                    var arg: arg_type = undefined;
                    const field = arg_info.@"struct".fields[0];
                    if (self.req.target.len <= f_decl.name.len) {
                        return error.NoArgProvided;
                    }
                    const str = self.req.target[f_decl.name.len..self.req.target.len];
                    const field_ti = @typeInfo(field.type);
                    switch (field_ti) {
                        // TODO: maybe handle BoundedArray?
                        .int => {
                            @field(arg, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
                        },
                        .@"enum" => {
                            @field(arg, field.name) = try parse_enum(field.type, str, 16);
                        },
                        else => {
                            @field(arg, field.name) = str;
                        },
                    }

                    try @call(.auto, f, .{ self, arg });
                }
                return true;
            }
        }
        return false;
    }

    pub fn @"/user/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            try write_profile(self.res, self.txn, self.logged_in, user);

            try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
                .show_posts = true,
                .show_quotes = false,
                .show_comments = false,
            });
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/comments/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            try write_profile(self.res, self.txn, self.logged_in, user);

            try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
                .show_posts = false,
                .show_quotes = false,
                .show_comments = true,
            });
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/quotes/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            try write_profile(self.res, self.txn, self.logged_in, user);

            try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
                .show_posts = false,
                .show_quotes = true,
                .show_comments = false,
            });
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/all/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            try write_profile(self.res, self.txn, self.logged_in, user);

            try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
                .show_posts = true,
                .show_quotes = true,
                .show_comments = true,
            });
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/following/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            const following_view = try user.following.open(self.txn);

            var paginate = try Paginate(UserList).init(self.res, following_view, Chirp.UsersPerPage);

            try self.res.write(
                \\<h2><a href="/user/{s}">{s}</a> follows:</h2>
            , .{ user.name.constSlice(), user.display_name.constSlice() });

            while (paginate.next()) |following_id| {
                const following_user = try users.get(following_id.key);

                try self.res.write(
                    \\<a href="/user/{s}">{s}</a><br />
                , .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
            }

            try paginate.write_navigation();
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/followers/"(self: Self, args: struct { username: []const u8 }) !void {
        const user_ids = try Db.user_ids(self.txn);
        if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
            const users = try Db.users(self.txn);
            const user = try users.get(user_id);

            const followers_view = try user.followers.open(self.txn);
            var paginate = try Paginate(UserList).init(self.res, followers_view, Chirp.UsersPerPage);

            try self.res.write(
                \\<h2><a href="/user/{s}">{s}</a> followers:</h2>
            , .{ user.name.constSlice(), user.display_name.constSlice() });

            while (paginate.next()) |follower_id| {
                const follower_user = try users.get(follower_id.key);

                try self.res.write(
                    \\<a href="/user/{s}">{s}</a><br />
                , .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
            }

            try paginate.write_navigation();
        } else |err| {
            try self.res.write(
                \\<p>User not found [{}]</p>
            , .{err});
        }
    }
    pub fn @"/post/"(self: Self, args: struct { post_id: PostId }) !void {
        try write_post(self.res, self.txn, self.logged_in, args.post_id, .{
            .recurse = 3, // TODO: factor out
            .show_comment_field = true,
        });
    }
    pub fn @"/upvotes/"(self: Self, args: struct { post_id: PostId }) !void {
        const posts = try Db.posts(self.txn);
        const post = try posts.get(args.post_id);
        try self.res.write("{} upvotes:<br />", .{post.upvotes});
        try write_votes(self.res, self.txn, post.votes, .{});
    }
    pub fn @"/quoted/"(self: Self, args: struct { post_id: PostId }) !void {
        const posts = try Db.posts(self.txn);
        const post = try posts.get(args.post_id);

        const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;

        if (self.logged_in != null) {
            try html_form(self.res, "/quote", .{
                .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
                .{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
                .{ "textarea", "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus", .{} },
                "type=\"submit\" value=\"Quote\"",
            });
            try self.res.write("<br />", .{});
        }

        // TODO: show all bc this only contains quotes?
        try write_posts(self.res, self.txn, self.logged_in, post.quotes, .{
            .show_posts = false,
            .show_quotes = true,
            .show_comments = false,
        });
    }
    pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
        try write_posts(self.res, self.txn, self.logged_in, PostList{ .base = .{ .idx = args.list_id } }, .{
            .show_posts = true,
            .show_quotes = true,
            .show_comments = true,
        });
    }
    pub fn @"/lists"(self: Self) !void {
        if (self.logged_in) |login| {
            const post_lists_view = try login.user.post_lists.open(self.txn);

            try html_form(self.res, "/create_list", .{
                "type=\"text\" name=\"name\"",
                "type=\"submit\" value=\"Add\"",
            });

            try self.res.write("<br /><br />", .{});

            var it = post_lists_view.iterator();
            while (it.next()) |kv| {
                const name = kv.val.name;
                const post_list = kv.val.list;
                try self.res.write(
                    \\<a href="/list/{x}">{s}</a> 
                , .{ post_list.base.idx.?, name.constSlice() });
                try html_form(self.res, "/delete_list", .{
                    .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
                    "type=\"submit\" value=\"Delete\"",
                });
                try self.res.write("<br />", .{});
            }
        } else {
            try self.res.write("not logged in", .{});
        }
    }
    pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Base.Index }) !void {
        try write_timeline(self.res, self.txn, self.logged_in, UserList{ .base = .{ .idx = args.feed_id } });
    }
    pub fn @"/feeds"(self: Self) !void {
        if (self.logged_in) |login| {
            const feeds_view = try login.user.feeds.open(self.txn);

            try html_form(self.res, "/create_feed", .{
                "type=\"text\" name=\"name\"",
                "type=\"submit\" value=\"Add\"",
            });

            try self.res.write("<br /><br />", .{});

            var it = feeds_view.iterator();
            while (it.next()) |kv| {
                const name = kv.val.name;
                const user_list = kv.val.list;
                try self.res.write(
                    \\<a href="/feed/{x}">{s}</a> 
                , .{ user_list.base.idx.?, name.constSlice() });
                try html_form(self.res, "/delete_feed", .{
                    .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
                    "type=\"submit\" value=\"Delete\"",
                });
                try self.res.write("<br />", .{});
            }
        } else {
            try self.res.write("not logged in", .{});
        }
    }
    pub fn @"/post"(self: Self) !void {
        if (self.logged_in) |login| {
            _ = login;
            const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;

            try html_form(self.res, "/post", .{
                .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
                .{ "textarea", "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus", .{} },
                "type=\"submit\" value=\"Post\"",
            });
        } else {
            try self.res.write("not logged in", .{});
        }
    }
    pub fn @"/edit"(self: Self) !void {
        if (self.logged_in) |login| {
            try self.res.write("<br />Username: ", .{});
            try html_form(self.res, "/set_username", .{
                .{ "type=\"text\" name=\"username\" placeholder=\"{s}\"", .{login.user.name.constSlice()} },
                "type=\"submit\" value=\"Change\"",
            });
            try self.res.write("<br />Display Name: ", .{});
            try html_form(self.res, "/set_display_name", .{
                .{ "type=\"text\" name=\"display_name\" placeholder=\"{s}\"", .{login.user.display_name.constSlice()} },
                "type=\"submit\" value=\"Change\"",
            });
            try self.res.write("<br />Description: ", .{});
            try html_form(self.res, "/set_description", .{
                .{ "textarea", "type=\"text\" name=\"description\" placeholder=\"{s}\"", .{login.user.description.constSlice()} },
                "type=\"submit\" value=\"Change\"",
            });
            try self.res.write("<br />Password: ", .{});
            try html_form(self.res, "/set_password", .{
                "type=\"text\" name=\"password\"",
                "type=\"submit\" value=\"Change\"",
            });
        } else {
            try self.res.write("not logged in", .{});
        }
    }
    pub fn @"/"(self: Self) !void {
        if (self.logged_in) |login| {
            try write_timeline(self.res, self.txn, self.logged_in, login.user.following);
        } else {
            // TODO: generic home
            try self.res.write("Homepage", .{});
        }
    }
};
// }}}

// POST {{{
const POST = struct {
    const Self = @This();

    env: lmdb.Env,
    req: http.Request,
    res: *http.Response,
    logged_in: ?Login,

    pub fn handle(self: Self) !bool {
        const ti = @typeInfo(Self);
        inline for (ti.@"struct".decls) |f_decl| {
            if (std.mem.eql(u8, f_decl.name, self.req.target)) {
                const f = @field(Self, f_decl.name);
                const fi = @typeInfo(@TypeOf(f));
                if (fi.@"fn".params.len == 1) {
                    _ = try @call(.auto, f, .{self});
                } else {
                    const args_type = fi.@"fn".params[fi.@"fn".params.len - 1].type.?;
                    const argsi = @typeInfo(args_type);
                    var args: args_type = undefined;
                    inline for (argsi.@"struct".fields) |field| {
                        const str = self.req.get_value(field.name) orelse return error.ArgNotFound;
                        const field_ti = @typeInfo(field.type);
                        switch (field_ti) {
                            .int => {
                                @field(args, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
                            },
                            .@"enum" => {
                                @field(args, field.name) = try parse_enum(field.type, str, 16);
                            },
                            else => {
                                @field(args, field.name) = str;
                            },
                        }
                    }
                    try @call(.auto, f, .{ self, args });
                }
                return true;
            }
        }
        return false;
    }

    pub fn @"/register"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
        // TODO: handle args not supplied
        std.debug.print("New user: {s} {s}\n", .{ args.username, args.password });
        _ = try Chirp.register_user(self.env, args.username, args.password);
    }
    pub fn @"/login"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
        // TODO: handle args not supplied
        std.debug.print("New login: {s} {s}\n", .{ args.username, args.password });
        if (Chirp.login_user(self.env, args.username, args.password)) |session_token| {
            self.res.status = .see_other;
            try self.res.add_header(
                "Set-Cookie",
                .{ "session_token={x}; HttpOnly", .{session_token} },
            );
        } else |err| {
            std.debug.print("login_user err: {}\n", .{err});
        }
    }
    pub fn @"/logout"(self: Self) !void {
        if (self.logged_in) |login| {
            try Chirp.logout_user(self.env, login.session_token);

            try self.res.add_header(
                "Set-Cookie",
                .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
            );
        }
    }
    pub fn @"/set_username"(self: Self, args: struct { username: []const u8 }) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;
        const username = try Username.fromSlice(args.username);

        const txn = try self.env.txn();
        defer txn.commit() catch {};

        const user_ids = try Db.user_ids(txn);

        if (!try user_ids.has(username)) {
            try user_ids.del(login.user.name);
            try user_ids.put(username, login.user.id);

            const users = try Db.users(txn);
            var user = login.user;
            user.name = username;
            try users.put(login.user.id, user);
        }
    }
    pub fn @"/set_display_name"(self: Self, args: struct { display_name: []const u8 }) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;
        const display_name = try DisplayName.fromSlice(args.display_name);

        const txn = try self.env.txn();
        defer txn.commit() catch {};

        const users = try Db.users(txn);
        var user = login.user;
        user.display_name = display_name;
        try users.put(login.user.id, user);
    }
    pub fn @"/set_description"(self: Self, args: struct { description: []const u8 }) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;
        const description = try reencode(UserDescription, args.description);

        const txn = try self.env.txn();
        defer txn.commit() catch {};

        const users = try Db.users(txn);
        var user = login.user;
        user.description = description;
        try users.put(login.user.id, user);
    }
    pub fn @"/set_password"(self: Self, args: struct { password: []const u8 }) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;

        const txn = try self.env.txn();
        defer txn.commit() catch {};

        const users = try Db.users(txn);
        var user = login.user;
        user.password_hash = try Chirp.hash_password(args.password);
        try users.put(login.user.id, user);
    }
    pub fn @"/post"(self: Self) !void {
        if (self.logged_in) |login| {
            const text = self.req.get_value("text").?;
            const has_referer = self.req.get_value("referer");

            try Chirp.post(self.env, login.user.id, text);

            if (has_referer) |r| {
                const decoded = try decode(r);
                try self.res.redirect(decoded.constSlice());
            }
        }
    }
    pub fn @"/comment"(self: Self) !void {
        if (self.logged_in) |login| {
            const text = self.req.get_value("text") orelse return error.NoText;
            const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
            const post_id = try parse_enum(PostId, post_id_str, 16);

            try Chirp.comment(self.env, login.user.id, post_id, text);
        }
    }
    pub fn @"/quote"(self: Self) !void {
        if (self.logged_in) |login| {
            const text = self.req.get_value("text") orelse return error.NoText;
            const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
            const has_referer = self.req.get_value("referer");

            const post_id = try parse_enum(PostId, post_id_str, 16);

            try Chirp.quote(self.env, login.user.id, post_id, text);

            if (has_referer) |r| {
                const decoded = try decode(r);
                try self.res.redirect(decoded.constSlice());
            }
        }
    }
    // TODO: add arguments instead of parsing manually
    pub fn @"/create_list"(self: Self) !void {
        if (self.logged_in) |login| {
            const name_str = self.req.get_value("name") orelse return error.NoName;
            const name = try Name.fromSlice(name_str);
            // TODO: decode name

            var txn = try self.env.txn();
            const postlist = try PostList.init(txn);
            try txn.commit();

            txn = try self.env.txn();
            var post_lists_view = try login.user.post_lists.open(txn);
            _ = try post_lists_view.append(.{ .name = name, .list = postlist });
            try txn.commit();
        }
    }
    pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
        if (self.logged_in) |login| {
            var post_list: ?PostList = null;
            {
                const txn = try self.env.txn();
                defer txn.commit() catch {};
                var post_lists_view = try login.user.post_lists.open(txn);
                post_list = (try post_lists_view.get(args.list_id)).list;
                try post_lists_view.del(args.list_id);
            }
            if (post_list != null) {
                const txn = try self.env.txn();
                defer txn.commit() catch {};
                var list_view = try post_list.?.open(txn);
                try list_view.clear();
            }
        }
    }
    pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.Base.Index, post_id: PostId }) !void {
        if (self.logged_in) |login| {
            _ = login;

            const txn = try self.env.txn();
            defer txn.commit() catch {};

            const post_list = PostList{ .base = .{ .idx = args.list_id } };
            var post_list_view = try post_list.open(txn);
            if (try post_list_view.has(args.post_id)) {
                try post_list_view.del(args.post_id);
            } else {
                try post_list_view.append(args.post_id);
            }
        }
    }
    pub fn @"/create_feed"(self: Self) !void {
        if (self.logged_in) |login| {
            const name_str = self.req.get_value("name") orelse return error.NoName;
            const name = try Name.fromSlice(name_str);

            var txn = try self.env.txn();
            const userlist = try UserList.init(txn);
            try txn.commit();

            txn = try self.env.txn();
            var feeds_view = try login.user.feeds.open(txn);
            _ = try feeds_view.append(.{ .name = name, .list = userlist });
            try txn.commit();
        }
    }
    pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Base.Index }) !void {
        if (self.logged_in) |login| {
            var user_list: ?UserList = null;

            {
                const txn = try self.env.txn();
                defer txn.commit() catch {};
                var feeds_view = try login.user.feeds.open(txn);
                user_list = (try feeds_view.get(args.list_id)).list;
                try feeds_view.del(args.list_id);
            }
            if (user_list != null) {
                const txn = try self.env.txn();
                defer txn.commit() catch {};
                var list_view = try user_list.?.open(txn);
                try list_view.clear();
            }
        }
    }
    pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.Base.Index, user_id: UserId }) !void {
        if (self.logged_in) |login| {
            _ = login;

            const txn = try self.env.txn();
            defer txn.commit() catch {};

            const user_list = UserList{ .base = .{ .idx = args.feed_id } };
            var user_list_view = try user_list.open(txn);
            if (try user_list_view.has(args.user_id)) {
                try user_list_view.del(args.user_id);
            } else {
                try user_list_view.append(args.user_id);
            }
        }
    }
    pub fn @"/upvote"(self: Self) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;

        const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
        const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));

        try Chirp.vote(self.env, post_id, login.user.id, .Up);
    }
    pub fn @"/downvote"(self: Self) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;

        const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
        const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));

        try Chirp.vote(self.env, post_id, login.user.id, .Down);
    }
    pub fn @"/follow"(self: Self) !void {
        const login = self.logged_in orelse return error.NotLoggedIn;

        const user_id_str = self.req.get_value("user_id") orelse return error.NoUserId;
        const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16));

        try Chirp.follow(self.env, login.user.id, user_id);
    }
    pub fn @"/quit"(self: Self) !void {
        if (self.req.get_header("Referer")) |ref| {
            try self.res.redirect(ref);
        } else {
            try self.res.redirect("/");
        }
        try self.res.send();
        // break :accept;
    }
};
// }}}

fn list_users(env: lmdb.Env) !void {
    const txn = try env.txn();
    defer txn.abort();

    const users = try Db.users(txn);
    var it = try users.iterator();

    while (it.next()) |kv| {
        const key = kv.key;
        const user = kv.val;
        std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() });
    }
}
fn list_user_ids(env: lmdb.Env) !void {
    const txn = try env.txn();
    defer txn.abort();

    const user_ids = try Db.user_ids(txn);
    var it = try user_ids.iterator();

    while (it.next()) |kv| {
        const key = kv.key;
        const user_id = kv.val;
        std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
    }
}

fn list_sessions(env: lmdb.Env) !void {
    const txn = try env.txn();
    defer txn.abort();

    const sessions = try Db.sessions(txn);
    var it = try sessions.iterator();

    while (it.next()) |kv| {
        const key = kv.key;
        const user_id = kv.val;
        std.debug.print("[{x}] {}\n", .{ key, user_id });
    }
}

fn list_posts(env: lmdb.Env) !void {
    const txn = try env.txn();
    defer txn.abort();

    const posts = try Db.posts(txn);
    var it = try posts.iterator();

    while (it.next()) |kv| {
        const key = kv.key;
        const post = kv.val;
        std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() });
    }
}

const ReqBufferSize = 4096;
const ResHeadBufferSize = 1024 * 64;
const ResBodyBufferSize = 1024 * 64;

// TODO: static?
var req_buffer: [ReqBufferSize]u8 = undefined;
var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
var res_body_buffer: [ResBodyBufferSize]u8 = undefined;

pub fn main() !void {
    // server
    var server = try http.Server.init("::", 8080);
    defer server.deinit();

    // lmdb
    var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
    defer env.close();

    std.debug.print("Users:\n", .{});
    try list_users(env);
    std.debug.print("User IDs:\n", .{});
    try list_user_ids(env);
    std.debug.print("Sessions:\n", .{});
    try list_sessions(env);
    std.debug.print("Posts:\n", .{});
    try list_posts(env);

    while (true) {
        server.wait();
        while (true) {
            const req = (server.next_request(&req_buffer) catch break) orelse break;
            // handle_request(env, req) catch {
            //     try handle_error(env, req);
            // };
            try handle_request(env, req);
        }
    }
    // const ThreadCount = 1;
    // var ts: [ThreadCount]std.Thread = undefined;

    // for (0..ThreadCount) |i| {
    //     ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
    // }
    // for (0..ThreadCount) |i| {
    //     ts[i].join();
    // }

    std.debug.print("done\n", .{});
}

fn handle_error(env: lmdb.Env, req: http.Request) !void {
    _ = env;
    var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);
    try write_start(&res);
    try res.write("Oops, something went terribly wrong there D:", .{});
    try write_end(&res);
    try res.send();
}
fn handle_request(env: lmdb.Env, req: http.Request) !void {
    // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
    // std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? });

    // reponse
    var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);

    // check session token
    const logged_in: ?Login = try check_login(env, req, &res);

    // html
    if (req.method == .GET) {
        try write_start(&res);
        try write_header(&res, logged_in);

        const txn = try env.txn();
        defer txn.abort();

        const get = GET{
            .txn = txn,
            .req = req,
            .res = &res,
            .logged_in = logged_in,
        };
        if (try get.handle()) {} else {
            try res.redirect("/");
        }

        try write_end(&res);
        try res.send();
    }
    // api
    else {
        const post = POST{
            .env = env,
            .req = req,
            .res = &res,
            .logged_in = logged_in,
        };
        if (try post.handle()) {} else {
            try res.write("<p>[POST] {s}</p>", .{req.target});
        }

        if (!res.has_header("Location")) {
            if (req.get_header("Referer")) |ref| {
                try res.redirect(ref);
            } else {
                try res.redirect("/");
            }
        }
        try res.send();
    }
}
