# frozen_string_literal: true

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

require_relative "lib/call_attempt_repo"
require_relative "lib/trust_level_repo"
require_relative "lib/cdr"
require_relative "lib/cdr_repo"
require_relative "lib/oob"
require_relative "lib/rev_ai"
require_relative "lib/roda_capture"
require_relative "lib/roda_em_promise"
require_relative "lib/rack_fiber"
require_relative "lib/reachability_repo"

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 unless ENV["ENV"] == "test" # Must go first!
	use Sentry::Rack::CaptureExceptions
	plugin :json_parser
	plugin :type_routing
	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.fullpath}: #{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 customer_repo(**kwargs)
		kwargs[:set_user] = Sentry.method(:set_user) unless kwargs[:set_user]
		opts[:customer_repo] || CustomerRepo.new(**kwargs)
	end

	def trust_level_repo(**kwargs)
		opts[:trust_level_repo] || TrustLevelRepo.new(**kwargs)
	end

	def reachability_repo(**kwargs)
		opts[:reachability_repo] || ReachabilityRepo::Voice.new(**kwargs)
	end

	def find_by_tel_with_fallback(sgx_repo:, **kwargs)
		customer_repo(sgx_repo: sgx_repo).find_by_tel(params["to"]).catch { |e|
			next EMPromise.reject(e) if e.is_a?(CustomerRepo::NotFound)

			log_error(e)
			customer_repo(
				sgx_repo: TrivialBackendSgxRepo.new(**kwargs)
			).find_by_tel(params["to"])
		}
	end

	def call_attempt_repo
		opts[:call_attempt_repo] || CallAttemptRepo.new
	end

	def cdr_repo
		opts[:cdr_repo] || CDRRepo.new
	end

	def rev_ai
		RevAi.new(logger: log.child(loggable_params))
	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
		else
			"#{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, customer_id=nil, call_id: nil)
		[
			"/inbound/calls/#{call_id || params['callId']}",
			suffix
		].compact.join("/") +
			(customer_id ? "?customer_id=#{customer_id}" : "")
	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
		)
	rescue Bandwidth::APIException
		# If call does not exist, don't need to hang up or send to voicemail
		# Other side must have hung up already
		raise $! unless [404, 409].include?($!.response_code)
	end

	def start_transcription(customer, call_id, media_url)
		return unless customer.transcription_enabled

		rev_ai.language_id(
			media_url,
			url(inbound_calls_path("voicemail/language_id", call_id: call_id)),
			from_jid: from_jid,
			customer_id: customer.customer_id
		)
	end

	def call_inputs(customer, from, call_id)
		EMPromise.all([
			customer.customer_id, customer.fwd,
			call_attempt_repo.find_inbound(customer, from, call_id: call_id)
		])
	end

	def json_call(customer, fwd, call_attempt)
		raise "Not allowed" unless params["secret"] == CONFIG[:component][:secret]

		customer.ogm(params["from"]).then do |ogm|
			call_attempt.as_json.merge(
				fwd: fwd,
				ogm: ogm
			).to_json
		end
	end

	def transfer_complete_url(customer_id, call_id, tries)
		url(
			inbound_calls_path(:transfer_complete, customer_id, call_id: call_id)
		) + (tries ? "&tries=#{tries}" : "")
	end

	def create_call(customer, from, call_id, application_id, tries: nil)
		call_inputs(customer, from, call_id).then do |(customer_id, fwd, ca)|
			request.json { json_call(customer, fwd, ca) }
			ca.create_call(fwd, CONFIG[:creds][:account]) do |cc|
				cc.from = from
				cc.application_id = application_id
				cc.answer_url = url inbound_calls_path(nil, customer_id)
				cc.disconnect_url = transfer_complete_url(customer_id, call_id, tries)
			end
		end
	end

	def inbound_from
		if params["from"] && params["from"] =~ /\A\+?\d+\Z/
			params["from"]
		else
			log.info "Inbound call with unusual from: #{params['from']}"
			TEL_CANDIDATES.fetch(params["from"], "19")
		end
	end

	def hangup
		request.json { {}.to_json }

		render :hangup
	end

	route do |r|
		r.get "healthcheck" do
			"OK"
		end

		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

						customer_repo.find_by_tel(params["to"]).then do |customer|
							cdr_repo.put(CDR.for_inbound(customer.customer_id, params))
						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" && params["tag"] == "connected"
							log.info "Normal hangup, now end #{call_id}", loggable_params
							modify_call(call_id) { |call| call.state = "completed" }
						elsif !outbound_leg
							log.debug "Inbound disconnected", loggable_params
						elsif params["cause"] == "error" && params["tries"].to_i < 15
							log.info "2nd leg error, retry", loggable_params
							customer_repo(
								sgx_repo: Bwmsgsv2Repo.new
							).find(params["customer_id"]).then { |customer|
								create_call(
									customer, params["from"], call_id, params["applicationId"],
									tries: params["tries"].to_i + 1
								).then { |call|
									outbound_transfers[params["callId"]] = call
								}.catch(&log.method(:error))
							}
						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"
							)

							find_by_tel_with_fallback(
								sgx_repo: Bwmsgsv2Repo.new,
								transcription_enabled: false
							).then do |customer|
								start_transcription(customer, call_id, jmp_media_url)

								m = Blather::Stanza::Message.new
								m.chat_state = nil
								m.from = from_jid
								m.subject = "New Voicemail"
								m << OOB.new(jmp_media_url)
								customer.stanza_to(m)

								"OK"
							end
						end

						r.post "language_id" do
							rev_ai.language_id_result(params).then { |result|
								rev_ai.stt(
									result["top_language"],
									result.dig("metadata", "media_url"),
									url(inbound_calls_path(
										"voicemail/transcription",
										call_id: call_id
									)),
									**result["metadata"].transform_keys(&:to_sym)
								).then { "OK" }
							}.catch_only(RevAi::Failed) { |e|
								log_error(e)
								"Failure logged"
							}
						end

						r.post "transcription" do
							rev_ai.stt_result(params, request.url).then { |result|
								next "OK" if result["text"].to_s.empty?

								customer_repo.find(
									result.dig("metadata", "customer_id")
								).then do |customer|
									m = Blather::Stanza::Message.new
									m.chat_state = nil
									m.from = result.dig("metadata", "from_jid")
									m.subject = "Voicemail Transcription"
									m.body = result["text"]
									customer.stanza_to(m)

									"OK"
								end
							}.catch_only(RevAi::Failed) { |e|
								log_error(e)
								"Failure logged"
							}
						end

						r.post do
							find_by_tel_with_fallback(
								sgx_repo: Bwmsgsv2Repo.new,
								ogm_url: nil
							).then { |c|
								c.ogm(params["from"])
							}.then { |ogm|
								next hangup unless ogm

								render :voicemail, locals: { ogm: ogm }
							}.catch_only(CustomerRepo::NotFound) {
								render "inbound/no_customer"
							}
						end
					end

					r.post do
						customer_repo(
							sgx_repo: Bwmsgsv2Repo.new
						).find(params.fetch("customer_id")).then do |customer|
							call_attempt_repo.find_inbound(
								customer,
								params["from"],
								call_id: call_id,
								digits: params["digits"]
							).then { |ca| render(*ca.to_render) }
						end
					end
				end

				r.post do
					customer_repo(
						sgx_repo: Bwmsgsv2Repo.new
					).find_by_tel(params["to"]).then { |customer|
						reachability_repo.find(customer, params["from"]).then do |reach|
							reach.filter(if_yes: ->(_) { hangup }) do
								create_call(
									customer,
									inbound_from,
									params["callId"],
									params["applicationId"]
								).then { |call|
									next EMPromise.reject(:voicemail) unless call

									outbound_transfers[params["callId"]] = call
									render :ring, locals: { duration: 300 }
								}
							end
						end
					}.catch_only(CustomerFwd::InfiniteTimeout) { |e|
						render :forward, locals: { fwd: e.fwd, from: params["from"] }
					}.catch { |e|
						log_error(e) unless e == :voicemail
						r.json { { error: e.to_s }.to_json }
						render :redirect, locals: { to: inbound_calls_path(:voicemail) }
					}
				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"
						from = params["from"].sub(/^(?:\+|c)/, "")

						customer_repo.find_by_format(from).then { |customer|
							trust_level_repo.find(customer).then { |tl| [customer, tl] }
						}.then { |(customer, trust_level)|
							next "OK" unless trust_level.write_cdr?

							customer_id = customer.customer_id
							call_attempt_repo.ending_call(customer_id, params["callId"])
							cdr_repo
								.put(CDR.for_outbound(customer_id, params))
								.catch(&method(:log_error))

							"OK"
						}
					else
						"OK"
					end
				end

				r.post do
					from = params["from"].sub(/^(?:\+|c)/, "")
					customer_repo(
						sgx_repo: Bwmsgsv2Repo.new
					).find_by_format(from).then { |c|
						call_attempt_repo.find_outbound(
							c,
							params["to"],
							call_id: params["callId"],
							digits: params["digits"]
						).then do |ca|
							r.json { ca.to_json }

							call_attempt_repo.starting_call(c, params["callId"]).then do
								render(*ca.to_render)
							end
						end
					}.catch_only(CustomerRepo::NotFound) {
						render "outbound/no_customer"
					}
				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
					FileUtils.mv(ogm.path, "#{CONFIG[:ogm_path]}/#{ogm.cid}")
					File.chmod(0o644, "#{CONFIG[:ogm_path]}/#{ogm.cid}")
					customer_repo.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
