# frozen_string_literal: true

require "forwardable"
require "value_semantics/monkey_patched"

require_relative "em"
require_relative "parented_domain"
require_relative "plan"

class CustomerPlan
	extend Forwardable

	def_delegator :plan, :name, :plan_name
	def_delegators :plan, :currency, :merchant_account,
	               :minute_limit, :message_limit

	value_semantics do
		customer_id           String
		plan                  Anything(), default: nil, coerce: true
		expires_at            Either(Time, nil), default_generator: -> { Time.now }
		auto_top_up_amount    Integer, default: 0
		monthly_overage_limit Integer, default: 0
		pending               Bool(), default: false
		parent_customer_id    Either(String, nil), default: nil
		parent_plan           Anything(), default: nil, coerce: true
	end

	class << self
		def default(customer_id, jid)
			new(customer_id, **ParentedDomain.for(jid)&.plan_kwargs || {})
		end

		def extract(**kwargs)
			new(**kwargs.slice(
				*value_semantics.attributes.map(&:name), :plan_name, :parent_plan_name
			))
		end

		def coerce_plan(plan_or_name_or_nil)
			return plan_or_name_or_nil if plan_or_name_or_nil.is_a?(OpenStruct)
			return OpenStruct.new unless plan_or_name_or_nil

			Plan.for(plan_or_name_or_nil)
		end
		alias coerce_parent_plan coerce_plan
	end

	def initialize(customer_id=nil, **kwargs)
		kwargs, customer_id = customer_id, nil if customer_id.is_a?(Hash)
		kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
		if kwargs.key?(:parent_plan_name)
			kwargs[:parent_plan] = kwargs.delete(:parent_plan_name)
		end
		super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
	end

	def active?
		plan_name && expires_at > Time.now
	end

	def status
		return :active if active?
		return :pending if pending

		:expired
	end

	def monthly_price
		plan.monthly_price - (parent_plan&.subaccount_discount || 0)
	end

	def verify_parent!
		return unless parent_customer_id

		result = DB.query(<<~SQL, [parent_customer_id])
			SELECT plan_name FROM customer_plans WHERE customer_id=$1
		SQL

		raise "Invalid parent account" if !result || !result.first

		plan = Plan.for(result.first["plan_name"])
		raise "Parent currency mismatch" unless plan.currency == currency
	end

	def save_plan!
		verify_parent!
		DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
			INSERT INTO plan_log
				(customer_id, plan_name, parent_customer_id, date_range)
			VALUES (
				$1,
				$2,
				$3,
				tsrange(
					LOCALTIMESTAMP - '2 seconds'::interval,
					LOCALTIMESTAMP - '1 second'::interval
				)
			)
		SQL
	end

	def bill_plan(note: nil)
		EMPromise.resolve(nil).then do
			DB.transaction do |db|
				next false unless !block_given? || yield(db)

				charge_for_plan(note)
				extend_plan
				true
			end
		end
	end

	def activate_plan_starting_now
		verify_parent!
		activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
			ON CONFLICT DO NOTHING
		SQL
		activated = activated.cmd_tuples.positive?
		return false unless activated

		DB.exec(<<~SQL, [customer_id])
			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
				AND upper(date_range) - lower(date_range) < '2 seconds'
		SQL
	end

	def extend_plan
		add_one_month_to_current_plan unless activate_plan_starting_now
	end

	def activation_date
		DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
			SELECT
				MIN(LOWER(date_range)) AS start_date
			FROM plan_log WHERE customer_id = $1;
		SQL
	end

	protected :customer_id, :plan, :pending, :[]

protected

	def charge_for_plan(note)
		raise "No plan setup" unless plan

		params = [
			customer_id,
			"#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
			-monthly_price,
			note
		]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions
				(customer_id, transaction_id, created_at, settled_after, amount, note)
			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
		SQL
	end

	def add_one_month_to_current_plan
		DB.exec(<<~SQL, [customer_id])
			UPDATE plan_log SET date_range=range_merge(
				date_range,
				tsrange(
					LOWER(date_range),
					GREATEST(UPPER(date_range), LOCALTIMESTAMP) + '1 month'
				)
			)
			WHERE
				customer_id=$1 AND
				UPPER(date_range) = (SELECT MAX(UPPER(date_range)) FROM plan_log WHERE
				customer_id=$1 AND date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
		SQL
	end
end
