require 'uuid'
require 'sitehub/equality'
require 'sitehub/getter_setter_methods'
require_relative 'collection/split_route_collection'
require_relative 'rules'
require_relative 'resolver'
require_relative 'collection/route_collection'
require_relative 'middleware'
require_relative 'forward_proxy'
require_relative 'downstream_client'

class SiteHub
  class RouteBuilder
    class InvalidDefinitionException < Exception
    end

    INVALID_SPLIT_MSG = 'url must be defined if not supplying a block'.freeze
    ROUTES_WITH_SPLITS_MSG = 'you cant register routes and splits at the same level'.freeze
    INVALID_ROUTE_DEF_MSG = 'rule must be specified when supplying a block'.freeze
    IGNORING_URL_LABEL_MSG = 'Block supplied, ignoring URL and Label parameters'.freeze
    URL_REQUIRED_MSG = 'URL must be supplied for splits and routes'.freeze

    class << self
      # TODO: support nest splits and routes
      def from_hash(hash, sitehub_cookie_name)
        new(sitehub_cookie_name: sitehub_cookie_name, sitehub_cookie_path: hash[:sitehub_cookie_path], mapped_path: hash[:path]) do
          extend CollectionMethods

          collection(hash, :splits).each do |split|
            split(percentage: split[:percentage], url: split[:url], label: split[:label])
          end

          collection(hash, :routes).each do |route|
            route(url: route[:url], label: route[:label])
          end

          default url: hash[:default] if hash[:default]
        end
      end
    end

    extend GetterSetterMethods
    include Rules, Equality, Middleware

    transient :id

    getter_setters :default_proxy, :sitehub_cookie_path, :sitehub_cookie_name
    attr_reader :mapped_path, :id

    def initialize(sitehub_cookie_name:, sitehub_cookie_path: nil, mapped_path:, rule: nil, &block)
      @id = UUID.generate(:compact)
      @mapped_path = mapped_path
      @sitehub_cookie_name = sitehub_cookie_name
      @sitehub_cookie_path = sitehub_cookie_path
      @splits = Collection::SplitRouteCollection.new
      @routes = Collection::RouteCollection.new
      rule(rule)

      return unless block_given?

      instance_eval(&block)
      raise InvalidDefinitionException unless valid?
    end

    def build
      if middleware?
        build_with_middleware
        build_default_with_middleware if default_proxy
      end

      self
    end

    def default(url:)
      default_proxy(forward_proxy(label: :default, url: url))
    end

    def endpoints(collection = nil)
      return @endpoints || Collection::RouteCollection.new unless collection

      raise InvalidDefinitionException, ROUTES_WITH_SPLITS_MSG if @endpoints && @endpoints != collection
      @endpoints = collection
    end

    def forward_proxy(label:, url:, rule: nil)
      proxy = ForwardProxy.new(mapped_url: url, mapped_path: mapped_path)

      id = (label || UUID.generate(:compact)).to_sym
      Route.new(proxy,
                id: id,
                sitehub_cookie_path: sitehub_cookie_path,
                sitehub_cookie_name: sitehub_cookie_name).tap do |wrapper|
        wrapper.rule(rule)
      end
    end

    def resolve(id: nil, env:)
      id = id.to_s.to_sym
      endpoints[id] || endpoints.resolve(env: env) || default_proxy
    end

    def route(url: nil, label: nil, rule: nil, &block)
      endpoint = if block
                   raise InvalidDefinitionException, INVALID_ROUTE_DEF_MSG unless rule
                   warn(IGNORING_URL_LABEL_MSG) if url || label
                   new(rule: rule, &block).build
                 else
                   forward_proxy(url: url, label: label, rule: rule)
                 end

      routes.add(endpoint.id, endpoint)
    end

    def split(percentage:, url: nil, label: nil, &block)
      raise InvalidDefinitionException, INVALID_SPLIT_MSG unless block || url

      proxy = if block
                warn(IGNORING_URL_LABEL_MSG) if url || label
                new(&block).build
              else
                forward_proxy(label: label, url: url)
              end

      splits.add proxy.id, proxy, percentage
    end

    def valid?
      return true if default_proxy
      endpoints.valid?
    end

    private

    def add_middleware_to_proxy(proxy)
      middlewares.each do |middleware_args_and_block|
        middleware_class, args, block = middleware_args_and_block
        proxy.use middleware_class, *args, &block
      end
    end

    def build_default_with_middleware
      add_middleware_to_proxy(default_proxy)
      default_proxy.init
    end

    def build_with_middleware
      endpoints.values.each do |proxy|
        add_middleware_to_proxy(proxy)
        proxy.init
      end
    end

    def new(rule: nil, &block)
      self.class.new(sitehub_cookie_name: sitehub_cookie_name,
                     mapped_path: mapped_path,
                     rule: rule,
                     &block).tap do |builder|
        builder.sitehub_cookie_name sitehub_cookie_name
        builder.sitehub_cookie_path sitehub_cookie_path
      end
    end

    def splits
      endpoints(@splits)
    end

    def routes
      endpoints(@routes)
    end
  end
end