// Copyright (c) 2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package database

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"math"

	bolt "go.etcd.io/bbolt"
)

// VoteChangeRecord is serialized to json and stored in bbolt db. The json keys
// are deliberately kept short because they are duplicated many times in the db.
type VoteChangeRecord struct {
	Request           string `json:"req"`
	RequestSignature  string `json:"reqs"`
	Response          string `json:"rsp"`
	ResponseSignature string `json:"rsps"`
}

// SaveVoteChange will insert the provided vote change record into the database,
// and if this breaches the maximum amount of allowed records, delete the oldest
// one which is currently stored. Records are stored using a serially increasing
// integer as the key.
func (vdb *VspDatabase) SaveVoteChange(ticketHash string, record VoteChangeRecord) error {
	return vdb.db.Update(func(tx *bolt.Tx) error {

		// Create or get a bucket for this ticket.
		bkt, err := tx.Bucket(vspBktK).Bucket(voteChangeBktK).
			CreateBucketIfNotExists([]byte(ticketHash))
		if err != nil {
			return fmt.Errorf("failed to create vote change bucket (ticketHash=%s): %w",
				ticketHash, err)
		}

		// Loop through the bucket to count the records, as well as finding the
		// most recent and the oldest record.
		var count int
		highest := uint32(0)
		lowest := uint32(math.MaxUint32)
		err = bkt.ForEach(func(k, v []byte) error {
			count++
			key := binary.LittleEndian.Uint32(k)
			if key > highest {
				highest = key
			}
			if key < lowest {
				lowest = key
			}
			return nil
		})
		if err != nil {
			return fmt.Errorf("error iterating over vote change bucket: %w", err)
		}

		// If bucket is at (or over) the limit of max allowed records, remove
		// the oldest one.
		if count >= vdb.maxVoteChangeRecords {
			keyBytes := make([]byte, 4)
			binary.LittleEndian.PutUint32(keyBytes, lowest)
			err = bkt.Delete(keyBytes)
			if err != nil {
				return fmt.Errorf("failed to delete old vote change record: %w", err)
			}
		}

		// Insert record with index 0 if the bucket is currently empty,
		// otherwise use most recent + 1.
		var newKey uint32
		if count > 0 {
			newKey = highest + 1
		}

		keyBytes := make([]byte, 4)
		binary.LittleEndian.PutUint32(keyBytes, newKey)

		// Insert record.
		recordBytes, err := json.Marshal(record)
		if err != nil {
			return fmt.Errorf("could not marshal vote change record: %w", err)
		}
		err = bkt.Put(keyBytes, recordBytes)
		if err != nil {
			return fmt.Errorf("could not store vote change record: %w", err)
		}

		return nil
	})
}

// GetVoteChanges retrieves all of the stored vote change records for the
// provided ticket hash.
func (vdb *VspDatabase) GetVoteChanges(ticketHash string) (map[uint32]VoteChangeRecord, error) {

	records := make(map[uint32]VoteChangeRecord)

	err := vdb.db.View(func(tx *bolt.Tx) error {
		bkt := tx.Bucket(vspBktK).Bucket(voteChangeBktK).
			Bucket([]byte(ticketHash))

		if bkt == nil {
			return nil
		}

		err := bkt.ForEach(func(k, v []byte) error {
			var record VoteChangeRecord
			err := json.Unmarshal(v, &record)
			if err != nil {
				return fmt.Errorf("could not unmarshal vote change record: %w", err)
			}

			records[binary.LittleEndian.Uint32(k)] = record

			return nil
		})
		if err != nil {
			return fmt.Errorf("error iterating over vote change bucket: %w", err)
		}

		return nil
	})

	return records, err
}
