# frozen_string_literal: true

require 'open_api/dsl/helpers'

module OpenApi
  module DSL
    class Api < Hash
      include DSL::Helpers

      attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_order

      def initialize(action_path = '', summary: nil, tags: [ ], id: nil)
        self.action_path = action_path
        self.dry_blocks  = [ ]
        self.merge!(summary: summary, operationId: id, tags: tags, description: '', parameters: [ ],
                    requestBody: nil, responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ])
      end

      def this_api_is_invalid!(*)
        self[:deprecated] = true
      end

      alias this_api_is_expired!      this_api_is_invalid!
      alias this_api_is_unused!       this_api_is_invalid!
      alias this_api_is_under_repair! this_api_is_invalid!

      def desc desc
        self[:description] = desc
      end

      alias description desc

      def dry only: nil, skip: nil, none: false
        return if dry_blocks.blank? || dryed
        self.dry_skip = skip && Array(skip)
        self.dry_only = none ? [:none] : only && Array(only)
        dry_blocks.each { |blk| instance_eval(&blk) }
        self.dry_skip = self.dry_only = nil
        self.dryed = true
      end

      def param param_type, name, type, required, schema = { }
        return if dry_skip&.include?(name) || dry_only&.exclude?(name)

        return unless schema = process_schema_input(type, schema, name)
        param_obj = ParamObj.new(name, param_type, type, required, schema)
        # The definition of the same name parameter will be overwritten
        index = self[:parameters].map(&:name).index(param_obj.name)
        index ? self[:parameters][index] = param_obj : self[:parameters] << param_obj
      end

      alias parameter param

      %i[ header header! path path! query query! cookie cookie! ].each do |param_type|
        define_method param_type do |name, type = nil, **schema|
          param param_type, name, type, (param_type['!'] ? :req : :opt), schema
        end

        define_method "in_#{param_type}" do |params|
          params.each_pair do |param_name, schema|
            param param_type, param_name, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema
          end
        end
      end

      def param_ref component_key, *keys
        self[:parameters] += [component_key, *keys].map { |key| RefObj.new(:parameter, key) }
      end

      # options: `exp_params` and `examples`
      def request_body required, media_type, data: { }, desc: '', **options
        (self[:requestBody] ||= RequestBodyObj.new(required, desc)).absorb(media_type, { data: data , **options })
      end

      def body_ref component_key
        self[:requestBody] = RefObj.new(:requestBody, component_key)
      end

      %i[ body body! ].each do |method|
        define_method method do |media_type, data: { }, **options|
          request_body (method['!'] ? :req : :opt), media_type, data: data, **options
        end
      end

      def form data:, **options
        body :form, data: data, **options
      end

      def form! data:, **options
        body! :form, data: data, **options
      end

      def data name, type = nil, schema = { }
        schema[:type] = type if type.present?
        form data: { name => schema }
      end

      def response code, desc, media_type = nil, headers: { }, data: { }, **options
        (self[:responses][code.to_s] ||= ResponseObj.new(desc)).absorb(desc, media_type, headers: headers, data: data, **options)
      end

      alias_method :resp,  :response
      alias_method :error, :response

      def response_ref code_and_compkey # = { }
        code_and_compkey.each { |code, component_key| self[:responses][code.to_s] = RefObj.new(:response, component_key) }
      end

      def security_require scheme_name, scopes: [ ]
        self[:security] << { scheme_name => scopes }
      end

      alias security  security_require
      alias auth      security_require
      alias auth_with security_require

      def callback event_name, http_method, callback_url, &block
        self[:callbacks].deep_merge! CallbackObj.new(event_name, http_method, callback_url, &block).process
      end

      def server url, desc: ''
        self[:servers] << { url: url, description: desc }
      end

      def param_examples exp_params = :all, examples_hash
        exp_params = self[:parameters].map(&:name) if exp_params == :all
        self[:examples] = ExampleObj.new(examples_hash, exp_params, multiple: true).process
      end

      alias examples param_examples

      def run_dsl(dry: false, &block)
        instance_exec(&block) if block_given?
        dry() if dry

        self[:parameters].map!(&:process)
        self[:requestBody] = self[:requestBody].try(:process)
        self[:responses].each { |code, response| self[:responses][code] = response.process }
        self[:responses] = self[:responses].sort.to_h
        self.delete_if { |_, v| v.blank? }
      end
    end
  end
end
