# frozen_string_literal: true

require 'roda'
require 'roda/endpoints/endpoint/collection'
require 'roda/endpoints/endpoint/item'
require 'rom/struct/to_json'

class Roda
  # Module containing {Roda} plugins.
  module RodaPlugins
    # Endpoints plugin for {Roda}
    module Endpoints
      # @param [Class(Roda)] app
      def self.load_dependencies(app, **_opts)
        app.plugin :all_verbs
        app.plugin :head
        app.plugin :caching
        app.plugin :monads
        app.plugin :symbol_status
        app.plugin :symbol_matchers
        app.plugin :slash_path_empty
        app.plugin :json_parser
        app.plugin :indifferent_params
        app.plugin :json, classes: [Array, Hash, ROM::Struct]
        app.plugin :flow
      end

      # @param [Class(Roda)] app
      # @param [Hash] opts
      def self.configure(app, container: app, **opts)
        opts = (app.opts[:endpoints] || {}).merge(opts)
        unless container.respond_to? :resolve
          require 'dry-container'
          container.extend Dry::Container::Mixin
        end
        app.opts[:endpoints] = opts.merge(container: container)
        Roda::Endpoints.roda_class ||= app
      end

      # {ClassMethods#register `Roda.register`} &&
      # {ClassMethods#merge `Roda.merge`}
      module ClassMethods
        # @param [String, Symbol] name
        # @param [Array] args
        # @param [Proc] block
        def register(name, *args, &block)
          opts[:container].register(name, *args, &block)
        end

        # @param [String, Symbol] name
        # @param [Array] args
        # @param [Proc] block
        def merge(name, *args, &block)
          opts[:container].merge(name, *args, &block)
        end
      end

      # `Roda::RodaRequest` instant extensions.
      # rubocop:disable Metrics/ModuleLength
      module RequestMethods
        # Implements collection endpoint using given args
        #
        # @param name [Symbol]
        # @param item [{Symbol=>Object}]
        # @param kwargs [{Symbol=>Object}]
        # @param type [Class(Roda::Endpoints::Endpoint::Collection)]
        # @param on [String, Symbol]
        # @param (see Endpoint::Collection#initialize)
        # @see Endpoint::Collection.defaults
        # @yieldparam endpoint [Collection]
        # @yieldreturn [#to_json]
        # @return [Endpoint]
        #
        # @example
        #   r.collection :articles, item: { only: %i(get delete) }
        #
        #   # is equivalent to
        #
        #   r.on 'articles', item: { only: %i(get delete) } do
        #     articles = Endpoint.new(
        #       name: :articles,
        #       container: container,
        #     )
        #
        #     r.last_modified articles.last_modified
        #
        #     r.get do
        #       articles.call(:get, r.params)
        #     end
        #
        #     r.post do
        #       articles.call(:post, r.params)
        #     end
        #
        #     r.child :id, only: %i(get delete)
        #   end
        def collection(name,
                       item: { on: :id },
                       type: Roda::Endpoints::Endpoint::Collection,
                       on: name.to_s,
                       **kwargs)
          endpoint name: name,
                   item: item,
                   type: type,
                   on: on,
                   **kwargs do |endpoint|
            yield endpoint if block_given?
          end
        end

        # rubocop:enable Metrics/MethodLength, Metrics/AbcSize

        # @param on [Symbol]
        # @param kwargs [Hash]
        #
        # @example
        #   r.collection :articles do |articles|
        #     r.item :id
        #   end
        #
        #   # is equivalent to
        #
        #   r.collection :articles do |articles|
        #     r.on :id do |id|
        #
        #       def article
        #         @article ||= articles.repository.fetch(id)
        #       end
        #
        #       r.get do
        #         article
        #       end
        #     end
        #   end
        def item(on: :id,
                 type: Roda::Endpoints::Endpoint::Item,
                 **kwargs)
          unless current_endpoint.is_a?(Roda::Endpoints::Endpoint::Collection)
            raise ArgumentError,
                  "#{self.class}#item called not within a collection endpoint"
          end
          # @route /{collection.name}/{id}
          endpoint(
            on: on,
            name: current_endpoint.item_name,
            type: type,
            **kwargs
          ) do |endpoint|
            yield endpoint if block_given?
          end
        end

        # @overload singleton(name, entity, type:, **kwargs)
        # @overload singleton(name:, entity:, type:, **kwargs)
        def singleton(*args,
                      name: args.first,
                      entity: args.last,
                      type: Roda::Endpoints::Endpoint::Singleton,
                      **kwargs)
          endpoint(
            name: name,
            entity: entity,
            type: type,
            **kwargs
          ) do |endpoint|
            yield endpoint if block_given?
          end
        end

        private

        # @param [Class(Roda::Endpoints::Endpoint::Singleton)] type
        # @param [Dry::Container::Mixin, #register, #resolve, #merge] container
        # @param [Roda::Endpoints::Endpoint] parent
        # @param [Hash] kwargs
        # rubocop:disable Metrics/ParameterLists
        def endpoint(name:,
                     type:,
                     container: roda_class.opts[:endpoints][:container],
                     parent: current_endpoint,
                     on: name,
                     **kwargs)
          on on do |*captures|
            with_current_endpoint parent.child(
              name: name,
              type: type,
              container: container,
              parent: parent,
              on: on,
              captures: captures,
              **kwargs
            ) do |endpoint|
              yield endpoint if block_given?
              instance_exec(self, endpoint, &endpoint.route)
            end
          end
        end

        # @param [Symbol] verb
        def match_transaction(verb)
          resolve current_endpoint.transactions.key_for(verb) do |transaction|
            transaction.call(params) do |m|
              statuses = current_endpoint.class.statuses[verb]

              m.success do |result|
                response.status = statuses[:success]
                result
              end

              m.failure do |result|
                if result.is_a?(Array) && result.size == 2
                  response.status, value = result
                  value
                else
                  response.status = statuses[:failure]
                  result
                end
              end
            end
          end
        end

        # @return [Endpoint]
        def current_endpoint
          endpoints.last
        end

        # @return [<Endpoint>]
        def endpoints
          @endpoints ||= [Roda::Endpoints::Endpoint.new(
            name: :root,
            ns: nil,
            container: roda_class.opts[:endpoints][:container]
          )]
        end

        def with_current_endpoint(endpoint)
          endpoints.push endpoint
          yield endpoint
          endpoints.pop
        end
      end
      # rubocop:enable Metrics/ModuleLength
    end

    register_plugin :endpoints, Endpoints
  end
end