#!/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 = ("lg_init", "lg_accept_ai_packet", "lg_accept_ai_decoded_result", "lg_output_sync", "pca9956b_init",
            "is31fl3236a_lamp_backend_init", "aiw_stream_push", "aiw_stream_push_message",
            "aiw_encode_ui_ack", "lamp_ui_wire_service_init", "lamp_ui_wire_service_handle",
            "lamp_ui_dispatcher_init", "lamp_ui_dispatcher_handle", "lamp_uart_init",
            "lamp_uart_tx_enqueue", "lamp_tx_queue_push", "lamp_tx_queue_pop",
            "lamp_ticket_dispatch_issue", "USART2_IRQHandler", "SysTick_Handler", "lamp_board_inputs_read", "lg_poll", "lamp_board_ir_init", "lamp_ir_filter_update", "lamp_adc_init", "lamp_adc_poll", "lamp_adc_get_frame", "lamp_adc_failed", "lamp_read_factory_vrefint_cal", "lamp_measurement_from_frame", "lamp_ntc_candidate_temperature", "pca_fault_init", "pca_fault_poll", "pca_fault_get", "lamp_i2c_pins_init", "lamp_i2c_pins_available", "lamp_i2c_read_start", "lamp_i2c_read_poll", "lamp_i2c_read_get", "pca_async_poll", "lamp_driver_diagnostics_poll", "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 "lg_accept_ai_decoded_result" not in main_body:
    raise SystemExit("main does not pass parsed results to the policy guard")
if "lamp_ticket_dispatch_issue" not in main_body:
    raise SystemExit("main does not call the scheduled ticket dispatcher")
for required_call in ("aiw_stream_push_message", "lamp_ui_wire_service_handle",
                      "aiw_encode_ui_ack", "lamp_uart_tx_enqueue"):
    if required_call not in main_body:
        raise SystemExit("main does not call the UI wire path: " + required_call)
# GPIO sampler and policy poll must be reachable from the actual entry loop,
# possibly through the compiler-kept sampling helper.
sample_path = main_body
if "sample_and_poll_inputs" in main_body:
    sample_path += function_body("sample_and_poll_inputs")
for call in ("lamp_board_inputs_read", "lg_poll", "lamp_adc_poll", "lamp_adc_get_frame", "lamp_adc_failed", "lamp_measurement_from_frame", "lamp_ntc_candidate_temperature"):
    if call not in sample_path:
        raise SystemExit("main has no bound physical sampler/poll call: " + call)
if "lamp_read_factory_vrefint_cal" not in main_body:
    raise SystemExit("main has no factory calibration read call")
if "lamp_adc_init" not in main_body:
    raise SystemExit("main has no raw ADC initializer call")
if "lamp_board_ir_init" not in main_body:
    raise SystemExit("main has no physical IR initializer call")
if "lamp_ir_filter_update" not in function_body("lamp_board_inputs_read"):
    raise SystemExit("GPIO sampling path has no IR filter call")
if "1fff75aa" not in function_body("lamp_read_factory_vrefint_cal") or "ldrh" not in function_body("lamp_read_factory_vrefint_cal"):
    raise SystemExit("factory calibration reader address/16bit load is wrong")
for forbidden in ("lg_init", "lg_arm", "lg_issue_ticket", "lg_output_sync", "pca_fault_init", "pca_fault_poll", "pca_fault_get"):
    if forbidden.startswith("pca_fault") and forbidden in sample_path:
        raise SystemExit("unbound PCA monitor unexpectedly reached at runtime")
    if forbidden in main_body:
        raise SystemExit("target runtime unexpectedly enables a guard/output path: " + forbidden)
if "lamp_i2c_pins_init" not in main_body or "lamp_i2c_pins_available" not in sample_path:
    raise SystemExit("I2C GPIO guard not called from actual target")

if "lamp_i2c_clock_init" not in main_body or "lamp_i2c_clock_available" not in sample_path:
    raise SystemExit("I2C clock owner is not called from target boot/poll path")
for symbol in ("lamp_i2c_read_start", "lamp_i2c_read_poll", "lamp_i2c_read_get"):
    if "lamp_i2c_clock_available" not in function_body(symbol):
        raise SystemExit("register reader lacks current clock guard: " + symbol)

if "lamp_i2c_read_start" in main_body or "lamp_i2c_read_start" in sample_path:
    raise SystemExit("Unqualified I2C reader unexpectedly started in main")

if "lamp_diagnostic_runtime_init" not in main_body or "poll_diagnostics" not in main_body:
    raise SystemExit("diagnostic owner not bound to main")
for call in ("lamp_diagnostic_runtime_poll","lamp_diagnostic_runtime_get"):
    if call not in function_body("poll_diagnostics"):
        raise SystemExit("diagnostic service missing " + call)
for call in ("lamp_driver_diagnostics_init","lamp_driver_diagnostics_poll","lamp_driver_diagnostics_failed"):
    if call not in function_body("lamp_diagnostic_runtime_poll"):
        raise SystemExit("runtime does not reach bank " + call)
if "lamp_diagnostic_runtime_needs_poll" not in main_body:
    raise SystemExit("main lacks active poll wake guard")

for symbol in ("lamp_i2c_read_start","lamp_i2c_write_start"):
    if "lamp_i2c_owner_acquire" not in function_body(symbol):
        raise SystemExit("transport lacks exclusive owner acquisition: " + symbol)
if "lamp_i2c_write_start" in main_body:
    raise SystemExit("writer unexpectedly started before configuration readback integration")

for call in ("lamp_dark_init_begin","lamp_dark_init_poll","lamp_dark_init_done"):
    if call not in function_body("lamp_diagnostic_runtime_poll"):
        raise SystemExit("dark initialization not bound: " + call)
for call in ("lamp_i2c_write_start","lamp_i2c_write_poll","lamp_i2c_read_start","lamp_i2c_read_poll","lamp_i2c_read_get"):
    if call not in function_body("lamp_dark_init_poll"):
        raise SystemExit("dark initializer missing actual transport: " + call)

for call in ("lamp_i2c_write_start","lamp_i2c_read_start","lamp_i2c_read_get"):
    if call not in function_body("lamp_frame_stage_poll"):
        raise SystemExit("frame stage missing actual transport: " + call)
if "lamp_frame_stage_begin" in main_body:
    raise SystemExit("unqualified frame staging unexpectedly starts in main")

for call in ("lamp_diagnostic_runtime_prepare_frame","lamp_diagnostic_runtime_frame_ready"):
    if call not in function_body("poll_diagnostics"):
        raise SystemExit("frame request path not reachable from main helper: " + call)
for call in ("lamp_frame_stage_begin","lamp_driver_diagnostics_pause"):
    if call not in function_body("lamp_diagnostic_runtime_prepare_frame"):
        raise SystemExit("frame request missing coordinator call: " + call)
for call in ("lamp_frame_stage_poll","lamp_driver_diagnostics_resume"):
    if call not in function_body("lamp_diagnostic_runtime_poll"):
        raise SystemExit("runtime lacks staged poll/resume: " + call)

symbols = subprocess.check_output(["arm-none-eabi-nm", "-S", str(elf)], text=True)
profile_match = re.search(r"^([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+\w\s+current_profile$", symbols, re.M)
if profile_match is None:
    raise SystemExit("missing immutable current profile placeholder")
profile_offset = int(profile_match.group(1),16)-0x08000000
profile_size = int(profile_match.group(2),16)
if profile_size != 68 or profile_offset < 0 or data[profile_offset:profile_offset+profile_size] != bytes(profile_size):
    raise SystemExit("unqualified profile placeholder is not all zero in linked flash image")

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_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,
    "qualified_frame_stage_linked": True,
    "qualified_frame_stage_request_path_bound": True,
    "qualified_profile_available_in_main": False,
    "main_profile_flash_zero_verified": True,
    "main_profile_flash_bytes": profile_size,
    "diagnostic_pause_resume_bound": True,
    "frame_readback_grants_physical_output": False,
    "I2C_shared_owner_bound": True,
    "I2C_nonblocking_writer_linked": True,
    "I2C_writer_runtime_start_path_bound": True,
    "configuration_write_readback_runtime_bound": True,
    "dark_init_registers_per_chip": 58,
    "dark_init_total_explicit_readbacks": 174,
    "uart_rx_frame_parser_linked": True,
    "crc_valid_frames_reach_policy_guard": True,
    "ui_session_command_ack_path_linked": True,
    "ui_ack_queue_is_atomic": True,
    "guard_configured_or_armed": False,
    "ticket_dispatch_path_linked": True,
    "ticket_issuance_enabled": False,
    "runtime_ticket_requests_queued": False,
    "tx_packet_enqueue_is_atomic": True,
    "tx_wire_delivery_confirmed": False,
    "guard_rejection_counter_linked": True,
    "output_adapter_bound": False,
    "led_output_enabled": False,
    "external_watchdog_kicked": False,
    "runtime_control_enabled": False,
    "physical_contact_GPIO_sampling_bound": True,
    "A35_IR_policy_linked": True,
    "IR_GPIO_bound": True,
    "ADC_raw_sampling_bound": True,
    "nominal_voltage_current_diagnostics_bound": True,
    "VREFINT_factory_cal_read_bound": True,
    "physical_frontend_calibration": False,
    "candidate_NTC_curve_bound": True,
    "physical_NTC_identification_and_calibration": False,
    "ADC_temperature_and_supply_sampling_bound": False,
    "ADC_calibrated_safety_permissions_bound": False,
    "LED_driver_health_readback_bound": False,
    "PCA_fault_monitor_linked": True,
    "PCA_fault_monitor_runtime_called": False,
    "PCA_read_callback_bound": False,
    "I2C_GPIO_initializer_bound": True,
    "I2C_GPIO_runtime_guard_bound": True,
    "I2C_peripheral_transport_bound": True,
    "I2C_clock_owner_initializer_bound": True,
    "I2C_clock_owner_runtime_guard_bound": True,
    "I2C_candidate_TIMINGR": "0x30521317",
    "I2C_physical_waveforms_qualified": False,
    "I2C_register_reader_linked": True,
    "I2C_register_reader_runtime_start_path_bound": True,
    "I2C_register_reader_started_on_physical_MCU": False,
    "async_bank_transport_adapter_linked": True,
    "async_bank_runtime_enabled": True,
    "diagnostic_RESET_release_bound": True,
    "diagnostic_eligible_masks": [0,0,0],
    "diagnostic_GPIO_fault_latch_bound": True,
}, indent=2))
