package v1alpha1

import (
	"context"
	"fmt"
	"reflect"
	"strings"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type (
	UpgradeOperationTarget string
	UpgradeOperationPhase  string
)

const (
	ControlPlane   UpgradeOperationTarget = "controlplane"
	AdminNodes     UpgradeOperationTarget = "admin-nodes"
	TiDBNodes      UpgradeOperationTarget = "tidb-nodes"
	CDCNodes       UpgradeOperationTarget = "cdc-nodes"
	DMNodes        UpgradeOperationTarget = "dm-nodes"
	EmptyNodePools UpgradeOperationTarget = "empty-nodepools"
	Addons         UpgradeOperationTarget = "addons"

	UpgradeOperationPhaseNone      UpgradeOperationPhase = ""
	UpgradeOperationPhasePending   UpgradeOperationPhase = "Pending"
	UpgradeOperationPhaseRunning   UpgradeOperationPhase = "Running"
	UpgradeOperationPhaseCompleted UpgradeOperationPhase = "Completed"
	UpgradeOperationPhaseFailed    UpgradeOperationPhase = "Failed"

	defaultSize = 3

	UpgradeOperationScheduleLayout = "2006-01-02 15:04:05 -0700 MST"
	UpgradeOperationFinalizer      = "upgradeoperation.dedicated.regionalserver.tidbcloud.com/finalizer"
)

// Step defines a single upgrade step, i.e. upgrade <Target> to <Version> for <Cluster>.
type Step struct {
	// +required
	Target UpgradeOperationTarget `json:"target"`

	// +required
	Version string `json:"version"`

	// +optional
	Cluster string `json:"cluster,omitempty"`

	// +optional
	BeginTime *metav1.Time `json:"beginTime,omitempty"`

	// +optional
	EndTime *metav1.Time `json:"endTime,omitempty"`

	// +optional
	Duration string `json:"duration,omitempty"`

	// Options is a set of key-value pairs that can be used to specify additional options.
	//
	// The following options are supported:
	//   - "migrate-managed-nodegroup": if set to "true", will migrate managed node groups to self-managed node groups.
	//   - "scale-out-extra-tidb": if set to "true", an extra TiDB instance will be scaled out when upgrading.
	// +optional
	Options map[string]string `json:"options,omitempty"`
}

func (s *Step) Equals(other Step) bool {
	return s.Target == other.Target &&
		s.Version == other.Version &&
		s.Cluster == other.Cluster &&
		reflect.DeepEqual(s.Options, other.Options)
}

func (s *Step) String() string {
	return fmt.Sprintf("(%s, %s, %s)", s.Target, s.Version, s.Cluster)
}

// Steps is a FIFO queue of upgrade steps.
type Steps []Step

func NewSteps() Steps {
	return make([]Step, 0, defaultSize)
}

// Push adds a step to the end of the queue.
func (s *Steps) Push(step Step) {
	*s = append(*s, step)
}

// Pop removes a step from the front of the queue.
func (s *Steps) Pop() *Step {
	if s.IsEmpty() {
		return nil
	}
	step := (*s)[0]
	*s = (*s)[1:]
	return &step
}

func (s *Steps) IsEmpty() bool {
	return len(*s) == 0
}

func (s *Steps) Size() int {
	return len(*s)
}

func (s *Steps) Equals(another Steps) bool {
	if s.Size() != another.Size() {
		return false
	}

	for i, step := range *s {
		if !step.Equals(another[i]) {
			return false
		}
	}

	return true
}

func (s *Steps) String() string {
	strs := make([]string, 0, s.Size())
	for _, step := range *s {
		strs = append(strs, step.String())
	}
	return strings.Join(strs, ",")
}

// UpgradeOperationSpec defines the desired state of UpgradeOperation
type UpgradeOperationSpec struct {
	// Schedule specifies when the upgrade should be performed.
	// The UpgradeOperationScheduleLayout should be `2006-01-02 15:04:05 -0700 MST`.
	// +kubebuilder:validation:Required
	Schedule string `json:"schedule"`

	// Duration is the desired duration of the upgrade operation, it's used for alerting.
	// If unset, this defaults to 2 hours.
	// Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
	// +kubebuilder:default="2h"
	// +kubebuilder:validation:Optional
	Duration *metav1.Duration `json:"duration,omitempty"`

	// Paused indicates if the upgrade operation is paused.
	// +kubebuilder:default=false
	// +kubebuilder:validation:Optional
	Paused bool `json:"paused,omitempty"`

	// TTL is the time to live for Completed/Failed UpgradeOperation CRs.
	// Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
	// If unset, this defaults to 8760 hours (1 year).
	// +kubebuilder:default="8760h"
	// +kubebuilder:validation:Optional
	TTL *metav1.Duration `json:"ttl,omitempty"`

	// AppBoxName is the name of the AppBox CR.
	// +kubebuilder:validation:Required
	AppBoxName string `json:"appBoxName"`

	// Steps is a list of upgrade steps to be performed.
	// +kubebuilder:validation:Required
	Steps Steps `json:"steps"`
}

// UpgradeOperationStatus defines the observed state of UpgradeOperation
type UpgradeOperationStatus struct {
	// Phase describes the current phase of the upgrade operation.
	Phase UpgradeOperationPhase `json:"phase,omitempty"`

	// Reason describes the reason for the current phase.
	Reason string `json:"reason,omitempty"`

	BeginTime *metav1.Time `json:"beginTime,omitempty"`
	EndTime   *metav1.Time `json:"endTime,omitempty"`
	Duration  string       `json:"duration,omitempty"`

	CurrentStep   *Step `json:"currentStep,omitempty"`
	NextSteps     Steps `json:"nextSteps,omitempty"`
	FinishedSteps Steps `json:"finishedSteps,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// +kubebuilder:resource:shortName=ruo
// +kubebuilder:printcolumn:name="AppBoxName",type=string,JSONPath=`.spec.appBoxName`
// +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=`.spec.schedule`
// +kubebuilder:printcolumn:name="Paused",type=boolean,JSONPath=`.spec.paused`
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"

// UpgradeOperation is the Schema for the upgradeoperations API
type UpgradeOperation struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   UpgradeOperationSpec   `json:"spec,omitempty"`
	Status UpgradeOperationStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// UpgradeOperationList contains a list of UpgradeOperation
type UpgradeOperationList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []UpgradeOperation `json:"items"`
}

func (uo *UpgradeOperation) UpdateCurrentStep(now time.Time) {
	if step := uo.Status.NextSteps.Pop(); step != nil {
		uo.Status.CurrentStep = step
		uo.Status.Reason = fmt.Sprintf("Begin to upgrade %s to %s", step.Target, step.Version)
		if uo.Status.BeginTime == nil {
			uo.Status.BeginTime = &metav1.Time{Time: now}
		}
		if uo.Status.CurrentStep.BeginTime == nil {
			uo.Status.CurrentStep.BeginTime = &metav1.Time{Time: now}
		}
	} else {
		uo.Status.Phase = UpgradeOperationPhaseCompleted
		uo.Status.Reason = "All steps are completed"
		if uo.Status.EndTime == nil {
			uo.Status.EndTime = &metav1.Time{Time: now}
			if uo.Status.BeginTime != nil {
				uo.Status.Duration = uo.Status.EndTime.Sub(uo.Status.BeginTime.Time).String()
			}
		}
	}
}

func (uo *UpgradeOperation) FinishCurrentStep() {
	uo.Status.CurrentStep.EndTime = &metav1.Time{Time: time.Now()}
	uo.Status.CurrentStep.Duration = uo.Status.CurrentStep.EndTime.Sub(uo.Status.CurrentStep.BeginTime.Time).String()
	uo.Status.FinishedSteps.Push(*uo.Status.CurrentStep)
	uo.Status.Reason = fmt.Sprintf("Upgraded %s to %s", uo.Status.CurrentStep.Target, uo.Status.CurrentStep.Version)
	uo.Status.CurrentStep = nil
}

func (uo *UpgradeOperation) GetCurrentStepOption(key string) string {
	if uo.Status.CurrentStep == nil {
		return ""
	}
	return uo.Status.CurrentStep.Options[key]
}

func (uo *UpgradeOperation) UpdateReason(ctx context.Context, cli client.Client, reason string) error {
	if uo.Status.Reason != reason {
		uo.Status.Reason = reason
		return cli.Status().Update(ctx, uo)
	}
	return nil
}

func (uo *UpgradeOperation) Fail(ctx context.Context, cli client.Client, reason string) error {
	uo.Status.Phase = UpgradeOperationPhaseFailed
	if uo.Status.EndTime == nil {
		uo.Status.EndTime = &metav1.Time{Time: time.Now()}
		if uo.Status.BeginTime != nil {
			uo.Status.Duration = uo.Status.EndTime.Sub(uo.Status.BeginTime.Time).String()
		}
	}
	return uo.UpdateReason(ctx, cli, reason)
}

func init() {
	SchemeBuilder.Register(&UpgradeOperation{}, &UpgradeOperationList{})
}
