// Copyright © 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package beacon

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	consensusclient "github.com/attestantio/go-eth2-client"
	"github.com/attestantio/go-eth2-client/spec/phase0"
	"github.com/pkg/errors"
	"github.com/wealdtech/ethdo/services/chaintime"
	"github.com/wealdtech/ethdo/util"
)

type ChainInfo struct {
	Version                        uint64
	Validators                     []*ValidatorInfo
	GenesisValidatorsRoot          phase0.Root
	Epoch                          phase0.Epoch
	GenesisForkVersion             phase0.Version
	CurrentForkVersion             phase0.Version
	BLSToExecutionChangeDomainType phase0.DomainType
	VoluntaryExitDomainType        phase0.DomainType
}

type chainInfoJSON struct {
	Version                        string           `json:"version"`
	Validators                     []*ValidatorInfo `json:"validators"`
	GenesisValidatorsRoot          string           `json:"genesis_validators_root"`
	Epoch                          string           `json:"epoch"`
	GenesisForkVersion             string           `json:"genesis_fork_version"`
	CurrentForkVersion             string           `json:"current_fork_version"`
	BLSToExecutionChangeDomainType string           `json:"bls_to_execution_change_domain_type"`
	VoluntaryExitDomainType        string           `json:"voluntary_exit_domain_type"`
}

type chainInfoVersionJSON struct {
	Version string `json:"version"`
}

// MarshalJSON implements json.Marshaler.
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
	return json.Marshal(&chainInfoJSON{
		Version:                        fmt.Sprintf("%d", c.Version),
		Validators:                     c.Validators,
		GenesisValidatorsRoot:          fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
		Epoch:                          fmt.Sprintf("%d", c.Epoch),
		GenesisForkVersion:             fmt.Sprintf("%#x", c.GenesisForkVersion),
		CurrentForkVersion:             fmt.Sprintf("%#x", c.CurrentForkVersion),
		BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
		VoluntaryExitDomainType:        fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
	})
}

// UnmarshalJSON implements json.Unmarshaler.
func (c *ChainInfo) UnmarshalJSON(input []byte) error {
	// See which version we are dealing with.
	var metadata chainInfoVersionJSON
	if err := json.Unmarshal(input, &metadata); err != nil {
		return errors.Wrap(err, "invalid JSON")
	}
	if metadata.Version == "" {
		return errors.New("version missing")
	}
	version, err := strconv.ParseUint(metadata.Version, 10, 64)
	if err != nil {
		return errors.Wrap(err, "version invalid")
	}
	if version < 2 {
		return errors.New("outdated version; please regenerate your offline data")
	}
	c.Version = version

	var data chainInfoJSON
	if err := json.Unmarshal(input, &data); err != nil {
		return errors.Wrap(err, "invalid JSON")
	}

	if len(data.Validators) == 0 {
		return errors.New("validators missing")
	}
	c.Validators = data.Validators

	if data.GenesisValidatorsRoot == "" {
		return errors.New("genesis validators root missing")
	}
	genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
	if err != nil {
		return errors.Wrap(err, "genesis validators root invalid")
	}
	if len(genesisValidatorsRootBytes) != phase0.RootLength {
		return errors.New("genesis validators root incorrect length")
	}
	copy(c.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)

	if data.Epoch == "" {
		return errors.New("epoch missing")
	}
	epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
	if err != nil {
		return errors.Wrap(err, "epoch invalid")
	}
	c.Epoch = phase0.Epoch(epoch)

	if data.GenesisForkVersion == "" {
		return errors.New("genesis fork version missing")
	}
	genesisForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisForkVersion, "0x"))
	if err != nil {
		return errors.Wrap(err, "genesis fork version invalid")
	}
	if len(genesisForkVersionBytes) != phase0.ForkVersionLength {
		return errors.New("genesis fork version incorrect length")
	}
	copy(c.GenesisForkVersion[:], genesisForkVersionBytes)

	if data.CurrentForkVersion == "" {
		return errors.New("current fork version missing")
	}
	currentForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.CurrentForkVersion, "0x"))
	if err != nil {
		return errors.Wrap(err, "current fork version invalid")
	}
	if len(currentForkVersionBytes) != phase0.ForkVersionLength {
		return errors.New("current fork version incorrect length")
	}
	copy(c.CurrentForkVersion[:], currentForkVersionBytes)

	if data.BLSToExecutionChangeDomainType == "" {
		return errors.New("bls to execution domain type missing")
	}
	blsToExecutionChangeDomainType, err := hex.DecodeString(strings.TrimPrefix(data.BLSToExecutionChangeDomainType, "0x"))
	if err != nil {
		return errors.Wrap(err, "bls to execution domain type invalid")
	}
	if len(blsToExecutionChangeDomainType) != phase0.DomainTypeLength {
		return errors.New("bls to execution domain type incorrect length")
	}
	copy(c.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType)

	if data.VoluntaryExitDomainType == "" {
		return errors.New("voluntary exit domain type missing")
	}
	voluntaryExitDomainType, err := hex.DecodeString(strings.TrimPrefix(data.VoluntaryExitDomainType, "0x"))
	if err != nil {
		return errors.Wrap(err, "voluntary exit domain type invalid")
	}
	if len(voluntaryExitDomainType) != phase0.DomainTypeLength {
		return errors.New("voluntary exit domain type incorrect length")
	}
	copy(c.VoluntaryExitDomainType[:], voluntaryExitDomainType)

	return nil
}

// FetchValidatorInfo fetches validator info given a validator identifier.
func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*ValidatorInfo, error) {
	var validatorInfo *ValidatorInfo
	switch {
	case id == "":
		return nil, errors.New("no validator specified")
	case strings.HasPrefix(id, "0x"):
		// ID is a public key.
		// Check that the key is the correct length.
		if len(id) != 98 {
			return nil, errors.New("invalid public key: incorrect length")
		}
		for _, validator := range c.Validators {
			if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
				validatorInfo = validator
				break
			}
		}
	case strings.Contains(id, "/"):
		// An account.
		_, account, err := util.WalletAndAccountFromPath(ctx, id)
		if err != nil {
			return nil, errors.Wrap(err, "unable to obtain account")
		}
		accPubKey, err := util.BestPublicKey(account)
		if err != nil {
			return nil, errors.Wrap(err, "unable to obtain public key for account")
		}
		pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
		for _, validator := range c.Validators {
			if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
				validatorInfo = validator
				break
			}
		}
	default:
		// An index.
		index, err := strconv.ParseUint(id, 10, 64)
		if err != nil {
			return nil, errors.Wrap(err, "failed to parse validator index")
		}
		validatorIndex := phase0.ValidatorIndex(index)
		for _, validator := range c.Validators {
			if validator.Index == validatorIndex {
				validatorInfo = validator
				break
			}
		}
	}

	if validatorInfo == nil {
		return nil, errors.New("unknown validator")
	}

	return validatorInfo, nil
}

// ObtainChainInfoFromNode obtains the chain information from a node.
func ObtainChainInfoFromNode(ctx context.Context,
	consensusClient consensusclient.Service,
	chainTime chaintime.Service,
) (
	*ChainInfo,
	error,
) {
	res := &ChainInfo{
		Version:    2,
		Validators: make([]*ValidatorInfo, 0),
		Epoch:      chainTime.CurrentEpoch(),
	}

	// Obtain validators.
	validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
	if err != nil {
		return nil, errors.Wrap(err, "failed to obtain validators")
	}

	for _, validator := range validators {
		res.Validators = append(res.Validators, &ValidatorInfo{
			Index:                 validator.Index,
			Pubkey:                validator.Validator.PublicKey,
			WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
			State:                 validator.Status,
		})
	}

	// Genesis validators root obtained from beacon node.
	genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to obtain genesis information")
	}
	res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot

	// Fetch the genesis fork version from the specification.
	spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to obtain spec")
	}
	tmp, exists := spec["GENESIS_FORK_VERSION"]
	if !exists {
		return nil, errors.New("genesis fork version not known by chain")
	}
	var isForkVersion bool
	res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
	if !isForkVersion {
		return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
	}

	// Fetch the current fork version from the fork schedule.
	forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to obtain fork schedule")
	}
	for i := range forkSchedule {
		if forkSchedule[i].Epoch <= res.Epoch {
			res.CurrentForkVersion = forkSchedule[i].CurrentVersion
		}
	}

	blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
	if !exists {
		return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
	}
	copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])

	voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
	if !exists {
		return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
	}
	copy(res.VoluntaryExitDomainType[:], voluntaryExitDomainType[:])

	return res, nil
}
