package common

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/go-logr/logr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
)

const (
	AnnotationRequestStepPrefix  = "maintenance.pingcap.com/request-step-"
	AnnotationReleaseStepPrefix  = "maintenance.pingcap.com/release-step-"
	maintenanceDoneResult        = "done"
	maintenanceHandshakeInterval = 15 * time.Second

	// ErrMaintenanceHandshakePending indicates the upgrader should requeue while waiting for DevOps.
	ErrMaintenanceHandshakePendingStr = "maintenance handshake pending"
)

var ErrMaintenanceHandshakePending = fmt.Errorf("%s", ErrMaintenanceHandshakePendingStr)

type maintenanceAnnotation struct {
	Spec struct {
		ClusterID string `json:"cluster_id"`
		Time      string `json:"time"`
	} `json:"spec"`
	Status struct {
		Result string `json:"result"`
		Time   string `json:"time"`
	} `json:"status"`
}

func NeedsMaintenanceHandshake(uo *v1alpha1.UpgradeOperation) bool {
	if !uo.IsClusterMaintenanceMode() || uo.Status.CurrentStep == nil {
		return false
	}
	return uo.Status.CurrentStep.Target == v1alpha1.TiDBNodes
}

func RequestStepAnnotationKey(stepIndex int) string {
	return AnnotationRequestStepPrefix + strconv.Itoa(stepIndex)
}

func ReleaseStepAnnotationKey(stepIndex int) string {
	return AnnotationReleaseStepPrefix + strconv.Itoa(stepIndex)
}

func parseMaintenanceAnnotation(raw string) (*maintenanceAnnotation, error) {
	if raw == "" {
		return &maintenanceAnnotation{}, nil
	}
	var ann maintenanceAnnotation
	if err := json.Unmarshal([]byte(raw), &ann); err != nil {
		return nil, fmt.Errorf("failed to parse maintenance annotation: %w", err)
	}
	return &ann, nil
}

func maintenanceAnnotationDone(raw string) (bool, error) {
	ann, err := parseMaintenanceAnnotation(raw)
	if err != nil {
		return false, err
	}
	return ann.Spec.ClusterID != "" && ann.Status.Result == maintenanceDoneResult, nil
}

func maintenanceAnnotationHasSpec(raw string) bool {
	ann, err := parseMaintenanceAnnotation(raw)
	if err != nil {
		return false
	}
	return ann.Spec.ClusterID != ""
}

func marshalMaintenanceAnnotationSpec(clusterID string) (string, error) {
	ann := maintenanceAnnotation{}
	ann.Spec.ClusterID = clusterID
	ann.Spec.Time = strconv.FormatInt(time.Now().Unix(), 10)
	data, err := json.Marshal(ann)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func ensureAnnotationMap(uo *v1alpha1.UpgradeOperation) {
	if uo.Annotations == nil {
		uo.Annotations = make(map[string]string)
	}
}

// HandlePreStepMaintenance gates step execution until DevOps acknowledges the request annotation.
func HandlePreStepMaintenance(ctx context.Context, cli client.Client, uo *v1alpha1.UpgradeOperation, logger logr.Logger) (ctrl.Result, error) {
	return handleStepMaintenance(ctx, cli, uo, logger, RequestStepAnnotationKey, "request")
}

// HandlePostStepMaintenance gates step completion until DevOps acknowledges the release annotation.
func HandlePostStepMaintenance(ctx context.Context, cli client.Client, uo *v1alpha1.UpgradeOperation, logger logr.Logger) (ctrl.Result, error) {
	return handleStepMaintenance(ctx, cli, uo, logger, ReleaseStepAnnotationKey, "release")
}

func handleStepMaintenance(
	ctx context.Context,
	cli client.Client,
	uo *v1alpha1.UpgradeOperation,
	logger logr.Logger,
	annotationKey func(int) string,
	kind string,
) (ctrl.Result, error) {
	if !NeedsMaintenanceHandshake(uo) {
		return ctrl.Result{}, nil
	}

	key := annotationKey(uo.CurrentStepSpecIndex())
	raw := uo.Annotations[key]

	done, err := maintenanceAnnotationDone(raw)
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to parse %s maintenance annotation %q: %w", kind, key, err)
	}
	if done {
		return ctrl.Result{}, nil
	}

	if !maintenanceAnnotationHasSpec(raw) {
		value, err := marshalMaintenanceAnnotationSpec(uo.Status.CurrentStep.Cluster)
		if err != nil {
			return ctrl.Result{}, err
		}
		ensureAnnotationMap(uo)
		uo.Annotations[key] = value
		if err := cli.Update(ctx, uo); err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to write %s maintenance annotation %q: %w", kind, key, err)
		}
		logger.Info("wrote "+kind+" maintenance annotation", "key", key, "cluster", uo.Status.CurrentStep.Cluster)
	}

	logger.Info("waiting for maintenance handshake", "key", key, "cluster", uo.Status.CurrentStep.Cluster)
	return ctrl.Result{RequeueAfter: maintenanceHandshakeInterval}, nil
}

// IsMaintenanceAnnotationKey reports whether the key belongs to maintenance handshake protocol.
func IsMaintenanceAnnotationKey(key string) bool {
	return strings.HasPrefix(key, AnnotationRequestStepPrefix) || strings.HasPrefix(key, AnnotationReleaseStepPrefix)
}

// ParseMaintenanceAnnotation exports parsing for metrics and tests.
func ParseMaintenanceAnnotation(raw string) (clusterID string, done bool, err error) {
	ann, err := parseMaintenanceAnnotation(raw)
	if err != nil {
		return "", false, err
	}
	return ann.Spec.ClusterID, ann.Status.Result == maintenanceDoneResult, nil
}

// MaintenanceAnnotationKind returns "request" or "release" for a maintenance annotation key.
func MaintenanceAnnotationKind(key string) string {
	switch {
	case strings.HasPrefix(key, AnnotationRequestStepPrefix):
		return "request"
	case strings.HasPrefix(key, AnnotationReleaseStepPrefix):
		return "release"
	default:
		return ""
	}
}

// MaintenanceAnnotationStepIndex extracts step index from a maintenance annotation key.
func MaintenanceAnnotationStepIndex(key string) (int, bool) {
	var idxStr string
	switch {
	case strings.HasPrefix(key, AnnotationRequestStepPrefix):
		idxStr = strings.TrimPrefix(key, AnnotationRequestStepPrefix)
	case strings.HasPrefix(key, AnnotationReleaseStepPrefix):
		idxStr = strings.TrimPrefix(key, AnnotationReleaseStepPrefix)
	default:
		return 0, false
	}
	idx, err := strconv.Atoi(idxStr)
	if err != nil {
		return 0, false
	}
	return idx, true
}
