package result

import (
	"encoding/json"
	"errors"
	"fmt"

	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
)

// ApplicationLog represent the results of the script executions for a block or a transaction.
type ApplicationLog struct {
	Container     util.Uint256
	IsTransaction bool
	Executions    []state.Execution
}

// applicationLogAux is an auxiliary struct for ApplicationLog JSON marshalling.
type applicationLogAux struct {
	TxHash     *util.Uint256     `json:"txid,omitzero"`
	BlockHash  *util.Uint256     `json:"blockhash,omitzero"`
	Executions []json.RawMessage `json:"executions"`
}

// MarshalJSON implements the json.Marshaler interface.
func (l ApplicationLog) MarshalJSON() ([]byte, error) {
	result := &applicationLogAux{
		Executions: make([]json.RawMessage, len(l.Executions)),
	}
	if l.IsTransaction {
		result.TxHash = &l.Container
	} else {
		result.BlockHash = &l.Container
	}
	var err error
	for i := range result.Executions {
		result.Executions[i], err = json.Marshal(l.Executions[i])
		if err != nil {
			return nil, fmt.Errorf("failed to marshal execution #%d: %w", i, err)
		}
	}
	return json.Marshal(result)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (l *ApplicationLog) UnmarshalJSON(data []byte) error {
	aux := new(applicationLogAux)
	if err := json.Unmarshal(data, aux); err != nil {
		return err
	}
	if aux.TxHash != nil {
		l.Container = *aux.TxHash
	} else if aux.BlockHash != nil {
		l.Container = *aux.BlockHash
	} else {
		return errors.New("no block or transaction hash")
	}
	l.Executions = make([]state.Execution, len(aux.Executions))
	for i := range l.Executions {
		err := json.Unmarshal(aux.Executions[i], &l.Executions[i])
		if err != nil {
			return fmt.Errorf("failed to unmarshal execution #%d: %w", i, err)
		}
	}

	return nil
}

// NewApplicationLog creates an ApplicationLog from a set of several application execution results
// including only the results with the specified trigger.
func NewApplicationLog(hash util.Uint256, aers []state.AppExecResult, trig trigger.Type) ApplicationLog {
	result := ApplicationLog{
		Container:     hash,
		IsTransaction: aers[0].Trigger == trigger.Application,
	}
	for _, aer := range aers {
		if aer.Trigger&trig != 0 {
			result.Executions = append(result.Executions, aer.Execution)
		}
	}
	return result
}
