package cursortoken

import (
	"encoding/base32"
	"encoding/json"
	"errors"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"strings"
	"time"
)

type Mode string

const (
	CTMStart  Mode = "START"
	CTMNormal Mode = "NORMAL"
	CTMEnd    Mode = "END"
)

type Extra struct {
	Timestamp *time.Time
	Id        *string
	Page      *int
	PageSize  *int
}

type CursorToken struct {
	Mode               Mode
	ValuePrimary       string
	ValueSecondary     string
	Direction          SortDirection
	DirectionSecondary SortDirection
	PageSize           int
	Extra              Extra
}

type cursorTokenSerialize struct {
	ValuePrimary       *string        `json:"v1,omitempty"`
	ValueSecondary     *string        `json:"v2,omitempty"`
	Direction          *SortDirection `json:"dir,omitempty"`
	DirectionSecondary *SortDirection `json:"dir2,omitempty"`
	PageSize           *int           `json:"size,omitempty"`

	ExtraTimestamp *time.Time `json:"ts,omitempty"`
	ExtraId        *string    `json:"id,omitempty"`
	ExtraPage      *int       `json:"pg,omitempty"`
	ExtraPageSize  *int       `json:"sz,omitempty"`
}

func Start() CursorToken {
	return CursorToken{
		Mode:               CTMStart,
		ValuePrimary:       "",
		ValueSecondary:     "",
		Direction:          "",
		DirectionSecondary: "",
		PageSize:           0,
		Extra:              Extra{},
	}
}

func End() CursorToken {
	return CursorToken{
		Mode:               CTMEnd,
		ValuePrimary:       "",
		ValueSecondary:     "",
		Direction:          "",
		DirectionSecondary: "",
		PageSize:           0,
		Extra:              Extra{},
	}
}

func (c *CursorToken) Token() string {
	if c.Mode == CTMStart {
		return "@start"
	}
	if c.Mode == CTMEnd {
		return "@end"
	}

	// We kinda manually implement omitempty for the CursorToken here
	// because omitempty does not work for time.Time and otherwise we would always
	// get weird time values when decoding a token that initially didn't have an Timestamp set
	// For this usecase we treat Unix=0 as an empty timestamp

	sertok := cursorTokenSerialize{}

	if c.ValuePrimary != "" {
		sertok.ValuePrimary = &c.ValuePrimary
	}
	if c.ValueSecondary != "" {
		sertok.ValueSecondary = &c.ValueSecondary
	}
	if c.Direction != "" {
		sertok.Direction = &c.Direction
	}
	if c.DirectionSecondary != "" {
		sertok.DirectionSecondary = &c.DirectionSecondary
	}
	if c.PageSize != 0 {
		sertok.PageSize = &c.PageSize
	}

	sertok.ExtraTimestamp = c.Extra.Timestamp
	sertok.ExtraId = c.Extra.Id
	sertok.ExtraPage = c.Extra.Page
	sertok.ExtraPageSize = c.Extra.PageSize

	body, err := json.Marshal(sertok)
	if err != nil {
		panic(err)
	}

	return "tok_" + base32.StdEncoding.EncodeToString(body)
}

func Decode(tok string) (CursorToken, error) {
	if tok == "" {
		return Start(), nil
	}
	if strings.ToLower(tok) == "@start" {
		return Start(), nil
	}
	if strings.ToLower(tok) == "@end" {
		return End(), nil
	}

	if !strings.HasPrefix(tok, "tok_") {
		return CursorToken{}, errors.New("could not decode token, missing prefix")
	}

	body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):])
	if err != nil {
		return CursorToken{}, err
	}

	var tokenDeserialize cursorTokenSerialize
	err = json.Unmarshal(body, &tokenDeserialize)
	if err != nil {
		return CursorToken{}, err
	}

	token := CursorToken{Mode: CTMNormal}

	if tokenDeserialize.ValuePrimary != nil {
		token.ValuePrimary = *tokenDeserialize.ValuePrimary
	}
	if tokenDeserialize.ValueSecondary != nil {
		token.ValueSecondary = *tokenDeserialize.ValueSecondary
	}
	if tokenDeserialize.Direction != nil {
		token.Direction = *tokenDeserialize.Direction
	}
	if tokenDeserialize.DirectionSecondary != nil {
		token.DirectionSecondary = *tokenDeserialize.DirectionSecondary
	}
	if tokenDeserialize.PageSize != nil {
		token.PageSize = *tokenDeserialize.PageSize
	}

	token.Extra.Timestamp = tokenDeserialize.ExtraTimestamp
	token.Extra.Id = tokenDeserialize.ExtraId
	token.Extra.Page = tokenDeserialize.ExtraPage
	token.Extra.PageSize = tokenDeserialize.ExtraPageSize

	return token, nil
}

func (c *CursorToken) ValuePrimaryObjectId() (primitive.ObjectID, bool) {
	if oid, err := primitive.ObjectIDFromHex(c.ValuePrimary); err == nil {
		return oid, true
	} else {
		return primitive.ObjectID{}, false
	}
}

func (c *CursorToken) ValueSecondaryObjectId() (primitive.ObjectID, bool) {
	if oid, err := primitive.ObjectIDFromHex(c.ValueSecondary); err == nil {
		return oid, true
	} else {
		return primitive.ObjectID{}, false
	}
}
