package ngoperation

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	"github.com/tidbcloud/pkgv2/bizerr"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

const resourcePoolOperationVisibilityGracePeriod = 15 * time.Minute

var (
	ActionPauseNextGenResourcePool = opent.ActionMetadata{
		Name: "PauseNextGenResourcePool",
		Type: opent.ActionTypeAuto,
	}
	ActionResumeNextGenResourcePool = opent.ActionMetadata{
		Name: "ResumeNextGenResourcePool",
		Type: opent.ActionTypeAuto,
	}
)

type resourcePoolPauseResume struct {
	actions.BaseAction
	comps         *actions.Components
	metadata      opent.ActionMetadata
	operationType nextgenv1.ResourcePoolPauseResumeOperation_Type
	qpsChecker    resourcePoolQPSChecker
}

func NewPauseNextGenResourcePool(comps *actions.Components) actions.AutoAction {
	return &resourcePoolPauseResume{
		comps:         comps,
		metadata:      ActionPauseNextGenResourcePool,
		operationType: nextgenv1.ResourcePoolPauseResumeOperation_OPERATION_PAUSE,
		qpsChecker:    newResourcePoolQPSChecker(comps),
	}
}

func NewResumeNextGenResourcePool(comps *actions.Components) actions.AutoAction {
	return &resourcePoolPauseResume{
		comps:         comps,
		metadata:      ActionResumeNextGenResourcePool,
		operationType: nextgenv1.ResourcePoolPauseResumeOperation_OPERATION_RESUME,
	}
}

func (a *resourcePoolPauseResume) Metadata() opent.ActionMetadata {
	return a.metadata
}

func (a *resourcePoolPauseResume) PreCheck(ctx context.Context, instance *opent.Instance) error {
	resourcePoolID, err := resourcePoolIDFromInstance(instance)
	if err != nil {
		return err
	}
	if a.operationType == nextgenv1.ResourcePoolPauseResumeOperation_OPERATION_RESUME {
		return nil
	}
	if _, err := createManualBackupBeforePauseFromInstance(instance); err != nil {
		return err
	}

	resp, err := a.comps.CoreSvc.Mgmt.ResourcePool.PauseResourcePool(ctx, &nextgenv1.PauseResourcePoolRequest{
		ResourcePoolId: resourcePoolID,
		ValidateOnly:   true,
	})
	if err != nil {
		return fmt.Errorf("validate resource pool pause: %w", err)
	}
	if resp == nil {
		return fmt.Errorf("validate resource pool pause: empty response")
	}
	if err := pauseValidationError(resp.GetValidationResults()); err != nil {
		return err
	}
	force, err := forcePauseFromInstance(instance)
	if err != nil {
		return err
	}

	return a.validatePauseQPS(ctx, resourcePoolID, force)
}

func (a *resourcePoolPauseResume) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	resourcePoolID, err := resourcePoolIDFromInstance(instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}
	if instance.ID == 0 {
		return actions.FailedActionResult(fmt.Errorf("operation instance ID is required")), nil
	}

	operationID := strconv.FormatUint(instance.ID, 10)
	operation, err := a.getResourcePoolOperation(ctx, resourcePoolID, operationID)
	if err == nil {
		return a.operationResult(operation), nil
	}
	if !isResourcePoolOperationNotFound(err) {
		return nil, fmt.Errorf("get resource pool operation %s before start: %w", operationID, err)
	}
	requestMetadata := []string{"x-resource-id", strconv.FormatUint(resourcePoolID, 10)}
	if tenantID := strings.TrimSpace(instance.Params[opent.ParamTenantID]); tenantID != "" && tenantID != "0" {
		requestMetadata = append(requestMetadata, "x-org-id", tenantID)
	}
	ctx = metadata.AppendToOutgoingContext(ctx, requestMetadata...)

	if a.operationType == nextgenv1.ResourcePoolPauseResumeOperation_OPERATION_PAUSE {
		force, err := forcePauseFromInstance(instance)
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		if err := a.validatePauseQPS(ctx, resourcePoolID, force); err != nil {
			return actions.FailedActionResult(err), nil
		}
		createManualBackupBeforePause, err := createManualBackupBeforePauseFromInstance(instance)
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		return a.startPause(ctx, resourcePoolID, operationID, createManualBackupBeforePause)
	}
	return a.startResume(ctx, resourcePoolID, operationID)
}

func (a *resourcePoolPauseResume) startPause(
	ctx context.Context,
	resourcePoolID uint64,
	operationID string,
	createManualBackupBeforePause bool,
) (*actions.ActionResult, error) {
	resp, err := a.comps.CoreSvc.Mgmt.ResourcePool.PauseResourcePool(ctx, &nextgenv1.PauseResourcePoolRequest{
		ResourcePoolId:                resourcePoolID,
		OperationId:                   operationID,
		CreateManualBackupBeforePause: createManualBackupBeforePause,
	})
	if err != nil {
		return actions.FailedActionResult(fmt.Errorf("pause resource pool: %w", err)), nil
	}
	if resp == nil {
		return nil, fmt.Errorf("pause resource pool: empty response")
	}
	if err := pauseValidationError(resp.GetValidationResults()); err != nil {
		return actions.FailedActionResult(err), nil
	}
	if err := validateStartedOperationID(operationID, resp.GetOperationId()); err != nil {
		return actions.FailedActionResult(fmt.Errorf("pause resource pool: %w", err)), nil
	}

	return resourcePoolOperationRunningResult("pause", resourcePoolID, operationID), nil
}

func (a *resourcePoolPauseResume) startResume(
	ctx context.Context,
	resourcePoolID uint64,
	operationID string,
) (*actions.ActionResult, error) {
	resp, err := a.comps.CoreSvc.Mgmt.ResourcePool.ResumeResourcePool(ctx, &nextgenv1.ResumeResourcePoolRequest{
		ResourcePoolId: resourcePoolID,
		OperationId:    operationID,
	})
	if err != nil {
		return actions.FailedActionResult(fmt.Errorf("resume resource pool: %w", err)), nil
	}
	if resp == nil {
		return nil, fmt.Errorf("resume resource pool: empty response")
	}
	if err := validateStartedOperationID(operationID, resp.GetOperationId()); err != nil {
		return actions.FailedActionResult(fmt.Errorf("resume resource pool: %w", err)), nil
	}

	return resourcePoolOperationRunningResult("resume", resourcePoolID, operationID), nil
}

func (a *resourcePoolPauseResume) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	resourcePoolID, err := resourcePoolIDFromInstance(instance)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}
	if instance.ID == 0 {
		err := fmt.Errorf("operation instance ID is required")
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}

	operationID := strconv.FormatUint(instance.ID, 10)
	operation, err := a.getResourcePoolOperation(ctx, resourcePoolID, operationID)
	if isResourcePoolOperationNotFound(err) {
		if a.withinOperationVisibilityGrace(instance) {
			return actions.RunningActionResult(
				fmt.Sprintf("resource pool operation %s is not visible yet", operationID),
				nil,
			), nil
		}
		return actions.NewActionResult(
			opent.ActionStatusUnknown,
			fmt.Sprintf("resource pool operation %s is not found; manual confirmation is required", operationID),
			nil,
		), nil
	}
	if err != nil {
		return nil, fmt.Errorf("get resource pool operation %s: %w", operationID, err)
	}
	if operation == nil {
		return nil, fmt.Errorf("get resource pool operation %s: empty response", operationID)
	}
	return a.operationResult(operation), nil
}

func (a *resourcePoolPauseResume) withinOperationVisibilityGrace(instance *opent.Instance) bool {
	for _, action := range instance.Actions {
		if action == nil || action.Name != a.metadata.Name || action.StartedAt == nil {
			continue
		}
		return time.Since(time.Unix(*action.StartedAt, 0)) < resourcePoolOperationVisibilityGracePeriod
	}
	return false
}

func (a *resourcePoolPauseResume) getResourcePoolOperation(
	ctx context.Context,
	resourcePoolID uint64,
	operationID string,
) (*nextgenv1.ResourcePoolPauseResumeOperation, error) {
	return a.comps.CoreSvc.Mgmt.ResourcePool.GetResourcePoolPauseResumeOperation(
		ctx,
		&nextgenv1.GetResourcePoolPauseResumeOperationRequest{
			ResourcePoolId: resourcePoolID,
			OperationId:    operationID,
		},
	)
}

func isResourcePoolOperationNotFound(err error) bool {
	code := status.Code(err)
	return code == codes.NotFound || code == codes.Code(bizerr.ErrCodeRecordNotFound)
}

func (a *resourcePoolPauseResume) operationResult(
	operation *nextgenv1.ResourcePoolPauseResumeOperation,
) *actions.ActionResult {
	if operation == nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, "resource pool operation response is empty", nil)
	}
	if operation.GetType() != a.operationType {
		err := fmt.Errorf(
			"resource pool operation %s has type %s, expected %s",
			operation.GetOperationId(),
			operation.GetType().String(),
			a.operationType.String(),
		)
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil)
	}

	output := resourcePoolOperationOutput(operation)
	switch operation.GetStatus() {
	case nextgenv1.ResourcePoolPauseResumeOperation_RUNNING:
		return actions.RunningActionResult(output, map[opent.OutputName]string{
			opent.OutputOperationID: operation.GetOperationId(),
		})
	case nextgenv1.ResourcePoolPauseResumeOperation_SUCCEEDED:
		return actions.NewActionResult(opent.ActionStatusSucceeded, output, nil)
	case nextgenv1.ResourcePoolPauseResumeOperation_FAILED:
		return actions.NewActionResult(opent.ActionStatusFailed, output, nil)
	default:
		return actions.NewActionResult(opent.ActionStatusUnknown, output, nil)
	}
}

func (a *resourcePoolPauseResume) OnCancel(context.Context, *opent.Instance) (bool, error) {
	return false, nil
}

func resourcePoolIDFromInstance(instance *opent.Instance) (uint64, error) {
	resourcePoolIDRaw, ok := instance.GetParam(opent.ParamResourcePoolID)
	if !ok || strings.TrimSpace(resourcePoolIDRaw) == "" {
		return 0, fmt.Errorf("resource_pool_id is required")
	}

	resourcePoolID, err := strconv.ParseUint(resourcePoolIDRaw, 10, 64)
	if err != nil || resourcePoolID == 0 {
		return 0, fmt.Errorf("invalid resource_pool_id %q", resourcePoolIDRaw)
	}
	return resourcePoolID, nil
}

func forcePauseFromInstance(instance *opent.Instance) (bool, error) {
	forceRaw, ok := instance.GetParam(opent.ParamForce)
	if !ok || strings.TrimSpace(forceRaw) == "" {
		return false, nil
	}

	force, err := strconv.ParseBool(forceRaw)
	if err != nil {
		return false, fmt.Errorf("invalid force %q", forceRaw)
	}
	return force, nil
}

func createManualBackupBeforePauseFromInstance(instance *opent.Instance) (bool, error) {
	raw, ok := instance.GetParam(opent.ParamCreateManualBackupBeforePause)
	if !ok || strings.TrimSpace(raw) == "" {
		return false, nil
	}

	value, err := strconv.ParseBool(raw)
	if err != nil {
		return false, fmt.Errorf("invalid create_manual_backup_before_pause %q", raw)
	}
	return value, nil
}

func (a *resourcePoolPauseResume) validatePauseQPS(
	ctx context.Context,
	resourcePoolID uint64,
	force bool,
) error {
	if force {
		return nil
	}

	qps, err := a.qpsChecker(ctx, resourcePoolID)
	if err != nil {
		return fmt.Errorf("resource pool QPS precheck failed: %w", err)
	}
	if qps > resourcePoolPauseQPSThreshold {
		return fmt.Errorf(
			"QPS_THRESHOLD_EXCEEDED: average aggregate QPS %.2f is greater than %.2f",
			qps,
			resourcePoolPauseQPSThreshold,
		)
	}
	return nil
}

func pauseValidationError(results []*nextgenv1.ResourcePoolPauseValidationResult) error {
	if len(results) == 0 {
		return nil
	}

	blockers := make([]string, 0, len(results))
	for _, result := range results {
		if result == nil {
			blockers = append(blockers, "UNKNOWN: empty validation result")
			continue
		}
		code := strings.TrimSpace(result.GetCheckCode())
		if code == "" {
			code = "UNKNOWN"
		}
		detail := strings.TrimSpace(result.GetDetail())
		if detail == "" {
			blockers = append(blockers, code)
			continue
		}
		blockers = append(blockers, fmt.Sprintf("%s: %s", code, detail))
	}

	return fmt.Errorf("resource pool pause is blocked: %s", strings.Join(blockers, "; "))
}

func validateStartedOperationID(requested, returned string) error {
	if returned == "" {
		return fmt.Errorf("operation ID is missing from response")
	}
	if returned != requested {
		return fmt.Errorf("operation ID mismatch: requested %q, got %q", requested, returned)
	}
	return nil
}

func resourcePoolOperationRunningResult(action string, resourcePoolID uint64, operationID string) *actions.ActionResult {
	return actions.RunningActionResult(
		fmt.Sprintf("started resource pool %s operation %s for pool %d", action, operationID, resourcePoolID),
		map[opent.OutputName]string{opent.OutputOperationID: operationID},
	)
}

func resourcePoolOperationOutput(operation *nextgenv1.ResourcePoolPauseResumeOperation) string {
	output := fmt.Sprintf(
		"resource pool operation %s is %s",
		operation.GetOperationId(),
		operation.GetStatus().String(),
	)
	if summary := strings.TrimSpace(operation.GetResultSummary()); summary != "" {
		output += ": " + summary
	}
	return output
}
