package oauth2

import (
	"context"
	"fmt"
	"time"

	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/auth-server/commons/config"
	dto "github.com/tidbcloud/auth-server/internal/dto/oauth2"
	"github.com/tidbcloud/auth-server/internal/entity"
	"github.com/tidbcloud/auth-server/internal/repository"
	tierrs "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
)

const (
	UserCodeInvalidMsg = "The code is invalid or expired. Please return to your terminal and re-initiate login request."
)

func (o *Oauth2ServiceImpl) HandleInnerDeviceCodeCheckRequest(ctx context.Context, userCode string) (*dto.InnerDeviceCheckResponse, tierrs.Error) {
	db := o.db.WithContext(ctx)
	grant, err := repository.GetOauth2DeviceGrantByUserCode(ctx, db, userCode)

	if err != nil || !CanApproveOrDenyDeviceGrant(ctx, grant) {
		tilog.Info(ctx, UserCodeInvalidMsg)
		return nil, tierrs.NewNotFound(UserCodeInvalidMsg, nil)
	}

	var resp = &dto.InnerDeviceCheckResponse{
		InnerBasicResponse: dto.InnerDefaultSuccessResponse(),
	}
	resp.Data.CreatedAt = grant.CreatedAt.Format(time.RFC3339)
	resp.Data.IP = grant.ClientIP
	resp.Data.UserAgent = grant.UserAgent
	return resp, nil
}

func (o *Oauth2ServiceImpl) HandleInnerDeviceCodeConfirmationRequest(ctx context.Context, user *account.User, userCode string, approve bool, orgId uint64) tierrs.Error {
	if approve {
		if !config.IsLocalEnv() {
			ret, err := IsUserInOrg(ctx, user, orgId, o.accountProviderClient)
			if err != nil {
				tilog.Error(ctx, "IsUserInOrg failed", zap.Uint64("userId", user.Id), zap.Uint64("orgId", orgId), zap.Error(err))
				return tierrs.NewInternalServerError("failed to check if user is in the org", err)
			}
			if !ret {
				tilog.Info(ctx, "user is not in the org", zap.Uint64("userId", user.Id), zap.Uint64("orgId", orgId), zap.Error(err))
				return tierrs.NewBadRequest("user is not in the org", nil)
			}
		}
	}

	var bizErr tierrs.Error
	db := o.db.WithContext(ctx)
	err := db.Transaction(func(tx *gorm.DB) error {
		grant, err := repository.GetOauth2DeviceGrantByUserCodeWithLock(ctx, tx, userCode)
		if err != nil || !CanApproveOrDenyDeviceGrant(ctx, grant) {
			bizErr = tierrs.NewNotFound(UserCodeInvalidMsg, nil)
			return nil
		}
		if approve {
			return ApproveOauth2DeviceGrant(ctx, tx, grant, user, orgId)
		} else {
			return DenyOauth2DeviceGrant(ctx, tx, grant, user)
		}
	})

	if err != nil {
		tilog.Error(
			ctx, "failed to handle device code confirmation request",
			zap.Uint64("userId", user.Id), zap.String("userCode", userCode),
			zap.Bool("approve", approve), zap.Uint64("orgId", orgId), zap.Error(err),
		)
		bizErr = tierrs.NewInternalServerError("failed to handle device code confirmation request", err)
	}
	return bizErr
}

// CanApproveOrDenyDeviceGrant check if the device grant is expired and not revoked
func CanApproveOrDenyDeviceGrant(ctx context.Context, grant *entity.Oauth2DeviceGrant) bool {
	if grant.Status != entity.Oauth2GrantStatusPending {
		tilog.Info(ctx, fmt.Sprintf("the DeviceGrant[%s] is not pending ", grant.UserCode))
		return false
	}
	if grant.RevokedAt != nil {
		tilog.Info(ctx, fmt.Sprintf("the DeviceGrant[%s] is revoked ", grant.UserCode))
		return false
	}
	if grant.CreatedAt.Add(time.Duration(grant.ExpiresIn) * time.Second).Before(time.Now()) {
		tilog.Info(ctx, fmt.Sprintf("the DeviceGrant[%s] is expire ", grant.UserCode))
		return false
	}
	return true
}

// IsUserInOrg check if the user is in the org
func IsUserInOrg(ctx context.Context, user *account.User, orgId uint64, accountProviderClient account.AccountAPIServiceClient) (bool, error) {
	resp, err := accountProviderClient.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{UserId: user.Id})
	if err != nil {
		return false, err
	}

	for _, i := range resp.Orgs {
		if i.Id == orgId {
			return true, nil
		}
	}

	return false, nil
}

func ApproveOauth2DeviceGrant(ctx context.Context, db *gorm.DB, grant *entity.Oauth2DeviceGrant, user *account.User, orgId uint64) error {
	// device grant's scopes will follow user's role
	grant.Scopes = "[]"
	// device grant will have access to all projects of the org that the user is in
	grant.GrantAllProjects = true
	grant.ProjectIDs = entity.ProjectIDs{IDs: []uint64{}}
	grant.UserID = user.Id
	grant.UserEmail = user.Email
	grant.Status = entity.Oauth2GrantStatusApprove
	grant.OrgID = orgId
	return repository.UpdateOauth2DeviceGrant(ctx, db, grant, []entity.Oauth2DeviceGrantDBSchemaField{
		entity.Oauth2DeviceGrantDBSchema.Scopes,
		entity.Oauth2DeviceGrantDBSchema.GrantAllProjects,
		entity.Oauth2DeviceGrantDBSchema.ProjectIDs,
		entity.Oauth2DeviceGrantDBSchema.UserID,
		entity.Oauth2DeviceGrantDBSchema.UserEmail,
		entity.Oauth2DeviceGrantDBSchema.Status,
		entity.Oauth2DeviceGrantDBSchema.OrgID,
	})
}

func DenyOauth2DeviceGrant(ctx context.Context, db *gorm.DB, grant *entity.Oauth2DeviceGrant, user *account.User) error {
	now := time.Now()
	grant.RevokedAt = &now
	grant.UserID = user.Id
	grant.UserEmail = user.Email
	grant.Status = entity.Oauth2GrantStatusDeny
	return repository.UpdateOauth2DeviceGrant(ctx, db, grant, []entity.Oauth2DeviceGrantDBSchemaField{
		entity.Oauth2DeviceGrantDBSchema.RevokedAt,
		entity.Oauth2DeviceGrantDBSchema.UserID,
		entity.Oauth2DeviceGrantDBSchema.UserEmail,
		entity.Oauth2DeviceGrantDBSchema.Status,
	})
}
