package kvstore

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	dbm "github.com/cometbft/cometbft-db"

	"github.com/cometbft/cometbft/abci/types"
	cryptoencoding "github.com/cometbft/cometbft/crypto/encoding"
	"github.com/cometbft/cometbft/libs/log"
	cryptoproto "github.com/cometbft/cometbft/proto/tendermint/crypto"
	"github.com/cometbft/cometbft/version"
)

var (
	stateKey        = []byte("stateKey")
	kvPairPrefixKey = []byte("kvPairKey:")
)

const (
	ValidatorPrefix        = "val="
	AppVersion      uint64 = 1
)

var _ types.Application = (*Application)(nil)

// Application is the kvstore state machine. It complies with the abci.Application interface.
// It takes transactions in the form of key=value and saves them in a database. This is
// a somewhat trivial example as there is no real state execution
type Application struct {
	types.BaseApplication

	state        State
	RetainBlocks int64 // blocks to retain after commit (via ResponseCommit.RetainHeight)
	stagedTxs    [][]byte
	logger       log.Logger

	// validator set
	valUpdates         []types.ValidatorUpdate
	valAddrToPubKeyMap map[string]cryptoproto.PublicKey

	// If true, the app will generate block events in BeginBlock. Used to test the event indexer
	// Should be false by default to avoid generating too much data.
	genBlockEvents bool
}

// NewApplication creates an instance of the kvstore from the provided database
func NewApplication(db dbm.DB) *Application {
	return &Application{
		logger:             log.NewNopLogger(),
		state:              loadState(db),
		valAddrToPubKeyMap: make(map[string]cryptoproto.PublicKey),
	}
}

// NewPersistentApplication creates a new application using the goleveldb database engine
func NewPersistentApplication(dbDir string) *Application {
	name := "kvstore"
	db, err := dbm.NewGoLevelDB(name, dbDir)
	if err != nil {
		panic(fmt.Errorf("failed to create persistent app at %s: %w", dbDir, err))
	}
	return NewApplication(db)
}

// NewInMemoryApplication creates a new application from an in memory database.
// Nothing will be persisted.
func NewInMemoryApplication() *Application {
	return NewApplication(dbm.NewMemDB())
}

func (app *Application) SetGenBlockEvents() {
	app.genBlockEvents = true
}

// Info returns information about the state of the application. This is generally used everytime a Tendermint instance
// begins and let's the application know what Tendermint versions it's interacting with. Based from this information,
// Tendermint will ensure it is in sync with the application by potentially replaying the blocks it has. If the
// Application returns a 0 appBlockHeight, Tendermint will call InitChain to initialize the application with consensus related data
func (app *Application) Info(context.Context, *types.RequestInfo) (*types.ResponseInfo, error) {
	// Tendermint expects the application to persist validators, on start-up we need to reload them to memory if they exist
	if len(app.valAddrToPubKeyMap) == 0 && app.state.Height > 0 {
		validators := app.getValidators()
		for _, v := range validators {
			pubkey, err := cryptoencoding.PubKeyFromProto(v.PubKey)
			if err != nil {
				panic(fmt.Errorf("can't decode public key: %w", err))
			}
			app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey
		}
	}

	return &types.ResponseInfo{
		Data:             fmt.Sprintf("{\"size\":%v}", app.state.Size),
		Version:          version.ABCIVersion,
		AppVersion:       AppVersion,
		LastBlockHeight:  app.state.Height,
		LastBlockAppHash: app.state.Hash(),
	}, nil
}

// InitChain takes the genesis validators and stores them in the kvstore. It returns the application hash in the
// case that the application starts prepopulated with values. This method is called whenever a new instance of the application
// starts (i.e. app height = 0).
func (app *Application) InitChain(_ context.Context, req *types.RequestInitChain) (*types.ResponseInitChain, error) {
	for _, v := range req.Validators {
		app.updateValidator(v)
	}
	appHash := make([]byte, 8)
	binary.PutVarint(appHash, app.state.Size)
	return &types.ResponseInitChain{
		AppHash: appHash,
	}, nil
}

// CheckTx handles inbound transactions or in the case of recheckTx assesses old transaction validity after a state transition.
// As this is called frequently, it's preferably to keep the check as stateless and as quick as possible.
// Here we check that the transaction has the correctly key=value format.
// For the KVStore we check that each transaction has the valid tx format:
// - Contains one and only one `=`
// - `=` is not the first or last byte.
// - if key is `val` that the validator update transaction is also valid
func (app *Application) CheckTx(_ context.Context, req *types.RequestCheckTx) (*types.ResponseCheckTx, error) {
	// If it is a validator update transaction, check that it is correctly formatted
	if isValidatorTx(req.Tx) {
		if _, _, _, err := parseValidatorTx(req.Tx); err != nil {
			//nolint:nilerr
			return &types.ResponseCheckTx{Code: CodeTypeInvalidTxFormat}, nil
		}
	} else if !isValidTx(req.Tx) {
		return &types.ResponseCheckTx{Code: CodeTypeInvalidTxFormat}, nil
	}

	return &types.ResponseCheckTx{Code: CodeTypeOK, GasWanted: 1}, nil
}

// Tx must have a format like key:value or key=value. That is:
// - it must have one and only one ":" or "="
// - It must not begin or end with these special characters
func isValidTx(tx []byte) bool {
	if bytes.Count(tx, []byte(":")) == 1 && bytes.Count(tx, []byte("=")) == 0 {
		if !bytes.HasPrefix(tx, []byte(":")) && !bytes.HasSuffix(tx, []byte(":")) {
			return true
		}
	} else if bytes.Count(tx, []byte("=")) == 1 && bytes.Count(tx, []byte(":")) == 0 {
		if !bytes.HasPrefix(tx, []byte("=")) && !bytes.HasSuffix(tx, []byte("=")) {
			return true
		}
	}
	return false
}

// PrepareProposal is called when the node is a proposer. CometBFT stages a set of transactions to the application. As the
// KVStore has two accepted formats, `:` and `=`, we modify all instances of `:` with `=` to make it consistent. Note: this is
// quite a trivial example of transaction modification.
// NOTE: we assume that CometBFT will never provide more transactions than can fit in a block.
func (app *Application) PrepareProposal(ctx context.Context, req *types.RequestPrepareProposal) (*types.ResponsePrepareProposal, error) {
	return &types.ResponsePrepareProposal{Txs: app.formatTxs(ctx, req.Txs)}, nil
}

// formatTxs validates and excludes invalid transactions
// also substitutes all the transactions with x:y to x=y
func (app *Application) formatTxs(ctx context.Context, blockData [][]byte) [][]byte {
	txs := make([][]byte, 0, len(blockData))
	for _, tx := range blockData {
		if resp, err := app.CheckTx(ctx, &types.RequestCheckTx{Tx: tx}); err == nil && resp.Code == CodeTypeOK {
			txs = append(txs, bytes.Replace(tx, []byte(":"), []byte("="), 1))
		}
	}
	return txs
}

// ProcessProposal is called whenever a node receives a complete proposal. It allows the application to validate the proposal.
// Only validators who can vote will have this method called. For the KVstore we reuse CheckTx.
func (app *Application) ProcessProposal(ctx context.Context, req *types.RequestProcessProposal) (*types.ResponseProcessProposal, error) {
	for _, tx := range req.Txs {
		// As CheckTx is a full validity check we can simply reuse this
		if resp, err := app.CheckTx(ctx, &types.RequestCheckTx{Tx: tx}); err != nil || resp.Code != CodeTypeOK {
			return &types.ResponseProcessProposal{Status: types.ResponseProcessProposal_REJECT}, nil
		}
	}
	return &types.ResponseProcessProposal{Status: types.ResponseProcessProposal_ACCEPT}, nil
}

// FinalizeBlock executes the block against the application state. It punishes validators who equivocated and
// updates validators according to transactions in a block. The rest of the transactions are regular key value
// updates and are cached in memory and will be persisted once Commit is called.
// ConsensusParams are never changed.
func (app *Application) FinalizeBlock(_ context.Context, req *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) {
	// reset valset changes
	app.valUpdates = make([]types.ValidatorUpdate, 0)
	app.stagedTxs = make([][]byte, 0)

	// Punish validators who committed equivocation.
	for _, ev := range req.Misbehavior {
		if ev.Type == types.MisbehaviorType_DUPLICATE_VOTE {
			addr := string(ev.Validator.Address)
			if pubKey, ok := app.valAddrToPubKeyMap[addr]; ok {
				app.valUpdates = append(app.valUpdates, types.ValidatorUpdate{
					PubKey: pubKey,
					Power:  ev.Validator.Power - 1,
				})
				app.logger.Info("Decreased val power by 1 because of the equivocation",
					"val", addr)
			} else {
				panic(fmt.Errorf("wanted to punish val %q but can't find it", addr))
			}
		}
	}

	respTxs := make([]*types.ExecTxResult, len(req.Txs))
	for i, tx := range req.Txs {
		if isValidatorTx(tx) {
			keyType, pubKey, power, err := parseValidatorTx(tx)
			if err != nil {
				panic(err)
			}
			app.valUpdates = append(app.valUpdates, types.UpdateValidator(pubKey, power, keyType))
		} else {
			app.stagedTxs = append(app.stagedTxs, tx)
		}

		var key, value string
		parts := bytes.Split(tx, []byte("="))
		if len(parts) == 2 {
			key, value = string(parts[0]), string(parts[1])
		} else {
			key, value = string(tx), string(tx)
		}
		respTxs[i] = &types.ExecTxResult{
			Code: CodeTypeOK,
			// With every transaction we can emit a series of events. To make it simple, we just emit the same events.
			Events: []types.Event{
				{
					Type: "app",
					Attributes: []types.EventAttribute{
						{Key: "creator", Value: "Cosmoshi Netowoko", Index: true},
						{Key: "key", Value: key, Index: true},
						{Key: "index_key", Value: "index is working", Index: true},
						{Key: "noindex_key", Value: "index is working", Index: false},
					},
				},
				{
					Type: "app",
					Attributes: []types.EventAttribute{
						{Key: "creator", Value: "Cosmoshi", Index: true},
						{Key: "key", Value: value, Index: true},
						{Key: "index_key", Value: "index is working", Index: true},
						{Key: "noindex_key", Value: "index is working", Index: false},
					},
				},
			},
		}
		app.state.Size++
	}

	app.state.Height = req.Height

	response := &types.ResponseFinalizeBlock{TxResults: respTxs, ValidatorUpdates: app.valUpdates, AppHash: app.state.Hash()}
	if !app.genBlockEvents {
		return response, nil
	}
	if app.state.Height%2 == 0 {
		response.Events = []types.Event{
			{
				Type: "begin_event",
				Attributes: []types.EventAttribute{
					{
						Key:   "foo",
						Value: "100",
						Index: true,
					},
					{
						Key:   "bar",
						Value: "200",
						Index: true,
					},
				},
			},
			{
				Type: "begin_event",
				Attributes: []types.EventAttribute{
					{
						Key:   "foo",
						Value: "200",
						Index: true,
					},
					{
						Key:   "bar",
						Value: "300",
						Index: true,
					},
				},
			},
		}
	} else {
		response.Events = []types.Event{
			{
				Type: "begin_event",
				Attributes: []types.EventAttribute{
					{
						Key:   "foo",
						Value: "400",
						Index: true,
					},
					{
						Key:   "bar",
						Value: "300",
						Index: true,
					},
				},
			},
		}
	}
	return response, nil
}

// Commit is called after FinalizeBlock and after Tendermint state which includes the updates to
// AppHash, ConsensusParams and ValidatorSet has occurred.
// The KVStore persists the validator updates and the new key values
func (app *Application) Commit(context.Context, *types.RequestCommit) (*types.ResponseCommit, error) {
	// apply the validator updates to state (note this is really the validator set at h + 2)
	for _, valUpdate := range app.valUpdates {
		app.updateValidator(valUpdate)
	}

	// persist all the staged txs in the kvstore
	for _, tx := range app.stagedTxs {
		parts := bytes.Split(tx, []byte("="))
		if len(parts) != 2 {
			panic(fmt.Sprintf("unexpected tx format. Expected 2 got %d: %s", len(parts), parts))
		}
		key, value := string(parts[0]), string(parts[1])
		err := app.state.db.Set(prefixKey([]byte(key)), []byte(value))
		if err != nil {
			panic(err)
		}
	}

	// persist the state (i.e. size and height)
	saveState(app.state)

	resp := &types.ResponseCommit{}
	if app.RetainBlocks > 0 && app.state.Height >= app.RetainBlocks {
		resp.RetainHeight = app.state.Height - app.RetainBlocks + 1
	}
	return resp, nil
}

// Returns an associated value or nil if missing.
func (app *Application) Query(_ context.Context, reqQuery *types.RequestQuery) (*types.ResponseQuery, error) {
	resQuery := &types.ResponseQuery{}

	if reqQuery.Path == "/val" {
		key := []byte(ValidatorPrefix + string(reqQuery.Data))
		value, err := app.state.db.Get(key)
		if err != nil {
			panic(err)
		}

		return &types.ResponseQuery{
			Key:   reqQuery.Data,
			Value: value,
		}, nil
	}

	if reqQuery.Prove {
		value, err := app.state.db.Get(prefixKey(reqQuery.Data))
		if err != nil {
			panic(err)
		}

		if value == nil {
			resQuery.Log = "does not exist"
		} else {
			resQuery.Log = "exists"
		}
		resQuery.Index = -1 // TODO make Proof return index
		resQuery.Key = reqQuery.Data
		resQuery.Value = value
		resQuery.Height = app.state.Height

		return resQuery, nil
	}

	resQuery.Key = reqQuery.Data
	value, err := app.state.db.Get(prefixKey(reqQuery.Data))
	if err != nil {
		panic(err)
	}
	if value == nil {
		resQuery.Log = "does not exist"
	} else {
		resQuery.Log = "exists"
	}
	resQuery.Value = value
	resQuery.Height = app.state.Height

	return resQuery, nil
}

func (app *Application) Close() error {
	return app.state.db.Close()
}

func isValidatorTx(tx []byte) bool {
	return strings.HasPrefix(string(tx), ValidatorPrefix)
}

func parseValidatorTx(tx []byte) (string, []byte, int64, error) {
	tx = tx[len(ValidatorPrefix):]

	//  get the pubkey and power
	typePubKeyAndPower := strings.Split(string(tx), "!")
	if len(typePubKeyAndPower) != 3 {
		return "", nil, 0, fmt.Errorf("expected 'pubkeytype!pubkey!power'. Got %v", typePubKeyAndPower)
	}
	keyType, pubkeyS, powerS := typePubKeyAndPower[0], typePubKeyAndPower[1], typePubKeyAndPower[2]

	// decode the pubkey
	pubkey, err := base64.StdEncoding.DecodeString(pubkeyS)
	if err != nil {
		return "", nil, 0, fmt.Errorf("pubkey (%s) is invalid base64", pubkeyS)
	}

	// decode the power
	power, err := strconv.ParseInt(powerS, 10, 64)
	if err != nil {
		return "", nil, 0, fmt.Errorf("power (%s) is not an int", powerS)
	}

	if power < 0 {
		return "", nil, 0, fmt.Errorf("power can not be less than 0, got %d", power)
	}

	return keyType, pubkey, power, nil
}

// add, update, or remove a validator
func (app *Application) updateValidator(v types.ValidatorUpdate) {
	pubkey, err := cryptoencoding.PubKeyFromProto(v.PubKey)
	if err != nil {
		panic(fmt.Errorf("can't decode public key: %w", err))
	}
	key := []byte(ValidatorPrefix + string(pubkey.Bytes()))

	if v.Power == 0 {
		// remove validator
		hasKey, err := app.state.db.Has(key)
		if err != nil {
			panic(err)
		}
		if !hasKey {
			pubStr := base64.StdEncoding.EncodeToString(pubkey.Bytes())
			app.logger.Info("tried to remove non existent validator. Skipping...", "pubKey", pubStr)
		}
		if err = app.state.db.Delete(key); err != nil {
			panic(err)
		}
		delete(app.valAddrToPubKeyMap, string(pubkey.Address()))
	} else {
		// add or update validator
		value := bytes.NewBuffer(make([]byte, 0))
		if err := types.WriteMessage(&v, value); err != nil {
			panic(err)
		}
		if err = app.state.db.Set(key, value.Bytes()); err != nil {
			panic(err)
		}
		app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey
	}
}

func (app *Application) getValidators() (validators []types.ValidatorUpdate) {
	itr, err := app.state.db.Iterator(nil, nil)
	if err != nil {
		panic(err)
	}
	for ; itr.Valid(); itr.Next() {
		if isValidatorTx(itr.Key()) {
			validator := new(types.ValidatorUpdate)
			err := types.ReadMessage(bytes.NewBuffer(itr.Value()), validator)
			if err != nil {
				panic(err)
			}
			validators = append(validators, *validator)
		}
	}
	if err = itr.Error(); err != nil {
		panic(err)
	}
	return
}

// -----------------------------

type State struct {
	db dbm.DB
	// Size is essentially the amount of transactions that have been processes.
	// This is used for the appHash
	Size   int64 `json:"size"`
	Height int64 `json:"height"`
}

func loadState(db dbm.DB) State {
	var state State
	state.db = db
	stateBytes, err := db.Get(stateKey)
	if err != nil {
		panic(err)
	}
	if len(stateBytes) == 0 {
		return state
	}
	err = json.Unmarshal(stateBytes, &state)
	if err != nil {
		panic(err)
	}
	return state
}

func saveState(state State) {
	stateBytes, err := json.Marshal(state)
	if err != nil {
		panic(err)
	}
	err = state.db.Set(stateKey, stateBytes)
	if err != nil {
		panic(err)
	}
}

// Hash returns the hash of the application state. This is computed
// as the size or number of transactions processed within the state. Note that this isn't
// a strong guarantee of state machine replication because states could
// have different kv values but still have the same size.
// This function is used as the "AppHash"
func (s State) Hash() []byte {
	appHash := make([]byte, 8)
	binary.PutVarint(appHash, s.Size)
	return appHash
}

func prefixKey(key []byte) []byte {
	return append(kvPairPrefixKey, key...)
}
