package dbhubsdk

import (
	"context"
	"errors"
	"sync"
	"time"

	"dataservices/commons/config"
	"dataservices/commons/errs"
	"dataservices/commons/grpc"
	"dataservices/commons/http"

	"github.com/dgrijalva/jwt-go"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const jwtFaultToleranceTime = 300

var globalJwtClient *JwtClient

func init() {
	globalJwtClient = NewJwtClient()
	tilog.Info(context.Background(), "init global JwtClient finish")
}

type JwtClient struct {
	cache sync.Map
}

func NewJwtClient() *JwtClient {
	cli := &JwtClient{
		cache: sync.Map{},
	}
	go cli.DeleteExpiredCacheLoop()
	return cli
}

func (c *JwtClient) GetJwt(ctx context.Context, tp dataapi.CrtType, clusterId uint64, key string, force bool) (*JwtResp, error) {
	var resp *JwtResp
	if !force {
		resp = c.GetJwtFromCache(tp, clusterId, key)
		if resp != nil {
			return resp, nil
		}
	}

	var err error
	switch tp {
	case dataapi.CrtType_AUTH0:
		resp, err = GetJwtByToken(ctx, clusterId, key)
	case dataapi.CrtType_APIKEY:
		resp, err = GetJwtByApiKey(ctx, clusterId, key)
	case dataapi.CrtType_EMAIL:
		resp, err = GetJwtByEmail(ctx, clusterId, key)
	default:
		err = errs.ErrUnsupportedCrtType
	}
	if resp != nil {
		c.StoreJwtIntoCache(tp, clusterId, key, resp)
	}
	return resp, err
}

func (c *JwtClient) GetJwtFromCache(tp dataapi.CrtType, clusterId uint64, key string) *JwtResp {
	k := JwtKey{
		tp:        tp,
		clusterId: clusterId,
		key:       key,
	}
	value, ok := c.cache.Load(k)
	if !ok {
		return nil
	}
	resp, ok := value.(*JwtResp)
	if !ok {
		return nil
	}
	if time.Now().Unix()+jwtFaultToleranceTime >= resp.ExpiresAt {
		return nil
	}
	return resp
}

func (c *JwtClient) StoreJwtIntoCache(tp dataapi.CrtType, clusterId uint64, key string, resp *JwtResp) {
	if resp == nil || resp.Jwt == "" {
		return
	}
	resp.ParseJwtExpiresAt()
	if resp.ExpiresAt == 0 {
		return
	}
	k := JwtKey{
		tp:        tp,
		clusterId: clusterId,
		key:       key,
	}
	c.cache.Store(k, resp)
}

var DefDeleteJwtExpiredCacheTicker = time.Minute

func (c *JwtClient) DeleteExpiredCacheLoop() {
	ctx := context.Background()
	defer func() {
		if r := recover(); r != nil {
			tilog.Error(ctx, "DeleteExpiredCacheLoop panic", zap.Any("panic", r))
		}
		tilog.Info(ctx, "DeleteExpiredCacheLoop exit")
	}()

	ticker := time.NewTicker(DefDeleteJwtExpiredCacheTicker)
	defer ticker.Stop()
	for {
		_ = <-ticker.C
		now := time.Now().Unix()
		deleted := 0
		c.cache.Range(func(key, value any) bool {
			resp, _ := value.(*JwtResp)
			if resp != nil && now >= resp.ExpiresAt {
				deleted++
				c.cache.Delete(key)
			}
			return true
		})
		if deleted > 0 {
			tilog.Info(ctx, "JwtClient removed expired jwt", zap.Int("deleted", deleted))
		}
	}
}

type JwtKey struct {
	tp        dataapi.CrtType
	clusterId uint64
	key       string
}

type JwtResp struct {
	Jwt       string
	DbUser    string
	ExpiresAt int64
}

func (j *JwtResp) ParseJwtExpiresAt() {
	claims, _ := ParseJwtToken(j.Jwt)
	if claims != nil && claims.ExpiresAt > 0 {
		j.ExpiresAt = claims.ExpiresAt
	}
}

func GetJwtByToken(ctx context.Context, clusterId uint64, token string) (*JwtResp, error) {
	if !config.IsCentralMode() {
		jwtRes, err := http.GetJwtByToken(ctx, clusterId, token)
		if err != nil {
			return nil, err
		}
		return &JwtResp{
			Jwt:    jwtRes.Jwt,
			DbUser: jwtRes.DbUser,
		}, nil
	}
	jwtRes, err := grpc.GetJwtByToken(ctx, &acl.GetJwtByTokenReq{
		ClusterId:  clusterId,
		Auth0Token: token,
	})
	if err != nil {
		if errors.Is(err, errs.ErrClusterNotSupportDbUser) {
			tilog.Info(ctx, "cluster not support dbuser", zap.Uint64("cluster_id", clusterId))
			return &JwtResp{}, nil
		}
		return nil, err
	}
	return &JwtResp{
		Jwt:    jwtRes.Jwt,
		DbUser: jwtRes.DbUser,
	}, nil
}

func GetJwtByApiKey(ctx context.Context, clusterId uint64, apikey string) (*JwtResp, error) {
	if !config.IsCentralMode() {
		jwtRes, err := http.GetJwtByApiKey(ctx, clusterId, apikey)
		if err != nil {
			return nil, err
		}
		return &JwtResp{
			Jwt:    jwtRes.Jwt,
			DbUser: jwtRes.DbUser,
		}, nil
	}
	jwtRes, err := grpc.GetJwtByApiKey(ctx, &acl.GetJwtByApiKeyReq{
		ClusterId: clusterId,
		AccessKey: apikey,
	})
	if err != nil {
		if errors.Is(err, errs.ErrClusterNotSupportDbUser) {
			tilog.Info(ctx, "cluster not support dbuser", zap.Uint64("cluster_id", clusterId))
			return &JwtResp{}, nil
		}
		return nil, err
	}
	return &JwtResp{
		Jwt:    jwtRes.Jwt,
		DbUser: jwtRes.DbUser,
	}, nil
}

func ParseJwtToken(token string) (*jwt.StandardClaims, error) {
	tokenClaims, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte("random"), nil
	})

	if tokenClaims != nil {
		if claims, ok := tokenClaims.Claims.(*jwt.StandardClaims); ok {
			return claims, nil
		}
	}
	return nil, err
}

func GetJwtByEmail(ctx context.Context, clusterId uint64, email string) (*JwtResp, error) {
	if !config.IsCentralMode() {
		jwtRes, err := http.GetJwtByEmail(ctx, clusterId, email)
		if err != nil {
			return nil, err
		}
		return &JwtResp{
			Jwt:    jwtRes.Jwt,
			DbUser: jwtRes.DbUser,
		}, nil
	}
	jwtRes, err := grpc.GetJwtByUser(ctx, &acl.GetJwtByUserReq{
		ClusterId: clusterId,
		Email:     email,
	})
	if err != nil {
		if errors.Is(err, errs.ErrClusterNotSupportDbUser) {
			tilog.Info(ctx, "cluster not support dbuser", zap.Uint64("cluster_id", clusterId))
			return &JwtResp{}, nil
		}
		return nil, err
	}
	return &JwtResp{
		Jwt:    jwtRes.Jwt,
		DbUser: jwtRes.DbUser,
	}, nil
}
