const std = @import("std");
const zap = @import("zap");
const zigimg = @import("zigimg");

const msg_queue = @import("../../msg_queue/root.zig");
const ResultArena = @import("../../result.zig").ResultArena;
const models = @import("../../models/root.zig");
const endpoints = @import("root.zig");
const stores = @import("../stores/root.zig");
const web = @import("../root.zig");

const Self = @This();

allocator: std.mem.Allocator,
channels: *stores.Channels,

path: []const u8,
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console,

pub fn init(
    allocator: std.mem.Allocator,
    channels: *stores.Channels,
    path: []const u8,
) Self {
    return .{
        .allocator = allocator,
        .channels = channels,
        .path = path,
    };
}

pub fn get(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    r.setStatus(.method_not_allowed);
    r.markAsFinished(true);

    // endpoints.pathGuard(r, self.path) catch return;

    // r.parseQuery();
    // const id = parseChannelId(r) orelse return;
    // const channel_internal = self.channels.get(id) orelse {
    //     r.setStatus(.no_content);
    //     r.markAsFinished(true);
    //     return;
    // };

    // const channel = server.web.api.models.Channel.fromInternal(channel_internal);
    // const json = try std.json.stringifyAlloc(self.allocator, channel, .{});
    // defer self.allocator.free(json);
    // try r.setContentType(.JSON);
    // try r.sendBody(json);
}

fn orientationFromParams(r: zap.Request) ?web.api.models.Orientation {
    if (r.getParamSlice("orientation")) |str| {
        return web.api.models.Orientation.fromString(str);
    }
    return null;
}

fn parseOrientation(r: zap.Request) ?web.api.models.Orientation {
    if (orientationFromParams(r)) |val| return val;
    r.setStatus(.bad_request);
    r.markAsFinished(true);
    return null;
}

pub fn post(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    const channel_id = endpoints.Channel.parseChannelId(r) orelse return;
    const orientation = parseOrientation(r) orelse return;
    const channel = self.channels.get(channel_id) orelse {
        r.setStatus(.no_content);
        r.markAsFinished(true);
        return;
    };

    try r.parseBody();
    const params = try r.parametersToOwnedList(self.allocator);
    defer params.deinit();

    var file_mimetype: ?[]const u8 = null;
    var file_data: ?[]const u8 = null;
    blk: {
        for (params.items) |kv| {
            if (kv.value) |v| {
                switch (v) {
                    .Hash_Binfile => |*file| {
                        if (file.mimetype == null or file.data == null) {
                            r.setStatus(.bad_request);
                            r.markAsFinished(true);
                            return;
                        }

                        file_mimetype = file.mimetype.?;
                        file_data = file.data.?;
                        break :blk;
                    },
                    .Array_Binfile => {
                        r.setStatus(.bad_request);
                        r.markAsFinished(true);
                        return;
                    },
                    else => {},
                }
            }
        }
    }

    var image: zigimg.ImageUnmanaged = undefined;
    if (file_mimetype != null and file_data != null) {
        if (std.mem.eql(u8, file_mimetype.?, "image/bmp")) {
            var stream_source = std.io.StreamSource{
                .const_buffer = std.io.fixedBufferStream(file_data.?),
            };

            image = zigimg.formats.bmp.BMP.readImage(self.allocator, &stream_source) catch |err| switch (err) {
                error.OutOfMemory => |e| return e,
                else => {
                    r.setStatus(.unsupported_media_type);
                    r.markAsFinished(true);
                    return;
                },
            };
            errdefer image.deinit(self.allocator);

            image.convert(self.allocator, .grayscale8) catch |err| switch (err) {
                error.OutOfMemory => |e| return e,
                else => {
                    r.setStatus(.internal_server_error);
                    r.markAsFinished(true);
                    return;
                },
            };
        } else if (std.mem.eql(u8, file_mimetype.?, "image/png")) {
            var stream_source = std.io.StreamSource{
                .const_buffer = std.io.fixedBufferStream(file_data.?),
            };

            image = zigimg.formats.png.PNG.readImage(self.allocator, &stream_source) catch |err| switch (err) {
                error.OutOfMemory => |e| return e,
                else => {
                    r.setStatus(.unsupported_media_type);
                    r.markAsFinished(true);
                    return;
                },
            };
            errdefer image.deinit(self.allocator);

            image.convert(self.allocator, .grayscale8) catch |err| switch (err) {
                error.OutOfMemory => |e| return e,
                else => {
                    r.setStatus(.internal_server_error);
                    r.markAsFinished(true);
                    return;
                },
            };
        } else {
            r.setStatus(.unsupported_media_type);
            r.markAsFinished(true);
            return;
        }
    } else {
        r.setStatus(.unsupported_media_type);
        r.markAsFinished(true);
        return;
    }

    defer image.deinit(self.allocator);

    if (image.width > std.math.maxInt(u32) or image.height > std.math.maxInt(u32)) {
        r.setStatus(.unsupported_media_type);
        r.markAsFinished(true);
        return;
    }

    var msg = ResultArena(models.FrameUpdateMsg){
        .arena = try self.allocator.create(std.heap.ArenaAllocator),
        .value = undefined,
    };
    errdefer self.allocator.destroy(msg.arena);
    msg.arena.* = std.heap.ArenaAllocator.init(self.allocator);
    errdefer msg.arena.deinit();

    msg.value = .{
        .channel_id = channel.id,
        .display = .{
            .width = @intCast(image.width),
            .height = @intCast(image.height),
            .orientation = orientation.toInternal(),
        },
        .frame = try msg.arena.allocator().dupe(u8, image.rawBytes()),
    };
    self.channels.queue.enqueue(msg);

    r.setStatus(.created);
}

pub fn put(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    r.setStatus(.method_not_allowed);
    r.markAsFinished(true);
}

pub fn delete(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    r.setStatus(.method_not_allowed);
    r.markAsFinished(true);
}

pub fn patch(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    r.setStatus(.method_not_allowed);
    r.markAsFinished(true);
}

pub fn options(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    try r.setHeader("Access-Control-Allow-Origin", "*");
    try r.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS, HEAD");
    r.setStatus(.no_content);
    r.markAsFinished(true);
}

pub fn head(self: *Self, r: zap.Request) !void {
    endpoints.pathGuard(r, self.path) catch return;

    r.setStatus(.no_content);
    r.markAsFinished(true);
}
