# frozen_string_literal: true

require "digest"
require "forwardable"
require "multibases"
require "multihashes"
require "roda"
require "thin"
require "sentry-ruby"

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

class OGMDownload
	def initialize(url)
		@digest = Digest::SHA512.new
		@f = Tempfile.open("ogm")
		@req = EM::HttpRequest.new(url, tls: { verify_peer: true })
	end

	def download
		http = @req.aget
		http.stream do |chunk|
			@digest << chunk
			@f.write chunk
		end
		http.then { @f.close }.catch do |e|
			@f.close!
			EMPromise.reject(e)
		end
	end

	def cid
		Multibases.encode(
			"base58btc",
			[1, 85].pack("C*") + Multihashes.encode(@digest.digest, "sha2-512")
		).pack.to_s
	end

	def path
		@f.path
	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, :outbound_transfers

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

	extend Forwardable
	def_delegators :'self.class', :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

	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/#{params['callId']}", 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"
						if (outbound_leg = outbound_transfers.delete(params["callId"]))
							modify_call(outbound_leg) do |call|
								call.state = "completed"
							end
						end

						CustomerRepo.new.find_by_tel(params["to"]).then do |customer|
							CDR.for_inbound(customer.customer_id, params).save
						end
					end
					"OK"
				end

				r.on :call_id do |call_id|
					r.post "transfer_complete" do
						outbound_leg = outbound_transfers.delete(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
							modify_call(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"
							)

							CustomerRepo.new.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
							CustomerRepo.new.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
							CustomerRepo
								.new(sgx_repo: Bwmsgsv2Repo.new)
								.find_by_tel(params["to"])
								.then { |c|
									EMPromise.all([c, c.ogm(params["from"])])
								}.then do |(customer, ogm)|
									render :voicemail, locals: {
										ogm: ogm,
										transcription_enabled: customer.transcription_enabled
									}
								end
						end
					end

					r.post do
						render :bridge, locals: { call_id: call_id }
					end
				end

				r.post do
					CustomerRepo.new(
						sgx_repo: Bwmsgsv2Repo.new
					).find_by_tel(params["to"]).then(&:fwd).then do |fwd|
						call = fwd.create_call(CONFIG[:creds][:account]) { |cc|
							cc.from = params["from"]
							cc.application_id = params["applicationId"]
							cc.answer_url = url inbound_calls_path(nil)
							cc.disconnect_url = url inbound_calls_path(:transfer_complete)
						}

						if call
							outbound_transfers[params["callId"]] = call
							render :ring, locals: { duration: 300 }
						else
							render :redirect, locals: { to: inbound_calls_path(:voicemail) }
						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(/^\+1/, "")
					CustomerRepo.new(
						sgx_repo: Bwmsgsv2Repo.new
					).find(customer_id).then do |c|
						render :forward, locals: {
							from: c.registered?.phone,
							to: params["to"]
						}
					end
				end
			end
		end

		r.on "ogm" do
			r.post "start" do
				render :record_ogm, locals: { customer_id: params["customer_id"] }
			end

			r.post do
				jmp_media_url = params["mediaUrl"].sub(
					/\Ahttps:\/\/voice.bandwidth.com\/api\/v2\/accounts\/\d+/,
					"https://jmp.chat"
				)
				ogm = OGMDownload.new(jmp_media_url)
				ogm.download.then do
					File.rename(ogm.path, "#{CONFIG[:ogm_path]}/#{ogm.cid}")
					File.chmod(0o644, "#{CONFIG[:ogm_path]}/#{ogm.cid}")
					CustomerRepo.new.find(params["customer_id"]).then do |customer|
						customer.set_ogm_url("#{CONFIG[:ogm_web_root]}/#{ogm.cid}.mp3")
					end
				end
			end
		end

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