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

const protocol = @import("eink_feed_protocol");

const models = @import("../../../models/root.zig");
const web = @import("../../root.zig");

const log = std.log.scoped(.ws_display);

var global_context_manager: ContextManager = undefined;

const Context = struct {
    allocator: std.mem.Allocator,

    channel: []const u8,
    client: models.DisplayClient,

    channels: *web.stores.Channels,

    subscribe_args: WebsocketHandler.SubscribeArgs,
    settings: WebsocketHandler.WebSocketSettings,
};

pub fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels) void {
    global_context_manager = ContextManager.init(allocator, channels);
}

pub fn deinit() void {
    global_context_manager.deinit();
}

pub const WebsocketHandler = WebSockets.Handler(Context);

const ContextList = std.ArrayList(*Context);

pub const ContextManager = struct {
    allocator: std.mem.Allocator,
    new_context_lock: std.Thread.Mutex = .{},
    contexts: ContextList = undefined,
    channels: *web.stores.Channels,

    fn init(allocator: std.mem.Allocator, channels: *web.stores.Channels) @This() {
        return .{
            .allocator = allocator,
            .contexts = ContextList.init(allocator),
            .channels = channels,
        };
    }

    pub fn deinit(self: *@This()) void {
        // for (self.contexts.items) |ctx| {
        // }
        self.contexts.deinit();
    }

    pub fn newContext(self: *@This(), channel: []const u8, client: models.DisplayClient) !*Context {
        self.new_context_lock.lock();
        defer self.new_context_lock.unlock();

        const ctx = try self.allocator.create(Context);
        errdefer self.allocator.destroy(ctx);

        ctx.* = .{
            .allocator = self.allocator,
            .channel = channel,
            .client = client,
            .channels = self.channels,
            .subscribe_args = .{
                .channel = channel,
                .context = ctx,
            },
            .settings = .{
                .on_open = onOpenWebsocket,
                .on_close = onCloseWebsocket,
                .on_message = onMessageWebsocket,
                .context = ctx,
            },
        };

        return ctx;
    }
};

fn onOpenWebsocket(context: ?*Context, handle: WebSockets.WsHandle) !void {
    if (context == null) return;
    const ctx = context.?;

    const channel = ctx.channels.get(ctx.client.channel_id) orelse {
        log.err("Error getting channel: {}", .{ctx.client.channel_id});
        return;
    };
    log.info("channel: {s}", .{channel.nameSlice()});

    _ = WebsocketHandler.subscribe(handle, &ctx.subscribe_args) catch |err| {
        log.err("Error opening websocket: {any}", .{err});
        return;
    };
    log.info("New websocket opened ({s})", .{ctx.channel});
}

fn onCloseWebsocket(context: ?*Context, uuid_ws: isize) !void {
    _ = uuid_ws;

    if (context) |ctx| {
        log.info("Websocket closed ({s})", .{ctx.channel});
        global_context_manager.allocator.free(ctx.channel);
        global_context_manager.allocator.destroy(ctx);
    }
}

fn onMessageWebsocket(
    context: ?*Context,
    handle: WebSockets.WsHandle,
    message: []const u8,
    is_text: bool,
) !void {
    _ = handle;
    _ = is_text;

    if (context == null) return;
    const ctx = context.?;

    const msg = protocol.protobuf.ClientMessage.decode(message, ctx.allocator) catch |err| switch (err) {
        error.OutOfMemory => |e| return e,
        else => return,
    };
    defer msg.deinit();

    if (msg.key_update) |ku| {
        if (ku.key_update) |key_update| {
            log.info("Key update ({s}): {s} => {}", .{ ctx.client.nameSlice(), key_update.key_name.getSlice(), key_update.value });
        }
    }
}

fn parseClientName(allocator: std.mem.Allocator, r: zap.Request, len: *usize) !?[models.DisplayClient.NameMaxLen]u8 {
    if (try r.getParamStr(allocator, "name")) |str| {
        defer allocator.free(str);
        if (str.len > models.DisplayClient.NameMaxLen) {
            r.setStatus(.bad_request);
            r.markAsFinished(true);
            return null;
        }

        len.* = str.len;
        var name: [models.DisplayClient.NameMaxLen:0]u8 = undefined;
        std.mem.copyBackwards(u8, &name, str);
        return name;
    }
    r.setStatus(.bad_request);
    r.markAsFinished(true);
    return null;
}

pub fn onUpgrade(r: zap.Request, target_protocol: []const u8) !void {
    if (!std.mem.eql(u8, target_protocol, "websocket")) {
        log.warn("Received illegal protocol: {s}", .{target_protocol});
        r.setStatus(.bad_request);
        return;
    }

    r.parseQuery();
    var name_len: usize = undefined;
    const name = try parseClientName(global_context_manager.allocator, r, &name_len) orelse return;
    log.info("client name: '{s}'", .{name[0..name_len]});

    const client = global_context_manager.channels.clients.get(name[0..name_len]) orelse {
        r.setStatus(.bad_request);
        r.markAsFinished(true);
        return;
    };

    const channel = try std.fmt.allocPrint(global_context_manager.allocator, "{d}", .{client.channel_id});
    var context = global_context_manager.newContext(channel, client) catch |err| {
        log.err("Error creating context: {any}", .{err});
        global_context_manager.allocator.free(channel);
        return;
    };

    WebsocketHandler.upgrade(r.h, &context.settings) catch |err| {
        log.err("Error during websocket upgrade: {any}", .{err});
        global_context_manager.allocator.free(channel);
        return;
    };
}
