package github_app

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

	googleGithub "github.com/google/go-github/v50/github"
	"github.com/tidbcloud/ecosystem-service/config"
	"github.com/tidbcloud/ecosystem-service/internal/metric"
	"github.com/tidbcloud/ecosystem-service/internal/service/github"
	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 (
	// BranchStatusCheckInterval BranchStatusCheckTimeout BranchStatusCheckDrop TODO alter the value after branch service's speed is faster
	BranchStatusCheckInterval = 30
	BranchStatusCheckTimeout  = 900
	BranchStatusCheckDrop     = 1800
)

type GitHubAppWorker struct {
	gitHubAppTaskDao gitDB.GitHubAppTaskDAO
	gitHubApp        *github.GitHubApp
	conf             *config.ConfigWrapper
}

func NewGitHubAppWorker(gitHubAppTaskDao gitDB.GitHubAppTaskDAO, gitHubApp *github.GitHubApp, conf *config.ConfigWrapper) *GitHubAppWorker {
	return &GitHubAppWorker{
		gitHubAppTaskDao: gitHubAppTaskDao,
		gitHubApp:        gitHubApp,
		conf:             conf,
	}
}

func (w *GitHubAppWorker) Run(ctx context.Context) (err error) {
	tilog.Info(ctx, "gitHub app worker: start to fetch all pending tasks")
	githubAppTasks, err := w.gitHubAppTaskDao.GetTasksByStatus(ctx, gitDB.Pending)
	if err != nil {
		tilog.Error(ctx, "fetch all pending tasks fail", zap.Error(err))
		return
	}
	tilog.Info(ctx, fmt.Sprintf("gitHub app worker: fetched %d tasks", len(githubAppTasks)))

	timeoutTask := make([]uint64, 0)
	successTask := make([]uint64, 0)
	dropTask := make([]uint64, 0)

	for i, task := range githubAppTasks {

		tilog.Info(ctx, fmt.Sprintf("gitHub app worker: start to handle task: %d", task.ID))

		client, err := w.gitHubApp.GetGitHubClient(ctx, task.InstallationID)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("get GitHub client fail with installID:%d", task.InstallationID), zap.Error(err))
			continue
		}

		orgID := task.TiDBOrgID
		projectID := task.TiDBProjectID
		clusterID := task.TiDBClusterID
		branchID := task.TiDBBranchID
		branchName := task.TiDBBranchName

		owner := task.GitHubOwner
		repo := task.GitHubRepo
		sha := task.GitHubCommitID
		prNumber := task.GitHubPrNumber

		commentUUID := task.GitHubCommentUUID
		branchPreviousStatus := task.TiDBBranchPreviousStatus
		checkRunID := task.GitHubCheckRunID
		action := task.Action

		externalID := &github.ExternalID{
			PullRequestAction: &githubAppTasks[i].Action,
			PullRequestBefore: &githubAppTasks[i].Before,
		}

		// drop the task which fall behind too much time, we will not retry it anymore
		if (task.CreatedAt.Add(time.Second * BranchStatusCheckDrop)).Before(time.Now()) {
			tilog.Info(ctx, fmt.Sprintf("drop task. projectID:%d, clusterID:%d, branchName:%s", projectID, clusterID, branchName))
			dropTask = append(dropTask, task.ID)
			continue
		}

		// update check run to timeout when timeout
		if (task.CreatedAt.Add(time.Second * BranchStatusCheckTimeout)).Before(time.Now()) {
			tilog.Info(ctx, fmt.Sprintf("task timeout. projectID:%d, clusterID:%d, branchName:%s", projectID, clusterID, branchName))
			// update check run
			text := &github.CheckRunOutputDetail{
				ClusterID:  Ptr(strconv.FormatUint(clusterID, 10)),
				BranchName: &branchName,
				Timeout:    Ptr(BranchStatusCheckTimeout),
			}
			summary := fmt.Sprintf(github.WatchBranchStatusTimeout, github.BuildBranchURL(w.conf.ServerConfig.TiDBCloudHost, orgID, projectID, clusterID))
			err = w.gitHubApp.UpdateCheckRun(ctx, task.InstallationID, owner, repo, sha, prNumber, github.Timeout, text, externalID, checkRunID, summary, action)
			if err != nil {
				// update message and retry when we meet unexpected error
				tilog.Error(ctx, "update check run fail", zap.Error(err))
				err = w.gitHubAppTaskDao.UpdateTaskMessage(ctx, task, "updateCheckRun error: "+err.Error())
				if err != nil {
					// db fail, we will record it with metric
					tilog.Error(ctx, "UpdateTaskMessage error", zap.Error(err))
				}
			} else {
				timeoutTask = append(timeoutTask, task.ID)
			}
			continue
		}

		// get branch
		branch, bizErr := w.gitHubApp.BranchSvc.GetBranch(ctx, clusterID, branchID)
		if bizErr != nil {
			tilog.Error(ctx, fmt.Sprintf("get branch fail. clusterID:%d,branchName:%s,branchID:%s", clusterID, branchName, branchID), zap.Error(err))
			if err := w.gitHubAppTaskDao.UpdateTaskMessage(ctx, task, "get branch fail: "+bizErr.Error()); err != nil {
				tilog.Error(ctx, "UpdateTaskMessage error", zap.Error(err))
			}

			// not retry for 4xx or 6xx
			if bizerr.IsBusinessErr(bizErr.ErrCode()) || bizerr.IsRequestErr(bizErr.ErrCode()) {
				summary := fmt.Sprintf(github.GetBranchStatusFail, github.BuildBranchURL(w.conf.ServerConfig.TiDBCloudHost, orgID, projectID, clusterID))
				text := &github.CheckRunOutputDetail{
					ClusterID:  Ptr(strconv.FormatUint(clusterID, 10)),
					BranchName: &branchName,
					Error:      Ptr(bizErr.Error()),
				}
				err = w.gitHubApp.UpdateCheckRun(ctx, task.InstallationID, owner, repo, sha, prNumber, github.Fail, text, externalID, checkRunID, summary, action)
				if err != nil {
					tilog.Error(ctx, "update check run fail", zap.Error(err))
				} else {
					dropTask = append(dropTask, task.ID)
				}
			}
			continue
		}

		// update comment if status changed
		status := branch.State
		if status.String() != branchPreviousStatus {
			// update comment message
			err = updateComment(ctx, client, owner, repo, prNumber, branchName, status.String(), commentUUID, orgID, projectID, clusterID, w.conf.ServerConfig.TiDBCloudHost, action, w.conf.GitConfig.GitHubAppConfig.Name)
			if err != nil {
				// update message and retry when we meet unexpected error
				tilog.Error(ctx, "update comment fail", zap.Error(err))
				err := w.gitHubAppTaskDao.UpdateTaskMessage(ctx, task, "update comment fail: "+err.Error())
				if err != nil {
					// db fail, we will record it with metric
					tilog.Error(ctx, "UpdateTaskMessage error", zap.Error(err))
				}
				continue
			}

			// Keep the process going when update previous status fail. Because it will not affect the update of GitHub comment and check run.
			err = w.gitHubAppTaskDao.UpdateBranchPreviousStatus(ctx, task, status.String())
			if err != nil {
				tilog.Error(ctx, "update comment previous status fail", zap.Error(err))
			}
		}

		// update check run if it is final status
		if w.gitHubApp.BranchSvc.IsFinalStatus(status) {
			conclusion := github.Success
			summary := github.Succeed
			var text *github.CheckRunOutputDetail
			if status == rpc.BranchStatus_ACTIVE {
				externalID = &github.ExternalID{
					ClusterID:  strconv.FormatUint(clusterID, 10),
					BranchID:   branchID,
					ProjectID:  strconv.FormatUint(projectID, 10),
					BranchName: branchName,
				}
			} else {
				conclusion = github.Fail
				summary = github.BranchStatusUnReady
				text = &github.CheckRunOutputDetail{
					ClusterID:    Ptr(strconv.FormatUint(clusterID, 10)),
					BranchName:   &branchName,
					BranchStatus: Ptr(status.String()),
				}
			}
			err = w.gitHubApp.UpdateCheckRun(ctx, task.InstallationID, owner, repo, sha, prNumber, conclusion, text, externalID, checkRunID, summary, action)
			if err != nil {
				// update message and retry when we meet unexpected error
				tilog.Error(ctx, "update check run fail", zap.Error(err))
				err = w.gitHubAppTaskDao.UpdateTaskMessage(ctx, task, "updateCheckRun error: "+err.Error())
				if err != nil {
					// db fail, we will record it with metric
					tilog.Error(ctx, "UpdateTaskMessage error", zap.Error(err))
				}
			} else {
				// need to update task status
				successTask = append(successTask, task.ID)
			}
			continue
		}
	}

	// update task status
	if len(dropTask) > 0 {
		tilog.Info(ctx, fmt.Sprintf("drop tasks. taskIDS:%+v", dropTask))
		err = w.gitHubAppTaskDao.UpdateTasksStatus(ctx, gitDB.Dropped, dropTask)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("update tasks status error. status: %s, taskIDS:%+v", gitDB.Dropped, dropTask), zap.Error(err))
		}
	}
	if len(successTask) > 0 {
		tilog.Info(ctx, fmt.Sprintf("success tasks. taskIDS:%+v", successTask))
		err = w.gitHubAppTaskDao.UpdateTasksStatus(ctx, gitDB.Completed, successTask)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("update tasks status error. status: %s, taskIDS:%+v", gitDB.Completed, successTask), zap.Error(err))
		}
	}
	if len(timeoutTask) > 0 {
		tilog.Info(ctx, fmt.Sprintf("timeout tasks. taskIDS:%+v", timeoutTask))
		err = w.gitHubAppTaskDao.UpdateTasksStatus(ctx, gitDB.Timeout, timeoutTask)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("update tasks status error. status: %s, taskIDS:%+v", gitDB.Timeout, timeoutTask), zap.Error(err))
		}
	}
	tilog.Info(ctx, "github app worker finish")
	return err
}

func updateComment(ctx context.Context, githubClient *googleGithub.Client, owner, repo string, prNumber int, branchName, status, commentUUID string, orgID, projectID, clusterID uint64, host string, action string, name string) (err error) {
	tilog.Debug(ctx, fmt.Sprintf("update comment for %s on pr:%d. status: %s", owner+repo, prNumber, status))

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

	body := github.GetCommentBody(branchName, commentUUID, status, host, orgID, projectID, clusterID)

	// find the comment
	issueComments, _, err := githubClient.Issues.ListComments(ctx, owner, repo, prNumber, nil)
	if err != nil {
		return
	}
	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
		}
	}

	// create comment if not found
	if issueComment == nil {
		tilog.Warn(ctx, "can not find github comment, maybe it has been deleted manually")
		_, _, err = githubClient.Issues.CreateComment(ctx, owner, repo, prNumber, &googleGithub.IssueComment{
			Body: Ptr(body),
		})
	} else if strings.Contains(*issueComment.Body, commentUUID) {
		// update comment if commentUUID is the same
		_, _, err = githubClient.Issues.EditComment(ctx, owner, repo, *issueComment.ID, &googleGithub.IssueComment{
			Body: Ptr(body),
		})
	}
	return
}

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

func (w *GitHubAppWorker) GetInterval() time.Duration {
	return time.Second * BranchStatusCheckInterval
}

func (w *GitHubAppWorker) GetName() string {
	return "GitHubAppWorker"
}
