package kernelimageaction

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	kernelimageent "github.com/tidbcloud/devops/entity/kernelimage"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/byocimagesynccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/fidcli"
	"github.com/tidbcloud/devops/pkg/service/kernelimage/repo"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
)

const (
	outputBYOCImageSyncState opent.OutputName = "kernel_image_byoc_sync_state"
	byocWorkflowFailedCode                    = "BYOC_WORKFLOW_FAILED"
	byocWorkflowTimeoutCode                   = "BYOC_WORKFLOW_TIMEOUT"
	byocManifestInvalidCode                   = "BYOC_MANIFEST_INVALID"
	defaultBYOCMaxWait                        = 2 * time.Hour
)

var ActionSyncBYOCImage = opent.ActionMetadata{
	Name: "SyncBYOCImage",
	Type: opent.ActionTypeAuto,
}

type syncBYOCImage struct {
	actions.BaseAction
	client   byocimagesynccli.Client
	releases repo.Repo
	maxWait  time.Duration
	now      func() time.Time
}

type byocImageSyncState struct {
	ImagesDigest  string    `json:"images_digest"`
	WorkflowRunID int64     `json:"run_id"`
	WorkflowURL   string    `json:"run_url"`
	DispatchedAt  time.Time `json:"dispatched_at"`
}

func NewSyncBYOCImage(comps *actions.Components) actions.AutoAction {
	maxWait := defaultBYOCMaxWait
	if comps.KernelImageConf.BYOCSync.MaxWait != nil {
		maxWait = *comps.KernelImageConf.BYOCSync.MaxWait
	}
	return &syncBYOCImage{
		client:   comps.BYOCSyncCli,
		releases: repo.NewRepo(comps.DBProvider),
		maxWait:  maxWait,
		now:      time.Now,
	}
}

func (a *syncBYOCImage) Metadata() opent.ActionMetadata {
	return ActionSyncBYOCImage
}

func (a *syncBYOCImage) PreCheck(_ context.Context, instance *opent.Instance) error {
	correlationID, ok := instance.GetParam(opent.ParamKernelImageDeliveryCorrelationID)
	if !ok || strings.TrimSpace(correlationID) == "" {
		return fmt.Errorf("param %s is required", opent.ParamKernelImageDeliveryCorrelationID)
	}
	if _, err := uuid.Parse(correlationID); err != nil {
		return fmt.Errorf("param %s must be a UUID: %w", opent.ParamKernelImageDeliveryCorrelationID, err)
	}
	environment, ok := instance.GetParam(opent.ParamKernelImageLocalDevOpsEnv)
	if !ok {
		return fmt.Errorf("param %s is required", opent.ParamKernelImageLocalDevOpsEnv)
	}
	switch environment {
	case "dev", "prod":
		return nil
	default:
		return fmt.Errorf("param %s must be dev or prod", opent.ParamKernelImageLocalDevOpsEnv)
	}
}

func (a *syncBYOCImage) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	batchUID := instance.Params[opent.ParamKernelImageDeliveryCorrelationID]
	releases, err := a.releases.ListReleasesByDeliveryBatchUID(ctx, batchUID)
	if err != nil {
		return nil, fmt.Errorf("get kernel image delivery batch: %w", err)
	}
	if err := validateBatchOperation(releases, instance.ID); err != nil {
		return nil, err
	}
	images, imagesDigest, err := buildBYOCImageManifest(releases)
	if err != nil {
		message := fmt.Sprintf("invalid BYOC image manifest: %s", err)
		if _, markErr := a.releases.MarkBatchSyncFailed(
			ctx, batchUID, instance.ID, byocManifestInvalidCode, message,
		); markErr != nil {
			return nil, fmt.Errorf("mark invalid BYOC image manifest: %w", markErr)
		}
		return actions.FailedActionResult(errors.New(message)), nil
	}
	run, err := a.client.Dispatch(ctx, byocimagesynccli.DispatchRequest{
		Environment:   instance.Params[opent.ParamKernelImageLocalDevOpsEnv],
		Images:        images,
		CorrelationID: batchUID,
		RequestedAt:   time.Unix(instance.CreatedAt, 0).UTC(),
	})
	if err != nil {
		return nil, fmt.Errorf("dispatch BYOC image sync workflow: %w", err)
	}
	if run == nil || run.ID <= 0 {
		return nil, fmt.Errorf("dispatch BYOC image sync workflow: run is empty")
	}
	if _, err := a.releases.MarkBatchSyncing(ctx, batchUID, instance.ID); err != nil {
		return nil, fmt.Errorf("mark kernel image delivery batch syncing: %w", err)
	}
	dispatchedAt := run.CreatedAt.UTC()
	if dispatchedAt.IsZero() {
		dispatchedAt = a.now().UTC()
	}
	state := byocImageSyncState{
		ImagesDigest:  imagesDigest,
		WorkflowRunID: run.ID,
		WorkflowURL:   run.URL,
		DispatchedAt:  dispatchedAt,
	}
	encodedState, err := json.Marshal(state)
	if err != nil {
		return nil, fmt.Errorf("marshal BYOC image sync state: %w", err)
	}
	return actions.RunningActionResult(
		fmt.Sprintf("dispatched BYOC image sync workflow: %s", run.URL),
		map[opent.OutputName]string{outputBYOCImageSyncState: string(encodedState)},
	), nil
}

func (a *syncBYOCImage) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	state, err := byocImageSyncStateFromInstance(instance)
	if err != nil {
		return nil, err
	}
	run, err := a.client.GetRun(ctx, state.WorkflowRunID)
	if err != nil {
		return nil, err
	}
	if run == nil {
		return nil, fmt.Errorf("BYOC image sync workflow run is empty")
	}
	batchUID := instance.Params[opent.ParamKernelImageDeliveryCorrelationID]
	if run.Status != "completed" {
		if a.now().UTC().Sub(state.DispatchedAt) > a.maxWait {
			message := fmt.Sprintf("BYOC image sync workflow timed out after %s: %s", a.maxWait, state.WorkflowURL)
			return a.failBatch(ctx, instance, batchUID, byocWorkflowTimeoutCode, message)
		}
		return actions.RunningActionResult(
			fmt.Sprintf("waiting for BYOC image sync workflow: %s", state.WorkflowURL), nil,
		), nil
	}
	if run.Conclusion != "success" {
		message := fmt.Sprintf("BYOC image sync workflow concluded with %s: %s", run.Conclusion, state.WorkflowURL)
		return a.failBatch(ctx, instance, batchUID, byocWorkflowFailedCode, message)
	}
	completedAt := a.now().UTC()
	if _, err := a.releases.MarkBatchAvailable(ctx, batchUID, instance.ID, completedAt); err != nil {
		return nil, fmt.Errorf("mark kernel image delivery batch available: %w", err)
	}
	return actions.SucceededActionResult(nil), nil
}

func (a *syncBYOCImage) failBatch(
	ctx context.Context,
	instance *opent.Instance,
	batchUID string,
	errorCode string,
	message string,
) (*actions.ActionResult, error) {
	if _, err := a.releases.MarkBatchSyncFailed(ctx, batchUID, instance.ID, errorCode, message); err != nil {
		return nil, fmt.Errorf("mark kernel image BYOC sync failed: %w", err)
	}
	return actions.FailedActionResult(errors.New(message)), nil
}

func buildBYOCImageManifest(releases []*kernelimageent.Release) ([]byocimagesynccli.Image, string, error) {
	images := make([]byocimagesynccli.Image, 0, len(releases))
	seenSources := make(map[string]struct{}, len(releases))
	for _, release := range releases {
		if release.Status != kernelimageent.ReleaseStatusSyncing && release.Status != kernelimageent.ReleaseStatusSyncFailed {
			return nil, "", fmt.Errorf(
				"kernel image release %s is %s, expected SYNCING or SYNC_FAILED",
				release.ArtifactUID,
				release.Status,
			)
		}
		digest := strings.TrimSpace(release.SourceDigest)
		if digest == "" {
			return nil, "", fmt.Errorf("source digest is missing for kernel image release %s", release.ArtifactUID)
		}
		awsTargets := make([]string, 0, 1)
		for _, target := range release.DeliveryTargetImages {
			if fidcli.CloudProviderFromImageRef(target) == "AWS" {
				awsTargets = append(awsTargets, target)
			}
		}
		if len(awsTargets) != 1 {
			return nil, "", fmt.Errorf(
				"kernel image release %s has %d AWS delivery targets, expected one",
				release.ArtifactUID,
				len(awsTargets),
			)
		}
		source := awsTargets[0]
		if _, duplicate := seenSources[source]; duplicate {
			return nil, "", fmt.Errorf("duplicate BYOC source image %s", source)
		}
		seenSources[source] = struct{}{}
		images = append(images, byocimagesynccli.Image{Source: source, Digest: digest})
	}
	if len(images) == 0 {
		return nil, "", fmt.Errorf("BYOC image manifest is empty")
	}
	sort.Slice(images, func(i, j int) bool { return images[i].Source < images[j].Source })
	encoded, err := json.Marshal(images)
	if err != nil {
		return nil, "", fmt.Errorf("marshal BYOC image manifest: %w", err)
	}
	digest := sha256.Sum256(encoded)
	return images, "sha256:" + hex.EncodeToString(digest[:]), nil
}

func byocImageSyncStateFromInstance(instance *opent.Instance) (*byocImageSyncState, error) {
	encoded, ok := instance.GetOutput(outputBYOCImageSyncState)
	if !ok || strings.TrimSpace(encoded) == "" {
		return nil, fmt.Errorf("BYOC image sync state is missing")
	}
	state := &byocImageSyncState{}
	if err := json.Unmarshal([]byte(encoded), state); err != nil {
		return nil, fmt.Errorf("parse BYOC image sync state: %w", err)
	}
	if state.WorkflowRunID <= 0 || strings.TrimSpace(state.ImagesDigest) == "" || state.DispatchedAt.IsZero() {
		return nil, fmt.Errorf("BYOC image sync state is incomplete")
	}
	return state, nil
}
