//go:build js && wasm

// Command pqsign exposes hybrid post-quantum signing to the browser.
//
// WebCrypto has Ed25519 but no ML-DSA, so the post-quantum half has to come
// from somewhere. Compiling the same pkg/pqid the server verifies with means
// there is exactly one implementation of the scheme in the project: a client
// and server that disagreed about signing would be a very expensive bug to
// find, and this makes it impossible by construction.
//
// Build:
//
//	GOOS=js GOARCH=wasm go build -o cmd/arcade/static/pqsign.wasm ./cmd/pqsign
//
// The private key never leaves the browser. It is generated here, exported for
// the page to store, and re-imported on the next visit.
package main

import (
	"crypto/rand"
	"encoding/hex"
	"syscall/js"

	"github.com/drjones/quantum-arcade/pkg/pqid"
)

func main() {
	js.Global().Set("qaPQ", js.ValueOf(map[string]any{
		"generateKey": js.FuncOf(generateKey),
		"sign":        js.FuncOf(sign),
		"publicKey":   js.FuncOf(publicKey),
		"sizes":       js.FuncOf(sizes),
	}))

	// A WASM module's main must not return, or the exported functions are
	// torn down with it.
	select {}
}

// result wraps a value or an error in the shape the page expects, so JavaScript
// never has to distinguish a thrown Go panic from a returned failure.
func result(value any, err error) any {
	if err != nil {
		return map[string]any{"error": err.Error()}
	}
	return map[string]any{"ok": value}
}

// generateKey creates a hybrid keypair and returns both halves hex-encoded.
//
// The private half is handed to the page to persist. That is unavoidable —
// the browser is where signing happens — but it never crosses the network.
func generateKey(this js.Value, args []js.Value) any {
	pub, priv, err := pqid.GenerateKey(rand.Reader)
	if err != nil {
		return result(nil, err)
	}

	edSeed := priv.Ed.Seed()
	pqBytes, err := priv.PQ.MarshalBinary()
	if err != nil {
		return result(nil, err)
	}

	return result(map[string]any{
		"public":  pub.Hex(),
		"ed_seed": hex.EncodeToString(edSeed),
		"pq_key":  hex.EncodeToString(pqBytes),
	}, nil)
}

// sign produces both signatures over a hex-encoded message.
//
//	qaPQ.sign(edSeedHex, pqKeyHex, messageHex) -> {ok: signatureHex}
func sign(this js.Value, args []js.Value) any {
	if len(args) != 3 {
		return result(nil, errArgs("sign expects (edSeed, pqKey, message)"))
	}

	priv, err := restore(args[0].String(), args[1].String())
	if err != nil {
		return result(nil, err)
	}
	msg, err := hex.DecodeString(args[2].String())
	if err != nil {
		return result(nil, errArgs("message is not hex"))
	}

	sig, err := pqid.Sign(priv, msg)
	if err != nil {
		return result(nil, err)
	}
	return result(hex.EncodeToString(sig), nil)
}

// publicKey re-derives the public half from stored private material, so the
// page never has to store the public key separately and cannot store a pair
// that does not match.
func publicKey(this js.Value, args []js.Value) any {
	if len(args) != 2 {
		return result(nil, errArgs("publicKey expects (edSeed, pqKey)"))
	}
	priv, err := restore(args[0].String(), args[1].String())
	if err != nil {
		return result(nil, err)
	}
	pub, err := pqid.PublicFromPrivate(priv)
	if err != nil {
		return result(nil, err)
	}
	return result(pub.Hex(), nil)
}

// sizes lets the page sanity-check what it stored without hardcoding lengths
// that could drift from the Go side.
func sizes(this js.Value, args []js.Value) any {
	return result(map[string]any{
		"public_key": pqid.PublicKeySize,
		"signature":  pqid.SignatureSize,
	}, nil)
}

func restore(edSeedHex, pqKeyHex string) (*pqid.PrivateKey, error) {
	edSeed, err := hex.DecodeString(edSeedHex)
	if err != nil {
		return nil, errArgs("ed seed is not hex")
	}
	pqBytes, err := hex.DecodeString(pqKeyHex)
	if err != nil {
		return nil, errArgs("pq key is not hex")
	}
	return pqid.PrivateFromBytes(edSeed, pqBytes)
}

type argError string

func (e argError) Error() string { return string(e) }

func errArgs(msg string) error { return argError("pqsign: " + msg) }
