package apikey

import (
	"bytes"
	"context"
	"crypto/rand"
	"fmt"
	"math/big"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	serverNoBits  = 3
	timestampBits = 32
	serialNoBits  = 5

	defSerialNo = -1
	maxSerialNo = 31

	//configServerNo = 7

	defaultV      = 0
	initializingV = 1
)

var (
	baseTimestamp int64

	alphaMap = map[int64]string{
		0:  "0",
		1:  "1",
		2:  "2",
		3:  "3",
		4:  "4",
		5:  "5",
		6:  "6",
		7:  "7",
		8:  "8",
		9:  "9",
		10: "A",
		11: "B",
		12: "C",
		13: "D",
		14: "E",
		15: "F",
		16: "G",
		17: "H",
		18: "I",
		19: "J",
		20: "K",
		21: "L",
		22: "M",
		23: "N",
		24: "O",
		25: "P",
		26: "Q",
		27: "R",
		28: "S",
		29: "T",
		30: "U",
		31: "V",
	}
)

var (
	kgInitOnce sync.Once
	kg         *keyGenerator
)

type keyGenerator struct {
	// use 40 bit to identity a key (why 40? for origin eight chars and 0-9、a-z、A-Z limit)
	// 3  bit as server token
	// 32 bit as timestamp
	// 5  bit as serial number

	timestamp int64
	serialNo  int32
	// when initializing，equal 1
	initializing int32
}

func GetKeyGenerator() *keyGenerator {
	kgInitOnce.Do(func() {
		kg = NewKeyGenerator()
	})
	return kg
}

func NewKeyGenerator() *keyGenerator {
	baseTimestamp = initBaseTimestamp()
	return &keyGenerator{
		timestamp:    defaultV,
		serialNo:     defSerialNo,
		initializing: defaultV,
	}
}

func (k *keyGenerator) NextKey() string {

	timestamp, serialNo := k.GenTimestampSerialNo()

	serverNo := k.GetServerNumber()

	number := fmt.Sprintf("%s%s%s", serverNo, timestamp, serialNo)
	tilog.Info(context.Background(), "gen next access key number", zap.Any("number", number), zap.Any("len", len(number)))

	key := convertNumber2Key(number)

	return key

}

func (k *keyGenerator) GetServerNumber() string {
	// just random a server number
	randomServerNo, _ := rand.Int(rand.Reader, big.NewInt(8))
	no := fmt.Sprintf("%b", randomServerNo)
	return assembleFixedLengthStr(no, serverNoBits)
}

func initBaseTimestamp() int64 {
	timeLayout := "2006-01-02 15:04:05"
	baseTime := "2023-01-01 00:00:00"
	base, _ := time.ParseInLocation(timeLayout, baseTime, time.Local)
	return convTimestamp(base)
}

func convTimestamp(t time.Time) int64 {
	return t.Unix()
}

func getCurrentTimestamp() int64 {
	// millis after 100 years, will use 42 bit
	// seconds after 100 years, will use 32 bit
	now := time.Now()
	nowTimestamp := convTimestamp(now)

	diff := nowTimestamp - baseTimestamp
	//fmt.Println(diff)

	return diff
}

func (k *keyGenerator) GenTimestampSerialNo() (string, string) {
	nTimestamp := getCurrentTimestamp()

	// add max retry count to avoid blocking for long time
	for {
		oTimestamp := atomic.LoadInt64(&k.timestamp)

		if nTimestamp != oTimestamp {
			if atomic.CompareAndSwapInt32(&k.initializing, defaultV, initializingV) {
				atomic.StoreInt64(&k.timestamp, nTimestamp)
				atomic.StoreInt32(&k.serialNo, defSerialNo)
				atomic.StoreInt32(&k.initializing, defaultV)
			} else {
				for {
					time.Sleep(time.Millisecond)
					initializing := atomic.LoadInt32(&k.initializing)
					if initializing == defaultV {
						break
					}
				}
			}
		} else {
			for {
				oSerialNo := atomic.LoadInt32(&k.serialNo)
				if oSerialNo >= maxSerialNo {
					nTimestamp = tillNextTime(nTimestamp)
					break
				}
				nSerialNo := oSerialNo + 1
				if atomic.CompareAndSwapInt32(&k.serialNo, oSerialNo, nSerialNo) {
					useTimestamp, useSerialNo := formatTimestampSerialNo(nTimestamp, nSerialNo)
					return useTimestamp, useSerialNo
				}
			}
		}
	}
}

func tillNextTime(oTimestamp int64) int64 {
	for {
		curTimestamp := getCurrentTimestamp()
		if curTimestamp > oTimestamp {
			return curTimestamp
		}
		time.Sleep(time.Millisecond * 50)
	}

}

func formatTimestampSerialNo(timestamp int64, serialNo int32) (string, string) {
	nSerialNo2 := fmt.Sprintf("%b", serialNo)
	useSerialNo := assembleFixedLengthStr(nSerialNo2, serialNoBits)

	nTimestamp2 := fmt.Sprintf("%b", timestamp)
	useTimestamp := assembleFixedLengthStr(nTimestamp2, timestampBits)

	return useTimestamp, useSerialNo
}

func assembleFixedLengthStr(s string, fixedLen int) string {
	var buffer bytes.Buffer
	l := len(s)
	if l < fixedLen {
		n := fixedLen - l
		for i := 0; i < n; i++ {
			buffer.WriteString("0")
		}
	}
	buffer.WriteString(s)
	return buffer.String()
}

func convertNumber2Key(number string) string {
	var buffer bytes.Buffer
	for i := 0; i < 8; i++ {
		s := i * 5
		e := s + 5
		tmp := number[s:e]
		tmp10, _ := strconv.ParseInt(tmp, 2, 64)
		value := alphaMap[tmp10]
		buffer.WriteString(value)
	}
	return buffer.String()
}
