# 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 = Hash[
          current_data.map 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