require "pact_broker/repositories"
require "pact_broker/services"
require "pact_broker/logging"
require "pact_broker/pacts/merger"
require "pact_broker/pacts/verifiable_pact"
require "pact_broker/pacts/squash_pacts_for_verification"
require "pact_broker/events/publisher"
require "pact_broker/messages"
require "pact_broker/hash_refinements"

module PactBroker
  module Pacts
    module Service
      using PactBroker::HashRefinements

      extend self
      extend PactBroker::Events::Publisher

      extend PactBroker::Repositories
      extend PactBroker::Services
      include PactBroker::Logging
      extend PactBroker::Messages
      extend SquashPactsForVerification

      def generate_sha(json_content)
        PactBroker.configuration.sha_generator.call(json_content)
      end

      def find_latest_pact params
        pact_repository.find_latest_pact(params[:consumer_name], params[:provider_name], params[:tag], params[:branch_name])
      end

      def search_for_latest_pact params
        pact_repository.search_for_latest_pact(params[:consumer_name], params[:provider_name], params[:tag])
      end

      def find_latest_pacts
        pact_repository.find_latest_pacts
      end

      def find_pact params
        pact_repository.find_pact(params[:consumer_name], params[:consumer_version_number], params[:provider_name], params[:pact_version_sha])
      end

      def find_by_consumer_version params
        pact_repository.find_by_consumer_version(params[:consumer_name], params[:consumer_version_number])
      end

      def delete params
        logger.info("Deleting pact version", params)
        pacts = pact_repository.find_all_revisions(params[:consumer_name], params[:consumer_version_number], params[:provider_name])
        webhook_service.delete_all_webhook_related_objects_by_pact_publication_ids(pacts.collect(&:id))
        pact_repository.delete(params)
      end

      def create_or_update_pact params
        provider = pacticipant_repository.find_by_name_or_create params[:provider_name]
        consumer = pacticipant_repository.find_by_name_or_create params[:consumer_name]
        consumer_version = version_repository.find_by_pacticipant_id_and_number_or_create consumer.id, params[:consumer_version_number]
        existing_pact = pact_repository.find_by_version_and_provider(consumer_version.id, provider.id)

        if existing_pact
          create_pact_revision params, existing_pact
        else
          create_pact params, consumer_version, provider
        end
      end

      def merge_pact params
        provider = pacticipant_repository.find_by_name_or_create params[:provider_name]
        consumer = pacticipant_repository.find_by_name_or_create params[:consumer_name]
        consumer_version = version_repository.find_by_pacticipant_id_and_number_or_create consumer.id, params[:consumer_version_number]
        existing_pact = pact_repository.find_by_version_and_provider(consumer_version.id, provider.id)

        params[:json_content] = Merger.merge_pacts(
          existing_pact.json_content, params[:json_content]
        )

        create_pact_revision params, existing_pact
      end

      def find_all_pact_versions_between consumer, options
        pact_repository.find_all_pact_versions_between consumer, options
      end

      def delete_all_pact_publications_between consumer, options
        pact_repository.delete_all_pact_publications_between consumer, options
      end

      def delete_all_pact_versions_between consumer, options
        pact_repository.delete_all_pact_versions_between consumer, options
      end

      def find_latest_pacts_for_provider provider_name, options = {}
        pact_repository.find_latest_pacts_for_provider provider_name, options[:tag]
      end

      def find_latest_pacts_for_provider_by_consumer_branch provider_name, options = {}
        options[:latest] = true
        pact_repository.find_pacts_by_consumer_branch provider_name, options
      end

      def find_pacts_for_provider_by_consumer_branch provider_name, options = {}
        pact_repository.find_pacts_by_consumer_branch provider_name, options
      end

      def find_pacts_for_provider_and_consumer_by_consumer_branch provider_name, consumer, options = {}
        options[:consumer] = consumer
        pact_repository.find_pacts_by_consumer_branch provider_name, options
      end

      def find_latest_pacts_for_provider_and_consumer_by_consumer_branch provider_name, consumer, options = {}
        options[:consumer] = consumer
        options[:latest] = true
        pact_repository.find_pacts_by_consumer_branch provider_name, options
      end

      def find_pact_versions_for_provider provider_name, options = {}
        pact_repository.find_pact_versions_for_provider provider_name, options[:tag]
      end

      def find_previous_distinct_pact_version params
        pact = find_pact params
        return nil if pact.nil?
        pact_repository.find_previous_distinct_pact pact
      end

      def find_distinct_pacts_between consumer, options
        # Assumes pacts are sorted from newest to oldest
        all = pact_repository.find_all_pact_versions_between consumer, options
        distinct = []
        (0...all.size).each do | i |
          if i == all.size - 1
            distinct << all[i]
          else
            if all[i].json_content != all[i+1].json_content
              distinct << all[i]
            end
          end
        end
        distinct
      end

      def find_for_verification(provider_name, provider_version_branch, provider_version_tags, consumer_version_selectors, options)
        explicitly_specified_verifiable_pacts = pact_repository
          .find_for_verification(provider_name, consumer_version_selectors)
          .collect do | selected_pact |
            # Todo move this into the repository
            squash_pacts_for_verification(provider_version_tags, provider_version_branch, selected_pact, options[:include_pending_status])
          end

        verifiable_wip_pacts =  if options[:include_wip_pacts_since]
                                  pact_repository.find_wip_pact_versions_for_provider(provider_name, provider_version_branch, provider_version_tags, explicitly_specified_verifiable_pacts, options)
                                else
                                  []
                                end

        explicitly_specified_verifiable_pacts + verifiable_wip_pacts
      end

      def find_for_verification_publication(pact_params, consumer_version_selector_hashes)
        if consumer_version_selector_hashes&.any?
          selected_pacts = consumer_version_selector_hashes.collect do | consumer_version_selector_hash |
            find_pact_params = {
              consumer_name: pact_params.consumer_name,
              provider_name: pact_params.provider_name,
              pact_version_sha: pact_params.pact_version_sha,
              consumer_version_number: consumer_version_selector_hash[:consumer_version_number]
            }
            pact = find_pact(find_pact_params)
            resolved_selector = Selector.new(consumer_version_selector_hash.without(:consumer_version_number)).resolve(pact.consumer_version)
            SelectedPact.new(pact, Selectors.new(resolved_selector))
          end
          SelectedPact.merge_by_consumer_version_number(selected_pacts)
        else
          [SelectedPact.new(find_pact(pact_params), Selectors.new)]
        end
      end

      # Overwriting an existing pact with the same consumer/provider/consumer version number
      # by creating a new revision (that is, a new PactPublication with an incremented revision number)
      # Modifing pacts is strongly discouraged now, and support for it will be dropped in the next major version of the Pact Broker
      def create_pact_revision params, existing_pact
        logger.info("Updating existing pact publication", params.without(:json_content))
        logger.debug("Content #{params[:json_content]}")
        pact_version_sha = params.fetch(:pact_version_sha)
        json_content = add_interaction_ids(params[:json_content])
        update_params = { pact_version_sha: pact_version_sha, json_content: json_content }
        updated_pact = pact_repository.update(existing_pact.id, update_params)

        event_context = { consumer_version_tags: updated_pact.consumer_version_tag_names }
        event_params = { event_context: event_context, pact: updated_pact }

        broadcast(:contract_published, event_params)

        if existing_pact.pact_version_sha != updated_pact.pact_version_sha
          broadcast(:contract_content_changed, event_params.merge(event_comment: "pact content modified since previous publication for #{updated_pact.consumer_name} version #{updated_pact.consumer_version_number}"))
        else
          broadcast(:contract_content_unchanged, event_params.merge(event_comment: "pact content was unchanged"))
        end

        updated_pact
      end

      private :create_pact_revision

      def disallowed_modification?(existing_pact, new_pact_version_sha)
        !PactBroker.configuration.allow_dangerous_contract_modification && existing_pact && existing_pact.pact_version_sha != new_pact_version_sha
      end

      # When no publication for the given consumer/provider/consumer version number exists
      def create_pact(params, version, provider)
        logger.info("Creating new pact publication", params.without(:json_content))
        logger.debug("Content #{params[:json_content]}")
        json_content = add_interaction_ids(params[:json_content])
        pact = pact_repository.create(
          version_id: version.id,
          provider_id: provider.id,
          consumer_id: version.pacticipant_id,
          pact_version_sha: params.fetch(:pact_version_sha),
          json_content: json_content,
          version: version
        )

        event_params = { event_context: { consumer_version_tags: pact.consumer_version_tag_names }, pact: pact }
        content_changed, explanation = pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version?(pact)

        if content_changed
          broadcast(:contract_published, event_params)
          broadcast(:contract_content_changed, event_params.merge(event_comment: explanation))
        else
          broadcast(:contract_published, event_params.merge(event_comment: contract_published_event_comment(pact)))
        end

        pact
      end

      private :create_pact

      def add_interaction_ids(json_content)
        Content.from_json(json_content).with_ids.to_json
      end

      private :add_interaction_ids

      def pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version? pact
        changed_pacts = pact_repository
          .find_previous_pacts(pact)
          .reject { |_, previous_pact| !sha_changed_or_no_previous_version?(previous_pact, pact) }
        explanation = explanation_for_content_changed(changed_pacts)
        return changed_pacts.any?, explanation
      end

      private :pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version?

      def sha_changed_or_no_previous_version?(previous_pact, new_pact)
        previous_pact.nil? || new_pact.pact_version_sha != previous_pact.pact_version_sha
      end

      private :sha_changed_or_no_previous_version?

      def explanation_for_content_changed(changed_pacts)
        if changed_pacts.any?
          messages = changed_pacts.collect do |tag, previous_pact|
            if tag == :untagged
              if previous_pact
                "pact content has changed since previous untagged version"
              else
                "first time untagged pact published"
              end
            else
              if previous_pact
                "pact content has changed since the last consumer version tagged with #{tag}"
              else
                "first time any pact published for this consumer with consumer version tagged #{tag}"
              end
            end
          end
          messages.join(", ")
        end
      end

      private :explanation_for_content_changed

      def contract_published_event_comment pact
        if pact.consumer_version_tag_names.count == 1
          message("messages.events.pact_published_unchanged_with_single_tag", tag_name: pact.consumer_version_tag_names.first)
        else
          message("messages.events.pact_published_unchanged_with_multiple_tags", tag_names: pact.consumer_version_tag_names.join(", "))
        end
      end
    end
  end
end
