package oauth2

import (
	"context"
	"time"

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

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

func (o *Oauth2ServiceImpl) CreateAccessTokenByCodeGrant(
	ctx context.Context, clientID, clientSecret, code, redirectURI string,
) (*dto.TokenResponse, error) {
	db := o.db.WithContext(ctx)
	app, err := repository.GetOauth2ApplicationByClientID(ctx, db, clientID)
	if err != nil {
		return nil, errs.ErrOauth2InvalidClient
	}
	if app.GrantType != entity.Oauth2AuthorizationCode {
		return nil, errs.ErrOauth2InvalidClient
	}
	passwordEncryptor := crypto.NewPbdkf2Sha512Encryptor()
	if !passwordEncryptor.Compare(clientSecret, app.ClientSecret) {
		tilog.Info(ctx, "client secret not match", zap.String("clientID", clientID))
		return nil, errs.ErrOauth2InvalidClient
	}
	var (
		resp   *dto.TokenResponse
		bizErr error
	)
	now := time.Now()
	err = db.Transaction(func(tx *gorm.DB) error {
		grant, err := repository.GetOauth2CodeGrantWithLock(ctx, tx, code)
		if err != nil {
			tilog.Info(ctx, "failed to get code grant", zap.Error(err))
			bizErr = errs.ErrOauth2InvalidGrant
			return nil
		}
		if grant.ApplicationID != uint64(app.ID) {
			tilog.Info(ctx, "application id not match", zap.Any("clientID", clientID))
			bizErr = errs.ErrOauth2InvalidGrant
			return nil
		}
		if grant.RedirectURI != redirectURI {
			tilog.Info(ctx, "redirect uri not match", zap.Any("req.redirectURI", redirectURI), zap.Any("grant.RedirectURI", grant.RedirectURI))
			bizErr = errs.ErrOauth2RedirectURIMismatch
			return nil
		}
		if codeGrantExpired(grant, now) {
			tilog.Info(ctx, "code grant expired", zap.Any("code", code))
			bizErr = errs.ErrOauth2InvalidGrant
			return nil
		}
		akObj, ak, rk, err := createAccessTokenByCodeGrant(ctx, tx, grant, app, o.accountProviderClient)
		if err != nil {
			tilog.Error(ctx, "failed to create access token", zap.Error(err))
			return err
		}
		err = repository.DeleteOauth2CodeGrant(ctx, tx, grant)
		if err != nil {
			tilog.Error(ctx, "failed to delete code grant", zap.Error(err))
			return err
		}
		resp = &dto.TokenResponse{
			AccessToken:  ak,
			TokenType:    TokenTypeBearer,
			ExpiresIn:    akObj.AccessTokenExpiresIn,
			RefreshToken: rk,
		}
		return nil
	})

	if err != nil {
		tilog.Error(ctx, "failed to handle code grant token request", zap.Error(err))
		return nil, errs.ErrOauth2ServerError
	}
	return resp, bizErr
}

func codeGrantExpired(grant *entity.Oauth2CodeGrant, now time.Time) bool {
	expiresAt := grant.CreatedAt.Add(time.Duration(grant.ExpiresIn) * time.Second)
	return now.After(expiresAt)
}

func createAccessTokenByCodeGrant(
	ctx context.Context, db *gorm.DB, grant *entity.Oauth2CodeGrant,
	application *entity.Oauth2Application, accountProvider account.AccountAPIServiceClient,
) (obj *entity.Oauth2AccessToken, accessToken string, refreshToken string, err error) {
	obj = &entity.Oauth2AccessToken{
		AccessTokenExpiresIn:  application.AccessTokenExpiresIn,
		RefreshTokenExpiresIn: application.RefreshTokenExpiresIn,
		ApplicationID:         grant.ApplicationID,
		UserID:                grant.UserID,
		UserEmail:             grant.UserEmail,
		OrgID:                 grant.OrgID,
		Scopes:                grant.Scopes,
		ProjectIDs:            grant.ProjectIDs,
		GrantAllProjects:      false,
		GrantType:             entity.Oauth2AuthorizationCode,
	}
	accessToken, refreshToken, err = createAccessTokenWithRetry(ctx, db, obj, accountProvider)
	return
}
