package innerring

import (
	"context"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"go.uber.org/zap"
)

type (
	notaryConfig struct {
		disabled bool // true if notary disabled on chain
	}
)

const (
	// gasMultiplier defines how many times more the notary
	// balance must be compared to the GAS balance of the IR:
	//     notaryBalance = GASBalance * gasMultiplier.
	gasMultiplier = 3

	// gasDivisor defines what part of GAS balance (1/gasDivisor)
	// should be transferred to the notary service.
	gasDivisor = 2
)

func (s *Server) depositMainNotary() (tx util.Uint256, err error) {
	depositAmount, err := client.CalculateNotaryDepositAmount(s.mainnetClient, gasMultiplier, gasDivisor)
	if err != nil {
		return util.Uint256{}, fmt.Errorf("could not calculate main notary deposit amount: %w", err)
	}

	return s.mainnetClient.DepositNotary(
		depositAmount,
		uint32(s.epochDuration.Load())+notaryExtraBlocks,
	)
}

func (s *Server) depositSideNotary() (tx util.Uint256, err error) {
	depositAmount, err := client.CalculateNotaryDepositAmount(s.morphClient, gasMultiplier, gasDivisor)
	if err != nil {
		return util.Uint256{}, fmt.Errorf("could not calculate side notary deposit amount: %w", err)
	}

	return s.morphClient.DepositEndlessNotary(depositAmount)
}

func (s *Server) notaryHandler(_ event.Event) {
	if !s.mainNotaryConfig.disabled {
		_, err := s.depositMainNotary()
		if err != nil {
			s.log.Error(logs.InnerringCantMakeNotaryDepositInMainChain, zap.Error(err))
		}
	}

	if _, err := s.depositSideNotary(); err != nil {
		s.log.Error(logs.InnerringCantMakeNotaryDepositInSideChain, zap.Error(err))
	}
}

func (s *Server) awaitMainNotaryDeposit(ctx context.Context, tx util.Uint256) error {
	return awaitNotaryDepositInClient(ctx, s.mainnetClient, tx)
}

func (s *Server) awaitSideNotaryDeposit(ctx context.Context, tx util.Uint256) error {
	return awaitNotaryDepositInClient(ctx, s.morphClient, tx)
}

func (s *Server) initNotary(ctx context.Context, deposit depositor, await awaiter, msg string) error {
	tx, err := deposit()
	if err != nil {
		return err
	}

	if tx.Equals(util.Uint256{}) {
		// non-error deposit with an empty TX hash means
		// that the deposit has already been made; no
		// need to wait it.
		s.log.Info(logs.InnerringNotaryDepositHasAlreadyBeenMade)
		return nil
	}

	s.log.Info(msg)

	return await(ctx, tx)
}

func awaitNotaryDepositInClient(ctx context.Context, cli *client.Client, txHash util.Uint256) error {
	for i := 0; i < notaryDepositTimeout; i++ {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		ok, err := cli.TxHalt(txHash)
		if err == nil {
			if ok {
				return nil
			}

			return errDepositFail
		}

		_ = cli.Wait(ctx, 1)
	}

	return errDepositTimeout
}

func notaryConfigs(withMainNotary bool) (main *notaryConfig) {
	main = new(notaryConfig)

	main.disabled = !withMainNotary

	return
}