/*
Copyright 2019 The Knative Authors

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 apis

import (
	"reflect"
	"sort"
	"time"

	"fmt"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Conditions is the interface for a Resource that implements the getter and
// setter for accessing a Condition collection.
// +k8s:deepcopy-gen=true
type ConditionsAccessor interface {
	GetConditions() Conditions
	SetConditions(Conditions)
}

// ConditionAccessor is used to access a condition through it's type
type ConditionAccessor interface {
	// GetCondition finds and returns the Condition that matches the ConditionType
	// It should return nil if the condition type is not present
	GetCondition(t ConditionType) *Condition
}

// ConditionSet is an abstract collection of the possible ConditionType values
// that a particular resource might expose.  It also holds the "happy condition"
// for that resource, which we define to be one of Ready or Succeeded depending
// on whether it is a Living or Batch process respectively.
// +k8s:deepcopy-gen=false
type ConditionSet struct {
	happy      ConditionType
	dependents []ConditionType
}

// ConditionManager allows a resource to operate on its Conditions using higher
// order operations.
type ConditionManager interface {
	ConditionAccessor

	// IsHappy looks at the happy condition and returns true if that condition is
	// set to true.
	IsHappy() bool

	// GetTopLevelCondition finds and returns the top level Condition (happy Condition).
	GetTopLevelCondition() *Condition

	// SetCondition sets or updates the Condition on Conditions for Condition.Type.
	// If there is an update, Conditions are stored back sorted.
	SetCondition(new Condition)

	// ClearCondition removes the non terminal condition that matches the ConditionType
	ClearCondition(t ConditionType) error

	// MarkTrue sets the status of t to true, and then marks the happy condition to
	// true if all dependents are true.
	MarkTrue(t ConditionType)

	// MarkUnknown sets the status of t to Unknown and also sets the happy condition
	// to Unknown if no other dependent condition is in an error state.
	MarkUnknown(t ConditionType, reason, messageFormat string, messageA ...interface{})

	// MarkFalse sets the status of t and the happy condition to False.
	MarkFalse(t ConditionType, reason, messageFormat string, messageA ...interface{})

	// InitializeConditions updates all Conditions in the ConditionSet to Unknown
	// if not set.
	InitializeConditions()
}

// NewLivingConditionSet returns a ConditionSet to hold the conditions for the
// living resource. ConditionReady is used as the happy condition.
// The set of condition types provided are those of the terminal subconditions.
func NewLivingConditionSet(d ...ConditionType) ConditionSet {
	return newConditionSet(ConditionReady, d...)
}

// NewBatchConditionSet returns a ConditionSet to hold the conditions for the
// batch resource. ConditionSucceeded is used as the happy condition.
// The set of condition types provided are those of the terminal subconditions.
func NewBatchConditionSet(d ...ConditionType) ConditionSet {
	return newConditionSet(ConditionSucceeded, d...)
}

// newConditionSet returns a ConditionSet to hold the conditions that are
// important for the caller. The first ConditionType is the overarching status
// for that will be used to signal the resources' status is Ready or Succeeded.
func newConditionSet(happy ConditionType, dependents ...ConditionType) ConditionSet {
	var deps []ConditionType
	for _, d := range dependents {
		// Skip duplicates
		if d == happy || contains(deps, d) {
			continue
		}
		deps = append(deps, d)
	}
	return ConditionSet{
		happy:      happy,
		dependents: deps,
	}
}

func contains(ct []ConditionType, t ConditionType) bool {
	for _, c := range ct {
		if c == t {
			return true
		}
	}
	return false
}

// Check that conditionsImpl implements ConditionManager.
var _ ConditionManager = (*conditionsImpl)(nil)

// conditionsImpl implements the helper methods for evaluating Conditions.
// +k8s:deepcopy-gen=false
type conditionsImpl struct {
	ConditionSet
	accessor ConditionsAccessor
}

// Manage creates a ConditionManager from an accessor object using the original
// ConditionSet as a reference. Status must be a pointer to a struct.
func (r ConditionSet) Manage(status ConditionsAccessor) ConditionManager {
	return conditionsImpl{
		accessor:     status,
		ConditionSet: r,
	}
}

// IsHappy looks at the top level Condition (happy Condition) and returns true if that condition is
// set to true.
func (r conditionsImpl) IsHappy() bool {
	return r.GetTopLevelCondition().IsTrue()
}

// GetTopLevelCondition finds and returns the top level Condition (happy Condition).
func (r conditionsImpl) GetTopLevelCondition() *Condition {
	return r.GetCondition(r.happy)
}

// GetCondition finds and returns the Condition that matches the ConditionType
// previously set on Conditions.
func (r conditionsImpl) GetCondition(t ConditionType) *Condition {
	if r.accessor == nil {
		return nil
	}

	for _, c := range r.accessor.GetConditions() {
		if c.Type == t {
			return &c
		}
	}
	return nil
}

// SetCondition sets or updates the Condition on Conditions for Condition.Type.
// If there is an update, Conditions are stored back sorted.
func (r conditionsImpl) SetCondition(new Condition) {
	if r.accessor == nil {
		return
	}
	t := new.Type
	var conditions Conditions
	for _, c := range r.accessor.GetConditions() {
		if c.Type != t {
			conditions = append(conditions, c)
		} else {
			// If we'd only update the LastTransitionTime, then return.
			new.LastTransitionTime = c.LastTransitionTime
			if reflect.DeepEqual(&new, &c) {
				return
			}
		}
	}
	new.LastTransitionTime = VolatileTime{Inner: metav1.NewTime(time.Now())}
	conditions = append(conditions, new)
	// Sorted for convenience of the consumer, i.e. kubectl.
	sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
	r.accessor.SetConditions(conditions)
}

func (r conditionsImpl) isTerminal(t ConditionType) bool {
	for _, cond := range r.dependents {
		if cond == t {
			return true
		}
	}
	return t == r.happy
}

func (r conditionsImpl) severity(t ConditionType) ConditionSeverity {
	if r.isTerminal(t) {
		return ConditionSeverityError
	}
	return ConditionSeverityInfo
}

// RemoveCondition removes the non terminal condition that matches the ConditionType
// Not implemented for terminal conditions
func (r conditionsImpl) ClearCondition(t ConditionType) error {
	var conditions Conditions

	if r.accessor == nil {
		return nil
	}
	// Terminal conditions are not handled as they can't be nil
	if r.isTerminal(t) {
		return fmt.Errorf("Clearing terminal conditions not implemented")
	}
	cond := r.GetCondition(t)
	if cond == nil {
		return nil
	}
	for _, c := range r.accessor.GetConditions() {
		if c.Type != t {
			conditions = append(conditions, c)
		}
	}

	// Sorted for convenience of the consumer, i.e. kubectl.
	sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
	r.accessor.SetConditions(conditions)

	return nil
}

// MarkTrue sets the status of t to true, and then marks the happy condition to
// true if all other dependents are also true.
func (r conditionsImpl) MarkTrue(t ConditionType) {
	// Set the specified condition.
	r.SetCondition(Condition{
		Type:     t,
		Status:   corev1.ConditionTrue,
		Severity: r.severity(t),
	})

	if c := r.findUnhappyDependent(); c != nil {
		// Propagate unhappy dependent to happy condition.
		r.SetCondition(Condition{
			Type:     r.happy,
			Status:   c.Status,
			Reason:   c.Reason,
			Message:  c.Message,
			Severity: r.severity(r.happy),
		})
	} else if t != r.happy {
		// Set the happy condition to true.
		r.SetCondition(Condition{
			Type:     r.happy,
			Status:   corev1.ConditionTrue,
			Severity: r.severity(r.happy),
		})
	}
}

func (r conditionsImpl) findUnhappyDependent() *Condition {
	// This only works if there are dependents.
	if len(r.dependents) == 0 {
		return nil
	}

	// Do not modify the accessors condition order.
	conditions := r.accessor.GetConditions().DeepCopy()

	// Filter based on terminal status.
	n := 0
	for _, c := range conditions {
		if c.Severity == ConditionSeverityError && c.Type != r.happy {
			conditions[n] = c
			n++
		}
	}
	conditions = conditions[:n]

	// Sort set conditions by time.
	sort.Slice(conditions, func(i, j int) bool {
		return conditions[i].LastTransitionTime.Inner.Time.After(conditions[j].LastTransitionTime.Inner.Time)
	})

	// First check the conditions with Status == False.
	for _, c := range conditions {
		// False conditions trump Unknown.
		if c.IsFalse() {
			return &c
		}
	}
	// Second check for conditions with Status == Unknown.
	for _, c := range conditions {
		if c.IsUnknown() {
			return &c
		}
	}

	// If something was not initialized.
	if len(r.dependents) != len(conditions) {
		return &Condition{
			Status: corev1.ConditionUnknown,
		}
	}

	// All dependents are fine.
	return nil
}

// MarkUnknown sets the status of t to Unknown and also sets the happy condition
// to Unknown if no other dependent condition is in an error state.
func (r conditionsImpl) MarkUnknown(t ConditionType, reason, messageFormat string, messageA ...interface{}) {
	// set the specified condition
	r.SetCondition(Condition{
		Type:     t,
		Status:   corev1.ConditionUnknown,
		Reason:   reason,
		Message:  fmt.Sprintf(messageFormat, messageA...),
		Severity: r.severity(t),
	})

	// check the dependents.
	isDependent := false
	for _, cond := range r.dependents {
		c := r.GetCondition(cond)
		// Failed conditions trump Unknown conditions
		if c.IsFalse() {
			// Double check that the happy condition is also false.
			happy := r.GetCondition(r.happy)
			if !happy.IsFalse() {
				r.MarkFalse(r.happy, reason, messageFormat, messageA...)
			}
			return
		}
		if cond == t {
			isDependent = true
		}
	}

	if isDependent {
		// set the happy condition, if it is one of our dependent subconditions.
		r.SetCondition(Condition{
			Type:     r.happy,
			Status:   corev1.ConditionUnknown,
			Reason:   reason,
			Message:  fmt.Sprintf(messageFormat, messageA...),
			Severity: r.severity(r.happy),
		})
	}
}

// MarkFalse sets the status of t and the happy condition to False.
func (r conditionsImpl) MarkFalse(t ConditionType, reason, messageFormat string, messageA ...interface{}) {
	types := []ConditionType{t}
	for _, cond := range r.dependents {
		if cond == t {
			types = append(types, r.happy)
		}
	}

	for _, t := range types {
		r.SetCondition(Condition{
			Type:     t,
			Status:   corev1.ConditionFalse,
			Reason:   reason,
			Message:  fmt.Sprintf(messageFormat, messageA...),
			Severity: r.severity(t),
		})
	}
}

// InitializeConditions updates all Conditions in the ConditionSet to Unknown
// if not set.
func (r conditionsImpl) InitializeConditions() {
	happy := r.GetCondition(r.happy)
	if happy == nil {
		happy = &Condition{
			Type:     r.happy,
			Status:   corev1.ConditionUnknown,
			Severity: ConditionSeverityError,
		}
		r.SetCondition(*happy)
	}
	// If the happy state is true, it implies that all of the terminal
	// subconditions must be true, so initialize any unset conditions to
	// true if our happy condition is true, otherwise unknown.
	status := corev1.ConditionUnknown
	if happy.Status == corev1.ConditionTrue {
		status = corev1.ConditionTrue
	}
	for _, t := range r.dependents {
		r.initializeTerminalCondition(t, status)
	}
}

// initializeTerminalCondition initializes a Condition to the given status if unset.
func (r conditionsImpl) initializeTerminalCondition(t ConditionType, status corev1.ConditionStatus) *Condition {
	if c := r.GetCondition(t); c != nil {
		return c
	}
	c := Condition{
		Type:     t,
		Status:   status,
		Severity: ConditionSeverityError,
	}
	r.SetCondition(c)
	return &c
}
