# == Schema Information
#
# Table name: easy_ml_deploys
#
#  id                :bigint           not null, primary key
#  model_id          :bigint
#  model_history_id  :bigint
#  retraining_run_id :bigint
#  model_file_id     :bigint
#  status            :string           not null
#  trigger           :string           default("manual")
#  stacktrace        :text
#  snapshot_id       :string
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#
module EasyML
  class Deploy < ActiveRecord::Base
    self.table_name = "easy_ml_deploys"

    belongs_to :model, class_name: "EasyML::Model"
    belongs_to :model_file, class_name: "EasyML::ModelFile", optional: true
    belongs_to :retraining_run, class_name: "EasyML::RetrainingRun"
    belongs_to :model_version, class_name: "EasyML::ModelHistory", optional: true, foreign_key: :model_history_id

    validates :status, presence: true
    after_initialize :set_defaults, if: :new_record?
    before_save :set_model_file, if: :new_record?
    validates :status, presence: true, inclusion: { in: %w[pending running success failed] }

    scope :latest, -> { select("DISTINCT ON (model_id) *").order("model_id, id DESC") }

    def unlocked?
      EasyML::Deploy.where(model_id: model_id).where.not(locked_at: nil).where(status: ["pending", "running"]).empty?
    end

    def locked?
      !unlocked?
    end

    def deploy(async: true)
      if async
        EasyML::DeployJob.perform_later(id)
      else
        actually_deploy
      end
    end

    def actually_deploy
      lock_deploy do
        begin
          update(status: "running")
          EasyML::Event.create_event(self, "started")

          if identical_deploy.present?
            self.model_file = identical_deploy.model_file
            self.model_version = identical_deploy.model_version
          else
            if model_file.present?
              model.model_file = model_file
            end
            # model.load_model
            self.model_version = model.actually_deploy
          end

          EasyML::Deploy.transaction do
            update(model_history_id: self.model_version.id, snapshot_id: self.model_version.snapshot_id, status: :success)
            model.retraining_runs.where(status: :deployed).update_all(status: :success, is_deploying: false)
            retraining_run.update(
              model_history_id: self.model_version.id,
              snapshot_id: self.model_version.snapshot_id,
              deploy_id: id,
              status: :deployed,
              is_deploying: false
            )
          end

          model_version.tap do
            EasyML::Event.create_event(self, "success")
          end
        rescue => e
          update(status: "failed")
          retraining_run.update(is_deploying: false)
          EasyML::Event.create_event(self, "failed")
          raise e
        ensure
          unlock!
        end
      end
    end

    alias_method :rollback, :deploy

    def unlock!
      Support::Lockable.unlock!(lock_key)
    end

    def lock_deploy
      with_lock do |client|
        yield
      end
    end

    def identical_deploy
      EasyML::Deploy.where(retraining_run_id: retraining_run_id).
        where.not(id: id).where(status: :success).limit(1).first
    end

    private

    def with_lock
      EasyML::Support::Lockable.with_lock(lock_key, stale_timeout: 60, resources: 1) do |client|
        yield client
      end
    end

    def lock_key
      "deploy:#{self.model.name}:#{self.model.id}"
    end

    def set_defaults
      self.status ||= :pending
    end

    def set_model_file
      self.model_file ||= retraining_run.model_file
    end
  end
end
