# frozen_string_literal: true

require "digest"
require "forwardable"
require "roda"
require "thin"
require "sentry-ruby"
require "bandwidth"

Faraday.default_adapter = :em_synchrony

require_relative "lib/cdr"
require_relative "lib/roda_capture"
require_relative "lib/roda_em_promise"
require_relative "lib/rack_fiber"

BANDWIDTH_VOICE = Bandwidth::Client.new(
	voice_basic_auth_user_name: CONFIG[:creds][:username],
	voice_basic_auth_password: CONFIG[:creds][:password]
).voice_client.client

module CustomerFwd
	def self.from_redis(redis, customer, tel)
		EMPromise.all([
			redis.get("catapult_fwd-#{tel}"),
			customer.fwd_timeout
		]).then do |(fwd, stimeout)|
			timeout = Timeout.new(stimeout)
			next if !fwd || timeout.zero?
			self.for(fwd, timeout)
		end
	end

	def self.for(uri, timeout)
		case uri
		when /^tel:/
			Tel.new(uri, timeout)
		when /^sip:/
			SIP.new(uri, timeout)
		when /^xmpp:/
			XMPP.new(uri, timeout)
		else
			raise "Unknown forward URI: #{uri}"
		end
	end

	class Timeout
		def initialize(s)
			@timeout = s.nil? || s.to_i.negative? ? 300 : s.to_i
		end

		def zero?
			@timeout.zero?
		end

		def to_i
			@timeout
		end
	end

	class Tel
		attr_reader :timeout

		def initialize(uri, timeout)
			@tel = uri.sub(/^tel:/, "")
			@timeout = timeout
		end

		def to
			@tel
		end
	end

	class SIP
		attr_reader :timeout

		def initialize(uri, timeout)
			@uri = uri
			@timeout = timeout
		end

		def to
			@uri
		end
	end

	class XMPP
		attr_reader :timeout

		def initialize(uri, timeout)
			@jid = uri.sub(/^xmpp:/, "")
			@timeout = timeout
		end

		def to
			"sip:#{ERB::Util.url_encode(@jid)}@sip.cheogram.com"
		end
	end
end

# rubocop:disable Metrics/ClassLength
class Web < Roda
	use Rack::Fiber # Must go first!
	use Sentry::Rack::CaptureExceptions
	plugin :json_parser
	plugin :public
	plugin :render, engine: "slim"
	plugin RodaCapture
	plugin RodaEMPromise # Must go last!

	class << self
		attr_reader :customer_repo, :log
		attr_reader :true_inbound_call, :outbound_transfers

		def run(log, customer_repo, *listen_on)
			plugin :common_logger, log, method: :info
			@customer_repo = customer_repo
			@true_inbound_call = {}
			@outbound_transfers = {}
			Thin::Logging.logger = log
			Thin::Server.start(
				*listen_on,
				freeze.app,
				signals: false
			)
		end
	end

	extend Forwardable
	def_delegators :'self.class', :customer_repo, :true_inbound_call,
	               :outbound_transfers
	def_delegators :request, :params

	def log
		opts[:common_logger]
	end

	def log_error(e)
		log.error(
			"Error raised during #{request.full_path}: #{e.class}",
			e,
			loggable_params
		)
		if e.is_a?(::Exception)
			Sentry.capture_exception(e)
		else
			Sentry.capture_message(e.to_s)
		end
	end

	def loggable_params
		params.dup.tap do |p|
			p.delete("to")
			p.delete("from")
		end
	end

	def pseudo_call_id
		request.captures_hash[:pseudo_call_id] ||
			Digest::SHA256.hexdigest("#{params['from']},#{params['to']}")
	end

	TEL_CANDIDATES = {
		"Restricted" => "14",
		"anonymous" => "15",
		"Anonymous" => "16",
		"unavailable" => "17",
		"Unavailable" => "18"
	}.freeze

	def sanitize_tel_candidate(candidate)
		if candidate.length < 3
			"13;phone-context=anonymous.phone-context.soprani.ca"
		elsif candidate[0] == "+" && /\A\d+\z/.match(candidate[1..-1])
			candidate
		elsif candidate == "Restricted"
			TEL_CANDIDATES.fetch(candidate, "19") +
				";phone-context=anonymous.phone-context.soprani.ca"
		end
	end

	def from_jid
		Blather::JID.new(
			sanitize_tel_candidate(params["from"]),
			CONFIG[:component][:jid]
		)
	end

	def inbound_calls_path(suffix)
		["/inbound/calls/#{pseudo_call_id}", suffix].compact.join("/")
	end

	def url(path)
		"#{request.base_url}#{path}"
	end

	def modify_call(call_id)
		body = Bandwidth::ApiModifyCallRequest.new
		yield body
		BANDWIDTH_VOICE.modify_call(
			CONFIG[:creds][:account],
			call_id,
			body: body
		)
	end

	route do |r|
		r.on "inbound" do
			r.on "calls" do
				r.post "status" do
					if params["eventType"] == "disconnect"
						p_call_id = pseudo_call_id
						call_id = params["callId"]
						EM.promise_timer(2).then {
							next unless true_inbound_call[p_call_id] == call_id
							true_inbound_call.delete(p_call_id)

							if (outbound_leg = outbound_transfers.delete(p_call_id))
								modify_call(outbound_leg) do |call|
									call.state = "completed"
								end
							end

							customer_repo.find_by_tel(params["to"]).then do |customer|
								CDR.for_inbound(customer.customer_id, params).save
							end
						}.catch(&method(:log_error))
					end
					"OK"
				end

				r.on :pseudo_call_id do |pseudo_call_id|
					r.post "transfer_complete" do
						outbound_leg = outbound_transfers.delete(pseudo_call_id)
						if params["cause"] == "hangup"
							log.debug "Normal hangup", loggable_params
						elsif !outbound_leg
							log.debug "Inbound disconnected", loggable_params
						else
							log.debug "Go to voicemail", loggable_params
							true_call_id = true_inbound_call[pseudo_call_id]
							modify_call(true_call_id) do |call|
								call.redirect_url = url inbound_calls_path(:voicemail)
							end
						end
						""
					end

					r.on "voicemail" do
						r.post "audio" do
							duration = Time.parse(params["endTime"]) -
							           Time.parse(params["startTime"])
							next "OK<5" unless duration > 5

							jmp_media_url = params["mediaUrl"].sub(
								/\Ahttps:\/\/voice.bandwidth.com\/api\/v2\/accounts\/\d+/,
								"https://jmp.chat"
							)

							customer_repo.find_by_tel(params["to"]).then do |customer|
								m = Blather::Stanza::Message.new
								m.chat_state = nil
								m.from = from_jid
								m.subject = "New Voicemail"
								m.body = jmp_media_url
								m << OOB.new(jmp_media_url, desc: "Voicemail Recording")
								customer.stanza_to(m)

								"OK"
							end
						end

						r.post "transcription" do
							customer_repo.find_by_tel(params["to"]).then do |customer|
								m = Blather::Stanza::Message.new
								m.chat_state = nil
								m.from = from_jid
								m.subject = "Voicemail Transcription"
								m.body = BANDWIDTH_VOICE.get_recording_transcription(
									params["accountId"], params["callId"], params["recordingId"]
								).data.transcripts[0].text
								customer.stanza_to(m)

								"OK"
							end
						end

						r.post do
							customer_repo
								.find_by_tel(params["to"])
								.then { |customer|
									EMPromise.all([
										customer.ogm(params["from"]),
										customer.catapult_flag(
											BackendSgx::VOICEMAIL_TRANSCRIPTION_DISABLED
										)
									])
								}.then do |(ogm, transcription_disabled)|
									render :voicemail, locals: {
										ogm: ogm,
										transcription_enabled: !transcription_disabled
									}
								end
						end
					end

					r.post do
						true_call_id = true_inbound_call[pseudo_call_id]
						render :bridge, locals: { call_id: true_call_id }
					end
				end

				r.post do
					if true_inbound_call[pseudo_call_id]
						true_inbound_call[pseudo_call_id] = params["callId"]
						return render :pause, locals: { duration: 300 }
					end

					customer_repo.find_by_tel(params["to"]).then do |customer|
						CustomerFwd.from_redis(::REDIS, customer, params["to"]).then do |fwd|
							if fwd
								body = Bandwidth::ApiCreateCallRequest.new.tap do |cc|
									cc.to = fwd.to
									cc.from = params["from"]
									cc.application_id = params["applicationId"]
									cc.call_timeout = fwd.timeout.to_i
									cc.answer_url = url inbound_calls_path(nil)
									cc.disconnect_url = url inbound_calls_path(:transfer_complete)
								end
								true_inbound_call[pseudo_call_id] = params["callId"]
								outbound_transfers[pseudo_call_id] = BANDWIDTH_VOICE.create_call(
									CONFIG[:creds][:account], body: body
								).data.call_id
								render :pause, locals: { duration: 300 }
							else
								render :redirect, locals: { to: inbound_calls_path(:voicemail) }
							end
						end
					end
				end
			end
		end

		r.on "outbound" do
			r.on "calls" do
				r.post "status" do
					log.info "#{params['eventType']} #{params['callId']}", loggable_params
					if params["eventType"] == "disconnect"
						CDR.for_outbound(params).save.catch(&method(:log_error))
					end
					"OK"
				end

				r.post do
					customer_id = params["from"].sub(/^\+/, "")
					customer_repo.find(customer_id).then(:registered?).then do |reg|
						render :forward, locals: {
							from: reg.phone,
							to: params["to"]
						}
					end
				end
			end
		end

		r.public
	end
end
# rubocop:enable Metrics/ClassLength
