package main

import (
	crand "crypto/rand"
	"fmt"

	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves"
	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/sharing"
	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/verenc/elgamal"
)

func main() {
	// Setup
	curve := curves.ED25519()
	serverPk, serverSk, _ := elgamal.NewKeys(curve)
	aliceEk, _, _ := elgamal.NewKeys(curve)
	bobEk, _, _ := elgamal.NewKeys(curve)

	signKey := curve.Scalar.Random(crand.Reader)
	verKey := curve.ScalarBaseMult(signKey)

	shamir, _ := sharing.NewShamir(2, 2, curve)
	shares, _ := shamir.Split(signKey, crand.Reader)

	// Create a proof s.t. Alice and Bob submit
	// proofs of correctness for their ciphertexts
	// Alice and Bob can decrypt their respective shares
	// But the server cannot.
	// The server checks whether the ciphertexts
	// are valid encrypted shares
	aliceDomain := []byte("alice_share")
	bobDomain := []byte("bob_share")
	aliceStuff := computeProof(curve, shares[0].Value, aliceDomain, aliceEk, serverPk)
	bobStuff := computeProof(curve, shares[1].Value, bobDomain, bobEk, serverPk)

	if !verifyProof(curve, aliceStuff, aliceDomain, aliceEk, serverPk) {
		panic("bad alice proof")
	}
	if !verifyProof(curve, bobStuff, bobDomain, bobEk, serverPk) {
		panic("bad bob proof")
	}
	aliceShare, _ := aliceStuff.serverctxt.ToHomomorphicCipherText().Decrypt(serverSk)
	bobShare, _ := bobStuff.serverctxt.ToHomomorphicCipherText().Decrypt(serverSk)

	avk := shamirCombinePoint(curve, []curves.Point{aliceShare, bobShare})
	if avk.Equal(verKey) {
		fmt.Println("Success")
	} else {
		fmt.Println("Failure")
	}
}

func shamirCombinePoint(curve *curves.Curve, points []curves.Point) curves.Point {
	result := curve.Point.Identity()

	for i, p := range points {
		basis := curve.Scalar.One()
		for j := range points {
			if i == j {
				continue
			}

			// x_m - x_j
			denom := curve.Scalar.New(j + 1).Sub(curve.Scalar.New(i + 1))
			if denom.IsZero() {
				return nil
			}
			// x_m / x_m - x_j
			basis = basis.Mul(curve.Scalar.New(j + 1).Div(denom))
		}

		result = result.Add(p.Mul(basis))
	}

	return result
}

func verifyProof(
	curve *curves.Curve,
	proof *commitTwinProof,
	domain []byte,
	encKey *elgamal.EncryptionKey,
	serverPk *elgamal.EncryptionKey,
) bool {
	genBytes := append(domain, encKey.Value.ToAffineUncompressed()...)
	genBytes = append(genBytes, proof.encctxt.Nonce...)
	h := curve.NewGeneratorPoint().Hash(genBytes)

	w1 := curve.ScalarBaseMult(proof.d).Add(serverPk.Value.Mul(proof.d1)).Add(proof.serverctxt.C2.Mul(proof.c))
	w2 := h.Mul(proof.d).Add(encKey.Value.Mul(proof.d2)).Add(proof.encctxt.C2.Mul(proof.c))
	c := curve.NewScalar().Hash(append(w1.ToAffineCompressed(), w2.ToAffineCompressed()...))
	return c.Cmp(proof.c) == 0
}

type commitTwinProof struct {
	c, d, d1, d2 curves.Scalar
	serverctxt   *elgamal.CipherText
	encctxt      *elgamal.CipherText
}

func computeProof(
	curve *curves.Curve,
	share, domain []byte,
	encKey *elgamal.EncryptionKey,
	servKey *elgamal.EncryptionKey,
) *commitTwinProof {
	r1 := curve.Scalar.Random(crand.Reader)
	r2 := curve.Scalar.Random(crand.Reader)
	encctxt, _, _ := encKey.VerifiableEncrypt(share, &elgamal.EncryptParams{
		Domain:          domain,
		Blinding:        r2,
		MessageIsHashed: true,
		ProofNonce:      domain,
	})
	serverCtxt, _, _ := servKey.VerifiableEncrypt(share, &elgamal.EncryptParams{
		Blinding:        r1,
		MessageIsHashed: true,
		ProofNonce:      domain,
	})

	w := curve.Scalar.Random(crand.Reader)
	n1 := curve.Scalar.Random(crand.Reader)
	n2 := curve.Scalar.Random(crand.Reader)

	genBytes := append(domain, encKey.Value.ToAffineUncompressed()...)
	genBytes = append(genBytes, encctxt.Nonce...)
	h := curve.NewGeneratorPoint().Hash(genBytes)

	w1 := curve.ScalarBaseMult(w).Add(servKey.Value.Mul(n1))
	w2 := h.Mul(w).Add(encKey.Value.Mul(n2))
	c := curve.NewScalar().Hash(append(w1.ToAffineCompressed(), w2.ToAffineCompressed()...))
	s, err := curve.NewScalar().SetBytes(share)
	if err != nil {
		panic(err)
	}
	// d = w - c s
	d := w.Sub(c.Mul(s))
	// d1 = n1 - c r1
	d1 := n1.Sub(c.Mul(r1))
	// d2 = n2 - c r2
	d2 := n2.Sub(c.Mul(r2))

	return &commitTwinProof{
		c:          c,
		d:          d,
		d1:         d1,
		d2:         d2,
		encctxt:    encctxt,
		serverctxt: serverCtxt,
	}
}
