package components

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path"
	"strings"
	"time"

	"github.com/bazelbuild/rules_go/go/tools/bazel"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/pkg/errors"
	"github.com/prysmaticlabs/prysm/config/params"
	"github.com/prysmaticlabs/prysm/crypto/bls"
	"github.com/prysmaticlabs/prysm/runtime/interop"
	e2e "github.com/prysmaticlabs/prysm/testing/endtoend/params"
	e2etypes "github.com/prysmaticlabs/prysm/testing/endtoend/types"
	"gopkg.in/yaml.v2"
)

const Web3RemoteSignerPort = 9000

var _ e2etypes.ComponentRunner = (*Web3RemoteSigner)(nil)

// rawKeyFile used for consensys's web3signer config files.
// See: https://docs.web3signer.consensys.net/en/latest/Reference/Key-Configuration-Files/#raw-unencrypted-files
type rawKeyFile struct {
	Type       string `yaml:"type"`       // always "file-raw" for this test.
	KeyType    string `yaml:"keyType"`    // always "BLS" for this test.
	PrivateKey string `yaml:"privateKey"` // hex encoded private key with 0x prefix.
}

type Web3RemoteSigner struct {
	ctx     context.Context
	started chan struct{}
}

func NewWeb3RemoteSigner() *Web3RemoteSigner {
	return &Web3RemoteSigner{
		started: make(chan struct{}, 1),
	}
}

// Start the web3remotesigner component with a keystore populated with the deterministic validator
// keys.
func (w *Web3RemoteSigner) Start(ctx context.Context) error {
	w.ctx = ctx

	binaryPath, found := bazel.FindBinary("", "web3signer")
	if !found {
		return errors.New("web3signer binary not found")
	}

	keystorePath := path.Join(bazel.TestTmpDir(), "web3signerkeystore")
	if err := writeKeystoreKeys(ctx, keystorePath, params.BeaconConfig().MinGenesisActiveValidatorCount); err != nil {
		return err
	}
	websignerDataDir := path.Join(bazel.TestTmpDir(), "web3signerdata")
	if err := os.MkdirAll(websignerDataDir, 0750); err != nil {
		return err
	}

	args := []string{
		// Global flags
		fmt.Sprintf("--key-store-path=%s", keystorePath),
		fmt.Sprintf("--data-path=%s", websignerDataDir),
		fmt.Sprintf("--http-listen-port=%d", Web3RemoteSignerPort),
		"--logging=ALL",
		// Command
		"eth2",
		// Command flags
		"--network=minimal",
		"--slashing-protection-enabled=false", // Otherwise, a postgres DB is required.
		"--enable-key-manager-api=true",
	}

	cmd := exec.CommandContext(ctx, binaryPath, args...) // #nosec G204 -- Test code is safe to do this.

	// Write stdout and stderr to log files.
	stdout, err := os.Create(path.Join(e2e.TestParams.LogPath, "web3signer.stdout.log"))
	if err != nil {
		return err
	}
	stderr, err := os.Create(path.Join(e2e.TestParams.LogPath, "web3signer.stderr.log"))
	if err != nil {
		return err
	}
	defer func() {
		if err := stdout.Close(); err != nil {
			log.WithError(err).Error("Failed to close stdout file")
		}
		if err := stderr.Close(); err != nil {
			log.WithError(err).Error("Failed to close stderr file")
		}
	}()
	cmd.Stdout = stdout
	cmd.Stderr = stderr

	log.Infof("Starting web3signer with flags: %s %s", binaryPath, strings.Join(args, " "))
	if err = cmd.Start(); err != nil {
		return err
	}

	go w.monitorStart()

	return cmd.Wait()
}

func (w *Web3RemoteSigner) Started() <-chan struct{} {
	return w.started
}

// monitorStart by polling server until it returns a 200 at /upcheck.
func (w *Web3RemoteSigner) monitorStart() {
	client := &http.Client{}
	for {
		req, err := http.NewRequestWithContext(w.ctx, "GET", fmt.Sprintf("http://localhost:%d/upcheck", Web3RemoteSignerPort), nil)
		if err != nil {
			panic(err)
		}
		res, err := client.Do(req)
		_ = err
		if res != nil && res.StatusCode == 200 {
			close(w.started)
			return
		}
		time.Sleep(time.Second)
	}
}

func (w *Web3RemoteSigner) wait(ctx context.Context) {
	select {
	case <-ctx.Done():
		return
	case <-w.ctx.Done():
		return
	case <-w.started:
		return
	}
}

// PublicKeys queries the web3signer and returns the response keys.
func (w *Web3RemoteSigner) PublicKeys(ctx context.Context) ([]bls.PublicKey, error) {
	w.wait(ctx)

	client := &http.Client{}
	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost:%d/api/v1/eth2/publicKeys", Web3RemoteSignerPort), nil)
	if err != nil {
		return nil, err
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	if res.StatusCode != 200 {
		return nil, fmt.Errorf("returned status code %d", res.StatusCode)
	}
	b, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	} else if len(b) == 0 {
		return nil, errors.New("no response body")
	}
	var keys []string
	if err := json.Unmarshal(b, &keys); err != nil {
		return nil, err
	}
	if len(keys) == 0 {
		return nil, errors.New("no keys returned")
	}

	var pks []bls.PublicKey
	for _, key := range keys {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		raw, err := hexutil.Decode(key)
		if err != nil {
			return nil, err
		}
		pk, err := bls.PublicKeyFromBytes(raw)
		if err != nil {
			return nil, err
		}
		pks = append(pks, pk)
	}
	return pks, nil
}

func writeKeystoreKeys(ctx context.Context, keystorePath string, numKeys uint64) error {
	if err := os.MkdirAll(keystorePath, 0750); err != nil {
		return err
	}

	priv, pub, err := interop.DeterministicallyGenerateKeys(0, numKeys)
	if err != nil {
		return err
	}
	for i, pk := range priv {
		if ctx.Err() != nil {
			return ctx.Err()
		}
		rkf := &rawKeyFile{
			Type:       "file-raw",
			KeyType:    "BLS",
			PrivateKey: hexutil.Encode(pk.Marshal()),
		}
		b, err := yaml.Marshal(rkf)
		if err != nil {
			return err
		}
		if err := os.WriteFile(path.Join(keystorePath, fmt.Sprintf("key-0x%s.yaml", hex.EncodeToString(pub[i].Marshal()))), b, 0600); err != nil {
			return err
		}
	}

	return nil
}
