#!/usr/bin/env python3
"""Prepare and verify reversible Pi 5 EEPROM configuration files; never flash hardware."""
import argparse
import difflib
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
from eeprom_inputs import PROJECT,prepare,sha256

MANAGED={'BOOT_ORDER','BOOT_UART','NET_INSTALL_ENABLED','NET_INSTALL_AT_POWER_ON','POWER_OFF_ON_HALT','WAIT_FOR_POWER_BUTTON'}

def settings(text):
    if len(text.encode())>4076 or '\0' in text:raise ValueError('EEPROM configuration must fit one 4076-byte record without NUL bytes')
    result={}
    for line in text.splitlines():
        if line.lstrip().startswith('#') or '=' not in line:continue
        key,value=line.split('=',1);key=key.strip();value=value.strip()
        if key in MANAGED:
            if key in result:raise ValueError(f'Duplicate managed setting: {key}')
            result[key]=value
    return result

def merge(original,profile):
    expected=settings(profile)
    if set(expected)!=MANAGED:raise ValueError('Incomplete EEPROM profile')
    retained=[]
    for line in original.splitlines():
        key=line.split('=',1)[0].strip() if '=' in line and not line.lstrip().startswith('#') else None
        if key not in MANAGED:retained.append(line)
    merged='\n'.join(retained).rstrip()+'\n\n'+profile.strip()+'\n'
    if settings(merged)!=expected:raise ValueError('Merged settings mismatch')
    return merged

def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--profile',choices=['production','development'],default='production')
    parser.add_argument('--base-image',type=Path,help='Regular-file Pi 5 EEPROM image; defaults to the pinned preview firmware')
    parser.add_argument('--current-config',type=Path,help='Saved board configuration to preserve instead of the image defaults')
    parser.add_argument('--output-directory',type=Path,help='New directory; existing paths are refused')
    args=parser.parse_args()
    cache,pin=prepare()
    base=(args.base_image or cache/'pieeprom.bin').resolve(strict=True)
    if not base.is_file() or base.stat().st_size!=2*1024*1024:raise ValueError('Base must be a regular 2 MiB Pi 5 EEPROM image, not a block device')
    base_checksum=sha256(base)
    tool=[sys.executable,str(cache/'rpi-eeprom-config')]
    original=subprocess.check_output([*tool,str(base)]).decode('utf-8')
    if args.current_config:
        if not args.current_config.is_file() or args.current_config.stat().st_size>4076:raise ValueError('Current configuration must be a regular file of at most 4076 bytes')
        original=args.current_config.read_text()
    # Validate bounds but preserve existing duplicates/conditional overrides in
    # the rollback configuration. All managed occurrences are replaced below.
    if len(original.encode())>4076 or '\0' in original:raise ValueError('Invalid original configuration size or NUL byte')
    profile=(PROJECT/'config/eeprom'/f'{args.profile}.conf').read_text()
    configured=merge(original,profile)
    if args.output_directory:
        work=args.output_directory.absolute();work.mkdir(mode=0o700)
    else:work=Path(tempfile.mkdtemp(prefix=f'eeprom-{args.profile}.',dir=PROJECT/'out'))
    shutil.copyfile(base,work/'base.bin')
    if sha256(work/'base.bin')!=base_checksum or sha256(base)!=base_checksum:raise ValueError('Base image changed during preparation')
    shutil.copyfile(cache/'LICENSE',work/'LICENSE')
    (work/'original.conf').write_text(original)
    (work/'configured.conf').write_text(configured)
    subprocess.run([*tool,'--config',str(work/'original.conf'),'--out',str(work/'rollback.bin'),str(work/'base.bin')],check=True)
    subprocess.run([*tool,'--config',str(work/'configured.conf'),'--out',str(work/'configured.bin'),str(work/'base.bin')],check=True)
    readback=subprocess.check_output([*tool,str(work/'configured.bin')]).decode()
    rollback=subprocess.check_output([*tool,str(work/'rollback.bin')]).decode()
    if readback!=configured or rollback!=original:raise ValueError('EEPROM configuration readback mismatch')
    (work/'review.diff').write_text(''.join(difflib.unified_diff(original.splitlines(True),configured.splitlines(True),fromfile='original.conf',tofile='configured.conf')))
    manifest={'format':1,'profile':args.profile,'upstream_commit':pin['commit'],
              'hardware_modified':False,'custom_inputs_provided':bool(args.current_config or args.base_image),
              'input_base_sha256':base_checksum,'settings':settings(configured),
              'files':{p.name:sha256(p) for p in sorted(work.iterdir()) if p.is_file()}}
    (work/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
    if not args.output_directory and not args.current_config and not args.base_image:
        link=PROJECT/'out'/f'eeprom-{args.profile}-latest.next'
        link.symlink_to(work.name)
        link.replace(PROJECT/'out'/f'eeprom-{args.profile}-latest')
    print(f'Prepared and read-back verified: {work}')
    print('No hardware was modified. Review review.diff and retain base.bin, original.conf and rollback.bin.')
    if not args.current_config and not args.base_image:print('This uses pinned preview defaults; it is not a backup of your Pi.')

if __name__=='__main__':
    try:main()
    except (OSError,ValueError,subprocess.CalledProcessError) as error:sys.exit(f'ERROR: {error}')
