package kernelimage

import (
	"errors"
	"fmt"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/devops/api"
	imageapi "github.com/tidbcloud/devops/api/kernelimage"
	ent "github.com/tidbcloud/devops/entity/kernelimage"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	svcapi "github.com/tidbcloud/devops/pkg/service/kernelimage/api"
	imagerepo "github.com/tidbcloud/devops/pkg/service/kernelimage/repo"
	imagesvc "github.com/tidbcloud/devops/pkg/service/kernelimage/service"
	"go.uber.org/dig"
	"gorm.io/gorm"
)

type ControllerParam struct {
	dig.In

	Service svcapi.Service
}

type Controller struct {
	service svcapi.Service
}

func NewController(param ControllerParam) ctrl.Controller {
	return &Controller{service: param.Service}
}

func (c *Controller) Name() string {
	return "KernelImageController"
}

func (c *Controller) RegisterAPI(suite *ctrl.Suite) error {
	group := suite.Router.Group("/kernel-images")
	group.GET("/releases", c.ListReleases)
	group.GET("/releases/:artifact_uid", c.GetRelease)
	group.GET("/selection", c.GetSelection)
	group.POST("/build-callback", c.BuildCallback)
	group.POST("/releases/:artifact_uid/availability-check", c.RefreshAvailability)
	return nil
}

// BuildCallback
//
//	@Id				KernelImageControllerBuildCallback
//	@Description	accept an idempotent callback, store a batch of successfully built image artifacts, and create one DeliverKernelImage operation
//	@Tags			KernelImageService
//	@Accept			json
//	@Produce		json
//	@Param			request	body		imageapi.BuildCallbackReq	true	"build callback"
//	@Success		200		{object}	imageapi.BuildCallbackResp
//	@Failure		default	{object}	api.BaseResp
//	@Router			/api/v1/devops/kernel-images/build-callback [post]
func (c *Controller) BuildCallback(ctx *gin.Context) {
	req := &imageapi.BuildCallbackReq{}
	if err := ctx.ShouldBindJSON(req); err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("invalid build callback: %w", err), api.ErrCodeInvalidArgument)
		return
	}
	images := make([]svcapi.BuildCallbackImage, 0, len(req.Images))
	for _, image := range req.Images {
		images = append(images, svcapi.BuildCallbackImage{
			SourceRepository: image.SourceRepository,
			SourceTag:        image.SourceTag,
			SourceDigest:     image.SourceDigest,
		})
	}
	result, err := c.service.AcceptBuildCallback(ctx, svcapi.BuildCallbackParam{
		SourceApplicant: req.SourceApplicant,
		SourceReleaseID: req.SourceReleaseID, SourceChangeID: req.SourceChangeID,
		Repo: req.Repo, Branch: req.Branch,
		CommitSHA: req.CommitSHA, GitTag: req.GitTag, Images: images,
	})
	if err != nil {
		fail(ctx, err)
		return
	}
	releases := make([]*imageapi.Release, 0, len(result.Releases))
	for _, release := range result.Releases {
		releases = append(releases, imageapi.NewRelease(release))
	}
	ctrl.Success(ctx, &imageapi.BuildCallbackResp{Releases: releases, InstanceID: result.InstanceID})
}

// ListReleases
//
//	@Id				KernelImageControllerListReleases
//	@Description	list local kernel image release records
//	@Tags			KernelImageService
//	@Accept			json
//	@Produce		json
//	@Param			req	query		imageapi.ListReleasesReq	true	"ListReleasesReq"
//	@Success		200	{object}	imageapi.ListReleasesResp
//	@Failure		500	{object}	api.BaseResp
//	@Router			/api/v1/devops/kernel-images/releases [get]
func (c *Controller) ListReleases(ctx *gin.Context) {
	req := &imageapi.ListReleasesReq{}
	if err := ctx.ShouldBindQuery(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	statuses := make([]ent.ReleaseStatus, 0, len(req.Statuses))
	for _, status := range req.Statuses {
		statuses = append(statuses, ent.ReleaseStatus(status))
	}
	items, total, err := c.service.ListReleases(ctx, svcapi.ListReleasesParam{
		Repository: req.Repository, Version: req.Version,
		Statuses: statuses, Masked: req.Masked, Page: int(req.Page), PerPage: int(req.PerPage),
	})
	if err != nil {
		fail(ctx, err)
		return
	}
	responseItems := make([]*imageapi.Release, 0, len(items))
	for _, item := range items {
		responseItems = append(responseItems, imageapi.NewRelease(item))
	}
	ctrl.Success(ctx, &imageapi.ListReleasesResp{Total: total, Items: responseItems})
}

// GetRelease
//
//	@Id				KernelImageControllerGetRelease
//	@Description	get a local kernel image release by artifact UID
//	@Tags			KernelImageService
//	@Accept			json
//	@Produce		json
//	@Param			artifact_uid	path		string	true	"artifact UID"
//	@Success		200				{object}	imageapi.GetReleaseResp
//	@Failure		500				{object}	api.BaseResp
//	@Router			/api/v1/devops/kernel-images/releases/{artifact_uid} [get]
func (c *Controller) GetRelease(ctx *gin.Context) {
	release, err := c.service.GetRelease(ctx, ctx.Param("artifact_uid"))
	if err != nil {
		fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &imageapi.GetReleaseResp{Release: imageapi.NewRelease(release)})
}

// GetSelection
//
//	@Id				KernelImageControllerGetSelection
//	@Description	calculate the local M x N kernel image selection
//	@Tags			KernelImageService
//	@Accept			json
//	@Produce		json
//	@Param			req	query		imageapi.SelectionReq	true	"SelectionReq"
//	@Success		200	{object}	imageapi.SelectionResp
//	@Failure		500	{object}	api.BaseResp
//	@Router			/api/v1/devops/kernel-images/selection [get]
func (c *Controller) GetSelection(ctx *gin.Context) {
	req := &imageapi.SelectionReq{}
	if err := ctx.ShouldBindQuery(req); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	result, err := c.service.GetSelection(ctx, svcapi.SelectionParam{
		ClusterType: req.ClusterType, Component: req.Component,
	})
	if err != nil {
		fail(ctx, err)
		return
	}
	ctrl.Success(ctx, imageapi.NewSelection(result))
}

// RefreshAvailability
//
//	@Id				KernelImageControllerRefreshAvailability
//	@Description	start an asynchronous registry availability check for every FID delivery target of a release
//	@Tags			KernelImageService
//	@Accept			json
//	@Produce		json
//	@Param			artifact_uid	path		string	true	"Artifact UID"
//	@Success		200				{object}	imageapi.GetReleaseResp
//	@Failure		default			{object}	api.BaseResp
//	@Router			/api/v1/devops/kernel-images/releases/{artifact_uid}/availability-check [post]
func (c *Controller) RefreshAvailability(ctx *gin.Context) {
	release, err := c.service.RefreshAvailability(ctx, ctx.Param("artifact_uid"))
	if err != nil {
		fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &imageapi.GetReleaseResp{Release: imageapi.NewRelease(release)})
}

func fail(ctx *gin.Context, err error) {
	switch {
	case errors.Is(err, imagesvc.ErrDisabled), errors.Is(err, gorm.ErrRecordNotFound):
		ctrl.FailWithCode(ctx, err, api.ErrCodeNotFound)
	case errors.Is(err, imagesvc.ErrInvalidArgument):
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
	case errors.Is(err, imagerepo.ErrImmutableArtifactConflict):
		ctrl.FailWithCode(ctx, err, api.ErrCodeAlreadyExists)
	default:
		ctrl.FailWithCode(ctx, err, api.ErrCodeInternal)
	}
}
