package git

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	gogithub "github.com/google/go-github/v50/github"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/ecosystem-service/api/auth"
	"github.com/tidbcloud/ecosystem-service/config"
	"github.com/tidbcloud/ecosystem-service/internal/metric"
	"github.com/tidbcloud/ecosystem-service/internal/service/github"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/account"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/mgmt"
	gitDB "github.com/tidbcloud/ecosystem-service/internal/storage/db/git"
	"github.com/tidbcloud/pkgv2/tracing"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	mgmtapi "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

var Tracer = otel.Tracer("git_controller")

const (
	GitHubAccessTokenHeader  = "GITHUB_ACCESS_TOKEN"
	GitHubRefreshTokenHeader = "GITHUB_REFRESH_TOKEN"
)

type Controller struct {
	tiDBGitIntegrationDAO gitDB.TiDBGitIntegrationDAO
	accountProviderSvc    account.AccountProviderSvc
	mgmtSvc               mgmt.MgmtSvc
	githubApp             *github.GitHubApp
	conf                  *config.ConfigWrapper
}

func NewController(tiDBGitIntegrationDAO gitDB.TiDBGitIntegrationDAO, accountProviderSvc account.AccountProviderSvc,
	mgmtSvc mgmt.MgmtSvc, githubApp *github.GitHubApp, conf *config.ConfigWrapper) *Controller {
	return &Controller{
		tiDBGitIntegrationDAO: tiDBGitIntegrationDAO,
		accountProviderSvc:    accountProviderSvc,
		mgmtSvc:               mgmtSvc,
		githubApp:             githubApp,
		conf:                  conf,
	}
}

//	@title			Git Swagger API
//	@version		1.0
//	@termsOfService	http://swagger.io/terms/

//	@host		localhost:8080
//	@BasePath	/ecosystem/api/v1
//	@schemes	http

func (c *Controller) GitHubWebHook(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "git.GitHubWebHook")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	g := c.githubApp
	// verify webhook signature
	payload, err := g.ValidatePayload(ctx)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized: fail to verify webhook signature"})
		tilog.Warn(ctxReq, "validatePayload error", zap.Error(err))
		return
	}

	// parse payload
	githubEvent, err := github.UnmarshallPayload(payload)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"message": "fail to unmarshallPayload"})
		tilog.Error(ctxReq, "unmarshallPayload error", zap.Error(err))
		return
	}

	// authenticated as an installation of a GitHub App. And the sdk will rotation the token automatically
	_, err = g.GetGitHubClient(ctxReq, githubEvent.Installation.ID)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized: fail to get GitHub client error"})
		tilog.Warn(ctxReq, "get GitHub client error", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("event_type", ctx.GetHeader("X-GITHUB-EVENT")), attribute.String("action", githubEvent.Action))
	tilog.Debug(ctxReq, fmt.Sprintf("received event %s. action: %s", ctx.GetHeader("X-GITHUB-EVENT"), githubEvent.Action))

	span.AddEvent("handle event")
	// handle event
	switch ctx.GetHeader("X-GITHUB-EVENT") {
	case "pull_request":
		githubEventPullRequest, err := github.UnmarshallPayloadToPullRequest(payload)
		if err != nil {
			tilog.Error(ctxReq, "unmarshallPayloadToPullRequest error", zap.Error(err))
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, metric.UnknownAction, metric.UnmarshallPayload).Inc()
			return
		}
		err = g.HandlePullRequest(ctxReq, githubEventPullRequest, false)
		if err != nil {
			tilog.Error(ctxReq, "handle pull request error", zap.Error(err))
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
			return
		}
	case "check_run":
		if githubEvent.Action == "rerequested" {
			checkRunEvent, err := github.UnmarshallPayloadToCheckRun(payload)
			if err != nil {
				tilog.Error(ctxReq, "unmarshallPayloadToCheckRun error", zap.Error(err))
				ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				metric.GitHubAppErrorCounter.WithLabelValues(metric.CheckRunEvent, metric.UnknownAction, metric.UnmarshallPayload).Inc()
				return
			}
			if err = g.HandleReRequestedCheckRun(ctxReq, checkRunEvent); err != nil {
				tilog.Error(ctxReq, "handle rerun check run error", zap.Error(err))
				ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
				return
			}
		}
	}
	ctx.JSON(http.StatusOK, "success")
}

// AddGitConnections godoc
//
//	@Description	Add Connections between TIDB Cloud cluster and Git.
//	@Accept			json
//	@Produce		json
//	@Param			org_id		path		string					true	"TiDB Org ID"
//	@Param			project_id	path		string					true	"TiDB Project ID"
//	@Param			cluster_id	path		string					true	"TiDB Cluster ID"
//	@Param			git			body		AddConnectionRequest	true	"Git information"
//	@Success		200			{object}	AddConnectionResponse	"ok"
//	@Failure		400			{object}	BaseResp				"error"
//	@Failure		500			{object}	BaseResp				"error"
//	@Router			/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/git/connections [post]
func (c *Controller) AddGitConnections(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	var orgID, projectID, clusterID uint64
	var repo string
	ctxReq, span := Tracer.Start(ctxReq, "git.AddGitConnections")
	defer func() {
		tracing.EndSpan(span, err)
		if c.conf.ServerConfig.EnableAuditLog {
			go submitBranchAudit(ctx, auditlog.EventConnectBranchGitHub, orgID, projectID, clusterID, &repo, err)
		}
	}()

	var path TiDBCluster
	if err = ctx.ShouldBindUri(&path); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required TiDB cluster information"))
		tilog.Error(ctxReq, "Missing required TiDB cluster information", zap.Error(err))
		return
	}
	orgID, projectID, clusterID, err = parseUintIDForTiDBCluster(path)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Invalid TiDB cluster identifier, all types should be uint64"))
		tilog.Error(ctxReq, "Invalid TiDB cluster identifier", zap.Error(err))
		return
	}

	user, err := auth.GetUserFromContext(ctx)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, NewBaseResp(errs.ErrCodeAuth, "Invalid user"))
		tilog.Error(ctxReq, "Can't get user from context", zap.Error(err))
		return
	}

	var req AddConnectionRequest
	if err = ctx.ShouldBindJSON(&req); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required git information"))
		tilog.Error(ctxReq, "Missing required git information", zap.Error(err))
		return
	}

	if err = req.check(); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, err.Error()))
		tilog.Error(ctxReq, err.Error(), zap.Error(err))
		return
	}

	repo = req.RepoName
	span.SetAttributes(attribute.String("tidb_org_id", strconv.FormatUint(orgID, 10)), attribute.String("tidb_project_id", strconv.FormatUint(projectID, 10)), attribute.String("tidb_cluster_id", strconv.FormatUint(clusterID, 10)), attribute.String("repo_name", repo), attribute.String("user_email", user.Email))
	tilog.Info(ctxReq, "Start to add git connection", zap.Uint64("TiDBOrgID", orgID), zap.Uint64("TiDBProjectID", projectID), zap.Uint64("TiDBClusterID", clusterID), zap.String("GitProvider", req.GitProvider), zap.String("RepoName", repo), zap.String("userEmail", user.Email))

	span.AddEvent("validate cluster")
	// check cluster
	cluster, err := c.mgmtSvc.GetClusterIncludeDedicated(ctxReq, clusterID, mgmtapi.ClusterFilter_CLUSTER_VIEW_BASIC)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Check TiDB Cloud Cluster failed. Maybe the cluster is not exist"))
		tilog.Error(ctxReq, "Get TiDB Cloud Cluster Information failed", zap.Error(err))
		return
	}
	if cluster.DeployType != mgmtapi.DeployType_DEPLOY_TYPE_SERVERLESS {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Only TiDB Serverless cluster can be connected to Git"))
		tilog.Error(ctxReq, "Only TiDB Serverless cluster can be connected to Git", zap.Error(err))
		return
	}
	if cluster.GetServerlessCluster().GetUserStatus() == globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_DELETED {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "TiDB Cloud Cluster has been deleted"))
		tilog.Warn(ctxReq, fmt.Sprintf("Cluster is to be deleted. clusterID: %s", path.TiDBClusterID))
		err = errors.New("TiDB Cloud Cluster has been deleted")
		return
	}

	span.AddEvent("check if connection exist")
	// check connection
	gitIntegration, err := c.tiDBGitIntegrationDAO.GetGitIntegrationByClusterID(ctxReq, clusterID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Can't check if connection exists"))
		tilog.Error(ctxReq, "Check connection if exist failed", zap.Error(err))
		return
	}
	if gitIntegration != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeRecordExist, "Cluster has been connected"))
		tilog.Info(ctxReq, "Connection already exists")
		err = errors.New("TiDB Cloud Cluster has been connected")
		return
	}

	span.AddEvent("check if repo has been connected")
	gitIntegration2, err := c.tiDBGitIntegrationDAO.GetGitIntegrationByRepo(ctxReq, repo, gitDB.GitProvider(req.GitProvider))
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Can't check if connection exists"))
		tilog.Error(ctxReq, "Check connection if exist failed", zap.Error(err))
		return
	}
	if gitIntegration2 != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeConflict, "Repositories has been connected"))
		tilog.Info(ctxReq, "Repositories has been connected")
		err = errors.New("GitHub repository has been connected")
		return
	}

	connection := gitDB.TiDBGitConnection{
		RepoName:      repo,
		TiDBProjectID: projectID,
		TiDBClusterID: clusterID,
		TiDBOrgID:     orgID,
		GitProvider:   gitDB.GitProvider(req.GitProvider),
	}

	span.AddEvent("save connection to db")
	newIntegrate, err := c.tiDBGitIntegrationDAO.AddGitIntegration(ctxReq, connection)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Save Git connection failed"))
		tilog.Error(ctxReq, "Save Git connection failed", zap.Error(err))
		return
	}

	tilog.Info(ctxReq, "Add connection between Git and TiDB cluster successfully.")
	ctx.JSON(200, AddConnectionResponse{ID: newIntegrate.ID})
}

// DeleteGitConnections godoc
//
//	@Description	Delete Connections between TIDB Cloud cluster and Git.
//	@Accept			json
//	@Produce		json
//	@Param			org_id		path		string		true	"TiDB Org ID"
//	@Param			project_id	path		string		true	"TiDB Project ID"
//	@Param			cluster_id	path		string		true	"TiDB Cluster ID"
//	@Success		200			{object}	BaseResp	"ok"
//	@Failure		400			{object}	BaseResp	"error"
//	@Failure		500			{object}	BaseResp	"error"
//	@Router			/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/git/connections [delete]
func (c *Controller) DeleteGitConnections(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	var orgID, projectID, clusterID uint64
	ctxReq, span := Tracer.Start(ctxReq, "git.DeleteGitConnections")
	defer func() {
		tracing.EndSpan(span, err)
		if c.conf.ServerConfig.EnableAuditLog {
			go submitBranchAudit(ctx, auditlog.EventDisconnectBranchGitHub, orgID, projectID, clusterID, nil, err)
		}
	}()

	var path TiDBCluster
	if err = ctx.ShouldBindUri(&path); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required TiDB cluster information"))
		tilog.Error(ctxReq, "Missing required TiDB cluster information", zap.Error(err))
		return
	}
	orgID, projectID, clusterID, err = parseUintIDForTiDBCluster(path)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Invalid TiDB cluster identifier, all types should be uint64"))
		tilog.Error(ctxReq, "Invalid TiDB cluster identifier", zap.Error(err))
		return
	}

	user, err := auth.GetUserFromContext(ctx)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, NewBaseResp(errs.ErrCodeAuth, "Invalid user"))
		tilog.Error(ctxReq, "Can't get user from context", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("tidb_org_id", strconv.FormatUint(orgID, 10)), attribute.String("tidb_project_id", strconv.FormatUint(projectID, 10)), attribute.String("tidb_cluster_id", strconv.FormatUint(clusterID, 10)), attribute.String("user_email", user.Email))
	tilog.Info(ctxReq, "Start to delete git connection", zap.Uint64("TiDBOrgID", orgID), zap.Uint64("TiDBProjectID", projectID), zap.Uint64("TiDBClusterID", clusterID), zap.String("userEmail", user.Email))

	span.AddEvent("delete connection from db")
	rowsAffected, err := c.tiDBGitIntegrationDAO.DeleteGitIntegrationByClusterID(ctxReq, clusterID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, err.Error()))
		tilog.Error(ctxReq, "Delete Git connection failed", zap.Error(err))
		return
	}
	if rowsAffected == 0 {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeRecordNotFound, "This connection does not exists"))
		tilog.Info(ctxReq, "This connection does not exists")
		err = errors.New("Connection does not exists")
		return
	}

	message := "Delete connection between git and TiDB cluster successfully."
	tilog.Info(ctxReq, message)
	ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeOK, message))
}

// ListGitConnections godoc
//
//	@Description	List Connections between TIDB Cloud cluster and Git.
//	@Accept			json
//	@Produce		json
//	@Param			org_id		path		string					true	"TiDB Org ID"
//	@Param			project_id	path		string					true	"TiDB Project ID"
//	@Success		200			{object}	ListConnectionResponse	"ok"
//	@Failure		400			{object}	BaseResp				"error"
//	@Failure		500			{object}	BaseResp				"error"
//	@Router			/orgs/{org_id}/projects/{project_id}/git/connections [get]
func (c *Controller) ListGitConnections(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "git.ListGitConnections")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	var path TiDBProject
	if err = ctx.ShouldBindUri(&path); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required TiDB project information"))
		tilog.Error(ctxReq, "Missing required TiDB cluster information", zap.Error(err))
		return
	}

	orgID, projectID, err := parseUintIDForTiDBProject(path)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Invalid TiDB Cloud identifier, all types should be uint64"))
		tilog.Error(ctxReq, "Invalid TiDB Cloud identifier", zap.Error(err))
		return
	}

	user, err := auth.GetUserFromContext(ctx)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, NewBaseResp(errs.ErrCodeAuth, "Invalid user"))
		tilog.Error(ctxReq, "Can't get user from context", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("tidb_org_id", strconv.FormatUint(orgID, 10)), attribute.String("tidb_project_id", strconv.FormatUint(projectID, 10)), attribute.String("user_email", user.Email))
	tilog.Info(ctxReq, "Start to list git connection", zap.Uint64("TiDBOrgID", orgID), zap.Uint64("TiDBProjectID", projectID), zap.String("userEmail", user.Email))

	span.AddEvent("list connection from db")
	gitConnections, err := c.tiDBGitIntegrationDAO.ListGitIntegrationByProjectID(ctxReq, projectID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "List Git connection failed"))
		tilog.Error(ctxReq, "List Git connection failed", zap.Error(err))
		return
	}

	span.AddEvent("build response")
	// build response
	listConnectionResponse, err := c.buildListConnectionResponse(ctxReq, gitConnections)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "build ListConnectionResponse failed"))
		tilog.Error(ctxReq, "Build ListConnectionResponse failed", zap.Error(err))
		return
	}
	ctx.JSON(200, listConnectionResponse)
}

// GetGitConnections godoc
//
//	@Description	List Connections between TIDB Cloud cluster and Git.
//	@Accept			json
//	@Produce		json
//	@Param			org_id		path		string					true	"TiDB Org ID"
//	@Param			project_id	path		string					true	"TiDB Project ID"
//	@Param			cluster_id	path		string					true	"TiDB Cluster ID"
//	@Success		200			{object}	GetConnectionResponse	"ok"
//	@Failure		400			{object}	BaseResp				"error"
//	@Failure		500			{object}	BaseResp				"error"
//	@Router			/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/git/connections [get]
func (c *Controller) GetGitConnections(ctx *gin.Context) {
	var err error
	ctxReq, span := Tracer.Start(ctx.Request.Context(), "git.GetGitConnections")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	var path TiDBCluster
	if err := ctx.ShouldBindUri(&path); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required TiDB cluster information"))
		tilog.Error(ctxReq, "Missing required TiDB cluster information", zap.Error(err))
		return
	}
	orgID, projectID, clusterID, err := parseUintIDForTiDBCluster(path)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Invalid TiDB cluster identifier, all types should be uint64"))
		tilog.Error(ctxReq, "Invalid TiDB cluster identifier", zap.Error(err))
		return
	}

	user, err := auth.GetUserFromContext(ctx)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, NewBaseResp(errs.ErrCodeAuth, "Invalid user"))
		tilog.Error(ctxReq, "Can't get user from context", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("tidb_org_id", strconv.FormatUint(orgID, 10)), attribute.String("tidb_project_id", strconv.FormatUint(projectID, 10)), attribute.String("tidb_cluster_id", strconv.FormatUint(clusterID, 10)), attribute.String("user_email", user.Email))
	tilog.Info(ctxReq, "Start to get git connection", zap.Uint64("TiDBOrgID", orgID), zap.Uint64("TiDBProjectID", projectID), zap.Uint64("TiDBClusterID", clusterID), zap.String("userEmail", user.Email))
	span.AddEvent("get git connection from db")

	gitConnection, err := c.tiDBGitIntegrationDAO.GetGitIntegrationByClusterID(ctxReq, clusterID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Get Git connection failed"))
		tilog.Error(ctxReq, "Get Git connection failed", zap.Error(err))
		return
	}
	if gitConnection == nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeRecordNotFound, "Record not find"))
		return
	}

	// build response
	resp := &GetConnectionResponse{
		Connection: &TiDBGitConnectionVO{
			TiDBClusterID: strconv.FormatUint(gitConnection.TiDBClusterID, 10),
			GitProvider:   string(gitConnection.GitProvider),
			RepoName:      gitConnection.RepoName,
		},
	}
	ctx.JSON(200, resp)
}

func (c *Controller) buildListConnectionResponse(ctx context.Context, gitConnections []*gitDB.TiDBGitConnection) (*ListConnectionResponse, error) {
	listConnectionResponse := &ListConnectionResponse{
		Connections: make([]*TiDBGitConnectionVO, 0, len(gitConnections)),
	}

	for _, connection := range gitConnections {
		cluster, err := c.mgmtSvc.GetCluster(ctx, connection.TiDBClusterID, mgmtapi.ClusterFilter_CLUSTER_VIEW_BASIC)
		if err != nil {
			return nil, err
		}
		if cluster.GetServerlessCluster().GetUserStatus() == globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_DELETED {
			tilog.Warn(ctx, fmt.Sprintf("Cluster is to be deleted. clusterID: %d", connection.TiDBClusterID))
			continue
		}

		tiDBGitConnectionVO := &TiDBGitConnectionVO{
			TiDBClusterID:   strconv.FormatUint(connection.TiDBClusterID, 10),
			GitProvider:     string(connection.GitProvider),
			RepoName:        connection.RepoName,
			TiDBClusterName: cluster.ClusterName,
		}
		listConnectionResponse.Connections = append(listConnectionResponse.Connections, tiDBGitConnectionVO)
	}
	return listConnectionResponse, nil
}

// GetGitHubRepoV2 godoc
//
//	@Description	List repositories that the TiDB Cloud App installation can access with a safer way. installation_id is not reliable, we need to judge whether the installation_id belongs to the user who provided the access_token. Please pass GITHUB_ACCESS_TOKEN and GITHUB_REFRESH_TOKEN with header.
//	@Accept			json
//	@Produce		json
//	@Param			installation_id	query		string					true	"GitHub account installation_id"
//	@Param			repo_name		query		string					false	"GitHub repository name"
//	@Param			page			query		int						false	"page"
//	@Param			per_page		query		int						false	"per_page"
//	@Success		200				{object}	GetGitHubRepoResponse	"ok"
//	@Failure		400				{object}	BaseResp				"error"
//	@Failure		500				{object}	BaseResp				"error"
//	@Router			/integration/github/search-repo-v2 [get]
func (c *Controller) GetGitHubRepoV2(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "git.GetGitHubRepo")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	var query GetGitHubRepoRequestV2
	if err := ctx.ShouldBindQuery(&query); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required param"))
		tilog.Error(ctxReq, "Missing required param", zap.Error(err))
		return
	}

	if ctx.GetHeader(GitHubAccessTokenHeader) != "" {
		query.AccessToken = ctx.GetHeader(GitHubAccessTokenHeader)
	}
	if ctx.GetHeader(GitHubRefreshTokenHeader) != "" {
		query.RefreshToken = ctx.GetHeader(GitHubRefreshTokenHeader)
	}

	if query.AccessToken == "" {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing access token"))
		return
	}

	span.SetAttributes(attribute.String("installation_id", strconv.FormatInt(query.InstallationID, 10)), attribute.String("repo_name", query.RepoName))
	tilog.Info(ctxReq, "Start to get GitHub repo", zap.Int64("InstallID", query.InstallationID), zap.String("RepoName", query.RepoName))

	span.AddEvent("check installation_id belongs to user")
	installations, _, baseResp := c.getUserInstalledAccountsWithToken(ctxReq, query.AccessToken, query.RefreshToken)
	if baseResp != nil {
		ctx.JSON(http.StatusOK, baseResp)
		return
	}
	installationFound := false
	for _, installation := range installations {
		if *installation.ID == query.InstallationID {
			installationFound = true
			break
		}
	}
	if !installationFound {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "no permission to access this installation_id"))
		return
	}

	span.AddEvent("list repos from github")
	repositories, err := c.githubApp.ListRepositories(ctxReq, query.InstallationID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "List Repositories failed"))
		tilog.Error(ctxReq, "List Repositories failed", zap.Error(err))
		return
	}

	span.AddEvent("build response")
	// build repositoriesVO
	repositoriesVO := make([]*RepositoriesVO, 0, 10)
	count := 0
	for _, repo := range repositories {
		if query.RepoName != "" && !strings.Contains(strings.ToLower(*repo.Name), strings.ToLower(query.RepoName)) {
			continue
		}
		repositoriesVO = append(repositoriesVO, &RepositoriesVO{
			ID:       *repo.ID,
			Name:     *repo.Name,
			FullName: *repo.FullName,
		})
		count++
		if count >= 10 {
			break
		}
	}

	GetGitHubRepoResponse := &GetGitHubRepoResponse{
		Total:        len(repositoriesVO),
		Repositories: repositoriesVO,
	}

	ctx.JSON(200, GetGitHubRepoResponse)
}

// ExchangeGitHubToken godoc
//
//	@Description	Exchange GitHub token with code. See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token
//	@Accept			json
//	@Produce		json
//	@Param			code	query		string						true	"The code you received in the previous step."
//	@Success		200		{object}	ExchangeGitHubTokenResponse	"ok"
//	@Failure		400		{object}	BaseResp					"error"
//	@Failure		500		{object}	BaseResp					"error"
//	@Router			/integration/github/exchange-token [get]
func (c *Controller) ExchangeGitHubToken(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "git.ExchangeGitHubToken")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	var query ExchangeGitHubTokenRequest
	if err = ctx.ShouldBindQuery(&query); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required param"))
		tilog.Error(ctxReq, "Missing required param", zap.Error(err))
		return
	}

	tilog.Info(ctxReq, "Start to get GitHub token", zap.String("code", query.Code))
	token, err := c.githubApp.GetUserAccessToken(ctxReq, query.Code)
	if err != nil {
		if _, ok := errors.Cause(err).(*github.UnauthorizedError); ok {
			ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeUnauthorized, "Bad verification code"))
			tilog.Error(ctxReq, "Bad verification code", zap.Error(err))
			return
		}
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "GetUserAccessToken failed"))
		tilog.Error(ctxReq, "GetUserAccessToken failed", zap.Error(err))
		return
	}

	resp := &ExchangeGitHubTokenResponse{
		AccessToken:  token.AccessToken,
		RefreshToken: token.RefreshToken,
	}

	ctx.JSON(200, resp)
}

// GetGitHubInstallAccounts godoc
//
//	@Description	Get TiDB Cloud App installation information, please pass GITHUB_ACCESS_TOKEN and GITHUB_REFRESH_TOKEN with header. We may refresh token if access_token is expired and refresh_token is passed.
//	@Accept			json
//	@Produce		json
//
//	@Success		200	{object}	GetGitHubInstallAccountsResponse	"ok"
//	@Failure		400	{object}	BaseResp							"error"
//	@Failure		500	{object}	BaseResp							"error"
//	@Router			/integration/github/accounts [get]
func (c *Controller) GetGitHubInstallAccounts(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "git.GetGitHubInstallAccounts")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	var query GetGitHubInstallAccountsRequest
	if err = ctx.ShouldBindQuery(&query); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required param"))
		tilog.Error(ctxReq, "Missing required param", zap.Error(err))
		return
	}

	if ctx.GetHeader(GitHubAccessTokenHeader) != "" {
		query.AccessToken = ctx.GetHeader(GitHubAccessTokenHeader)
	}
	if ctx.GetHeader(GitHubRefreshTokenHeader) != "" {
		query.RefreshToken = ctx.GetHeader(GitHubRefreshTokenHeader)
	}
	if query.AccessToken == "" {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing access token"))
	}

	tilog.Info(ctxReq, "Start to get installed accounts", zap.String("token", query.AccessToken), zap.String("refreshToken", query.RefreshToken))

	span.AddEvent("get user installed accounts")
	getGitHubInstallAccountsResponse := &GetGitHubInstallAccountsResponse{}
	installations, token, baseResp := c.getUserInstalledAccountsWithToken(ctxReq, query.AccessToken, query.RefreshToken)
	if baseResp != nil {
		ctx.JSON(http.StatusOK, baseResp)
		return
	}
	if token != nil {
		getGitHubInstallAccountsResponse.AccessToken = token.AccessToken
		getGitHubInstallAccountsResponse.RefreshToken = token.RefreshToken
	}
	accountsVO := make([]*AccountsVO, 0, len(installations))
	for _, install := range installations {
		accountsVO = append(accountsVO, &AccountsVO{
			Name:           *install.Account.Login,
			InstallationID: *install.ID,
			OwnerType:      *install.Account.Type,
		})
	}
	getGitHubInstallAccountsResponse.Accounts = accountsVO

	ctx.JSON(200, getGitHubInstallAccountsResponse)
}

func (c *Controller) getUserInstalledAccountsWithToken(ctx context.Context, accessToken, refreshToken string) ([]*gogithub.Installation, *github.UserAccessToken, *BaseResp) {
	installations, err := c.githubApp.GetUserInstalledAccounts(ctx, accessToken)
	var token *github.UserAccessToken
	if err != nil {
		if _, ok := errors.Cause(err).(*github.UnauthorizedError); ok {
			if refreshToken == "" {
				tilog.Error(ctx, "Bad credentials", zap.Error(err))
				return nil, nil, NewBaseResp(errs.ErrCodeUnauthorized, "Bad credentials")
			} else {
				// try to refresh token
				tilog.Warn(ctx, "user access token is invalid, try to refresh it")
				userAccessToken, err := c.githubApp.RefreshUserAccessToken(ctx, refreshToken)
				if err != nil {
					tilog.Error(ctx, "Refresh token failed", zap.Error(err))
					return nil, nil, NewBaseResp(errs.ErrCodeUnauthorized, "Refresh token failed "+err.Error())
				}
				token = userAccessToken
				installations, err = c.githubApp.GetUserInstalledAccounts(ctx, userAccessToken.AccessToken)
				if err != nil {
					tilog.Error(ctx, "Get installed accounts failed after refresh token", zap.Error(err))
					return nil, nil, NewBaseResp(errs.ErrCodeUnauthorized, "Get installed accounts failed after refresh token "+err.Error())
				}
			}
		} else {
			tilog.Error(ctx, "Get installed accounts failed", zap.Error(err))
			return nil, nil, NewBaseResp(errs.ErrCodeInternalServerError, "Get installed accounts failed")
		}
	}
	return installations, token, nil
}

func submitBranchAudit(ctx *gin.Context, event auditlog.EventType, orgID, projectID, clusterID uint64, repo *string, err error) {
	/* set web console auditlog */
	detail := auditlog.Details{
		auditlog.KeyError: err,
	}
	if repo != nil {
		detail[auditlog.KeyGitHubRepoName] = *repo
	}
	eventBuilder := auditlog.NewEvent(ctx.Request.Context(), event).
		ParseGIN(ctx).
		SetResult(err == nil).
		SetProjectID(projectID).
		SetOrgID(orgID).
		SetClusterID(clusterID).
		SetDetails(detail.Build())
	if e := eventBuilder.Submit(); e != nil {
		tilog.Error(ctx, "submit audit log failed", zap.Error(e))
	}

}
