package auth

import (
	"context"
	"strings"
	"time"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/core-portal/internal/central/config"
	"github.com/tidbcloud/core-portal/internal/central/grpc/grpcli"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/bizerr"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type TenantStatus string

const (
	TenantStatusPending TenantStatus = "pending"
	TenantStatusDeleted TenantStatus = "deleted"
	TenantStatusCleared TenantStatus = "cleared"
)

type Tenant struct {
	ID     entity.ID
	Email  string
	Status TenantStatus
}

type Project struct {
	ID entity.ID
}

type User struct {
	ID             entity.ID
	Email          string
	Status         account.UserStatus
	InvitedAt      *time.Time
	Role           acl.OrgRole
	Projects       []Project
	Tenant         *Tenant
	IsImpersonated bool
}

// IsSubSSO checks if the "sub" claim of auth0 token is sso
func IsSubSSO(subClaim string) bool {
	return strings.HasPrefix(subClaim, config.Auth0SocialConnectionGoogle.String()) ||
		strings.HasPrefix(subClaim, config.Auth0SocialConnectionGithub.String())
}

// The frontend relies on the error message, if you modify the content, you need to synchronize with the frontend.
const (
	// UnregisteredUserErrMsg ... The custom-email user has not registered in the system
	UnregisteredUserErrMsg = "unregistered user"

	// UnregisteredSsoUserErrMsg ... The sso user has not registered in the system
	UnregisteredSsoUserErrMsg = "unregistered sso user"

	// InactiveUserErrMsg ... User has been deleted
	InactiveUserErrMsg = "inactive user"

	// PendingUserErrMsg ... The user did not accept the invitation in the email
	PendingUserErrMsg = "pending user"

	// PendingSignupUserErrMsg ... The user has self-registered and has not activated the account in the email
	PendingSignupUserErrMsg = "pending signup user"
)

func FromTimestamp(t interface{}) *time.Time {
	var tm time.Time
	switch v := t.(type) {
	case timestamppb.Timestamp:
		tm = v.AsTime()
	case *timestamppb.Timestamp:
		if t == nil {
			return nil
		}
		tm = v.AsTime()
	default:
		return nil
	}

	return &tm
}

func buildUser(resp *account.GetAuthUserResponse) *User {
	user := resp.User
	org := resp.Org

	out := &User{
		ID:             entity.ID(user.Id),
		Email:          user.Email,
		Status:         user.Status,
		InvitedAt:      FromTimestamp(user.InvitedAt),
		Role:           resp.OrgRole,
		Projects:       nil, // fill later
		Tenant:         nil, // fill later
		IsImpersonated: resp.IsImpersonated,
	}

	// fill Tenant
	tenant := &Tenant{
		ID:     entity.ID(org.Id),
		Email:  org.Email,
		Status: TenantStatus(org.Status),
	}
	out.Tenant = tenant

	// fill Projects
	for _, p := range resp.AvailableProjects {
		project := Project{
			ID: entity.ID(p.Id),
		}
		out.Projects = append(out.Projects, project)
	}

	return out
}

func getUserByRPC(ctx context.Context, email string, orgID uint64) (*User, bizerr.BizError) {
	req := &account.GetAuthUserRequest{
		UserEmail:    email,
		OrgId:        orgID,
		SkipCheckOrg: false,
	}

	resp, err := grpcli.GetAuthUser(ctx, req)
	if err != nil {
		return nil, err
	}
	return buildUser(resp), nil
}

func GetUser(ctx context.Context, email string, subClaim string, orgID uint64) (*User, error) {
	user, err := getUserByRPC(ctx, email, orgID)
	if err != nil {
		if err.RawErrCode() == bizerr.ErrCodeRecordNotFound {
			if IsSubSSO(subClaim) {
				return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredSsoUserErrMsg))
			}
			return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredUserErrMsg))
		}

		return nil, err
	}

	if user.Tenant.Status == TenantStatusCleared || user.Tenant.Status == TenantStatusDeleted {
		return nil, errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
	}

	switch user.Status {
	case account.UserStatus_USER_STATUS_ACTIVE:
		return user, nil
	case account.UserStatus_USER_STATUS_PENDING:
		if user.InvitedAt != nil {
			return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
		}
		if user.Email == user.Tenant.Email && user.Tenant.Status == TenantStatusPending {
			return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingSignupUserErrMsg))
		}
		return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
	case account.UserStatus_USER_STATUS_INACTIVE:
		return nil, errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
	}

	return user, nil
}

func GetOrCreateSystemUser(ctx context.Context, orgID uint64) (*User, error) {
	req := &account.GetOrCreateSystemUserRequest{
		OrgId: orgID,
	}
	resp, err := grpcli.GetOrCreateSystemUser(ctx, req)
	if err != nil {
		return nil, err
	}
	return GetUser(ctx, resp.User.Email, "", orgID)
}

// GetUserFromContext gets the auth.User from context if exists.
func GetUserFromContext(ctx context.Context) (*User, error) {
	claimsIFace := ctx.Value(auth0.AuthTokenClaimKey)
	if claimsIFace == nil {
		return nil, errors.New("user not found in context")
	}
	user, ok := claimsIFace.(*User)
	if !ok {
		return nil, errors.New("expect auth.user in context")
	}
	return user, nil
}

func userInProject(userProjects []Project, projectID entity.ID) bool {
	userInProject := false
	for _, project := range userProjects {
		if project.ID == projectID {
			userInProject = true
		}
	}
	return userInProject
}

func (u *User) ContainProject(projectID entity.ID) bool {
	return userInProject(u.Projects, projectID)
}
