#!/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",
    "vpr_init", "vpr_step", "pk_step", "vui_step", "vacuum_board_read_panel",
    "vd_render", "vd_begin", "vd_service", "display_bus_init", "display_bus_write", "si_write",
    "display_bus_read_register", "si_read_register", "xp_decode", "g_pressure_decoded",
    "pv_init", "pv_begin", "pv_complete", "pv_tick", "g_pressure_read_recent",
    "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")
if "vpr_init" not in main_body or "vpr_step" not in main_body or "vacuum_board_read_panel" not in main_body:
    raise SystemExit("main does not bind panel init/ADC/runtime")
for caller, callee in [("vpr_step","pk_step"),("vpr_step","vui_step")]:
    if callee not in function_body(caller):
        raise SystemExit(caller+" missing "+callee)
display_body=main_body
if re.search(r"^[0-9a-fA-F]+ <service_display>:",disassembly,re.M):
    if "service_display" not in main_body:raise SystemExit("main missing display service")
    display_body=function_body("service_display")
for name in ["vd_render","vd_begin","vd_service"]:
    if name not in display_body:raise SystemExit("actual display service missing "+name)
if "display_bus_init" not in main_body or "si_write" not in function_body("display_bus_write"):
    raise SystemExit("display init or GPIO transport binding missing")
pressure_body=main_body
if re.search(r"^[0-9a-fA-F]+ <service_pressure>:",disassembly,re.M):
    if "service_pressure" not in main_body:raise SystemExit("main missing pressure service")
    pressure_body=function_body("service_pressure")
if "display_bus_read_register" not in pressure_body:raise SystemExit("actual main missing pressure bus read")
if "si_read_register" not in function_body("display_bus_read_register"):raise SystemExit("pressure GPIO transport missing")
for name in ["pv_begin","pv_complete"]:
    if name not in pressure_body:raise SystemExit("pressure service missing age binding: "+name)
for name in ["pv_init","pv_tick"]:
    if name not in main_body:raise SystemExit("main missing age binding: "+name)
if "xp_decode" not in pressure_body:raise SystemExit("actual pressure service missing decoder")
nm_text=subprocess.check_output(["arm-none-eabi-nm","-S",str(elf)],text=True)
profile_symbol=re.search(r"^([0-9a-fA-F]+)\s+0000000c\s+\w\s+pressure_profile$",nm_text,re.M)
if not profile_symbol:raise SystemExit("missing immutable pressure profile")
profile_offset=int(profile_symbol.group(1),16)-0x08000000
expected_profile=struct.pack("<BxHii",0x58,31,-500,500)
if data[profile_offset:profile_offset+12]!=expected_profile:raise SystemExit("linked pressure profile mismatch")
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,
    "panel_adc_and_runtime_bound_from_main": True,
    "panel_sample_interval_ms": 10,
    "display_runtime_bound_from_main": True,
    "display_gpio_pins": "PA6 SCL / PA7 SDA / open drain",
    "display_timer": "TIM14 nominal1MHz",
    "display_service_schedule_ms": 5,
    "display_frame_interval_ms": 500,
    "physical_display_validation": False,
    "pressure_read_called_from_main": True,
    "pressure_decoder_bound_from_main": True,
    "linked_expected_pressure_profile": {"address": "0x58", "protocol_revision":31,"min_pa":-500,"max_pa":500},
    "physical_sensor_identity_verified": False,
    "pressure_candidate_address": "0x58",
    "pressure_register_start": "0x04",
    "pressure_nominal_poll_interval_ms": 250,
    "pressure_measurement_qualified": False,
    "panel_debug_record_bytes": 44,
    "learning_resources_ready": False,
    "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))
