module VCAP::Services::ServiceBrokers::V2
  class Catalog
    attr_reader :service_broker, :services, :plans, :errors, :incompatibility_errors

    alias_method :validation_errors, :errors

    def initialize(service_broker, catalog_hash)
      @service_broker = service_broker
      @services = []
      @plans = []
      @errors = VCAP::Services::ValidationErrors.new
      @incompatibility_errors = VCAP::Services::ValidationErrors.new

      catalog_hash.fetch('services', []).each do |service_attrs|
        service = CatalogService.new(service_broker, service_attrs)
        @services << service
        @plans += service.plans
      end
    end

    def valid?
      validate_services
      validate_all_service_ids_are_unique
      validate_all_service_names_are_unique
      validate_all_plan_ids_are_unique
      validate_all_service_dashboard_clients_are_unique
      validation_errors.empty?
    end

    def compatible?
      services.each do |service|
        if service.route_service? && route_services_disabled?
          incompatibility_errors.add("Service #{service.name} is declared to be a route service but support for route services is disabled.")
        end

        if service.volume_mount_service? && volume_services_disabled?
          incompatibility_errors.add("Service #{service.name} is declared to be a volume mount service but support for volume mount services is disabled.")
        end
      end
      incompatibility_errors.empty?
    end

    private

    def validate_all_service_dashboard_clients_are_unique
      dashboard_clients = valid_dashboard_clients(services)
      dashboard_client_ids = valid_dashboard_client_ids(dashboard_clients)
      return unless has_duplicates?(dashboard_client_ids)

      validation_errors.add('Service dashboard_client id must be unique')
    end

    def valid_dashboard_client_ids(clients)
      clients.pluck('id').compact
    end

    def valid_dashboard_clients(services)
      services.map(&:dashboard_client).select { |client| client.is_a? Hash }
    end

    def validate_all_service_ids_are_unique
      return unless has_duplicates?(services.map(&:broker_provided_id).compact)

      validation_errors.add('Service ids must be unique')
    end

    def validate_all_service_names_are_unique
      new_services_names = services.map(&:name)
      validation_errors.add('Service names must be unique within a broker') if has_duplicates?(new_services_names)

      return unless service_broker.exists?

      taken_names = taken_names(new_services_names)
      return if taken_names.empty?

      validation_errors.add("Service names must be unique within a broker. Services with names #{taken_names} already exist")
    end

    def validate_all_plan_ids_are_unique
      catalog_plans = {}
      services.each do |s|
        s.plans.each do |p|
          if catalog_plans[p.broker_provided_id]
            validation_errors.add('Plan ids must be unique. ' \
                                  "Unable to register plan with id '#{p.broker_provided_id}' " \
                                  "(plan name '#{p.name}', service name '#{s.name}') " \
                                  'because it uses the same id as another plan in the catalog ' \
                                  "(plan name '#{catalog_plans[p.broker_provided_id][:plan].name}', service name '#{catalog_plans[p.broker_provided_id][:service].name}')")
          end
          catalog_plans[p.broker_provided_id] = { service: s, plan: p }
        end
      end

      service_broker.service_plans.each do |p|
        next unless catalog_plans[p.unique_id] && !updating_service?(catalog_plans[p.unique_id][:service], p.service)

        validation_errors.add('Plan ids must be unique. ' \
                              "Unable to register plan with id '#{p.unique_id}' " \
                              "(plan name '#{catalog_plans[p.unique_id][:plan].name}', " \
                              "service name '#{catalog_plans[p.unique_id][:service].name}') " \
                              'because it uses the same id as an existing plan ' \
                              "(plan name '#{p.name}', " \
                              "service name '#{p.service.name}', " \
                              "broker name '#{p.service_broker.name}')")
      end
    end

    def taken_names(new_services_names)
      clashing_names = []
      clashing_services = service_broker.services_dataset.where(label: new_services_names)
      clashing_services.each do |old_service|
        new_service = services.detect { |ns| ns.name == old_service.name }
        next if updating_service?(new_service, old_service)

        clashing_names << new_service.name unless can_delete_service?(old_service)
      end
      clashing_names
    end

    def can_delete_service?(service)
      VCAP::CloudController::ServiceInstance.
        join(:service_plans, id: :service_instances__service_plan_id).
        where(service_plans__service_id: service.id).
        empty?
    end

    def updating_service?(new_service, old_service)
      new_service.broker_provided_id == old_service.unique_id
    end

    def has_duplicates?(array)
      array.uniq.count < array.count
    end

    def validate_services
      services.each do |service|
        validation_errors.add_nested(service, service.errors) unless service.valid?
      end

      validation_errors.add('Service broker must provide at least one service') if services.empty?
    end

    def volume_services_disabled?
      !VCAP::CloudController::Config.config.get(:volume_services_enabled)
    end

    def route_services_disabled?
      !VCAP::CloudController::Config.config.get(:route_services_enabled)
    end
  end
end
