require_relative 'ns1/client'

module RecordStore
  class Provider::NS1 < Provider
    class Error < StandardError; end

    class << self
      def client
        Provider::NS1::Client.new(api_key: secrets['api_key'])
      end

      # Downloads all the records from the provider.
      #
      # Returns: an array of `Record` for each record in the provider's zone
      def retrieve_current_records(zone:, stdout: $stdout) # rubocop:disable Lint/UnusedMethodArgument
        full_api_records = records_for_zone(zone).map do |short_record|
          client.record(
            zone: zone,
            fqdn: short_record["domain"],
            type: short_record["type"],
            must_exist: true,
          )
        end

        full_api_records.map { |r| build_from_api(r) }.flatten.compact
      end

      # Returns an array of the zones managed by provider as strings
      def zones
        client.zones.map { |zone| zone['zone'] }
      end

      private

      # Fetches simplified records for the provided zone
      def records_for_zone(zone)
        client.zone(zone)["records"]
      end

      # Creates a new record to the zone. It is expected this call modifies external state.
      #
      # Arguments:
      # record - a kind of `Record`
      def add(record, zone)
        new_answers = [{ answer: build_api_answer_from_record(record) }]

        record_fqdn = record.fqdn.sub(/\.$/, '')

        existing_record = client.record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type
        )

        if existing_record.nil?
          client.create_record(
            zone: zone,
            fqdn: record_fqdn,
            type: record.type,
            params: { answers: new_answers, ttl: record.ttl }
          )
          return
        end

        existing_answers = existing_record['answers'].map { |answer| symbolize_keys(answer) }
        client.modify_record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type,
          params: { answers: existing_answers + new_answers, ttl: record.ttl }
        )
      end

      # Deletes an existing record from the zone. It is expected this call modifies external state.
      #
      # Arguments:
      # record - a kind of `Record`
      def remove(record, zone)
        record_fqdn = record.fqdn.sub(/\.$/, '')

        existing_record = client.record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type
        )
        return if existing_record.nil?

        pruned_answers = existing_record['answers']
          .map { |answer| symbolize_keys(answer) }
          .reject { |answer| answer[:answer] == build_api_answer_from_record(record) }

        if pruned_answers.empty?
          client.delete_record(
            zone: zone,
            fqdn: record_fqdn,
            type: record.type
          )
          return
        end

        client.modify_record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type,
          params: { answers: pruned_answers }
        )
      end

      # Updates an existing record in the zone. It is expected this call modifies external state.
      #
      # Arguments:
      # id - provider specific ID of record to update
      # record - a kind of `Record` which the record with `id` should be updated to
      def update(id, record, zone)
        record_fqdn = record.fqdn.sub(/\.$/, '')

        existing_record = client.record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type,
          must_exist: true,
        )

        # Identify the answer in this record with the matching ID, and update it
        updated = false
        existing_record["answers"].each do |answer|
          next if answer["id"] != id
          updated = true
          answer["answer"] = build_api_answer_from_record(record)
        end

        unless updated
          error = +'while trying to update a record, could not find answer with fqdn: '
          error << "#{record.fqdn}, type; #{record.type}, id: #{id}"
          raise Error, error
        end

        client.modify_record(
          zone: zone,
          fqdn: record_fqdn,
          type: record.type,
          params: { answers: existing_record["answers"], ttl: record.ttl }
        )
      end

      def build_from_api(api_record)
        fqdn = Record.ensure_ends_with_dot(api_record["domain"])

        record_type = api_record["type"]
        return if record_type == 'SOA'

        api_record["answers"].map do |api_answer|
          answer = api_answer["answer"]
          record = {
            ttl: api_record["ttl"],
            fqdn: fqdn.downcase,
            record_id: api_answer["id"],
          }

          case record_type
          when 'A', 'AAAA'
            record.merge!(address: answer.first)
          when 'ALIAS'
            record.merge!(alias: answer.first)
          when 'CAA'
            flags, tag, value = answer

            record.merge!(
              flags: flags.to_i,
              tag: tag,
              value: Record.unquote(value),
            )
          when 'CNAME'
            record.merge!(cname: answer.first)
          when 'MX'

            preference, exchange = answer

            record.merge!(
              preference: preference.to_i,
              exchange: exchange,
            )
          when 'NS'
            record.merge!(nsdname: answer.first)
          when 'SPF', 'TXT'
            record.merge!(txtdata: Record.unlong_quote(Record.unescape(answer.first).gsub(';', '\;')))
          when 'SRV'
            priority, weight, port, host = answer

            record.merge!(
              priority: priority.to_i,
              weight: weight.to_i,
              port: port.to_i,
              target: Record.ensure_ends_with_dot(host),
            )
          end
          Record.const_get(record_type).new(record)
        end
      end

      def build_api_answer_from_record(record)
        if record.is_a?(Record::MX)
          [record.preference, record.exchange]
        elsif record.is_a?(Record::TXT) || record.is_a?(Record::SPF)
          [Record.long_quote(record.txtdata)]
        elsif record.is_a?(Record::CAA)
          [record.flags, record.tag, record.value]
        elsif record.is_a?(Record::SRV)
          [record.priority, record.weight, record.port, record.target]
        else
          [record.rdata_txt]
        end
      end

      def symbolize_keys(hash)
        hash.map { |key, value| [key.to_sym, value] }.to_h
      end

      def secrets
        super.fetch('ns1')
      end
    end
  end
end