class UnattendedController < ApplicationController
  include Foreman::Renderer

  layout false

  # Methods which return configuration files for syslinux(pxe), pxegrub or g/ipxe
  PXE_CONFIG_URLS = TemplateKind.where("name LIKE ?","PXELinux").map(&:name)
  PXEGRUB_CONFIG_URLS = TemplateKind.where("name LIKE ?", "PXEGrub").map(&:name)
  IPXE_CONFIG_URLS = TemplateKind.where("name LIKE ?", "iPXE").map(&:name) + ['gPXE']
  CONFIG_URLS = PXE_CONFIG_URLS + IPXE_CONFIG_URLS + PXEGRUB_CONFIG_URLS

  # Methods which return valid provision instructions, used by the OS
  PROVISION_URLS = TemplateKind.where("name LIKE ?", "provision").map(&:name)

  # Methods which returns post install instructions for OS's which require it
  FINISH_URLS = TemplateKind.where("name LIKE ?", "finish").map(&:name)

  # We dont require any of these methods for provisioning
  FILTERS = [:require_login, :session_expiry, :update_activity_time, :set_taxonomy, :authorize]
  FILTERS.each do |f|
    define_method("#{f}_with_unattended") do
      send("#{f}_without_unattended") if params.key?(:spoof) or params.key?(:hostname)
    end
    alias_method_chain f, :unattended
  end

  # We want to find out our requesting host
  before_filter :get_host_details, :allowed_to_install?, :except => :template
  before_filter :handle_ca, :only => PROVISION_URLS
  before_filter :handle_realm, :only => PROVISION_URLS
  # load "helper" variables to be available in the templates
  before_filter :load_template_vars, :only => PROVISION_URLS
  before_filter :pxe_config, :only => CONFIG_URLS
  # all of our requests should be returned in text/plain
  after_filter :set_content_type
  before_filter :set_admin_user, :only => :built

  # this actions is called by each operatingsystem post/finish script - it notify us that the OS installation is done.
  def built
    logger.info "#{controller_name}: #{@host.name} is Built!"
    update_ip if Setting[:update_ip_from_built_request]
    head(@host.built ? :created : :conflict)
  end

  def template
    return head(:not_found) unless (params.has_key?("id") and params.has_key?(:hostgroup))

    template = ProvisioningTemplate.find(params['id'])
    @host = Hostgroup.find(params['hostgroup'])

    return head(:not_found) unless template and @host

    load_template_vars if template.template_kind.name == 'provision'
    safe_render template.template
  end

  # Generate an action for each template kind
  # i.e. /unattended/provision will render the provisioning template for the requesting host
  TemplateKind.all.each do |kind|
    define_method kind.name do
      render_template kind.name
    end
  end
  # Using alias_method causes test failures as iPXE method is unknown in an empty DB
  def gPXE; iPXE; end

  protected

  def require_ssl?
    return super if params.key?(:spoof) || params.key?(:hostname)
    false
  end

  private

  def render_template(type)
    if (config = @host.provisioning_template({ :kind => type }))
      logger.debug "rendering DB template #{config.name} - #{type}"
      safe_render config
    else
      msg = "unable to find #{type} template for [#{@host.name}] running [#{@host.operatingsystem}]"
      logger.error msg
      render :text => msg, :status => :not_found
    end
  end

  # lookup for a host based on the ip address and if possible by a mac address(as sent by anaconda)
  # if the host was found than its record will be in @host
  # if the host doesn't exists, it will return 404 and the requested method will not be reached.

  def get_host_details
    @host = find_host_by_spoof || find_host_by_token || find_host_by_ip_or_mac
    unless @host
      logger.info "#{controller_name}: unable to find a host that matches the request from #{request.env['REMOTE_ADDR']}"
      head(:not_found) and return
    end
    unless @host.operatingsystem
      logger.error "#{controller_name}: #{@host.name}'s operating system is missing!"
      head(:conflict) and return
    end
    unless @host.operatingsystem.family
      # Then, for some reason, the OS has not been specialized into a Redhat or Debian class
      logger.error "#{controller_name}: #{@host.name}'s operating system [#{@host.operatingsystem.fullname}] has no OS family!"
      head(:conflict) and return
    end
    logger.info "Found #{@host}"
  end

  def find_host_by_spoof
    host = Nic::Base.primary.find_by_ip(params.delete('spoof')).try(:host) if params['spoof'].present?
    host ||= Host.find(params.delete('hostname')) if params['hostname'].present?
    @spoof = host.present?
    host
  end

  def find_host_by_token
    token = params.delete('token')
    return nil if token.blank?
    # Quirk: ZTP requires the .slax suffix
    if ( result = token.match(/^([a-z0-9-]+)(.slax)$/i) )
      token, _suffix = result.captures
    end
    Host.for_token(token).first
  end

  def find_host_by_ip_or_mac
    # try to find host based on our client ip address
    ip = ip_from_request_env

    # in case we got back multiple ips (see #1619)
    ip = ip.split(',').first

    # search for a mac address in any of the RHN provisioning headers
    # this section is kickstart only relevant
    mac_list = []
    if request.env['HTTP_X_RHN_PROVISIONING_MAC_0'].present?
      begin
        request.env.keys.each do |header|
          mac_list << request.env[header].split[1].strip.downcase if header =~ /^HTTP_X_RHN_PROVISIONING_MAC_/
        end
      rescue => e
        Foreman::Logging.exception("unknown RHN_PROVISIONING header", e)
        mac_list = []
      end
    end
    # we try to match first based on the MAC, falling back to the IP
    # host is readonly because of association so we reload it if we find it
    host = Host.joins(:provision_interface).where(mac_list.empty? ? {:nics => {:ip => ip}} : ["lower(nics.mac) IN (?)", mac_list]).first
    host ? Host.find(host.id) : nil
  end

  def allowed_to_install?
    (@host.build or @spoof) ? true : head(:method_not_allowed)
  end

  # Cleans Certificate and enable autosign. This is run as a before_filter for provisioning templates.
  # The host is requesting its build configuration so I guess we just send them some text so a post mortum can see what happened
  def handle_ca
    # The reason we do it here is to minimize the amount of time it is possible to automatically get a certificate

    # We don't do anything if we are in spoof mode.
    return true if @spoof

    # This should terminate the before_filter and the action. We return a HTTP
    # error so the installer knows something is wrong. This is tested with
    # Anaconda, but maybe Suninstall will choke on it.
    render(:text => _("Failed to clean any old certificates or add the autosign entry. Terminating the build!"), :status => :internal_server_error) unless @host.handle_ca
    #TODO: Email the user who initiated this build operation.
  end

  # Reset realm OTP. This is run as a before_filter for provisioning templates.
  def handle_realm
    # We don't do anything if we are in spoof mode.
    return true if @spoof

    # This should terminate the before_filter and the action. We return a HTTP
    # error so the installer knows something is wrong. This is tested with
    # Anaconda, but maybe Suninstall will choke on it.
    render(:text => _("Failed to get a new realm OTP. Terminating the build!"), :status => :internal_server_error) unless @host.handle_realm
  end

  def set_content_type
    response.headers['Content-Type'] = 'text/plain'
  end

  # This method updates the IP held by Foreman from the incoming request.
  # Useful on unmanaged DHCP systems, with token-based installs where Foreman
  # doesn't know the IP in advance (and has been given a fake one just to make
  # the form save)
  def update_ip
    ip = ip_from_request_env
    logger.debug "Built notice from #{ip}, current host ip is #{@host.ip}, updating" if @host.ip != ip

    # @host has been changed even if the save fails, so we have to change it back
    old_ip = @host.ip
    @host.ip = old_ip unless @host.update_attributes({'ip' => ip})
  end

  def ip_from_request_env
    ip = request.env['REMOTE_ADDR']

    # check if someone is asking on behalf of another system (load balance etc)
    if request.env['HTTP_X_FORWARDED_FOR'].present? and (ip =~ Regexp.new(Setting[:remote_addr]))
      ip = request.env['HTTP_X_FORWARDED_FOR']
    end

    ip
  end

  def safe_render(template)
    @template_name = 'Unnamed'
    if template.is_a?(String)
      @unsafe_template  = template
    elsif template.is_a?(ProvisioningTemplate)
      @unsafe_template  = template.template
      @template_name = template.name
    else
      raise "unknown template"
    end

    begin
      render :inline => "<%= unattended_render(@unsafe_template, @template_name).html_safe %>" and return
    rescue => error
      msg = _("There was an error rendering the %s template: ") % (@template_name)
      Foreman::Logging.exception(msg, error)
      render :text => msg + error.message, :status => :internal_server_error and return
    end
  end
end
