"""Explicit next-boot netplan files, CAS cancellation; no runtime NIC apply."""
import base64
import hashlib
import json
from pathlib import Path
import tempfile

import field_profiles as fp
import field_network as net
from field_ui import input, print


def sha(raw):return hashlib.sha256(raw).hexdigest()


def changes(profile):
    import yaml
    result={}
    for p in sorted(net.NETPLAN.glob('*.yaml')):
        fp.safe_path(p)
        raw=p.read_bytes()
        if len(raw)>131072:raise ValueError('Netplan file too large')
        value=yaml.safe_load(raw)
        if not isinstance(value,dict):raise ValueError('Malformed netplan file')
        network=value.get('network',{})
        if network.get('renderer','networkd')!='networkd':raise ValueError('Non-networkd renderer requires explicit adapter')
        ethers=network.get('ethernets',{})
        # Match/rename and bridge membership need review instead of overlapping definitions.
        for name,item in ethers.items():
            if name!=profile['network']['interface'] and ('match' in item or item.get('set-name')==profile['network']['interface']):
                raise ValueError('NIC match/set-name ambiguity; inspect existing netplan')
        for group in ('bridges','bonds'):
            if any(profile['network']['interface'] in v.get('interfaces',[]) for v in network.get(group,{}).values()):
                raise ValueError('Managed NIC belongs to bridge/bond')
        if profile['network']['interface'] in ethers:
            del ethers[profile['network']['interface']]
            result[p.name]=(raw,(json.dumps(value,indent=2)+'\n').encode())
    target=net.NETPLAN/'90-contextra.yaml'
    before=target.read_bytes() if target.exists() else None
    value=yaml.safe_load(result.get(target.name,(before,before))[1]) if before else {'network':{'version':2,'renderer':'networkd','ethernets':{}}}
    value['network'].setdefault('ethernets',{}).update(net.render(profile)['network']['ethernets'])
    result[target.name]=(before,(json.dumps(value,indent=2)+'\n').encode())
    return result


def validate_files(changed):
    # Validate the complete candidate in a private root, not the running host network.
    with tempfile.TemporaryDirectory(prefix='netplan-',dir=fp.STATE) as root:
        dest=Path(root)/'etc/netplan';dest.mkdir(parents=True,mode=0o700)
        for p in net.NETPLAN.glob('*.yaml'):fp.atomic(dest/p.name,fp.safe_path(p).read_bytes())
        for name,(_,raw) in changed.items():fp.atomic(dest/name,raw)
        net.command(['netplan','generate','--root-dir',root],30)


def schedule(p):
    path=fp.STATE/'network-scheduled.json'
    if path.exists() or (fp.STATE/'network-pending.json').exists():raise ValueError('Existing network transaction preserved')
    if p['network']['ssh_port']!=(fp.server.CURRENT or {}).get('ssh_port',1023):raise ValueError('SSH listener change not part of NIC schedule')
    changed=changes(p);validate_files(changed)
    record={'profile':p['id'],'status':'WRITING','boot_id':Path('/proc/sys/kernel/random/boot_id').read_text().strip(),
        'expected_address':p['network']['address'] or 'DHCP','ssh_port':p['network']['ssh_port'],
        'before':{k:base64.b64encode(v[0]).decode() if v[0] is not None else None for k,v in changed.items()},
        'after':{k:sha(v[1]) for k,v in changed.items()}}
    fp.atomic(path,record)
    for name,(before,after) in changed.items():
        target=fp.safe_path(net.NETPLAN/name)
        if (target.read_bytes() if target.exists() else None)!=before:raise ValueError('Concurrent netplan edit; ct net cancel for CAS recovery')
        fp.atomic(target,after)
    record['status']='NEXT_BOOT_NOT_APPLIED';fp.atomic(path,record)
    print('Next boot files staged; current NIC unchanged. No reboot executed. Recovery: ct net cancel')


def cancel():
    path=fp.STATE/'network-scheduled.json';r=fp.read_json(path,1048576)
    for name,old in r['before'].items():
        if Path(name).name!=name or not name.endswith('.yaml'):raise ValueError('Invalid journal path')
        target=fp.safe_path(net.NETPLAN/name);raw=target.read_bytes() if target.exists() else None
        before=base64.b64decode(old) if old is not None else None
        if raw!=before and (raw is None or sha(raw)!=r['after'][name]):raise ValueError('Later NIC edit preserved: '+name)
    for name,old in r['before'].items():
        target=net.NETPLAN/name
        if old is None:target.unlink(missing_ok=True)
        else:fp.atomic(target,base64.b64decode(old))
    r['status']='CANCELLED_FILES_ONLY';fp.atomic(fp.STATE/'network-scheduled-last.json',r);path.unlink()
    print('Scheduled files restored. Current NIC unchanged; after reboot use physical-console ct net apply if required.')


def main(action,p):
    if action=='nextboot':
        print('Next boot:',p['label'],p['network']['address'] or 'DHCP','SSH port',p['network']['ssh_port'])
        if input('Write next-boot files only? NEXTBOOT / Enter=cancel: ')=='NEXTBOOT':schedule(p)
    elif action=='cancel':
        if input('Cancel scheduled files only? CANCEL / Enter=cancel: ')=='CANCEL':cancel()
    else:
        path=fp.STATE/'network-scheduled.json'
        if not path.exists():print('No scheduled network change');return
        r=fp.read_json(path)
        print('Schedule:',r['status'],'expected:',r['expected_address'],'SSH:',r['ssh_port'])
        print('Boot changed:',r['boot_id']!=Path('/proc/sys/kernel/random/boot_id').read_text().strip(),
              'Actual address still requires ct net status. Recovery: ct net cancel (files only).')
