require_relative 'tag'
require_relative 'link'
require_relative 'user'
require_relative 'role'
require_relative 'error'
require_relative 'entry'
require_relative 'array'
require_relative 'space'
require_relative 'asset'
require_relative 'locale'
require_relative 'upload'
require_relative 'webhook'
require_relative 'api_key'
require_relative 'resource'
require_relative 'snapshot'
require_relative 'environment'
require_relative 'organization'
require_relative 'content_type'
require_relative 'webhook_call'
require_relative 'ui_extension'
require_relative 'dynamic_entry'
require_relative 'webhook_health'
require_relative 'preview_api_key'
require_relative 'space_membership'
require_relative 'editor_interface'
require_relative 'space_periodic_usage'
require_relative 'personal_access_token'
require_relative 'organization_periodic_usage'

module Contentful
  module Management
    # Transforms a Contentful::Response into a Contentful::Resource or a Contentful::Error
    # See example/resource_mapping.rb for avanced usage
    class ResourceBuilder
      # Default Resource Mapping
      # @see _ README for more information on Resource Mapping
      DEFAULT_RESOURCE_MAPPING = {
        'Space' => Contentful::Management::Space,
        'SpaceMembership' => Contentful::Management::SpaceMembership,
        'Organization' => Contentful::Management::Organization,
        'SpacePeriodicUsage' => Contentful::Management::SpacePeriodicUsage,
        'OrganizationPeriodicUsage' => Contentful::Management::OrganizationPeriodicUsage,
        'User' => Contentful::Management::User,
        'Environment' => Contentful::Management::Environment,
        'ContentType' => Contentful::Management::ContentType,
        'Entry' => :find_entry_class,
        'Asset' => Contentful::Management::Asset,
        'Array' => :array_or_sync_page,
        'Link' => Contentful::Management::Link,
        'WebhookDefinition' => Contentful::Management::Webhook,
        'WebhookCallOverview' => Contentful::Management::WebhookCall,
        'WebhookCallDetails' => Contentful::Management::WebhookCall,
        'Webhook' => Contentful::Management::WebhookHealth,
        'ApiKey' => Contentful::Management::ApiKey,
        'PreviewApiKey' => Contentful::Management::PreviewApiKey,
        'PersonalAccessToken' => Contentful::Management::PersonalAccessToken,
        'Locale' => Contentful::Management::Locale,
        'Role' => Contentful::Management::Role,
        'Extension' => Contentful::Management::UIExtension,
        'EditorInterface' => Contentful::Management::EditorInterface,
        'Snapshot' => Contentful::Management::Snapshot,
        'Upload' => Contentful::Management::Upload,
        'Tag' => Contentful::Management::Tag
      }.freeze

      # Default Entry Mapping
      # @see _ README for more information on Entry Mapping
      DEFAULT_ENTRY_MAPPING = {}.freeze

      attr_reader :client, :response, :resource_mapping, :entry_mapping, :resource

      def initialize(response, client, resource_mapping = {}, entry_mapping = {})
        @response = response
        @client = client
        @included_resources = {}
        @known_resources = Hash.new { |hash, key| hash[key] = {} }
        @nested_locales = true
        @resource_mapping = default_resource_mapping.merge(resource_mapping)
        @entry_mapping = default_entry_mapping.merge(entry_mapping)
      end

      # Starts the parsing process.
      # @return [Contentful::Management::Resource, Contentful::Management::Error]
      def run
        if response.status == :ok
          create_all_resources!
        else
          response.object
        end
      end

      # PARSING MECHANISM
      # - raise error if response not valid
      # - look for included objects and parse them to resources
      # - parse main object to resource
      # - replace links in included resources with known resources
      # - replace links in main resource with known resources
      # - return main resource
      def create_all_resources!
        create_included_resources! response.object['includes']
        @resource = create_resource(response.object)

        unless @included_resources.empty?
          replace_links_in_included_resources_with_known_resources
          replace_links_with_known_resources @resource
        end

        @resource
      rescue UnparsableResource => error
        error
      end

      # Creates a single resource from the response object
      def create_resource(object)
        res_class = detect_resource_class(object)
        @nested_locales ||= res_class.nested_locale_fields?
        res = res_class.new(object, response.request, client, @nested_locales)

        add_to_known_resources res
        replace_children res, object
        replace_child_array res.items if res.array?

        res
      end

      # Checks in a custom class for an entry was defined in entry_mapping
      def find_entry_class(object)
        entry_mapping[content_type_id_for_entry(object)] || try_dynamic_entry(object)
      end

      # Automatically converts Entry to DynamicEntry if in cache
      def try_dynamic_entry(object)
        get_dynamic_entry(object) || Contentful::Management::Entry
      end

      # Finds the proper DynamicEntry class for an entry
      def get_dynamic_entry(object)
        content_id = content_type_id_for_entry(object)
        client.dynamic_entry_cache[content_id.to_sym] if content_id
      end

      # Returns the id of the related ContentType, if there is one
      def content_type_id_for_entry(object)
        object['sys']['contentType']['sys']['id']
      end

      # Detects if a resource is an Contentful::Array or a SyncPage
      def array_or_sync_page(object)
        if object['nextPageUrl'] || object['nextSyncUrl']
          SyncPage
        else
          Array
        end
      end

      # Uses the resource mapping to find the proper Resource class to initialize
      # for this Response object type
      #
      # The mapping value can be a
      # - Class
      # - Proc: Will be called, expected to return the proper Class
      # - Symbol: Will be called as method of the ResourceBuilder itself
      def detect_resource_class(object)
        type = object['sys'] && object['sys']['type']
        case res_class = resource_mapping[type]
        when Symbol
          public_send(res_class, object)
        when Proc
          res_class[object]
        when nil
          fail UnparsableResource, response
        else
          res_class
        end
      end

      # The default mapping for #detect_resource_class
      def default_resource_mapping
        DEFAULT_RESOURCE_MAPPING.dup
      end

      # The default entry mapping
      def default_entry_mapping
        DEFAULT_ENTRY_MAPPING.dup
      end

      private

      def detect_child_objects(object)
        if object.is_a? Hash
          object.select { |_key, value| value.is_a?(Hash) && value.key?('sys') }
        else
          {}
        end
      end

      def detect_child_arrays(object)
        if object.is_a? Hash
          object.select do |_, value|
            value.is_a?(::Array) &&
              value.first &&
              value.first.is_a?(Hash) &&
              value.first.key?('sys')
          end
        else
          {}
        end
      end

      def add_to_known_resources(res)
        @known_resources[res.type][res.id] = res if res.sys && res.id && res.type != 'Link'
      end

      def replace_children(res, object)
        object.each do |name, potential_objects|
          detect_child_objects(potential_objects).each do |child_name, child_object|
            res.public_send(name)[child_name.to_sym] = create_resource(child_object)
          end
          next if %w[includes metadata].include?(name)
          detect_child_arrays(potential_objects).each do |child_name, _child_array|
            replace_child_array res.public_send(name)[child_name.to_sym]
          end
        end
      end

      def replace_child_array(child_array)
        child_array.map! { |resource_object| create_resource(resource_object) }
      end

      def create_included_resources!(included_objects)
        return unless included_objects

        included_objects.each do |type, objects|
          @included_resources[type] = Hash[
            objects.map do |object|
              res = create_resource(object)
              [res.id, res]
            end
          ]
        end
      end

      def replace_links_with_known_resources(res, seen_resource_ids = [])
        seen_resource_ids << res.id

        property_containers = %i[properties sys fields].map do |property_container_name|
          res.public_send(property_container_name)
        end.compact

        property_containers.each do |property_container|
          replace_links_in_properties(property_container, seen_resource_ids)
        end

        replace_links_in_array res.items, seen_resource_ids if res.array?
      end

      def replace_links_in_properties(property_container, seen_resource_ids)
        property_container.each do |property_name, property_value|
          if property_value.is_a? ::Array
            replace_links_in_array property_value, seen_resource_ids
          else
            replace_link_or_check_recursively property_value, property_container, property_name, seen_resource_ids
          end
        end
      end

      def replace_links_in_array(property_container, seen_resource_ids)
        property_container.each.with_index do |child_property, property_index|
          replace_link_or_check_recursively child_property, property_container, property_index, seen_resource_ids
        end
      end

      def replace_link_or_check_recursively(property_value, property_container, property_name, seen_resource_ids)
        if property_value.is_a? Link
          maybe_replace_link(property_value, property_container, property_name)
        elsif property_value.is_a?(Resource) && property_value.sys && !seen_resource_ids.include?(property_value.id)
          replace_links_with_known_resources(property_value, seen_resource_ids)
        end
      end

      def maybe_replace_link(link, parent, key)
        return unless @known_resources[link.link_type] &&
                      @known_resources[link.link_type].key?(link.id)

        parent[key] = @known_resources[link.link_type][link.id]
      end

      def replace_links_in_included_resources_with_known_resources
        @included_resources.each do |_, for_type|
          for_type.each do |_, res|
            replace_links_with_known_resources(res)
          end
        end
      end
    end
  end
end
