# frozen_string_literal: true

#  Copyright (c) 2018, Grünliberale Partei Schweiz. This file is part of
#  hitobito and licensed under the Affero General Public License version 3
#  or later. See the COPYING file at the top-level directory or at
#  https://github.com/hitobito/hitobito.

require "digest/md5"
require "rubygems/package"

module Synchronize
  module Mailchimp
    class Client # rubocop:todo Metrics/ClassLength
      MAX_RETRIES = 5
      attr_reader :list_id, :count, :api, :merge_fields, :member_fields

      def initialize(mailing_list, member_fields: [], merge_fields: [],
        count: Settings.mailchimp.batch_size, debug: false)
        @list_id = mailing_list.mailchimp_list_id
        @count = count
        @merge_fields = merge_fields
        @member_fields = member_fields
        @max_attempts = Settings.mailchimp.max_attempts
        @count = count

        @api = Gibbon::Request.new(api_key: mailing_list.mailchimp_api_key, debug: debug)
      end

      def http_client
        Gibbon::APIRequest.new(builder: @api).send(:rest_client)
      end

      def fetch_merge_fields
        paged("merge_fields", %w[tag name type]) do
          api.lists(list_id).merge_fields
        end
      end

      def fetch_segments
        paged("segments", %w[id name member_count]) do
          api.lists(list_id).segments
        end
      end

      def fetch_members
        fields = %w[email_address status tags merge_fields]
        fields += member_fields.collect(&:first).collect(&:to_s)

        paged("members", fields) do
          api.lists(list_id).members
        end.map { |member| downcase_email(member) }
      end

      def ping_api(error_handler:)
        api.ping.retrieve
        true
      rescue Gibbon::MailChimpError, Gibbon::GibbonError => e
        error_handler&.call(e)
        false
      end

      def create_segments(names)
        execute_batch(names) do |name|
          create_segment_operation(name)
        end
      end

      def create_merge_fields(list)
        execute_batch(list) do |name, type, options|
          create_merge_field_operation(name, type, options)
        end
      end

      def update_segments(list)
        execute_batch(list) do |segment_id, payload|
          update_segment_operation(segment_id, payload)
        end
      end

      def delete_segments(segment_ids)
        execute_batch(segment_ids) do |segment_id|
          destroy_segment_operation(segment_id)
        end
      end

      def update_members(people)
        execute_batch(people) do |person|
          update_member_operation(person)
        end
      end

      def subscribe_members(people)
        execute_batch(people) do |person|
          subscribe_member_operation(person)
        end
      end

      def unsubscribe_members(emails)
        execute_batch(emails) do |email|
          unsubscribe_member_operation(email)
        end
      end

      def fetch_batch(batch_id)
        api.batches(batch_id).retrieve.body.fetch("response_body_url")
      end

      def create_merge_field_operation(name, type, options = {})
        {
          method: "POST",
          path: "lists/#{list_id}/merge-fields",
          body: {tag: name.upcase, name: name, type: type, options: options}.to_json
        }
      end

      def create_segment_operation(name)
        {
          method: "POST",
          path: "lists/#{list_id}/segments",
          body: {name: name, static_segment: []}.to_json
        }
      end

      def destroy_segment_operation(segment_id)
        {
          method: "DELETE",
          path: "lists/#{list_id}/segments/#{segment_id}"
        }
      end

      def update_segment_operation(segment_id, body)
        {
          method: "POST",
          path: "lists/#{list_id}/segments/#{segment_id}",
          body: body.to_json
        }
      end

      def unsubscribe_member_operation(email)
        {
          method: "DELETE",
          path: "lists/#{list_id}/members/#{subscriber_id(email)}"
        }
      end

      def subscribe_member_operation(person)
        {
          method: "POST",
          path: "lists/#{list_id}/members",
          body: subscriber_body(person).merge(status: :subscribed).to_json
        }
      end

      def update_member_operation(person)
        {
          method: "PUT",
          path: "lists/#{list_id}/members/#{subscriber_id(person.email)}",
          body: subscriber_body(person).to_json
        }
      end

      def subscriber_body(person)
        {
          email_address: person.email.strip,
          merge_fields: {
            FNAME: person.first_name.to_s.strip,
            LNAME: person.last_name.to_s.strip
          }.merge(merge_field_values(person))
        }.merge(member_field_values(person))
      end

      private

      def subscriber_id(email)
        Digest::MD5.hexdigest(email.downcase)
      end

      # rubocop:todo Metrics/AbcSize
      # rubocop:todo Metrics/CyclomaticComplexity
      def paged(key, fields, list: [], offset: 0, &block) # rubocop:disable Metrics/MethodLength
        retries ||= 0
        body = block.call(list).retrieve(params: {count: count, offset: offset}).body.to_h

        body[key].each do |entry|
          list << entry.slice(*fields).deep_symbolize_keys
        end

        total_items = body["total_items"]
        next_offset = offset + count

        if total_items > next_offset
          paged(key, fields, list: list, offset: next_offset, &block)
        else
          list
        end
      rescue Gibbon::MailChimpError => e
        fail e unless [0, 400].include?(e.status_code.to_i)
        retries += 1
        (retries < MAX_RETRIES) ? retry : fail("Max retries exceeded")
      end
      # rubocop:enable Metrics/CyclomaticComplexity
      # rubocop:enable Metrics/AbcSize

      def execute_batch(list)
        operations = list.collect do |item|
          yield(item).tap do |operation|
            log "mailchimp: #{list_id}, op: #{operation[:method]}, item: #{item}"
            log operation
          end
        end

        if operations.present?
          batch_id = api.batches.create(body: {operations: operations}).body.fetch("id")
          result = wait_for_finish(batch_id)
          [operations, result]
        end
      end

      # rubocop:todo Metrics/AbcSize
      def wait_for_finish(batch_id, prev_status = nil, attempt = 0) # rubocop:disable Metrics/MethodLength
        sleep attempt * attempt
        body = api.batches(batch_id).retrieve.body
        status = body.fetch("status")
        attempt = 0 if status != prev_status

        log "batch #{batch_id}, status: #{status}, attempt: #{attempt}"
        raise "Batch #{batch_id} exeeded max_attempts, status: #{status}" if attempt > @max_attempts

        if status != "finished"
          wait_for_finish(batch_id, status, attempt + 1)
        else
          attrs = %w[total_operations finished_operations errored_operations response_body_url]
          body.slice(*attrs).then do |meta|
            log meta
            operation_results = extract_operation_results(meta.delete("response_body_url"))
            meta.merge("operation_results" => operation_results).deep_symbolize_keys
          end
        end
      end
      # rubocop:enable Metrics/AbcSize

      def extract_operation_results(response_body_url)
        retries = 0
        body = RestClient.get(response_body_url)
        extract_tgz(body).flat_map do |operations|
          operations.map do |operation|
            operation["response"].slice("title", "detail", "status", "errors")
          end
        end
      rescue RestClient::BadRequest
        retries += 1
        (retries < MAX_RETRIES) ? retry : fail("Max retries exceeded")
      end

      def merge_field_values(person)
        merge_fields.collect do |field, _type, options, evaluator|
          value = evaluator.call(person)
          next if value.blank?
          next if options.key?(:choices) && !options[:choices].include?(value)

          [field.upcase, value]
        end.compact.to_h.deep_symbolize_keys
      end

      def member_field_values(person)
        member_fields.collect do |field, evaluator|
          value = evaluator.call(person)
          next if value.blank?

          [field, value]
        end.compact.to_h.deep_symbolize_keys
      end

      def downcase_email(member)
        member.tap do
          member[:email_address] = member[:email_address].downcase
        end
      end

      def extract_tgz(data)
        gzip_reader = Zlib::GzipReader.new(StringIO.new(data))
        tar_reader = Gem::Package::TarReader.new(gzip_reader)
        tar_reader
          .map { |entry| parse_tar_entry(entry) if entry.file? }
          .compact_blank
      ensure
        tar_reader.close
      end

      def parse_tar_entry(entry)
        JSON.parse(entry.read).each do |item|
          item["response"] = JSON.parse(item["response"])
        end
      end

      def log(message, logger = Rails.logger)
        logger.info(message)
      end
    end
  end
end
