package cannon

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"math/big"

	"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
	preimage "github.com/ethereum-optimism/optimism/op-preimage"
	"github.com/ethereum-optimism/optimism/op-service/eth"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/crypto/kzg4844"
	"github.com/ethereum/go-ethereum/params"
)

const (
	fieldElemKeyLength = 80
	commitmentLength   = 48
	lengthPrefixSize   = 8
)

var (
	ErrInvalidScalarValue     = errors.New("invalid scalar value")
	ErrInvalidBlobKeyPreimage = errors.New("invalid blob key preimage")
)

type preimageSource func(key common.Hash) ([]byte, error)

type preimageLoader struct {
	getPreimage preimageSource
}

func newPreimageLoader(getPreimage preimageSource) *preimageLoader {
	return &preimageLoader{
		getPreimage: getPreimage,
	}
}

func (l *preimageLoader) LoadPreimage(proof *proofData) (*types.PreimageOracleData, error) {
	if len(proof.OracleKey) == 0 {
		return nil, nil
	}
	switch preimage.KeyType(proof.OracleKey[0]) {
	case preimage.BlobKeyType:
		return l.loadBlobPreimage(proof)
	case preimage.PrecompileKeyType:
		return l.loadPrecompilePreimage(proof)
	default:
		return types.NewPreimageOracleData(proof.OracleKey, proof.OracleValue, proof.OracleOffset), nil
	}
}

func (l *preimageLoader) loadBlobPreimage(proof *proofData) (*types.PreimageOracleData, error) {
	// The key for a blob field element is a keccak hash of commitment++fieldElementIndex.
	// First retrieve the preimage of the key as a keccak hash so we have the commitment and required field element
	inputsKey := preimage.Keccak256Key(proof.OracleKey).PreimageKey()
	inputs, err := l.getPreimage(inputsKey)
	if err != nil {
		return nil, fmt.Errorf("failed to get key preimage: %w", err)
	}
	if len(inputs) != fieldElemKeyLength {
		return nil, fmt.Errorf("%w, expected length %v but was %v", ErrInvalidBlobKeyPreimage, fieldElemKeyLength, len(inputs))
	}
	commitment := inputs[:commitmentLength]
	requiredFieldElement := binary.BigEndian.Uint64(inputs[72:])

	// Now, reconstruct the full blob by loading the 4096 field elements.
	blob := eth.Blob{}
	fieldElemKey := make([]byte, fieldElemKeyLength)
	copy(fieldElemKey[:commitmentLength], commitment)
	for i := 0; i < params.BlobTxFieldElementsPerBlob; i++ {
		binary.BigEndian.PutUint64(fieldElemKey[72:], uint64(i))
		key := preimage.BlobKey(crypto.Keccak256(fieldElemKey)).PreimageKey()
		fieldElement, err := l.getPreimage(key)
		if err != nil {
			return nil, fmt.Errorf("failed to load field element %v with key %v:  %w", i, common.Hash(key), err)
		}
		copy(blob[i<<5:(i+1)<<5], fieldElement[:])
	}

	// Sanity check the blob data matches the commitment
	blobCommitment, err := blob.ComputeKZGCommitment()
	if err != nil || !bytes.Equal(blobCommitment[:], commitment[:]) {
		return nil, fmt.Errorf("invalid blob commitment: %w", err)
	}
	// Compute the KZG proof for the required field element
	var point kzg4844.Point
	new(big.Int).SetUint64(requiredFieldElement).FillBytes(point[:])
	kzgProof, claim, err := kzg4844.ComputeProof(kzg4844.Blob(blob), point)
	if err != nil {
		return nil, fmt.Errorf("failed to compute kzg proof: %w", err)
	}
	err = kzg4844.VerifyProof(kzg4844.Commitment(commitment), point, claim, kzgProof)
	if err != nil {
		return nil, fmt.Errorf("failed to verify proof: %w", err)
	}

	claimWithLength := lengthPrefixed(claim[:])
	return types.NewPreimageOracleBlobData(proof.OracleKey, claimWithLength, proof.OracleOffset, requiredFieldElement, commitment, kzgProof[:]), nil
}

func (l *preimageLoader) loadPrecompilePreimage(proof *proofData) (*types.PreimageOracleData, error) {
	inputKey := preimage.Keccak256Key(proof.OracleKey).PreimageKey()
	input, err := l.getPreimage(inputKey)
	if err != nil {
		return nil, fmt.Errorf("failed to get key preimage: %w", err)
	}
	inputWithLength := lengthPrefixed(input)
	return types.NewPreimageOracleData(proof.OracleKey, inputWithLength, proof.OracleOffset), nil
}

func lengthPrefixed(data []byte) []byte {
	dataWithLength := make([]byte, len(data)+lengthPrefixSize)
	binary.BigEndian.PutUint64(dataWithLength[:lengthPrefixSize], uint64(len(data)))
	copy(dataWithLength[lengthPrefixSize:], data)
	return dataWithLength
}
