package user

import (
	"context"
	"time"

	accounterr "github.com/tidbcloud/account/commons/errors"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/pkgv2/auth/auth0"

	"github.com/golang/protobuf/ptypes/timestamp"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"

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

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	accounthandler "github.com/tidbcloud/account/thirdparty/account"
)

const (
	UserIsNotInOrgErrMsg = "user is not in org"
)

type User struct {
	entity.User
	Role           acl.OrgRole
	Status         account.UserStatus
	TenantID       entity.ID
	IsImpersonated bool
}

func timePointer(t *timestamp.Timestamp) *time.Time {
	if t == nil {
		return nil
	}

	if t.Seconds == 0 && t.Nanos == 0 {
		return nil
	}

	tt := t.AsTime()
	if tt.IsZero() {
		return nil
	}

	return &tt
}

func convertUser(user *account.User) *entity.User {
	return &entity.User{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(user.Id),
				CreatedAt: user.CreatedAt.AsTime(),
			},
			UpdatedAt: timePointer(user.UpdatedAt),
		},
		FirstName:   user.FirstName,
		LastName:    user.LastName,
		Email:       user.Email,
		InvitedAt:   timePointer(user.InvitedAt),
		Job:         user.Job,
		PhoneNumber: user.PhoneNumber,
	}
}

func convertOrg(org *account.Org) *entity.Tenant {
	return &entity.Tenant{

		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(org.Id),
				CreatedAt: org.CreatedAt.AsTime(),
			},
			UpdatedAt: timePointer(org.UpdatedAt),
		},
		Name:                      org.Name,
		Alias:                     org.Alias,
		Email:                     org.Email,
		Country:                   org.Country,
		TimeZone:                  org.TimeZone,
		Status:                    entity.TenantStatus(org.Status),
		POCBindedPackageID:        org.PocBindedPackageId,
		OnDemandBindedPackageID:   org.OnDemandBindedPackageId,
		DevTierBindedPackageID:    org.DevTierBindedPackageId,
		Kind:                      entity.TenantKind(org.Kind),
		AcceptService:             org.AcceptService,
		ZendeskID:                 entity.ID(org.ZendeskId),
		ThirdPartyAccountProvider: &org.ThirdPartyAccountProvider,
		ThirdPartyAccountCode:     &org.ThirdPartyAccountCode,
		ThirdPartyFreeTrial:       org.ThirdPartyFreeTrial,
		//Quotas:                    []*entity.Quota{},   // WARN: Empty value, do not use
		Projects:       []*entity.Project{}, // WARN: Empty value, do not use
		BillingEmail:   org.BillingEmail,
		AccountManager: utils.GetOrgAccountManager(context.Background(), org.Id),
	}
}

func convertProject(project *account.Project) *entity.Project {
	return &entity.Project{
		Updatable: entity.Updatable{
			Base: entity.Base{
				ID:        entity.ID(project.Id),
				CreatedAt: project.CreatedAt.AsTime(),
			},
			UpdatedAt: timePointer(project.UpdatedAt),
		},
		Name:           project.Name,
		TenantID:       entity.ID(project.TenantId),
		Tenant:         &entity.Tenant{},      // WARN: Empty value, do not use
		Users:          []entity.User{},       // WARN: Empty value, do not use
		OpenAPIKeys:    []entity.OpenAPIKey{}, // WARN: Empty value, do not use
		Status:         entity.ProjectStatus(project.Status),
		DeletedTime:    timePointer(project.DeletedAt),
		IsCrossAccount: project.IsCrossAccount,
	}
}

func GetUser(ctx context.Context, email string, orgID uint64, skipCheckOrg bool) (*User, bool, error) {
	tilog.Info(ctx, "GetUser  param", zap.String("email", email), zap.Any("orgID", orgID), zap.Any("skipCheckOrg", skipCheckOrg))
	if config.IsLocalEnv() {
		return getUserForLocalEnv(ctx, email, orgID, skipCheckOrg)
	}
	req := &account.GetAuthUserRequest{
		UserEmail:    email,
		OrgId:        orgID,
		SkipCheckOrg: skipCheckOrg,
	}
	authUser, err := accounthandler.GetHandler().GetAuthUser(ctx, req)
	//	tilog.Info(ctx, "GetUser  GetAuthUser result", zap.Any("authUser", authUser))
	if err != nil {
		if errs.IsError(err, errs.ErrNotFound) {
			return nil, false, nil
		}
		if errs.IsError(err, accounterr.ErrUserNotInOrg) {
			tilog.Warn(ctx, "Get auth user failed, user is not in org", zap.String("Email", email), zap.Uint64("OrgId", orgID), zap.Any("err", err))
			return nil, false, errcode.NewNotAuthenticatedErr(errors.New(UserIsNotInOrgErrMsg))
		}
		if errs.IsError(err, errs.ErrForbidden) {
			return nil, false, errcode.NewForbiddenErr(err)
		}
		tilog.Warn(ctx, "Get auth user failed", zap.String("Email", email), zap.Uint64("OrgId", orgID), zap.Error(err))
		return nil, false, errcode.NewNotAuthenticatedErr(err)
	}

	user := convertUser(authUser.User)
	user.Tenant = convertOrg(authUser.Org)
	for _, project := range authUser.AvailableProjects {
		user.Projects = append(user.Projects, *convertProject(project))
	}

	return &User{
		User:           *user,
		Role:           authUser.OrgRole,     // WARN: DO NOT use this field as entity.UserRole (nor string)
		Status:         authUser.User.Status, // WARN: DO NOT use this field as entity.UserStatus (nor string)
		TenantID:       user.Tenant.ID,
		IsImpersonated: authUser.IsImpersonated,
	}, true, nil
}

func getUserForLocalEnv(ctx context.Context, email string, orgID uint64, skipCheckOrg bool) (*User, bool, error) {
	var user entity.User
	tenantID := entity.ID(orgID)
	err := database.Transaction(func(tx *gorm.DB) error {
		user, _ = GetUserByEmail(tx, email)
		if skipCheckOrg {
			tenantID = user.TenantID
			return nil
		}
		inOrg, err := dao.GetDaoHub().Cross.IsUserInOrg(ctx, tx.WithContext(ctx), user.ID.Uint64(), orgID)
		if err != nil {
			return err
		}
		if !inOrg {
			return errcode.NewNotAuthenticatedErr(errors.New(UserIsNotInOrgErrMsg))
		}
		return nil
	})
	if err != nil {
		return nil, false, err

	}

	return &User{
		User:           user,
		TenantID:       tenantID,
		IsImpersonated: false,
	}, true, nil
}

// GetUserFromContext gets the tenantctx.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 in context")
	}
	user, ok := claimsIFace.(*User)
	if !ok {
		return nil, errors.New("expect type tenant.User in context")
	}
	return user, nil
}

// GetUserFromContext gets the tenantctx.User from context if exists.
func GetUserSubFromContext(ctx context.Context) (string, error) {
	subIFace := ctx.Value(entity.ClaimsSubKey)
	if subIFace == nil {
		return "", errors.New("user sub not exist in context")
	}
	sub, ok := subIFace.(string)
	if !ok {
		return "", errors.New("expect type sub in context")
	}
	return sub, nil
}
