# frozen_string_literal: true

require "active_support/core_ext/module/delegation"
require "influxer/metrics/relation/time_query"
require "influxer/metrics/relation/calculations"
require "influxer/metrics/relation/where_clause"
require "influxer/metrics/quoting/timestamp"

module Influxer
  # Relation is used to build queries
  class Relation
    include Influxer::TimeQuery
    include Influxer::Calculations
    include Influxer::TimestampQuoting
    prepend Influxer::WhereClause

    attr_reader :values

    SUPPORTED_EPOCH_FORMAT = %i[h m s ms u ns].freeze

    MULTI_VALUE_METHODS = %i[select where group order].freeze

    MULTI_KEY_METHODS = %i[fanout].freeze

    SINGLE_VALUE_METHODS = %i[fill time limit offset slimit soffset from normalized timezone].freeze

    MULTI_VALUE_SIMPLE_METHODS = %i[select group].freeze

    SINGLE_VALUE_SIMPLE_METHODS = %i[fill limit offset slimit soffset from timezone].freeze

    MULTI_VALUE_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}_values                          # def select_values
          @values[:#{name}] ||= []                  #   @values[:select] || []
        end                                         # end
      CODE
    end

    MULTI_KEY_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}_values                          # def fanout_values
          @values[:#{name}] ||= {}                  #   @values[:fanout] || {}
        end                                         # end
      CODE
    end

    SINGLE_VALUE_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}_value                           # def limit_value
          @values[:#{name}]                         #   @values[:limit]
        end                                         # end
      CODE
    end

    SINGLE_VALUE_SIMPLE_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(val)                            # def limit(val)
          @values[:#{name}] = val                   #   @value[:limit] = val
          self                                      #   self
        end                                         # end
      CODE
    end

    MULTI_VALUE_SIMPLE_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(*args)                          # def select(*args)
          #{name}_values.concat args.map(&:to_s)    #  select_values.concat args.map(&:to_s)
          self                                      #  self
        end                                         # end
      CODE
    end

    # delegate array methods to to_a
    delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
             to: :to_a

    # Initialize new Relation for 'klass' (Class) metrics.
    #
    # Available params:
    #  :attributes - hash of attributes to be included to new Metrics object
    #  and where clause of Relation
    #
    def initialize(klass, params = {})
      @klass = klass
      @instance = klass.new params[:attributes]
      reset
      where(**params[:attributes]) if params[:attributes].present?
    end

    def write(params = {})
      build(params).write
    end

    def write!(params = {})
      build(params).write!
    end

    def build(params = {})
      point = @instance.dup
      params.each do |key, val|
        point.send("#{key}=", val) if point.respond_to?(key)
      end
      point
    end

    alias new build

    def normalized
      @values[:normalized] = true
      self
    end

    def normalized?
      @values[:normalized] == true
    end

    def epoch(val)
      return self unless SUPPORTED_EPOCH_FORMAT.include? val

      @values[:epoch] = val
      self
    end

    def timezone(val)
      return self if val.blank?

      @values[:timezone] = val
      self
    end

    def order(val)
      case val
      when Hash
        val.each { |k, v| order_values << "#{k} #{v}" }
      when String
        order_values << val
      end
      self
    end

    # rubocop:disable Metrics/AbcSize
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/PerceivedComplexity
    def to_sql
      sql = ["select"]
      select_values << "*" if select_values.empty?

      sql << select_values.uniq.join(", ")

      sql << "from #{build_series_name}"

      sql << "where #{where_values.join(" and ")}" unless where_values.empty?

      unless group_values.empty? && time_value.nil?
        group_fields = (time_value.nil? ? [] : ["time(" + @values[:time] + ")"]) + group_values
        group_fields.uniq!
        sql << "group by #{group_fields.join(", ")}"
      end

      sql << "fill(#{fill_value})" unless fill_value.nil?

      sql << "order by #{order_values.uniq.join(",")}" unless order_values.empty?

      sql << "limit #{limit_value}" unless limit_value.nil?
      sql << "offset #{offset_value}" unless offset_value.nil?
      sql << "slimit #{slimit_value}" unless slimit_value.nil?
      sql << "soffset #{soffset_value}" unless soffset_value.nil?
      sql << "TZ('#{timezone_value}')" unless timezone_value.blank?
      sql.join " "
    end
    # rubocop:enable Metrics/AbcSize
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/MethodLength
    # rubocop:enable Metrics/PerceivedComplexity

    def to_a
      return @records if loaded?

      load
    end

    def inspect
      entries = to_a.take(11).map!(&:inspect)
      entries[10] = "..." if entries.size == 11

      "#<#{self.class.name} [#{entries.join(", ")}]>"
    end

    def empty?
      unless loaded?
        # we don't need selects here
        select_values.clear
        limit(1).load
      end
      @records.empty?
    end

    def as_json(options = nil)
      to_a.as_json(options)
    end

    def load
      @records = get_points(
        @instance.client.query(
          to_sql,
          denormalize: !normalized?,
          epoch: @values[:epoch]
        )
      )
      @loaded = true
      @records
    end

    def delete_all
      sql = if where_contains_time?
        ["delete"]
      else
        ["drop series"]
      end

      sql << "from #{@instance.series(write: true)}"

      sql << "where #{where_values.join(" and ")}" unless where_values.empty?

      sql = sql.join " "

      @instance.client.query sql
    end

    def scoping
      previous = @klass.current_scope
      @klass.current_scope = self
      yield
    ensure
      @klass.current_scope = previous
    end

    # rubocop:disable Metrics/AbcSize
    # rubocop:disable Metrics/MethodLength
    def merge!(rel)
      return self if rel.nil?

      MULTI_VALUE_METHODS.each do |method|
        (@values[method] ||= []).concat(rel.values[method]).uniq! unless rel.values[method].nil?
      end

      MULTI_KEY_METHODS.each do |method|
        (@values[method] ||= {}).merge!(rel.values[method]) unless rel.values[method].nil?
      end

      SINGLE_VALUE_METHODS.each do |method|
        @values[method] = rel.values[method] unless rel.values[method].nil?
      end

      self
    end
    # rubocop:enable Metrics/AbcSize
    # rubocop:enable Metrics/MethodLength

    protected

    def build_series_name
      from_value.present? ? @klass.quoted_series(from_value) : @instance.series
    end

    def loaded?
      @loaded
    end

    def reset
      @values = {}
      @records = []
      @loaded = false
      self
    end

    def reload
      reset
      load
      self
    end

    def quoted(val, key = nil)
      if val.is_a?(String) || val.is_a?(Symbol) || @klass.tag?(key)
        "'#{val}'"
      elsif key.to_s == "time"
        quote_timestamp val, @instance.client
      else
        val.to_s
      end
    end

    def get_points(list)
      return list if normalized?

      list.reduce([]) do |a, e|
        a + e.fetch("values", []).map { |v| inject_tags(v, e["tags"] || {}) }
      end
    end

    def inject_tags(val, tags)
      val.merge(tags)
    end

    def method_missing(method, *args, &block)
      return super unless @klass.respond_to?(method)

      merge!(scoping { @klass.public_send(method, *args, &block) })
    end

    def respond_to_missing?(method, *args)
      return true if @klass.respond_to?(method)

      super
    end
  end
end
