local st = require "util.stanza";
local http = require "net.http";
local jid = require "util.jid";
local json = require "util.json";
local util = module:require "util";
local is_admin = util.is_admin;

local ROLE_JOINING = module:shared("politictalk/roles/joining");
local ROOM_INACTIVE_CALLBACK_URL = module:get_option_string(
    "politictalk_room_inactive_callback_url",
    os.getenv("POLITICTALK_ROOM_INACTIVE_CALLBACK_URL")
);
local ROOM_INACTIVE_CALLBACK_SECRET = module:get_option_string(
    "politictalk_room_inactive_callback_secret",
    os.getenv("POLITICTALK_ROOM_INACTIVE_CALLBACK_SECRET")
);

module:log("info", "Loaded PoliticTalk JWT role enforcement");

local function get_user(session)
    return session and session.jitsi_meet_context_user or nil;
end

local function get_user_id(session)
    local user = get_user(session);
    return user and user.id or nil;
end

local function is_moderator_session(session)
    local user = get_user(session);
    local moderator = user and user.moderator;

    return moderator == true or moderator == "true";
end

local function table_contains(values, value)
    if not values or not value then
        return false;
    end

    for _, current in ipairs(values) do
        if current == value then
            return true;
        end
    end

    return false;
end

local function add_unique(values, value)
    if not value then
        return values or {};
    end

    values = values or {};

    if not table_contains(values, value) then
        table.insert(values, value);
    end

    return values;
end

local function remove_value(values, value)
    if not values or not value then
        return values or {};
    end

    local filtered = {};

    for _, current in ipairs(values) do
        if current ~= value then
            table.insert(filtered, current);
        end
    end

    return filtered;
end

local function ensure_room_data(room)
    if not room then
        return false;
    end

    room._data = room._data or {};
    room._data.moderators = room._data.moderators or {};
    room._data.participants = room._data.participants or {};
    room._data.politictalk_host_jids = room._data.politictalk_host_jids or {};
    room._data.politictalk_jid_user_ids = room._data.politictalk_jid_user_ids or {};
    -- Enables Jitsi AV moderation automatically: hosts can speak, participants
    -- must be explicitly allowed by a host before they can unmute.
    room._data.av_can_unmute = false;

    return true;
end

local function update_room_role_lists(room, user_id, is_moderator)
    if not room or not user_id then
        return;
    end

    ensure_room_data(room);

    if is_moderator then
        room._data.moderators = add_unique(room._data.moderators, user_id);
        room._data.participants = remove_value(room._data.participants, user_id);
    else
        room._data.participants = add_unique(room._data.participants, user_id);
        room._data.moderators = remove_value(room._data.moderators, user_id);
    end

    module:fire_event("room-metadata-changed", { room = room; });
end

local function mark_host(room, occupant, user_id)
    if not room or not occupant or not occupant.bare_jid or not user_id then
        return;
    end

    ensure_room_data(room);
    room._data.politictalk_host_jids = add_unique(room._data.politictalk_host_jids, occupant.bare_jid);
    room._data.politictalk_jid_user_ids[occupant.bare_jid] = user_id;
end

local function mark_participant(room, occupant, user_id)
    if not room or not occupant or not occupant.bare_jid or not user_id then
        return;
    end

    ensure_room_data(room);
    room._data.politictalk_jid_user_ids[occupant.bare_jid] = user_id;
end

local function unmark_occupant(room, occupant)
    if not room or not occupant or not occupant.bare_jid then
        return nil;
    end

    ensure_room_data(room);

    local user_id = room._data.politictalk_jid_user_ids[occupant.bare_jid];
    room._data.politictalk_jid_user_ids[occupant.bare_jid] = nil;
    room._data.politictalk_host_jids = remove_value(room._data.politictalk_host_jids, occupant.bare_jid);

    return user_id;
end

local function has_active_host(room)
    if not room then
        return false;
    end

    ensure_room_data(room);

    for _, occupant in room:each_occupant() do
        if table_contains(room._data.politictalk_host_jids, occupant.bare_jid) then
            return true;
        end
    end

    return false;
end

local function deny_join(event, reason)
    if event.origin and event.stanza then
        event.origin.send(st.error_reply(event.stanza, "cancel", "not-allowed", reason));
    end

    return true;
end

local function get_room_meeting_code(room)
    if not room or not room.jid then
        return nil;
    end

    return jid.split(room.jid);
end

local function notify_room_inactive(room, reason)
    if
        not ROOM_INACTIVE_CALLBACK_URL
        or ROOM_INACTIVE_CALLBACK_URL == ""
        or not ROOM_INACTIVE_CALLBACK_SECRET
        or ROOM_INACTIVE_CALLBACK_SECRET == ""
    then
        module:log("warn", "Skipping PoliticTalk inactive-room callback because callback URL or secret is missing");
        return;
    end

    local meeting_code = get_room_meeting_code(room);
    if not meeting_code then
        module:log("warn", "Skipping PoliticTalk inactive-room callback because room meeting code is missing");
        return;
    end

    ensure_room_data(room);
    if room._data.politictalk_inactive_notified then
        return;
    end
    room._data.politictalk_inactive_notified = true;

    http.request(ROOM_INACTIVE_CALLBACK_URL, {
        method = "POST";
        headers = {
            ["Content-Type"] = "application/json";
            ["X-PoliticTalk-Jitsi-Secret"] = ROOM_INACTIVE_CALLBACK_SECRET;
        };
        body = json.encode({
            meetingCode = meeting_code;
            reason = reason or "host_left";
            roomJid = room.jid;
        });
    }, function(_, code)
        if code and code >= 200 and code < 300 then
            module:log("info", "Marked PoliticTalk room inactive in PgApi: %s", tostring(meeting_code));
        else
            module:log(
                "warn",
                "Failed to mark PoliticTalk room inactive in PgApi: room=%s status=%s",
                tostring(meeting_code),
                tostring(code)
            );
        end
    end);
end

module:hook("muc-room-pre-create", function(event)
    if event.stanza and event.stanza.attr and is_admin(event.stanza.attr.from) then
        return;
    end

    if is_moderator_session(event.origin) then
        return;
    end

    module:log("warn", "Blocking non-host from creating PoliticTalk room: %s", tostring(event.stanza and event.stanza.attr.to));
    return deny_join(event, "PoliticTalk room is waiting for the host");
end, 90);

module:hook("muc-occupant-pre-join", function(event)
    local session = event.origin;
    local room = event.room;
    local occupant = event.occupant;
    local user_id = get_user_id(session);
    local is_moderator = is_moderator_session(session);
    local occupant_jid = occupant and occupant.bare_jid;

    if occupant_jid and is_admin(occupant_jid) then
        return;
    end

    if not occupant_jid or not user_id then
        module:log("warn", "Blocking participant without PoliticTalk JWT user id from room: %s", tostring(room and room.jid));
        return deny_join(event, "PoliticTalk identity is required");
    end

    if not is_moderator and not has_active_host(room) then
        module:log("warn", "Blocking participant %s from room without host: %s", tostring(user_id), tostring(room and room.jid));
        return deny_join(event, "PoliticTalk room is waiting for the host");
    end

    ROLE_JOINING[occupant_jid] = {
        is_moderator = is_moderator;
        user_id = user_id;
    };

    update_room_role_lists(room, user_id, is_moderator);
end, 80);

module:hook("muc-occupant-joined", function(event)
    local room = event.room;
    local occupant = event.occupant;
    local occupant_jid = occupant and occupant.bare_jid;
    local role = occupant_jid and ROLE_JOINING[occupant_jid] or nil;

    if occupant_jid then
        ROLE_JOINING[occupant_jid] = nil;
    end

    if not role then
        return;
    end

    if role.is_moderator then
        mark_host(room, occupant, role.user_id);
        update_room_role_lists(room, role.user_id, true);
        room:set_affiliation(true, occupant.bare_jid, "owner");
        module:log(
            "info",
            "PoliticTalk host joined as moderator with AV moderation: %s room=%s",
            tostring(role.user_id),
            tostring(room.jid)
        );
    else
        mark_participant(room, occupant, role.user_id);
        update_room_role_lists(room, role.user_id, false);
        room:set_affiliation(true, occupant.bare_jid, "member");
        module:log("info", "PoliticTalk participant joined as member: %s room=%s", tostring(role.user_id), tostring(room.jid));
    end
end, 1);

module:hook("muc-occupant-left", function(event)
    local room = event.room;
    local occupant = event.occupant;
    local was_host =
        room and occupant and room._data and table_contains(room._data.politictalk_host_jids, occupant.bare_jid);
    local user_id = unmark_occupant(room, occupant) or get_user_id(event.origin);

    if room and user_id then
        ensure_room_data(room);
        room._data.moderators = remove_value(room._data.moderators, user_id);
        room._data.participants = remove_value(room._data.participants, user_id);
    end

    if was_host and not has_active_host(room) then
        module:log("info", "Destroying PoliticTalk room after host left: %s", tostring(room.jid));
        notify_room_inactive(room, "host_left");
        room:destroy(nil, "The host has left the PoliticTalk room");
    end
end, -20);

module:hook("muc-room-destroyed", function(event)
    local room = event.room;

    if room and room._data and room._data.politictalk_host_jids then
        notify_room_inactive(room, "room_destroyed");
    end
end, -30);
