package result

import (
	"encoding/json"
	"fmt"

	"github.com/google/uuid"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage/dboper"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/invocations"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

// Invoke represents a code invocation result and is used by several RPC calls
// that invoke functions, scripts and generic bytecode.
type Invoke struct {
	State          string
	GasConsumed    int64
	Script         []byte
	Stack          []stackitem.Item
	FaultException string
	Notifications  []state.NotificationEvent
	Transaction    *transaction.Transaction
	Diagnostics    *InvokeDiag
	Session        uuid.UUID
}

// InvokeDiag is an additional diagnostic data for invocation.
type InvokeDiag struct {
	Changes     []dboper.Operation  `json:"storagechanges"`
	Invocations []*invocations.Tree `json:"invokedcontracts"`
}

type invokeAux struct {
	State          string                    `json:"state"`
	GasConsumed    int64                     `json:"gasconsumed,string"`
	Script         []byte                    `json:"script"`
	Stack          json.RawMessage           `json:"stack"`
	FaultException *string                   `json:"exception"`
	Notifications  []state.NotificationEvent `json:"notifications"`
	Transaction    []byte                    `json:"tx,omitzero"`
	Diagnostics    *InvokeDiag               `json:"diagnostics,omitzero"`
	Session        string                    `json:"session,omitzero"`
}

// iteratorInterfaceName is a string used to mark Iterator inside the InteropInterface.
const iteratorInterfaceName = "IIterator"

type iteratorAux struct {
	Type      string            `json:"type"`
	Interface string            `json:"interface,omitzero"`
	ID        string            `json:"id,omitzero"`
	Value     []json.RawMessage `json:"iterator,omitzero"`
	Truncated bool              `json:"truncated,omitzero"`
}

// Iterator represents a VM iterator identifier. It can be in one of three
// states: 1. If the JSON-RPC server supports session-based iterators, the
// ID field is set, and the Values field may contain a partial expansion of
// the iterator if session expansion is enabled. 2. If the JSON-RPC server
// does not support sessions but allows in-place iteration, the Values field
// will be populated, and Truncated will indicate whether more items exist.
// 3. If the JSON-RPC server neither supports sessions nor in-place iteration,
// all fields will be unset.
type Iterator struct {
	// ID represents iterator ID. It is non-nil iff JSON-RPC server support session mechanism.
	ID *uuid.UUID

	// Values contains deserialized VM iterator values with a truncated flag. It may be non-nil
	// if session expansion is enabled (even when ID is set), or if the JSON-RPC server does not
	// support sessions but allows in-place iterator traversal.
	Values []stackitem.Item
	// Truncated indicates whether the iterator has more elements. It is set only if sessions
	// are disabled.
	Truncated bool
}

// MarshalJSON implements the json.Marshaler.
func (r Iterator) MarshalJSON() ([]byte, error) {
	var iaux iteratorAux
	iaux.Type = stackitem.InteropT.String()
	if r.ID != nil {
		iaux.Interface = iteratorInterfaceName
		iaux.ID = r.ID.String()
	}
	if r.Values != nil {
		value := make([]json.RawMessage, len(r.Values))
		for i := range r.Values {
			var err error
			value[i], err = stackitem.ToJSONWithTypes(r.Values[i])
			if err != nil {
				return nil, err
			}
		}
		iaux.Value = value
	}
	iaux.Truncated = r.Truncated
	return json.Marshal(iaux)
}

// UnmarshalJSON implements the json.Unmarshaler.
func (r *Iterator) UnmarshalJSON(data []byte) error {
	iteratorAux := new(iteratorAux)
	err := json.Unmarshal(data, iteratorAux)
	if err != nil {
		return err
	}
	if len(iteratorAux.Interface) != 0 {
		if iteratorAux.Interface != iteratorInterfaceName {
			return fmt.Errorf("unknown InteropInterface: %s", iteratorAux.Interface)
		}
		var iID uuid.UUID
		iID, err = uuid.Parse(iteratorAux.ID)
		if err != nil {
			return fmt.Errorf("failed to unmarshal iterator ID: %w", err)
		}
		r.ID = &iID
	}
	if iteratorAux.Value != nil {
		r.Values = make([]stackitem.Item, len(iteratorAux.Value))
		for j := range r.Values {
			r.Values[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j])
			if err != nil {
				return fmt.Errorf("failed to unmarshal iterator values: %w", err)
			}
		}
	}
	r.Truncated = iteratorAux.Truncated
	return nil
}

// MarshalJSON implements the json.Marshaler.
func (r Invoke) MarshalJSON() ([]byte, error) {
	var (
		st       json.RawMessage
		err      error
		faultSep string
		arr      = make([]json.RawMessage, len(r.Stack))
	)
	if len(r.FaultException) != 0 {
		faultSep = " / "
	}
	for i := range arr {
		var data []byte

		iter, ok := r.Stack[i].Value().(Iterator)
		if (r.Stack[i].Type() == stackitem.InteropT) && ok {
			data, err = json.Marshal(iter)
		} else {
			data, err = stackitem.ToJSONWithTypes(r.Stack[i])
		}
		if err != nil {
			r.FaultException += fmt.Sprintf("%sjson error: %v", faultSep, err)
			break
		}
		arr[i] = data
	}

	if err == nil {
		st, err = json.Marshal(arr)
		if err != nil {
			return nil, err
		}
	}
	var txbytes []byte
	if r.Transaction != nil {
		txbytes = r.Transaction.Bytes()
	}
	var sessionID string
	if r.Session != (uuid.UUID{}) {
		sessionID = r.Session.String()
	}
	aux := &invokeAux{
		GasConsumed:   r.GasConsumed,
		Script:        r.Script,
		State:         r.State,
		Stack:         st,
		Notifications: r.Notifications,
		Transaction:   txbytes,
		Diagnostics:   r.Diagnostics,
		Session:       sessionID,
	}
	if len(r.FaultException) != 0 {
		aux.FaultException = &r.FaultException
	}
	return json.Marshal(aux)
}

// UnmarshalJSON implements the json.Unmarshaler.
func (r *Invoke) UnmarshalJSON(data []byte) error {
	var err error
	aux := new(invokeAux)
	if err = json.Unmarshal(data, aux); err != nil {
		return err
	}
	if len(aux.Session) != 0 {
		r.Session, err = uuid.Parse(aux.Session)
		if err != nil {
			return fmt.Errorf("failed to parse session ID: %w", err)
		}
	}
	var arr []json.RawMessage
	if err = json.Unmarshal(aux.Stack, &arr); err == nil {
		st := make([]stackitem.Item, len(arr))
		for i := range arr {
			st[i], err = stackitem.FromJSONWithTypes(arr[i])
			if err != nil {
				break
			}
			if st[i].Type() == stackitem.InteropT {
				var iter = Iterator{}
				err = json.Unmarshal(arr[i], &iter)
				if err != nil {
					break
				}
				st[i] = stackitem.NewInterop(iter)
			}
		}
		if err != nil {
			return fmt.Errorf("failed to unmarshal stack: %w", err)
		}
		r.Stack = st
	}
	var tx *transaction.Transaction
	if len(aux.Transaction) != 0 {
		tx, err = transaction.NewTransactionFromBytes(aux.Transaction)
		if err != nil {
			return err
		}
	}
	r.GasConsumed = aux.GasConsumed
	r.Script = aux.Script
	r.State = aux.State
	if aux.FaultException != nil {
		r.FaultException = *aux.FaultException
	}
	r.Notifications = aux.Notifications
	r.Transaction = tx
	r.Diagnostics = aux.Diagnostics
	return nil
}

// AppExecToInvocation converts state.AppExecResult to result.Invoke and can be used
// as a wrapper for actor.Wait. The result of AppExecToInvocation doesn't have all fields
// properly filled, it's limited by State, GasConsumed, Stack, FaultException and Notifications.
// The result of AppExecToInvocation can be passed to unwrap package helpers.
func AppExecToInvocation(aer *state.AppExecResult, err error) (*Invoke, error) {
	if err != nil {
		return nil, err
	}
	return &Invoke{
		State:          aer.VMState.String(),
		GasConsumed:    aer.GasConsumed,
		Stack:          aer.Stack,
		FaultException: aer.FaultException,
		Notifications:  aer.Events,
	}, nil
}
