package kernelimageaction

import (
	"context"
	"fmt"
	"strconv"
	"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/fidcli"
	"github.com/tidbcloud/devops/pkg/service/kernelimage/repo"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
)

const (
	outputFIDIssueNumber      opent.OutputName = "kernel_image_fid_issue_number"
	outputFIDRequestComment   opent.OutputName = "kernel_image_fid_request_comment_id"
	outputFIDRequestURL       opent.OutputName = "kernel_image_fid_request_url"
	outputFIDRequestCreated   opent.OutputName = "kernel_image_fid_request_created_at"
	outputFIDResultURL        opent.OutputName = "kernel_image_fid_result_url"
	fidFailedErrorCode                         = "FID_FAILED"
	fidResultInvalidErrorCode                  = "FID_RESULT_INVALID"
)

var ActionDeliverKernelImage = opent.ActionMetadata{
	Name: "DeliverKernelImage",
	Type: opent.ActionTypeAuto,
}

type deliverKernelImage struct {
	actions.BaseAction
	fid      fidcli.Client
	releases repo.Repo
	now      func() time.Time
}

func NewDeliverKernelImage(comps *actions.Components) actions.AutoAction {
	return &deliverKernelImage{
		fid:      comps.FIDCli,
		releases: repo.NewRepo(comps.DBProvider),
		now:      time.Now,
	}
}

func (a *deliverKernelImage) Metadata() opent.ActionMetadata {
	return ActionDeliverKernelImage
}

func (a *deliverKernelImage) PreCheck(_ context.Context, instance *opent.Instance) error {
	required := []opent.ParamName{
		opent.ParamKernelImageArtifactUID,
		opent.ParamKernelImageLocalDevOpsEnv,
		opent.ParamKernelImageSourceRepository,
		opent.ParamKernelImageSourceTag,
		opent.ParamKernelImageDeliveryCorrelationID,
	}
	for _, name := range required {
		value, ok := instance.GetParam(name)
		if !ok || strings.TrimSpace(value) == "" {
			return fmt.Errorf("param %s is required", name)
		}
	}

	correlationID, _ := instance.GetParam(opent.ParamKernelImageDeliveryCorrelationID)
	if _, err := uuid.Parse(correlationID); err != nil {
		return fmt.Errorf("param %s must be a UUID: %w", opent.ParamKernelImageDeliveryCorrelationID, err)
	}
	return nil
}

func (a *deliverKernelImage) 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 := make([]string, 0, len(releases))
	for _, release := range releases {
		if release.SourceDigest == "" {
			return nil, fmt.Errorf("source digest is not resolved for kernel image release %s", release.ArtifactUID)
		}
		images = append(images, release.SourceRepository+":"+release.SourceTag)
	}
	submission, err := a.fid.Submit(ctx, fidcli.SubmitRequest{
		Environment:   instance.Params[opent.ParamKernelImageLocalDevOpsEnv],
		Images:        images,
		CorrelationID: batchUID,
		RequestedAt:   time.Unix(instance.CreatedAt, 0).UTC(),
	})
	if err != nil {
		return nil, fmt.Errorf("submit kernel image to FID: %w", err)
	}
	if _, err := a.releases.MarkBatchSyncing(ctx, batchUID, instance.ID); err != nil {
		return nil, fmt.Errorf("mark kernel image delivery batch syncing: %w", err)
	}

	outputs := map[opent.OutputName]string{
		outputFIDIssueNumber:    strconv.Itoa(submission.IssueNumber),
		outputFIDRequestComment: strconv.FormatInt(submission.CommentID, 10),
		outputFIDRequestURL:     submission.CommentURL,
		outputFIDRequestCreated: submission.CreatedAt.UTC().Format(time.RFC3339Nano),
	}
	return actions.RunningActionResult(
		fmt.Sprintf("submitted FID request: %s", submission.CommentURL),
		outputs,
	), nil
}

func (a *deliverKernelImage) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	submission, err := submissionFromInstance(instance)
	if err != nil {
		return nil, err
	}
	deliveryResult, err := a.fid.GetResult(
		ctx,
		submission,
		instance.Params[opent.ParamKernelImageDeliveryCorrelationID],
	)
	if err != nil {
		return nil, fmt.Errorf("get FID result: %w", err)
	}

	batchUID := instance.Params[opent.ParamKernelImageDeliveryCorrelationID]
	outputs := map[opent.OutputName]string{}
	if deliveryResult.ResultCommentURL != "" {
		outputs[outputFIDResultURL] = deliveryResult.ResultCommentURL
	}
	switch deliveryResult.Status {
	case fidcli.DeliveryStatusRunning:
		return actions.RunningActionResult("waiting for FID to complete", nil), nil
	case fidcli.DeliveryStatusSucceeded:
		completedAt := a.now().UTC()
		releases, err := a.releases.ListReleasesByDeliveryBatchUID(ctx, batchUID)
		if err != nil {
			return nil, fmt.Errorf("get kernel image delivery batch: %w", err)
		}
		updates, err := buildDeliveryResultUpdates(releases, deliveryResult, completedAt)
		if err != nil {
			message := fmt.Sprintf("invalid FID result: %s", err)
			if _, markErr := a.releases.MarkBatchSyncFailed(
				ctx,
				batchUID,
				instance.ID,
				fidResultInvalidErrorCode,
				message,
			); markErr != nil {
				return nil, fmt.Errorf("mark invalid FID result: %w", markErr)
			}
			return actions.NewActionResult(opent.ActionStatusFailed, message, outputs), nil
		}
		if instanceHasAction(instance, ActionSyncBYOCImage.Name) {
			if _, err := a.releases.MarkBatchFIDDelivered(
				ctx, batchUID, instance.ID, completedAt, updates,
			); err != nil {
				return nil, fmt.Errorf("save FID delivery result: %w", err)
			}
		} else if _, err := a.releases.MarkBatchSyncSucceeded(
			ctx, batchUID, instance.ID, completedAt, updates,
		); err != nil {
			return nil, fmt.Errorf("mark kernel image delivery batch available: %w", err)
		}
		return actions.NewActionResult(
			opent.ActionStatusSucceeded,
			fmt.Sprintf("FID completed successfully: %s", deliveryResult.ResultCommentURL),
			outputs,
		), nil
	case fidcli.DeliveryStatusFailed:
		message := fmt.Sprintf("FID failed: %s", deliveryResult.ResultCommentURL)
		if _, err := a.releases.MarkBatchSyncFailed(ctx, batchUID, instance.ID, fidFailedErrorCode, message); err != nil {
			return nil, fmt.Errorf("mark kernel image delivery batch sync failed: %w", err)
		}
		return actions.NewActionResult(opent.ActionStatusFailed, message, outputs), nil
	default:
		return nil, fmt.Errorf("unsupported FID delivery status %q", deliveryResult.Status)
	}
}

func instanceHasAction(instance *opent.Instance, name opent.ActionName) bool {
	for _, action := range instance.Actions {
		if action != nil && action.Name == name {
			return true
		}
	}
	return false
}

func validateBatchOperation(releases []*kernelimageent.Release, instanceID uint64) error {
	if len(releases) == 0 {
		return fmt.Errorf("kernel image delivery batch is empty")
	}
	batchUID := releases[0].DeliveryBatchUID
	for _, release := range releases {
		if release.DeliveryBatchUID != batchUID {
			return fmt.Errorf("kernel image release %s belongs to another delivery batch", release.ArtifactUID)
		}
		if release.DeliveryOperationInstanceID == nil || *release.DeliveryOperationInstanceID != instanceID {
			return fmt.Errorf(
				"kernel image release %s is not linked to delivery operation %d",
				release.ArtifactUID,
				instanceID,
			)
		}
	}
	return nil
}

func buildDeliveryResultUpdates(
	releases []*kernelimageent.Release,
	deliveryResult *fidcli.DeliveryResult,
	checkedAt time.Time,
) ([]repo.DeliveryResultUpdate, error) {
	if deliveryResult == nil {
		return nil, fmt.Errorf("result is empty")
	}
	expectedImages := make(map[string]struct{}, len(releases))
	releaseByComponentTag := make(map[string]*kernelimageent.Release, len(releases))
	targetsByArtifact := make(map[string][]string, len(releases))
	for _, release := range releases {
		imageRef := release.SourceRepository + ":" + release.SourceTag
		expectedImages[imageRef] = struct{}{}
		componentTag, err := imageComponentTag(imageRef)
		if err != nil {
			return nil, err
		}
		if _, exists := releaseByComponentTag[componentTag]; exists {
			return nil, fmt.Errorf("source image key %s is ambiguous", componentTag)
		}
		releaseByComponentTag[componentTag] = release
		targetsByArtifact[release.ArtifactUID] = []string{}
	}
	if len(deliveryResult.Images) != len(expectedImages) {
		return nil, fmt.Errorf("expected %d source images, got %d", len(expectedImages), len(deliveryResult.Images))
	}
	seenImages := make(map[string]struct{}, len(deliveryResult.Images))
	for _, imageRef := range deliveryResult.Images {
		if _, ok := expectedImages[imageRef]; !ok {
			return nil, fmt.Errorf("unexpected source image %s", imageRef)
		}
		if _, duplicate := seenImages[imageRef]; duplicate {
			return nil, fmt.Errorf("duplicate source image %s", imageRef)
		}
		seenImages[imageRef] = struct{}{}
	}
	for _, targetImage := range deliveryResult.TargetImages {
		componentTag, err := imageComponentTag(targetImage)
		if err != nil {
			return nil, err
		}
		release, ok := releaseByComponentTag[componentTag]
		if !ok {
			return nil, fmt.Errorf("target image %s does not match a source image", targetImage)
		}
		targetsByArtifact[release.ArtifactUID] = append(targetsByArtifact[release.ArtifactUID], targetImage)
	}
	updates := make([]repo.DeliveryResultUpdate, 0, len(releases))
	for _, release := range releases {
		targets := targetsByArtifact[release.ArtifactUID]
		updates = append(updates, repo.DeliveryResultUpdate{
			ArtifactUID:  release.ArtifactUID,
			TargetImages: targets,
			Availability: deliveryAvailability(targets, checkedAt),
		})
	}
	return updates, nil
}

func imageComponentTag(imageRef string) (string, error) {
	lastSlash := strings.LastIndex(imageRef, "/")
	lastColon := strings.LastIndex(imageRef, ":")
	if lastColon <= lastSlash || lastColon == len(imageRef)-1 {
		return "", fmt.Errorf("image reference %s does not contain a tag", imageRef)
	}
	component := imageRef[lastSlash+1 : lastColon]
	if component == "" {
		return "", fmt.Errorf("image reference %s does not contain a repository component", imageRef)
	}
	return component + ":" + imageRef[lastColon+1:], nil
}

func deliveryAvailability(targetImages []string, checkedAt time.Time) []*kernelimageent.PlatformAvailability {
	result := make([]*kernelimageent.PlatformAvailability, 0, len(targetImages))
	for _, imageRef := range targetImages {
		result = append(result, &kernelimageent.PlatformAvailability{
			CloudProvider: fidcli.CloudProviderFromImageRef(imageRef),
			Registry:      fidcli.RegistryFromImageRef(imageRef),
			ImageRef:      imageRef,
			Status:        kernelimageent.PlatformAvailabilityStatusAvailable,
			CheckedAt:     checkedAt,
			Source:        "FID_DELIVERY",
		})
	}
	return result
}

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

func submissionFromInstance(instance *opent.Instance) (fidcli.Submission, error) {
	issueNumber, err := strconv.Atoi(instance.Outputs[outputFIDIssueNumber])
	if err != nil || issueNumber <= 0 {
		return fidcli.Submission{}, fmt.Errorf("invalid FID issue number output")
	}
	commentID, err := strconv.ParseInt(instance.Outputs[outputFIDRequestComment], 10, 64)
	if err != nil || commentID <= 0 {
		return fidcli.Submission{}, fmt.Errorf("invalid FID request comment ID output")
	}
	createdAt, err := time.Parse(time.RFC3339Nano, instance.Outputs[outputFIDRequestCreated])
	if err != nil {
		return fidcli.Submission{}, fmt.Errorf("invalid FID request creation time output: %w", err)
	}
	return fidcli.Submission{
		IssueNumber: issueNumber,
		CommentID:   commentID,
		CommentURL:  instance.Outputs[outputFIDRequestURL],
		CreatedAt:   createdAt,
	}, nil
}
