package action

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/entity/event"
	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/entity/validation"
	"github.com/tidbcloud/devops/pkg/infra/clients/eecli"
	"github.com/tidbcloud/devops/pkg/infra/clients/githubcli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"

	"github.com/google/go-cmp/cmp"
	"github.com/shurcooL/githubv4"
)

// PatchChangeStatus transitions the status and records the reason
//
// Recommend to use this function to patch status to make status transition clear.
func PatchChangeStatus(status relent.ChangeStatus, reason string) repo.PatchChange {
	return func(current *relent.ChangeInRepo) error {
		current.Status = status
		current.StatusReason = reason
		return nil
	}
}

func PatchChangeToClose(result relent.ChangeResult, summary string) repo.PatchChange {
	return func(current *relent.ChangeInRepo) error {
		current.ClosedAt = pointer.Pointer(time.Now())
		current.Status = relent.ChangeStatusClosed
		current.StatusReason = ""
		current.Result = &result
		current.Detail.Summary = &summary
		return nil
	}
}

func PatchChangeToUpdateBeforeApproved(update relsvcapi.UpdateChangeParam, record *event.ChangeEventUpdate) repo.PatchChange {
	return func(current *relent.ChangeInRepo) error {
		if update.Name != nil && *update.Name != current.Name {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "name",
				From:  pointer.Pointer(current.Name),
				To:    pointer.Pointer(*update.Name),
			})
			current.Name = *update.Name
		}
		if update.WindowStartAt != nil && *update.WindowStartAt != current.WindowStartAt {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "window_start_at",
				From:  pointer.Pointer(strconv.FormatInt(current.WindowStartAt, 10)),
				To:    pointer.Pointer(strconv.FormatInt(*update.WindowStartAt, 10)),
			})
			current.WindowStartAt = *update.WindowStartAt
		}
		if update.WindowEndAt != nil && *update.WindowEndAt != current.WindowEndAt {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "window_start_at",
				From:  pointer.Pointer(strconv.FormatInt(current.WindowEndAt, 10)),
				To:    pointer.Pointer(strconv.FormatInt(*update.WindowEndAt, 10)),
			})
			current.WindowEndAt = *update.WindowEndAt
		}
		if update.Tier != nil && len(*update.Tier) != 0 {
			if !cmp.Equal(current.Tier, *update.Tier) {
				record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
					Field: "tier",
					From: pointer.Pointer(strings.Join(
						container.TransformSlice(current.Tier, container.ToString[relent.ChangeTier]), ",",
					)),
					To: pointer.Pointer(strings.Join(
						container.TransformSlice(*update.Tier, container.ToString[relent.ChangeTier]), ",",
					)),
				})
			}
			current.Tier = *update.Tier
		}
		if update.RiskLevel != nil && *update.RiskLevel != current.RiskLevel {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "risk_level",
				From:  pointer.Pointer(string(current.RiskLevel)),
				To:    pointer.Pointer(string(*update.RiskLevel)),
			})
			current.RiskLevel = *update.RiskLevel
		}
		if update.Type != nil && *update.Type != current.Type {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "type",
				From:  pointer.Pointer(string(current.Type)),
				To:    pointer.Pointer(string(*update.Type)),
			})
			current.Type = *update.Type
		}
		if update.NeedOutage != nil && *update.NeedOutage != current.Detail.NeedOutage {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "need_outage",
				From:  pointer.Pointer(strconv.FormatBool(current.Detail.NeedOutage)),
				To:    pointer.Pointer(strconv.FormatBool(*update.NeedOutage)),
			})
			current.Detail.NeedOutage = *update.NeedOutage
		}
		if update.Note != nil && *update.Note != current.Detail.Note {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "note",
				From:  pointer.Pointer(current.Detail.Note),
				To:    pointer.Pointer(*update.Note),
			})
			current.Detail.Note = *update.Note
		}
		if update.RollbackPlan != nil && *update.RollbackPlan != current.Detail.RollbackPlan {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "rollback_plan",
				From:  pointer.Pointer(current.Detail.RollbackPlan),
				To:    pointer.Pointer(*update.RollbackPlan),
			})
			current.Detail.RollbackPlan = *update.RollbackPlan
		}
		if update.StagingTestReport != nil && *update.StagingTestReport != current.Detail.StagingTestReport {
			record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
				Field: "staging_test_report",
				From:  pointer.Pointer(current.Detail.StagingTestReport),
				To:    pointer.Pointer(*update.StagingTestReport),
			})
			current.Detail.StagingTestReport = *update.StagingTestReport
		}

		if update.Deployments != nil && len(*update.Deployments) > 0 {

			deployments := make([]*relent.ChangeDeployment, 0, len(*update.Deployments))
			for _, ud := range *update.Deployments {
				index := slices.IndexFunc(current.Deployments, func(d *relent.ChangeDeployment) bool {
					return d.Order == ud.Order
				})

				// append new deployment
				if index == -1 {
					deployments = append(deployments, repo.DeploymentWhenCreate(ud))
					record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
						Field: fmt.Sprintf("deployments[%d]", ud.Order),
						From:  nil,
						To:    pointer.Pointer(string(pointer.MarshalWithoutError(ud))),
					})
					continue
				}

				cur := current.Deployments[index]

				// create a copy of current deployment to ignore some fields for comparison
				curUpdatable := &relent.ChangeDeployment{
					Order:   cur.Order,
					Regions: cur.Regions,
					Method:  cur.Method,
					Info:    cur.Info,
				}
				if !cmp.Equal(curUpdatable, ud) {
					record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
						Field: fmt.Sprintf("deployments[%d]", ud.Order),
						From:  pointer.Pointer(string(pointer.MarshalWithoutError(curUpdatable))),
						To:    pointer.Pointer(string(pointer.MarshalWithoutError(ud))),
					})
				}

				// update existing deployment and reserve some fields like id, status ...
				cur.Order = ud.Order
				cur.Regions = ud.Regions
				cur.Method = ud.Method
				cur.Info = ud.Info
				deployments = append(deployments, cur)
			}

			// record deleted deployments
			for _, cur := range current.Deployments {
				index := slices.IndexFunc(deployments, func(d *relent.ChangeDeployment) bool {
					return d.Order == cur.Order
				})
				if index == -1 {
					record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
						Field: fmt.Sprintf("deployments[%d]", cur.Order),
						From:  pointer.Pointer(string(pointer.MarshalWithoutError(cur))),
						To:    nil,
					})
				}
			}

			current.Deployments = deployments
		}

		if update.ChangeItemNames != nil {
			if !cmp.Equal(current.ChangeItemNames, *update.ChangeItemNames) {
				record.UpdatedFields = append(record.UpdatedFields, event.UpdatedField{
					Field: "change_item_names",
					From:  pointer.Pointer(strings.Join(current.ChangeItemNames, ",")),
					To:    pointer.Pointer(strings.Join(*update.ChangeItemNames, ",")),
				})
			}

			current.ChangeItemNames = *update.ChangeItemNames
		}

		return nil
	}
}

func CheckDeployments(ctx context.Context, ghcli *githubcli.GithubClient, deployments []*relent.ChangeDeployment) error {
	for _, d := range deployments {

		if d.Info.ArgoCD != nil {
			err := validation.Change.DeploymentInfoArgoCD(d.Info.ArgoCD)
			if err != nil {
				return err
			}

			for _, dpr := range d.Info.ArgoCD.DeployPRs {
				pr, err := ghcli.PullRequest.Get(ctx, dpr.RepoOwner, dpr.RepoName, int(dpr.PRNumber))
				if err != nil {
					return fmt.Errorf("get related PR failed: %w", err)
				}
				if pr.State != githubv4.PullRequestStateOpen {
					return fmt.Errorf("PR %s/%s:%d is not open", dpr.RepoOwner, dpr.RepoName, dpr.PRNumber)
				}
			}
		}
	}

	return nil
}

func CheckChangeItems(ctx context.Context, release *relent.Release, ticketNames []string) ([]*relent.ReleaseTicket, error) {
	// ensure all tickets are existing in release
	tickets := make([]*relent.ReleaseTicket, 0, len(ticketNames))
	for _, name := range ticketNames {
		index := slices.IndexFunc(release.Tickets, func(ticket *relent.ReleaseTicket) bool {
			return ticket.Name == name
		})
		if index == -1 {
			return nil, errs.APIErrorf(api.ErrCodeInvalidArgument,
				"ticket %s not found in release %s", name, release.Name)
		}
		tickets = append(tickets, release.Tickets[index])
	}

	return tickets, nil
}

func ExecDeployment(ctx context.Context, comps Components, change *relent.ChangeInRepo, deployment *relent.ChangeDeployment) error {
	var deployErr error
	switch deployment.Method {
	case relent.ChangeDeployMethodArgoCD:

		msg := fmt.Sprintf("related change detail: [%s](%s)",
			change.Name, ChangeDetailLink(change.ReleaseID, change.ID))
		for _, pr := range deployment.Info.ArgoCD.DeployPRs {
			deployErr = ExecDeploymentViaGithubPR(ctx, comps.GithubService.RawAPI(), pr, msg)
			if deployErr != nil {
				break
			}
		}

	case relent.ChangeDeployMethodManual:
		deployErr = nil

	case relent.ChangeDeployMethodEEAPI:
		ngImageRequest := deployment.Info.EEAPI.EEAPIBuildNgImage
		ngImageRequest.Author = change.ApplicantEmail
		ngImageRequest.Meta.OpsReq = eecli.BumpNgTagOpsReq{
			ChangeID:  strconv.FormatUint(change.ID, 10),
			ReleaseID: strconv.FormatUint(change.ReleaseID, 10),
			Applicant: change.ApplicantEmail,
		}
		cli := comps.EEClient
		_, err := cli.BumpNgTag(ctx, ngImageRequest)
		if err != nil {
			deployErr = fmt.Errorf("bump ng tag via ee api failed: %w", err)
			break
		}

	default:
		deployErr = fmt.Errorf("unknown deploy method: %s", deployment.Method)
	}

	return deployErr
}

func ExecDeploymentViaGithubPR(ctx context.Context, ghCli *githubcli.GithubClient, pr relent.GithubPR, msg string) error {
	owner := pr.RepoOwner
	repo := pr.RepoName
	prNumber := int(pr.PRNumber)

	_, err := ghCli.PullRequest.Approve(ctx, owner, repo, prNumber, msg)
	if err != nil {
		return fmt.Errorf("approve PR %s/%s:%d failed: %w", owner, repo, prNumber, err)
	}

	_, err = ghCli.PullRequest.Merge(ctx, owner, repo, prNumber, githubv4.PullRequestMergeMethodSquash)
	if err != nil {
		return fmt.Errorf("merge PR %s/%s:%d failed: %w", owner, repo, prNumber, err)
	}

	return nil
}

func CheckDeploymentReady(ctx context.Context, comps Components, change *relent.ChangeInRepo, deployment *relent.ChangeDeployment) ([]string, error) {
	now := time.Now()
	reasons := []string{}

	// If this is the first deployment, check window and dependency
	if deployment.Order.IsFirst() {
		// wait for window start
		windowReasons, err := CheckChangeDeployWindowOK(change)
		if err != nil {
			return nil, err
		}
		reasons = append(reasons, windowReasons...)

		return reasons, nil
	}

	// Check previous deployments
	for _, d := range change.Deployments {
		if d.Order >= deployment.Order {
			continue
		}

		if d.DeployedAt == nil {
			reasons = append(reasons, fmt.Sprintf("prev deployment %d isn't deployed", d.Order))
			break
		}
		if now.Sub(*d.DeployedAt) < *comps.Conf.MinDeployInterval {
			reasons = append(reasons, fmt.Sprintf("wait at least %s between deployments",
				comps.Conf.MinDeployInterval.String()))
			break
		}
	}

	return reasons, nil
}

func CheckChangeDeployWindowOK(change *relent.ChangeInRepo) ([]string, error) {
	now := time.Now().Unix()

	reasons := []string{}

	if change.WindowStartAt != 0 && change.WindowStartAt > now {
		reasons = append(reasons, fmt.Sprintf("window start time %s not reached", change.WindowStartTime().UTC().Format(time.RFC3339)))
	}
	if change.WindowEndAt != 0 && change.WindowEndAt < now {
		reasons = append(reasons, fmt.Sprintf("window end time %s passed", change.WindowEndTime().UTC().Format(time.RFC3339)))
	}

	return reasons, nil
}

func ChangeDetailLink(releaseID, changeID uint64) string {
	return fmt.Sprintf("https://%s/release/%d/change/%d", consts.ConsoleAddr(), releaseID, changeID)
}

func ActionOperator(ctx context.Context) string {
	return consts.GetOperatorFromCtx(ctx, "unknown")
}
