#!/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)
main_disassembly = subprocess.check_output(
    ["arm-none-eabi-objdump", "-d", "--disassemble=main", str(elf)], text=True)
safe_boot_disassembly = subprocess.check_output(
    ["arm-none-eabi-objdump", "-d", "--disassemble=drill_safe_boot", str(elf)], text=True)
map_text = map_file.read_text(errors="replace")
data = binary.read_bytes()
size_fields = subprocess.check_output(["arm-none-eabi-size", str(elf)], text=True).splitlines()[-1].split()
text_bytes, data_bytes, bss_bytes = map(int, size_fields[:3])
ram_used_bytes = data_bytes + bss_bytes
if len(data) < 8:
    raise SystemExit("binary is too short for an Arm vector table")
initial_sp, reset_vector = struct.unpack_from("<II", data)
if initial_sp != 0x20005800:
    raise SystemExit(f"unexpected initial SP: 0x{initial_sp:08x}")
if reset_vector & 1 == 0 or not (0x08000000 <= (reset_vector & ~1) < 0x08020000):
    raise SystemExit(f"reset vector outside STM32G431CB flash: 0x{reset_vector:08x}")
required = ("SystemInit", "drill_safe_boot", "drill_safe_gpio_validate",
            "drill_safe_gpio_preload", "drill_safe_gpio_configure_outputs",
            "drv8316_configure_off", "ds_init", "charge_manager_step",
            "charge_registers_init", "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")
for symbol, disassembly in (("drill_safe_boot", main_disassembly),
                           ("drill_safe_gpio_preload", safe_boot_disassembly),
                           ("drill_safe_gpio_configure_outputs", safe_boot_disassembly)):
    if symbol not in disassembly:
        raise SystemExit(f"expected safe-boot call missing from disassembly: {symbol}")
if 'drill_pwm_stage_init' not in main_disassembly:
    raise SystemExit('TIM1 stopped initializer not reachable from main')
for name in ['drill_pwm_stage_available','drill_pwm_stage_write','drill_debug_pwm_stage_available']:
    if name not in map_text: raise SystemExit('missing PWM stage symbol: '+name)
if 'drill_spi_init' not in main_disassembly: raise SystemExit('SPI initializer not bound')
for symbol in ['drill_spi_start','drill_spi_poll','drill_spi_get']:
    if symbol not in map_text: raise SystemExit('SPI transport missing: '+symbol)
symbol_text=subprocess.check_output(['arm-none-eabi-nm',str(elf)],text=True)
handler=re.search(r'^([0-9a-fA-F]+)\s+\w\s+SysTick_Handler$',symbol_text,re.M)
if handler is None or struct.unpack_from('<I',data,15*4)[0]!=(int(handler[1],16)|1):
    raise SystemExit('SysTick vector does not point to linked handler')
for symbol in ['drill_timebase_init','drill_timebase_get','drill_spi_poll','drill_spi_active']:
    if symbol not in main_disassembly: raise SystemExit('main timebase scheduling missing: '+symbol)
if "drill_config_service" not in main_disassembly: raise SystemExit("configuration service not bound")
if "drill_config_bench_begin" in main_disassembly: raise SystemExit("main must not request unqualified wake")
for sym in ["dc_begin","dc_poll","drill_config_bench_begin"]:
    if sym not in map_text: raise SystemExit("missing async config symbol "+sym)
if "drill_adc_poll" not in main_disassembly or "drill_adc_busy" not in main_disassembly: raise SystemExit("ADC service not bound")
if "drill_adc_bench_begin" in main_disassembly or "drill_adc_capture_start" in main_disassembly: raise SystemExit("main must not request unqualified ADC")
if "drill_dual_poll" not in main_disassembly or "drill_dual_busy" not in main_disassembly: raise SystemExit("dual ADC service not bound")
if "drill_dual_bench_begin" in main_disassembly or "drill_dual_capture_start" in main_disassembly: raise SystemExit("main must not start unqualified internal sample clock")
ring=re.search(r"^([0-9a-fA-F]+)\s+\w\s+ds_ring$",symbol_text,re.M)
sizes=subprocess.check_output(["arm-none-eabi-nm","-S",str(elf)],text=True)
ring_size=re.search(r"^[0-9a-fA-F]+\s+([0-9a-fA-F]+)\s+\w\s+ds_ring$",sizes,re.M)
if ring_size is None or int(ring_size[1],16)!=8: raise SystemExit("DMA ring must be two aligned 32-bit pairs")
if ring is None or int(ring[1],16)%4 or not 0x20000000<=int(ring[1],16)<=0x20005800-8: raise SystemExit("DMA ring not aligned in normal SRAM")
if "drill_dual_stream_take" not in main_disassembly: raise SystemExit("stream diagnostic consumer missing")
if "drill_dual_stream_start" in main_disassembly: raise SystemExit("default main cannot start bench DMA")
print(json.dumps({
    "status": "LINKED_IMAGE_STATIC_CHECK_PASS",
    "target": "STM32G431CBT6 / Cortex-M4",
    "toolchain": subprocess.check_output(["arm-none-eabi-gcc", "--version"], text=True).splitlines()[0],
    "flash_binary_bytes": len(data),
    "flash_capacity_bytes": 131072,
    "ram_used_bytes": ram_used_bytes,
    "ram_capacity_bytes": 22528,
    "initial_sp": f"0x{initial_sp:08x}",
    "reset_vector": f"0x{reset_vector:08x}",
    "linked_policy_symbols": list(required[:-1]),
    "entry_symbol": "main",
    "sha256_bin": hashlib.sha256(data).hexdigest(),
    "sha256_elf": hashlib.sha256(elf.read_bytes()).hexdigest(),
    "physical_hardware_validation": False,
    "ADC1_diagnostic_service_bound": True,
    "ADC1_started_in_main": False,
    "simultaneous_FOC_sampling_bound": False,
    "ADC12_dual_regular_trigger_candidate_linked": True,
    "DMA1_ADC12_stream_candidate_linked": True,
    "DMA_ring_SRAM_address": ring[1],
    "DMA_stream_started_in_main": False,
    "FOC_consumer_connected": False,
    "internal_TIM1_sampling_clock_started_in_main": False,
    "TIM2_microsecond_timebase_and_SysTick_wake_bound": True,
    "timebase_and_SPI_schedule_called_in_main": True,
    "SysTick_vector_verified": True,
    "clock_accuracy_and_runtime_latency_qualified": False,
    "SPI2_initializer_bound": True,
    "SPI2_async_transport_linked": True,
    "SPI2_transactions_started_in_main": False,
    "DRV_register_configuration_runtime_bound": True,
    "DRV_configuration_started_in_main": False,
    "safe_gpio_boot_function_linked": True,
    "physical_gpio_levels_verified": False,
    "tim1_initializer_bound": True,
    "tim1_initialized_on_physical_MCU": False,
    "tim1_counter_enabled": False,
    "tim1_channels_and_MOE_enabled": False,
    "PWM_GPIO_alternate_functions_bound": False,
    "PWM_compare_staging_linked": True,
    "motor_power_enabled": False,
    "charge_enabled": False,
    "runtime_control_enabled": False,
}, indent=2))
