package openapi

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/pkgv2/auth/util"

	"github.com/tidbcloud/account/internel/controllers"
	"github.com/tidbcloud/account/internel/svc/org/tenantctx"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/thirdparty/jsm"

	"github.com/microcosm-cc/bluemonday"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/idl/pbgen/openapi/openapi/cbp"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
)

const OpenAPIAccessKey util.ContextKey = "OpenAPIAccessKey"

type projectService struct {
	cbp.UnimplementedProjectServer
	sanitizerPolicy *bluemonday.Policy
}

// NewProjectService constructs a projectService
func NewProjectService() cbp.ProjectServer {
	return &projectService{
		sanitizerPolicy: bluemonday.UGCPolicy(),
	}
}

func (s *projectService) CreateProject(ctx context.Context, req *cbp.CreateProjectReq) (*cbp.CreateProjectResp, error) {
	accessValue := ctx.Value(OpenAPIAccessKey)
	apiKeyInfo, _ := accessValue.(*account.GetApiKeyByAccessKeyRsp)
	orgId := getAccessOrg(apiKeyInfo)
	if orgId == 0 {
		return nil, errs.NewBadRequest("not found valid orgID", nil)
	}
	identity := apiKeyInfo.ApiKey.AccessKey
	hasPermission, msg, err := checkAccessPermission(ctx, identity, orgId)
	if err != nil {
		tilog.Error(ctx, "CheckApiKeyPermission error", zap.String("identity", identity), zap.Uint64("orgID", orgId), zap.Error(err))
		return nil, errs.NewForbidden(fmt.Sprintf("CheckApiKeyPermission err: %s", msg), err)
	}
	if !hasPermission {
		tilog.Info(ctx, "CheckApiKeyPermission not pass", zap.String("identity", identity), zap.Uint64("orgID", orgId))
		return nil, utils.ConvertErr2GrpcErr(errs.NewForbidden(fmt.Sprintf("CheckApiKeyPermission not pass: %s", msg), err))
	}
	tilog.Info(ctx, "CheckApiKeyPermission pass", zap.String("identity", identity), zap.Uint64("orgID", orgId))

	projectName := s.sanitizerPolicy.Sanitize(strings.TrimSpace(req.Name))
	err = controllers.IsValidProjectName(projectName)
	if err != nil {
		return nil, utils.ConvertErr2GrpcErr(errs.NewBadRequest("invalid project name", err))
	}

	createProjectReq := tenantctx.CreateProjectReq{
		OrgID:                entity.ID(orgId),
		ProjectName:          req.Name,
		EnableXAccountDeploy: false,
		AwsCmekEnabled:       req.GetAwsCmekEnabled(),
	}
	projectID, err := tenantctx.CreateProjectForOpenAPI(ctx, createProjectReq)
	details := auditlog.Details{
		auditlog.KeyNewProjectName: projectName,
		auditlog.KeyError:          err,
	}
	e := auditlog.NewEvent(ctx, auditlog.EventCreateProject).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(orgId)
	if err == nil {
		e = e.SetProjectID(projectID)
	}
	e.Submit() //nolint:errcheck

	if err != nil {
		return nil, err
	}

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

	return &cbp.CreateProjectResp{
		Id: projectID,
	}, nil
}

func (s *projectService) ListProjects(ctx context.Context, req *cbp.ListProjectsReq) (*cbp.ListProjectsResp, error) {
	// TODO(iven): Use gRPC error code instead of raw errors
	accessValue := ctx.Value(OpenAPIAccessKey)
	apiKeyInfo, _ := accessValue.(*account.GetApiKeyByAccessKeyRsp)
	orgId := getAccessOrg(apiKeyInfo)
	if orgId == 0 {
		return nil, utils.ConvertErr2GrpcErr(errs.NewBadRequest("not found valid orgID", nil))
	}
	identity := apiKeyInfo.ApiKey.AccessKey
	hasPermission, msg, err := checkCreateProjectPermission(ctx, identity, orgId)
	if err != nil {
		tilog.Error(ctx, "CheckApiKeyPermission error", zap.String("identity", identity), zap.Uint64("orgID", orgId), zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(errs.NewForbidden(fmt.Sprintf("CheckApiKeyPermission err: %s", msg), err))
	}
	if !hasPermission {
		tilog.Info(ctx, "CheckApiKeyPermission not pass", zap.String("identity", identity), zap.Uint64("orgID", orgId))
		return nil, utils.ConvertErr2GrpcErr(errs.NewForbidden(fmt.Sprintf("CheckApiKeyPermission not pass: %s", msg), err))
	}
	tilog.Info(ctx, "CheckApiKeyPermission pass", zap.String("identity", identity), zap.Uint64("orgID", orgId))

	projectItems := []*cbp.ListProjectItem{}

	projects, err := tenantctx.ListProjectsByOrg(ctx, entity.ID(orgId), false)
	if err != nil {
		return nil, errs.NewInternalDB(err.Error(), err)
	}

	for _, project := range projects {
		projectItem := &cbp.ListProjectItem{
			Id:              project.Id,
			OrgId:           orgId,
			Name:            project.Name,
			ClusterCount:    uint32(project.ActiveClustersCount),
			UserCount:       project.UsersCount,
			CreateTimestamp: uint64(project.CreatedAt.GetSeconds()),
			AwsCmekEnabled:  project.AwsCmekEnabled,
			Type:            project.Type,
		}

		projectItems = append(projectItems, projectItem)
	}

	return &cbp.ListProjectsResp{
		Total: uint32(len(projectItems)),
		Items: utils.Pagination(req.Page, req.PageSize, projectItems).([]*cbp.ListProjectItem),
	}, nil
}

func getAccessOrg(rsp *account.GetApiKeyByAccessKeyRsp) uint64 {
	if len(rsp.ResourceInfos) == 0 {
		return 0
	}
	for _, resourceInfo := range rsp.ResourceInfos {
		if strings.ToUpper(resourceInfo.ScopeType) == string(entity.RoleBindingScopeOrg) {
			// return the first org_id
			return resourceInfo.ResourceId
		}
	}
	return 0
}

func checkAccessPermission(ctx context.Context, accessKey string, resourceID uint64) (bool, string, error) {
	resource := "account/openapi/orgs/{org_id}/list_projects"
	resourceParam := map[string]string{
		"org_id": fmt.Sprintf("%d", resourceID),
	}
	return useadapter.CheckApiKeyPermission(ctx, accessKey, resource, resourceParam)
}

func checkCreateProjectPermission(ctx context.Context, accessKey string, resourceID uint64) (bool, string, error) {
	resource := "account/openapi/orgs/{org_id}/create_project"
	resourceParam := map[string]string{
		"org_id": fmt.Sprintf("%d", resourceID),
	}
	return useadapter.CheckApiKeyPermission(ctx, accessKey, resource, resourceParam)
}
