package vercel_webhook

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/tidbcloud/ecosystem-service/api/v1/vercel"
	"github.com/tidbcloud/ecosystem-service/config"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/branch"
	vercelProxy "github.com/tidbcloud/ecosystem-service/internal/service/proxy/vercel"
	vercelDB "github.com/tidbcloud/ecosystem-service/internal/storage/db/vercel"
	"github.com/tidbcloud/ecosystem-service/internal/utils"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/rpc"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	BranchStatusCheckInterval = 15
	BranchStatusCheckTimeout  = 600
)

type VercelWebhookWorker struct {
	branchLogic     *vercel.BranchLogic
	branchDAO       vercelDB.BranchTaskDAO
	branchSvc       branch.BranchSvc
	vercelAPIClient vercelProxy.APIClient
	checkDAO        vercelDB.CheckDAO
	conf            *config.ConfigWrapper
	clusterConnDAO  vercelDB.ClusterConnectionDAO
	branchEnvIDDAO  vercelDB.BranchEnvironmentIdentifierDAO
}

func NewVercelWebhookWorker(branchDAO vercelDB.BranchTaskDAO,
	branchLogic *vercel.BranchLogic,
	branchSvc branch.BranchSvc,
	vercelAPIClient vercelProxy.APIClient,
	checkDAO vercelDB.CheckDAO,
	conf *config.ConfigWrapper,
	clusterConnDAO vercelDB.ClusterConnectionDAO,
	branchEnvIDDAO vercelDB.BranchEnvironmentIdentifierDAO,
) *VercelWebhookWorker {
	return &VercelWebhookWorker{
		branchDAO:       branchDAO,
		branchLogic:     branchLogic,
		branchSvc:       branchSvc,
		vercelAPIClient: vercelAPIClient,
		checkDAO:        checkDAO,
		conf:            conf,
		clusterConnDAO:  clusterConnDAO,
		branchEnvIDDAO:  branchEnvIDDAO,
	}
}

func (w *VercelWebhookWorker) Run(ctx context.Context) (err error) {
	ctx = logid.WithLogid(ctx, logid.GenLogid())
	logger := tilog.WithContextL(ctx).With(zap.String("worker", w.GetName()))

	logger.Info("vercel webhook worker: start to fetch all pending tasks")
	branchTasks, err := w.branchDAO.GetTasksByStatus(ctx, vercelDB.Pending)
	if err != nil {
		logger.Error("fetch all pending tasks fail", zap.Error(err))
		return
	}
	logger.Info(fmt.Sprintf("vercel webhook worker: fetched %d tasks", len(branchTasks)))

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

	for _, task := range branchTasks {
		taskLogger := logger.With(zap.Uint64("task_id", task.ID)).With(zap.String("deployment_id", task.DeploymentID)).
			With(zap.String("deployment_name", task.DeploymentName)).With(zap.String("vercel_project_id", task.VercelProjectID))
		taskLogger.Info("start to handle task")

		conn, err := w.clusterConnDAO.GetVercelConnectionByVercelProjectID(ctx, task.VercelProjectID)
		if err != nil {
			taskLogger.Error("failed to get vercel connection", zap.Error(err))
			continue
		}
		if conn == nil {
			dropTask = append(dropTask, task.ID)
			taskLogger.Error("vercel connection not found")
			continue
		}

		check, err := w.checkDAO.GetCheckByDeploymentID(ctx, task.DeploymentID)
		if err != nil {
			taskLogger.Error("failed to get check", zap.Error(err))
			continue
		}
		// check not found, which means the deployment is finished regardless of the result.
		if check == nil {
			dropTask = append(dropTask, task.ID)
			taskLogger.Info("drop task due to no check found")
			continue
		}

		teamID := utils.GetTeamIDString(check.VercelIntegrationConfiguration.TeamID)
		detailedUrl := utils.GenerateDetailUrl(w.conf.ServerConfig.TiDBCloudHost, conn)

		// update check run result to timeout when timeout
		if (task.CreatedAt.Add(time.Second * BranchStatusCheckTimeout)).Before(time.Now()) {
			timeoutTaskID := w.updateCheckWithFailedConclusion(ctx, task, check, detailedUrl,
				"timeout to wait for branch ready", teamID, taskLogger)
			if timeoutTaskID != nil {
				timeoutTask = append(timeoutTask, *timeoutTaskID)
			}

			taskLogger.Info("timeout to wait for branch ready")
			continue
		}

		bran, bizErr := w.branchSvc.GetBranch(ctx, conn.TiDBClusterID, task.BranchID)
		if bizErr != nil {
			// not retry for 4xx or 6xx
			if bizerr.IsBusinessErr(bizErr.ErrCode()) || bizerr.IsRequestErr(bizErr.ErrCode()) {
				dropTaskID := w.updateCheckWithFailedConclusion(ctx, task, check, detailedUrl,
					"failed to get branch", teamID, taskLogger)
				if dropTaskID != nil {
					dropTask = append(dropTask, *dropTaskID)
				}
			}

			taskLogger.Error("failed to get branch", zap.String("branch_id", task.BranchID), zap.Error(err))
			continue
		}

		if bran.State == rpc.BranchStatus_ACTIVE {
			envs, err := w.branchEnvIDDAO.GetEnvironmentIdentifierByGitBranch(ctx, check.GitHubRepoID, check.GitBranchRef, conn.ID)
			if err != nil {
				taskLogger.Error("failed to get environment identifier", zap.Error(err))
				continue
			}

			if envs == nil {
				// If the environment variables are not created in the preview of this git branch, create them.
				err := w.branchLogic.CreateVercelEnvs(ctx, conn, bran.Id, taskLogger, check, teamID)
				if err != nil {
					// not retry for 4xx from vercel api
					var bizErr bizerr.BizError
					if errors.As(err, &bizErr) && bizErr.ErrCode() == vercelProxy.ErrorCodeVercelApiUnrecoverableError {
						dropTaskID := w.updateCheckWithFailedConclusion(ctx, task, check, detailedUrl,
							"failed to create vercel project envs", teamID, taskLogger)
						if dropTaskID != nil {
							dropTask = append(dropTask, *dropTaskID)
						}
					}

					taskLogger.Error("failed to create vercel envs", zap.Error(err))
					continue
				}

				successTask = append(successTask, task.ID)
				taskLogger.Info("succeed to add environment variables")
			} else {
				// Branch may have been deleted and recreated with the same name. If so, we need to update the environment variables.
				err := w.branchLogic.UpdateVercelEnvs(ctx, conn, bran.Id, taskLogger, check, teamID, envs)
				if err != nil {
					var bizErr bizerr.BizError
					if errors.As(err, &bizErr) && bizErr.ErrCode() == vercelProxy.ErrorCodeVercelApiUnrecoverableError {
						dropTaskID := w.updateCheckWithFailedConclusion(ctx, task, check, detailedUrl,
							"failed to update vercel project envs", teamID, taskLogger)
						if dropTaskID != nil {
							dropTask = append(dropTask, *dropTaskID)
						}
					} else if errors.As(err, &bizErr) && bizErr.ErrCode() == vercelProxy.ErrorCodeVercelApiNotFoundError {
						// If the environment variables has been deleted manually, delete it from our database.
						taskLogger.Info("environment identifier has been deleted manually, delete it", zap.Uint64("env_id", envs.ID))
						envsToDelete := utils.GetBranchEnvsToDelete(*envs)
						err := w.vercelAPIClient.DeleteEnvironmentVariables(ctx, task.VercelProjectID, teamID, envsToDelete, check.VercelIntegrationConfiguration.ApiAccessToken)
						if err != nil {
							taskLogger.Error("failed to delete environment variables", zap.Error(err))
							continue
						}

						err = w.branchEnvIDDAO.DeleteEnvironmentIdentifier(ctx, envs.ID)
						if err != nil {
							taskLogger.Error("failed to delete environment identifier", zap.Error(err))
						}
					}

					taskLogger.Error("failed to update vercel envs", zap.Error(err))
					continue
				}

				successTask = append(successTask, task.ID)
				taskLogger.Info("succeed to update environment variables")
			}
		}
	}

	// update task status
	if len(dropTask) > 0 {
		logger.Info("drop tasks", zap.Uint64s("task_ids", dropTask))
		err = w.branchDAO.UpdateTasksStatus(ctx, vercelDB.Dropped, dropTask)
		if err != nil {
			logger.Error("update tasks status error", zap.Error(err))
		}
	}
	if len(successTask) > 0 {
		logger.Info("success tasks", zap.Uint64s("task_ids", successTask))
		err = w.branchDAO.UpdateTasksStatus(ctx, vercelDB.Completed, successTask)
		if err != nil {
			logger.Error("update tasks status error", zap.Error(err))
		}
	}
	if len(timeoutTask) > 0 {
		logger.Info("timeout tasks", zap.Uint64s("task_ids", timeoutTask))
		err = w.branchDAO.UpdateTasksStatus(ctx, vercelDB.Timeout, timeoutTask)
		if err != nil {
			logger.Error("update tasks status error", zap.Error(err))
		}
	}
	logger.Info("vercel webhook worker finish")
	return err
}

// updateCheckWithFailedConclusion update check with failed conclusion, return task id if update success or with unrecoverable error, otherwise return nil
func (w *VercelWebhookWorker) updateCheckWithFailedConclusion(ctx context.Context, task vercelDB.BranchTask, check *vercelDB.Check,
	detailedUrl string, errMessage string, teamID string, taskLogger *zap.Logger) *uint64 {
	err := w.vercelAPIClient.UpdateCheck(ctx, task.DeploymentID, check.ID, vercelProxy.UpdateCheckBody{
		Conclusion: vercelProxy.ConclusionFailed,
		DetailsUrl: detailedUrl,
		Name:       errMessage,
	}, teamID, check.VercelIntegrationConfiguration.ApiAccessToken)
	if err != nil {
		var bizErr bizerr.BizError
		if errors.As(err, &bizErr) && bizErr.ErrCode() == vercelProxy.ErrorCodeVercelApiUnrecoverableError {
			taskLogger.Error("drop this task because of unrecoverable error when updating the check", zap.Error(err))
			return &task.ID
		} else {
			// update check failed with recoverable error, so we still need to retry this task
			taskLogger.Error("failed to update check", zap.Error(err))
			return nil
		}
	} else {
		return &task.ID
	}
}

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

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