// 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 webapi

import (
	"bytes"
	"errors"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/decred/dcrd/blockchain/stake/v3"
	"github.com/decred/dcrd/chaincfg/chainhash"
	"github.com/decred/vspd/rpc"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/gorilla/sessions"
	"github.com/jrick/wsrpc/v2"
)

type ticketHashRequest struct {
	TicketHash string `json:"tickethash" binding:"required"`
}

type ticketRequest struct {
	TicketHex  string `json:"tickethex" binding:"required"`
	TicketHash string `json:"tickethash" binding:"required"`
	ParentHex  string `json:"parenthex"`
}

// withSession middleware adds a gorilla session to the request context for
// downstream handlers to make use of. Sessions are used by admin pages to
// maintain authentication status.
func withSession(store *sessions.CookieStore) gin.HandlerFunc {
	return func(c *gin.Context) {
		session, err := store.Get(c.Request, "vspd-session")
		if err != nil {
			// "value is not valid" occurs if the cookie secret changes. This is
			// common during development (eg. when using the test harness) but
			// it should not occur in production.
			if strings.Contains(err.Error(), "securecookie: the value is not valid") {
				log.Warn("Cookie secret has changed. Generating new session.")

				// Persist the newly generated session.
				err = store.Save(c.Request, c.Writer, session)
				if err != nil {
					log.Errorf("Error saving session: %v", err)
					c.String(http.StatusInternalServerError, "Error saving session")
					c.Abort()
					return
				}
			} else {
				log.Errorf("Session error: %v", err)
				c.String(http.StatusInternalServerError, "Error getting session")
				c.Abort()
				return
			}
		}

		c.Set("session", session)
	}
}

// requireAdmin will only allow the request to proceed if the current session is
// authenticated as an admin, otherwise it will render the login template.
func requireAdmin() gin.HandlerFunc {
	return func(c *gin.Context) {
		session := c.MustGet("session").(*sessions.Session)
		admin := session.Values["admin"]

		if admin == nil {
			c.HTML(http.StatusUnauthorized, "login.html", gin.H{
				"VspStats": getVSPStats(),
			})
			c.Abort()
			return
		}
	}
}

// withDcrdClient middleware adds a dcrd client to the request context for
// downstream handlers to make use of.
func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc {
	return func(c *gin.Context) {
		client, err := dcrd.Client(c, cfg.NetParams)
		if err != nil {
			log.Error(err)
			sendError(errInternalError, c)
			return
		}

		c.Set("DcrdClient", client)
	}
}

// withWalletClients middleware attempts to add voting wallet clients to the
// request context for downstream handlers to make use of. Downstream handlers
// must handle the case where no wallet clients are connected.
func withWalletClients(wallets rpc.WalletConnect) gin.HandlerFunc {
	return func(c *gin.Context) {
		clients, failedConnections := wallets.Clients(c, cfg.NetParams)
		if len(clients) == 0 {
			log.Error("Could not connect to any wallets")
		} else if len(failedConnections) > 0 {
			log.Errorf("Failed to connect to %d wallet(s), proceeding with only %d",
				len(failedConnections), len(clients))
		}
		c.Set("WalletClients", clients)
		c.Set("FailedWalletClients", failedConnections)
	}
}

// broadcastTicket will parse ticket hash and ticket hex from the request body,
// and ensure the local dcrd instance can retrieve information about the ticket.
// If no info can be found, the ticket hex will be broadcast.
func broadcastTicket() gin.HandlerFunc {
	return func(c *gin.Context) {
		const funcName = "broadcastTicket"

		// Read request bytes and then replace the request reader for
		// downstream handlers to use.
		reqBytes, err := ioutil.ReadAll(c.Request.Body)
		if err != nil {
			log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
			sendErrorWithMsg(err.Error(), errBadRequest, c)
			return
		}
		c.Request.Body.Close()
		c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBytes))

		// Parse request and ensure ticket hash and hex are included.
		var request ticketRequest
		if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
			log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
			sendErrorWithMsg(err.Error(), errBadRequest, c)
			return
		}

		// Ensure the provided hex is a valid ticket.
		msgTx, err := decodeTransaction(request.TicketHex)
		if err != nil {
			log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
			sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c)
			return
		}

		err = isValidTicket(msgTx)
		if err != nil {
			log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v",
				funcName, c.ClientIP(), request.TicketHash, err)
			sendError(errInvalidTicket, c)
			return
		}

		// Ensure hex matches hash.
		if msgTx.TxHash().String() != request.TicketHash {
			log.Warnf("%s: Ticket hex/hash mismatch (clientIP=%s, ticketHash=%s)",
				funcName, c.ClientIP(), request.TicketHash)
			sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c)
			return
		}

		dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)

		if request.ParentHex != "" {
			parentTx, err := decodeTransaction(request.ParentHex)
			if err != nil {
				log.Errorf("%s: Failed to decode parent hex (ticketHash=%s): %v", funcName, request.TicketHash, err)
				sendErrorWithMsg("cannot decode parent hex", errBadRequest, c)
				return
			}
			parentHash := parentTx.TxHash()

			_, err = dcrdClient.GetRawTransaction(parentHash.String())
			if err == nil {
				// No error means dcrd already knows the parent, we are done here.
				goto processTicket
			}

			var found bool
			for _, txIn := range msgTx.TxIn {
				if !txIn.PreviousOutPoint.Hash.IsEqual(&parentHash) {
					continue
				}
				found = true
				break
			}
			if !found {
				log.Errorf("%s: Invalid ticket parent (ticketHash=%s)", funcName, request.TicketHash)
				sendErrorWithMsg("invalid ticket parent", errBadRequest, c)
				return
			}
			log.Debugf("%s: Broadcasting parent tx %s (ticketHash=%s)", funcName, parentHash, request.TicketHash)
			err = dcrdClient.SendRawTransaction(request.ParentHex)
			if err != nil {
				log.Errorf("%s: dcrd.SendRawTransaction for parent tx failed (ticketHash=%s): %v",
					funcName, request.TicketHash, err)
				sendError(errCannotBroadcastTicket, c)
				return
			}
		}

	processTicket:
		// Use GetRawTransaction to check if local dcrd already knows this
		// ticket.
		_, err = dcrdClient.GetRawTransaction(request.TicketHash)
		if err == nil {
			// No error means dcrd already knows the ticket, we are done here.
			return
		}

		// ErrNoTxInfo means local dcrd is not aware of the ticket. We have the
		// hex, so we can broadcast it here.
		var e *wsrpc.Error
		if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo {
			log.Debugf("%s: Broadcasting ticket (ticketHash=%s)", funcName, request.TicketHash)
			err = dcrdClient.SendRawTransaction(request.TicketHex)
			if err != nil {
				log.Errorf("%s: dcrd.SendRawTransaction for ticket failed (ticketHash=%s): %v",
					funcName, request.TicketHash, err)
				sendError(errCannotBroadcastTicket, c)
				return
			}
		} else {
			log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v",
				funcName, request.TicketHash, err)
			sendError(errInternalError, c)
			return
		}
	}
}

// vspAuth middleware reads the request body and extracts the ticket hash. The
// commitment address for the ticket is retrieved from the database if it is
// known, or it is retrieved from the chain if not.
// The middleware errors out if the VSP-Client-Signature header of the request
// does not contain the request body signed with the commitment address.
// Ticket information is added to the request context for downstream handlers to
// use.
func vspAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		const funcName = "vspAuth"

		// Read request bytes.
		reqBytes, err := ioutil.ReadAll(c.Request.Body)
		if err != nil {
			log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err)
			sendErrorWithMsg(err.Error(), errBadRequest, c)
			return
		}

		// Add request bytes to request context for downstream handlers to reuse.
		// Necessary because the request body reader can only be used once.
		c.Set("RequestBytes", reqBytes)

		// Parse request and ensure there is a ticket hash included.
		var request ticketHashRequest
		if err := binding.JSON.BindBody(reqBytes, &request); err != nil {
			log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err)
			sendErrorWithMsg(err.Error(), errBadRequest, c)
			return
		}
		hash := request.TicketHash

		// Before hitting the db or any RPC, ensure this is a valid ticket hash.
		// A ticket hash should be 64 chars (MaxHashStringSize) and should parse
		// into a chainhash.Hash without error.
		if len(hash) != chainhash.MaxHashStringSize {
			log.Errorf("%s: Incorrect hash length (clientIP=%s): got %d, expected %d",
				funcName, c.ClientIP(), len(hash), chainhash.MaxHashStringSize)
			sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
			return
		}
		_, err = chainhash.NewHashFromStr(hash)
		if err != nil {
			log.Errorf("%s: Invalid hash (clientIP=%s): %v", funcName, c.ClientIP(), err)
			sendErrorWithMsg("invalid ticket hash", errBadRequest, c)
			return
		}

		// Check if this ticket already appears in the database.
		ticket, ticketFound, err := db.GetTicketByHash(hash)
		if err != nil {
			log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, hash, err)
			sendError(errInternalError, c)
			return
		}

		// If the ticket was found in the database, we already know its
		// commitment address. Otherwise we need to get it from the chain.
		var commitmentAddress string
		if ticketFound {
			commitmentAddress = ticket.CommitmentAddress
		} else {
			dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)

			resp, err := dcrdClient.GetRawTransaction(hash)
			if err != nil {
				log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, hash, err)
				sendError(errInternalError, c)
				return
			}

			msgTx, err := decodeTransaction(resp.Hex)
			if err != nil {
				log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, ticket.Hash, err)
				sendError(errInternalError, c)
				return
			}

			err = isValidTicket(msgTx)
			if err != nil {
				log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), hash, err)
				sendError(errInvalidTicket, c)
				return
			}

			addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
			if err != nil {
				log.Errorf("%s: AddrFromSStxPkScrCommitment error (ticketHash=%s): %v", funcName, hash, err)
				sendError(errInternalError, c)
				return
			}

			commitmentAddress = addr.Address()
		}

		// Validate request signature to ensure ticket ownership.
		err = validateSignature(reqBytes, commitmentAddress, c)
		if err != nil {
			log.Warnf("%s: Bad signature (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), hash, err)
			sendError(errBadSignature, c)
			return
		}

		// Add ticket information to context so downstream handlers don't need
		// to access the db for it.
		c.Set("Ticket", ticket)
		c.Set("KnownTicket", ticketFound)
		c.Set("CommitmentAddress", commitmentAddress)
	}

}
