package common

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	clustercontrollerv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/acscli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/clustercli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/upgradeoperation"
)

// FinishUpgrading unpauses network and cluster CRs' reconcile and updates UpgradeOperation's status.
func FinishUpgrading(ctx context.Context, k8sClient client.Client, uoWriter upgradeoperation.Writer, uo *v1alpha1.UpgradeOperation) error {
	if k8sClient != nil && NeedsMaintenanceHandshake(uo) {
		res, err := HandlePostStepMaintenance(ctx, k8sClient, uo, log.FromContext(ctx))
		if err != nil {
			return err
		}
		if res.Requeue || res.RequeueAfter > 0 {
			return ErrMaintenanceHandshakePending
		}
	}
	uo.FinishCurrentStep()
	return uoWriter.UpdateStatus(ctx, uo)
}

// FinishUpgradingResult wraps FinishUpgrading and converts handshake pending into requeue.
func FinishUpgradingResult(ctx context.Context, k8sClient client.Client, uoWriter upgradeoperation.Writer, uo *v1alpha1.UpgradeOperation) (ctrl.Result, error) {
	if err := FinishUpgrading(ctx, k8sClient, uoWriter, uo); err != nil {
		if errors.Is(err, ErrMaintenanceHandshakePending) {
			return ctrl.Result{RequeueAfter: maintenanceHandshakeInterval}, nil
		}
		return ctrl.Result{}, err
	}
	return ctrl.Result{}, nil
}

// IsCRReadyAndSynced checks if the crossplane CR is ready and synced.
func IsCRReadyAndSynced(conditions []commonv1.Condition) bool {
	ready, synced := false, false

	for _, c := range conditions {
		switch c.Type {
		case commonv1.TypeReady:
			ready = c.Status == corev1.ConditionTrue
		case commonv1.TypeSynced:
			synced = c.Status == corev1.ConditionTrue
		}
	}

	return ready && synced
}

// LogAndEmitEvent logs the event and emits it using the provided logger and eventRecorder.
// The event type determines how the event is logged.
func LogAndEmitEvent(logger logr.Logger, eventRecorder record.EventRecorder, obj runtime.Object, eventType, msg string, err error) {
	switch eventType {
	case NormalEvent:
		logger.Info(msg)

	case WarningEvent:
		logger.Error(err, msg)
	}
	eventRecorder.Event(obj, eventType, "", msg)
}

// PauseReconcile pauses the reconciliation of appbox.
func PauseReconcile(ctx context.Context, cli appbox.APPBoxReaderWriter, appBox *v1alpha1.APPBox) (updated bool, err error) {
	if appBox.Status.Phase != v1alpha1.APPBoxPhaseActive {
		return false, fmt.Errorf("appBox %s/%s is not active, phase: %s", appBox.Namespace, appBox.Name, appBox.Status.Phase)
	}
	if !utils.IsPaused(appBox.GetObjectMeta()) {
		appBox.Annotations[v1alpha1.AnnotationKeyReconciliationPaused] = "true"
		if err := cli.Update(ctx, appBox); err != nil {
			return false, fmt.Errorf("failed to pause appBox %s/%s: %w", appBox.Namespace, appBox.Name, err)
		}
		updated = true
	}
	return updated, nil
}

// ResumeReconcile resumes the reconciliation of appbox.
func ResumeReconcile(ctx context.Context, cli appbox.APPBoxReaderWriter, appBox *v1alpha1.APPBox) error {
	if utils.IsPaused(appBox.GetObjectMeta()) {
		appBox.Annotations[v1alpha1.AnnotationKeyReconciliationPaused] = "false"
		return cli.Update(ctx, appBox)
	}
	return nil
}

func GenerateIdempotencyKey(uo *v1alpha1.UpgradeOperation) string {
	return fmt.Sprintf("%s-%s-%s", uo.Name, strings.ReplaceAll(uo.Status.CurrentStep.Version, ".", ""), uo.Status.CurrentStep.Target)
}

func UpgradeDataNodes(ctx context.Context, cli clustercli.ClusterClient, uo *v1alpha1.UpgradeOperation, desiredVersion string) (msg string, done bool, err error) {
	var nodeType clustercontrollerv1.UpgradeNodesRequest_NodeType
	switch uo.Status.CurrentStep.Target {
	case v1alpha1.DMNodes:
		nodeType = clustercontrollerv1.UpgradeNodesRequest_DM
	case v1alpha1.TiDBNodes:
		nodeType = clustercontrollerv1.UpgradeNodesRequest_TIDB
	case v1alpha1.CDCNodes:
		nodeType = clustercontrollerv1.UpgradeNodesRequest_CDC
	}

	req := &clustercontrollerv1.UpgradeNodesRequest{
		IdempotencyKey: GenerateIdempotencyKey(uo),
		ClusterId:      uo.Status.CurrentStep.Cluster,
		NodeType:       nodeType,
		DesiredVersion: desiredVersion,
		Options:        uo.Status.CurrentStep.Options,
	}
	resp, err := cli.UpgradeNodes(ctx, req)
	if err != nil {
		return "", false, fmt.Errorf("failed to upgrade TiDB nodes: %w, req: %s", err, req)
	}

	if resp.EndTime != nil {
		done = true
	}
	ApplyUpgradeNodesProgress(uo, resp)
	msg = resp.Message
	return msg, done, nil
}

// ApplyUpgradeNodesProgress copies fine-grained progress from cluster-server response to regional UO current step.
func ApplyUpgradeNodesProgress(uo *v1alpha1.UpgradeOperation, resp *clustercontrollerv1.UpgradeNodesResponse) {
	if uo.Status.CurrentStep == nil {
		return
	}
	uo.Status.CurrentStep.Phases = stepPhasesFromClusterPB(resp.Phases)
	if resp.LastProgressTime != nil {
		t := resp.LastProgressTime.AsTime()
		uo.Status.CurrentStep.LastProgressTime = &metav1.Time{Time: t}
	}
}

func stepPhasesFromClusterPB(phases []*clustercontrollerv1.StepPhase) []v1alpha1.StepPhase {
	if len(phases) == 0 {
		return nil
	}
	out := make([]v1alpha1.StepPhase, 0, len(phases))
	for _, p := range phases {
		if p == nil {
			continue
		}
		sp := v1alpha1.StepPhase{
			Name:         p.Name,
			ProgressInfo: p.ProgressInfo,
			Done:         int(p.Done),
			Total:        int(p.Total),
			MaxDone:      int(p.MaxDone),
		}
		if p.BeginTime != nil {
			t := p.BeginTime.AsTime()
			sp.BeginTime = &metav1.Time{Time: t}
		}
		if p.EndTime != nil {
			t := p.EndTime.AsTime()
			sp.EndTime = &metav1.Time{Time: t}
		}
		if p.LastProgressTime != nil {
			t := p.LastProgressTime.AsTime()
			sp.LastProgressTime = &metav1.Time{Time: t}
		}
		out = append(out, sp)
	}
	return out
}

func DoUpgradeO11yNodes(ctx context.Context, k8sClient client.Client, acsCli acscli.Interface, uoWriter upgradeoperation.Writer, eventRecorder record.EventRecorder, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("UpgradeO11yNodes")

	step := uo.Status.CurrentStep
	tenantID := appBox.Labels[v1alpha1.TagTenantKey]
	projectID := appBox.Labels[v1alpha1.TagProjectKey]

	listResp, err := acsCli.ListNodeGroups(ctx, tenantID, projectID)
	if err != nil {
		LogAndEmitEvent(logger, eventRecorder, uo, WarningEvent, "failed to list o11y node groups", err)
		return ctrl.Result{}, err
	}

	var agentName string
	switch step.Target {
	case v1alpha1.O11yK8sNodes:
		var matched []string
		for _, ng := range listResp.Data.NodeGroups {
			if ng.Kind == string(v1alpha1.O11yK8sNodes) {
				matched = append(matched, ng.AgentName)
			}
		}
		if len(matched) != 1 {
			err := fmt.Errorf("expected exactly 1 o11y-k8s-nodes agent for tenant=%s/project=%s, got %d: %v", tenantID, projectID, len(matched), matched)
			LogAndEmitEvent(logger, eventRecorder, uo, WarningEvent, err.Error(), err)
			return ctrl.Result{}, err
		}
		agentName = matched[0]

	case v1alpha1.O11yTiDBNodes:
		agentName = step.Cluster
		if agentName == "" {
			err := fmt.Errorf("cluster name is empty in upgrade step for o11y-tidb-nodes")
			LogAndEmitEvent(logger, eventRecorder, uo, WarningEvent, err.Error(), err)
			return ctrl.Result{}, err
		}
	}

	// The target cluster may have been deleted before the upgrade was initiated;
	// if so, finish the step immediately.
	if step.Target == v1alpha1.O11yTiDBNodes {
		var found bool
		for _, ng := range listResp.Data.NodeGroups {
			if ng.Kind == string(v1alpha1.O11yTiDBNodes) && ng.AgentName == agentName {
				found = true
				break
			}
		}
		if !found {
			LogAndEmitEvent(logger, eventRecorder, uo, NormalEvent, fmt.Sprintf("target cluster %s not found, likely deleted before upgrade; finishing step", agentName), nil)
			return FinishUpgradingResult(ctx, k8sClient, uoWriter, uo)
		}
	}

	// Check upgrade status from ACS:
	//   1) version == target && ready  -> upgrade complete, finish step
	//   2) version == target && !ready -> version updated but NodeGroup still reconciling, requeue
	//   3) version != target           -> break and initiate upgrade via UpgradeNodeGroup below
	for _, ng := range listResp.Data.NodeGroups {
		if ng.AgentName == agentName && ng.Kind == string(step.Target) {
			if ng.Version == step.Version && ng.Ready {
				LogAndEmitEvent(logger, eventRecorder, uo, NormalEvent, fmt.Sprintf("o11y agent %s already at target version %s and ready", agentName, step.Version), nil)
				return FinishUpgradingResult(ctx, k8sClient, uoWriter, uo)
			}
			if ng.Version == step.Version && !ng.Ready {
				LogAndEmitEvent(logger, eventRecorder, uo, NormalEvent, fmt.Sprintf("o11y agent %s node group reconciling, waiting (version=%s, ready=%v)", agentName, ng.Version, ng.Ready), nil)
				return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
			}
			break
		}
	}

	resp, err := acsCli.UpgradeNodeGroup(ctx, &acscli.NodeGroupsUpgradeRequest{
		Kind:      string(step.Target),
		Version:   step.Version,
		AgentName: agentName,
	})
	if err != nil {
		LogAndEmitEvent(logger, eventRecorder, uo, WarningEvent, "failed to upgrade o11y nodes", err)
		return ctrl.Result{}, err
	}

	LogAndEmitEvent(logger, eventRecorder, uo, NormalEvent, fmt.Sprintf("o11y node upgrade accepted: %s %s → %s", resp.Data.AgentName, resp.Data.CurrentVersion, resp.Data.TargetVersion), nil)
	return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
}
