package vercel

import (
	"crypto/hmac"
	"crypto/sha1"
	"database/sql"
	"encoding/hex"
	"fmt"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/google/go-querystring/query"
	"github.com/tidbcloud/ecosystem-service/api/auth"
	"github.com/tidbcloud/ecosystem-service/config"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/account"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/acl"
	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/tracing"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

var Tracer = otel.Tracer("vercel_controller")

type VercelController struct {
	clusterConnectionDAO vercelDB.ClusterConnectionDAO
	dataAppConnectionDAO vercelDB.DataAppConnectionDAO
	accountDAO           vercelDB.TiDBVercelAccountDAO
	configurationDAO     vercelDB.VercelIntegrationConfigurationDAO
	vercelAPIClient      vercelProxy.APIClient
	composeEntityDAO     vercelDB.ComposeEntityDAO
	clusterLogic         *ClusterConnLogic
	dataAppLogic         *DataAppConnLogic
	conf                 *config.ConfigWrapper
	aclSvc               acl.AclSvc
	branchLogic          *BranchLogic
	accountSvc           account.AccountProviderSvc
}

func NewVercelController(connectionDAO vercelDB.ClusterConnectionDAO,
	accountDAO vercelDB.TiDBVercelAccountDAO,
	configurationDAO vercelDB.VercelIntegrationConfigurationDAO,
	vercelAPIClient vercelProxy.APIClient,
	composeEntityDAO vercelDB.ComposeEntityDAO,
	vercelLogic *ClusterConnLogic,
	conf *config.ConfigWrapper,
	dataAppLogic *DataAppConnLogic,
	dataAppConnectionDAO vercelDB.DataAppConnectionDAO,
	aclSvc acl.AclSvc,
	branchLogic *BranchLogic,
	accountSvc account.AccountProviderSvc,
) *VercelController {
	return &VercelController{
		clusterConnectionDAO: connectionDAO,
		accountDAO:           accountDAO,
		configurationDAO:     configurationDAO,
		vercelAPIClient:      vercelAPIClient,
		composeEntityDAO:     composeEntityDAO,
		clusterLogic:         vercelLogic,
		conf:                 conf,
		dataAppLogic:         dataAppLogic,
		dataAppConnectionDAO: dataAppConnectionDAO,
		aclSvc:               aclSvc,
		branchLogic:          branchLogic,
		accountSvc:           accountSvc,
	}
}

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

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

// ListVercelConnectionsByOrg godoc
//
//	@Description	List connections between Vercel project and (cluster or data app) By TiDB Org.
//
// This API is used by TiDB integration Dashboard, so the connections returned maybe configured by different Vercel integrations.
//
//	@Accept			json
//	@Produce		json
//	@Param			org_id	path		string					true	"TiDB org ID"
//	@Success		200		{object}	TiDBOrgVercelConnection	"ok"
//	@Failure		400		{object}	BaseResp				"error"
//	@Failure		500		{object}	BaseResp				"error"
//	@Router			/orgs/{org_id}/vercel/connections [get]
func (c *VercelController) ListVercelConnectionsByOrg(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "vercel.ListVercelConnectionsByOrg")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	logger := tilog.WithContextL(ctxReq).With(zap.String("method", "ListVercelConnectionsByOrg"))

	var path TiDBOrg
	if err = ctx.ShouldBindUri(&path); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required TiDB org information"))
		logger.Error("Missing required TiDB org information", zap.Error(err))
		return
	}
	orgID, err := strconv.ParseUint(path.TiDBOrgID, 10, 64)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Invalid TiDB org identifier, should be uint64"))
		logger.Error("Invalid TiDB org id", zap.Error(err))
		return
	}

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

	span.SetAttributes(attribute.String("tidb_org_id", strconv.FormatUint(orgID, 10)), attribute.String("user_email", user.Email))
	logger = logger.With(zap.String("tidb_org_id", strconv.FormatUint(orgID, 10)), zap.String("user_email", user.Email))
	logger.Info("Start to list Vercel connections by org")

	span.AddEvent("list cluster connections")
	clusterConnections, err := c.clusterConnectionDAO.ListVercelConnectionsByOrgID(ctxReq, orgID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get cluster connections under TiDB org"))
		logger.Error("Failed to get cluster connections under TiDB org", zap.Error(err))
		return
	}

	span.AddEvent("list data app connections")
	dataAppConnections, err := c.dataAppConnectionDAO.ListVercelConnectionsByOrgID(ctxReq, orgID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get data app connections under TiDB org"))
		logger.Error("Failed to get data app connections under TiDB org", zap.Error(err))
		return
	}

	span.AddEvent("generate connectionsVOs")
	connectionVOs, err := c.clusterLogic.getConnectionVOsByOrgID(ctxReq, clusterConnections, dataAppConnections, logger)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get connectionsVO under TiDB org"))
		logger.Error("Failed to get connectionsVO under TiDB org", zap.Error(err))
		return
	}

	logger.Info("List Vercel connections by org successfully")
	response := TiDBOrgVercelConnection{
		TiDBClustersVercelConnection: connectionVOs,
	}
	ctx.JSON(200, response)
}

// ListVercelConnectionsByIntegrationConfiguration godoc
//
//	@Description	List connections between Vercel project and (cluster or data app) By integration configuration.
//
// This API is used by Vercel integration config console.
//
//	@Accept			json
//	@Produce		json
//	@Param			configuration_id	path		string					true	"Vercel integration configuration id"
//	@Success		200					{object}	ListConnectionResponse	"ok"
//	@Failure		400					{object}	BaseResp				"error"
//	@Failure		500					{object}	BaseResp				"error"
//	@Router			/vercel/integration/configurations/{configuration_id}/connections [get]
func (c *VercelController) ListVercelConnectionsByIntegrationConfiguration(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "vercel.ListVercelConnectionsByIntegrationConfiguration")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	logger := tilog.WithContextL(ctxReq).With(zap.String("method", "ListVercelConnectionsByIntegrationConfiguration"))

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

	authUser, err := c.accountSvc.GetAuthUser(ctxReq, user.Email, 0, true)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, NewBaseResp(errs.ErrCodeAuth, "Invalid user"))
		logger.Error("Can't get user from account svc", zap.Error(err))
		return
	}
	// This method is to get the accessible projects of all organizations that the user has access to.
	// skipCheckOrg only return the first organization that the user has access to.
	projects, err := c.clusterLogic.getAccessibleProjects(ctxReq, authUser)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Can't get projects"))
		logger.Error("Can't get projects", zap.Error(err))
		return
	}

	var req VercelIntegrationConfiguration
	if err = ctx.ShouldBindUri(&req); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required Vercel integration configuration information"))
		logger.Error("Missing required Vercel integration integration config information", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("vercel_integration_configuration_id", req.ConfigurationID), attribute.String("user_email", user.Email))
	logger = logger.With(zap.String("vercel_integration_configuration_id", req.ConfigurationID), zap.String("user_email", user.Email))
	logger.Info("Start to list Vercel connections by integration configuration")

	projectIDs := make([]uint64, len(projects))
	for i, project := range projects {
		projectIDs[i] = project.Id
	}

	span.AddEvent("list cluster connections")
	// There are two scopes: one is that projects must be visible to the user(one of the tenant(Org)'s owner or project's member)
	// and the other is that connections must be visible under this configuration(configured by this configuration).
	clusterConnections, err := c.clusterConnectionDAO.ListVercelConnectionsByProjectIDsAndConfigurationID(ctxReq, projectIDs, req.ConfigurationID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get cluster connections under Vercel integration configuration"))
		logger.Error("Failed to get cluster connections under Vercel integration configuration", zap.Error(err))
		return
	}

	span.AddEvent("list data app connections")
	dataAppConnections, err := c.dataAppConnectionDAO.ListVercelConnectionsByProjectIDsAndConfigurationID(ctxReq, projectIDs, req.ConfigurationID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get data app connections under Vercel integration configuration"))
		logger.Error("Failed to get data app connections under Vercel integration configuration", zap.Error(err))
		return
	}

	span.AddEvent("generate connectionsVOs")
	connectionVOs, err := c.clusterLogic.getConnectionVOsByConfiguration(ctxReq, clusterConnections, dataAppConnections, req, logger)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get connectionsVO under Vercel integration configuration"))
		logger.Error("Failed to get connectionsVO under Vercel integration configuration", zap.Error(err))
		return
	}

	logger.Info("List connections by integration configuration successfully")
	// return response
	response := ListConnectionResponse{
		Connections: connectionVOs,
	}
	ctx.JSON(200, response)
}

// ListVercelProjectsByIntegrationConfiguration godoc
//
//	@Description	List Vercel projects accessible to the integration configuration.
//	@Accept			json
//	@Produce		json
//	@Param			configuration_id	path		string					true	"Vercel integration configuration information"
//	@Success		200					{object}	RetrieveProjectResponse	"ok"
//	@Failure		400					{object}	BaseResp				"error"
//	@Failure		500					{object}	BaseResp				"error"
//	@Router			/vercel/integration/configurations/{configuration_id}/projects [get]
func (c *VercelController) ListVercelProjectsByIntegrationConfiguration(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "vercel.ListVercelProjectsByIntegrationConfiguration")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	logger := tilog.WithContextL(ctxReq).With(zap.String("method", "ListVercelProjectsByIntegrationConfiguration"))

	var req VercelIntegrationConfiguration
	if err = ctx.ShouldBindUri(&req); err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeBadRequest, "Missing required Vercel integration configuration information"))
		logger.Error("Missing required Vercel integration integration config information", zap.Error(err))
		return
	}

	span.SetAttributes(attribute.String("vercel_integration_configuration_id", req.ConfigurationID))
	logger = logger.With(zap.String("vercel_integration_configuration_id", req.ConfigurationID))
	logger.Info("Start to list Vercel projects by integration configuration")

	span.AddEvent("get configuration from DB")
	configuration, err := c.configurationDAO.GetIntegrationConfigurationByID(ctxReq, req.ConfigurationID)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to find configuration in DB"))
		logger.Error("Failed to find configuration in DB", zap.Error(err))
		return
	}

	span.AddEvent("get configuration info from Vercel API")
	teamID := utils.GetTeamIDString(configuration.TeamID)
	integrationConfiguration, err := c.vercelAPIClient.RetrieveIntegrationConfiguration(ctxReq, req.ConfigurationID, teamID, configuration.ApiAccessToken)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get integration configuration from Vercel"))
		logger.Error("Failed to get integration configuration from Vercel", zap.Error(err))
		return
	}

	span.AddEvent("list vercel projects from Vercel API")
	projectsResponse, err := c.clusterLogic.RetrieveAllProjects(ctxReq, vercelProxy.RetrieveProjectQueryParameters{
		TeamID: teamID,
	}, configuration.ApiAccessToken, logger)
	if err != nil {
		ctx.JSON(http.StatusOK, NewBaseResp(errs.ErrCodeInternalServerError, "Failed to get projects from Vercel"))
		logger.Error("Failed to get projects from Vercel", zap.Error(err))
		return
	}

	span.AddEvent("convert to projectVO")
	projectsVOResponse := convertToProjectResp(integrationConfiguration, projectsResponse)

	logger.Info("List Vercel projects by integration configuration successfully")
	ctx.JSON(http.StatusOK, projectsVOResponse)
}

// Webhook godoc
//
//	@Description	Webhook for Vercel event https://vercel.com/docs/integrations/webhooks-overview#
//
// We will only delete the connection metadata, the other things like database user and data stored in TiDB will not be deleted.
//
//	@Accept			json
//	@Produce		json
//	@Param			x-vercel-signature	header		string		true	"Webhook request"
//	@Param			vercel_event		body		VercelEvent	true	"Vercel event callback"
//	@Success		200					{string}	string		"ok"
//	@Failure		400					{string}	string		"error"
//	@Failure		403					{string}	string		"error"
//	@Failure		500					{string}	string		"error"
//	@Router			/vercel/webhooks [post]
func (c *VercelController) Webhook(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "vercel.Webhook")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	logger := tilog.WithContextL(ctxReq).With(zap.String("method", "vercel.Webhook"))

	var vercelEvent VercelEvent
	requestBodyJson, err := ctx.GetRawData()
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to get request body"})
		logger.Error("Can't get request body", zap.Error(err))
		return
	}
	if err = binding.JSON.BindBody(requestBodyJson, &vercelEvent); err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Missing required Vercel event info"})
		logger.Error("Missing required Vercel event info", zap.Error(err))
		return
	}

	// Use integration secret as key, sha1 as hash algorithm https://vercel.com/docs/webhooks#introduction/securing-webhooks
	hash := hmac.New(sha1.New, []byte(c.conf.VercelConfig.IntegrationSecret))
	// encode request body to hex string
	hash.Write(requestBodyJson)
	encodedHexString := hex.EncodeToString(hash.Sum(nil))
	if header := ctx.GetHeader("x-vercel-signature"); encodedHexString != header {
		ctx.JSON(http.StatusForbidden, gin.H{
			"message": "Invalid signature"})
		logger.Error("Invalid signature", zap.String("signature", header), zap.String("requestBody", string(requestBodyJson)))
		return
	}
	span.SetAttributes(attribute.String("event_id", vercelEvent.ID), attribute.String("VercelEventType", string(vercelEvent.Type)))
	logger = logger.With(zap.String("event_id", vercelEvent.ID), zap.String("VercelEventType", string(vercelEvent.Type)))

	payload := vercelEvent.Payload
	switch vercelEvent.Type {
	case ProjectRemovedDeprecated, ProjectRemoved:
		p := payload.(ProjectEventPayload)
		logger.Info("Start to handle ProjectRemoved event", zap.String("ProjectID", p.Project.ID), zap.String("ProjectName", p.Project.Name))
		err = c.composeEntityDAO.DeleteVercelConnectionsByVercelProjectIDs(ctxReq, []string{p.Project.ID})
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to delete connections under Vercel project"})
			logger.Error("Failed to delete connections under Vercel project", zap.Error(err))
			return
		}

		message := "Delete connections under Vercel project successfully"
		ctx.JSON(200, message)
		logger.Info(message)
	case IntegrationRemovedDeprecated, IntegrationRemoved:
		p := payload.(IntegrationEventPayload)
		logger.Info("Start to handle IntegrationRemoved event", zap.String("IntegrationID", p.Configuration.ID), zap.String("ProjectSelection", p.Configuration.ProjectSelection), zap.Any("Projects", p.Configuration.Projects))

		// Vercel will delete the related environment variables when the integration is removed, so we don't need to delete them by Verce API, just delete the identifier in our database.
		if err = c.composeEntityDAO.DeleteIntegrationConfiguration(ctxReq, p.Configuration.ID); err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to delete connections under Vercel integration"})
			logger.Error("Failed to delete connections under Vercel integration", zap.Error(err))
			return
		}

		message := "Delete connections under Vercel configuration successfully"
		ctx.JSON(200, message)
		logger.Info(message)
	case DeploymentCreated:
		p := payload.(DeploymentPayload)
		err := c.branchLogic.HandleCreated(ctxReq, p, logger)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to handle deployment created event"})
			logger.Error("Failed to handle deployment created event", zap.Error(err))
			return
		}
		ctx.JSON(http.StatusOK, gin.H{"message": "ok"})
	case DeploymentReady:
		p := payload.(DeploymentPayload)
		err := c.branchLogic.HandleReady(ctxReq, p, logger)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to handle deployment ready event"})
			logger.Error("Failed to handle deployment ready event", zap.Error(err))
			return
		}
		ctx.JSON(http.StatusOK, gin.H{"message": "ok"})
	case DeploymentCanceled:
		p := payload.(DeploymentPayload)
		err := c.branchLogic.HandleCanceled(ctxReq, p, logger)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to handle deployment canceled event"})
			logger.Error("Failed to handle deployment canceled event", zap.Error(err))
			return
		}
	case DeploymentError:
		p := payload.(DeploymentPayload)
		err := c.branchLogic.HandleError(ctxReq, p, logger)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to handle deployment error event"})
			logger.Error("Failed to handle deployment error event", zap.Error(err))
			return
		}
	default:
		ctx.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("Unsupported event type: %s", vercelEvent.Type)})
		logger.Error(fmt.Sprintf("Unsupported event type: %s", vercelEvent.Type))
	}
}

// ExchangeIntegrationAccessToken godoc
//
//	@Description	Get Vercel access token. For the concern of security, we will store the long-lived access token in our database
//
// and not exposed to the user. That means we need to proxy all the requests to Vercel API.
// This method will redirect the user to add-connection page in spite of failure or success.
//
//	@Accept			json
//	@Produce		json
//	@Param			vercel_configuration	query		IntegrationAccessTokenRequest	true	"Other query parameters"
//	@Success		200						{object}	AccessTokenResponse				"ok"
//	@Failure		400						{object}	AccessTokenResponse				"error"
//	@Failure		500						{object}	AccessTokenResponse				"error"
//	@Router			/vercel/access_token [post]
func (c *VercelController) ExchangeIntegrationAccessToken(ctx *gin.Context) {
	ctxReq := ctx.Request.Context()
	var err error
	ctxReq, span := Tracer.Start(ctxReq, "vercel.ExchangeIntegrationAccessToken")
	defer func() {
		tracing.EndSpan(span, err)
	}()
	logger := tilog.WithContextL(ctxReq).With(zap.String("method", "ExchangeIntegrationAccessToken"))

	details := c.conf.VercelConfig.AccessDetails
	var req IntegrationAccessTokenRequest
	if err = ctx.ShouldBindQuery(&req); err != nil {
		queryString, _ := query.Values(req)
		res := AccessTokenResponse{
			RedirectURL: fmt.Sprintf("%s?%s", details.IntegrationLinkPage, queryString.Encode()),
			BaseResp:    *NewBaseResp(errs.ErrCodeBadRequest, " Missing required parameters"),
		}
		ctx.JSON(http.StatusOK, res)
		logger.Error("Missing required parameters", zap.Error(err))
		return
	}
	span.SetAttributes(attribute.String("vercel_configuration_id", *req.ConfigurationID))

	span.AddEvent("exchange access token from Vercel API")
	tokenResult, err := c.vercelAPIClient.ExchangeAccessToken(ctxReq, req.Code, details.IntegrationRedirectUri, details.IntegrationID, details.IntegrationSecret)
	if err != nil {
		queryString, _ := query.Values(req)
		res := AccessTokenResponse{
			RedirectURL: fmt.Sprintf("%s?%s", details.IntegrationLinkPage, queryString.Encode()),
			BaseResp:    *NewBaseResp(errs.ErrCodeInternalServerError, "Failed to exchange access token"),
		}
		ctx.JSON(http.StatusOK, res)
		logger.Error("Failed to exchange access token", zap.Error(err))
		return
	}

	logger.Debug("Exchange access token successfully", zap.Any("tokenResult", tokenResult))

	var teamID sql.NullString
	if tokenResult.TeamID != nil {
		teamID = sql.NullString{String: *tokenResult.TeamID, Valid: true}
	} else {
		teamID = sql.NullString{Valid: false}
	}

	span.AddEvent("save access token to database")
	err = c.configurationDAO.AddVercelIntegrationConfiguration(ctxReq, vercelDB.VercelIntegrationConfiguration{
		ID:             tokenResult.InstallationID,
		ApiAccessToken: tokenResult.AccessToken,
		TeamID:         teamID,
		UserID:         tokenResult.UserID,
	})

	if err != nil {
		logger.Error("Failed to save access token to database", zap.Error(err))
	}

	req.ConfigurationID = &tokenResult.InstallationID
	queryString, _ := query.Values(req)

	res := AccessTokenResponse{
		RedirectURL: fmt.Sprintf("%s?%s", details.IntegrationLinkPage, queryString.Encode()),
	}
	ctx.JSON(http.StatusOK, res)
}
