package parser

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

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

const DefaultTimeout = 10

var (
	ErrCallEndpoint        = errors.New("ERROR CALL ENPOINT")
	ErrNotFound            = errors.New("NOT FOUND")
	ErrInternalError       = errors.New("INTERNAL ERROR")
	ErrUnproceesableEntity = errors.New("UNPROCESSABLE")
	ErrBadRequest          = errors.New("BAD REQUEST")
)

// ParseVaaResponse represent a parse vaa response.
type ParseVaaResponse struct {
	ChainID        uint16      `json:"chainId"`
	EmitterAddress string      `json:"address"`
	Sequence       string      `json:"sequence"`
	AppID          string      `json:"appId"`
	Result         interface{} `json:"result"`
}

// ParserVAAAPIClient parse vaa api client.
type ParserVAAAPIClient struct {
	Client  http.Client
	BaseURL string
	Logger  *zap.Logger
}

// ParseVaaFunc represent a parse vaa function.
type ParseVaaFunc func(vaa *sdk.VAA) (*ParseVaaWithStandarizedPropertiesdResponse, error)

// NewParserVAAAPIClient create new instances of ParserVAAAPIClient.
func NewParserVAAAPIClient(timeout int64, baseURL string, logger *zap.Logger) (ParserVAAAPIClient, error) {
	if timeout == 0 {
		timeout = DefaultTimeout
	}
	if baseURL == "" {
		return ParserVAAAPIClient{}, errors.New("baseURL can not be empty")
	}

	return ParserVAAAPIClient{
		Client: http.Client{
			Timeout: time.Duration(timeout) * time.Second,
		},
		BaseURL: baseURL,
		Logger:  logger,
	}, nil
}

type ParseData struct {
	PayloadID int `bson:"payloadid"`
	Fields    interface{}
}

// ParsePayload invoke the endpoint to parse a VAA from the VAAParserAPI.
func (c ParserVAAAPIClient) ParsePayload(chainID uint16, address, sequence string, vaa []byte) (*ParseVaaResponse, error) {
	endpointUrl := fmt.Sprintf("%s/vaa/parser/%v/%s/%v", c.BaseURL, chainID,
		address, sequence)

	// create request body.
	payload := struct {
		Payload []byte `json:"payload"`
	}{
		Payload: vaa,
	}

	body, err := json.Marshal(payload)
	if err != nil {
		c.Logger.Error("error marshalling payload", zap.Error(err), zap.Uint16("chainID", chainID),
			zap.String("address", address), zap.String("sequence", sequence))
		return nil, err
	}

	response, err := c.Client.Post(endpointUrl, "application/json", bytes.NewBuffer(body))
	if err != nil {
		c.Logger.Error("error call parse vaa endpoint", zap.Error(err), zap.Uint16("chainID", chainID),
			zap.String("address", address), zap.String("sequence", sequence))
		return nil, ErrCallEndpoint
	}
	defer response.Body.Close()

	switch response.StatusCode {
	case http.StatusCreated:
		var parsedVAA ParseVaaResponse
		json.NewDecoder(response.Body).Decode(&parsedVAA)
		return &parsedVAA, nil
	case http.StatusNotFound:
		return nil, ErrNotFound
	case http.StatusBadRequest:
		return nil, ErrBadRequest
	case http.StatusUnprocessableEntity:
		return nil, ErrUnproceesableEntity
	default:
		return nil, ErrInternalError
	}
}

// StandardizedProperties represent a standardized properties.
type StandardizedProperties struct {
	AppIds       []string    `json:"appIds" bson:"appIds"`
	FromChain    sdk.ChainID `json:"fromChain" bson:"fromChain"`
	FromAddress  string      `json:"fromAddress" bson:"fromAddress"`
	ToChain      sdk.ChainID `json:"toChain" bson:"toChain"`
	ToAddress    string      `json:"toAddress" bson:"toAddress"`
	TokenChain   sdk.ChainID `json:"tokenChain" bson:"tokenChain"`
	TokenAddress string      `json:"tokenAddress" bson:"tokenAddress"`
	Amount       string      `json:"amount" bson:"amount"`
	FeeAddress   string      `json:"feeAddress" bson:"feeAddress"`
	FeeChain     sdk.ChainID `json:"feeChain" bson:"feeChain"`
	Fee          string      `json:"fee" bson:"fee"`
}

// ParseVaaWithStandarizedPropertiesdResponse represent a parse vaa response.
type ParseVaaWithStandarizedPropertiesdResponse struct {
	ParsedPayload          interface{}            `json:"parsedPayload"`
	StandardizedProperties StandardizedProperties `json:"standardizedProperties"`
}

// ParseVaaWithStandarizedProperties invoke the endpoint to parse a VAA from the VAAParserAPI.
func (c *ParserVAAAPIClient) ParseVaaWithStandarizedProperties(vaa *sdk.VAA) (*ParseVaaWithStandarizedPropertiesdResponse, error) {
	endpointUrl := fmt.Sprintf("%s/vaas/parse", c.BaseURL)

	vaaBytes, err := vaa.Marshal()
	if err != nil {
		return nil, errors.New("error marshalling vaa")
	}

	body := base64.StdEncoding.EncodeToString(vaaBytes)
	response, err := c.Client.Post(endpointUrl, "text/plain", bytes.NewBuffer([]byte(body)))
	if err != nil {
		c.Logger.Error("error call parse vaa endpoint", zap.Error(err), zap.Uint16("chainID", uint16(vaa.EmitterChain)),
			zap.String("address", vaa.EmitterAddress.String()), zap.Uint64("sequence", vaa.Sequence))
		return nil, ErrCallEndpoint
	}
	defer response.Body.Close()

	switch response.StatusCode {
	case http.StatusCreated:
		var parsedVAA ParseVaaWithStandarizedPropertiesdResponse
		json.NewDecoder(response.Body).Decode(&parsedVAA)
		return &parsedVAA, nil
	case http.StatusNotFound:
		return nil, ErrNotFound
	case http.StatusBadRequest:
		return nil, ErrBadRequest
	case http.StatusUnprocessableEntity:
		return nil, ErrUnproceesableEntity
	default:
		return nil, ErrInternalError
	}
}
