/*
 * Copyright (c) Microsoft Corporation.
 * Licensed under the MIT license.
 */

package conditions

import (
	"fmt"

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

type Conditions []Condition

// FindIndexByType returns the index of the condition with the given ConditionType if it exists.
// If the Condition with the specified ConditionType is doesn't exist, the second boolean parameter is false.
func (c Conditions) FindIndexByType(conditionType ConditionType) (int, bool) {
	for i := range c {
		condition := c[i]
		if condition.Type == conditionType {
			return i, true
		}
	}

	return -1, false
}

// TODO: Hah, name...
type Conditioner interface {
	GetConditions() Conditions
	SetConditions(conditions Conditions)
}

// ConditionSeverity expresses the severity of a Condition.
type ConditionSeverity string

const (
	// ConditionSeverityError specifies that a failure of a condition type
	// should be viewed as an error. Errors are fatal to reconciliation and
	// mean that the user must take some action to resolve
	// the problem before reconciliation will be attempted again.
	ConditionSeverityError ConditionSeverity = "Error"

	// ConditionSeverityWarning specifies that a failure of a condition type
	// should be viewed as a warning. Warnings are informational. The operator
	// may be able to retry and resolve the warning without any action from the user, but
	// in some cases user action to resolve the warning will be required.
	ConditionSeverityWarning ConditionSeverity = "Warning"

	// ConditionSeverityInfo specifies that a failure of a condition type
	// should be viewed as purely informational. Things are working.
	// This is the happy path.
	ConditionSeverityInfo ConditionSeverity = "Info"

	// ConditionSeverityNone specifies that there is no condition severity.
	// For conditions which have positive polarity (Status == True is their normal/healthy state), this will set when Status == True
	// For conditions which have negative polarity (Status == False is their normal/healthy state), this will be set when Status == False.
	// Conditions in Status == Unknown always have a severity of None as well.
	// This is the default state for conditions.
	ConditionSeverityNone ConditionSeverity = ""
)

type ConditionType string

const (
	// ConditionTypeReady is a condition indicating if the resource is ready or not.
	// A ready resource is one that has been successfully provisioned to Azure according to the
	// resource spec. It has reached the goal state. This usually means that the resource is ready
	// to use, but the exact meaning of Ready may vary slightly from resource to resource. Resources with
	// caveats to Ready's meaning will call that out in the resource specific documentation.
	ConditionTypeReady = "Ready"
)

var _ fmt.Stringer = Condition{}

// Condition defines an extension to status (an observation) of a resource
// +kubebuilder:object:generate=true
//
//nolint:recvcheck
type Condition struct {
	// Type of condition.
	// +kubebuilder:validation:Required
	Type ConditionType `json:"type"`

	// Status of the condition, one of True, False, or Unknown.
	// +kubebuilder:validation:Required
	Status metav1.ConditionStatus `json:"status"`

	// Severity with which to treat failures of this type of condition.
	// For conditions which have positive polarity (Status == True is their normal/healthy state), this will be omitted when Status == True
	// For conditions which have negative polarity (Status == False is their normal/healthy state), this will be omitted when Status == False.
	// This is omitted in all cases when Status == Unknown
	// +kubebuilder:validation:Optional
	Severity ConditionSeverity `json:"severity,omitempty"`

	// LastTransitionTime is the last time the condition transitioned from one status to another.
	// +kubebuilder:validation:Required
	LastTransitionTime metav1.Time `json:"lastTransitionTime"`

	// Note: see the https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1623-standardize-conditions
	// KEP for details about ObservedGeneration

	// ObservedGeneration is the .metadata.generation that the condition was set based upon. For instance, if
	// .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date
	// with respect to the current state of the instance.
	// +kubebuilder:validation:Optional
	ObservedGeneration int64 `json:"observedGeneration,omitempty"`

	// Reason for the condition's last transition.
	// Reasons are upper CamelCase (PascalCase) with no spaces. A reason is always provided, this field will not be empty.
	// +kubebuilder:validation:Required
	Reason string `json:"reason"`

	// Message is a human readable message indicating details about the transition. This field may be empty.
	// +kubebuilder:validation:Optional
	Message string `json:"message,omitempty"`
}

// IsEquivalent returns true if this condition is equivalent to the passed in condition.
// Two conditions are equivalent if all of their fields EXCEPT LastTransitionTime are the same.
func (c *Condition) IsEquivalent(other Condition) bool {
	return c.Type == other.Type &&
		c.Status == other.Status &&
		c.Severity == other.Severity &&
		c.Reason == other.Reason &&
		c.ObservedGeneration == other.ObservedGeneration &&
		c.Message == other.Message
}

// ShouldOverwrite determines if this condition should overwrite the other condition.
func (c *Condition) ShouldOverwrite(other Condition) bool {
	// Safety check that the two conditions are of the same type. If not they certainly shouldn't overwrite
	if c.Type != other.Type {
		return false
	}
	// If this condition corresponds to a newer generation than the previous condition always overwrite
	// as other is out of date
	if c.ObservedGeneration > other.ObservedGeneration {
		return true
	}
	// If the Conditions are equivalent, don't overwrite. We want to keep the first occurrence of the condition
	// so that the LastTransitionTime is correct
	if c.IsEquivalent(other) {
		return false
	}
	// At this point the Conditions are the same type, same generation, so the winning Condition must be chosen
	// based on priority

	if c.priority() >= other.priority() {
		return true
	} else {
		return false
	}
}

// priority of the condition for overwrite purposes if Condition ObservedGeneration's are the same. Higher is more important.
// The result of this is the following:
//  1. Status == True conditions, and Status == False conditions with Severity == Warning or Error are all the highest priority.
//     This means that the most recent update with any of those states will overwrite anything.
//  2. Status == False conditions with Severity == Info will only overwrite other Status == False conditions with Severity == Info.
//  3. Status == Unknown conditions will not overwrite anything.
//
// Keep in mind that this priority is specifically for comparing Conditions with the same ObservedGeneration. If the ObservedGeneration
// is different, the newer one always wins.
func (c *Condition) priority() int {
	switch c.Status {
	case metav1.ConditionTrue:
		return 5
	case metav1.ConditionFalse:
		switch c.Severity {
		case ConditionSeverityError:
			return 5
		case ConditionSeverityWarning:
			return 5
		case ConditionSeverityInfo:
			return 4
		case ConditionSeverityNone:
			// This shouldn't happen as a Condition with Status False should always specify a severity.
			// In the interest of safety though, we set this to 5 so if this DOES somehow happen it ties
			// or wins against most other things and users will see it
			return 5
		}
	case metav1.ConditionUnknown:
		return 3
	}

	// This shouldn't happen
	return 0
}

// Copy returns an independent copy of the Condition
func (c *Condition) Copy() Condition {
	// NB: If you change this to a non-simple copy
	// you will need to update genruntime.CloneSliceOfCondition
	return *c
}

// String returns a string representation of this condition
func (c Condition) String() string {
	return fmt.Sprintf(
		"Condition [%s], Status = %q, ObservedGeneration = %d, Severity = %q, Reason = %q, Message = %q, LastTransitionTime = %q",
		c.Type,
		c.Status,
		c.ObservedGeneration,
		c.Severity,
		c.Reason,
		c.Message,
		c.LastTransitionTime)
}

// SetCondition sets the provided Condition on the Conditioner. The condition is only
// set if the new condition is different from the existing condition of the same type.
// See Condition.IsEquivalent and Condition.ShouldOverwrite for more details.
func SetCondition(o Conditioner, new Condition) {
	setCondition(o, new, func(new Condition, old Condition) bool { return new.ShouldOverwrite(old) })
}

// Reasons other than those explicitly called out here have the default priority of 0.
// These are given a negative priority so that they "lose" to the default of 0 and are overwritten.
// Take care to not modify this structure (Golang doesn't support a readonly map). We could use
// v2/tools/generator/internal/readonly/readonly_map.go but given
// Golang generics bugs for now we go with the simpler approach
var reasonPriority = map[string]int{
	ReasonReferenceNotFound.Name: -2,
	ReasonSecretNotFound.Name:    -2,
	ReasonConfigMapNotFound.Name: -2,
	// AzureResourceNotFound only comes up when ReconcilePolicy is skip. This conditions priority being less than
	// Reconciling allows skip -> reconcile to immediately update the condition to Reconciling rather than continuing to
	// report AzureResourceNotFound until the resource is created.
	ReasonAzureResourceNotFound.Name: -2,
	ReasonWaitingForOwner.Name:       -2,
	ReasonReconciling.Name:           -1,
}

// SetConditionReasonAware sets the provided Condition on the Conditioner. This is similar to SetCondition
// with one difference: SetConditionReasonAware understands common Reasons used by ASO and allows some of them to
// modify the standard Condition priority rules. This is primarily used to allow the Reconciling condition to overwrite
// Warning conditions raised by the operator that have been fixed. This is useful because sometimes getting a success or
// error from Azure can take a long time, and workflows like: submit -> warning -> fix warning -> call Azure -> wait -> success
// otherwise would continue reporting the Warning Condition until the final success step (possibly many minutes after the
// warning was resolved).
func SetConditionReasonAware(o Conditioner, new Condition) {
	shouldOverwrite := func(new Condition, old Condition) bool {
		if new.ShouldOverwrite(old) {
			return true
		}

		// If we normally wouldn't overwrite, check the reason of the old and new condition and compare their priorities
		oldPriority := reasonPriority[old.Reason] // Default is 0 if not mapped
		newPriority := reasonPriority[new.Reason] // Default is 0 if not mapped
		return newPriority > oldPriority          // Just > rather than >= here to prevent things overwriting themselves
	}

	setCondition(o, new, shouldOverwrite)
}

func setCondition(o Conditioner, new Condition, shouldOverwrite func(new Condition, old Condition) bool) {
	if o == nil {
		return
	}

	conditions := o.GetConditions()
	i, exists := conditions.FindIndexByType(new.Type)
	if exists {
		if !shouldOverwrite(new, conditions[i]) {
			// Nothing to do, the new condition is not supposed to overwrite
			return
		}
		conditions[i] = new
	} else {
		conditions = append(conditions, new)
	}

	// TODO: do we sort conditions here? CAPI does.

	o.SetConditions(conditions)
}

// GetCondition gets the Condition with the specified type from the provided Conditioner.
// Returns the Condition and true if a Condition with the specified type is found, or an empty Condition
// and false if not.
func GetCondition(o Conditioner, conditionType ConditionType) (Condition, bool) {
	if o == nil {
		return Condition{}, false
	}

	conditions := o.GetConditions()
	i, exists := conditions.FindIndexByType(conditionType)
	if exists {
		return conditions[i], true
	}

	return Condition{}, false
}
