require 'erb'
require 'delegate'
require 'pact_broker/client/error'
require 'pact_broker/client/hal/link'
require 'pact_broker/client/hal/links'

module PactBroker
  module Client
    module Hal
      class RelationNotFoundError < ::PactBroker::Client::Error; end
      class EmbeddedEntityNotFoundError < ::PactBroker::Client::Error; end
      class ErrorResponseReturned < ::PactBroker::Client::Error
        attr_reader :entity

        def initialize(message, entity)
          super(message)
          @entity = entity
        end
      end

      class Entity
        def initialize(href, data, http_client, response = nil)
          @href = href
          @data = data
          @links = (@data || {}).fetch("_links", {})
          @client = http_client
          @response = response
        end

        def get(key, *args)
          _link(key).get(*args)
        end

        def get!(key, *args)
          get(key, *args).assert_success!
        end

        def post(key, *args)
          _link(key).post(*args)
        end

        def post!(key, *args)
          post(key, *args).assert_success!
        end

        def put(key, *args)
          _link(key).put(*args)
        end

        def put!(key, *args)
          put(key, *args).assert_success!
        end

        def patch(key, *args)
          _link(key).patch(*args)
        end

        def patch!(key, *args)
          patch(key, *args).assert_success!
        end

        def can?(key)
          @links.key? key.to_s
        end

        def follow(key, http_method, *args)
          Link.new(@links[key].merge(method: http_method), @client).run(*args)
        end

        def _link(key, fallback_key = nil)
          if @links[key]
            Link.new(@links[key], @client)
          elsif fallback_key && @links[fallback_key]
            Link.new(@links[fallback_key], @client)
          else
            nil
          end
        end

        def _links(key)
          if @links[key] && @links[key].is_a?(Array)
            link_collection = @links[key].collect do | hash |
              Link.new(hash, @client)
            end
            Links.new(@href, key, link_collection)
          elsif @links[key].is_a?(Hash)
            Links.new(@href, key, [Link.new(@links[key], @client)])
          else
            nil
          end
        end

        def _link!(key)
          _link(key) or raise RelationNotFoundError.new(relation_not_found_error_message(key, @href))
        end

        def _links!(key)
          _links(key) or raise RelationNotFoundError.new(relation_not_found_error_message(key, @href))
        end

        def embedded_entity
          embedded_ent = yield @data["_embedded"]
          if embedded_ent
            Entity.new(self_href(embedded_ent), embedded_ent, @client, response)
          end
        end

        def embedded_entities!(key)
          embedded_ents = (@data["_embedded"] && @data["_embedded"][key])
          raise EmbeddedEntityNotFoundError.new("Could not find embedded entity with key '#{key}' in resource at #{@href}") unless embedded_ents
          embedded_ents.collect do | embedded_ent |
            Entity.new(self_href(embedded_ent), embedded_ent, @client, response)
          end
        end

        def embedded_entities(key = nil)
          embedded_ents = if key
            @data["_embedded"][key]
          else
            yield @data["_embedded"]
          end
          embedded_ents.collect do | embedded_ent |
            Entity.new(self_href(embedded_ent), embedded_ent, @client, response)
          end
        end

        def success?
          true
        end

        def does_not_exist?
          false
        end

        def response
          @response
        end

        def fetch(key, fallback_key = nil)
          @links[key] || (fallback_key && @links[fallback_key])
        end

        def method_missing(method_name, *args, &block)
          if @data.respond_to?(:key?) && @data.key?(method_name.to_s)
            @data[method_name.to_s]
          elsif @links.respond_to?(:key?) && @links.key?(method_name)
            Link.new(@links[method_name], @client).run(*args)
          else
            nil
          end
        end

        def respond_to_missing?(method_name, include_private = false)
          @data.key?(method_name) || @links.key?(method_name)
        end

        def assert_success!(_ignored = nil)
          self
        end

        def self_href(entity_hash)
          entity_hash["_links"] && entity_hash["_links"]["self"] && entity_hash["_links"]["self"]["href"]
        end

        def relation_not_found_error_message(key, href)
          "Could not find relation '#{key}' in resource at #{href}. The most likely reason for this is that you are on an old version of the Pact Broker and you need to upgrade, or you are using PactFlow and you don't have the permissions required for this action."
        end
      end

      class ErrorEntity < Entity
        def initialize(href, data, http_client, response = nil)
          @href = href
          @data = data
          @links = {}
          @client = http_client
          @response = response
        end

        def does_not_exist?
          response && response.status == 404
        end

        def success?
          false
        end

        def error_message(messages = {})
          default_message = "Error making request to #{@href} status=#{response ? response.status: nil} #{response ? response.raw_body : ''}".strip
          message = if response && messages[response.status]
            (messages[response.status] || "") + " (#{default_message})"
          else
            default_message
          end
        end

        def assert_success!(messages = {})
          raise ErrorResponseReturned.new(error_message(messages), self)
        end
      end
    end
  end
end
