package beacon_api

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	neturl "net/url"

	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/pkg/errors"
	"github.com/prysmaticlabs/prysm/v5/api/server/structs"
	"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
	"github.com/prysmaticlabs/prysm/v5/network/httputil"
	ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
	"github.com/prysmaticlabs/prysm/v5/runtime/version"
)

type abstractProduceBlockResponseJson struct {
	Version string          `json:"version"`
	Data    json.RawMessage `json:"data"`
}

func (c *beaconApiValidatorClient) beaconBlock(ctx context.Context, slot primitives.Slot, randaoReveal, graffiti []byte) (*ethpb.GenericBeaconBlock, error) {
	queryParams := neturl.Values{}
	queryParams.Add("randao_reveal", hexutil.Encode(randaoReveal))
	if len(graffiti) > 0 {
		queryParams.Add("graffiti", hexutil.Encode(graffiti))
	}

	var ver string
	var blinded bool
	var decoder *json.Decoder

	// Try v3 endpoint first. If it's not supported, then we fall back to older endpoints.
	// We try the blinded block endpoint first. If it fails, we assume that we got a full block and try the full block endpoint.
	queryUrl := buildURL(fmt.Sprintf("/eth/v3/validator/blocks/%d", slot), queryParams)
	produceBlockV3ResponseJson := structs.ProduceBlockV3Response{}
	err := c.jsonRestHandler.Get(ctx, queryUrl, &produceBlockV3ResponseJson)
	errJson := &httputil.DefaultJsonError{}
	if err != nil {
		if !errors.As(err, &errJson) {
			return nil, err
		}
		if errJson.Code != http.StatusNotFound {
			return nil, errJson
		}
		log.Debug("Endpoint /eth/v3/validator/blocks is not supported, falling back to older endpoints for block proposal.")
		fallbackResp, err := c.fallBackToBlinded(ctx, slot, queryParams)
		errJson = &httputil.DefaultJsonError{}
		if err != nil {
			if !errors.As(err, &errJson) {
				return nil, err
			}
			log.Debug("Endpoint /eth/v1/validator/blinded_blocks failed to produce a blinded block, trying /eth/v2/validator/blocks.")
			fallbackResp, err = c.fallBackToFull(ctx, slot, queryParams)
			if err != nil {
				return nil, err
			}
			blinded = false
		} else {
			blinded = true
		}
		ver = fallbackResp.Version
		decoder = json.NewDecoder(bytes.NewReader(fallbackResp.Data))
	} else {
		ver = produceBlockV3ResponseJson.Version
		blinded = produceBlockV3ResponseJson.ExecutionPayloadBlinded
		decoder = json.NewDecoder(bytes.NewReader(produceBlockV3ResponseJson.Data))
	}

	var response *ethpb.GenericBeaconBlock
	switch ver {
	case version.String(version.Phase0):
		jsonPhase0Block := structs.BeaconBlock{}
		if err := decoder.Decode(&jsonPhase0Block); err != nil {
			return nil, errors.Wrap(err, "failed to decode phase0 block response json")
		}
		genericBlock, err := jsonPhase0Block.ToGeneric()
		if err != nil {
			return nil, errors.Wrap(err, "failed to get phase0 block")
		}
		response = genericBlock
	case version.String(version.Altair):
		jsonAltairBlock := structs.BeaconBlockAltair{}
		if err := decoder.Decode(&jsonAltairBlock); err != nil {
			return nil, errors.Wrap(err, "failed to decode altair block response json")
		}
		genericBlock, err := jsonAltairBlock.ToGeneric()
		if err != nil {
			return nil, errors.Wrap(err, "failed to get altair block")
		}
		response = genericBlock
	case version.String(version.Bellatrix):
		if blinded {
			jsonBellatrixBlock := structs.BlindedBeaconBlockBellatrix{}
			if err := decoder.Decode(&jsonBellatrixBlock); err != nil {
				return nil, errors.Wrap(err, "failed to decode blinded bellatrix block response json")
			}
			genericBlock, err := jsonBellatrixBlock.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get blinded bellatrix block")
			}
			response = genericBlock
		} else {
			jsonBellatrixBlock := structs.BeaconBlockBellatrix{}
			if err := decoder.Decode(&jsonBellatrixBlock); err != nil {
				return nil, errors.Wrap(err, "failed to decode bellatrix block response json")
			}
			genericBlock, err := jsonBellatrixBlock.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get bellatrix block")
			}
			response = genericBlock
		}
	case version.String(version.Capella):
		if blinded {
			jsonCapellaBlock := structs.BlindedBeaconBlockCapella{}
			if err := decoder.Decode(&jsonCapellaBlock); err != nil {
				return nil, errors.Wrap(err, "failed to decode blinded capella block response json")
			}
			genericBlock, err := jsonCapellaBlock.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get blinded capella block")
			}
			response = genericBlock
		} else {
			jsonCapellaBlock := structs.BeaconBlockCapella{}
			if err := decoder.Decode(&jsonCapellaBlock); err != nil {
				return nil, errors.Wrap(err, "failed to decode capella block response json")
			}
			genericBlock, err := jsonCapellaBlock.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get capella block")
			}
			response = genericBlock
		}
	case version.String(version.Deneb):
		if blinded {
			jsonDenebBlock := structs.BlindedBeaconBlockDeneb{}
			if err := decoder.Decode(&jsonDenebBlock); err != nil {
				return nil, errors.Wrap(err, "failed to decode blinded deneb block response json")
			}
			genericBlock, err := jsonDenebBlock.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get blinded deneb block")
			}
			response = genericBlock
		} else {
			jsonDenebBlockContents := structs.BeaconBlockContentsDeneb{}
			if err := decoder.Decode(&jsonDenebBlockContents); err != nil {
				return nil, errors.Wrap(err, "failed to decode deneb block response json")
			}
			genericBlock, err := jsonDenebBlockContents.ToGeneric()
			if err != nil {
				return nil, errors.Wrap(err, "failed to get deneb block")
			}
			response = genericBlock
		}
	default:
		return nil, errors.Errorf("unsupported consensus version `%s`", ver)
	}
	return response, nil
}

func (c *beaconApiValidatorClient) fallBackToBlinded(
	ctx context.Context,
	slot primitives.Slot,
	queryParams neturl.Values,
) (*abstractProduceBlockResponseJson, error) {
	resp := &abstractProduceBlockResponseJson{}
	url := buildURL(fmt.Sprintf("/eth/v1/validator/blinded_blocks/%d", slot), queryParams)
	if err := c.jsonRestHandler.Get(ctx, url, resp); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *beaconApiValidatorClient) fallBackToFull(
	ctx context.Context,
	slot primitives.Slot,
	queryParams neturl.Values,
) (*abstractProduceBlockResponseJson, error) {
	resp := &abstractProduceBlockResponseJson{}
	url := buildURL(fmt.Sprintf("/eth/v2/validator/blocks/%d", slot), queryParams)
	if err := c.jsonRestHandler.Get(ctx, url, resp); err != nil {
		return nil, err
	}
	return resp, nil
}
