package locking

import (
	"errors"
	"time"

	"github.com/runatlantis/atlantis/server/events/command"
)

//go:generate pegomock generate --package mocks -o mocks/mock_apply_lock_checker.go ApplyLockChecker

// ApplyLockChecker is an implementation of the global apply lock retrieval.
// It returns an object that contains information about apply locks status.
type ApplyLockChecker interface {
	CheckApplyLock() (ApplyCommandLock, error)
}

//go:generate pegomock generate --package mocks -o mocks/mock_apply_locker.go ApplyLocker

// ApplyLocker interface that manages locks for apply command runner
type ApplyLocker interface {
	// LockApply creates a lock for ApplyCommand if lock already exists it will
	// return existing lock without any changes
	LockApply() (ApplyCommandLock, error)
	// UnlockApply deletes apply lock created by LockApply if present, otherwise
	// it is a no-op
	UnlockApply() error
	ApplyLockChecker
}

// ApplyCommandLock contains information about apply command lock status.
type ApplyCommandLock struct {
	// Locked is true is when apply commands are locked
	// Either by using omitting apply from AllowCommands or creating a global ApplyCommandLock
	// DisableApply lock take precedence when set
	Locked                 bool
	GlobalApplyLockEnabled bool
	Time                   time.Time
	Failure                string
}

type ApplyClient struct {
	backend                Backend
	disableApply           bool
	disableGlobalApplyLock bool
}

func NewApplyClient(backend Backend, disableApply bool, disableGlobalApplyLock bool) ApplyLocker {
	return &ApplyClient{
		backend:                backend,
		disableApply:           disableApply,
		disableGlobalApplyLock: disableGlobalApplyLock,
	}
}

// LockApply acquires global apply lock.
// DisableApply takes precedence to any existing locks, if it is set to true
// this function returns an error
func (c *ApplyClient) LockApply() (ApplyCommandLock, error) {
	response := ApplyCommandLock{}

	if c.disableApply {
		return response, errors.New("apply is omitted from AllowCommands; Apply commands are locked globally until flag is updated")
	}

	applyCmdLock, err := c.backend.LockCommand(command.Apply, time.Now())
	if err != nil {
		return response, err
	}

	if applyCmdLock != nil {
		response.Locked = true
		response.Time = applyCmdLock.LockTime()
	}
	return response, nil
}

// UnlockApply releases a global apply lock.
// DisableApply takes precedence to any existing locks, if it is set to true
// this function returns an error
func (c *ApplyClient) UnlockApply() error {
	if c.disableApply {
		return errors.New("apply commands are disabled until AllowCommands flag is updated")
	}

	err := c.backend.UnlockCommand(command.Apply)
	if err != nil {
		return err
	}

	return nil
}

// CheckApplyLock retrieves an apply command lock if present.
// If DisableApply is set it will always return a lock.
func (c *ApplyClient) CheckApplyLock() (ApplyCommandLock, error) {
	response := ApplyCommandLock{
		GlobalApplyLockEnabled: true,
	}

	if c.disableApply {
		return ApplyCommandLock{
			Locked: true,
		}, nil
	}

	applyCmdLock, err := c.backend.CheckCommandLock(command.Apply)
	if err != nil {
		return response, err
	}

	if applyCmdLock != nil {
		response.Locked = true
		response.Time = applyCmdLock.LockTime()
	}
	if c.disableGlobalApplyLock {
		response.GlobalApplyLockEnabled = false
	}

	return response, nil
}
