package github

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

	googleGithub "github.com/google/go-github/v50/github"
	"github.com/google/uuid"
	"github.com/tidbcloud/ecosystem-service/internal/metric"
	gitDB "github.com/tidbcloud/ecosystem-service/internal/storage/db/git"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/rpc"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	BranchNameMaxLen = 60
	ShaDefaultLen    = 7

	BranchAnnotationKey   = "created-by"
	BranchAnnotationValue = "tidbcloud/branching-github"
)

// CheckRunSummary
const (
	Succeed                  = "Manage the TiDB Serverless branch successfully."
	InternalError            = "Internal error. Please try again later."
	InvalidGitConfig         = "Invalid git config. Please check your git config."
	DeletePreviousBranchFail = "Fail to delete the previous branch. Try to delete it manually."
	DeleteBranchInReRunFail  = "Fail to delete the branch when rerun. Please try again later."
	CreateBranchFail         = "Fail to create the branch. Please try again later."
	CommentFail              = "Create branch success but fail to comment."
	WatchBranchStatusTimeout = "Watch branch status timeout. Please go to %s to check the branch status."
	GetBranchStatusFail      = "Fail to get branch status. Please go to %s to check the branch status."
	GetBranchFail            = "Fail to get branch. Please go to %s to check the branch status."
	BranchStatusUnReady      = "Branch status is not ready."
	ResetBranchInReRunFail   = "Fail to reset the branch when rerun. Please go to %s to check the branch status."
	UnknownMode              = "Unknown mode. Please check your git config."
	ResetBranchFail          = "Fail to reset the branch. Please go to %s to check the branch status."
)

func (g *GitHubApp) HandlePullRequest(ctx context.Context, githubEventPullRequest *GitHubPullRequestEvent, rerun bool) (err error) {
	tilog.Info(ctx, fmt.Sprintf("handle event pull_request, action: %s, rerun: %v", githubEventPullRequest.Action, rerun))

	repository := githubEventPullRequest.Repository
	PullRequest := githubEventPullRequest.PullRequest
	head := PullRequest.Head
	action := githubEventPullRequest.Action

	defer func() {
		if err != nil {
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.HandlePullRequest).Inc()
		}
	}()

	// get cluster info
	clusterInfo, err := GetClusterInfo(ctx, g, githubEventPullRequest)
	if err != nil {
		tilog.Error(ctx, "get clusterId error", zap.Error(err))
		return err
	}
	if clusterInfo == nil {
		return nil
	}

	// get git config
	gitConfig := GetGitConfig(ctx, g, githubEventPullRequest)

	// check branch according to the config
	match, err := gitConfig.MatchBranch(head.Ref)
	// We have checked the config before, This error generally does not occur
	if err != nil {
		tilog.Error(ctx, "matchBranch error", zap.Error(err))
		metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.MatchBranch).Inc()
	}
	if !match {
		tilog.Info(ctx, fmt.Sprintf("The branch %s under %s has been filtered", repository.FullName, head.Ref), zap.Error(err))
		return nil
	}

	// handle event
	switch githubEventPullRequest.Action {
	case ActionOpened, ActionSynchronize:
		return doHandlePullRequest(ctx, githubEventPullRequest, clusterInfo, gitConfig, rerun, g)
	case ActionReopened:
		if gitConfig.GitHubConfig.BranchConfig.AutoDestroy {
			return doHandlePullRequest(ctx, githubEventPullRequest, clusterInfo, gitConfig, rerun, g)
		}
	case ActionClosed:
		return handlePrClose(ctx, githubEventPullRequest, clusterInfo, gitConfig, g)
	}
	return nil
}

func doHandlePullRequest(ctx context.Context, githubEvent *GitHubPullRequestEvent, clusterInfo *ClusterInfo, gitConfig *Config, rerun bool, g *GitHubApp) error {
	owner := githubEvent.Repository.Owner.Login
	repo := githubEvent.Repository.Name
	sha := githubEvent.PullRequest.Head.Sha
	ref := githubEvent.PullRequest.Head.Ref
	action := githubEvent.Action
	mode := gitConfig.GitHubConfig.BranchConfig.Mode
	prNumber := githubEvent.PullRequest.Number
	branchName := buildBranchName(ref, sha, prNumber, mode)
	installationID := githubEvent.Installation.ID
	githubClient, err := g.GetGitHubClient(ctx, installationID)
	if err != nil {
		tilog.Error(ctx, "get GitHub client error", zap.Error(err))
		return err
	}

	// get externalID for rerun
	externalID := &ExternalID{
		PullRequestAction: &action,
		PullRequestBefore: &githubEvent.Before,
	}
	externalIDJson, err := externalID.MarshallExternalID()
	if err != nil {
		tilog.Error(ctx, "fail to marshall externalID", zap.Any("externalID", externalID), zap.Error(err))
		return err
	}

	// create check run
	tilog.Debug(ctx, fmt.Sprintf("create check run for %s on %s.%s", owner+repo, ref, sha))
	checkRun, _, err := githubClient.Checks.CreateCheckRun(ctx, owner, repo, googleGithub.CreateCheckRunOptions{
		Name:       checkName,
		HeadSHA:    sha,
		ExternalID: &externalIDJson,
		DetailsURL: Ptr("https://tidbcloud.com/"),
		Status:     Ptr("in_progress"),
		StartedAt:  &googleGithub.Timestamp{Time: time.Now()},
	})
	if err != nil {
		tilog.Error(ctx, "CreateCheckRun error", zap.Error(err))
		return err
	}

	// update check run when unknown panic occur
	defer func() {
		if err := recover(); err != nil {
			tilog.Error(ctx, "Unknown panic"+fmt.Sprintln(err))
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.UnknownPanic).Inc()
			err1 := g.UpdateCheckRun(ctx, installationID, owner, repo, sha, prNumber, Skip, nil, externalID, *checkRun.ID, InternalError, action)
			if err1 != nil {
				tilog.Error(ctx, "UpdateCheckRun error", zap.Error(err1))
			}
		}
	}()

	conclusion, branchID, currentStatus, UUID, summary, text := manageTiDBCloudBranch(ctx, githubEvent, g, gitConfig, clusterInfo, rerun)
	// update check run when conclusion is a failure
	if conclusion == Success {
		// save task to db to watch status and update the check run in background
		task := &gitDB.GithubAppTask{
			TiDBOrgID:                clusterInfo.OrgID,
			TiDBProjectID:            clusterInfo.ProjectId,
			TiDBClusterID:            clusterInfo.ClusterID,
			TiDBBranchID:             branchID,
			TiDBBranchName:           branchName,
			GitHubCommentUUID:        UUID,
			InstallationID:           installationID,
			GitHubOwner:              owner,
			GitHubRepo:               repo,
			GitHubPrNumber:           prNumber,
			GitHubCheckRunID:         *checkRun.ID,
			GitHubCommitID:           sha,
			TiDBBranchPreviousStatus: currentStatus,
			Status:                   gitDB.Pending,
			Action:                   action,
			Before:                   githubEvent.Before,
		}
		_, err := g.gitHubAppTaskDao.AddTasks(ctx, task)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("save task fail. task:%+v", task), zap.Error(err))
			return err
		}
	} else {
		err := g.UpdateCheckRun(ctx, installationID, owner, repo, sha, prNumber, conclusion, text, externalID, *checkRun.ID, summary, action)
		if err != nil {
			tilog.Error(ctx, "UpdateCheckRun error", zap.Error(err))
			return err
		}
	}
	return nil
}

func manageTiDBCloudBranch(ctx context.Context, githubEvent *GitHubPullRequestEvent, g *GitHubApp, gitConfig *Config, clusterInfo *ClusterInfo, rerun bool) (conclusion Conclusion, branchID, currentStatus, UUID string, summary string, text *CheckRunOutputDetail) {
	owner := githubEvent.Repository.Owner.Login
	repo := githubEvent.Repository.Name
	sha := githubEvent.PullRequest.Head.Sha
	ref := githubEvent.PullRequest.Head.Ref
	action := githubEvent.Action
	mode := gitConfig.GitHubConfig.BranchConfig.Mode
	prNumber := githubEvent.PullRequest.Number
	branchName := buildBranchName(ref, sha, prNumber, mode)
	branch := &rpc.FullBranch{}
	var id *string
	tilog.Debug(ctx, fmt.Sprintf("manage branch %s on action: %s, mode: %s, rerun: %v", branchName, action, mode, rerun))

	// lock delete and create to prevent the creation of the previous commit from falling behind
	unlock := g.mutexes.Lock(fmt.Sprintf("%s/%s/%d", owner, repo, prNumber))
	defer unlock()

	if rerun {
		// only reserve need to delete the previous branch, because the branch name is the same
		if mode == ModeReserve {
			bizErr := g.BranchSvc.DeleteBranchByNameAndAnnotation(ctx, clusterInfo.ClusterID, branchName, BranchAnnotationKey, BranchAnnotationValue)
			if bizErr != nil && bizErr.ErrCode() != bizerr.ErrCodeRecordNotFound {
				tilog.Error(ctx, fmt.Sprintf("Delete branch %s under cluster %d fail.", branchName, clusterInfo.ClusterID), zap.Error(bizErr))
				text := &CheckRunOutputDetail{
					ClusterID:  Ptr(strconv.FormatUint(clusterInfo.ClusterID, 10)),
					BranchName: &branchName,
					Error:      Ptr(bizErr.Error()),
				}
				return Fail, "", "", "", DeleteBranchInReRunFail, text
			}
		}
	}
	switch mode {
	case ModeReset:
		// get existed branch, if not exist, create a new one
		var err bizerr.BizError
		branch, err = g.BranchSvc.GetBranchByNameAndAnnotation(ctx, clusterInfo.ClusterID, branchName, BranchAnnotationKey, BranchAnnotationValue)
		if err != nil {
			tilog.Info(ctx, fmt.Sprintf("Get branch(name: %s) under cluster %d fail, create a new one", branchName, clusterInfo.ClusterID), zap.Error(err))
			if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
				// branch not exist when the action is opened or reopened, or when the previous branch is manually deleted
				tilog.Info(ctx, fmt.Sprintf("Create a new branch(name: %s)", branchName))
				id, text = callCreateBranch(ctx, g, clusterInfo, branchName, action)
				if id == nil {
					return Fail, "", "", "", CreateBranchFail, text
				}
				currentStatus = rpc.BranchStatus_CREATING.String()
			} else {
				return Fail, "", "", "", GetBranchFail, text
			}
		} else {
			text = callResetBranch(ctx, g, clusterInfo, branch.Id, branchName)
			if text != nil {
				return Fail, "", "", "", ResetBranchFail, text
			}
			currentStatus = rpc.BranchStatus_RESTORING.String()
		}

	case ModeReserve:
		id, text = callCreateBranch(ctx, g, clusterInfo, branchName, action)
		if id == nil {
			return Fail, "", "", "", CreateBranchFail, text
		}
		currentStatus = rpc.BranchStatus_CREATING.String()
	default:
		return Fail, "", "", "", UnknownMode, nil
	}

	if id == nil {
		branchID = branch.Id
	} else {
		branchID = *id
	}

	// create or update comment after create branch success
	UUID = uuid.New().String()
	err := createOrUpdateComment(ctx, githubEvent, g, gitConfig, currentStatus, UUID, clusterInfo, g.conf.ServerConfig.TiDBCloudHost, g.conf.GitConfig.GitHubAppConfig.Name)
	if err != nil {
		tilog.Error(ctx, "createOrUpdateComment fail", zap.Error(err))
		text := &CheckRunOutputDetail{
			ClusterID:  Ptr(strconv.FormatUint(clusterInfo.ClusterID, 10)),
			BranchName: &branchName,
			Error:      Ptr(err.Error()),
		}
		return Fail, "", "", "", CommentFail, text
	}
	return Success, branchID, currentStatus, UUID, Succeed, nil
}

func createOrUpdateComment(ctx context.Context, githubEvent *GitHubPullRequestEvent, g *GitHubApp, gitConfig *Config, status string, uuid string, cluster *ClusterInfo, host string, name string) (err error) {
	sha := githubEvent.PullRequest.Head.Sha
	ref := githubEvent.PullRequest.Head.Ref
	owner := githubEvent.Repository.Owner.Login
	repo := githubEvent.Repository.Name
	number := githubEvent.PullRequest.Number
	githubClient := g.ghClientMap[githubEvent.Installation.ID]
	action := githubEvent.Action
	mode := gitConfig.GitHubConfig.BranchConfig.Mode

	clusterID := cluster.ClusterID

	defer func() {
		if err != nil {
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.GitCommentBranch).Inc()
		}
	}()

	tilog.Debug(ctx, fmt.Sprintf("create or update comment for %s on pr:%d. status: %s", owner+repo, number, status))

	body := GetCommentBody(buildBranchName(ref, sha, number, mode), uuid, status, host, cluster.OrgID, cluster.ProjectId, clusterID)

	issueComments, _, err := githubClient.Issues.ListComments(ctx, owner, repo, number, nil)
	if err != nil {
		return err
	}
	var issueComment *googleGithub.IssueComment
	for _, comment := range issueComments {
		if strings.Contains(*comment.User.Login, name) && comment.Body != nil && strings.Contains(*comment.Body, "The latest updates on TiDB Serverless Branch") {
			issueComment = comment
			break
		}
	}
	if issueComment != nil {
		_, _, err = githubClient.Issues.EditComment(ctx, owner, repo, *issueComment.ID, &googleGithub.IssueComment{
			Body: Ptr(body),
		})
	} else {
		_, _, err = githubClient.Issues.CreateComment(ctx, owner, repo, number, &googleGithub.IssueComment{
			Body: Ptr(body),
		})
	}
	return err
}

func handlePrClose(ctx context.Context, githubEvent *GitHubPullRequestEvent, clusterInfo *ClusterInfo, gitConfig *Config, g *GitHubApp) error {
	sha := githubEvent.PullRequest.Head.Sha
	ref := githubEvent.PullRequest.Head.Ref
	number := githubEvent.Number
	action := githubEvent.Action
	// check config
	if !gitConfig.GitHubConfig.BranchConfig.AutoDestroy {
		tilog.Info(ctx, fmt.Sprintf("autoDestroy is disabled. save branches under cluster %d.", clusterInfo.ClusterID))
		return nil
	}

	tilog.Debug(ctx, fmt.Sprintf("delete branches under cluster %d.", clusterInfo.ClusterID))
	// get all branches
	branchToDelete := make([]string, 0)
	branches, err := g.BranchSvc.ListBranches(ctx, clusterInfo.ClusterID)
	if err != nil {
		return err
	}
	// get all branches need to delete
	for _, branch := range branches {
		if !strings.Contains(branch.DisplayName, buildBranchNameWithoutSha(ref, sha, number)) {
			continue
		}
		if branch.Annotations[BranchAnnotationKey] != BranchAnnotationValue {
			continue
		}
		if branch.State != rpc.BranchStatus_DELETED {
			branchToDelete = append(branchToDelete, branch.Id)
		}
	}

	// delete all branches
	for _, branchId := range branchToDelete {
		err := g.BranchSvc.DeleteBranch(ctx, clusterInfo.ClusterID, branchId)
		if err != nil {
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.DeleteBranch).Inc()
			tilog.Error(ctx, fmt.Sprintf("Delete Branch %s under %d fail", branchId, clusterInfo.ClusterID), zap.Error(err))
		} else {
			tilog.Debug(ctx, fmt.Sprintf("Delete Branch %s under cluster %d success", branchId, clusterInfo.ClusterID), zap.Error(err))
		}
	}
	return nil
}

func buildBranchName(ref, sha string, prNumber int, mode string) string {
	switch mode {
	case ModeReserve:
		return buildBranchNameWithSha(ref, sha, prNumber)
	case ModeReset:
		return buildBranchNameWithoutSha(ref, sha, prNumber)
	}
	return ""
}

func buildBranchNameWithSha(ref, sha string, prNumber int) string {
	number := prNumber
	prNumberLen := 0
	for number > 0 {
		prNumberLen++
		number /= 10
	}
	ref = ref[0:min(BranchNameMaxLen-ShaDefaultLen-1-prNumberLen, len(ref))]
	sha = sha[0:ShaDefaultLen]

	return fmt.Sprintf("%s_%d_%s", ref, prNumber, sha)
}

func buildBranchNameWithoutSha(ref, _ string, prNumber int) string {
	number := prNumber
	prNumberLen := 0
	for number > 0 {
		prNumberLen++
		number /= 10
	}
	ref = ref[0:min(BranchNameMaxLen-ShaDefaultLen-1-prNumberLen, len(ref))]

	return fmt.Sprintf("%s_%d", ref, prNumber)
}

func Ptr[T any](v T) *T {
	return &v
}

func GetCommentBody(branchName, commentUUID, status, host string, orgID, projectID, clusterID uint64) string {
	branchName = fmt.Sprintf("[%s](%s)", branchName, BuildBranchURL(host, orgID, projectID, clusterID))
	body := fmt.Sprintf(`[tidbcloud]:%s

**The latest updates on TiDB Serverless Branch**. Learn more about [branching GitHub integration↗︎](https://docs.pingcap.com/tidbcloud/branch-github-integration)
| BranchName | ClusterID | Status | Updated |
| :--- | :------- | :------- | :------ |
| %s | %d | %s | %s | `, commentUUID, branchName, clusterID, status, time.Now().Format(time.RFC3339))

	return body
}

func GetGitConfig(ctx context.Context, g *GitHubApp, event *GitHubPullRequestEvent) *Config {
	owner := event.Repository.Owner.Login
	repo := event.Repository.Name
	action := event.Action
	head := event.PullRequest.Head
	installationID := event.Installation.ID
	number := event.PullRequest.Number

	// get git config
	var gitConfig *Config
	var err error
	gitConfig, err = g.getGitConfig(ctx, installationID, head)
	if err != nil {
		gitConfig = GetDefaultConfig()
		if !strings.Contains(err.Error(), "404 Not Found") {
			tilog.Error(ctx, "get GitConfig error, GitConfig is set to default value", zap.Error(err))
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.GitGetContents).Inc()
		} else {
			tilog.Debug(ctx, "No GitConfig found, GitConfig is set to default value", zap.Error(err))
		}
	}

	// check git config
	if err = gitConfig.CheckGitConfig(); err != nil {
		gitConfig = GetDefaultConfig()
		tilog.Warn(ctx, "invalid GitConfig, GitConfig is set to default value", zap.Error(err))
		err = g.CommentError(ctx, installationID, owner, repo, number, InvalidGitConfig, err.Error(), action)
		if err != nil {
			tilog.Error(ctx, "commentError fail", zap.Error(err))
		}
	}

	if gitConfig == nil {
		gitConfig = GetDefaultConfig()
		tilog.Error(ctx, "GitConfig is null due to unknown reason, set it to default value")
	}
	return gitConfig
}

func GetClusterInfo(ctx context.Context, g *GitHubApp, event *GitHubPullRequestEvent) (_ *ClusterInfo, err error) {
	repository := event.Repository

	connection, err := g.tiDBGitIntegrationDAO.GetGitIntegrationByRepo(ctx, repository.FullName, gitDB.GitHub)
	if err != nil {
		return nil, err
	}
	if connection == nil {
		tilog.Info(ctx, fmt.Sprintf("empty integration, make sure you have connect the repo %s to a TiDB cluster", repository.FullName))
		return nil, nil
	}
	return &ClusterInfo{
		OrgID:     connection.TiDBOrgID,
		ClusterID: connection.TiDBClusterID,
		ProjectId: connection.TiDBProjectID,
	}, nil
}

func BuildBranchURL(host string, orgID, projectID, clusterID uint64) string {
	// backwards compatible
	if orgID == 0 {
		return fmt.Sprintf("%s/console/clusters/%d/branches?projectId=%d", host, clusterID, projectID)
	}
	return fmt.Sprintf("%s/console/clusters/%d/branches?orgId=%d&projectId=%d", host, clusterID, orgID, projectID)
}

func callCreateBranch(ctx context.Context, g *GitHubApp, clusterInfo *ClusterInfo, branchName string, action string) (id *string, text *CheckRunOutputDetail) {
	tilog.Debug(ctx, fmt.Sprintf("create branch %s under cluster %d.", branchName, clusterInfo.ClusterID))
	annotation := map[string]string{BranchAnnotationKey: BranchAnnotationValue}
	id, bizErr := g.BranchSvc.CreateBranch(ctx, clusterInfo.ClusterID, strconv.FormatUint(clusterInfo.ClusterID, 10), branchName, annotation)
	if bizErr != nil {
		tilog.Error(ctx, fmt.Sprintf("Create branch(name: %s) under cluster %d fail", branchName, clusterInfo.ClusterID), zap.Error(bizErr))
		metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.CreateBranch).Inc()
		text := &CheckRunOutputDetail{
			ClusterID:  Ptr(strconv.FormatUint(clusterInfo.ClusterID, 10)),
			BranchName: &branchName,
			Error:      Ptr(bizErr.Error()),
		}
		return nil, text
	}
	tilog.Info(ctx, fmt.Sprintf("create branch(ID: %s) under cluster %d success.", *id, clusterInfo.ClusterID))
	return id, nil
}

func callResetBranch(ctx context.Context, g *GitHubApp, clusterInfo *ClusterInfo, branchID string, branchName string) (text *CheckRunOutputDetail) {
	bizErr := g.BranchSvc.ResetBranch(ctx, clusterInfo.ClusterID, branchID)
	if bizErr != nil {
		tilog.Error(ctx, fmt.Sprintf("Reset branch(name: %s) under cluster %d fail.", branchName, clusterInfo.ClusterID), zap.Error(bizErr))
		text := &CheckRunOutputDetail{
			ClusterID:  Ptr(strconv.FormatUint(clusterInfo.ClusterID, 10)),
			BranchName: &branchName,
			Error:      Ptr(bizErr.Error()),
		}
		return text
	}
	return nil
}
