package token

import (
	"crypto/hmac"
	"crypto/sha256"
	"errors"
	"strconv"
	"strings"
	"time"

	"github.com/btcsuite/btcutil/base58"
)

var (
	EXPIRED      = errors.New("token expired")
	JUST_EXPIRED = errors.New("token just expired")
)

type Tokenizer struct {
	secret []byte
}

func NewTokenizer(secret string) *Tokenizer {
	return &Tokenizer{[]byte(secret)}
}

type TokenData struct {
	ID      uint64
	Delay   int64
	ExpTime int64
}

func (tokenizer *Tokenizer) sign(body string) []byte {
	mac := hmac.New(sha256.New, tokenizer.secret)
	mac.Write([]byte(body))
	return mac.Sum(nil)
}

func (tokenizer *Tokenizer) Compose(d TokenData) string {
	body := strconv.FormatUint(d.ID, 36) +
		"." + strconv.FormatInt(d.Delay, 36) +
		"." + strconv.FormatInt(d.ExpTime, 36)
	sign := base58.Encode(tokenizer.sign(body))
	return body + "." + sign
}

func (tokenizer *Tokenizer) Parse(token string) (*TokenData, error) {
	data := strings.Split(token, ".")
	if len(data) != 4 {
		return nil, errors.New("wrong token format")
	}
	if !hmac.Equal(
		base58.Decode(data[len(data)-1]),
		tokenizer.sign(strings.Join(data[:len(data)-1], ".")),
	) {
		return nil, errors.New("wrong token sign")
	}
	id, err := strconv.ParseUint(data[0], 36, 64)
	if err != nil {
		return nil, err
	}
	delay, err := strconv.ParseInt(data[1], 36, 64)
	if err != nil {
		return nil, err
	}
	expTime, err := strconv.ParseInt(data[2], 36, 64)
	if err != nil {
		return nil, err
	}
	res := &TokenData{id, delay, expTime}
	if expTime <= time.Now().UnixMilli() {
		// If token is expired less than 30 seconds ago, we still consider it semi-valid
		if expTime+30000 > time.Now().UnixMilli() {
			return res, JUST_EXPIRED
		}
		return res, EXPIRED
	}
	return res, nil
}
