class VimPerformanceState < ApplicationRecord
  include Purging

  serialize :state_data

  belongs_to :resource, :polymorphic => true

  ASSOCIATIONS = [:vms, :hosts, :ems_clusters, :ext_management_systems, :storages, :container_nodes, :container_groups,
                  :all_container_groups, :containers]

  # Define accessors for state_data information
  [
    :assoc_ids,
    :host_sockets,
    :parent_host_id,
    :parent_storage_id,
    :parent_ems_id,
    :parent_ems_cluster_id,
    :tag_names,
    :image_tag_names,
    :numvcpus,
    :total_cpu,
    :total_mem,
    :reserve_cpu,
    :reserve_mem,
    :vm_allocated_disk_storage,
    :allocated_disk_types,
    :vm_used_disk_storage
  ].each do |m|
    define_method(m)       { state_data[m] }
    define_method(:"#{m}=") { |value| state_data[m] = value }
  end

  # state_data:
  # => assoc_ids
  # => total_memory
  # => total_cpu
  # => reserve_memory
  # => reserve_cpu
  # => vm_count_on      (derive from assoc_ids)
  # => vm_count_off     (derive from assoc_ids)
  # => vm_count_total   (derive from assoc_ids)
  # => host_count_on    (derive from assoc_ids)
  # => host_count_off   (derive from assoc_ids)
  # => host_count_total (derive from assoc_ids)
  # => host_sockets     (derive from assoc_ids)

  # TODO: do a single query for the finds
  # @param objs [Array[Object]|Object] MetricsCapture#target - all should be the same object class
  # @returns [Array[VimPerformanceState]|VimPerformanceState] return an array if an array was passed in
  # NOTE: a few calls (with a single object) do use the return and expect a single result
  def self.capture(objs)
    ts = Time.now.utc
    ts = Time.utc(ts.year, ts.month, ts.day, ts.hour)

    states = Array(objs).map do |obj|
      state = obj.vim_performance_states.find_by(:timestamp => ts)
      state ||= obj.vim_performance_states.build(:timestamp => ts).capture_and_save
    end

    objs.kind_of?(Array) ? states : states.first
  end

  def capture
    self.state_data ||= {}
    self.capture_interval = 1.hour.to_i
    capture_assoc_ids
    capture_parent_host
    capture_parent_storage
    capture_parent_ems
    capture_parent_cluster
    capture_cpu_total_cores
    capture_totals
    capture_reserve
    capture_vm_disk_storage
    capture_disk_types
    capture_tag_names
    capture_image_tag_names
    capture_host_sockets

    self
  end

  def capture_and_save
    capture
    save
    self
  end

  def vm_count_on
    get_assoc(:vms, :on).length
  end

  def vm_count_off
    get_assoc(:vms, :off).length
  end

  def vm_count_total
    get_assoc(:vms).length
  end

  def host_count_on
    get_assoc(:hosts, :on).length
  end

  def host_count_off
    get_assoc(:hosts, :off).length
  end

  def host_count_total
    get_assoc(:hosts).length
  end

  def storages
    ids = get_assoc(:storages, :on)
    ids.empty? ? Storage.none : Storage.where(:id => ids).order(:id)
  end

  def ext_management_systems
    ids = get_assoc(:ext_management_systems, :on)
    ids.empty? ? ExtManagementSystem.none : ExtManagementSystem.where(:id => ids).order(:id)
  end

  def ems_clusters
    ids = get_assoc(:ems_clusters, :on)
    ids.empty? ? EmsCluster.none : EmsCluster.where(:id => ids).order(:id)
  end

  def hosts
    ids = get_assoc(:hosts)
    ids.empty? ? Host.none : Host.where(:id => ids).order(:id)
  end

  def vms
    ids = get_assoc(:vms)
    ids.empty? ? VmOrTemplate.none : VmOrTemplate.where(:id => ids).order(:id)
  end

  def container_nodes
    ids = get_assoc(:container_nodes)
    ids.empty? ? ContainerNode.none : ContainerNode.where(:id => ids).order(:id)
  end

  def container_groups
    ids = get_assoc(:container_groups)
    ids.empty? ? ContainerGroup.none : ContainerGroup.where(:id => ids).order(:id).not_archived_before(timestamp)
  end

  def containers
    ids = get_assoc(:containers)
    ids.empty? ? Container.none : Container.where(:id => ids).order(:id).not_archived_before(timestamp)
  end

  def all_container_groups
    ids = get_assoc(:all_container_groups)
    ids.empty? ? ContainerGroup.none : ContainerGroup.where(:id => ids).order(:id)
  end

  def get_assoc(relat, mode = nil)
    assoc = state_data.fetch_path(:assoc_ids, relat.to_sym)
    return [] if assoc.nil?

    ids = mode.nil? ? (assoc[:on] || []) + (assoc[:off] || []) : assoc[mode.to_sym]
    ids.nil? ? [] : ids.uniq.sort
  end

  # @param timestamp [Time|String] hourly timestamp, prefer Time
  # @returns [Range] time range
  def self.get_time_interval(timestamp)
    timestamp = Time.parse(timestamp).utc if timestamp.kind_of?(String)
    (timestamp - 1.hour)..timestamp
  end

  private

  def capture_disk_types
    if hardware
      self.allocated_disk_types = hardware.disks.each_with_object({}) do |disk, res|
        next if disk.size.nil?

        type = disk.backing.try(:volume_type) || 'unclassified'
        res[type] = (res[type] || 0) + disk.size
      end
    end
  end

  def capture_totals
    self.total_cpu = capture_total(:cpu_speed)
    self.total_mem = capture_total(:memory)
  end

  def capture_total(field)
    return resource.send(:"aggregate_#{field}") if resource.respond_to?(:"aggregate_#{field}")

    field == :memory ? hardware.try(:memory_mb) : hardware.try(:aggregate_cpu_speed)
  end

  def capture_assoc_ids
    result = {}
    ASSOCIATIONS.each do |assoc|
      assoc_recs = fetch_assoc_records(resource, assoc)
      next if assoc_recs == false

      has_state = assoc_recs[0] && assoc_recs[0].respond_to?(:state)

      r = result[assoc] = {:on => [], :off => []}
      r_on = r[:on]
      r_off = r[:off]
      assoc_recs.each do |o|
        state = has_state ? o.state : 'on'
        case state
        when 'on' then r_on << o.id
        else r_off << o.id
        end
      end

      r_on.uniq!
      r_on.sort!
      r_off.uniq!
      r_off.sort!
    end
    self.assoc_ids = result.presence
  end

  def fetch_assoc_records(resource, assoc)
    method = if assoc == :vms
               if resource.kind_of?(EmsCluster)
                 :all_vms_and_templates
               elsif resource.kind_of?(Service)
                 :vms
               else
                 :vms_and_templates
               end
             else
               assoc
             end
    return false unless resource.respond_to?(method)

    records = resource.send(method)
    records = records.not_archived_before(timestamp) if records.try(:klass).respond_to?(:not_archived_before)
    records
  end

  def capture_parent_cluster
    if resource.kind_of?(Host) || resource.kind_of?(VmOrTemplate)
      self.parent_ems_cluster_id = resource.parent_cluster.try(:id)
    end
  end

  def capture_parent_host
    self.parent_host_id = resource.host_id if resource.kind_of?(VmOrTemplate)
  end

  def capture_parent_storage
    self.parent_storage_id = resource.storage_id if resource.kind_of?(VmOrTemplate)
  end

  def capture_parent_ems
    self.parent_ems_id = resource.try(:ems_id)
  end

  def capture_reserve
    self.reserve_cpu = resource.try(:cpu_reserve)
    self.reserve_mem = resource.try(:memory_reserve)
  end

  def capture_tag_names
    self.tag_names = resource.perf_tags
  end

  def capture_image_tag_names
    self.image_tag_names = if resource.respond_to?(:container_image) && resource.container_image.present?
                             resource.container_image.perf_tags
                           else
                             ''
                           end
  end

  def capture_vm_disk_storage
    if resource.kind_of?(VmOrTemplate)
      [:used_disk, :allocated_disk].each do |type|
        send(:"vm_#{type}_storage=", resource.send(:"#{type}_storage"))
      end
    end
  end

  def capture_cpu_total_cores
    # TODO: This is cpu_total_cores and needs to be renamed, but reports depend on the name :numvcpus
    self.numvcpus = hardware.try(:cpu_total_cores)
  end

  def capture_host_sockets
    self.host_sockets = if resource.kind_of?(Host)
                          resource.hardware.try(:cpu_sockets)
                        elsif resource.respond_to?(:hosts)
                          resource.hosts.includes(:hardware).collect { |h| h.hardware.try(:cpu_sockets) }.compact.sum
                        end
  end

  def hardware
    if resource.respond_to?(:hardware)
      resource.hardware
    elsif resource.respond_to?(:container_node)
      resource.container_node.try(:hardware)
    end
  end
end
