#!/usr/bin/env python3
"""Write the ledgerctl install manifest for a built app.

cargo-ledger does not emit `app_<device>.json` on macOS, so generate it from
the built ELF plus `[package.metadata.ledger]` in Cargo.toml.

Everything that can be read from the ELF is read from the ELF: app name,
version, api level and target id all come from its `ledger.*` sections, so the
manifest cannot drift from the binary it installs.

`dataSize` is `_envram_data - _nvram_data`: the NVRAM the app is granted at
install time. Getting it too small installs an app that will not launch.

Usage:  ./scripts/gen-manifest [stax|flex|nanox|nanosplus]   (default: stax)
"""
import json
import pathlib
import subprocess
import sys
import tomllib

LLVM_NM = "/opt/homebrew/opt/llvm/bin/llvm-nm"
LLVM_OBJCOPY = "/opt/homebrew/opt/llvm/bin/llvm-objcopy"

# cargo-ledger's Cargo.toml key -> the [package.metadata.ledger.<key>] table
DEVICE_TABLE = {
    "stax": "stax", "flex": "flex",
    "nanox": "nanox", "nanosplus": "nanosplus", "apex_p": "apex_p",
}


def elf_section(elf, name):
    """Read a `ledger.*` metadata string section out of the ELF."""
    out = subprocess.run(
        [LLVM_OBJCOPY, f"--dump-section=ledger.{name}=/dev/stdout", elf, "/dev/null"],
        capture_output=True,
    ).stdout
    return out.decode("utf-8", "replace").strip("\0\n ")


def data_size(elf):
    """NVRAM granted to the app: `_envram_data - _nvram_data`."""
    out = subprocess.run([LLVM_NM, elf], capture_output=True, text=True).stdout
    sym = {}
    for line in out.splitlines():
        parts = line.split()
        if len(parts) == 3 and parts[2] in ("_nvram_data", "_envram_data"):
            sym[parts[2]] = int(parts[0], 16)
    missing = {"_nvram_data", "_envram_data"} - sym.keys()
    if missing:
        sys.exit(f"gen-manifest: {elf} has no {', '.join(sorted(missing))} symbol")
    return sym["_envram_data"] - sym["_nvram_data"]


def main():
    device = sys.argv[1] if len(sys.argv) > 1 else "stax"
    root = pathlib.Path(__file__).resolve().parent.parent

    cargo = tomllib.loads((root / "Cargo.toml").read_text())
    pkg = cargo["package"]
    meta = pkg["metadata"]["ledger"]
    per_device = meta.get(DEVICE_TABLE.get(device, device), {})

    elf = root / "target" / device / "release" / pkg["name"]
    if not elf.is_file():
        sys.exit(f"gen-manifest: {elf} not built: run ./scripts/build {device} first")
    hex_path = elf.with_suffix(".hex")
    if not hex_path.is_file():
        sys.exit(f"gen-manifest: {hex_path} missing: run ./scripts/build {device} first")

    icon = per_device.get("icon")
    if not icon:
        sys.exit(f"gen-manifest: no icon for '{device}' in [package.metadata.ledger.{device}]")

    flags = meta.get("flags", "0")
    manifest = {
        "name": elf_section(elf, "app_name"),
        "version": elf_section(elf, "app_version"),
        "icon": str(root / icon),
        "targetId": elf_section(elf, "target_id"),
        "flags": f"0x{int(str(flags), 0):03x}",
        "apiLevel": elf_section(elf, "api_level"),
        "derivationPath": {"curves": meta["curve"], "paths": meta["path"]},
        "binary": hex_path.name,
        "dataSize": data_size(elf),
    }

    out = elf.parent / f"app_{device}.json"
    out.write_text(json.dumps(manifest, indent=4) + "\n")
    print(f"wrote {out.relative_to(root)}  "
          f"(name={manifest['name']} apiLevel={manifest['apiLevel']} "
          f"dataSize={manifest['dataSize']})")


if __name__ == "__main__":
    main()
