package illegalboundary

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

	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"

	"github.com/mitchellh/mapstructure"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	clusteragent "github.com/tidbcloud/eks-provider/controllers/utils/cluster_agent"
	"github.com/tidbcloud/eks-provider/controllers/workflow/action"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

// mergeTFRegionKeys
type mergeTFRegionKeys struct {
	cli client.Client
}

type mergeTFRegionKeysInputs struct {
	DryRun      bool   `mapstructure:"dryRun" json:"dryRun"`
	Namespace   string `mapstructure:"namespace" json:"namespace" validate:"required"`
	ClusterName string `mapstructure:"clusterName" json:"clusterName" validate:"required"`
	InvalidKeys string `mapstructure:"invalidKeys" json:"invalidKeys" validate:"required"`
}

var (
	_ action.Action = &mergeTFRegionKeys{}
)

func NewMergeTFRegionKeys(cli client.Client) action.Action {
	return &mergeTFRegionKeys{cli: cli}
}

func (p *mergeTFRegionKeys) Name() cloudv1.WorkflowActionName {
	return "MergeTFRegionKeys"
}

func (p *mergeTFRegionKeys) PreCheck(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	inputs := &mergeTFRegionKeysInputs{}
	if err := mapstructure.Decode(step.Inputs.Object, inputs); err != nil {
		return fmt.Errorf("decode inputs: %w", err)
	}
	if err := validate.Struct(inputs); err != nil {
		return fmt.Errorf("validate inputs: %w", err)
	}

	return nil
}

// Handle mergeTFRegionKeys exec pd-ctl region --auto-fix to fix all invalid regions
func (p *mergeTFRegionKeys) Handle(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	stepStatus, _ := wf.StepStatus(ctx, step.Name)

	inputs := &mergeTFRegionKeysInputs{}
	_ = mapstructure.Decode(step.Inputs.Object, inputs)

	// If no invalid-keys from check step, or dry-run is set
	if inputs.DryRun || inputs.InvalidKeys == "" {
		// If dry-run set, or no invalid-keys from check-step
		// then skip merge step
		return nil
	}

	outputs := stepStatus.Outputs
	if outputs == nil {
		outputs = map[string]string{}
	}

	cluster := &cloudv1.Cluster{}
	if err := p.cli.Get(ctx, client.ObjectKey{Namespace: inputs.Namespace, Name: inputs.ClusterName}, cluster); err != nil {
		if apierrors.IsNotFound(err) {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("cluster %s not found", inputs.ClusterName))
		}
		return fmt.Errorf("get cluster: %w", err)
	}
	cli, err := clusteragent.NewClient(cluster)
	if err != nil {
		return fmt.Errorf("create cluster-agent client: %w", err)
	}

	resp, err := cli.ExecCommand(ctx, &apiv1.ExecuteCommandReq{
		ClusterId: inputs.ClusterName,
		Command: strings.Join([]string{
			PD_PATROL_CTL_BIN,
			fmt.Sprintf("-u%s", PD_HTTPS_URL),
			"region",
			"invalid-tiflash-key",
			"--auto-fix",
		}, " "),
	})
	if err != nil {
		return fmt.Errorf("failed execute PD-CTL region auto-fix: %w", err)
	}

	// The fix is async, and idempotent
	// It should be run multiple times until all regions are fixed -- no errors return.
	// Fill and update outputs during merging.
	err = parseTiFlashKeysMergeError(resp, outputs)
	_ = stepStatus.UpdateOutput(ctx, outputs)
	return err
}

func parseTiFlashKeysMergeError(resp *apiv1.ExecuteCommandResp, keyMapResult map[string]string) (err error) {

	err = parsePDCTLRespError(resp)
	if err != nil {
		return
	}

	out := &InvalidKeyOutput{}
	stdout := resp.StdoutMsg
	err = json.Unmarshal([]byte(stdout), out)
	if err != nil {
		return
	}

	nonRetriable := false
	var mergeFailures []string
	for _, patrolResult := range out.Results {
		// Record merge process
		keyMapResult[patrolResult.Key] = patrolResult.Status

		// Gather all merge failures
		if patrolResult.Status == PATROL_STATUS_MERGE_FAILED {
			mergeFailures = append(mergeFailures, fmt.Sprintf("[key: %s] [status: %s] [desc: %s]",
				patrolResult.Key, patrolResult.Status, patrolResult.Description))

			if strings.Contains(strings.ToLower(patrolResult.Description), "errregionabnormalpeer") {
				// If ErrRegionAbnormalPeer encountered, terminate this workflow.
				nonRetriable = true
			}
		}
	}

	if len(mergeFailures) > 0 {
		if nonRetriable {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("found abnormal tiflash peers. It should be fixed first"))
		}
		return fmt.Errorf("found key merge failure: %s", strings.Join(mergeFailures, "; "))

	} else {
		// If no merge request is found anymore,
		// but there are still [merge_failed] outputs exist, update all failed keys to "unknown" and return.
		// As the merge may have completed in the previous reconcile, but the output update failed.
		for k, status := range keyMapResult {
			if status == PATROL_STATUS_MERGE_FAILED {
				keyMapResult[k] = "unknown"
			}
		}
	}

	return
}
