local bare = require "util.jid".bare;
local generate_uuid = require "util.uuid".generate;
local jid = require "util.jid";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local st = require "util.stanza";
local get_room_from_jid = module:require "util".get_room_from_jid;
local wrap_async_run = module:require "util".wrap_async_run;
local update_presence_identity = module:require "util".update_presence_identity;
local timer = require "util.timer";
local MUC_NS = "http://jabber.org/protocol/muc";
local expired_status  = "expired";

-- Options
local poltergeist_component
    = module:get_option_string("poltergeist_component", module.host);
-- defaults to 3 min
local poltergeist_timeout
    = module:get_option_string("poltergeist_leave_timeout", 180);
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
    log("error", "Failed to start - unable to get parent hostname");
    return;
end

local parentCtx = module:context(parentHostName);
if parentCtx == nil then
    log("error",
        "Failed to start - unable to get parent context for host: %s",
        tostring(parentHostName));
    return;
end
local token_util = module:require "token/util".new(parentCtx);

-- option to enable/disable token verifications
local disableTokenVerification
    = module:get_option_boolean("disable_polergeist_token_verification", false);

-- table to store all poltergeists we create
local poltergeists = {};
-- table to mark that outgoing unavailable presences
-- should be marked with ignore
local poltergeists_pr_ignore = {};

-- poltergaist management functions

-- Returns the room if available, work and in multidomain mode
-- @param room_name the name of the room
-- @param group name of the group (optional)
-- @return returns room if found or nil
function get_room(room_name, group)
    local room_address = jid.join(room_name, module:get_host());
    -- if there is a group we are in multidomain mode and that group is not
    -- our parent host
    if group and group ~= "" and group ~= parentHostName then
        room_address = "["..group.."]"..room_address;
    end

    return get_room_from_jid(room_address);
end

-- Stores the username in the table where we store poltergeist usernames
-- based on their room names
-- @param room the room instance
-- @param user_id the user id
-- @param username the username to store
function store_username(room, user_id, username)
    local room_name = jid.node(room.jid);

    -- we store in poltergeist user ids for room names
    if (not poltergeists[room_name]) then
        poltergeists[room_name] = {};
    end
    poltergeists[room_name][user_id] = username;
    log("debug", "stored in session: %s", username);
end

-- Retrieve the username for a user
-- @param room the room instance
-- @param user_id the user id
-- @return returns the stored username for user or nil
function get_username(room, user_id)
    local room_name = jid.node(room.jid);

    if (not poltergeists[room_name]) then
        return nil;
    end

    return poltergeists[room_name][user_id];
end

-- Removes poltergeist values from table
-- @param room the room instance
-- @param nick the user nick
function remove_username(room, nick)
    local room_name = jid.node(room.jid);
    if (poltergeists[room_name]) then
        local user_id_to_remove;
        for name,username in pairs(poltergeists[room_name]) do
            if (string.sub(username, 0, 8) == nick) then
                user_id_to_remove = name;
            end
        end
        if (user_id_to_remove) then
            poltergeists[room_name][user_id_to_remove] = nil;
        end
    end
end

-- Provides a new presence stanza for a poltergeist.
-- @param room the room instance
-- @param nick the user nick
function generate_poltergeist_presence(room, nick, status)
    local presence_stanza = st.presence({
        to = room.jid.."/"..nick,
        from = poltergeist_component.."/"..nick,
    }):tag("x", { xmlns = MUC_NS }):up();

    presence_stanza:tag("call_cancel"):text(nil):up();
    presence_stanza:tag("call_id"):text(nil):up();

    if status then
       presence_stanza:tag("status"):text(status):up();
    else
       presence_stanza:tag("status"):text(nil):up();
    end

    return presence_stanza;
end

--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_name, group, session)
    if disableTokenVerification then
        return true;
    end

    -- if not disableTokenVerification and we do not have token
    -- stop here, cause the main virtual host can have guest access enabled
    -- (allowEmptyToken = true) and we will allow access to rooms info without
    -- a token
    if token == nil then
        log("warn", "no token provided");
        return false;
    end

    session.auth_token = token;
    local verified, reason = token_util:process_and_verify_token(session);
    if not verified then
        log("warn", "not a valid token %s", tostring(reason));
        return false;
    end

    local room_address = jid.join(room_name, module:get_host());
    -- if there is a group we are in multidomain mode and that group is not
    -- our parent host
    if group and group ~= "" and group ~= parentHostName then
        room_address = "["..group.."]"..room_address;
    end

    if not token_util:verify_room(session, room_address) then
        log("warn", "Token %s not allowed to join: %s",
            tostring(token), tostring(room_address));
        return false;
    end

    return true;
end

-- if we found that a session for a user with id has a poltergiest already
-- created, retrieve its jid and return it to the authentication
-- so we can reuse it and we that real user will replace the poltergiest
prosody.events.add_handler("pre-jitsi-authentication", function(session)

    if (session.jitsi_meet_context_user) then
        local room = get_room(
            session.jitsi_bosh_query_room,
            session.jitsi_meet_domain);

        if (not room) then
            return nil;
        end

        local username
            = get_username(room, session.jitsi_meet_context_user["id"]);

        if (not username) then
            return nil;
        end

        log("debug", "Found predefined username %s", username);

        -- let's find the room and if the poltergeist occupant is there
        -- lets remove him before the real participant joins
        -- when we see the unavailable presence to go out the server
        -- we will mark it with ignore tag
        local nick = string.sub(username, 0, 8);
        if (have_poltergeist_occupant(room, nick)) then
            -- notify that user connected using the poltergeist
            update_poltergeist_occupant_status(
			   room, nick, "connected");
            remove_poltergeist_occupant(room, nick, true);
        end

        return username;
    end

    return nil;
end);

-- Creates poltergeist occupant
-- @param room the room instance where we create the occupant
-- @param nick the nick to use for the new occupant
-- @param name the display name fot the occupant (optional)
-- @param avatar the avatar to use for the new occupant (optional)
-- @param status the initial status to use for the new occupant (optional)
-- @param context the information that we will store for this poltergeist
function create_poltergeist_occupant(room, nick, name, avatar, status, context)
    log("debug", "create_poltergeist_occupant %s", nick);
    -- Join poltergeist occupant to room, with the invited JID as their nick
    local join_presence = generate_poltergeist_presence(room, nick, status)

    if (name) then
        join_presence:tag(
            "nick",
            { xmlns = "http://jabber.org/protocol/nick" }):text(name):up();
    end
    if (avatar) then
        join_presence:tag("avatar-url"):text(avatar):up();
    end

    -- If the room has a password set, let the poltergeist enter using it
    local room_password = room:get_password();
    if room_password then
        local join = join_presence:get_child("x", MUC_NS);
        join:tag("password", { xmlns = MUC_NS }):text(room_password);
    end

    -- Update the nil call id to the initial call id for call flows.
	local call_id = get_username(room, context.user.id);
    join_presence:maptags(function (tag)
        if tag.name == "call_id" then
           return st.stanza("call_id"):text(call_id);
        end
        return tag;
    end);

    update_presence_identity(
        join_presence,
        context.user,
        context.group,
        context.creator_user,
        context.creator_group
    );

    room:handle_first_presence(
        prosody.hosts[poltergeist_component], join_presence);

    -- the timeout before removing so participants can see the status update
    local removeTimeout = 5;
    local timeout = poltergeist_timeout - removeTimeout;

    timer.add_task(timeout,
        function ()
            update_poltergeist_occupant_status(
                room, nick, expired_status);
            -- and remove it after some time so participant can see
            -- the update
            timer.add_task(removeTimeout,
                function ()
                    if (have_poltergeist_occupant(room, nick)) then
                        remove_poltergeist_occupant(room, nick, false);
                    end
                end);
        end);
end

-- Removes poltergeist occupant
-- @param room the room instance where to remove the occupant
-- @param nick the nick of the occupant to remove
-- @param ignore to mark the poltergeist unavailble presence to be ignored
function remove_poltergeist_occupant(room, nick, ignore)
    log("debug", "remove_poltergeist_occupant %s", nick);
    local leave_presence = st.presence({
        to = room.jid.."/"..nick,
        from = poltergeist_component.."/"..nick,
        type = "unavailable" });
    if (ignore) then
        poltergeists_pr_ignore[room.jid.."/"..nick] = true;
    end
    room:handle_normal_presence(
        prosody.hosts[poltergeist_component], leave_presence);
    remove_username(room, nick);
end

-- Updates poltergeist occupant status
-- @param room the room instance where to remove the occupant
-- @param nick the nick of the occupant to remove
-- @param status the status to update
-- @param call_details is a table of call flow details
function update_poltergeist_occupant_status(room, nick, status, call_details)
    local update_presence = get_presence(room, nick);

    if (not update_presence) then
        -- no presence found for occupant, create one
        update_presence = generate_poltergeist_presence(room, nick)
    else
        -- update occupant presence with appropriate to and from
        -- so we can send it again
        update_presence = st.clone(update_presence);
        update_presence.attr.to = room.jid.."/"..nick;
        update_presence.attr.from = poltergeist_component.."/"..nick;
    end

    update_presence = update_presence_tags(update_presence, status, call_details)

    room:handle_normal_presence(
        prosody.hosts[poltergeist_component], update_presence);
end

-- Updates the status tags and call flow tags of an existing poltergeist's
-- presence.
-- @param presence_stanza is the actual presence stanza for a poltergeist.
-- @param status is the new status to be updated in the stanza.
-- @param call_details is a table of call flow signal information.
function update_presence_tags(presence_stanza, status, call_details)
    local call_cancel = false;
    local call_id = nil;

    -- Extract optional call flow signal information.
    if call_details then
        call_id = call_details["id"];

        if call_details["cancel"] then
            call_cancel = call_details["cancel"];
        end
    end

    presence_stanza:maptags(function (tag)
        if tag.name == "status" then
            if call_cancel then
                -- If call cancel is set then the status should not be changed.
                return tag
            end
            return st.stanza("status"):text(status);
        elseif tag.name == "call_id" then
            if call_id then
                return st.stanza("call_id"):text(call_id);
            else
                -- If no call id is provided the re-use the existing id.
                return tag;
            end
        elseif tag.name == "call_cancel" then
            if call_cancel then
                return st.stanza("call_cancel"):text("true");
            else
                return st.stanza("call_cancel"):text("false");
            end
        end
        return tag;
    end);

    return presence_stanza
end

-- Checks for existance of a poltergeist occupant
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return true if occupant is found, false otherwise
function have_poltergeist_occupant(room, nick)
	-- Find out if we have a poltergeist occupant in the room for this JID
	return not not room:get_occupant_jid(poltergeist_component.."/"..nick);
end

-- Returns the last presence of occupant
-- @param room the room instance where to check for occupant
-- @param nick the nick of the occupant
-- @return presence of the occupant
function get_presence(room, nick)
    local occupant_jid
        = room:get_occupant_jid(poltergeist_component.."/"..nick);
    if (occupant_jid) then
        return room:get_occupant_by_nick(occupant_jid):get_presence();
    end

    return nil;
end

-- Event handlers

--- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
--- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
module:hook("muc-decline", function (event)
	remove_poltergeist_occupant(event.room, bare(event.stanza.attr.from), false);
end, -100);
-- before sending the presence for a poltergeist leaving add ignore tag
-- as poltergeist is leaving just before the real user joins and in the client
-- we ignore this presence to avoid leaving/joining experience and the real
-- user will reuse all currently created UI components for the same nick
module:hook("muc-broadcast-presence", function (event)
    if (bare(event.occupant.jid) == poltergeist_component) then
        if(event.stanza.attr.type == "unavailable"
            and poltergeists_pr_ignore[event.occupant.nick]) then
            event.stanza:tag(
                "ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
            poltergeists_pr_ignore[event.occupant.nick] = nil;
        end
    end
end, -100);

-- cleanup room table after room is destroyed
module:hook("muc-room-destroyed",function(event)
    local room_name = jid.node(event.room.jid);
    if (poltergeists[room_name]) then
        poltergeists[room_name] = nil;
    end
end);

--- Handles request for creating/managing poltergeists
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_create_poltergeist (event)
    if (not event.request.url.query) then
        return 400;
    end

    local params = parse(event.request.url.query);
    local user_id = params["user"];
    local room_name = params["room"];
    local group = params["group"];
    local name = params["name"];
    local avatar = params["avatar"];
    local status = params["status"];
    local session = {};

    if not verify_token(params["token"], room_name, group, session) then
        return 403;
    end

    local room = get_room(room_name, group);
    if (not room) then
        log("error", "no room found %s", room_name);
        return 404;
    end

    local username = get_username(room, user_id);
    if (username ~= nil
        and have_poltergeist_occupant(room, string.sub(username, 0, 8))) then
        log("warn", "poltergeist for username:%s already in the room:%s",
            username, room_name);
        return 202;
    else
        username = generate_uuid();
        store_username(room, user_id, username);
        local context = {
            user = {
                id = user_id;
            };
            group = group;
            creator_user = session.jitsi_meet_context_user;
            creator_group = session.jitsi_meet_context_group;
        };
        create_poltergeist_occupant(
            room, string.sub(username, 0, 8), name, avatar, status, context);
        return 200;
    end
end

--- Handles request for updating poltergeists status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_poltergeist (event)
    if (not event.request.url.query) then
        return 400;
    end

    local params = parse(event.request.url.query);
    local user_id = params["user"];
    local room_name = params["room"];
    local group = params["group"];
    local status = params["status"];
	local call_id = params["callid"];

	local call_cancel = false
	if params["callcancel"] == "true" then
	   call_cancel = true;
	end

    if not verify_token(params["token"], room_name, group, {}) then
        return 403;
    end

    local room = get_room(room_name, group);
    if (not room) then
        log("error", "no room found %s", room_name);
        return 404;
    end

    local username = get_username(room, user_id);
    if (not username) then
        return 404;
    end

	local call_details = {
	   ["cancel"] = call_cancel;
	   ["id"] = call_id;
	};

    local nick = string.sub(username, 0, 8);
    if (have_poltergeist_occupant(room, nick)) then
        update_poltergeist_occupant_status(room, nick, status, call_details);
        return 200;
    else
        return 404;
    end
end

--- Handles remove poltergeists
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_remove_poltergeist (event)
    if (not event.request.url.query) then
        return 400;
    end

    local params = parse(event.request.url.query);
    local user_id = params["user"];
    local room_name = params["room"];
    local group = params["group"];

    if not verify_token(params["token"], room_name, group, {}) then
        return 403;
    end

    local room = get_room(room_name, group);
    if (not room) then
        log("error", "no room found %s", room_name);
        return 404;
    end

    local username = get_username(room, user_id);
    if (not username) then
        return 404;
    end

    local nick = string.sub(username, 0, 8);
    if (have_poltergeist_occupant(room, nick)) then
        remove_poltergeist_occupant(room, nick, false);
        return 200;
    else
        return 404;
    end
end

log("info", "Loading poltergeist service");
module:depends("http");
module:provides("http", {
    default_path = "/";
    name = "poltergeist";
    route = {
        ["GET /poltergeist/create"] = function (event) return wrap_async_run(event,handle_create_poltergeist) end;
        ["GET /poltergeist/update"] = function (event) return wrap_async_run(event,handle_update_poltergeist) end;
        ["GET /poltergeist/remove"] = function (event) return wrap_async_run(event,handle_remove_poltergeist) end;
    };
});
