"""Install the bounded field console without changing NIC, auth or running services."""
import argparse
import base64
import hashlib
import json
import os
from pathlib import Path
import pwd
import shutil
import subprocess
import sys
import time

PREFIX=Path('/usr/local/lib/contextra-maintenance/20260928-r01')
PRIVATE=Path('/var/lib/contextra-maintenance/field')


def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('bundle',type=Path)
    parser.add_argument('--interface',required=True)
    parser.add_argument('--office-address',required=True)
    parser.add_argument('--office-web',required=True)
    parser.add_argument('--field-address',required=True)
    parser.add_argument('--field-web',required=True)
    parser.add_argument('--field-media',required=True)
    args=parser.parse_args()
    if os.geteuid()!=0:raise ValueError('sudo required')
    if shutil.disk_usage('/').free<15*1024**3:raise ValueError('root 여유15GiB 필요')
    if args.bundle.is_symlink() or args.bundle.stat().st_uid!=0:raise ValueError('검증해 root 전용 경로로 복사한 bundle만 사용하세요.')
    manifest=json.loads((args.bundle/'manifest.json').read_text())
    targets=[]
    for row in manifest['files']:
        source=args.bundle/row['path'];relative=Path(row['path'])
        if relative.is_absolute() or '..' in relative.parts or source.is_symlink() or sha(source)!=row['sha256']:
            raise ValueError('Bundle source mismatch')
        if relative.parts[0]=='maintenance' and len(relative.parts)==2 and source.suffix=='.py':target=PREFIX/source.name
        elif relative.parts[0]=='guides' and len(relative.parts)==2 and source.suffix=='.md':target=Path('/workspace/contextra/setup/guides')/source.name
        elif relative.as_posix()=='vendor/tools/secure_bridge_enroll.py':target=PREFIX/'bridge_tools/tools/secure_bridge_enroll.py'
        else:raise ValueError('Unknown bundle target')
        if source.suffix=='.py':compile(source.read_bytes(),str(source),'exec')
        if target.is_symlink():raise ValueError('Target symlink')
        if target.exists() and sha(target) not in (row['previous_sha256'],row['sha256']):raise ValueError('설치 도구의 별도 수정 보존: '+target.name)
        targets.append((source,target))
    backup=PRIVATE/('install-'+time.strftime('%Y%m%dT%H%M%S')+'-'+str(os.getpid()))
    backup.mkdir(parents=True,mode=0o700)
    # Only the installation's exact files are backed up; no credential values are exported.
    previous=[]
    for source,target in targets:
        if target.exists():
            old=backup/(hashlib.sha256(str(target).encode()).hexdigest()[:12]+'-'+target.name+'.before');old.write_bytes(target.read_bytes());old.chmod(0o600)
            previous.append({'target':str(target),'before':str(old),'sha256':sha(old)})
    (backup/'previous.json').write_text(json.dumps(previous,indent=2))
    sys.path.insert(0,str(args.bundle/'maintenance'))
    sys.path.append(str(PREFIX))
    import field_profiles as fp
    from field_network import wait_unit
    u=pwd.getpwnam('si')
    fp.PROFILES.mkdir(parents=True,exist_ok=True,mode=0o700)
    os.chown(fp.PROFILES,u.pw_uid,u.pw_gid);fp.PROFILES.chmod(0o700)
    office=fp.create('office','스인 내부망',args.interface,args.office_address,args.office_web)
    field=fp.create('field','폐쇄 현장망',args.interface,args.field_address,args.field_web,mode='static')
    field['media']=[{'label':'현장 미디어 (진단 참고용)','host':args.field_media,'port':554}]
    for profile in (office,field):
        if fp.path(profile['id']).exists():fp.load(profile['id'])
        else:fp.save(profile)
    if not (PRIVATE/'selected.json').exists():fp.atomic(PRIVATE/'selected.json',{'profile':'office'})
    dropin=Path('/etc/systemd/system/systemd-networkd-wait-online.service.d/90-contextra-bound.conf')
    if dropin.exists():
        original=dropin.read_bytes()
        if original!=wait_unit(args.interface).encode():raise ValueError('기존 대기 상한 별도 설정이 있어 보존합니다.')
    else:original=None
    fp.atomic(backup/'wait-before.json',{'path':str(dropin),'before':base64.b64encode(original).decode() if original else None})
    # Modules first, dispatcher last. No shell profile or Runtime image changes.
    for source,target in sorted(targets,key=lambda pair:pair[1].name=='ct.py'):
        fp.atomic(target,source.read_bytes(),mode=0o644,user=target.is_relative_to('/workspace/contextra/setup/guides'))
    fp.atomic(dropin,wait_unit(args.interface).encode(),mode=0o644)
    subprocess.run(['systemctl','daemon-reload'],check=True,timeout=20)
    import field_runtime
    field_runtime.remember_baseline()
    result={'status':'TOOLS_INSTALLED_LIVE_TEST_DEFERRED','backup':str(backup),'network_changed':False,
            'wait_cap_seconds':60,'login_requests':0,'services_started':False,'profiles':['office','field'],
            'files':{str(target):sha(target) for _,target in targets}}
    fp.atomic(backup/'receipt.json',result)
    print(json.dumps(result,ensure_ascii=False,indent=2))


if __name__=='__main__':main()
