#!/usr/bin/env python3
import hashlib
import json
import pathlib
import re
import struct
import subprocess
import sys

elf = pathlib.Path(sys.argv[1])
binary = pathlib.Path(sys.argv[2])
map_file = pathlib.Path(sys.argv[3])
objdump = subprocess.check_output(["arm-none-eabi-objdump", "-h", str(elf)], text=True)
map_text = map_file.read_text(errors="replace")
data = binary.read_bytes()
if len(data) < 8:
    raise SystemExit("binary is too short for a Cortex-M vector table")
initial_sp, reset_vector = struct.unpack_from("<II", data)
if initial_sp != 0x20002000:
    raise SystemExit(f"unexpected initial SP: 0x{initial_sp:08x}")
if reset_vector & 1 == 0 or not (0x08000000 <= (reset_vector & ~1) < 0x08008000):
    raise SystemExit(f"reset vector outside STM32G030F6 flash: 0x{reset_vector:08x}")
required = (
    "vacuum_fan_manager_init",
    "vacuum_fan_manager_step",
    "vacuum_output_lock_allows_power",
    "vacuum_output_lock_allows_duty",
    "vacuum_board_set_fan_power",
    "vacuum_board_set_fan_duty",
    "vacuum_board_init",
    "vacuum_board_sample",
    "vacuum_board_force_fan_disabled",
    "vacuum_vbus_count_to_mv",
    "vacuum_current_count_to_ma",
    "main",
)
missing = [symbol for symbol in required
           if not re.search(r"\b" + re.escape(symbol) + r"\b", map_text)]
if missing:
    raise SystemExit("linked image missing required symbols: " + ", ".join(missing))
if ".isr_vector" not in objdump or ".text" not in objdump:
    raise SystemExit("ELF does not contain vector and text sections")
disassembly = subprocess.check_output(["arm-none-eabi-objdump", "-d", str(elf)], text=True)
def function_body(symbol):
    match = re.search(r"^[0-9a-fA-F]+ <" + re.escape(symbol) + r">:\n(.*?)(?=^[0-9a-fA-F]+ <[^>]+>:\n|\Z)", disassembly, re.M | re.S)
    if match is None:
        raise SystemExit("missing disassembly for " + symbol)
    return match.group(1)
main_body = function_body("main")
power_body = function_body("vacuum_board_set_fan_power")
duty_body = function_body("vacuum_board_set_fan_duty")
if "vacuum_fan_manager_step" not in main_body:
    raise SystemExit("main does not call the vacuum fan policy")
if "vacuum_output_lock_allows_power" not in power_body or "vacuum_board_force_fan_disabled" not in power_body:
    raise SystemExit("fan power callback does not enforce the off lock")
if "vacuum_output_lock_allows_duty" not in duty_body or "vacuum_board_force_fan_disabled" not in duty_body:
    raise SystemExit("fan PWM callback does not enforce the zero-duty lock")
print(json.dumps({
    "status": "LINKED_IMAGE_STATIC_CHECK_PASS",
    "target": "STM32G030F6P6 / Cortex-M0+",
    "toolchain": subprocess.check_output(["arm-none-eabi-gcc", "--version"], text=True).splitlines()[0],
    "flash_binary_bytes": len(data),
    "initial_sp": f"0x{initial_sp:08x}",
    "reset_vector": f"0x{reset_vector:08x}",
    "linked_runtime_symbols": list(required[:-1]),
    "fan_policy_called_from_main": True,
    "fan_power_and_pwm_callbacks_hard_locked_off": True,
    "entry_symbol": "main",
    "sha256_bin": hashlib.sha256(data).hexdigest(),
    "sha256_elf": hashlib.sha256(elf.read_bytes()).hexdigest(),
    "physical_hardware_validation": False,
    "sensor_and_timer_runtime_enabled": True,
    "fan_power_output_enabled": False,
    "physical_gpio_levels_verified": False,
    "blower_profile_validated": False,
    "product_function_complete": False,
}, indent=2))
