#!/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", "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_read_start" in main_body or "lamp_i2c_read_start" in sample_path:
    raise SystemExit("Unqualified I2C reader unexpectedly started in main")

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,
    "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": False,
    "I2C_register_reader_linked": True,
    "I2C_register_reader_runtime_started": False,
}, indent=2))
