package user

import (
	"context"
	"slices"
	"strings"

	authapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/auth/v1"
	iamapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/iam/v1"
	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	iamentv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/iam/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	auth "github.com/tidbcloud/devops/idl/proto/devops_server/auth/v1"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"

	"github.com/go-logr/logr"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/dig"
	"google.golang.org/grpc"
	"google.golang.org/grpc/status"
)

type AuthControllerParam struct {
	dig.In

	Logger      logr.Logger
	UserService userv1.UserServiceClient
	OpsCore     *opscore.Client
}

// AuthController provide the api to manage users.
type AuthController struct {
	auth.UnimplementedAuthServiceServer

	logger  logr.Logger
	userCli userv1.UserServiceClient
	authCli authapiv1.AuthServiceClient
	iam     iamapiv1.IAMServiceClient
}

func (c *AuthController) Name() string {
	return "AuthController"
}

func NewAuthController(p AuthControllerParam) (ctrl.Controller, error) {
	return &AuthController{
		logger:  p.Logger.WithName("AuthController"),
		userCli: p.UserService,
		authCli: p.OpsCore.Auth,
		iam:     p.OpsCore.IAM,
	}, nil
}

func (c *AuthController) RegisterService(s *grpc.Server) error {
	auth.RegisterAuthServiceServer(s, c)
	return nil
}

func (c *AuthController) HasAccess(ctx context.Context, req *auth.HasAccessReq) (*auth.HasAccessResp, error) {
	item, err := c.getAvailableUser(ctx, req.Email)
	if err != nil {
		return nil, err
	}
	resp, err := c.authCli.Authorize(ctx, &authapiv1.AuthorizeRequest{
		UserEmail: &item.Email,
		Uri:       req.Uri,
		Method:    strings.ToUpper(req.Method),
	})
	if err != nil {
		return nil, err
	}
	hasAccess := resp.Authorized
	if !hasAccess {
		c.logger.Info("user has no access", "email", req.Email, "uri", req.Uri, "method", req.Method)
	}
	return &auth.HasAccessResp{
		HasAccess: hasAccess,
		BaseResp:  &base.BaseResp{},
	}, nil
}

func (c *AuthController) GetUser(ctx context.Context, req *auth.GetUserReq) (*auth.GetUserResp, error) {
	item, err := c.getAvailableUser(ctx, req.Email)
	if err != nil {
		return nil, err
	}
	resp, err := c.iam.ResolveEntityPermissions(ctx, &iamapiv1.ResolveEntityPermissionsRequest{
		Target: &iamentv1.RoleBindingTarget{
			Type: iamentv1.RoleBindingTargetType_ROLE_BINDING_TARGET_TYPE_USER,
			Name: req.Email,
		},
	})
	if err != nil {
		return nil, err
	}
	roles := make([]string, 0)
	roleSet := make(map[string]struct{})
	for _, grantedPermission := range resp.Permissions {
		for _, grantSource := range grantedPermission.GrantPath {
			if grantSource.Role == nil {
				continue
			}
			if _, ok := roleSet[grantSource.Role.Name]; ok {
				continue
			}
			roleSet[grantSource.Role.Name] = struct{}{}
			roles = append(roles, grantSource.Role.Name)
		}
	}

	regions := make([]string, 0)
	alreadyGlobal := false
	for _, role := range roles {
		if alreadyGlobal {
			continue
		}
		alreadyGlobal = isGlobalRole(role)
		if alreadyGlobal {
			continue
		}
		region := roleNameToRegion(role)
		if region != "" {
			regions = append(regions, region)
		}
	}
	return &auth.GetUserResp{
		User: &auth.User{
			Email:              item.Email,
			Available:          item.Available,
			LarkUserId:         item.LarkUserId,
			LarkUserName:       item.LarkUserName,
			LarkDepartmentId:   item.LarkDepartmentId,
			LarkDepartmentName: item.LarkDepartmentName,
			Roles:              roles,
			RegionInfo: &auth.RegionInfo{
				Global:  alreadyGlobal,
				Regions: regions,
			},
		},
		BaseResp: &base.BaseResp{},
	}, nil
}

func (c *AuthController) getAvailableUser(ctx context.Context, email string) (*coreuser.User, error) {
	resp, err := c.userCli.GetUser(ctx, &userv1.GetUserRequest{Email: email})
	if err != nil {
		return nil, err
	}
	if !resp.User.Available {
		return nil, status.Errorf(403, "user %s is not available", email)
	}
	return resp.User, nil
}

func isGlobalRole(name string) bool {
	roleName := strings.ToLower(name)
	if roleName == "admin" {
		return true
	}
	// TODO(yujie): delete when the new roles are switched
	if roleName == "auto-tse" || roleName == "auto-sales" || roleName == "auto-presales" || roleName == "tmp-tse" {
		return true
	}

	if slices.Contains([]string{
		"auto-tse",
		"auto-sales",
		"auto-presales",
		"tmp-tse",
		"department-tse",
		"department-sales",
		"department-presales",
	}, roleName) {
		return true
	}

	if strings.HasSuffix(roleName, "-gs") ||
		strings.HasSuffix(roleName, "-rd") ||
		strings.HasSuffix(roleName, "-bizops") ||
		strings.HasSuffix(roleName, "-pm") ||
		strings.HasSuffix(roleName, "readonly") {
		return true
	}
	return false
}

func roleNameToRegion(name string) string {
	roleName := strings.ToLower(name)
	if strings.Contains(roleName, "-tse-") ||
		strings.Contains(roleName, "-sales-") ||
		strings.Contains(roleName, "-presales-") ||
		strings.Contains(roleName, "-presale-") ||
		strings.Contains(roleName, "-tam-") {
		if strings.HasSuffix(roleName, "-apac") {
			return "APAC"
		} else if strings.HasSuffix(roleName, "-emea") {
			return "EMEA"
		} else if strings.HasSuffix(roleName, "-na") {
			return "NA"
		} else if strings.HasSuffix(roleName, "-japan") {
			return "JAPAN"
		}
	}
	return ""
}
