const std = @import("std");
const httpz = @import(".deps/http.zig/src/httpz.zig");
const models = @import("db/models.zig");
const ztime = @import(".deps/time.zig");

const utils = @import("utils.zig");

const budget = @import("routes/budget.zig");
const auth = @import("routes/auth.zig");
const user = @import("routes/user.zig");
const trans = @import("routes/transactions.zig");
const dash = @import("routes/dashboard.zig");
const note = @import("routes/shared_note.zig");

const Db = @import("db/db.zig").Db;

var db: ?Db = null;

pub fn getDb() *Db {
    return &db.?;
}

pub fn startHttpServer() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    // db = try Db.init(allocator, null);
    // defer db.deinit();
    var args = try std.process.argsWithAllocator(allocator);

    // skip program name
    _ = args.skip();
    while (args.next()) |arg| {
        if (std.mem.eql(u8, arg, "--db_path")) {
            const path = args.next();
            // std.debug.print("Got path: {any}", .{path});
            if (path) |db_path| {
                db = try Db.init(allocator, db_path, null);
            } else {
                std.log.err("Db path not provided after arg", .{});
                return;
            }
        }
        if (std.mem.eql(u8, arg, "--make-migration")) {
            if (db == null) {
                std.log.err("Cannot migrate, provide db path first", .{});
                return;
            }
            try db.?.wipeAndMigrateDb();
        }
    }

    if (db == null) {
        db = try Db.init(allocator, null, null);
    }

    defer db.?.deinit();

    var server = try httpz.Server().init(allocator, .{ .port = 8081 });

    // overwrite the default notFound handler
    server.notFound(notFound);

    // overwrite the default error handler
    server.errorHandler(errorHandler);

    var router = server.router();

    router.post("/auth/login", user.login);
    router.post("/auth/signup", user.signup);

    // router.get("/user/:id", user.getUser);
    router.put("/user", user.putUser);
    // router.delete("/user/:id", user.deleteUser);

    router.get("/shared_note/:limit", note.getSharedNotes);
    router.put("/shared_note", note.putSharedNote);
    router.post("/shared_note", note.postSharedNote);

    // router.get("/budget/:id", budget.getBudget);
    // router.put("/budget", budget.putBudget);
    // router.post("/budget", budget.postBudget);

    router.post("/budget_category", budget.postBudgetCategory);
    router.put("/budget_category", budget.putBudgetCategory);
    router.delete("/budget_category", budget.deleteBudgetCategory);

    router.post("/transaction", trans.postTransaction);
    router.put("/transaction", trans.putTransaction);
    router.delete("/transaction", trans.deleteTransaction);

    router.get("/dashboard", dash.getDashboard);

    std.debug.print("Starting http server listening on port {}\n", .{8081});
    // start the server in the current thread, blocking.
    try server.listen();
}

fn notFound(_: *httpz.Request, res: *httpz.Response) !void {
    res.status = 404;

    // you can set the body directly to a []u8, but note that the memory
    // must be valid beyond your handler. Use the res.arena if you need to allocate
    // memory for the body.
    try res.json(
        .{ .success = false, .message = "Not Found" },
        .{},
    );
}

// note that the error handler return `void` and not `!void`
fn errorHandler(req: *httpz.Request, res: *httpz.Response, err: anyerror) void {
    res.status = 500;
    res.body = "Internal Server Error";
    std.log.warn("httpz: unhandled exception for request: {s}\nErr: {}", .{ req.url.raw, err });
}

pub fn returnError(message: ?[]const u8, comptime statusCode: u16, res: *httpz.Response) void {
    comptime {
        if (statusCode > 500 or statusCode < 200) {
            @compileError("Failed responses must have status codes between 200 and 500");
        }
    }
    res.status = statusCode;
    std.log.info("Returning error", .{});
    return res.json(.{ .success = false, .message = message }, .{}) catch |err| {
        std.log.warn("Couldnt create error body: {}", .{err});
        res.body = "{ \"success\": false }";
    };
}

pub fn returnData(data: anytype, res: *httpz.Response) !void {
    const body = utils.structConcatFields(data, .{ .success = true });
    res.status = 200;
    try res.json(body, .{});
}

const ReqDataError = error{
    MalformedBody,
    NoData,
};

pub fn getReqJson(req: *httpz.Request, res: *httpz.Response, comptime ReqType: type) ReqDataError!ReqType {
    const body_data = req.json(ReqType) catch |err| {
        std.debug.print("Malformed body: {any}\nExpected {any} for req {s} on {s}", .{ err, ReqType, @tagName(req.method), req.url.query });
        returnError("Bad request: Malformed Body", 400, res);
        return ReqDataError.MalformedBody;
    };
    if (body_data == null) {
        std.debug.print("Bad request, no data\nExpected {any} for req {s} on {s}", .{ ReqType, @tagName(req.method), req.url.query });
        returnError("Bad request: No Data", 400, res);
        return ReqDataError.NoData;
    }
    var body = body_data.?;
    return body;
}
