package release

import (
	"fmt"
	"net/url"
	"strconv"
	"strings"

	"github.com/tidbcloud/devops/api"
	releaseapi "github.com/tidbcloud/devops/api/release"
	relent "github.com/tidbcloud/devops/entity/release"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/eecli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"

	"github.com/gin-gonic/gin"
)

// DefaultChange
//
//	@Id				ReleaseControllerDefaultChange
//	@Description	get a default change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			req	query		releaseapi.DefaultChangeReq	true	"request param"
//	@Success		200	{object}	releaseapi.DefaultChangeResp
//	@Router			/api/v1/devops/release/changes/default [get]
func (c *ReleaseController) DefaultChange(ctx *gin.Context) {
	req := &releaseapi.DefaultChangeReq{}
	err := ctx.ShouldBindQuery(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	param := relsvcapi.DefaultChangeParam{
		ReleaseID:       req.ReleaseID,
		ApplicationName: req.ApplicationName,
	}

	change, err := c.chgSvc.DefaultChange(ctx, param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &releaseapi.DefaultChangeResp{
		WindowStartAt: change.WindowStartAt,
		WindowEndAt:   change.WindowEndAt,
	}
	ctrl.Success(ctx, resp)
}

// CreateChange
//
//	@Id				ReleaseControllerCreateChange
//	@Description	create a new change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			req	body		releaseapi.CreateChangeReq	true	"request body"
//	@Success		200	{object}	releaseapi.CreateChangeResp
//	@Router			/api/v1/devops/release/changes [post]
func (c *ReleaseController) CreateChange(ctx *gin.Context) {
	req := &releaseapi.CreateChangeReq{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	email := req.Applicant
	if email == "" {
		v, exist := ctx.Get(consts.CtxUserEmail)
		if !exist {
			ctrl.FailWithCode(ctx, fmt.Errorf("account not found in ctx"), api.ErrCodeInternal)
			return
		}
		email = v.(string)
	}

	param, err := buildParamToCreateChange(email, req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	id, err := c.chgSvc.CreateChange(ctx, *param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &releaseapi.CreateChangeResp{ChangeID: id}
	ctrl.Success(ctx, resp)
}

// GetChange
//
//	@Id				ReleaseControllerGetChange
//	@Description	get a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		int						true	"change id"
//	@Param			req			query		releaseapi.GetChangeReq	false	"request param"
//	@Success		200			{object}	releaseapi.GetChangeResp
//	@Router			/api/v1/devops/release/changes/{change_id} [get]
func (c *ReleaseController) GetChange(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &releaseapi.GetChangeReq{}
	err = ctx.ShouldBindQuery(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	param := relsvcapi.GetChangeParam{
		WithChangeDetail: req.WithChangeDetail,
		WithChangeItems:  req.WithChangeItems,
		WithRelease:      req.WithRelease,
		WithApplicant:    req.WithApplicant,
		WithApplication:  req.WithApplication,
	}
	change, err := c.chgSvc.GetChange(ctx, id, param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &releaseapi.GetChangeResp{Change: change}
	if req.WithTickets {
		release, err := c.relSvc.GetRelease(ctx, change.ReleaseID, relsvcapi.GetReleaseParam{WithTickets: true})
		if err != nil {
			ctrl.Fail(ctx, err)
			return
		}
		resp.ReleaseTickets = release.Tickets
	}

	ctrl.Success(ctx, resp)
}

// UpdateChange
//
//	@Id				ReleaseControllerUpdateChange
//	@Description	update a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		int							true	"change id"
//	@Param			req			body		releaseapi.UpdateChangeReq	true	"request body"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id} [put]
func (c *ReleaseController) UpdateChange(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &releaseapi.UpdateChangeReq{}
	err = ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	param, err := buildParamToUpdateChange(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.UpdateChangeBeforeDeploy(ctx, id, *param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// ExecuteDeployment
//
//	@Id				ReleaseControllerExecuteDeployment
//	@Description	exeuete a deployment of a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id			path		int								true	"change id"
//	@Param			deployment_order	path		int								true	"deployment id"
//	@Param			req					body		releaseapi.ExecuteDeploymentReq	true	"request body"
//	@Success		200					{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id}/deployments/{deployment_order}/deploy [post]
func (c *ReleaseController) ExecuteDeployment(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	order, err := strconv.ParseInt(ctx.Param("deployment_order"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("deployment order is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &releaseapi.ExecuteDeploymentReq{}
	err = ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.ExecuteDeployment(ctx, id, relent.ChangeDeployOrder(order), req.Force)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// VerifyDeployment
//
//	@Id				ReleaseControllerVerifyDeployment
//	@Description	verify a deployment of a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id			path		int								true	"change id"
//	@Param			deployment_order	path		int								true	"deployment id"
//	@Param			req					body		releaseapi.VerifyDeploymentReq	true	"request body"
//	@Success		200					{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id}/deployments/{deployment_order}/verify [post]
func (c *ReleaseController) VerifyDeployment(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	order, err := strconv.ParseInt(ctx.Param("deployment_order"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("deployment order is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &releaseapi.VerifyDeploymentReq{}
	err = ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.VerifyDeployment(ctx, id, relent.ChangeDeployOrder(order), req.Note)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// IsDeploymentReady
//
//	@Id				ReleaseControllerIsDeploymentReady
//	@Description	return if a deployment of a change is ready to execute
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id			path		int	true	"change id"
//	@Param			deployment_order	path		int	true	"deployment id"
//	@Success		200					{object}	releaseapi.IsDeploymentReadyResp
//	@Router			/api/v1/devops/release/changes/{change_id}/deployments/{deployment_order}/ready [get]
func (c *ReleaseController) IsDeploymentReady(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	order, err := strconv.ParseInt(ctx.Param("deployment_order"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("deployment order is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	reasons, err := c.chgSvc.IsDeploymentReady(ctx, id, relent.ChangeDeployOrder(order))
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &releaseapi.IsDeploymentReadyResp{IsReady: len(reasons) == 0, Reasons: reasons}
	ctrl.Success(ctx, resp)
}

// SubmitChange
//
//	@Id				ReleaseControllerSubmitChange
//	@Description	submit a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		int	true	"change id"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id}/submit [post]
func (c *ReleaseController) SubmitChange(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.SubmitChange(ctx, id)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// CancelChange
//
//	@Id				ReleaseControllerCancelChange
//	@Description	cancel a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		int	true	"change id"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id}/cancel [post]
func (c *ReleaseController) CancelChange(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.CancelChange(ctx, id)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// CloseChange
//
//	@Id				ReleaseControllerCloseChange
//	@Description	close a change
//	@Tags			ReleaseController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		int							true	"change id"
//	@Param			req			body		releaseapi.CloseChangeReq	true	"request body"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/release/changes/{change_id}/close [post]
func (c *ReleaseController) CloseChange(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &releaseapi.CloseChangeReq{}
	err = ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	err = c.chgSvc.CloseChange(ctx, id, req.Result, req.Summary)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

func buildParamToCreateChange(accountEmail string, req *releaseapi.CreateChangeReq) (*relsvcapi.CreateChangeParam, error) {
	deployments := []*relent.ChangeDeployment{}
	for _, deployInReq := range req.Deployments {
		d, err := parseDeployment(deployInReq)
		if err != nil {
			return nil, err
		}
		deployments = append(deployments, d)
	}

	spec := relent.ChangeInRepo{
		Name:          req.Name,
		WindowStartAt: req.WindowStartAt,
		WindowEndAt:   req.WindowEndAt,
		Tier:          req.Tier,
		RiskLevel:     req.RiskLevel,
		Type:          req.Type,

		Detail: &relent.ChangeDetail{
			NeedOutage:        req.NeedOutage,
			Note:              req.Note,
			RollbackPlan:      req.RollbackPlan,
			StagingTestReport: req.StagingTestReport,
		},
		Deployments: deployments,

		ChangeItemNames: req.ChangeItemNames,
		ReleaseID:       req.ReleaseID,
		ApplicantEmail:  accountEmail,
		ApplicationName: req.ApplicationName,
	}

	param := &relsvcapi.CreateChangeParam{
		Spec:    spec,
		IsDraft: req.IsDraft,
	}

	return param, nil
}

func buildParamToUpdateChange(req *releaseapi.UpdateChangeReq) (*relsvcapi.UpdateChangeParam, error) {
	deployments := []*relent.ChangeDeployment{}
	for _, deployInReq := range req.Deployments {
		d, err := parseDeployment(deployInReq)
		if err != nil {
			return nil, err
		}
		deployments = append(deployments, d)
	}

	update := relsvcapi.UpdateChangeParam{
		Name:              req.Name,
		WindowStartAt:     req.WindowStartAt,
		WindowEndAt:       req.WindowEndAt,
		Tier:              req.Tier,
		RiskLevel:         req.RiskLevel,
		NeedOutage:        req.NeedOutage,
		Note:              req.Note,
		RollbackPlan:      req.RollbackPlan,
		StagingTestReport: req.StagingTestReport,
		Type:              req.Type,

		Deployments: &deployments,

		ChangeItemNames: req.ChangeItemNames,
	}

	return &update, nil
}

func parseDeployment(deployInReq releaseapi.Deployment) (*relent.ChangeDeployment, error) {
	deployment := &relent.ChangeDeployment{
		Order:   relent.ChangeDeployOrder(deployInReq.Order),
		Regions: deployInReq.Regions,
		Method:  deployInReq.DeployMethod,
		Info: relent.ChangeDeployInfo{
			Description: deployInReq.DeployDescription,
		},
	}

	switch deployInReq.DeployMethod {
	case relent.ChangeDeployMethodManual:
		deployment.Info.Manual = &relent.ChangeDeployManual{}

	case relent.ChangeDeployMethodArgoCD:
		u, err := url.Parse(deployInReq.DeployCDLink)
		if err != nil {
			return nil, err
		}

		if !strings.Contains(u.Host, "github.com") || !strings.Contains(u.Path, "/pull/") {
			return nil, fmt.Errorf("not a valid GitHub PR URL")
		}

		fields := strings.Split(u.Path, "/")
		if len(fields) < 5 {
			return nil, fmt.Errorf("not a valid GitHub PR URL")
		}

		owner := fields[1]
		repo := fields[2]
		prID, err := strconv.ParseInt(fields[4], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("not a valid GitHub PR URL")
		}

		switch deployInReq.DeployMethod {
		case relent.ChangeDeployMethodArgoCD:
			deployment.Info.ArgoCD = &relent.ChangeDeployArgoCD{
				DeployPRs: []relent.GithubPR{
					{
						RepoOwner: owner,
						RepoName:  repo,
						PRNumber:  prID,
						Link:      deployInReq.DeployCDLink,
					},
				},
			}
		}
	case relent.ChangeDeployMethodEEAPI:
		deployment.Info.EEAPI = &relent.ChangeDeployEEAPI{
			EEAPIBuildNgImage: &eecli.BumpNgTagRequest{
				Repo:   eecli.TiDBRepo(deployInReq.Repo),
				Branch: deployInReq.Branch,
				Commit: deployInReq.Commit,
				PRLink: deployInReq.DeployCDLink,
			},
		}
	default:
		return nil, fmt.Errorf("not a valid deployment method")
	}

	return deployment, nil
}
