package chaincmdrunner

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/cenkalti/backoff"

	"github.com/ignite/cli/v29/ignite/pkg/chaincmd"
	"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
	"github.com/ignite/cli/v29/ignite/pkg/cosmosver"
	"github.com/ignite/cli/v29/ignite/pkg/errors"
)

// Start starts the blockchain.
func (r Runner) Start(ctx context.Context, args ...string) error {
	return r.run(
		ctx,
		runOptions{wrappedStdErrMaxLen: 50000},
		r.chainCmd.StartCommand(args...),
	)
}

// Init inits the blockchain.
func (r Runner) Init(ctx context.Context, moniker string, args ...string) error {
	return r.run(ctx, runOptions{}, r.chainCmd.InitCommand(moniker, args...))
}

// KV holds a key, value pair.
type KV struct {
	key   string
	value string
}

// NewKV returns a new key, value pair.
func NewKV(key, value string) KV {
	return KV{key, value}
}

var gentxRe = regexp.MustCompile(`(?m)"(.+?)"`)

func (r Runner) InPlace(ctx context.Context, newChainID, newOperatorAddress string, options ...chaincmd.InPlaceOption) error {
	runOptions := runOptions{
		stdout: os.Stdout,
		stderr: os.Stderr,
	}
	return r.run(
		ctx,
		runOptions,
		r.chainCmd.TestnetInPlaceCommand(newChainID, newOperatorAddress, options...),
	)
}

// Initialize config directories & files for a multi-validator testnet locally.
func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error {
	runOptions := runOptions{
		stdout: os.Stdout,
		stderr: os.Stderr,
	}
	return r.run(
		ctx,
		runOptions,
		r.chainCmd.TestnetMultiNodeCommand(options...),
	)
}

// Gentx generates a genesis tx carrying a self delegation.
func (r Runner) Gentx(
	ctx context.Context,
	validatorName,
	selfDelegation string,
	options ...chaincmd.GentxOption,
) (gentxPath string, err error) {
	b := newBuffer()

	if err := r.run(ctx, runOptions{
		stdout: b,
		stderr: b,
		stdin:  os.Stdin,
	}, r.chainCmd.GentxCommand(validatorName, selfDelegation, options...)); err != nil {
		return "", err
	}

	return gentxRe.FindStringSubmatch(b.String())[1], nil
}

// CollectGentxs collects gentxs.
func (r Runner) CollectGentxs(ctx context.Context) error {
	return r.run(ctx, runOptions{}, r.chainCmd.CollectGentxsCommand())
}

// ValidateGenesis validates genesis.
func (r Runner) ValidateGenesis(ctx context.Context) error {
	return r.run(ctx, runOptions{}, r.chainCmd.ValidateGenesisCommand())
}

// UnsafeReset resets the blockchain database.
func (r Runner) UnsafeReset(ctx context.Context) error {
	return r.run(ctx, runOptions{}, r.chainCmd.UnsafeResetCommand())
}

// ShowNodeID shows node id.
func (r Runner) ShowNodeID(ctx context.Context) (nodeID string, err error) {
	b := newBuffer()
	err = r.run(ctx, runOptions{stdout: b}, r.chainCmd.ShowNodeIDCommand())
	nodeID = strings.TrimSpace(b.String())
	return
}

// NodeStatus keeps info about node's status.
type NodeStatus struct {
	ChainID string
}

// Status returns the node's status.
func (r Runner) Status(ctx context.Context) (NodeStatus, error) {
	b := newBuffer()

	if err := r.run(ctx, runOptions{stdout: b, stderr: b}, r.chainCmd.StatusCommand()); err != nil {
		return NodeStatus{}, err
	}

	var chainID string

	data, err := b.JSONEnsuredBytes()
	if err != nil {
		return NodeStatus{}, err
	}

	version := r.chainCmd.SDKVersion()
	switch {
	case version.GTE(cosmosver.StargateFortyVersion):
		out := struct {
			NodeInfo struct {
				Network string `json:"network"`
			} `json:"NodeInfo"`
		}{}

		if err := json.Unmarshal(data, &out); err != nil {
			return NodeStatus{}, err
		}

		chainID = out.NodeInfo.Network
	default:
		out := struct {
			NodeInfo struct {
				Network string `json:"network"`
			} `json:"node_info"`
		}{}

		if err := json.Unmarshal(data, &out); err != nil {
			return NodeStatus{}, err
		}

		chainID = out.NodeInfo.Network
	}

	return NodeStatus{
		ChainID: chainID,
	}, nil
}

// BankSend sends amount from fromAccount to toAccount.
func (r Runner) BankSend(ctx context.Context, fromAccount, toAccount, amount string, options ...chaincmd.BankSendOption) (string, error) {
	b := newBuffer()
	opt := []step.Option{
		r.chainCmd.BankSendCommand(fromAccount, toAccount, amount, options...),
	}

	if r.chainCmd.KeyringPassword() != "" {
		input := newBuffer()
		_, err := fmt.Fprintln(input, r.chainCmd.KeyringPassword())
		if err != nil {
			return "", err
		}
		_, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword())
		if err != nil {
			return "", err
		}
		_, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword())
		if err != nil {
			return "", err
		}
		opt = append(opt, step.Write(input.Bytes()))
	}

	if err := r.run(ctx, runOptions{stdout: b}, opt...); err != nil {
		if strings.Contains(err.Error(), "key not found") {
			return "", errors.New("account doesn't have any balances")
		}

		return "", err
	}

	txResult, err := decodeTxResult(b)
	if err != nil {
		return "", err
	}

	if txResult.Code > 0 {
		return "", errors.Errorf("cannot send tokens (SDK code %d): %s", txResult.Code, txResult.RawLog)
	}

	return txResult.TxHash, nil
}

// WaitTx waits until a tx is successfully added to a block and can be queried.
func (r Runner) WaitTx(ctx context.Context, txHash string, retryDelay time.Duration, maxRetry int) error {
	retry := 0

	// retry querying the request
	checkTx := func() error {
		b := newBuffer()
		if err := r.run(ctx, runOptions{stdout: b}, r.chainCmd.QueryTxCommand(txHash)); err != nil {
			// filter not found error and check for max retry
			if !strings.Contains(err.Error(), "not found") {
				return backoff.Permanent(err)
			}
			retry++
			if retry == maxRetry {
				return backoff.Permanent(errors.Errorf("can't retrieve tx %s", txHash))
			}
			return err
		}

		// parse tx and check code
		txResult, err := decodeTxResult(b)
		if err != nil {
			return backoff.Permanent(err)
		}
		if txResult.Code != 0 {
			return backoff.Permanent(errors.Errorf("tx %s failed: %s", txHash, txResult.RawLog))
		}

		return nil
	}
	return backoff.Retry(checkTx, backoff.WithContext(backoff.NewConstantBackOff(retryDelay), ctx))
}

// Export exports the state of the chain into the specified file.
func (r Runner) Export(ctx context.Context, exportedFile string) error {
	// Make sure the path exists
	dir := filepath.Dir(exportedFile)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}

	stdout, stderr := newBuffer(), newBuffer()
	if err := r.run(ctx, runOptions{stdout: stdout, stderr: stderr}, r.chainCmd.ExportCommand()); err != nil {
		return err
	}

	// Exported genesis is written on stderr from Cosmos-SDK v0.44.0
	var exportedState []byte
	if stdout.Len() > 0 {
		exportedState = stdout.Bytes()
	} else {
		exportedState = stderr.Bytes()
	}

	// Save the new state
	return os.WriteFile(exportedFile, exportedState, 0o600)
}

// EventSelector is used to query events.
type EventSelector struct {
	typ   string
	attr  string
	value string
}

// NewEventSelector creates a new event selector.
func NewEventSelector(typ, addr, value string) EventSelector {
	return EventSelector{typ, addr, value}
}

// Event represents a TX event.
type Event struct {
	Type       string
	Attributes []EventAttribute
	Time       time.Time
}

// EventAttribute holds event's attributes.
type EventAttribute struct {
	Key   string
	Value string
}

// QueryTxByEvents queries tx events by event selectors.
func (r Runner) QueryTxByEvents(
	ctx context.Context,
	selectors ...EventSelector,
) ([]Event, error) {
	if len(selectors) == 0 {
		return nil, errors.New("event selector list should be greater than zero")
	}
	list := make([]string, len(selectors))
	for i, event := range selectors {
		list[i] = fmt.Sprintf("%s.%s=%s", event.typ, event.attr, event.value)
	}
	query := strings.Join(list, "&")
	return r.QueryTx(ctx, r.chainCmd.QueryTxEventsCommand(query))
}

// QueryTxByQuery queries tx events by event selectors.
func (r Runner) QueryTxByQuery(
	ctx context.Context,
	selectors ...EventSelector,
) ([]Event, error) {
	if len(selectors) == 0 {
		return nil, errors.New("event selector list should be greater than zero")
	}
	list := make([]string, len(selectors))
	for i, query := range selectors {
		list[i] = fmt.Sprintf("%s.%s='%s'", query.typ, query.attr, query.value)
	}
	query := strings.Join(list, " AND ")
	return r.QueryTx(ctx, r.chainCmd.QueryTxQueryCommand(query))
}

// QueryTx queries tx events/query selectors.
func (r Runner) QueryTx(
	ctx context.Context,
	option ...step.Option,
) ([]Event, error) {
	// execute the command and parse the output.
	b := newBuffer()
	if err := r.run(ctx, runOptions{stdout: b}, option...); err != nil {
		return nil, err
	}

	out := struct {
		Txs []struct {
			Logs []struct {
				Events []struct {
					Type  string `json:"type"`
					Attrs []struct {
						Key   string `json:"key"`
						Value string `json:"value"`
					} `json:"attributes"`
				} `json:"events"`
			} `json:"logs"`
			TimeStamp string `json:"timestamp"`
		} `json:"txs"`
	}{}

	data, err := b.JSONEnsuredBytes()
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(data, &out); err != nil {
		return nil, err
	}

	var events []Event
	for _, tx := range out.Txs {
		for _, log := range tx.Logs {
			for _, e := range log.Events {
				var attrs []EventAttribute
				for _, attr := range e.Attrs {
					attrs = append(attrs, EventAttribute{
						Key:   attr.Key,
						Value: attr.Value,
					})
				}

				txTime, err := time.Parse(time.RFC3339, tx.TimeStamp)
				if err != nil {
					return nil, err
				}

				events = append(events, Event{
					Type:       e.Type,
					Attributes: attrs,
					Time:       txTime,
				})
			}
		}
	}

	return events, nil
}
