# frozen_string_literal: true

require 'jsonapi/formatter'
require 'jsonapi/processor'
require 'concurrent'

module JSONAPI
  class Configuration
    attr_reader :json_key_format,
                :resource_key_type,
                :route_format,
                :raise_if_parameters_not_allowed,
                :warn_on_route_setup_issues,
                :warn_on_missing_routes,
                :warn_on_performance_issues,
                :default_allow_include_to_one,
                :default_allow_include_to_many,
                :allow_sort,
                :allow_filter,
                :default_paginator,
                :default_page_size,
                :maximum_page_size,
                :default_processor_klass_name,
                :use_text_errors,
                :top_level_links_include_pagination,
                :top_level_meta_include_record_count,
                :top_level_meta_record_count_key,
                :top_level_meta_include_page_count,
                :top_level_meta_page_count_key,
                :allow_transactions,
                :include_backtraces_in_errors,
                :include_application_backtraces_in_errors,
                :exception_class_allowlist,
                :allow_all_exceptions,
                :always_include_to_one_linkage_data,
                :always_include_to_many_linkage_data,
                :cache_formatters,
                :use_relationship_reflection,
                :resource_cache,
                :default_caching,
                :default_resource_cache_field,
                :resource_cache_digest_function,
                :resource_cache_usage_report_function,
                :default_exclude_links

    def initialize
      #:underscored_key, :camelized_key, :dasherized_key, or custom
      self.json_key_format = :dasherized_key

      #:underscored_route, :camelized_route, :dasherized_route, or custom
      self.route_format = :dasherized_route

      #:integer, :uuid, :string, or custom (provide a proc)
      self.resource_key_type = :integer

      # optional request features
      self.default_allow_include_to_one = true
      self.default_allow_include_to_many = true
      self.allow_sort = true
      self.allow_filter = true

      self.raise_if_parameters_not_allowed = true

      self.warn_on_route_setup_issues = true
      self.warn_on_missing_routes = true
      self.warn_on_performance_issues = true

      # :none, :offset, :paged, or a custom paginator name
      self.default_paginator = :none

      # Output pagination links at top level
      self.top_level_links_include_pagination = true

      self.default_page_size = 10
      self.maximum_page_size = 20

      # Metadata
      # Output record count in top level meta for find operation
      self.top_level_meta_include_record_count = false
      self.top_level_meta_record_count_key = :record_count

      self.top_level_meta_include_page_count = false
      self.top_level_meta_page_count_key = :page_count

      self.use_text_errors = false

      # Whether or not to include exception backtraces in JSONAPI error
      # responses.  Defaults to `false` in anything other than development or test.
      self.include_backtraces_in_errors = (Rails.env.development? || Rails.env.test?)

      # Whether or not to include exception application backtraces in JSONAPI error
      # responses.  Defaults to `false` in anything other than development or test.
      self.include_application_backtraces_in_errors = (Rails.env.development? || Rails.env.test?)

      # List of classes that should not be rescued by the operations processor.
      # For example, if you use Pundit for authorization, you might
      # raise a Pundit::NotAuthorizedError at some point during operations
      # processing. If you want to use Rails' `rescue_from` macro to
      # catch this error and render a 403 status code, you should add
      # the `Pundit::NotAuthorizedError` to the `exception_class_allowlist`.
      self.exception_class_allowlist = []

      # If enabled, will override configuration option `exception_class_allowlist`
      # and allow all exceptions.
      self.allow_all_exceptions = false

      # Resource Linkage
      # Controls the serialization of resource linkage for non compound documents
      # NOTE: always_include_to_many_linkage_data is not currently implemented
      self.always_include_to_one_linkage_data = false
      self.always_include_to_many_linkage_data = false

      # The default Operation Processor to use if one is not defined specifically
      # for a Resource.
      self.default_processor_klass_name = 'JSONAPI::Processor'

      # Allows transactions for creating and updating records
      # Set this to false if your backend does not support transactions (e.g. Mongodb)
      self.allow_transactions = true

      # Formatter Caching
      # Set to false to disable caching of string operations on keys and links.
      # Note that unlike the resource cache, formatter caching is always done
      # internally in-memory and per-thread; no ActiveSupport::Cache is used.
      self.cache_formatters = true

      # Relationship reflection invokes the related resource when updates
      # are made to a has_many relationship. By default relationship_reflection
      # is turned off because it imposes a small performance penalty.
      self.use_relationship_reflection = false

      # Resource cache
      # An ActiveSupport::Cache::Store or similar, used by Resources with caching enabled.
      # Set to `nil` (the default) to disable caching, or to `Rails.cache` to use the
      # Rails cache store.
      self.resource_cache = nil

      # Cache resources by default
      # Cache resources by default. Individual resources can be excluded from caching by calling:
      # `caching false`
      self.default_caching = false

      # Default resource cache field
      # On Resources with caching enabled, this field will be used to check for out-of-date
      # cache entries, unless overridden on a specific Resource. Defaults to "updated_at".
      self.default_resource_cache_field = :updated_at

      # Resource cache digest function
      # Provide a callable that returns a unique value for string inputs with
      # low chance of collision. The default is SHA256 base64.
      self.resource_cache_digest_function = Digest::SHA2.new.method(:base64digest)

      # Resource cache usage reporting
      # Optionally provide a callable which JSONAPI will call with information about cache
      # performance. Should accept three arguments: resource name, hits count, misses count.
      self.resource_cache_usage_report_function = nil

      # Global configuration for links exclusion
      # Controls whether to generate links like `self`, `related` with all the resources
      # and relationships. Accepts either `:default`, `:none`, or array containing the
      # specific default links to exclude, which may be `:self` and `:related`.
      self.default_exclude_links = :none
    end

    def cache_formatters=(bool)
      @cache_formatters = bool
      if bool
        @key_formatter_tlv = Concurrent::ThreadLocalVar.new
        @route_formatter_tlv = Concurrent::ThreadLocalVar.new
      else
        @key_formatter_tlv = nil
        @route_formatter_tlv = nil
      end
    end

    def json_key_format=(format)
      @json_key_format = format
      if defined?(@cache_formatters)
        @key_formatter_tlv = Concurrent::ThreadLocalVar.new
      end
    end

    def route_format=(format)
      @route_format = format
      if defined?(@cache_formatters)
        @route_formatter_tlv = Concurrent::ThreadLocalVar.new
      end
    end

    def key_formatter
      if self.cache_formatters
        formatter = @key_formatter_tlv.value
        return formatter if formatter
      end

      formatter = JSONAPI::Formatter.formatter_for(self.json_key_format)

      if self.cache_formatters
        formatter = @key_formatter_tlv.value = formatter.cached
      end

      return formatter
    end

    def resource_key_type=(key_type)
      @resource_key_type = key_type
    end

    def route_formatter
      if self.cache_formatters
        formatter = @route_formatter_tlv.value
        return formatter if formatter
      end

      formatter = JSONAPI::Formatter.formatter_for(self.route_format)

      if self.cache_formatters
        formatter = @route_formatter_tlv.value = formatter.cached
      end

      return formatter
    end

    def exception_class_allowed?(e)
      @allow_all_exceptions ||
        @exception_class_allowlist.flatten.any? { |k| e.class.ancestors.map(&:to_s).include?(k.to_s) }
    end

    def default_processor_klass=(default_processor_klass)
      ActiveSupport::Deprecation.warn('`default_processor_klass` has been replaced by `default_processor_klass_name`.')
      @default_processor_klass = default_processor_klass
    end

    def default_processor_klass
      @default_processor_klass ||= default_processor_klass_name.safe_constantize
    end

    def default_processor_klass_name=(default_processor_klass_name)
      @default_processor_klass = nil
      @default_processor_klass_name = default_processor_klass_name
    end

    def allow_include=(allow_include)
      ActiveSupport::Deprecation.warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.')
      @default_allow_include_to_one = allow_include
      @default_allow_include_to_many = allow_include
    end

    def whitelist_all_exceptions=(allow_all_exceptions)
      ActiveSupport::Deprecation.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
      @allow_all_exceptions = allow_all_exceptions
    end

    def exception_class_whitelist=(exception_class_allowlist)
      ActiveSupport::Deprecation.warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`')
      @exception_class_allowlist = exception_class_allowlist
    end

    attr_writer :allow_sort, :allow_filter, :default_allow_include_to_one, :default_allow_include_to_many

    attr_writer :default_paginator

    attr_writer :default_page_size

    attr_writer :maximum_page_size

    attr_writer :use_text_errors

    attr_writer :top_level_links_include_pagination

    attr_writer :top_level_meta_include_record_count

    attr_writer :top_level_meta_record_count_key

    attr_writer :top_level_meta_include_page_count

    attr_writer :top_level_meta_page_count_key

    attr_writer :allow_transactions

    attr_writer :include_backtraces_in_errors

    attr_writer :include_application_backtraces_in_errors

    attr_writer :exception_class_allowlist

    attr_writer :allow_all_exceptions

    attr_writer :always_include_to_one_linkage_data

    attr_writer :always_include_to_many_linkage_data

    attr_writer :raise_if_parameters_not_allowed

    attr_writer :warn_on_route_setup_issues

    attr_writer :warn_on_missing_routes

    attr_writer :warn_on_performance_issues

    attr_writer :use_relationship_reflection

    attr_writer :resource_cache

    attr_writer :default_caching

    attr_writer :default_resource_cache_field

    attr_writer :resource_cache_digest_function

    attr_writer :resource_cache_usage_report_function

    attr_writer :default_exclude_links
  end

  class << self
    attr_accessor :configuration
  end

  @configuration ||= Configuration.new

  def self.configure
    yield(@configuration)
  end
end
