# frozen_string_literal: true

require "hanami/model/types"
require "rom/types"

module Hanami
  module Model
    module Sql
      # Types definitions for SQL databases
      #
      # @since 0.7.0
      module Types
        include Dry::Types.module

        # Types for schema definitions
        #
        # @since 0.7.0
        module Schema
          require "hanami/model/sql/types/schema/coercions"

          String   = Types::Optional::Coercible::String

          Int      = Types::Strict::Nil | Types::Int.constructor(Coercions.method(:int))
          Float    = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:float))
          Decimal  = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:decimal))

          Bool     = Types::Strict::Nil | Types::Strict::Bool

          Date     = Types::Strict::Nil | Types::Date.constructor(Coercions.method(:date))
          DateTime = Types::Strict::Nil | Types::DateTime.constructor(Coercions.method(:datetime))
          Time     = Types::Strict::Nil | Types::Time.constructor(Coercions.method(:time))

          Array    = Types::Strict::Nil | Types::Array.constructor(Coercions.method(:array))
          Hash     = Types::Strict::Nil | Types::Hash.constructor(Coercions.method(:hash))

          PG_JSON  = Types::Strict::Nil | Types::Any.constructor(Coercions.method(:pg_json))

          # @since 0.7.0
          # @api private
          MAPPING = {
            Types::String.pristine => Schema::String,
            Types::Int.pristine => Schema::Int,
            Types::Float.pristine => Schema::Float,
            Types::Decimal.pristine => Schema::Decimal,
            Types::Bool.pristine => Schema::Bool,
            Types::Date.pristine => Schema::Date,
            Types::DateTime.pristine => Schema::DateTime,
            Types::Time.pristine => Schema::Time,
            Types::Array.pristine => Schema::Array,
            Types::Hash.pristine => Schema::Hash,
            Types::String.optional.pristine => Schema::String,
            Types::Int.optional.pristine => Schema::Int,
            Types::Float.optional.pristine => Schema::Float,
            Types::Decimal.optional.pristine => Schema::Decimal,
            Types::Bool.optional.pristine => Schema::Bool,
            Types::Date.optional.pristine => Schema::Date,
            Types::DateTime.optional.pristine => Schema::DateTime,
            Types::Time.optional.pristine => Schema::Time,
            Types::Array.optional.pristine => Schema::Array,
            Types::Hash.optional.pristine => Schema::Hash
          }.freeze

          # Convert given type into coercible
          #
          # @since 0.7.0
          # @api private
          def self.coercible(attribute)
            return attribute if attribute.constrained?

            type      = attribute.type
            unwrapped = type.optional? ? type.right : type

            # NOTE: In the future rom-sql should be able to always return Ruby
            # types instead of Sequel types. When that will happen we can get
            # rid of this logic in the block and fall back to:
            #
            #  MAPPING.fetch(unwrapped.pristine, attribute)
            MAPPING.fetch(unwrapped.pristine) do
              if pg_json?(unwrapped.pristine)
                Schema::PG_JSON
              else
                attribute
              end
            end
          end

          # @since 1.0.4
          # @api private
          def self.pg_json_pristines
            @pg_json_pristines ||= ::Hash.new do |hash, type|
              hash[type] = (ROM::SQL::Types::PG.const_get(type).pristine if defined?(ROM::SQL::Types::PG))
            end
          end

          # @since 1.0.2
          # @api private
          def self.pg_json?(pristine)
            pristine == pg_json_pristines["JSONB"] || # rubocop:disable Style/MultipleComparison
              pristine == pg_json_pristines["JSON"]
          end

          private_class_method :pg_json?

          # Coercer for SQL associations target
          #
          # @since 0.7.0
          # @api private
          class AssociationType < Hanami::Model::Types::Schema::CoercibleType
            # Check if value can be coerced
            #
            # @param value [Object] the value
            #
            # @return [TrueClass,FalseClass] the result of the check
            #
            # @since 0.7.0
            # @api private
            def valid?(value)
              value.inspect =~ /\[#{primitive}\]/ || super
            end

            # @since 0.7.0
            # @api private
            def success(*args)
              result(Dry::Types::Result::Success, primitive.new(args.first.to_h))
            end
          end
        end
      end
    end
  end
end
