// Command wasm is the TinyGo WebAssembly entry point for the Galaxy UI
// client. It exposes a small "compute boundary" API on
// `globalThis.galaxyCore` so the TypeScript-side `WasmCore` adapter can
// call into the Go canonical-bytes serializer and signature verifier
// without duplicating the contract in JavaScript.
//
// Public surface (all functions live under `globalThis.galaxyCore`):
//
//   - signRequest(fields)                        -> Uint8Array
//     Returns the canonical bytes for a v1 request envelope. The actual
//     Ed25519 signing happens outside WASM (Phase 6 introduces WebCrypto
//     with non-exportable keys).
//   - verifyResponse(publicKey, signature, fields) -> boolean
//   - verifyEvent(publicKey, signature, fields)    -> boolean
//   - verifyPayloadHash(payloadBytes, payloadHash) -> boolean
//
// Phase 18 adds the ship-math bridge over `pkg/calc/ship.go`. Each
// function is a thin wrapper around the same-named upstream calc
// function (zero math here, the bridge only marshals JS objects):
//
//   - driveEffective(fields)    -> number
//   - emptyMass(fields)         -> number | null   (null when invalid)
//   - weaponsBlockMass(fields)  -> number | null   (null when invalid)
//   - fullMass(fields)          -> number
//   - speed(fields)             -> number
//   - cargoCapacity(fields)     -> number
//   - carryingMass(fields)      -> number
//   - blockUpgradeCost(fields)  -> number          (Phase 20: modernize cost preview)
//
// Phase 30 adds the calculator bridge over the combat, planet-build, and
// inverse goal-seek math in `pkg/calc` (combat / build helpers, plus the
// single-target solvers):
//
//   - effectiveAttack(fields)    -> number
//   - effectiveDefence(fields)   -> number
//   - bombingPower(fields)       -> number
//   - shipBuildCost(fields)      -> number
//   - produceShipsInTurn(fields) -> { ships, materialLeft, productionUsed, progress }
//   - weaponsForAttack(fields)   -> number | null   (null when infeasible)
//   - driveForSpeed(fields)      -> number | null
//   - shieldsForDefence(fields)  -> number | null
//   - cargoForEmptyMass(fields)  -> number | null
//   - loadForFullMass(fields)    -> number | null
//
// Field objects are plain JS objects with camelCase keys matching the
// TypeScript `Core` interface, and bytes fields are Uint8Array.
// Timestamps are JS Number (Unix milliseconds fit in 53 bits well past
// year 2200).
//
// All functions return either a Uint8Array, a number, a boolean, null,
// or fail closed. They never throw — callers may inspect the result
// or rely on the canon-byte length to detect malformed input.

//go:build js && wasm

package main

import (
	"syscall/js"

	"galaxy/core/calc"
	"galaxy/core/canon"
)

func main() {
	js.Global().Set("galaxyCore", js.ValueOf(map[string]any{
		"signRequest":        js.FuncOf(signRequest),
		"verifyResponse":     js.FuncOf(verifyResponse),
		"verifyEvent":        js.FuncOf(verifyEvent),
		"verifyPayloadHash":  js.FuncOf(verifyPayloadHash),
		"driveEffective":     js.FuncOf(driveEffective),
		"emptyMass":          js.FuncOf(emptyMass),
		"weaponsBlockMass":   js.FuncOf(weaponsBlockMass),
		"fullMass":           js.FuncOf(fullMass),
		"speed":              js.FuncOf(speed),
		"cargoCapacity":      js.FuncOf(cargoCapacity),
		"carryingMass":       js.FuncOf(carryingMass),
		"blockUpgradeCost":   js.FuncOf(blockUpgradeCost),
		"effectiveAttack":    js.FuncOf(effectiveAttack),
		"effectiveDefence":   js.FuncOf(effectiveDefence),
		"bombingPower":       js.FuncOf(bombingPower),
		"shipBuildCost":      js.FuncOf(shipBuildCost),
		"produceShipsInTurn": js.FuncOf(produceShipsInTurn),
		"weaponsForAttack":   js.FuncOf(weaponsForAttack),
		"driveForSpeed":      js.FuncOf(driveForSpeed),
		"shieldsForDefence":  js.FuncOf(shieldsForDefence),
		"cargoForEmptyMass":  js.FuncOf(cargoForEmptyMass),
		"loadForFullMass":    js.FuncOf(loadForFullMass),
		"ceil3":              js.FuncOf(ceil3),
	}))

	// Block forever so the Go runtime stays alive while JS keeps calling
	// the registered functions.
	select {}
}

func signRequest(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	fields := canon.RequestSigningFields{
		ProtocolVersion: args[0].Get("protocolVersion").String(),
		DeviceSessionID: args[0].Get("deviceSessionId").String(),
		MessageType:     args[0].Get("messageType").String(),
		TimestampMS:     int64(args[0].Get("timestampMs").Float()),
		RequestID:       args[0].Get("requestId").String(),
		PayloadHash:     copyBytesFromJS(args[0].Get("payloadHash")),
	}
	return copyBytesToJS(canon.BuildRequestSigningInput(fields))
}

func verifyResponse(_ js.Value, args []js.Value) any {
	if len(args) != 3 {
		return js.ValueOf(false)
	}
	fields := canon.ResponseSigningFields{
		ProtocolVersion: args[2].Get("protocolVersion").String(),
		RequestID:       args[2].Get("requestId").String(),
		TimestampMS:     int64(args[2].Get("timestampMs").Float()),
		ResultCode:      args[2].Get("resultCode").String(),
		PayloadHash:     copyBytesFromJS(args[2].Get("payloadHash")),
	}
	publicKey := copyBytesFromJS(args[0])
	signature := copyBytesFromJS(args[1])
	if err := canon.VerifyResponseSignature(publicKey, signature, fields); err != nil {
		return js.ValueOf(false)
	}
	return js.ValueOf(true)
}

func verifyEvent(_ js.Value, args []js.Value) any {
	if len(args) != 3 {
		return js.ValueOf(false)
	}
	fields := canon.EventSigningFields{
		EventType:   args[2].Get("eventType").String(),
		EventID:     args[2].Get("eventId").String(),
		TimestampMS: int64(args[2].Get("timestampMs").Float()),
		RequestID:   args[2].Get("requestId").String(),
		TraceID:     args[2].Get("traceId").String(),
		PayloadHash: copyBytesFromJS(args[2].Get("payloadHash")),
	}
	publicKey := copyBytesFromJS(args[0])
	signature := copyBytesFromJS(args[1])
	if err := canon.VerifyEventSignature(publicKey, signature, fields); err != nil {
		return js.ValueOf(false)
	}
	return js.ValueOf(true)
}

func verifyPayloadHash(_ js.Value, args []js.Value) any {
	if len(args) != 2 {
		return js.ValueOf(false)
	}
	payloadBytes := copyBytesFromJS(args[0])
	payloadHash := copyBytesFromJS(args[1])
	if err := canon.VerifyPayloadHash(payloadBytes, payloadHash); err != nil {
		return js.ValueOf(false)
	}
	return js.ValueOf(true)
}

// driveEffective bridges `calc.DriveEffective`. Input
// `{ drive, driveTech }`, output a JS number.
func driveEffective(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	drive := args[0].Get("drive").Float()
	driveTech := args[0].Get("driveTech").Float()
	return js.ValueOf(calc.DriveEffective(drive, driveTech))
}

// emptyMass bridges `calc.EmptyMass`. Input
// `{ drive, weapons, armament, shields, cargo }`, output a JS number
// or null when the upstream validator rejects the weapons/armament
// pairing.
func emptyMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	drive := args[0].Get("drive").Float()
	weapons := args[0].Get("weapons").Float()
	armament := uint(args[0].Get("armament").Int())
	shields := args[0].Get("shields").Float()
	cargo := args[0].Get("cargo").Float()
	mass, ok := calc.EmptyMass(drive, weapons, armament, shields, cargo)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(mass)
}

// weaponsBlockMass bridges `calc.WeaponsBlockMass`. Input
// `{ weapons, armament }`, output a JS number or null on the same
// invalid pairing as emptyMass.
func weaponsBlockMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	weapons := args[0].Get("weapons").Float()
	armament := uint(args[0].Get("armament").Int())
	mass, ok := calc.WeaponsBlockMass(weapons, armament)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(mass)
}

// fullMass bridges `calc.FullMass`. Input
// `{ emptyMass, carryingMass }`, output a JS number.
func fullMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	em := args[0].Get("emptyMass").Float()
	cm := args[0].Get("carryingMass").Float()
	return js.ValueOf(calc.FullMass(em, cm))
}

// speed bridges `calc.Speed`. Input `{ driveEffective, fullMass }`,
// output a JS number (zero when fullMass is non-positive).
func speed(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	de := args[0].Get("driveEffective").Float()
	fm := args[0].Get("fullMass").Float()
	return js.ValueOf(calc.Speed(de, fm))
}

// cargoCapacity bridges `calc.CargoCapacity`. Input
// `{ cargo, cargoTech }`, output a JS number (cargo units of hold).
func cargoCapacity(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	cargo := args[0].Get("cargo").Float()
	cargoTech := args[0].Get("cargoTech").Float()
	return js.ValueOf(calc.CargoCapacity(cargo, cargoTech))
}

// carryingMass bridges `calc.CarryingMass`. Input
// `{ load, cargoTech }`, output a JS number (mass of `load` cargo
// units at the player's cargo tech).
func carryingMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	load := args[0].Get("load").Float()
	cargoTech := args[0].Get("cargoTech").Float()
	return js.ValueOf(calc.CarryingMass(load, cargoTech))
}

// blockUpgradeCost bridges `calc.BlockUpgradeCost`. Input
// `{ blockMass, currentTech, targetTech }`, output a JS number
// (production cost of moving one block from currentTech to
// targetTech; zero when blockMass is zero or targetTech is not
// above currentTech).
func blockUpgradeCost(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	blockMass := args[0].Get("blockMass").Float()
	currentTech := args[0].Get("currentTech").Float()
	targetTech := args[0].Get("targetTech").Float()
	return js.ValueOf(calc.BlockUpgradeCost(blockMass, currentTech, targetTech))
}

// effectiveAttack bridges `calc.EffectiveAttack`. Input
// `{ weapons, weaponsTech }`, output a JS number.
func effectiveAttack(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	weapons := args[0].Get("weapons").Float()
	weaponsTech := args[0].Get("weaponsTech").Float()
	return js.ValueOf(calc.EffectiveAttack(weapons, weaponsTech))
}

// effectiveDefence bridges `calc.EffectiveDefence`. Input
// `{ shields, shieldsTech, fullMass }`, output a JS number (zero when
// fullMass is non-positive).
func effectiveDefence(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	shields := args[0].Get("shields").Float()
	shieldsTech := args[0].Get("shieldsTech").Float()
	fullMass := args[0].Get("fullMass").Float()
	return js.ValueOf(calc.EffectiveDefence(shields, shieldsTech, fullMass))
}

// bombingPower bridges `calc.BombingPower`. Input
// `{ weapons, weaponsTech, armament, number }`, output a JS number.
func bombingPower(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	weapons := args[0].Get("weapons").Float()
	weaponsTech := args[0].Get("weaponsTech").Float()
	armament := args[0].Get("armament").Float()
	number := args[0].Get("number").Float()
	return js.ValueOf(calc.BombingPower(weapons, weaponsTech, armament, number))
}

// shipBuildCost bridges `calc.ShipBuildCost`. Input
// `{ shipMass, material, resources }`, output a JS number.
func shipBuildCost(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	shipMass := args[0].Get("shipMass").Float()
	material := args[0].Get("material").Float()
	resources := args[0].Get("resources").Float()
	return js.ValueOf(calc.ShipBuildCost(shipMass, material, resources))
}

// produceShipsInTurn bridges `calc.ProduceShipsInTurn`. Input
// `{ productionAvailable, material, resources, shipMass }`, output a JS
// object `{ ships, materialLeft, productionUsed, progress }`.
func produceShipsInTurn(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	productionAvailable := args[0].Get("productionAvailable").Float()
	material := args[0].Get("material").Float()
	resources := args[0].Get("resources").Float()
	shipMass := args[0].Get("shipMass").Float()
	ships, materialLeft, productionUsed, progress := calc.ProduceShipsInTurn(
		productionAvailable, material, resources, shipMass,
	)
	return js.ValueOf(map[string]any{
		"ships":          float64(ships),
		"materialLeft":   materialLeft,
		"productionUsed": productionUsed,
		"progress":       progress,
	})
}

// weaponsForAttack bridges `calc.WeaponsForAttack`. Input
// `{ targetAttack, weaponsTech }`, output a JS number or null when the
// request is infeasible.
func weaponsForAttack(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	targetAttack := args[0].Get("targetAttack").Float()
	weaponsTech := args[0].Get("weaponsTech").Float()
	v, ok := calc.WeaponsForAttack(targetAttack, weaponsTech)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(v)
}

// driveForSpeed bridges `calc.DriveForSpeed`. Input
// `{ targetSpeed, driveTech, restMass }`, output a JS number or null when
// the target is unreachable.
func driveForSpeed(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	targetSpeed := args[0].Get("targetSpeed").Float()
	driveTech := args[0].Get("driveTech").Float()
	restMass := args[0].Get("restMass").Float()
	v, ok := calc.DriveForSpeed(targetSpeed, driveTech, restMass)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(v)
}

// shieldsForDefence bridges `calc.ShieldsForDefence`. Input
// `{ targetDefence, shieldsTech, restMass }`, output a JS number or null
// when the request is infeasible.
func shieldsForDefence(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	targetDefence := args[0].Get("targetDefence").Float()
	shieldsTech := args[0].Get("shieldsTech").Float()
	restMass := args[0].Get("restMass").Float()
	v, ok := calc.ShieldsForDefence(targetDefence, shieldsTech, restMass)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(v)
}

// cargoForEmptyMass bridges `calc.CargoForEmptyMass`. Input
// `{ targetEmptyMass, restMass }`, output a JS number or null when the
// target is below the fixed block mass.
func cargoForEmptyMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	targetEmptyMass := args[0].Get("targetEmptyMass").Float()
	restMass := args[0].Get("restMass").Float()
	v, ok := calc.CargoForEmptyMass(targetEmptyMass, restMass)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(v)
}

// loadForFullMass bridges `calc.LoadForFullMass`. Input
// `{ targetFullMass, emptyMass, cargoTech }`, output a JS number or null
// when the target is below the empty mass.
func loadForFullMass(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	targetFullMass := args[0].Get("targetFullMass").Float()
	emptyMass := args[0].Get("emptyMass").Float()
	cargoTech := args[0].Get("cargoTech").Float()
	v, ok := calc.LoadForFullMass(targetFullMass, emptyMass, cargoTech)
	if !ok {
		return js.Null()
	}
	return js.ValueOf(v)
}

// ceil3 bridges `calc.Ceil3`. Input `{ value }`, output a JS number
// rounded up to three decimal places.
func ceil3(_ js.Value, args []js.Value) any {
	if len(args) != 1 {
		return js.Null()
	}
	return js.ValueOf(calc.Ceil3(args[0].Get("value").Float()))
}

// copyBytesFromJS materialises a JS Uint8Array (or any indexable
// byte-shaped value) into a Go byte slice. We avoid `js.CopyBytesToGo`
// because TinyGo's implementation panics on values it does not
// recognise as Uint8Array — a check that misfires for Uint8Arrays
// constructed from Node's Buffer in Vitest's jsdom environment. The
// per-element copy is slower but always correct, and the canonical
// envelope payloads are small enough (≤ a few hundred bytes) that the
// difference is negligible.
func copyBytesFromJS(value js.Value) []byte {
	if value.IsUndefined() || value.IsNull() {
		return nil
	}
	length := value.Length()
	if length == 0 {
		return nil
	}
	dst := make([]byte, length)
	for i := 0; i < length; i++ {
		dst[i] = byte(value.Index(i).Int())
	}
	return dst
}

// copyBytesToJS allocates a JS Uint8Array of the same length as src and
// copies src into it. The result is safe to hand back across the
// JS/Go boundary as the canonical-bytes return value.
func copyBytesToJS(src []byte) js.Value {
	dst := js.Global().Get("Uint8Array").New(len(src))
	js.CopyBytesToJS(dst, src)
	return dst
}
