require 'fileutils'

module Shipit
  class Deploy < Task
    CONFIRMATIONS_REQUIRED = 5

    state_machine :status do
      after_transition to: :success, do: :schedule_continuous_delivery
      after_transition to: :success, do: :schedule_merges
      after_transition to: :success, do: :update_undeployed_commits_count
      after_transition to: :aborted, do: :trigger_revert_if_required
      after_transition any => any, do: :update_commit_deployments
      after_transition any => any, do: :update_last_deploy_time
    end

    has_many :commit_deployments, dependent: :destroy, inverse_of: :task, foreign_key: :task_id do
      GITHUB_STATUSES = {
        'pending' => 'pending',
        'failed' => 'failure',
        'success' => 'success',
        'error' => 'error',
        'aborted' => 'error',
      }.freeze

      def append_status(task_status)
        if github_status = GITHUB_STATUSES[task_status]
          each do |deployment|
            deployment.statuses.create!(status: github_status)
          end
        end
      end
    end

    before_create :denormalize_commit_stats
    after_create :create_commit_deployments
    after_commit :broadcast_update

    delegate :broadcast_update, :filter_deploy_envs, to: :stack

    def build_rollback(user = nil, env: nil, force: false)
      Rollback.new(
        user_id: user.try!(:id),
        stack_id: stack_id,
        parent_id: id,
        since_commit: stack.last_deployed_commit,
        until_commit: until_commit,
        env: env.try!(:to_h) || {},
        allow_concurrency: force,
      )
    end

    # Rolls the stack back to this deploy
    def trigger_rollback(user = AnonymousUser.new, env: nil, force: false)
      rollback = build_rollback(user, env: env, force: force)
      rollback.save!
      rollback.enqueue

      lock_reason = "A rollback for #{rollback.since_commit.sha} has been triggered. " \
        "Please make sure the reason for the rollback has been addressed before deploying again."
      stack.update!(lock_reason: lock_reason, lock_author_id: user.id)

      rollback
    end

    # Rolls the stack back to the **previous** deploy
    def trigger_revert
      rollback = Rollback.create!(
        user_id: user_id,
        stack_id: stack_id,
        parent_id: id,
        since_commit: until_commit,
        until_commit: since_commit,
      )
      rollback.enqueue
      lock_reason = "A rollback for #{until_commit.sha} has been triggered. " \
        "Please make sure the reason for the rollback has been addressed before deploying again."
      stack.update!(lock_reason: lock_reason, lock_author_id: user_id)
      rollback
    end

    def rollback?
      false
    end

    def commit_range
      [since_commit, until_commit]
    end

    delegate :supports_rollback?, to: :stack

    def rollbackable?
      success? && supports_rollback? && !currently_deployed?
    end

    def currently_deployed?
      until_commit_id == stack.last_deployed_commit.id
    end

    def commits
      return Commit.none unless stack

      @commits ||= stack.commits.reachable.newer_than(since_commit_id).until(until_commit_id).order(id: :desc)
    end

    def commits_since
      return Commit.none unless stack

      @commits_since ||= stack.commits.reachable.newer_than(until_commit_id).order(id: :desc)
    end

    def since_commit_id
      super || default_since_commit_id
    end

    def variables
      stack.deploy_variables
    end

    def reject!
      return if failed?
      transaction do
        flap! unless flapping?
        update!(confirmations: [confirmations - 1, -1].min)
        failure! if confirmed?
      end
    end

    def accept!
      return if success?
      transaction do
        flap! unless flapping?
        update!(confirmations: [confirmations + 1, 1].max)
        complete! if confirmed?
      end
    end

    def confirmed?
      confirmations.abs >= CONFIRMATIONS_REQUIRED
    end

    private

    def create_commit_deployments
      commits.each do |commit|
        commit_deployments.create!(commit: commit)
      end
    end

    def update_commit_deployments
      commit_deployments.append_status(status)
    end

    def trigger_revert_if_required
      return unless rollback_once_aborted?
      return unless supports_rollback?
      trigger_revert
    end

    def default_since_commit_id
      return unless stack
      @default_since_commit_id ||= last_successful_deploy.try!(:until_commit_id)
    end

    def denormalize_commit_stats
      self.additions = commits.map(&:additions).compact.sum
      self.deletions = commits.map(&:deletions).compact.sum
    end

    def schedule_merges
      stack.schedule_merges
    end

    def schedule_continuous_delivery
      return unless stack.continuous_deployment?
      ContinuousDeliveryJob.perform_later(stack)
    end

    def last_successful_deploy
      stack.deploys.where(status: "success").last
    end

    def update_undeployed_commits_count
      stack.update_undeployed_commits_count(until_commit)
    end

    def update_last_deploy_time
      stack.update(last_deployed_at: ended_at)
    end
  end
end
