// Copyright (c) 2020-2023 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 (
	"fmt"
	"time"

	bolt "go.etcd.io/bbolt"
)

// FeeStatus represents the current state of a ticket fee payment.
type FeeStatus string

const (
	// NoFee indicates no fee tx has been received yet.
	NoFee FeeStatus = "none"
	// FeeReceieved indicates fee tx has been received but not broadcast.
	FeeReceieved FeeStatus = "received"
	// FeeBroadcast indicates fee tx has been broadcast but not confirmed.
	FeeBroadcast FeeStatus = "broadcast"
	// FeeConfirmed indicates fee tx has been broadcast and confirmed.
	FeeConfirmed FeeStatus = "confirmed"
	// FeeError indicates fee tx could not be broadcast due to an error.
	FeeError FeeStatus = "error"
)

// TicketOutcome describes the reason a ticket is no longer votable.
type TicketOutcome string

const (
	// Expired indicates the ticket expired and has been revoked.
	Expired TicketOutcome = "expired"
	// Missed indicates the ticket was missed and has been revoked.
	Missed TicketOutcome = "missed"
	// Voted indicates the ticket has already voted.
	Voted TicketOutcome = "voted"

	// Revoked is a deprecated status which should no longer be used. It was
	// used before vspd was able to distinguish between expired and missed
	// tickets.
	Revoked TicketOutcome = "revoked"
)

// The keys used to store ticket values in the database.
var (
	hashK              = []byte("Hash")
	purchaseHeightK    = []byte("PurchaseHeight")
	commitmentAddressK = []byte("CommitmentAddress")
	feeAddressIndexK   = []byte("FeeAddressIndex")
	feeAddressK        = []byte("FeeAddress")
	feeAmountK         = []byte("FeeAmount")
	feeExpirationK     = []byte("FeeExpiration")
	confirmedK         = []byte("Confirmed")
	votingWIFK         = []byte("VotingWIF")
	voteChoicesK       = []byte("VoteChoices")
	tSpendPolicyK      = []byte("TSpendPolicy")
	treasuryPolicyK    = []byte("TreasuryPolicy")
	feeTxHexK          = []byte("FeeTxHex")
	feeTxHashK         = []byte("FeeTxHash")
	feeTxStatusK       = []byte("FeeTxStatus")
	outcomeK           = []byte("Outcome")
)

type Ticket struct {
	Hash              string
	PurchaseHeight    int64
	CommitmentAddress string
	FeeAddressIndex   uint32
	FeeAddress        string
	FeeAmount         int64
	FeeExpiration     int64

	// Confirmed will be set when the ticket has 6+ confirmations.
	Confirmed bool

	// VotingWIF is set in /payfee.
	VotingWIF string

	// VoteChoices, TSpendPolicy and TreasuryPolicy are initially set in
	// /payfee, but can be updated in /setvotechoices.
	VoteChoices    map[string]string
	TSpendPolicy   map[string]string
	TreasuryPolicy map[string]string

	// FeeTxHex and FeeTxHash will be set when the fee tx has been received.
	FeeTxHex  string
	FeeTxHash string

	// FeeTxStatus indicates the current state of the fee transaction.
	FeeTxStatus FeeStatus

	// Outcome is set once a ticket is either voted or revoked. An empty outcome
	// indicates that a ticket is still votable.
	Outcome TicketOutcome
}

type TicketList []Ticket

// EarliestPurchaseHeight returns the lowest non-zero purchase height in the
// list of tickets. Zero will be returned if the list is empty, or if every
// ticket in the list has zero purchase height.
func (t TicketList) EarliestPurchaseHeight() int64 {
	var oldestHeight int64
	for _, ticket := range t {
		// Skip unconfirmed tickets.
		if ticket.PurchaseHeight == 0 {
			continue
		}
		if oldestHeight == 0 || oldestHeight > ticket.PurchaseHeight {
			oldestHeight = ticket.PurchaseHeight
		}
	}
	return oldestHeight
}

func (t *Ticket) FeeExpired() bool {
	now := time.Now()
	return now.After(time.Unix(t.FeeExpiration, 0))
}

// InsertNewTicket will insert the provided ticket into the database. Returns an
// error if either the ticket hash or fee address already exist.
func (vdb *VspDatabase) InsertNewTicket(ticket Ticket) error {
	vdb.ticketsMtx.Lock()
	defer vdb.ticketsMtx.Unlock()

	return vdb.db.Update(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

		// Create a bucket for the new ticket. Returns an error if bucket
		// already exists.
		newTicketBkt, err := ticketBkt.CreateBucket([]byte(ticket.Hash))
		if err != nil {
			return fmt.Errorf("could not create bucket for ticket: %w", err)
		}

		err = putTicketInBucket(newTicketBkt, ticket)
		if err != nil {
			return fmt.Errorf("putting ticket in bucket failed: %w", err)
		}

		return nil
	})
}

// putTicketInBucket encodes each of the fields of the provided ticket as a byte
// array, and stores them as values within the provided db bucket.
func putTicketInBucket(bkt *bolt.Bucket, ticket Ticket) error {
	var err error
	if err = bkt.Put(hashK, []byte(ticket.Hash)); err != nil {
		return err
	}
	if err = bkt.Put(commitmentAddressK, []byte(ticket.CommitmentAddress)); err != nil {
		return err
	}
	if err = bkt.Put(feeAddressK, []byte(ticket.FeeAddress)); err != nil {
		return err
	}
	if err = bkt.Put(votingWIFK, []byte(ticket.VotingWIF)); err != nil {
		return err
	}
	if err = bkt.Put(feeTxHexK, []byte(ticket.FeeTxHex)); err != nil {
		return err
	}
	if err = bkt.Put(feeTxHashK, []byte(ticket.FeeTxHash)); err != nil {
		return err
	}
	if err = bkt.Put(feeTxStatusK, []byte(ticket.FeeTxStatus)); err != nil {
		return err
	}
	if err = bkt.Put(outcomeK, []byte(ticket.Outcome)); err != nil {
		return err
	}
	if err = bkt.Put(purchaseHeightK, int64ToBytes(ticket.PurchaseHeight)); err != nil {
		return err
	}
	if err = bkt.Put(feeAddressIndexK, uint32ToBytes(ticket.FeeAddressIndex)); err != nil {
		return err
	}
	if err = bkt.Put(feeAmountK, int64ToBytes(ticket.FeeAmount)); err != nil {
		return err
	}
	if err = bkt.Put(feeExpirationK, int64ToBytes(ticket.FeeExpiration)); err != nil {
		return err
	}
	if err = bkt.Put(confirmedK, boolToBytes(ticket.Confirmed)); err != nil {
		return err
	}
	if err = bkt.Put(tSpendPolicyK, stringMapToBytes(ticket.TSpendPolicy)); err != nil {
		return err
	}
	if err = bkt.Put(treasuryPolicyK, stringMapToBytes(ticket.TreasuryPolicy)); err != nil {
		return err
	}

	return bkt.Put(voteChoicesK, stringMapToBytes(ticket.VoteChoices))
}

func getTicketFromBkt(bkt *bolt.Bucket) (Ticket, error) {
	var ticket Ticket

	ticket.Hash = string(bkt.Get(hashK))
	ticket.CommitmentAddress = string(bkt.Get(commitmentAddressK))
	ticket.FeeAddress = string(bkt.Get(feeAddressK))
	ticket.VotingWIF = string(bkt.Get(votingWIFK))
	ticket.FeeTxHex = string(bkt.Get(feeTxHexK))
	ticket.FeeTxHash = string(bkt.Get(feeTxHashK))
	ticket.FeeTxStatus = FeeStatus(bkt.Get(feeTxStatusK))
	ticket.Outcome = TicketOutcome(bkt.Get(outcomeK))

	ticket.PurchaseHeight = bytesToInt64(bkt.Get(purchaseHeightK))
	ticket.FeeAddressIndex = bytesToUint32(bkt.Get(feeAddressIndexK))
	ticket.FeeAmount = bytesToInt64(bkt.Get(feeAmountK))
	ticket.FeeExpiration = bytesToInt64(bkt.Get(feeExpirationK))

	ticket.Confirmed = bytesToBool(bkt.Get(confirmedK))

	var err error
	ticket.VoteChoices, err = bytesToStringMap(bkt.Get(voteChoicesK))
	if err != nil {
		return ticket, fmt.Errorf("unmarshal VoteChoices err: %w", err)
	}

	ticket.TSpendPolicy, err = bytesToStringMap(bkt.Get(tSpendPolicyK))
	if err != nil {
		return ticket, fmt.Errorf("unmarshal TSpendPolicy err: %w", err)
	}

	ticket.TreasuryPolicy, err = bytesToStringMap(bkt.Get(treasuryPolicyK))
	if err != nil {
		return ticket, fmt.Errorf("unmarshal TreasuryPolicy err: %w", err)
	}

	return ticket, nil
}

func (vdb *VspDatabase) DeleteTicket(ticket Ticket) error {
	vdb.ticketsMtx.Lock()
	defer vdb.ticketsMtx.Unlock()

	return vdb.db.Update(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

		err := ticketBkt.DeleteBucket([]byte(ticket.Hash))
		if err != nil {
			return fmt.Errorf("could not delete ticket: %w", err)
		}

		return nil
	})
}

func (vdb *VspDatabase) UpdateTicket(ticket Ticket) error {
	vdb.ticketsMtx.Lock()
	defer vdb.ticketsMtx.Unlock()

	return vdb.db.Update(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

		bkt := ticketBkt.Bucket([]byte(ticket.Hash))

		if bkt == nil {
			return fmt.Errorf("ticket does not exist with hash %s", ticket.Hash)
		}

		return putTicketInBucket(bkt, ticket)
	})
}

func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error) {
	vdb.ticketsMtx.RLock()
	defer vdb.ticketsMtx.RUnlock()

	var ticket Ticket
	var found bool
	err := vdb.db.View(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK).Bucket([]byte(ticketHash))

		if ticketBkt == nil {
			return nil
		}

		var err error
		ticket, err = getTicketFromBkt(ticketBkt)
		if err != nil {
			return fmt.Errorf("could not get ticket: %w", err)
		}

		found = true

		return nil
	})

	return ticket, found, err
}

// Size returns the current size of the database in bytes. Note that this may
// not exactly match the size of the database file stored on disk.
func (vdb *VspDatabase) Size() (uint64, error) {
	var size uint64
	err := vdb.db.View(func(tx *bolt.Tx) error {
		size = uint64(tx.Size())
		return nil
	})
	return size, err
}

// CountTickets returns the total number of voted, expired, missed, and
// currently voting tickets. This func iterates over every ticket so should be
// used sparingly.
func (vdb *VspDatabase) CountTickets() (int64, int64, int64, int64, error) {
	vdb.ticketsMtx.RLock()
	defer vdb.ticketsMtx.RUnlock()

	var voting, voted, expired, missed int64
	err := vdb.db.View(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

		return ticketBkt.ForEachBucket(func(k []byte) error {
			tBkt := ticketBkt.Bucket(k)

			if FeeStatus(tBkt.Get(feeTxStatusK)) == FeeConfirmed {
				switch TicketOutcome(tBkt.Get(outcomeK)) {
				case Voted:
					voted++
				case Expired:
					expired++
				case Missed:
					missed++
				case Revoked:
					// There shouldn't be any revoked tickets in the db, they
					// should have been updated to expired/missed. Give benefit
					// of doubt to VSP admin and count these as expired.
					expired++
				default:
					voting++
				}
			}

			return nil
		})
	})

	return voting, voted, expired, missed, err
}

// GetUnconfirmedTickets returns tickets which are not yet confirmed.
func (vdb *VspDatabase) GetUnconfirmedTickets() (TicketList, error) {
	vdb.ticketsMtx.RLock()
	defer vdb.ticketsMtx.RUnlock()

	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return !bytesToBool(t.Get(confirmedK))
	})
}

// GetPendingFees returns tickets which are confirmed and have a fee tx which is
// not yet broadcast.
func (vdb *VspDatabase) GetPendingFees() (TicketList, error) {
	vdb.ticketsMtx.RLock()
	defer vdb.ticketsMtx.RUnlock()

	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return bytesToBool(t.Get(confirmedK)) && FeeStatus(t.Get(feeTxStatusK)) == FeeReceieved
	})
}

// GetUnconfirmedFees returns tickets with a fee tx that is broadcast but not
// confirmed yet.
func (vdb *VspDatabase) GetUnconfirmedFees() (TicketList, error) {
	vdb.ticketsMtx.RLock()
	defer vdb.ticketsMtx.RUnlock()

	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return FeeStatus(t.Get(feeTxStatusK)) == FeeBroadcast
	})
}

// GetVotableTickets returns tickets with a confirmed fee tx and no outcome (ie.
// not expired/voted/missed).
func (vdb *VspDatabase) GetVotableTickets() (TicketList, error) {
	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed && TicketOutcome(t.Get(outcomeK)) == ""
	})
}

// GetVotedTickets returns tickets with a confirmed fee tx and outcome == voted.
func (vdb *VspDatabase) GetVotedTickets() (TicketList, error) {
	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed && TicketOutcome(t.Get(outcomeK)) == Voted
	})
}

// GetRevokedTickets returns all tickets which have outcome == revoked.
func (vdb *VspDatabase) GetRevokedTickets() (TicketList, error) {
	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return TicketOutcome(t.Get(outcomeK)) == Revoked
	})
}

// GetMissingPurchaseHeight returns tickets which are confirmed but do not have
// a purchase height.
func (vdb *VspDatabase) GetMissingPurchaseHeight() (TicketList, error) {
	return vdb.filterTickets(func(t *bolt.Bucket) bool {
		return bytesToBool(t.Get(confirmedK)) && bytesToInt64(t.Get(purchaseHeightK)) == 0
	})
}

// filterTickets accepts a filter function and returns all tickets from the
// database which match the filter.
//
// This function must be called with the lock held.
func (vdb *VspDatabase) filterTickets(filter func(*bolt.Bucket) bool) (TicketList, error) {
	var tickets TicketList
	err := vdb.db.View(func(tx *bolt.Tx) error {
		ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

		return ticketBkt.ForEachBucket(func(k []byte) error {
			ticketBkt := ticketBkt.Bucket(k)

			if filter(ticketBkt) {
				ticket, err := getTicketFromBkt(ticketBkt)
				if err != nil {
					return fmt.Errorf("could not get ticket: %w", err)
				}
				tickets = append(tickets, ticket)
			}

			return nil
		})
	})

	return tickets, err
}
