package sui

import (
	"context"
	"crypto/rand"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/url"
	"strings"
	"time"

	"encoding/base64"
	"encoding/json"

	"github.com/gorilla/websocket"

	"github.com/certusone/wormhole/node/pkg/common"
	"github.com/certusone/wormhole/node/pkg/p2p"
	gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
	"github.com/certusone/wormhole/node/pkg/readiness"
	"github.com/certusone/wormhole/node/pkg/supervisor"

	eth_common "github.com/ethereum/go-ethereum/common"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"

	"github.com/wormhole-foundation/wormhole/sdk/vaa"
	"go.uber.org/zap"
)

type (
	// Watcher is responsible for looking over Sui blockchain and reporting new transactions to the wormhole contract
	Watcher struct {
		suiRPC     string
		suiWS      string
		suiAccount string
		suiPackage string

		unsafeDevMode bool

		msgChan  chan *common.MessagePublication
		obsvReqC chan *gossipv1.ObservationRequest

		subId      int64
		subscribed bool
	}

	SuiResult struct {
		Timestamp *int64  `json:"timestamp"`
		TxDigest  *string `json:"txDigest"`
		Event     struct {
			MoveEvent *struct {
				PackageID         *string `json:"packageId"`
				TransactionModule *string `json:"transactionModule"`
				Sender            *string `json:"sender"`
				Type              *string `json:"type"`
				Fields            *struct {
					ConsistencyLevel *uint8  `json:"consistency_level"`
					Nonce            *uint64 `json:"nonce"`
					Payload          *string `json:"payload"`
					Sender           *uint64 `json:"sender"`
					Sequence         *uint64 `json:"sequence"`
				} `json:"fields"`
				Bcs string `json:"bcs"`
			} `json:"moveEvent"`
		} `json:"event"`
	}

	SuiEventError struct {
		Code    int64  `json:"code"`
		Message string `json:"message"`
	}

	SuiEventMsg struct {
		Jsonrpc string         `json:"jsonrpc"`
		Method  *string        `json:"method"`
		ID      *int64         `json:"id"`
		Error   *SuiEventError `json:"error"`
		Params  *struct {
			Subscription int64      `json:"subscription"`
			Result       *SuiResult `json:"result"`
		} `json:"params"`
	}

	SuiTxnQuery struct {
		Jsonrpc string `json:"jsonrpc"`
		Result  struct {
			Data       []SuiResult `json:"data"`
			NextCursor interface{} `json:"nextCursor"`
		} `json:"result"`
		ID int `json:"id"`
	}

	SuiCommitteeInfo struct {
		Jsonrpc string `json:"jsonrpc"`
		Result  struct {
			Epoch         int             `json:"epoch"`
			CommitteeInfo [][]interface{} `json:"committee_info"`
		} `json:"result"`
		ID int `json:"id"`
	}
)

var (
	suiMessagesConfirmed = promauto.NewCounter(
		prometheus.CounterOpts{
			Name: "wormhole_sui_observations_confirmed_total",
			Help: "Total number of verified Sui observations found",
		})
	currentSuiHeight = promauto.NewGauge(
		prometheus.GaugeOpts{
			Name: "wormhole_sui_current_height",
			Help: "Current Sui block height",
		})
)

// NewWatcher creates a new Sui appid watcher
func NewWatcher(
	suiRPC string,
	suiWS string,
	suiAccount string,
	suiPackage string,
	unsafeDevMode bool,
	messageEvents chan *common.MessagePublication,
	obsvReqC chan *gossipv1.ObservationRequest,
) *Watcher {
	return &Watcher{
		suiRPC:        suiRPC,
		suiWS:         suiWS,
		suiAccount:    suiAccount,
		suiPackage:    suiPackage,
		unsafeDevMode: unsafeDevMode,
		msgChan:       messageEvents,
		obsvReqC:      obsvReqC,
		subId:         0,
		subscribed:    false,
	}
}

func (e *Watcher) inspectBody(logger *zap.Logger, body SuiResult) error {
	if (body.Timestamp == nil) || (body.TxDigest == nil) {
		return errors.New("Missing event fields")
	}
	if body.Event.MoveEvent == nil {
		return nil
	}
	moveEvent := *body.Event.MoveEvent
	if (moveEvent.PackageID == nil) || (moveEvent.Sender == nil) {
		return errors.New("Missing event fields")
	}

	if moveEvent.Fields == nil {
		return nil
	}
	fields := *moveEvent.Fields
	if (fields.ConsistencyLevel == nil) || (fields.Nonce == nil) || (fields.Payload == nil) || (fields.Sender == nil) || (fields.Sequence == nil) {
		return nil
	}

	if e.suiAccount != *moveEvent.Sender {
		logger.Info("account missmatch", zap.String("e.suiAccount", e.suiAccount), zap.String("account", *moveEvent.Sender))
		return errors.New("account missmatch")
	}

	if !e.unsafeDevMode && e.suiPackage != *moveEvent.PackageID {
		logger.Info("package missmatch", zap.String("e.suiPackage", e.suiPackage), zap.String("package", *moveEvent.PackageID))
		return errors.New("package missmatch")
	}

	emitter := make([]byte, 8)
	binary.BigEndian.PutUint64(emitter, *fields.Sender)

	var a vaa.Address
	copy(a[24:], emitter)

	id, err := base64.StdEncoding.DecodeString(*body.TxDigest)
	if err != nil {
		return err
	}

	var txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b

	pl, err := base64.StdEncoding.DecodeString(*fields.Payload)
	if err != nil {
		return err

	}

	observation := &common.MessagePublication{
		TxHash: txHash,
		// We do NOT have a useful source of timestamp
		// information.  Every node has its own concept of a
		// timestamp and nothing is persisted into the
		// blockchain to make re-observation possible.  Later
		// we could explore putting the epoch or block height
		// here but even those are currently not available.
		//
		// Timestamp:        time.Unix(int64(timestamp.Uint()/1000), 0),
		Timestamp:        time.Unix(0, 0),
		Nonce:            uint32(*fields.Nonce), // uint32
		Sequence:         *fields.Sequence,
		EmitterChain:     vaa.ChainIDSui,
		EmitterAddress:   a,
		Payload:          pl,
		ConsistencyLevel: uint8(*fields.ConsistencyLevel),
	}

	suiMessagesConfirmed.Inc()

	logger.Info("message observed",
		zap.Stringer("txHash", observation.TxHash),
		zap.Time("timestamp", observation.Timestamp),
		zap.Uint32("nonce", observation.Nonce),
		zap.Uint64("sequence", observation.Sequence),
		zap.Stringer("emitter_chain", observation.EmitterChain),
		zap.Stringer("emitter_address", observation.EmitterAddress),
		zap.Binary("payload", observation.Payload),
		zap.Uint8("consistencyLevel", observation.ConsistencyLevel),
	)

	e.msgChan <- observation

	return nil
}

func (e *Watcher) Run(ctx context.Context) error {
	p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSui, &gossipv1.Heartbeat_Network{
		ContractAddress: e.suiAccount,
	})

	logger := supervisor.Logger(ctx)

	u := url.URL{Scheme: "ws", Host: e.suiWS}

	logger.Info("Sui watcher connecting to WS node ", zap.String("url", u.String()))

	ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		logger.Error(fmt.Sprintf("e.suiWS: %s", err.Error()))
		return err
	}

	var s string

	nBig, _ := rand.Int(rand.Reader, big.NewInt(27))
	e.subId = nBig.Int64()

	if e.unsafeDevMode {
		// There is no way to have a fixed package id on
		// deployment.  This means that in devnet, everytime
		// we publish the contracts we will get a new package
		// id.  The solution is to just subscribe to the whole
		// deployer account instead of to a specific package
		// in that account...
		s = fmt.Sprintf(`{"jsonrpc":"2.0", "id": %d, "method": "sui_subscribeEvent", "params": [{"SenderAddress": "%s"}]}`, e.subId, e.suiAccount)
	} else {
		s = fmt.Sprintf(`{"jsonrpc":"2.0", "id": %d, "method": "sui_subscribeEvent", "params": [{"SenderAddress": "%s", "Package": "%s"}]}`, e.subId, e.suiAccount, e.suiPackage)
	}

	logger.Info("Subscribing using", zap.String("filter", s))

	if err := ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
		logger.Error(fmt.Sprintf("write: %s", err.Error()))
		return err
	}

	timer := time.NewTicker(time.Second * 1)
	defer timer.Stop()

	supervisor.Signal(ctx, supervisor.SignalHealthy)

	errC := make(chan error)
	defer close(errC)
	pumpData := make(chan []byte)
	defer close(pumpData)

	go func() {
		for {
			if _, msg, err := ws.ReadMessage(); err != nil {
				logger.Error(fmt.Sprintf("ReadMessage: '%s'", err.Error()))
				if strings.HasSuffix(err.Error(), "EOF") {
					errC <- err
					return
				}
			} else {
				pumpData <- msg
			}
		}
	}()

	for {
		select {
		case err := <-errC:
			_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
			logger.Error("Pump died")
			return err
		case <-ctx.Done():
			_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
			return ctx.Err()
		case r := <-e.obsvReqC:
			if vaa.ChainID(r.ChainId) != vaa.ChainIDSui {
				panic("invalid chain ID")
			}

			id := base64.StdEncoding.EncodeToString(r.TxHash)

			logger.Info("obsv request", zap.String("TxHash", string(id)))

			buf := fmt.Sprintf(`{"jsonrpc":"2.0", "id": 1, "method": "sui_getEvents", "params": [{"Transaction": "%s"}, null, 10, true]}`, id)

			resp, err := http.Post(e.suiRPC, "application/json", strings.NewReader(buf))
			if err != nil {
				logger.Error("getEvents API failed", zap.String("suiRPC", e.suiRPC), zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				continue
			}

			body, err := io.ReadAll(resp.Body)
			if err != nil {
				logger.Error("unexpected truncated body when calling getEvents", zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				continue

			}
			resp.Body.Close()

			logger.Info("receive", zap.String("body", string(body)))

			var res SuiTxnQuery
			err = json.Unmarshal(body, &res)
			if err != nil {
				logger.Error("failed to unmarshal event message", zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				continue

			}

			for _, chunk := range res.Result.Data {
				err := e.inspectBody(logger, chunk)
				if err != nil {
					logger.Error("unspected error while parsing chunk data in event", zap.Error(err))
				}
			}
		case msg := <-pumpData:
			logger.Info("receive", zap.String("body", string(msg)))

			var res SuiEventMsg
			err = json.Unmarshal(msg, &res)
			if err != nil {
				logger.Error("Failed to unmarshal SuiEventMsg", zap.String("body", string(msg)), zap.Error(err))
				continue
			}
			if res.Error != nil {
				_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
				return errors.New((*res.Error).Message)
			}
			if res.ID != nil {
				if *res.ID == e.subId {
					logger.Info("Subscribed set to true")
					e.subscribed = true
				}
				continue
			}

			if res.Params != nil && (*res.Params).Result != nil {
				err := e.inspectBody(logger, *(*res.Params).Result)
				if err != nil {
					logger.Error(fmt.Sprintf("inspectBody: %s", err.Error()))
				}
				continue
			}

		case <-timer.C:
			resp, err := http.Post(e.suiRPC, "application/json", strings.NewReader(`{"jsonrpc":"2.0", "id": 1, "method": "sui_getCommitteeInfo", "params": []}`))
			if err != nil {
				logger.Error("sui_getCommitteeInfo failed", zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				break

			}
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				logger.Error("sui_getCommitteeInfo failed", zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				break
			}
			resp.Body.Close()

			var res SuiCommitteeInfo
			err = json.Unmarshal(body, &res)
			if err != nil {
				logger.Error("unmarshal failed into SuiCommitteeInfo", zap.String("body", string(body)), zap.Error(err))
				p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSui, 1)
				continue

			}

			// Epoch is currently not ticking in 0.16.0.  They also
			// might release another API that gives a
			// proper block height as we traditionally
			// understand it...
			currentSuiHeight.Set(float64(res.Result.Epoch))
			p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSui, &gossipv1.Heartbeat_Network{
				Height:          int64(res.Result.Epoch),
				ContractAddress: e.suiAccount,
			})

			if e.subscribed {
				readiness.SetReady(common.ReadinessSuiSyncing)
			}
		}
	}
}
