package regionalserver

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

	"go.opentelemetry.io/otel"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/encryptionchange"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
)

// externalIdPattern restricts ExternalId to a shell-safe character set so the
// single-quoted --encryption-context JSON cannot be broken out of. The only
// legitimate producer today is strconv.FormatUint(orgId, 10), which matches
// this pattern trivially.
var externalIdPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)

var tracer = otel.Tracer("encryption_change_executor")

type executorImpl struct {
	regionalCli regionalserver.Client
}

func NewExecutor(regionalCli regionalserver.Client) encryptionchange.Executor {
	return &executorImpl{regionalCli: regionalCli}
}

func (e *executorImpl) GenMeta(
	ctx context.Context,
	region *entity.Region,
	req *encryptionchange.GenMetaRequest,
) (*encryptionchange.GenMetaResponse, error) {
	_, span := tracer.Start(ctx, "GenMeta")
	defer span.End()

	cmd := fmt.Sprintf(
		"cse-ctl encryption gen-meta --keyspace-name %s --pd https://db-pd:2379 --mode %s --vendor %s --region %s --cmek-id %s",
		req.KeyspaceName, req.Mode, req.Vendor, req.KmsRegion, req.CmekId,
	)
	if req.Endpoint != "" {
		cmd += fmt.Sprintf(" --endpoint %s", req.Endpoint)
	}
	if req.ExternalId != "" {
		// Reject anything outside [A-Za-z0-9_-] before it reaches the shell.
		// cse-ctl runs under `sh -c <cmd>` downstream, so a rogue single quote
		// in ExternalId would break out of our quoting and allow injection.
		if !externalIdPattern.MatchString(req.ExternalId) {
			return nil, fmt.Errorf("invalid ExternalId %q: must match %s", req.ExternalId, externalIdPattern.String())
		}
		// json.Marshal rather than string-concat so any future special chars
		// in ExternalId are escaped correctly. Outer single-quotes keep the
		// shell from interpreting the double-quotes inside.
		ctxJSON, jsonErr := json.Marshal(map[string]string{"ExternalId": req.ExternalId})
		if jsonErr != nil {
			return nil, fmt.Errorf("marshal encryption context failed: %w", jsonErr)
		}
		cmd += fmt.Sprintf(" --encryption-context '%s'", string(ctxJSON))
	}

	resp, err := e.regionalCli.ExecuteClusterAgentCommand(ctx, region, req.ClusterId, req.SharedPoolId, cmd)
	if err != nil {
		return nil, fmt.Errorf("gen-meta command failed: %w", err)
	}

	var out genMetaOutput
	if parseErr := json.Unmarshal([]byte(resp.StdoutMsg), &out); parseErr != nil {
		return nil, fmt.Errorf("gen-meta parse stdout failed: %w, stdout=%s, stderr=%s", parseErr, resp.StdoutMsg, resp.StderrMsg)
	}

	return &encryptionchange.GenMetaResponse{
		FileId:    out.Generated.FileId,
		PdUpdated: out.PdUpdated,
		PdAfter: encryptionchange.PdAfterState{
			CurrentFileId: out.PdAfter.CurrentFileId,
			NextFileId:    out.PdAfter.NextFileId,
		},
		OwnerStoreId: out.OwnerStoreId,
		EncryptionConfig: encryptionchange.EncryptionConfigInfo{
			Enabled: out.EncryptionConfig.Enabled,
			CmekId:  out.EncryptionConfig.CmekId,
			Vendor:  out.EncryptionConfig.Vendor,
			Region:  out.EncryptionConfig.Region,
		},
	}, nil
}

func (e *executorImpl) SyncMeta(
	ctx context.Context,
	region *entity.Region,
	req *encryptionchange.SyncMetaRequest,
) (*encryptionchange.SyncMetaResponse, error) {
	_, span := tracer.Start(ctx, "SyncMeta")
	defer span.End()

	cmd := fmt.Sprintf(
		"cse-ctl encryption sync-meta --keyspace-name %s --pd https://db-pd:2379 --mode remote",
		req.KeyspaceName,
	)

	resp, err := e.regionalCli.ExecuteClusterAgentCommand(ctx, region, req.ClusterId, req.SharedPoolId, cmd)
	if err != nil {
		return nil, fmt.Errorf("sync-meta command failed: %w", err)
	}

	var out syncMetaOutput
	if parseErr := json.Unmarshal([]byte(resp.StdoutMsg), &out); parseErr != nil {
		return nil, fmt.Errorf("sync-meta parse stdout failed: %w, stdout=%s, stderr=%s", parseErr, resp.StdoutMsg, resp.StderrMsg)
	}

	return &encryptionchange.SyncMetaResponse{
		Started:       out.Started,
		Running:       out.Running,
		CurrentFileId: out.CurrentFileId,
		NextFileId:    out.NextFileId,
	}, nil
}

func (e *executorImpl) GetStatus(
	ctx context.Context,
	region *entity.Region,
	req *encryptionchange.StatusRequest,
) (*encryptionchange.StatusResponse, error) {
	_, span := tracer.Start(ctx, "GetStatus")
	defer span.End()

	cmd := fmt.Sprintf(
		"cse-ctl encryption status --keyspace-name %s --pd https://db-pd:2379",
		req.KeyspaceName,
	)

	resp, err := e.regionalCli.ExecuteClusterAgentCommand(ctx, region, req.ClusterId, req.SharedPoolId, cmd)
	if err != nil {
		return nil, fmt.Errorf("status command failed: %w", err)
	}

	var out statusOutput
	if parseErr := json.Unmarshal([]byte(resp.StdoutMsg), &out); parseErr != nil {
		return nil, fmt.Errorf("status parse stdout failed: %w, stdout=%s, stderr=%s", parseErr, resp.StdoutMsg, resp.StderrMsg)
	}

	return &encryptionchange.StatusResponse{
		KeyspaceName:      out.KeyspaceName,
		EncryptionEnabled: out.EncryptionEnabled,
		MetaState: encryptionchange.MetaStateInfo{
			CurrentFileId: out.MetaState.CurrentFileId,
			NextFileId:    out.MetaState.NextFileId,
		},
		EncryptionConfig: encryptionchange.EncryptionConfigInfo{
			Enabled: out.EncryptionConfig.Enabled,
			CmekId:  out.EncryptionConfig.CmekId,
			Vendor:  out.EncryptionConfig.Vendor,
			Region:  out.EncryptionConfig.Region,
		},
	}, nil
}

// cse-ctl JSON output structures

type fileIdPair struct {
	CurrentFileId uint64 `json:"current_file_id"`
	NextFileId    uint64 `json:"next_file_id"`
}

type generatedBlock struct {
	FileId uint64 `json:"file_id"`
}

type genMetaOutput struct {
	Generated        generatedBlock            `json:"generated"`
	PdUpdated        bool                      `json:"pd_updated"`
	PdAfter          fileIdPair                `json:"pd_after"`
	OwnerStoreId     uint64                    `json:"owner_store_id"`
	EncryptionConfig encryptionConfigJsonBlock `json:"encryption_config"`
}

type syncMetaOutput struct {
	Started       bool   `json:"started"`
	Running       bool   `json:"running"`
	CurrentFileId uint64 `json:"current_file_id"`
	NextFileId    uint64 `json:"next_file_id"`
}

type statusOutput struct {
	KeyspaceName      string                    `json:"keyspace_name"`
	EncryptionEnabled bool                      `json:"encryption_enabled"`
	MetaState         fileIdPair                `json:"meta_state"`
	EncryptionConfig  encryptionConfigJsonBlock `json:"encryption_config"`
}

type encryptionConfigJsonBlock struct {
	Enabled bool   `json:"enabled"`
	CmekId  string `json:"cmek_id"`
	Vendor  string `json:"vendor"`
	Region  string `json:"region"`
}
