require 'miq-process'
require 'pid_file'

class EvmServer
  include Vmdb::Logging

  SERVER_PROCESS_TITLE = 'MIQ Server'.freeze

  attr_accessor :servers_to_monitor

  def initialize
    $log ||= Rails.logger
    @servers_to_monitor = servers_from_db
  end

  def start
    if pid = MiqServer.running?
      $log.warn("EVM is already running (PID=#{pid})")
      exit
    end

    MiqServer.kill_all_workers

    PidFile.create(MiqServer.pidfile)
    set_process_title
    validate_database
    EvmDatabase.seed_primordial
    check_migrations_up_to_date

    start_servers
    monitor_servers
  rescue Interrupt => e
    _log.info("Received #{e.message} signal, killing server")
    kill_servers
    exit 1
  rescue SignalException => e
    _log.info("Received #{e.message} signal, shutting down server")
    stop_servers
    exit 0
  end

  def start_servers
    @servers_to_monitor = servers_from_db if servers_to_monitor.empty?

    as_each_server { start_server }
  end

  def monitor_servers
    loop do
      refresh_servers_to_monitor
      as_each_server { monitor }
      sleep ::Settings.server.monitor_poll.to_i_with_method
    end
  end

  def stop_servers
    as_each_server { @current_server.shutdown }
  end

  def kill_servers
    as_each_server do
      @current_server.worker_manager.kill_all_workers
      @current_server.update(:stopped_on => Time.now.utc, :status => "killed", :is_master => false)
    end
  end

  def refresh_servers_to_monitor
    servers_to_start    = servers_from_db    - servers_to_monitor
    servers_to_shutdown = servers_to_monitor - servers_from_db

    servers_to_start.each do |s|
      servers_to_monitor << s
      impersonate_server(s)
      start_server
    end

    servers_to_shutdown.each do |s|
      servers_to_monitor.delete(s)
      s.shutdown
    end
  end

  def self.start(*_args)
    new.start
  end

  private

  def servers_from_db
    my_server = MiqServer.my_server(true)

    if MiqEnvironment::Command.is_podified?
      # Ensure that the "primary" miq_server is first in the list of servers.
      # This ensures that any work that is only done on the primary is completed
      # before processing the rest of the servers.
      MiqServer.in_my_region.to_a.unshift(my_server).uniq.compact
    else
      [my_server].compact
    end
  end

  def set_process_title
    Process.setproctitle(SERVER_PROCESS_TITLE) if Process.respond_to?(:setproctitle)
  end

  def start_server
    Vmdb::Settings.activate

    save_local_network_info
    set_local_server_vm
    reset_server_runtime_info
    log_server_info

    Vmdb::Appliance.log_config_on_startup

    @current_server.set_database_application_name

    EvmDatabase.seed_rest

    MiqEvent.raise_evm_event(@current_server, "evm_server_start")

    _log.info("Server starting...")
    puts "** Server starting..."

    @current_server.starting_server_record

    configure_server_roles
    clear_queue

    MiqServer.startup!

    @current_server.delete_active_log_collections_queue

    start_workers

    @current_server.update(:status => "started")
    _log.info("Server starting...Complete")
    puts "** Server starting...Complete"
  end

  def monitor
    _dummy, timings = Benchmark.realtime_block(:total_time) { @current_server.monitor }
    _log.info("Server Monitoring Complete - Timings: #{timings.inspect}") unless timings[:total_time] < ::Settings.server.server_log_timings_threshold.to_i_with_method
  end

  def validate_database
    ActiveRecord::Base.postgresql_ssl_friendly_base_reconnect

    # Log the Versions
    _log.info("Database Adapter: [#{ActiveRecord::Base.connection.adapter_name}], version: [#{ActiveRecord::Base.connection.database_version_details}]") if ActiveRecord::Base.connection.respond_to?(:database_version_details)
  end

  def check_migrations_up_to_date
    up_to_date, *message = SchemaMigration.up_to_date?
    level = up_to_date ? :info : :warn
    Array.wrap(message.presence).each { |msg| _log.send(level, msg) }
    up_to_date
  end

  def save_local_network_info
    server_hash = {}
    config_hash = {}

    ipaddr, hostname, mac_address = MiqServer.get_network_information

    if ipaddr&.match?(Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex).freeze)
      server_hash[:ipaddress] = config_hash[:host] = ipaddr
    end

    if hostname.present? && hostname.hostname?
      hostname = nil if /.*localhost.*/.match?(hostname)
      server_hash[:hostname] = config_hash[:hostname] = hostname
    end

    if mac_address.present?
      server_hash[:mac_address] = mac_address
    end

    if config_hash.any?
      Vmdb::Settings.save!(@current_server, :server => config_hash)
      ::Settings.reload!
    end

    @current_server.update(server_hash)
  end

  def set_local_server_vm
    if @current_server.vm_id.nil?
      vm_ids = find_vms_by_mac_address_and_hostname_and_ipaddress(@current_server.mac_address, @current_server.hostname, @current_server.ipaddress).pluck(:id)
      if vm_ids.length > 1
        _log.warn("Found multiple Vms that may represent this MiqServer: #{vm_ids.sort.inspect}")
      elsif vm_ids.length == 1
        @current_server.update(:vm_id => vm_ids.first)
      end
    end
  end

  def find_vms_by_mac_address_and_hostname_and_ipaddress(mac_address, hostname, ipaddress)
    return Vm.none if mac_address.blank? && hostname.blank? && ipaddress.blank?

    scope = Vm
    # NOTE: nics are guest devices with an extra where()
    scope = scope.joins(:hardware => :nics).where(:guest_devices => {:address => mac_address}) if mac_address
    scope = scope.joins(:hardware => :networks).where(:networks => {:hostname => hostname}) if hostname
    scope = scope.joins(:hardware => :networks).where(:networks => {:ipaddress => ipaddress}) if ipaddress

    scope
  end

  def reset_server_runtime_info
    @current_server.update(
      :drb_uri        => nil,
      :last_heartbeat => nil,
      :memory_usage   => nil,
      :memory_size    => nil,
      :percent_memory => nil,
      :percent_cpu    => nil,
      :cpu_time       => nil
    )
  end

  def log_server_info
    _log.info("Server IP Address: #{@current_server.ipaddress}")    if @current_server.ipaddress.present?
    _log.info("Server Hostname: #{@current_server.hostname}")       if @current_server.hostname.present?
    _log.info("Server MAC Address: #{@current_server.mac_address}") if @current_server.mac_address.present?
    _log.info("Server GUID: #{MiqServer.my_guid}")
    _log.info("Server Zone: #{MiqServer.my_zone}")
    _log.info("Server Role: #{MiqServer.my_role}")
    region = MiqRegion.my_region
    _log.info("Server Region number: #{region.region}, name: #{region.name}") unless region.nil?
    _log.info("Database Latency: #{EvmDatabase.ping(ApplicationRecord.connection)} ms")
  end

  def configure_server_roles
    @current_server.ensure_default_roles

    #############################################################
    # Server Role Assignment
    #
    # - Deactivate all roles from last time
    # - Assert database_owner role - based on vmdb being local
    # - Role activation should happen inside monitor_servers
    # - Synchronize active roles to monitor for role changes
    #############################################################
    @current_server.deactivate_all_roles
    @current_server.set_database_owner_role(EvmDatabase.local?)
    @current_server.server_monitor.monitor_servers
    @current_server.monitor_server_roles if @current_server.is_master?
    @current_server.sync_active_roles
    @current_server.set_active_role_flags
  end

  def clear_queue
    #############################################################
    # Clear the MiqQueue for server and its workers
    #############################################################
    @current_server.clean_stop_worker_queue_items
    @current_server.clear_miq_queue_for_this_server
  end

  def start_workers
    #############################################################
    # Start all the configured workers
    #############################################################
    @current_server.start_workers
  end

  ######################################################################
  # Warning:
  #
  # The following methods can lead to unexpected (and likely unpleasant)
  # behavior if used out of the scope of the orchestrator process.
  #
  # They change the global state which is used to determine the current
  # server's identity. This intentionally will alter the values of calls
  # such as MiqServer.my_server and MiqServer.my_guid, and also the
  # contents of the global ::Settings constant.
  ######################################################################
  def as_each_server
    initial_server = @current_server
    servers_to_monitor.each do |s|
      impersonate_server(s)
      yield
    end
  ensure
    clear_server_caches if @current_server != initial_server
  end

  def impersonate_server(s)
    return if s == @current_server

    _log.info("Impersonating server - id: #{s.id}, guid: #{s.guid}")

    MiqServer.my_server_clear_cache
    MiqServer.my_guid = s.guid

    # It is important that we continue to use the same server instance here.
    # A lot of "global" state is stored in instance variables on the server.
    @current_server = s
    Vmdb::Settings.reset_settings_constant(s.settings_for_resource)
  end

  def clear_server_caches
    MiqServer.my_guid = nil
    MiqServer.my_server_clear_cache
    # Use Vmdb::Settings.for_resource(:my_server) here as MiqServer.my_server might be nil
    Vmdb::Settings.reset_settings_constant(Vmdb::Settings.for_resource(:my_server))
  end
end
