local pubsub = require "util.pubsub";
local st = require "util.stanza";
local jid_bare = require "util.jid".bare;
local usermanager = require "core.usermanager";
local new_id = require "util.id".medium;
local storagemanager = require "core.storagemanager";

local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";

local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
local pubsub_disco_name = module:get_option_string("name", "Prosody PubSub Service");
local expose_publisher = module:get_option_boolean("expose_publisher", false)

local service;

local lib_pubsub = module:require "pubsub";

module:depends("disco");
module:add_identity("pubsub", "service", pubsub_disco_name);
module:add_feature("http://jabber.org/protocol/pubsub");

function handle_pubsub_iq(event)
	return lib_pubsub.handle_pubsub_iq(event, service);
end

-- An itemstore supports the following methods:
--   items(): iterator over (id, item)
--   get(id): return item with id
--   set(id, item): set id to item
--   clear(): clear all items
--   resize(n): set new limit and trim oldest items
--   tail(): return the latest item

-- A nodestore supports the following methods:
--   set(node_name, node_data)
--   get(node_name)
--   users(): iterator over (node_name)


local node_store = module:open_store(module.name.."_nodes");

local function create_simple_itemstore(node_config, node_name)
	local driver = storagemanager.get_driver(module.host, "pubsub_data");
	local archive = driver:open("pubsub_"..node_name, "archive");
	return lib_pubsub.archive_itemstore(archive, node_config, nil, node_name);
end

function simple_broadcast(kind, node, jids, item, actor, node_obj)
	if node_obj then
		if node_obj.config["notify_"..kind] == false then
			return;
		end
	end
	if kind == "retract" then
		kind = "items"; -- XEP-0060 signals retraction in an <items> container
	end

	if item then
		item = st.clone(item);
		item.attr.xmlns = nil; -- Clear the pubsub namespace
		if kind == "items" then
			if node_obj and node_obj.config.include_payload == false then
				item:maptags(function () return nil; end);
			end
			if expose_publisher and actor then
				item.attr.publisher = actor
			end
		end
	end

	local id = new_id();
	local msg_type = node_obj and node_obj.config.notification_type or "headline";
	local message = st.message({ from = module.host, type = msg_type, id = id })
		:tag("event", { xmlns = xmlns_pubsub_event })
			:tag(kind, { node = node })

	if item then
		message:add_child(item);
	end

	local summary;
	-- Compose a sensible textual representation of at least Atom payloads
	if item and item.tags[1] then
		local payload = item.tags[1];
		summary = module:fire_event("pubsub-summary/"..payload.attr.xmlns, {
			kind = kind, node = node, jids = jids, actor = actor, item = item, payload = payload,
		});
	end

	for jid, options in pairs(jids) do
		local new_stanza = st.clone(message);
		if summary and type(options) == "table" and options["pubsub#include_body"] then
			new_stanza:body(summary);
		end
		new_stanza.attr.to = jid;
		module:send(new_stanza);
	end
end

local max_max_items = module:get_option_number("pubsub_max_items", 256);
function check_node_config(node, actor, new_config) -- luacheck: ignore 212/actor 212/node
	if (new_config["max_items"] or 1) > max_max_items then
		return false;
	end
	if new_config["access_model"] ~= "whitelist" and new_config["access_model"] ~= "open" then
		return false;
	end
	return true;
end

function is_item_stanza(item)
	return st.is_stanza(item) and item.attr.xmlns == xmlns_pubsub and item.name == "item" and #item.tags == 1;
end

module:hook("pubsub-summary/http://www.w3.org/2005/Atom", function (event)
	local payload = event.payload;
	local title = payload:get_child_text("title");
	local summary = payload:get_child_text("summary");
	if not summary and title then
		local author = payload:find("author/name#");
		summary = title;
		if author then
			summary = author .. " posted " .. summary;
		end
	end
	return summary;
end, -1);

module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);

local function add_disco_features_from_service(service) --luacheck: ignore 431/service
	for feature in lib_pubsub.get_feature_set(service) do
		module:add_feature(xmlns_pubsub.."#"..feature);
	end
end

module:hook("host-disco-info-node", function (event)
	return lib_pubsub.handle_disco_info_node(event, service);
end);

module:hook("host-disco-items-node", function (event)
	return lib_pubsub.handle_disco_items_node(event, service);
end);


module:hook("host-disco-items", function (event)
	local stanza, reply = event.stanza, event.reply;
	local ok, ret = service:get_nodes(stanza.attr.from);
	if not ok then
		return;
	end
	for node, node_obj in pairs(ret) do
		reply:tag("item", { jid = module.host, node = node, name = node_obj.config.title }):up();
	end
end);

local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
local function get_affiliation(jid)
	local bare_jid = jid_bare(jid);
	if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
		return admin_aff;
	end
end

function get_service()
	return service;
end

function set_service(new_service)
	service = new_service;
	module.environment.service = service;
	add_disco_features_from_service(service);
end

function module.save()
	return { service = service };
end

function module.restore(data)
	set_service(data.service);
end

function module.load()
	if module.reloading then return; end

	set_service(pubsub.new({
		autocreate_on_publish = autocreate_on_publish;
		autocreate_on_subscribe = autocreate_on_subscribe;

		nodestore = node_store;
		itemstore = create_simple_itemstore;
		broadcaster = simple_broadcast;
		itemcheck = is_item_stanza;
		check_node_config = check_node_config;
		get_affiliation = get_affiliation;

		normalize_jid = jid_bare;
	}));
end
