#!/usr/bin/ruby
# frozen_string_literal: true

require "blather/client/dsl"
require "dhall"
require "em-hiredis"
require "em-http"
require "em_promise"
require "json"
require "ougai"
require "sentry-ruby"

require_relative "lib/addresses"
require_relative "lib/blather_client"
require_relative "lib/em"
require_relative "lib/event_emitter"
require_relative "lib/incoming_mms"
require_relative "lib/oob"
require_relative "lib/outgoing_mms"
require_relative "lib/proxied_jid"
require_relative "lib/registration_repo"
Dir["#{File.dirname(__FILE__)}/lib/blather_ext/**/*.rb"]
	.sort
	.each(&Kernel.method(:require))

singleton_class.class_eval do
	include Blather::DSL
	include EventEmitter

	Blather::DSL.append_features(self)
end

$stdout.sync = true
LOG = Ougai::Logger.new($stdout)
LOG.level = ENV.fetch("LOG_LEVEL", "info")
LOG.formatter = Ougai::Formatters::Readable.new(
	nil,
	nil,
	plain: !$stdout.isatty
)
Blather.logger = LOG

def log
	Thread.current[:log] || LOG
end

Sentry.init do |config|
	config.logger = LOG
	config.breadcrumbs_logger = [:sentry_logger]
end

CONFIG = Dhall::Coder
	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
	.load(
		"(#{ARGV[0]}) : #{__dir__}/config-schema.dhall",
		transform_keys: ->(k) { k&.to_sym }
	)

def panic(e, hub=nil)
	log.fatal(
		"Error raised during event loop: #{e.class}",
		e
	)
	if e.is_a?(Exception)
		(hub || Sentry).capture_exception(e, hint: { background: false })
	else
		(hub || Sentry).capture_message(e.to_s, hint: { background: false })
	end
	exit 1
end

EM.error_handler(&method(:panic))

@client = BlatherClient.new

setup(
	CONFIG[:component][:jid],
	CONFIG[:component][:secret],
	CONFIG[:server][:host],
	CONFIG[:server][:port],
	nil,
	nil,
	async: true
)

@connected = false
@shutdown_requested = false

when_ready do
	@connected = true
	log.info "Ready"
	REDIS = EM::Hiredis.connect
	REGISTRATION_REPO = RegistrationRepo.new(redis: REDIS)
end

disconnected do
	next if @shutdown_requested

	if @connected
		log.fatal(
			"XMPP connection lost",
			server: "#{CONFIG[:server][:host]}:#{CONFIG[:server][:port]}",
			component: CONFIG[:component][:jid]
		)
	else
		log.fatal(
			"Failed to establish XMPP connection",
			server: "#{CONFIG[:server][:host]}:#{CONFIG[:server][:port]}",
			component: CONFIG[:component][:jid]
		)
	end

	EM.stop
end

disco_info to: Blather::JID.new(CONFIG[:component][:jid]) do |iq|
	reply = iq.reply
	reply.identities = [{
		name: "SGX Endstream",
		type: "sms",
		category: "gateway"
	}]
	reply.features = [
		"http://jabber.org/protocol/disco#info",
		"http://jabber.org/protocol/address",
		"jabber:iq:register"
	]
	self << reply
end

disco_info to: /\A\+?\d+@/ do |iq|
	reply = iq.reply
	reply.identities = [{
		name: "SGX Endstream",
		type: "sms",
		category: "client"
	}]
	reply.features = [
		"urn:xmpp:receipts"
	]
	self << reply
end

ibr type: :get do |iq|
	REGISTRATION_REPO.find(iq.from).then do |creds|
		reply = iq.reply
		reply.registered = true if creds&.first
		reply.phone = creds&.first.to_s
		reply.username = ""
		reply.password = ""
		self << reply
	end
end

ibr type: :set do |iq|
	unless iq.remove?
		if iq.phone.to_s !~ /\A\+?1\d{10}\Z/
			self << iq.as_error("bad-request", :modify, "Invalid phone number")
			next
		end

		if iq.username.to_s.empty? || iq.password.to_s.empty?
			self << iq.as_error(
				"bad-request",
				:modify,
				"Username and password are required"
			)
			next
		end
	end

	REGISTRATION_REPO.delete(iq.from).then { |status|
		next status if !status[:code].to_i.zero? || iq.remove?

		REGISTRATION_REPO.put(iq.from, iq.phone, iq.username, iq.password)
	}.then { |status|
		self << if status[:code].to_i.zero?
			iq.reply
		else
			iq.as_error("bad-request", :modify, status[:text])
		end
	}
end

message from: /@sms.chat.1pcom.net\Z/ do |m|
	json = JSON.parse(m.body) rescue nil

	tel = if m.from.node.length > 7
		"+#{m.from.node}"
	else
		"#{m.from.node};phone-context=ca-us.phone-context.soprani.ca"
	end
	m = m.dup.tap { _1.subject = nil } # They send a generic subject
	m.from = Blather::JID.new(tel, CONFIG[:component][:jid])
	m.to = ProxiedJID.new(m.to).unproxied

	if json.is_a?(Hash) && (resp = json["response"])
		log.info("SMS Status", json)
		m = m.reply.tap { _1.body = "" }.tap { _1.id = resp["id"] }.as_error(
			"recipient-unavailable",
			:cancel,
			"#{resp['text']} (#{resp['code']} #{resp['subcode']} #{resp['dlrid']})"
		)
		emit_failed_event(
			endstream_id: resp["id"],
			error_code: "#{resp['code']} #{resp['subcode']}",
			error_description: resp["text"]
		)
	else
		emit_incoming_event(m.to, from: tel, body: m.body, endstream_id: m.id)
	end

	self << m
end

message from: /@mms.chat.1pcom.net\Z/ do |m|
	json = JSON.parse(m.body)
	if json.is_a?(Hash) && json["GlobalStatus"]
		log.info("MMS Status", json["disposition"])
		next
	end

	IncomingMMS.for(m.to, json).then { |incoming|
		tel = if m.from.node.length > 7
			"+#{m.from.node}"
		else
			"#{m.from.node};phone-context=ca-us.phone-context.soprani.ca"
		end

		to_send = incoming.to_stanza
		to_send.id = m.id
		to_send.from = Blather::JID.new(tel, CONFIG[:component][:jid])
		self << to_send

		emit_incoming_event(
			incoming.unproxied_to,
			from: tel,
			body: incoming.body_text,
			endstream_id: m.id,
			media_urls: incoming.media_urls
		)
	}
end

# Swallow errors, endstream doesn't want them
message type: :error do
	true
end

# @parameter m [Blather::Stanza::Message]
def send_outgoing_mms(m)
	oobs = m.oobs
	id = m.id
	self << OutgoingMMS.for(m).to_stanza(id: id, from: m.from)

	emit_outgoing_event(
		m.from,
		to: m.recipients,
		body: m.body.to_s.sub(oobs.first&.url.to_s, ""), # OOB's already captured
		stanza_id: id,
		media_urls: oobs.map(&:url)
	)
end

message :addresses, to: Blather::JID.new(CONFIG[:component][:jid]) do |m|
	send_outgoing_mms(m)
end

message ->(m) { !m.oobs.empty? }, to: /\A\+?\d+@/ do |m|
	# TODO: if too big or bad mime, send sms
	send_outgoing_mms(m)
end

def too_long_for_sms?(m)
	# ~3 segments
	m.body.length > (m.body.ascii_only? ? 160 : 70) * 3
end

message :body, method(:too_long_for_sms?).to_proc, to: /\A\+?\d+@/ do |m|
	send_outgoing_mms(m)
end

message(
	:body,
	to: /(?:\A\+?\d+@)|(?:;phone-context=ca-us\.phone-context\.soprani\.ca@)/
) do |m|
	owner_jid = m.from
	dest = m.to.node
	body = m.body
	stanza_id = m.id

	m.to = Blather::JID.new(
		dest.sub(/\A\+/, "").sub(/;phone-context=.*\Z/, ""),
		"sms.chat.1pcom.net"
	)
	m.from = ProxiedJID.proxy(m.from, CONFIG[:component][:jid])
	self << m

	emit_outgoing_event(
		owner_jid,
		to: [dest],
		body: body,
		stanza_id: stanza_id
	)
end

iq type: [:get, :set] do |iq|
	self << iq.as_error("service-unavailable", :cancel)
end

trap(:INT) do
	@shutdown_requested = true
	EM.stop
end

trap(:TERM) do
	@shutdown_requested = true
	EM.stop
end

EM.run { client.run }
