module Katello
  class TaskStatus < Katello::Model
    include Util::TaskStatus

    serialize :result
    serialize :progress
    serialize :parameters, Hash
    class Status
      WAITING = :waiting
      RUNNING = :running
      ERROR = :error
      FINISHED = :finished
      CANCELED = :canceled
      TIMED_OUT = :timed_out
    end

    belongs_to :organization, :inverse_of => :task_statuses, :class_name => "Organization"
    belongs_to :user, :inverse_of => :task_statuses, :class_name => "::User"

    belongs_to :task_owner, :polymorphic => true

    # needed to delete providers w/ task status
    has_one :provider, :class_name => "Katello::Provider", :dependent => :nullify

    validates_lengths_from_database
    before_save :setup_task_type

    before_save do |status|
      unless status.user
        status.user = User.current
      end
    end

    # log error to the rails log
    before_save do |status|
      if status.state_changed?
        begin
          if status.state == TaskStatus::Status::ERROR.to_s
            Rails.logger.error "Task #{status.task_type} (#{status.id}) is in error state"
            Rails.logger.debug "Task parameters: #{status.parameters.inspect.to_s[0, 255]}, result: #{status.result.inspect.to_s[0, 255]}"
          else
            Rails.logger.debug "Task #{status.task_type} (#{status.id}) #{status.state}" if status.id
          end
          return true
        rescue
          Rails.logger.debug "Unable to report status change" # minor error
          # if logger level is higher than debug logger return false that would cause rollback
          # since this is log only callback we must be sure to return true
          true
        end
      end
    end

    def initialize(attrs = nil, _options = {})
      unless attrs.nil?
        # only keep keys for which we have db columns
        attrs = attrs.reject do |k, _v|
          !self.class.column_defaults.keys.member?(k.to_s) && (!respond_to?(:"#{k.to_s}=") rescue true)
        end
      end

      super(attrs)
    end

    def overall_status
      # the overall status of tasks (e.g. associated with a system) are determined by a
      # combination of the task state and the status of the unit within the task.
      unit_status = true
      if (self.result.is_a? Hash) && (self.result.key? :details)
        if self.result[:details].key? :rpm
          unit_status = self.result[:details][:rpm][:succeeded]
        elsif self.result[:details].key? :package_group
          unit_status = self.result[:details][:package_group][:succeeded]
        end
      end

      (self.state.to_s == "error" || !unit_status) ? "error" : self.state
    end

    def pending?
      self.state.to_s == "waiting" || self.state.to_s == "running"
    end

    def finished?
      ((self.state != TaskStatus::Status::WAITING.to_s) && (self.state != TaskStatus::Status::RUNNING.to_s))
    end

    def canceled?
      self.state == TaskStatus::Status::CANCELED.to_s
    end

    def error?
      (self.state == TaskStatus::Status::ERROR.to_s)
    end

    def refresh
      self
    end

    def human_readable_message
      task_template = TaskStatus::TYPES[self.task_type]
      return '' if task_template.nil?
      if task_template[:user_message]
        task_template[:user_message] % self.user.login
      else
        task_template[:english_name]
      end
    end

    def pending_message
      # Retrieve a text message that may be rendered for a 'pending' task's status.  This is used in various places,
      # such as System Event history.
      details = TaskStatus::TYPES[self.task_type]
      case details[:type]
      when :package
        p = self.parameters[:packages]
        unless p && p.length > 0
          if self.task_type == "package_update"
            return _("all packages")
          end
          return ""
        end
        if p.length == 1
          return p.first
        else
          return _("%{package} (%{total} other packages)") % {:package => p.first, :total => p.length - 1}
        end
      when :package_group
        p = self.parameters[:groups]
        if p.length == 1
          return p.first
        else
          return _("%{group} (%{total} other package groups)") % {:group => p.first, :total => p.length - 1}
        end
      when :errata
        p = self.parameters[:errata_ids]
        if p.length == 1
          return p.first
        else
          return _("%{errata} (%{total} other errata)") % {:errata => p.first, :total => p.length - 1}
        end
      end
    end

    # TODO: break up method
    # rubocop:disable Metrics/MethodLength
    def message
      # Retrieve a text message that may be rendered for a task's status.  This is used in various places,
      # such as System Event history.
      details = TaskStatus::TYPES[self.task_type]
      return _("Non-system event") if details.nil?

      case details[:type]
      when :package
        p = self.parameters[:packages]
        first_package = p.first.is_a?(Hash) ? p.first[:name] : p.first
        if !p && p.length > 0 && (self.task_type == "package_update")
          case self.overall_status
          when "running"
            return "updating"
          when "waiting"
            return "updating"
          when "error"
            return _("all packages update failed")
          else
            return _("all packages update")
          end
        end

        msg = details[:event_messages][self.overall_status]
        return n_(msg[1], msg[2], p.length) % { package: first_package, total: p.length - 1 }
      when :candlepin_event
        return self.result
      when :package_group
        p = self.parameters[:groups]
        msg = details[:event_messages][self.overall_status]
        return n_(msg[1], msg[2], p.length) % { group: p.first, total: p.length - 1 }
      when :errata
        p = self.parameters[:errata_ids]
        msg = details[:event_messages][self.overall_status]
        return n_(msg[1], msg[2], p.length) % { errata: p.first, total: p.length - 1 }
      end
    end

    def humanize_type
      TaskStatus::TYPES[self.task_type][:name]
    end

    def humanize_parameters
      humanized_parameters = []
      if (packages = self.parameters[:packages])
        humanized_parameters.concat(packages)
      end
      if (groups = self.parameters[:groups])
        humanized_parameters.concat(groups.map { |g| g =~ /^@/ ? g : "@#{g}" })
      end
      if (errata = self.parameters[:errata_ids])
        humanized_parameters.concat(errata)
      end
      humanized_parameters.join(", ")
    end

    def description
      ret = ""
      ret << humanize_type << ": "
      ret << humanize_parameters
    end

    def result_description
      case self.state.to_s
      when "finished"
        # tasks initiated by pulp to the system can have state=finished
        # when the request is fully successful (e.g. all packages installed)
        # as well as if the task is not fully successful (e.g. attempt to
        # install a pkg that does not exist)
        generate_description
      when "error"
        # tasks initiated by pulp to the system will only have state=error
        # if an exception is thrown from the system/agent during remote
        # method invocation
        rmi_error_description
      else ""
      end
    end

    def generate_description
      ret = []
      task_type = self.task_type.to_s

      case task_type
      when /^package_group/
        action = task_type.include?("remove") ? :removed : :installed
        ret = packages_change_description(result[:details][:package_group], action)
      when "package_install", "errata_install"
        ret = packages_change_description(result[:details][:rpm], :installed)
      when "package_update"
        ret = packages_change_description(result[:details][:rpm], :updated)
      when "package_remove"
        ret = packages_change_description(result[:details][:rpm], :removed)
      end
      ret
    end

    def rmi_error_description
      errors, stacktrace = self.result[:errors]
      return "" unless errors
      # Handle not very friendly Pulp message
      case errors
      when /^\(.*\)$/
        if stacktrace.instance_of?(Array)
          stacktrace.last.split(":").first
        else
          stacktrace.split("(").first
        end
      when /^\[.*,.*\]$/m
        result = errors.split(",").map do |error|
          error.gsub(/^\W+|\W+$/, "")
        end
        result.join("\n")
      else
        errors
      end
    rescue
      if self.result.is_a? Hash
        self.result[:errors].join(' ').to_s
      else
        self.result
      end
    end

    protected

    def setup_task_type
      unless self.task_type
        self.task_type = self.class.name
      end
    end

    def packages_change_description(data, action)
      ret = []

      data ||= {}
      data[:details] ||= {}
      data[:details][:resolved] ||= []
      data[:details][:deps] ||= []

      packages = data[:details][:resolved] + data[:details][:deps]
      if packages.empty?
        case action
        when :updated
          ret << _("No packages updated")
        when :removed
          ret << _("No packages removed")
        else
          ret << _("No new packages installed")
        end
      else
        if data[:succeeded]
          ret = packages.map do |package_attrs|
            package_attrs[:qname]
          end
        else
          ret << data[:details][:message]
        end
      end
      ret.sort
    end
  end
end
