/*
Copyright 2020 the Velero contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package hook

import (
	"fmt"
	"sync"
)

const (
	HookSourceAnnotation = "annotation"
	HookSourceSpec       = "spec"
)

// hookKey identifies a backup/restore hook
type hookKey struct {
	// PodNamespace indicates the namespace of pod where hooks are executed.
	// For hooks specified in the backup/restore spec, this field is the namespace of an applicable pod.
	// For hooks specified in pod annotation, this field is the namespace of pod where hooks are annotated.
	podNamespace string
	// PodName indicates the pod where hooks are executed.
	// For hooks specified in the backup/restore spec, this field is an applicable pod name.
	// For hooks specified in pod annotation, this field is the pod where hooks are annotated.
	podName string
	// HookPhase is only for backup hooks, for restore hooks, this field is empty.
	hookPhase HookPhase
	// HookName is only for hooks specified in the backup/restore spec.
	// For hooks specified in pod annotation, this field is empty or "<from-annotation>".
	hookName string
	// HookSource indicates where hooks come from.
	hookSource string
	// Container indicates the container hooks use.
	// For hooks specified in the backup/restore spec, the container might be the same under different hookName.
	container string
	// hookIndex contains the slice index for the specific hook, in order to track multiple hooks
	// for the same container
	hookIndex int
}

// hookStatus records the execution status of a specific hook.
// hookStatus is extensible to accommodate additional fields as needs develop.
type hookStatus struct {
	// HookFailed indicates if hook failed to execute.
	hookFailed bool
	// hookExecuted indicates if hook already execute.
	hookExecuted bool
}

// HookTracker tracks all hooks' execution status in a single backup/restore.
type HookTracker struct {
	lock *sync.RWMutex
	// tracker records all hook info for a single backup/restore.
	tracker map[hookKey]hookStatus
	// hookAttemptedCnt indicates the number of attempted hooks.
	hookAttemptedCnt int
	// hookFailedCnt indicates the number of failed hooks.
	hookFailedCnt int
	// HookExecutedCnt indicates the number of executed hooks.
	hookExecutedCnt int
	// hookErrs records hook execution errors if any.
	hookErrs []HookErrInfo
}

// NewHookTracker creates a hookTracker instance.
func NewHookTracker() *HookTracker {
	return &HookTracker{
		lock:    &sync.RWMutex{},
		tracker: make(map[hookKey]hookStatus),
	}
}

// Add adds a hook to the hook tracker
// Add must precede the Record for each individual hook.
// In other words, a hook must be added to the tracker before its execution result is recorded.
func (ht *HookTracker) Add(podNamespace, podName, container, source, hookName string, hookPhase HookPhase, hookIndex int) {
	ht.lock.Lock()
	defer ht.lock.Unlock()

	key := hookKey{
		podNamespace: podNamespace,
		podName:      podName,
		hookSource:   source,
		container:    container,
		hookPhase:    hookPhase,
		hookName:     hookName,
		hookIndex:    hookIndex,
	}

	if _, ok := ht.tracker[key]; !ok {
		ht.tracker[key] = hookStatus{
			hookFailed:   false,
			hookExecuted: false,
		}
		ht.hookAttemptedCnt++
	}
}

// Record records the hook's execution status
// Add must precede the Record for each individual hook.
// In other words, a hook must be added to the tracker before its execution result is recorded.
func (ht *HookTracker) Record(podNamespace, podName, container, source, hookName string, hookPhase HookPhase, hookIndex int, hookFailed bool, hookErr error) error {
	ht.lock.Lock()
	defer ht.lock.Unlock()

	key := hookKey{
		podNamespace: podNamespace,
		podName:      podName,
		hookSource:   source,
		container:    container,
		hookPhase:    hookPhase,
		hookName:     hookName,
		hookIndex:    hookIndex,
	}

	if _, ok := ht.tracker[key]; !ok {
		return fmt.Errorf("hook not exist in hook tracker, hook: %+v", key)
	}

	if !ht.tracker[key].hookExecuted {
		ht.tracker[key] = hookStatus{
			hookFailed:   hookFailed,
			hookExecuted: true,
		}
		ht.hookExecutedCnt++
		if hookFailed {
			ht.hookFailedCnt++
			ht.hookErrs = append(ht.hookErrs, HookErrInfo{Namespace: key.podNamespace, Err: hookErr})
		}
	}
	return nil
}

// Stat returns the number of attempted hooks and failed hooks
func (ht *HookTracker) Stat() (hookAttemptedCnt int, hookFailedCnt int) {
	ht.lock.RLock()
	defer ht.lock.RUnlock()

	return ht.hookAttemptedCnt, ht.hookFailedCnt
}

// IsComplete returns whether the execution of all hooks has finished or not
func (ht *HookTracker) IsComplete() bool {
	ht.lock.RLock()
	defer ht.lock.RUnlock()

	return ht.hookAttemptedCnt == ht.hookExecutedCnt
}

// HooksErr returns hook execution errors
func (ht *HookTracker) HookErrs() []HookErrInfo {
	ht.lock.RLock()
	defer ht.lock.RUnlock()

	return ht.hookErrs
}

// MultiHookTrackers tracks all hooks' execution status for multiple backups/restores.
type MultiHookTracker struct {
	lock *sync.RWMutex
	// trackers is a map that uses the backup/restore name as the key and stores a HookTracker as value.
	trackers map[string]*HookTracker
}

// NewMultiHookTracker creates a multiHookTracker instance.
func NewMultiHookTracker() *MultiHookTracker {
	return &MultiHookTracker{
		lock:     &sync.RWMutex{},
		trackers: make(map[string]*HookTracker),
	}
}

// Add adds a backup/restore hook to the tracker
func (mht *MultiHookTracker) Add(name, podNamespace, podName, container, source, hookName string, hookPhase HookPhase, hookIndex int) {
	mht.lock.Lock()
	defer mht.lock.Unlock()

	if _, ok := mht.trackers[name]; !ok {
		mht.trackers[name] = NewHookTracker()
	}
	mht.trackers[name].Add(podNamespace, podName, container, source, hookName, hookPhase, hookIndex)
}

// Record records a backup/restore hook execution status
func (mht *MultiHookTracker) Record(name, podNamespace, podName, container, source, hookName string, hookPhase HookPhase, hookIndex int, hookFailed bool, hookErr error) error {
	mht.lock.RLock()
	defer mht.lock.RUnlock()

	var err error
	if _, ok := mht.trackers[name]; ok {
		err = mht.trackers[name].Record(podNamespace, podName, container, source, hookName, hookPhase, hookIndex, hookFailed, hookErr)
	} else {
		err = fmt.Errorf("the backup/restore not exist in hook tracker, backup/restore name: %s", name)
	}
	return err
}

// Stat returns the number of attempted hooks and failed hooks for a particular backup/restore
func (mht *MultiHookTracker) Stat(name string) (hookAttemptedCnt int, hookFailedCnt int) {
	mht.lock.RLock()
	defer mht.lock.RUnlock()

	if _, ok := mht.trackers[name]; ok {
		return mht.trackers[name].Stat()
	}
	return
}

// Delete removes the hook data for a particular backup/restore
func (mht *MultiHookTracker) Delete(name string) {
	mht.lock.Lock()
	defer mht.lock.Unlock()

	delete(mht.trackers, name)
}

// IsComplete returns whether the execution of all hooks for a particular backup/restore has finished or not
func (mht *MultiHookTracker) IsComplete(name string) bool {
	mht.lock.RLock()
	defer mht.lock.RUnlock()

	if _, ok := mht.trackers[name]; ok {
		return mht.trackers[name].IsComplete()
	}
	return true
}

// HooksErr returns hook execution errors for a particular backup/restore
func (mht *MultiHookTracker) HookErrs(name string) []HookErrInfo {
	mht.lock.RLock()
	defer mht.lock.RUnlock()

	if _, ok := mht.trackers[name]; ok {
		return mht.trackers[name].HookErrs()
	}
	return nil
}
