// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2025, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package cometbft

import (
	"context"
	"errors"
	"fmt"

	storetypes "cosmossdk.io/store/types"
	"github.com/berachain/beacon-kit/beacon/blockchain"
	"github.com/berachain/beacon-kit/beacon/validator"
	"github.com/berachain/beacon-kit/chain"
	"github.com/berachain/beacon-kit/consensus/cometbft/service/cache"
	"github.com/berachain/beacon-kit/consensus/cometbft/service/delay"
	servercmtlog "github.com/berachain/beacon-kit/consensus/cometbft/service/log"
	statem "github.com/berachain/beacon-kit/consensus/cometbft/service/state"
	"github.com/berachain/beacon-kit/log/phuslu"
	"github.com/berachain/beacon-kit/primitives/crypto"
	"github.com/berachain/beacon-kit/primitives/transition"
	"github.com/berachain/beacon-kit/storage"
	abci "github.com/cometbft/cometbft/api/cometbft/abci/v1"
	cmtcfg "github.com/cometbft/cometbft/config"
	cmtcrypto "github.com/cometbft/cometbft/crypto"
	"github.com/cometbft/cometbft/node"
	"github.com/cometbft/cometbft/p2p"
	pvm "github.com/cometbft/cometbft/privval"
	"github.com/cometbft/cometbft/proxy"
	cmttypes "github.com/cometbft/cometbft/types"
	dbm "github.com/cosmos/cosmos-db"
	sdk "github.com/cosmos/cosmos-sdk/types"
)

const (
	initialAppVersion uint64 = 0
	AppName           string = "beacond"
)

type Service struct {
	node        *node.Node
	nodeAddress cmtcrypto.Address

	delayCfg delay.ConfigGetter

	// cmtConsensusParams are part of the blockchain state and
	// are agreed upon by all validators in the network.
	cmtConsensusParams *cmttypes.ConsensusParams

	// cmtCfg are node-specific settings that influence how
	// the consensus engine operates on a particular node.
	// Loaded from config file (config.toml), not part of state.
	cmtCfg *cmtcfg.Config

	telemetrySink TelemetrySink

	logger       *phuslu.Logger
	sm           *statem.Manager
	Blockchain   blockchain.BlockchainI
	BlockBuilder validator.BlockBuilderI

	// cachedStates tracks in memory the post block states
	// of blocks which were successfully verified. It allows
	// finalizing without re-execution
	cachedStates cache.States

	interBlockCache storetypes.MultiStorePersistentCache

	// initialHeight is the initial height at which we start the node
	initialHeight   int64
	minRetainBlocks uint64

	chainID string

	// ctx is the context passed in for the service. CometBFT currently does
	// not support context usage. It passes "context.TODO()" to apps that
	// implement the ABCI++ interface, and does not provide a context that is
	// a child context of the one the node originally provides to comet.
	// Thus the app cannot tell when the context as been cancelled or not.
	// TODO: We must use this as a workaround for now until CometBFT properly
	// generates contexts that inherit from the parent context we provide.
	ctx context.Context

	// calculates block delay for the next block
	//
	// NOTE: may be nil until either InitChain or FinalizeBlock is called.
	blockDelay *delay.BlockDelay

	// syncingToHeight is a helper to track node sync state and support node-apis.
	syncingToHeight int64
}

func NewService(
	logger *phuslu.Logger,
	db dbm.DB,
	blockchain blockchain.BlockchainI,
	blockBuilder validator.BlockBuilderI,
	cs chain.Spec,
	cmtCfg *cmtcfg.Config,
	telemetrySink TelemetrySink,
	options ...func(*Service),
) *Service {
	if err := validateConfig(cmtCfg); err != nil {
		panic(err)
	}
	cmtConsensusParams, err := extractConsensusParams(cmtCfg)
	if err != nil {
		panic(err)
	}

	// some configs, while legit, causes issues if applied
	// carelessly. We warn about them
	log := servercmtlog.WrapSDKLogger(logger)
	warnAboutConfigs(cmtCfg, log)

	s := &Service{
		logger:             logger,
		sm:                 statem.NewManager(db, log),
		Blockchain:         blockchain,
		BlockBuilder:       blockBuilder,
		delayCfg:           cs,
		cmtConsensusParams: cmtConsensusParams,
		cmtCfg:             cmtCfg,
		telemetrySink:      telemetrySink,
		cachedStates:       cache.New(),
	}

	s.MountStore(storage.StoreKey, storetypes.StoreTypeIAVL)

	for _, option := range options {
		option(s)
	}

	if s.interBlockCache != nil {
		s.sm.GetCommitMultiStore().SetInterBlockCache(s.interBlockCache)
	}

	// Load latest height, once all stores have been set
	if err = s.sm.LoadLatestVersion(); err != nil {
		panic(fmt.Errorf("failed loading latest version: %w", err))
	}

	lastBlockHeight := s.lastBlockHeight()
	s.syncingToHeight = lastBlockHeight

	// Make sure that SBT consensus parameters are duly set when the node restart.
	// Note that we can't rely on genesis.json having these parameters set right
	// because we introduced stable block time post (mainnet) genesis.
	if lastBlockHeight >= s.delayCfg.SbtConsensusUpdateHeight() {
		s.cmtConsensusParams.Feature.SBTEnableHeight = s.delayCfg.SbtConsensusEnableHeight()
	}

	// Load block delay.
	//
	// If not found, we will initialize it in FinalizeBlock once SBTEnableHeight
	// is reached.
	bz, err := s.sm.LoadBlockDelay()
	if err != nil {
		panic(fmt.Errorf("failed loading block delay: %w", err))
	}
	if bz != nil {
		s.blockDelay, err = delay.FromBytes(bz)
		if err != nil {
			panic(fmt.Errorf("failed decoding block delay: %w", err))
		}
	}

	return s
}

// TODO: Move nodeKey into being created within the function.
func (s *Service) Start(
	ctx context.Context,
) error {
	cfg := s.cmtCfg
	nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile())
	if err != nil {
		return err
	}

	privVal, err := pvm.LoadOrGenFilePV(
		cfg.PrivValidatorKeyFile(),
		cfg.PrivValidatorStateFile(),
		nil,
	)
	if err != nil {
		return err
	}

	s.ResetAppCtx(ctx)
	s.node, err = node.NewNode(
		ctx,
		cfg,
		privVal,
		nodeKey,
		proxy.NewLocalClientCreator(s),
		GetGenDocProvider(cfg),
		cmtcfg.DefaultDBProvider,
		node.DefaultMetricsProvider(cfg.Instrumentation),
		servercmtlog.WrapCometLogger(s.logger),
	)
	if err != nil {
		return err
	}

	pubKey, errPk := s.node.PrivValidator().GetPubKey()
	if errPk != nil {
		return fmt.Errorf("failed retrieving pub key: %w", err)
	}
	s.nodeAddress = pubKey.Address()

	started := make(chan struct{})

	// we start the node in a goroutine since calling Start() can block if genesis
	// time is in the future causing us not to handle signals gracefully.
	go func() {
		err = s.node.Start()
		started <- struct{}{}
	}()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-started:
	}

	close(started)

	return err
}

func (s *Service) Stop() error {
	var errs []error

	if s.node != nil && s.node.IsRunning() {
		s.logger.Info("Stopping CometBFT Node")
		err := s.node.Stop()
		if err != nil {
			errs = append(errs, fmt.Errorf("failed to stop CometBFT Node: %w", err))
		}
		s.logger.Info("Waiting for CometBFT Node to stop")
		s.node.Wait()
	}

	s.logger.Info("Closing application.db")
	if err := s.sm.Close(); err != nil {
		errs = append(errs, fmt.Errorf("failed to close application.id: %w", err))
	}
	return errors.Join(errs...)
}

// ResetAppCtx sets the app ctx for the service. This is used
// primarily for the mock service.
func (s *Service) ResetAppCtx(ctx context.Context) {
	s.ctx = ctx
}

// Name returns the name of the cometbft.
func (s *Service) Name() string {
	return AppName
}

// CommitMultiStore returns the CommitMultiStore of the cometbft.
// This needs to be exposed because it is used by commands like Rollback.
func (s *Service) CommitMultiStore() storetypes.CommitMultiStore {
	return s.sm.GetCommitMultiStore()
}

// AppVersion returns the application's protocol version.
func (s *Service) AppVersion(_ context.Context) (uint64, error) {
	return s.appVersion()
}

func (s *Service) appVersion() (uint64, error) {
	cp := s.cmtConsensusParams.ToProto()
	return cp.Version.App, nil
}

// MountStore mounts a store to the provided key in the Service multistore,
// using the default DB.
func (s *Service) MountStore(
	key storetypes.StoreKey,
	typ storetypes.StoreType,
) {
	s.sm.GetCommitMultiStore().MountStoreWithDB(key, typ, nil)
}

// LastBlockHeight returns the last committed block height.
func (s *Service) lastBlockHeight() int64 {
	return s.sm.GetCommitMultiStore().LastCommitID().Version
}

func (s *Service) setMinRetainBlocks(minRetainBlocks uint64) {
	s.minRetainBlocks = minRetainBlocks
}

func (s *Service) setInterBlockCache(
	cache storetypes.MultiStorePersistentCache,
) {
	s.interBlockCache = cache
}

// resetState provides a fresh state which can be used to reset
// prepareProposal/processProposal/finalizeBlock State.
// A state is explicitly returned to avoid false positives from
// nilaway tool.
func (s *Service) resetState(ctx context.Context) *cache.State {
	ms := s.sm.GetCommitMultiStore().CacheMultiStore()

	newCtx := sdk.NewContext(
		ms,
		false,
		servercmtlog.WrapSDKLogger(s.logger),
	).WithContext(ctx)

	return cache.NewState(ms, newCtx)
}

// convertValidatorUpdate abstracts the conversion of a
// transition.ValidatorUpdate to an appmodulev2.ValidatorUpdate.
// TODO: this is so hood, bktypes -> sdktypes -> generic is crazy
// maybe make this some kind of codec/func that can be passed in?
func convertValidatorUpdate[ValidatorUpdateT any](
	u **transition.ValidatorUpdate,
) (ValidatorUpdateT, error) {
	var valUpdate ValidatorUpdateT
	update := *u
	if update == nil {
		return valUpdate, errors.New("undefined validator update")
	}
	//nolint:errcheck // should be safe
	return any(abci.ValidatorUpdate{
		PubKeyBytes: update.Pubkey[:],
		PubKeyType:  crypto.CometBLSType,
		Power:       int64(update.EffectiveBalance.Unwrap()), // #nosec G115 -- this is safe.
	}).(ValidatorUpdateT), nil
}

// getContextForProposal returns the correct Context for PrepareProposal and
// ProcessProposal. We use finalizeBlockState on the first block to be able to
// access any state changes made in InitChain.
func (s *Service) getContextForProposal(
	ctx sdk.Context,
	height int64,
) sdk.Context {
	if height != s.initialHeight {
		return ctx
	}

	_, finalState, err := s.cachedStates.GetFinal()
	if err != nil {
		// this is unexpected since cometBFT won't call PrepareProposal
		// on initialHeight. Panic appeases nilaway.
		panic(fmt.Errorf("getContextForProposal: %w", err))
	}
	newCtx, _ := finalState.Context().CacheContext()
	// Preserve the CosmosSDK context while using the correct base ctx.
	return newCtx.WithContext(ctx.Context())
}
