// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package state

import (
	"encoding/binary"
	"fmt"
	"math"
	"reflect"
	"time"
)

// indexTimeLatest is a special sentinel type that can be specified
// when querying by IndexTime to use the largest possible time value.
type indexTimeLatest struct{}

// IndexTime indexes a time.Time field of a struct.
// TODO(mitchellh): test
type IndexTime struct {
	Field string

	// Asc if true will index with ascending order. Usually you want to
	// find the most recent data so this is false by default.
	Asc bool
}

func (idx *IndexTime) FromObject(obj interface{}) (bool, []byte, error) {
	v := reflect.Indirect(reflect.ValueOf(obj))
	fv := v.FieldByName(idx.Field)
	if !fv.IsValid() {
		return false, nil,
			fmt.Errorf("field '%s' is invalid %#v ", idx.Field, obj)
	}

	timeVal, ok := fv.Interface().(time.Time)
	if !ok {
		return false, nil,
			fmt.Errorf("field '%s' is not a time %v ", idx.Field, obj)
	}

	return true, idx.fromTime(timeVal), nil
}

func (idx *IndexTime) FromArgs(args ...interface{}) ([]byte, error) {
	if len(args) != 1 {
		return nil, fmt.Errorf("must provide only a single argument")
	}

	if _, ok := args[0].(indexTimeLatest); ok {
		if idx.Asc {
			return nil, fmt.Errorf("ascending indexTimeLatest value")
		}

		var zeroValue [8]byte
		return zeroValue[:], nil
	}

	arg, ok := args[0].(time.Time)
	if !ok {
		return nil, fmt.Errorf("argument must be a time: %#v", args[0])
	}

	return idx.fromTime(arg), nil
}

func (idx *IndexTime) fromTime(t time.Time) []byte {
	// If we're ascending, we just use the unix timestamp. This will give us
	// the proper ordering.
	val := t.UnixNano()

	// If we're descending, we use the time until max int64 which is the
	// maximum size of a Go unix timestamp. This will give us a SMALLER
	// value for NEWER (more recent) times. A descending order.
	if !idx.Asc {
		val = math.MaxInt64 - val
	}

	// Encoding uint64 is exactly 8 bytes. You can verify this in
	// the encoding/binary source in the Go stdlib.
	var buf [8]byte
	binary.BigEndian.PutUint64(buf[:], uint64(val))
	return buf[:]
}
