package api

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

	"github.com/tidbcloud/account/internel/controllers"
	auditlogsvc "github.com/tidbcloud/account/internel/svc/auditlog"
	projectsvc "github.com/tidbcloud/account/internel/svc/project"
	rbacsvc "github.com/tidbcloud/account/internel/svc/rbac"

	"github.com/tidbcloud/account/central/server"
	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/internel/dao/project"
	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/account/thirdparty/jsm"

	"github.com/tidbcloud/account/internel/dao/role"

	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/tidbcloud/account/commons/config"

	"github.com/duke-git/lancet/v2/slice"

	"github.com/tidbcloud/account/internel/dao/entity"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
)

// create swag doc command: make generate_openapi_docs

// @title  Project OPENAPI
// @version v1beta1
// @description This is the project related API.
// @host iam.tidbapi.com
// @BasePath /v1beta1
func RegisterProjectRouter(e gin.IRouter) {
	// @BasePath /v1beta1/projects
	r := e.Group("/v1beta1/projects")
	r.Use(server.LocalTestIdentity)
	if !config.IsLocalEnv() {

		pathWhitelist := []string{}
		auth.InitAccountClientByEndpoint(config.Global().GrpcConfig.AccountSvcAddr, 0)

		r.Use(
			auth.GinGetIdentityInfoInterceptor(&auth.IdentityAuthOption{
				EnableLog: true,
			}),
			auth.GinPermissionCheckInterceptor(&auth.PermissionCheckerCommonParam{
				IdentityF:               auth.GinGetIdentityFunc,
				PathPattern2ResourceMap: pathPattern2ResourceMap,
				PathWhitelist:           pathWhitelist,
				GenAccessParam4GinF:     auth.GenAccessParam4GinWithPrefixOrgId,
				HttpReqCheckRetHandleF:  openApiPermissionCheckResultHandleFunc,
				EnableLog:               true,
			}),
		)
	}

	r.GET("", ListProjectHandler)
	r.POST("", CreateProjectHandler)

}

// https://iam.tidbapi.com/v1beta1/projects
// @Tags Project
// @Summary Get  list of org projects
// @Description  This endpoint returns a list of org projects.
// @Schemes
// @Accept json
// @Produce json
// @Param pageToken query string false "The page token of the next page."
// @Param pageSize query int false "The page size of the next page. The page size. If unspecified, at most 100 API keys will be returned in one page."
// @Success 200 {object} ListProjectsRsp
// @Failure 400  {object}  OpenApiError
// @Router /projects [GET]
func ListProjectHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "ListProjectHandler")
	defer bm.OnExit(ctx)
	//req := ListProjectsReq{}
	req := OpenApiPageReq{}

	if err := c.ShouldBindQuery(&req); err != nil {
		tilog.Warn(ctx, "ListProjectHandler ShouldBindUri err", zap.Error(err))
		writeError(ctx, c.Writer,
			err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}
	tilog.Info(ctx, "ListProject ", zap.Any("req", req))
	var authInfo *auth.AuthnIdentityInfo
	var err error

	authInfo, err = auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		tilog.Info(ctx, "get auth Info failed %v", zap.Error(err))
	}

	var projects []entity.Project

	// 1、call by access token
	if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {

		projects, err = getProjectsByAccessToken(ctx, authInfo)

		// 2、call by apikey
	} else if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		projects, err = getProjectsByApiKey(ctx, authInfo)
	}
	if err != nil {
		tilog.Info(ctx, "ListProject err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusInternalServerError, internalServerErrorCode, "internal server error")
		return
	}

	tilog.Info(ctx, "ListActiveProjectsByOrgID result", zap.Any("project.size", len(projects)), zap.Any("project", projects))
	if len(projects) == 0 {

		openApiWriteResult(c.Writer, &ListProjectsRsp{})
		return
	}

	var projectItems []Project
	var pids []entity.ID
	for _, project := range projects {
		projectResp := Project{
			Id:    strconv.FormatUint(project.ID.Uint64(), 10),
			OrgId: strconv.FormatUint(project.TenantID.Uint64(), 10),
			Name:  project.Name,
			//UserCount:       project.UsersCount,
			CreateTimestamp: strconv.FormatInt(timestamppb.New(project.CreatedAt).GetSeconds(), 10), // will be removed later, replaced by createTime
			AwsCmekEnabled:  project.AwsCmekEnabled,                                                 // will be removed later, replaced by cmekEnabled
			CmekEnabled:     project.AwsCmekEnabled,
			CreateTime:      project.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
			Type:            project.Type,
		}
		activeClustersCount, err := infra.GetHandler().CountClustersByProject(ctx, []uint64{project.ID.Uint64()})
		tilog.Info(ctx, "CountClustersByProject", zap.Any("project_id", project), zap.Any("activeClustersCount", activeClustersCount))
		//activeClustersCount := 10
		if err != nil {
			tilog.Error(ctx, "CountClustersByProject fail", zap.Any("project_id", project), zap.Error(err))
		} else {

			projectResp.ClusterCount = activeClustersCount
		}
		projectItems = append(projectItems, projectResp)
		pids = append(pids, entity.ID(project.ID.Uint64()))
	}

	// set UserCount
	roleBindingSvc, _ := rbacsvc.GetRbacRoleBindingSvc()

	// 1. query all user of all projects
	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
		ScopeIDs:   pids,
		Status:     []string{entity.RoleBindingStatusActive},
	}

	bindings, err := roleBindingSvc.ListRoleBindings(ctx, filter)
	if err != nil {
		tilog.Error(ctx, "ListRoleBindings err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusInternalServerError, internalServerErrorCode, "internal server error")
		return
	}
	// 2. count project users
	// map[project_id][]entity.RoleBinding
	projectMap := slice.GroupWith(bindings, func(b entity.RoleBinding) uint64 {
		return b.ScopeID.Uint64()
	})
	for i := 0; i < len(projectItems); i++ {
		projectId, _ := strconv.ParseUint(projectItems[i].Id, 10, 64)
		count := len(projectMap[projectId])
		projectItems[i].UserCount = uint64(count)

	}
	//tilog.Info(ctx, "show projectItems", zap.Any("projectItems", projectItems))
	// set page
	pageReq := OpenApiPageReq{}
	var projectsResp []Project
	if err := c.ShouldBindQuery(&pageReq); err != nil {
		tilog.Warn(ctx, "ShouldBindQuery err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid: "+err.Error())
		return
	}

	tilog.Info(ctx, "pageReq", zap.Any("pageReq", pageReq))
	err = slice.SortByField(projectItems, "Id", "asc")
	if err != nil {
		tilog.Error(ctx, "SortByField err", zap.Error(err))
		return
	}
	//tilog.Info(ctx, "sort projectItems", zap.Any("projectItems", projectItems))

	pageSize, pageToken := getPageInfo(ctx, pageReq)

	curPageToken := utils.PageToken[string]{}

	nextPageToken := utils.PageToken[string]{}
	if pageToken != "" {
		if err := curPageToken.DecodeFromString(pageToken); err != nil {
			tilog.PlainInfo("invalid page token")
			return
		}
		tilog.Info(ctx, "pageToken="+curPageToken.Prev)
		for i, p := range projectItems {
			// find  the  offsetprojecsResp
			if p.Id > curPageToken.Prev && len(projectsResp) < int(pageSize) {
				////foundPre = true
				//continue

				//if user.UserName == token.Prev || len(sqlUserResp) > 0 {
				//  the next user is exits
				projectsResp = append(projectsResp, p)
				nextPageToken.Prev = p.Id

				// the last page
				if i == len(projectItems)-1 {
					nextPageToken.Prev = ""
				}

				if len(projectsResp) == int(pageSize) {
					break
				}
			}

		}
	} else {

		// first page
		// 1、only one page
		if pageSize >= int32(len(projectItems)) {
			//endIndex = int32(total)
			projectsResp = projectItems
			nextPageToken.Prev = ""
		} else {
			// 2、more than one page
			projectsResp = projectItems[0:pageSize]
			nextPageToken.Prev = projectItems[pageSize-1].Id
		}
	}

	resp := ListProjectsRsp{
		Projects: projectsResp,
	}
	if nextPageToken.Prev != "" {
		nextPageToken, err := nextPageToken.EncodeToString()
		if err != nil {
			tilog.Info(ctx, "next page curPageToken encode failed", zap.Error(err))
		} else {
			resp.NextPageToken = nextPageToken
		}
	}
	tilog.Info(ctx, "NextPageToken result", zap.Any("nextPageToken.Prev", nextPageToken.Prev), zap.Any("base64Encode", resp.NextPageToken))
	tilog.Info(ctx, "ListSqlUsers result", zap.Any("projectsResp", projectsResp))

	openApiWriteResult(c.Writer, resp)
}

func getProjectsByAccessToken(ctx context.Context, authInfo *auth.AuthnIdentityInfo) ([]entity.Project, error) {
	var projects []entity.Project
	//tilog.Info(ctx, "test HasPermissionForAccessToken: get access token info ", zap.Any("accessToken", accessToken), zap.Any("AccessTokenIdentity", authInfo.AccessTokenRsp.AccessTokenIdentity))
	tilog.Info(ctx, "HasPermissionForAccessToken: get access token info", zap.Any("accessToken", authInfo.IdentityVal), zap.Any("AccessTokenIdentity", authInfo.AccessTokenRsp.AccessTokenIdentity))
	//accessToken = authInfo.IdentityVal
	orgId := authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	userId := authInfo.AccessTokenRsp.AccessTokenIdentity.UserId

	svc, _ := projectsvc.GetProjectSvc()
	userProjects, err := svc.ListUserVisibleProjects(ctx, userId, []uint64{orgId})
	if err != nil {
		tilog.Error(ctx, "ListActiveProjectsByOrgID err", zap.Any("customerOrgId", orgId), zap.Error(err))
		return projects, err
	}

	// if access token not GrantAllProjects, only use user grant projects
	if !authInfo.AccessTokenRsp.AccessTokenIdentity.GrantAllProjects {
		grantProjectIds := authInfo.AccessTokenRsp.AccessTokenIdentity.ProjectIds
		for _, p := range userProjects {
			if slice.Contain(grantProjectIds, p.ID.Uint64()) {
				projects = append(projects, p)
			}
		}
	} else {
		projects = userProjects
	}

	tilog.Info(ctx, "HasPermissionForAccessToken: get access token info", zap.Any("accessToken", authInfo.IdentityVal), zap.Any("AccessTokenIdentity", authInfo.AccessTokenRsp.AccessTokenIdentity))
	return projects, nil
}

// getProjectsByApiKey
//
//	@Description:
//	@Author wangletian 2024-02-18 16:41:57
//	@param ctx
//	@param authInfo
//	@return []entity.Project
//	@return error
func getProjectsByApiKey(ctx context.Context, authInfo *auth.AuthnIdentityInfo) ([]entity.Project, error) {
	var projects []entity.Project
	apiKeyRsp := authInfo.ApiKeyRsp
	orgId, err := auth.GetOrgIdFromApiKeyRsp(ctx, apiKeyRsp)

	if err != nil {
		tilog.Error(ctx, "GetOrgIdFromApiKeyRsp err", zap.Error(err))
		return projects, err
	}

	tilog.Info(ctx, "HasPermissionForApiKey: get apiKey info", zap.Any("apiKeyRsp", apiKeyRsp), zap.Any("orgId", orgId))
	svc, _ := projectsvc.GetProjectSvc()
	projects, err = svc.ListActiveProjectsByOrgID(ctx, orgId)
	if err != nil {
		tilog.Error(ctx, "ListActiveProjectsByOrgID err", zap.Any("orgId", orgId), zap.Error(err))
		return projects, err
	}
	return projects, nil
}

/*
func getLocalAuthnIdentityInfo(c *gin.Context) *auth.AuthnIdentityInfo {

	orgIdStr := c.GetHeader("X-Test-Org-ID")
	orgId, _ := strconv.ParseUint(orgIdStr, 10, 64)
	userIdStr := c.GetHeader("X-Test-User-ID")
	userId, _ := strconv.ParseUint(userIdStr, 10, 64)
	apiKey := c.GetHeader("X-Test-ApiKey")
	authIdentityTypeStr := c.GetHeader("X-Test-AuthIdentityType")
	var authnIdentityType auth.AuthnIdentityType
	if authIdentityTypeStr == string(auth.AuthnIdentityTypeAccessToken) {

		authnIdentityType = auth.AuthnIdentityTypeAccessToken
	} else if authIdentityTypeStr == string(auth.AuthnIdentityTypeApiKeyAK) {

		authnIdentityType = auth.AuthnIdentityTypeApiKeyAK
	}

authInfo := &auth.AuthnIdentityInfo{
		IdentityType: authnIdentityType,
		ApiKeyRsp: &account.GetApiKeyByAccessKeyRsp{
			ApiKey: &account.ApiKey{AccessKey: apiKey, ResourceInfos: []*account.ApiKeyResourceInfo{{ScopeType: "ORG", ResourceId: orgId}}}},
		AccessTokenRsp: &provider.GetIdentityByAccessTokenResp{AccessTokenIdentity: &provider.AccessTokenIdentity{UserId: userId, OrgId: orgId}},
	}
	return authInfo

}
*/

// @Tags
// @Summary Create a new Project
// @Description  This endpoint creates a new Project.
// @Schemes
// @Accept json
// @Produce json
// @Param project body CreateProjectReq true "create Project request"
// @Success 200 {object} CreateProjectResp
// @Failure 400  {object}  OpenApiError
// @Router /projects [POST]
func CreateProjectHandler(c *gin.Context) {
	ctx := c.Request.Context()
	bm := utils.Begin(ctx, "CreateProjectHandler")
	defer bm.OnExit(ctx)
	req := CreateProjectReq{}
	if err := c.ShouldBindJSON(&req); err != nil {
		tilog.Warn(ctx, "CreateProjectHandler ShouldBindJSON err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "param invalid")
		return
	}
	tilog.Info(ctx, "ListProject ", zap.Any("req", req))
	var authInfo *auth.AuthnIdentityInfo
	var err error
	var orgId uint64

	authInfo, err = auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		tilog.Info(ctx, "get auth Info failed %v", zap.Error(err))
	}

	var userId uint64
	var operatorType = entity.RoleBindingTargetUser.String()
	var publicKey string
	// 1、call by access token
	if authInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
		userId = authInfo.AccessTokenRsp.AccessTokenIdentity.UserId

		// 2、call by apikey
	} else if authInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		apiKeyRsp := authInfo.ApiKeyRsp
		orgId, err = auth.GetOrgIdFromApiKeyRsp(ctx, apiKeyRsp)
		userId = apiKeyRsp.ApiKey.Id
		operatorType = entity.RoleBindingTargetAPIKey.String()
		publicKey = apiKeyRsp.ApiKey.AccessKey
	}
	if err != nil {
		tilog.Info(ctx, "ListProject err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusInternalServerError, internalServerErrorCode, "internal server error")
		return
	}
	err = controllers.IsValidProjectName(req.Name)
	if err != nil {
		tilog.Info(ctx, "ListProject err", zap.Error(err))
		writeError(ctx, c.Writer, err, http.StatusBadRequest, paramInvalidErrorCode, "invalid project name")
		return
	}

	svc, _ := projectsvc.GetProjectSvc()
	createProjectReq := project.CreateProjectReq{
		Name:         req.Name,
		OrgId:        orgId,
		OperatorID:   userId,
		OperatorType: operatorType,
		Type:         entity.ProjectTypeDedicated,
	}
	project, err := svc.CreateProject(ctx, createProjectReq)
	tilog.Info(ctx, "CreateProject result", zap.Any("project", project), zap.Any("err", err))
	if err != nil {
		httpcode := http.StatusInternalServerError
		codeStr := internalServerErrorCode
		if te, ok := errs.AsTiError(err); ok {
			httpcode = te.HTTPCode()
			codeStr = paramInvalidErrorCode
		} else {
			tilog.Error(ctx, "CreateProject err", zap.Any("req", createProjectReq), zap.Error(err))
		}

		writeError(ctx, c.Writer, err, httpcode, codeStr, err.Error())
		return
	}
	details := auditlog.Details{
		auditlog.KeyNewProjectName: req.Name,
		auditlog.KeyError:          err,
	}
	e := auditlog.NewEvent(ctx, auditlog.EventCreateProject).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(orgId)
	if err == nil {
		e = e.SetProjectID(project.ID.Uint64())
	}
	if operatorType == entity.RoleBindingTargetAPIKey.String() {
		e = e.SetAPIPubKey(publicKey)
	} else {
		e = e.SetOperatorID(userId)
	}
	e.SetOperatorIP(auditlogsvc.GetClientIpFromReq(c.Request))

	e.Submit() //nolint:errcheck

	jsmClient := jsm.GetJsmClientSingleton()
	if jsmClient.IsEnable() {
		notifyCreateProjectErr := jsmClient.NotifyCreateProjectMessage(ctx, project.ID.Uint64(), orgId)
		if notifyCreateProjectErr != nil {
			tilog.Error(ctx, "Sync Jsm failed during new project creation", zap.Any("err", notifyCreateProjectErr))
		}
	}

	resp := &CreateProjectResp{
		Id: project.ID.Uint64(),
	}
	tilog.Info(ctx, "open api create project result", zap.Any("project", project))
	openApiWriteResult(c.Writer, resp)

}
