# frozen_string_literal: true

module LedgerSync
  module Util
    class ResourcesBuilder
      attr_reader :cast,
                  :data,
                  :ignore_unrecognized_attributes,
                  :ledger,
                  :root_resource_external_id,
                  :root_resource_type

      def initialize(args = {})
        @all_resources                  = {}
        @ledger                         = args.fetch(:ledger)
        @cast                           = args.fetch(:cast, true)
        @data                           = Util::HashHelpers.deep_symbolize_keys(args.fetch(:data))
        @ignore_unrecognized_attributes = args.fetch(:ignore_unrecognized_attributes, false)
        @root_resource_external_id      = args.fetch(:root_resource_external_id)
        @root_resource_type             = args.fetch(:root_resource_type)
      end

      def resource
        @resource ||= build_resource(
          external_id: root_resource_external_id,
          type: root_resource_type
        )
      end

      def resources
        @resources ||= begin
          resource
          @all_resources.values
        end
      end

      private

      def build_resource(args = {}) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
        external_id  = args.fetch(:external_id).to_sym
        type         = args.fetch(:type).to_sym
        ledger_id    = @data.dig(type, external_id, :ledger_id)
        current_data = @data.dig(type, external_id, :data)

        raise "No data provided for #{type} (ID: #{external_id})" if current_data.nil?

        resource_class = if ledger == :root
                           LedgerSync.resources.find { |e| e.resource_type == type }
                         else
                           LedgerSync.ledgers.send(ledger).client_class.resources[type]
                         end
        raise "#{type} is an invalid resource type" if resource_class.nil?

        current_data = current_data.to_h do |k, v|
          k = k.to_sym

          attribute = resource_class.resource_attributes[k]
          if attribute.nil? && !ignore_unrecognized_attributes
            raise "Unrecognized attribute for #{resource_class.name}: #{k}"
          end

          v = if attribute.is_a?(ResourceAttribute::Reference::One)
                resource_type = resource_type_by(external_id: current_data[k])
                resource_or_build(
                  external_id: current_data[k],
                  type: resource_type
                )
              elsif attribute.is_a?(ResourceAttribute::Reference::Many)
                current_data[k].map do |many_reference|
                  resource_or_build(
                    external_id: many_reference,
                    type: attribute.type.resource_class.resource_type
                  )
                end
              elsif cast
                attribute.type.cast(value: v)
              else
                v
              end

          [k, v]
        end

        @all_resources[resource_key(external_id: external_id, type: type)] ||= resource_class.new(
          external_id: external_id,
          ledger_id: ledger_id,
          **current_data
        )
      end

      # This will break if multiple objects of different types have the same external_id
      def external_id_to_type_hash
        @external_id_to_type_hash ||= begin
          ret = {}
          data.each do |type, type_resources|
            type_resources.each_key do |external_id|
              ret[external_id] = type
            end
          end
          ret
        end
      end

      def resource_key(external_id:, type:)
        "#{type}/#{external_id}"
      end

      def resource_or_build(**external_id_and_type)
        @all_resources.fetch(
          resource_key(**external_id_and_type),
          build_resource(**external_id_and_type)
        )
      end

      def resource_type_by(external_id:)
        external_id_to_type_hash[external_id.to_sym]
      end
    end
  end
end
