const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = jetzig.jetquery;

const Zmd = @import("zmd").Zmd;
const tokens = @import("zmd").tokens;
const Node = @import("zmd").Node;

pub const layout = "panel";

pub const Tag = struct {
    id: i32,
    name: []const u8,
    emoji: ?[]const u8,
    created_at: jetzig.jetquery.DateTime,
    updated_at: jetzig.jetquery.DateTime,
};

pub const Blog = struct {
    id: i32,
    title: []const u8,
    content: ?[]const u8,
    blob: []const u8,
    created_at: jetzig.jetquery.DateTime,
    updated_at: jetzig.jetquery.DateTime,
    tags: ?[]const Tag,
};

pub fn fetchBlogs(allocator: std.mem.Allocator, req: *jetzig.Request) ![]Blog {
    const tag_query = jetzig.database.Query(.Tag)
        .select(.{})
        .include(.blog_tags, .{});

    var blogs: std.AutoArrayHashMapUnmanaged(i32, Blog) = .empty;
    const tags = try req.repo.all(tag_query);

    for (tags) |tag| {
        for (tag.blog_tags) |blog_tag| {
            // fetch blog
            const blog = try fetchBlog(allocator, req, blog_tag.blog_id);
            try blogs.put(allocator, blog.id, blog);
        }
    }

    return blogs.values();
}

pub fn fetchBlog(allocator: std.mem.Allocator, req: *jetzig.Request, id: i32) !Blog {
    const query = jetzig.database.Query(.Blog).find(id);
    const blog = try req.repo.execute(query) orelse return error.BlogNotFound;

    return .{
        .id = blog.id,
        .title = blog.title,
        .content = blog.content,
        .blob = blog.blob,
        .created_at = blog.created_at,
        .updated_at = blog.updated_at,
        .tags = blk: {
            const blog_tag_query = jetzig.database.Query(.BlogTag)
                .findBy(.{ .blog_id = blog.id });
            if (try req.repo.execute(blog_tag_query)) |blog_tag| {
                var found: std.ArrayList(Tag) = .init(allocator);

                const tag_query = jetzig.database.Query(.Tag)
                    .where(.{ .id = blog_tag.tag_id });

                const tags = try req.repo.all(tag_query);

                for (tags) |tag| {
                    try found.append(.{
                        .id = tag.id,
                        .name = tag.name,
                        .emoji = tag.emoji,
                        .created_at = tag.created_at,
                        .updated_at = tag.updated_at,
                    });
                }

                break :blk try found.toOwnedSlice();
            } else {
                break :blk null;
            }
        },
    };
}

pub fn fetchTag(id: i32, req: *jetzig.Request) !Tag {
    const query = jetzig.database.Query(.Tag)
        .find(id);
    const tag = try req.repo.execute(query) orelse return error.TagNotFound;

    return .{
        .id = tag.id,
        .name = tag.name,
        .emoji = tag.emoji,
        .created_at = tag.created_at,
        .updated_at = tag.updated_at,
    };
}

pub fn index(request: *jetzig.Request) !jetzig.View {
    var root = try request.data(.object);

    const blogs = try fetchBlogs(request.allocator, request);

    try root.put("blogs", blogs);

    const cookies = try request.cookies();
    const allowed = blk: {
        const session = cookies.get("session") orelse break :blk false;

        const session_query = jetzig.database.Query(.Session)
            .findBy(.{ .session_id = session.value });
        _ = request.repo.execute(session_query) catch break :blk false;
        break :blk true;
    };

    try root.put("allowed", allowed);

    return request.render(.ok);
}

pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
    var zmd = Zmd.init(request.allocator);
    defer zmd.deinit();

    var blog = try fetchBlog(
        request.allocator,
        request,
        try std.fmt.parseInt(i32, id, 10),
    );

    try zmd.parse(blog.content orelse {
        return request.fail(.unprocessable_entity);
    });

    const blob = try zmd.toHtml(@import("zmd").html.DefaultFragments);

    blog.content = blob;

    var root = try request.data(.object);
    try root.put("blog", blog);

    const cookies = try request.cookies();
    const allowed = blk: {
        const session = cookies.get("session") orelse break :blk false;

        const session_query = jetzig.database.Query(.Session)
            .findBy(.{ .session_id = session.value });
        _ = request.repo.execute(session_query) catch break :blk false;
        break :blk true;
    };

    try root.put("allowed", allowed);
    try root.put("description", blog.blob);

    try request.process();
    return request.render(.ok);
}

pub fn new(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
    const cookies = try request.cookies();
    const allowed = blk: {
        const session = cookies.get("session") orelse break :blk false;

        const session_query = jetzig.database.Query(.Session)
            .findBy(.{ .session_id = session.value });
        _ = request.repo.execute(session_query) catch break :blk false;
        break :blk true;
    };

    const root = try data.object();
    try root.put("allowed", allowed);
    return request.render(.ok);
}

pub fn post(request: *jetzig.Request) !jetzig.View {
    // check "session" cookie for authentication
    const cookies = try request.cookies();
    const session = cookies.get("session") orelse {
        return request.fail(.not_found);
    };

    // check if session is valid
    const query = jetzig.database.Query(.Session)
        .findBy(.{ .session_id = session.value });

    if (try request.repo.execute(query)) |_| {
        const params = try request.params();

        const title = params.getT(.string, "title") orelse {
            return request.fail(.unprocessable_entity);
        };

        const content = params.getT(.string, "content") orelse {
            return request.fail(.unprocessable_entity);
        };

        const preview = params.getT(.string, "preview") orelse {
            return request.fail(.unprocessable_entity);
        };

        const tag_str = params.getT(.string, "tags") orelse {
            return request.fail(.unprocessable_entity);
        };

        try request.repo.insert(.Blog, .{
            .title = title,
            .blob = preview,
            .content = content,
        });

        var tag_names = std.mem.splitScalar(u8, tag_str, ',');
        while (tag_names.next()) |tag_name| {
            // check if tag exists
            const tag_query = jetzig.database.Query(.Tag)
                .findBy(.{ .name = tag_name });

            const tag = blk: {
                break :blk try request.repo.execute(tag_query) orelse {
                    // tag does not exist, create it
                    try request.repo.insert(.Tag, .{ .name = tag_name });
                    break :blk try request.repo.execute(tag_query) orelse {
                        return request.fail(.unprocessable_entity);
                    };
                };
            };

            try request.repo.insert(.BlogTag, .{
                .tag_id = tag.id,
                .blog_id = blk: {
                    const blog_query = jetzig.database.Query(.Blog)
                        .findBy(.{ .title = title });
                    if (try request.repo.execute(blog_query)) |blog| {
                        break :blk blog.id;
                    } else {
                        return request.fail(.not_found);
                    }
                },
            });
        }

        return request.redirect("/blogs", .moved_permanently);
    }

    // session is not valid
    return request.fail(.not_found);
}

pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
    // check "session" cookie for authentication
    const cookies = try request.cookies();
    const session = cookies.get("session") orelse {
        return request.fail(.not_found);
    };

    // check if session is valid

    const query = jetzig.database.Query(.Session)
        .findBy(.{ .session_id = session.value });

    if (try request.repo.execute(query)) |_| {
        const delete_query = jetzig.database.Query(.Blog)
            .find(try std.fmt.parseInt(i32, id, 10));

        if (try request.repo.execute(delete_query)) |blog_post| {
            try request.repo.delete(blog_post);
        }

        return request.redirect("/blogs", .moved_permanently);
    } else {
        return request.fail(.not_found);
    }
}

// blogs/:id/edit
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
    // check "session" cookie for authentication
    const cookies = try request.cookies();
    const session = cookies.get("session") orelse {
        return request.fail(.not_found);
    };

    const session_query = jetzig.database.Query(.Session)
        .findBy(.{ .session_id = session.value });

    _ = request.repo.execute(session_query) catch {
        return request.fail(.not_found);
    };

    switch (request.method) {
        .GET => {
            // get blog
            const blog_query = jetzig.database.Query(.Blog)
                .find(try std.fmt.parseInt(i32, id, 10));

            const blog = try request.repo.execute(blog_query) orelse {
                return request.fail(.not_found);
            };

            const root = try request.data(.object);
            try root.put("allowed", true);

            try root.put("blog_tags", blk: {
                var tagstring: std.ArrayList(u8) = .init(request.allocator);
                errdefer tagstring.deinit();

                const blog_tag_query = jetzig.database.Query(.BlogTag)
                    .where(.{ .blog_id = blog.id });

                const blog_tags = try request.repo.all(blog_tag_query);

                for (blog_tags) |blog_tag| {
                    const tag_query = jetzig.database.Query(.Tag)
                        .find(blog_tag.tag_id);

                    const tag = try request.repo.execute(tag_query) orelse {
                        return request.fail(.not_found);
                    };

                    try tagstring.appendSlice(tag.name);
                    try tagstring.append(',');
                }

                break :blk tagstring.toOwnedSlice();
            });
            try root.put("blog", blog);

            return request.render(.ok);
        },
        .POST => {
            // make two queries for now
            const params = try request.params();

            const blog_query_original = jetzig.database.Query(.Blog)
                .find(try std.fmt.parseInt(i32, id, 10));

            if (try request.repo.execute(blog_query_original)) |blog| {
                const title = params.getT(.string, "title") orelse blog.title;
                const content = params.getT(.string, "content") orelse blog.content;
                const preview = params.getT(.string, "preview") orelse blog.blob;
                const tags = params.getT(.string, "tags") orelse "";

                // query the blogpost first
                const blog_query = jetzig.database.Query(.Blog)
                    .update(.{ .title = title, .blob = preview, .content = content })
                    .where(.{ .id = try std.fmt.parseInt(i32, id, 10) });

                const prune = jetzig.database.Query(.BlogTag)
                    .delete()
                    .where(.{ .blog_id = blog.id });

                try request.repo.execute(prune);

                var tag_names = std.mem.splitScalar(u8, tags, ',');
                while (tag_names.next()) |tag_name| {
                    const tag_query = jetzig.database.Query(.Tag)
                        .findBy(.{ .name = tag_name });

                    const tag = blk: {
                        break :blk try request.repo.execute(tag_query) orelse {
                            // tag does not exist, create it
                            try request.repo.insert(.Tag, .{ .name = tag_name });
                            break :blk try request.repo.execute(tag_query) orelse {
                                return request.fail(.unprocessable_entity);
                            };
                        };
                    };

                    // insert the tag
                    try request.repo.insert(.BlogTag, .{
                        .tag_id = tag.id,
                        .blog_id = blog.id,
                    });
                }

                // update the blogpost
                try request.repo.execute(blog_query);

                return request.redirect("/blogs", .moved_permanently);
            } else {
                return request.fail(.not_found);
            }
        },
        else => return request.fail(.not_found),
    }
}

test "index" {
    var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
    defer app.deinit();

    const response = try app.request(.GET, "/blogs", .{});
    try response.expectStatus(.ok);
}

test "get" {
    var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
    defer app.deinit();

    const response = try app.request(.GET, "/blogs/example-id", .{});
    try response.expectStatus(.ok);
}

test "new" {
    var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
    defer app.deinit();

    const response = try app.request(.GET, "/blogs/new", .{});
    try response.expectStatus(.ok);
}

test "post" {
    var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
    defer app.deinit();

    const response = try app.request(.POST, "/blogs", .{});
    try response.expectStatus(.created);
}
