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"
	O11yK8sNodes   UpgradeOperationTarget = "o11y-k8s-nodes"
	O11yTiDBNodes  UpgradeOperationTarget = "o11y-tidb-nodes"

	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"

	MaintenancePolicyModeLegacy             = "legacy"
	MaintenancePolicyModeClusterMaintenance = "cluster-maintenance"
)

// 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.
	//   - "delay": specifies a delay duration before executing this step, e.g., "10m" for 10 minutes.
	// +optional
	Options map[string]string `json:"options,omitempty"`

	// Phases records fine-grained sub-phase history within this step.
	// +optional
	Phases []StepPhase `json:"phases,omitempty"`
	// LastProgressTime is the step-level high-water mark for stuck detection.
	// +optional
	LastProgressTime *metav1.Time `json:"lastProgressTime,omitempty"`
}

// StepPhase records one visit to a fine-grained sub-phase within a step.
type StepPhase struct {
	Name             string       `json:"name,omitempty"`
	BeginTime        *metav1.Time `json:"beginTime,omitempty"`
	EndTime          *metav1.Time `json:"endTime,omitempty"`
	ProgressInfo     string       `json:"progressInfo,omitempty"`
	Done             int          `json:"done,omitempty"`
	Total            int          `json:"total,omitempty"`
	MaxDone          int          `json:"maxDone,omitempty"`
	LastProgressTime *metav1.Time `json:"lastProgressTime,omitempty"`
}

// ActivePhase returns the most recent sub-phase entry, or nil.
func (s *Step) ActivePhase() *StepPhase {
	if s == nil || len(s.Phases) == 0 {
		return nil
	}
	return &s.Phases[len(s.Phases)-1]
}

// ResetPhases clears sub-phase history when a new step begins.
func (s *Step) ResetPhases() {
	if s == nil {
		return
	}
	s.Phases = nil
	s.LastProgressTime = nil
}

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"`

	// MaintenancePolicyMode controls step-level maintenance coordination with DevOps.
	// +kubebuilder:default="legacy"
	// +kubebuilder:validation:Enum=legacy;cluster-maintenance
	// +kubebuilder:validation:Optional
	MaintenancePolicyMode string `json:"maintenancePolicyMode,omitempty"`
}

// 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}
		}
		uo.Status.CurrentStep.ResetPhases()
	} 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]
}

// CurrentStepSpecIndex returns the index of the current step in spec.steps.
func (uo *UpgradeOperation) CurrentStepSpecIndex() int {
	return uo.Status.FinishedSteps.Size()
}

// IsClusterMaintenanceMode reports whether cluster-level maintenance handshake is enabled.
func (uo *UpgradeOperation) IsClusterMaintenanceMode() bool {
	return uo.Spec.MaintenancePolicyMode == MaintenancePolicyModeClusterMaintenance
}

func (uo *UpgradeOperation) EffectiveMaintenancePolicyMode() string {
	if uo.Spec.MaintenancePolicyMode == "" {
		return MaintenancePolicyModeLegacy
	}
	return uo.Spec.MaintenancePolicyMode
}

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{})
}
