# frozen_string_literal: true

require "blather"
require "em-http"
require "em_promise"
require "em-synchrony/em-http" # For apost vs post
require "link-header-parser"

module Snikket
	class Launch < Blather::Stanza::Iq
		register nil, "launch", "xmpp:snikket.org/hosting/v1"

		def self.new(
			type=nil, to=nil, id=nil,
			domain: nil, region: nil, av_region: "na"
		)
			stanza = super(type || :set, to, id)
			node = Nokogiri::XML::Node.new("launch", stanza.document)
			node.default_namespace = registered_ns
			stanza << node
			stanza.domain = domain if domain
			stanza.region = region if region
			stanza.av_region = av_region if av_region
			stanza
		end

		def domain=(domain)
			query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("domain", document)
			node.default_namespace = self.class.registered_ns
			node.content = domain
			query << node
		end

		def region=(domain)
			query.at_xpath("./ns:region", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("region", document)
			node.default_namespace = self.class.registered_ns
			node.content = domain
			query << node
		end

		def av_region=(domain)
			query.at_xpath("./ns:av-region", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("av-region", document)
			node.default_namespace = self.class.registered_ns
			node.content = domain
			query << node
		end

		def query
			at_xpath("./ns:launch", ns: self.class.registered_ns)
		end
	end

	class Launched < Blather::Stanza::Iq
		register :snikket_launched, "launched", "xmpp:snikket.org/hosting/v1"

		def instance_id
			query
				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
				&.content
		end

		def bootstrap_token
			query
				.at_xpath("./ns:bootstrap/ns:token", ns: self.class.registered_ns)
				&.content
		end

		def status
			query
				.at_xpath("./ns:status", ns: self.class.registered_ns)
				&.content&.to_sym
		end

		def records
			query
				.xpath("./ns:records/ns:record", ns: self.class.registered_ns)
				&.map(&Record.method(:new))
		end

		def query
			at_xpath("./ns:launched", ns: self.class.registered_ns)
		end
	end

	class Record
		NS = "xmpp:snikket.org/hosting/v1"

		def initialize(element)
			@element = element
		end

		def name
			@element
				.at_xpath("./ns:name", ns: NS)
				&.content
		end

		def type
			@element
				.at_xpath("./ns:type", ns: NS)
				&.content
		end

		def status
			@element
				.at_xpath("./ns:status", ns: NS)
				&.content&.to_sym
		end

		def expected
			@element
				.xpath("./ns:expected/ns:value", ns: NS)
				&.map(&:content)
		end

		def found
			@element
				.xpath("./ns:found/ns:value", ns: NS)
				&.map(&:content)
		end
	end

	class Instance < Blather::Stanza::Iq
		register :snikket_instance, "instance", "xmpp:snikket.org/hosting/v1"

		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
			stanza = super(type || :get, to, id)
			node = Nokogiri::XML::Node.new("instance", stanza.document)
			node.default_namespace = registered_ns
			stanza << node
			stanza.instance_id = instance_id if instance_id
			stanza
		end

		def inherit(*)
			query.remove
			super
		end

		def instance_id=(instance_id)
			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("instance-id", document)
			node.default_namespace = self.class.registered_ns
			node.content = instance_id
			query << node
		end

		def instance_id
			query
				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
				&.content
		end

		def update_needed?
			!!query.at_xpath("./ns:update-needed", ns: self.class.registered_ns)
		end

		def operation
			query.at_xpath("./ns:operation", ns: self.class.registered_ns)
		end

		def status
			query
				.at_xpath("./ns:status", ns: self.class.registered_ns)
				&.content&.to_sym
		end

		def query
			at_xpath("./ns:instance", ns: self.class.registered_ns)
		end
	end

	class Stop < Blather::Stanza::Iq
		register nil, "stop", "xmpp:snikket.org/hosting/v1"

		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
			stanza = super(type || :set, to, id)
			node = Nokogiri::XML::Node.new("stop", stanza.document)
			node.default_namespace = registered_ns
			stanza << node
			stanza.instance_id = instance_id if instance_id
			stanza
		end

		def instance_id=(instance_id)
			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("instance-id", document)
			node.default_namespace = self.class.registered_ns
			node.content = instance_id
			query << node
		end

		def query
			at_xpath("./ns:stop", ns: self.class.registered_ns)
		end
	end

	class Delete < Blather::Stanza::Iq
		register nil, "delete", "xmpp:snikket.org/hosting/v1"

		def self.new(type=nil, to=nil, id=nil, instance_id: nil)
			stanza = super(type || :set, to, id)
			node = Nokogiri::XML::Node.new("delete", stanza.document)
			node.default_namespace = registered_ns
			stanza << node
			stanza.instance_id = instance_id if instance_id
			stanza
		end

		def instance_id=(instance_id)
			query.at_xpath("./ns:instance-id", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("instance-id", document)
			node.default_namespace = self.class.registered_ns
			node.content = instance_id
			query << node
		end

		def query
			at_xpath("./ns:delete", ns: self.class.registered_ns)
		end
	end

	class DomainInfo < Blather::Stanza::Iq
		register nil, "domain-info", "xmpp:snikket.org/hosting/v1"

		def self.new(type=nil, to=nil, id=nil, domain: nil)
			stanza = super(type || :get, to, id)
			if domain
				node = Nokogiri::XML::Node.new("domain-info", stanza.document)
				node.default_namespace = registered_ns
				stanza << node
			end
			stanza.domain = domain if domain
			stanza
		end

		def domain=(domain)
			query.at_xpath("./ns:domain", ns: self.class.registered_ns)&.remove
			node = Nokogiri::XML::Node.new("domain", document)
			node.default_namespace = self.class.registered_ns
			node.content = domain
			query << node
		end

		def instance_id
			query
				.at_xpath("./ns:instance-id", ns: self.class.registered_ns)
				&.content
		end

		def available?
			!!query.at_xpath("./ns:available", ns: self.class.registered_ns)
		end

		def custom?
			!!query.at_xpath("./ns:custom", ns: self.class.registered_ns)
		end

		def query
			at_xpath("./ns:domain-info", ns: self.class.registered_ns)
		end
	end

	class CustomerInstance
		def self.for(customer, domain, launched)
			new(
				instance_id: launched.instance_id,
				bootstrap_token: launched.bootstrap_token || "",
				customer_id: customer.customer_id,
				domain: domain
			)
		end

		value_semantics do
			instance_id     String
			bootstrap_token String
			customer_id     String
			domain          String
		end

		def bootstrap_uri
			"https://#{domain}/invites_bootstrap?token=#{bootstrap_token}"
		end

		def fetch_invite
			url = bootstrap_uri
			EM::HttpRequest.new(
				url, tls: { verify_peer: true }
			).ahead(redirects: 5).then { |res|
				LinkHeaderParser.parse(
					Array(res.response_header["LINK"]), base: url
				).group_by_relation_type[:alternate]&.find do |header|
					URI.parse(header.target_uri).scheme == "xmpp"
				end&.target_uri
			}.catch { nil }
		end
	end

	class Repo
		def initialize(db: LazyObject.new { DB })
			@db = db
		end

		def find_by_customer(customer)
			promise = @db.query_defer(<<~SQL, [customer.customer_id])
				SELECT instance_id, bootstrap_token, customer_id, domain
				FROM snikket_instances
				WHERE customer_id=$1
			SQL
			promise.then do |rows|
				rows.map { |row| CustomerInstance.new(**row.transform_keys(&:to_sym)) }
			end
		end

		def del(instance)
			return EMPromise.resolve(nil) unless instance

			params = [instance.instance_id, instance.customer_id, instance.domain]

			IQ_MANAGER.write(Delete.new(
				nil, CONFIG[:snikket_hosting_api], instance_id: instance.instance_id
			)).then do
				@db.exec_defer(<<~SQL, params)
					DELETE FROM snikket_instances
					WHERE instance_id=$1 AND customer_id=$2 AND domain=$3
				SQL
			end
		end

		def put(instance)
			params = [
				instance.instance_id, instance.bootstrap_token,
				instance.customer_id, instance.domain
			]
			@db.exec_defer(<<~SQL, params)
				INSERT INTO snikket_instances
					(instance_id, bootstrap_token, customer_id, domain)
				VALUES
					($1, $2, $3, $4)
				ON CONFLICT (instance_id)
				DO UPDATE SET bootstrap_token=$2
			SQL
		end
	end
end
