package session

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	token2 "github.com/tidbcloud/account/internel/svc/token"

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

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

	"github.com/form3tech-oss/jwt-go"
	"github.com/tidbcloud/account/internel/dao/entity"

	"gorm.io/gorm"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	sessionAliveDuration = time.Hour * 2
	renewalWindowSeconds = 60 * 5
	cacheUserDuration    = time.Hour * 3

	duplicateEntryErr = "Error 1062: Duplicate entry"
)

const (
	sessionFlushInterval  = 10 * time.Second
	sessionFlushBatchSize = 500
)

var (
	sessionDirtyFlushOnce sync.Once
	sessionDirtySet       sync.Map
)

type UserSessionSvcIface interface {
	OpenLoginSession(ctx context.Context, token string) (string, error)
	RevokeLoginSessionBySessionID(ctx context.Context, sid string) error
	RevokeLoginSessionByEmail(ctx context.Context, email string) error

	AuthnLoginSession(ctx context.Context, p *AuthnSessionParam) (*AuthnSessionRet, error)
	AuthnLoginSessionByToken(ctx context.Context, token string) (*AuthnSessionRet, error)
}

type AuthnSessionParam struct {
	SessionID    string `json:"sid"`
	RenewSession bool   `json:"renew"`
}

type AuthnSessionRet struct {
	OK         bool      `json:"ok"`
	Message    string    `json:"message"`
	UserMeta   *UserMeta `json:"user_meta"`
	Auth0Token string    `json:"auth0_token"`
}

type UserMeta struct {
	ID          uint64    `json:"id"`
	Email       string    `json:"email"`
	FirstName   string    `json:"first_name"`
	LastName    string    `json:"last_name"`
	Status      string    `json:"status"`
	Job         string    `json:"job"`
	PhoneNumber string    `json:"phone_number"`
	CreatedAt   time.Time `json:"created_at"`
}

var (
	userSessionSvcInitOnce sync.Once
	userSessionSvcInstance UserSessionSvcIface
)

type UserSessionSvc struct {
}

func GetUserSessionSvc() UserSessionSvcIface {
	userSessionSvcInitOnce.Do(func() {
		userSessionSvcInstance = &UserSessionSvc{}

	})
	return userSessionSvcInstance
}

func (s *UserSessionSvc) OpenLoginSession(ctx context.Context, token string) (string, error) {
	tilog.Info(ctx, "OpenLoginSession param", zap.Any("token", token))
	// validate token
	tokensvc, _ := token2.GetTokenSvc()
	claims, err := tokensvc.GetClaimsFromToken(ctx, token)
	if err != nil {
		tilog.Info(ctx, "OpenLoginSession GetClaimsFromToken fail: token is invalid", zap.Any("err", err))
		return "", errs.NewUnauthorized("token is invalid", err)
	}
	tilog.Info(ctx, "OpenLoginSession get claims from token", zap.Any("claims", claims))

	// check whether token already open session
	opened, sid, err := tokenIdempotentCheck(ctx, claims)
	if err != nil {
		return "", err
	}
	if opened {
		return sid, nil
	}

	// gen cache session
	cSession, err := genCacheSessionFromToken(claims, token)
	if err != nil {
		tilog.Info(ctx, "OpenLoginSession genCacheSessionFromToken error", zap.Any("err", err))
		return "", err
	}
	expiredAt := time.Unix(cSession.SessionExp, 0)
	sEnt := &entity.UserSession{
		SessionID: cSession.SessionID,
		Email:     cSession.Email,
		ExpiredAt: &expiredAt,
		IDToken:   token,
	}

	err = database.Transaction(func(tx *gorm.DB) error {
		err := dao.GetDaoHub().UserSession.CreateUserSession(ctx, tx, sEnt)
		if err != nil {
			tilog.Warn(ctx, "OpenLoginSession CreateUserSession error", zap.Any("ent", sEnt), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		err = setCacheSession(ctx, cSession, getSessionAliveDuration())
		if err != nil {
			tilog.Error(ctx, "OpenLoginSession setCacheSession error", zap.Any("cache_session", cSession), zap.Any("err", err))
			return errs.NewInternalUnknown("internal error", err)
		}
		tilog.Info(ctx, "OpenLoginSession success", zap.Any("cache_session", cSession))
		return nil
	})
	// ignore duplicate entry error
	if err != nil && !isSidDuplicateEntryErr(err) {
		return "", err
	}
	return cSession.SessionID, nil

}

func isSidDuplicateEntryErr(err error) bool {
	for err != nil {
		if strings.Contains(err.Error(), duplicateEntryErr) {
			return true
		}
		c, ok := err.(interface{ Cause() error })
		if !ok {
			return false
		}
		err = c.Cause()
	}
	return false
}

func tokenIdempotentCheck(ctx context.Context, claims jwt.MapClaims) (bool, string, error) {
	sid, _, err := getSidIatFromClaims(claims)
	if err != nil {
		tilog.Error(ctx, "OpenLoginSession getSidIatFromClaims error", zap.Any("err", err))
		return false, "", errs.NewInternalUnknown("get sid/iat internal error", err)
	}
	sEnt, err := dao.GetDaoHub().UserSession.GetUserSessionBySessionID(ctx, database.DBConn.DB, sid)
	if err != nil {
		tilog.Error(ctx, "OpenLoginSession GetUserSessionBySessionID error", zap.Any("sid", sid), zap.Any("err", err))
		return false, "", errs.NewInternalUnknown("get session internal error", err)
	}
	if sEnt == nil {
		tilog.Info(ctx, "OpenLoginSession token not used, to open session", zap.Any("sid", sid))
		return false, "", nil
	}
	cSession, err := getCacheSession(ctx, sid)
	if err != nil {
		if isRedisError(err) {
			tilog.Warn(ctx, "OpenLoginSession get cache session redis error, using DB result",
				zap.Any("sid", sid), zap.Any("err", err))
			if sEnt.RevokedAt != nil {
				tilog.Info(ctx, "OpenLoginSession token already used and session is revoked, to refuse", zap.Any("sid", sid))
				return false, sid, errs.NewBadRequest("token already used", nil)
			}
			if sEnt.ExpiredAt != nil && time.Now().After(*sEnt.ExpiredAt) {
				tilog.Info(ctx, "OpenLoginSession token already used and session is expired, to refuse", zap.Any("sid", sid))
				return false, sid, errs.NewBadRequest("token already used", nil)
			}
			tilog.Info(ctx, "OpenLoginSession token already used and session is valid (from DB), to reuse", zap.Any("sid", sid))
			return true, sid, nil
		}
		tilog.Error(ctx, "OpenLoginSession get cache session error", zap.Any("sid", sid), zap.Any("err", err))
		return false, "", errs.NewInternalUnknown("get cache session internal error", err)
	}
	if cSession != nil {
		tilog.Info(ctx, "OpenLoginSession token already used and session is valid, to reuse", zap.Any("sid", sid))
		return true, sid, nil
	}
	tilog.Info(ctx, "OpenLoginSession token already used and session is expired, to refuse", zap.Any("sid", sid))
	return false, "", errs.NewBadRequest("token already used", nil)

}

func getSidIatFromClaims(claims jwt.MapClaims) (string, int64, error) {
	isid, ok := claims["sid"]
	if !ok {
		return "", 0, errs.NewInternalUnknown("sid not found in claims", nil)
	}
	sid, ok := isid.(string)
	if !ok {
		return "", 0, errs.NewInternalUnknown("sid not string in claims", nil)
	}

	iiat, ok := claims["iat"]
	if !ok {
		return "", 0, errs.NewInternalUnknown("iat not found in claims", nil)
	}
	fiat, ok := iiat.(float64)
	if !ok {
		return "", 0, errs.NewInternalUnknown("iat not float64 in claims", nil)
	}
	iat := int64(fiat)

	return sid, iat, nil
}

func genCacheSessionFromToken(claims jwt.MapClaims, token string) (*cacheSessionVal, error) {
	isid, ok := claims["sid"]
	if !ok {
		return nil, errs.NewInternalUnknown("sid not found in claims", nil)
	}
	sid, ok := isid.(string)
	if !ok {
		return nil, errs.NewInternalUnknown("sid not string in claims", nil)
	}
	iemail, ok := claims["email"]
	if !ok {
		return nil, errs.NewInternalUnknown("email not found in claims", nil)
	}
	email, ok := iemail.(string)
	if !ok {
		return nil, errs.NewInternalUnknown("email not string in claims", nil)
	}
	iusage, ok := claims["usage"]
	if !ok {
		return nil, errs.NewInternalUnknown("usage not found in claims", nil)
	}
	usage, ok := iusage.(string)
	if !ok {
		return nil, errs.NewInternalUnknown("usage not string in claims", nil)
	}
	iexp, ok := claims["exp"]
	if !ok {
		return nil, errs.NewInternalUnknown("exp not found in claims", nil)
	}
	fexp, ok := iexp.(float64)
	if !ok {
		return nil, errs.NewInternalUnknown("exp not float64 in claims", nil)
	}

	sess := &cacheSessionVal{
		RawToken:   token,
		SessionID:  sid,
		Email:      email,
		Usage:      usage,
		TokenExp:   int64(fexp),
		SessionExp: time.Now().Add(getSessionAliveDuration()).Unix(),
	}

	return sess, nil
}

func getSessionAliveDuration() time.Duration {
	if config.IsProdEnv() {
		return sessionAliveDuration
	}
	return time.Hour * 10
}

func (s *UserSessionSvc) RevokeLoginSessionByEmail(ctx context.Context, email string) error {
	tilog.Info(ctx, "RevokeLoginSessionByEmail param", zap.Any("email", email))
	if email == "" {
		return errs.NewBadRequest("email is blank", nil)
	}
	sDao := dao.GetDaoHub().UserSession
	tx := database.DBConn.DB

	sessions, err := sDao.ListUserSessionsByEmail(ctx, tx, email)
	if err != nil {
		tilog.Error(ctx, "RevokeLoginSessionByEmail ListUserSessionsByEmail error", zap.Any("email", email))
		return err
	}
	var sids []string
	for _, sess := range sessions {
		sids = append(sids, sess.SessionID)
	}
	tilog.Info(ctx, "RevokeLoginSessionByEmail start to delete email relate all sessions", zap.Any("email", email), zap.Any("sessions", sessions))

	err = deleteCacheSession(ctx, sids...)
	if err != nil {
		tilog.Error(ctx, "RevokeLoginSessionByEmail deleteCacheSession error", zap.Any("sids", sids))
		return err
	}
	// just async to rec the change
	go func() {
		now := time.Now()
		session := &entity.UserSession{
			Email:     email,
			RevokedAt: &now,
		}
		_ = dao.GetDaoHub().UserSession.UpdateUserSessionByEmail(context.Background(), database.DBConn.DB, session)
	}()
	tilog.Info(ctx, "RevokeLoginSessionByEmail delete email relate all sessions success", zap.Any("email", email), zap.Any("sids", sids))
	return nil

}

func (s *UserSessionSvc) RevokeLoginSessionBySessionID(ctx context.Context, sid string) error {
	tilog.Info(ctx, "RevokeUserSessionBySessionID param", zap.Any("sid", sid))
	if sid == "" {
		return errs.NewBadRequest("session_id is blank", nil)
	}
	sDao := dao.GetDaoHub().UserSession
	sEnt, err := sDao.GetUserSessionBySessionID(ctx, database.DBConn.DB, sid)
	if err != nil {
		tilog.Error(ctx, "RevokeLoginSessionBySessionID GetUserSessionBySessionID error", zap.Any("sid", sid))
		return err
	}
	if sEnt == nil {
		tilog.Info(ctx, "RevokeUserSessionBySessionID session not found, ignore", zap.Any("sid", sid))
		return nil
	}
	tilog.Info(ctx, "RevokeUserSessionBySessionID start to delete special session", zap.Any("session_ent", sEnt))

	err = deleteCacheSession(ctx, sid)
	if err != nil {
		tilog.Warn(ctx, "RevokeLoginSessionBySessionID deleteCacheSession error", zap.Any("sid", sid))
		return err
	}
	// just async to rec the change
	go func() {
		now := time.Now()
		session := &entity.UserSession{
			SessionID: sid,
			RevokedAt: &now,
		}
		_ = dao.GetDaoHub().UserSession.UpdateUserSessionBySessionID(context.Background(), database.DBConn.DB, session)
	}()

	tilog.Info(ctx, "RevokeLoginSessionBySessionID delete special session success", zap.Any("session_ent", sEnt))
	return nil
}

func (s *UserSessionSvc) AuthnLoginSessionByToken(ctx context.Context, token string) (*AuthnSessionRet, error) {
	tilog.Info(ctx, "AuthnLoginSessionByToken param", zap.Any("token", token))
	if token == "" {
		return nil, errs.NewBadRequest("token is blank", nil)
	}
	// validate token
	tokensvc, _ := token2.GetTokenSvc()
	claims, err := tokensvc.GetClaimsFromToken(ctx, token)
	if err != nil {
		tilog.Info(ctx, "AuthnLoginSessionByToken fail: token is invalid", zap.Any("err", err))
		return &AuthnSessionRet{
			Message: "token is invalid",
		}, nil
	}
	// get sid
	isid, ok := claims["sid"]
	if !ok {
		return nil, errs.NewInternalUnknown("sid not found in claims", nil)
	}
	sid, ok := isid.(string)
	if !ok {
		return nil, errs.NewInternalUnknown("sid not string in claims", nil)
	}

	p := AuthnSessionParam{
		SessionID: sid,
	}
	return s.AuthnLoginSession(ctx, &p)
}

func (s *UserSessionSvc) AuthnLoginSession(ctx context.Context, p *AuthnSessionParam) (*AuthnSessionRet, error) {
	tilog.Info(ctx, "AuthnLoginSession param", zap.Any("param", p))
	if p == nil || p.SessionID == "" {
		return &AuthnSessionRet{
			Message: "session_id is blank",
		}, nil
	}

	start := time.Now()
	defer func() {
		metrics.RecAuthnSessionMetrics("AuthnLoginSession", "total", start, true)
	}()

	// validate session
	cSession, err := getCacheSession(ctx, p.SessionID)
	if err != nil {
		tilog.Warn(ctx, "AuthnLoginSession get cache session error, trying DB fallback",
			zap.Any("sid", p.SessionID), zap.Any("err", err))

		if !isRedisError(err) {
			tilog.Error(ctx, "AuthnLoginSession get cache session non-redis error",
				zap.Any("sid", p.SessionID), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("get cache session internal error", err)
		}

		cSession, err = s.getSessionFromDB(ctx, p.SessionID)
		if err != nil {
			tilog.Error(ctx, "AuthnLoginSession DB fallback also failed",
				zap.Any("sid", p.SessionID), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("get session error after fallback", err)
		}
	}
	if cSession == nil {
		tilog.Info(ctx, "AuthnLoginSession fail: session not found", zap.Any("sid", p.SessionID))
		return &AuthnSessionRet{
			Message: "session is expired",
		}, nil
	}
	tilog.Info(ctx, "AuthnLoginSession get cache session result",
		zap.Any("sid", p.SessionID), zap.Any("email", cSession.Email), zap.Any("session_exp", cSession.SessionExp))
	curTs := time.Now().Unix()
	if curTs > cSession.TokenExp {
		tilog.Info(ctx, "AuthnLoginSession fail: current time has exceeded token's alive period", zap.Any("sid", p.SessionID), zap.Any("cur_ts", curTs), zap.Any("token_exp", cSession.TokenExp))
		return &AuthnSessionRet{
			Message: "token is expired",
		}, nil
	}
	if curTs > cSession.SessionExp {
		tilog.Info(ctx, "AuthnLoginSession fail: current time has exceeded session's alive period", zap.Any("sid", p.SessionID), zap.Any("cur_ts", curTs), zap.Any("session_exp", cSession.SessionExp))
		return &AuthnSessionRet{
			Message: "session is expired",
		}, nil
	}

	// renew session
	if p.RenewSession {
		renewLoginSession(ctx, cSession, curTs)
	}

	// get user info
	cUser, err := getAndCacheUser(ctx, cSession.Email)
	if err != nil {
		tilog.Error(ctx, "AuthnLoginSession getAndCacheUser error", zap.Any("err", err))
		return nil, err
	}
	if cUser == nil {
		tilog.Error(ctx, "AuthnLoginSession get cache user fail")
		return nil, errs.NewInternalUnknown("get cache user internal error", err)
	}

	ret := &AuthnSessionRet{
		OK:         true,
		Message:    "authenticate success",
		Auth0Token: cSession.RawToken,
		UserMeta:   convCacheUserToUserMeta(cUser),
	}
	tilog.Info(ctx, "AuthnLoginSession success", zap.Any("sid", p.SessionID), zap.Bool("ok", ret.OK))
	return ret, nil
}

func convCacheUserToUserMeta(cUser *cacheUserVal) *UserMeta {
	return &UserMeta{
		ID:          cUser.ID,
		Email:       cUser.Email,
		FirstName:   cUser.FirstName,
		LastName:    cUser.LastName,
		Status:      cUser.Status,
		Job:         cUser.Job,
		PhoneNumber: cUser.PhoneNumber,
		CreatedAt:   time.Unix(cUser.CreatedAt, 0),
	}
}

func getAndCacheUser(ctx context.Context, email string) (*cacheUserVal, error) {
	start := time.Now().UnixMilli()
	var cnt = 1
	for {
		// 1. get user from redis
		cUser, err := getCacheUser(ctx, email)
		if err != nil {
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d get cache user error", cnt), zap.Any("email", email))
			return nil, errs.NewInternalUnknown("get cache user internal error", err)
		}
		if cUser != nil {
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d get cache user success", cnt), zap.Any("email", email))
			return cUser, nil
		}

		tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d get cache user is nil, to acquire load user lock", cnt), zap.Any("email", email))

		// 2. add lock
		locked, err := setLoadUserLock(ctx, email, time.Millisecond*100)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("AuthnLoginSession round %d set load user lock error", cnt), zap.Any("email", email))
			return nil, errs.NewInternalUnknown("set load user lock internal error", err)
		}

		// 3. locked to load db
		if locked {
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d acquired lock, to load db and set cache", cnt), zap.Any("email", email))
			uEnt, err := dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB, email)
			if err != nil {
				tilog.Error(ctx, fmt.Sprintf("AuthnLoginSession round %d get user by email error", cnt), zap.Any("email", email), zap.Any("err", err))
				err = delLoadUserLock(ctx, email)
				tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d get db error to del load user lock result", cnt), zap.Any("email", email), zap.Any("err", err))
				return nil, errs.NewInternalUnknown("get user by email error", err)
			}
			cUser = &cacheUserVal{
				ID:          uEnt.ID.Uint64(),
				Email:       uEnt.Email,
				FirstName:   uEnt.FirstName,
				LastName:    uEnt.LastName,
				Job:         uEnt.Job,
				PhoneNumber: uEnt.PhoneNumber,
				Status:      string(uEnt.Status),
				CreatedAt:   uEnt.CreatedAt.Unix(),
			}
			err = setCacheUser(ctx, cUser, cacheUserDuration)
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d set cache user result", cnt), zap.Any("email", email), zap.Any("err", err))
			err = delLoadUserLock(ctx, email)
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d del load user lock result", cnt), zap.Any("email", email), zap.Any("err", err))
			return cUser, nil
		}

		// 4. not locked, wait for a while
		tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d not acquired lock, wait a while", cnt), zap.Any("email", email))
		time.Sleep(time.Millisecond * 10)

		// 5. exit after timeout
		if time.Now().UnixMilli()-start > 200 {
			tilog.Info(ctx, fmt.Sprintf("AuthnLoginSession round %d reached max time limit, direct exit", cnt), zap.Any("email", email))
			return nil, nil
		}

		cnt++
	}
}

func renewLoginSession(ctx context.Context, cSession *cacheSessionVal, curTs int64) {
	remain := cSession.SessionExp - curTs
	lost := int64(getSessionAliveDuration().Seconds()) - remain
	if lost < renewalWindowSeconds {
		tilog.Info(ctx, "AuthnLoginSession not reached renew window, ignore renew session",
			zap.Any("session_exp", cSession.SessionExp), zap.Any("cur_ts", curTs), zap.Any("remain", remain), zap.Any("lost", lost))
		return
	}

	//concurrency is low, and just redis operate, do not need to lock
	exp := time.Now().Add(getSessionAliveDuration())
	cSession.SessionExp = exp.Unix()
	err := setCacheSession(ctx, cSession, getSessionAliveDuration())
	tilog.Info(ctx, "AuthnLoginSession reached renew window, renew session result",
		zap.Any("sid", cSession.SessionID), zap.Any("session_exp", cSession.SessionExp), zap.Any("cur_ts", curTs), zap.Any("remain", remain), zap.Any("lost", lost), zap.Any("err", err))

	// mark dirty, flushed to DB in batch by the background worker
	sessionDirtyFlushOnce.Do(startSessionDirtyFlusher)
	sessionDirtySet.Store(cSession.SessionID, exp)
}

func startSessionDirtyFlusher() {
	go func() {
		defer func() {
			if r := recover(); r != nil {
				tilog.Error(context.Background(), "session dirty flusher panic, restart",
					zap.Any("panic", r))
				sessionDirtyFlushOnce = sync.Once{}
				startSessionDirtyFlusher()
			}
		}()
		ticker := time.NewTicker(sessionFlushInterval)
		defer ticker.Stop()
		for range ticker.C {
			flushDirtySessions()
		}
	}()
}

func flushDirtySessions() {
	items := make([]entity.SessionExpiredAtItem, 0, sessionFlushBatchSize)
	sessionDirtySet.Range(func(key, value any) bool {
		sid, _ := key.(string)
		exp, _ := value.(time.Time)
		if sid == "" || exp.IsZero() {
			sessionDirtySet.Delete(key)
			return true
		}
		items = append(items, entity.SessionExpiredAtItem{SessionID: sid, ExpiredAt: exp})
		sessionDirtySet.Delete(key)
		return len(items) < sessionFlushBatchSize
	})
	if len(items) == 0 {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := dao.GetDaoHub().UserSession.BatchUpdateSessionExpiredAt(ctx, database.DBConn.DB, items); err != nil {
		tilog.Warn(ctx, "flushDirtySessions batch update error",
			zap.Any("count", len(items)), zap.Any("err", err))
	}
}

func (s *UserSessionSvc) getSessionFromDB(ctx context.Context, sid string) (*cacheSessionVal, error) {
	start := time.Now()
	defer func() {
		metrics.RecAuthnSessionMetrics("AuthnLoginSession", "db_query", start, true)
	}()

	sEnt, err := dao.GetDaoHub().UserSession.GetUserSessionBySessionID(ctx, database.DBConn.DB, sid)
	if err != nil {
		tilog.Error(ctx, "getSessionFromDB GetUserSessionBySessionID error",
			zap.Any("sid", sid), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("get session from db error", err)
	}

	if sEnt == nil {
		tilog.Info(ctx, "getSessionFromDB session not found in DB", zap.Any("sid", sid))
		return nil, nil
	}

	if sEnt.RevokedAt != nil {
		tilog.Info(ctx, "getSessionFromDB session is revoked",
			zap.Any("sid", sid), zap.Any("revoked_at", sEnt.RevokedAt))
		return nil, nil
	}

	if sEnt.ExpiredAt != nil && time.Now().After(*sEnt.ExpiredAt) {
		tilog.Info(ctx, "getSessionFromDB session is expired",
			zap.Any("sid", sid), zap.Any("expired_at", sEnt.ExpiredAt))
		return nil, nil
	}

	tokensvc, err := token2.GetTokenSvc()
	if err != nil {
		tilog.Error(ctx, "getSessionFromDB GetTokenSvc error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("get token service error", err)
	}

	claims, err := tokensvc.GetClaimsFromToken(ctx, sEnt.IDToken)
	if err != nil {
		tilog.Warn(ctx, "getSessionFromDB GetClaimsFromToken error",
			zap.Any("sid", sid), zap.Any("err", err))
		return nil, nil
	}

	cSession, err := genCacheSessionFromToken(claims, sEnt.IDToken)
	if err != nil {
		tilog.Error(ctx, "getSessionFromDB genCacheSessionFromToken error",
			zap.Any("sid", sid), zap.Any("err", err))
		return nil, err
	}

	cSession.SessionExp = sEnt.ExpiredAt.Unix()

	tilog.Info(ctx, "getSessionFromDB success, caching to local",
		zap.Any("sid", sid), zap.Any("email", cSession.Email))

	_ = setCacheSession(ctx, cSession, getSessionAliveDuration())

	return cSession, nil
}
