"""FIELD-AB-02: bounded network waiting and explicit, recoverable NIC changes."""
import base64
import hashlib
import json
import os
from pathlib import Path
import subprocess

import field_profiles as profiles
from field_ui import print, input

NETPLAN = Path('/etc/netplan')


def command(args, timeout=20):
    return subprocess.run(args, check=True, capture_output=True, text=True, timeout=timeout)


def addresses(interface):
    data = json.loads(command(['ip','-j','-4','address','show','dev',interface]).stdout)
    return [a['local'] for link in data for a in link.get('addr_info',[]) if a.get('scope') == 'global']


def ai_ip(p):
    actual = addresses(p['network']['interface'])
    if p['network']['mode'] == 'dhcp':
        if len(actual) != 1: raise ValueError('DHCP IPv4가 없거나 여러 개입니다. ct net status로 확인하세요.')
        return profiles.ipv4(actual[0])
    from ipaddress import IPv4Interface
    return str(IPv4Interface(p['network']['address']).ip)


def render(p):
    n = profiles.validate(p)['network']
    interface = {'dhcp4': n['mode'] == 'dhcp'}
    if n['mode'] == 'static': interface['addresses'] = [n['address']]
    if n['gateway']: interface['routes'] = [{'to':'default','via':n['gateway']}]
    if n['dns']: interface['nameservers'] = {'addresses':n['dns']}
    return {'network':{'version':2,'renderer':'networkd','ethernets':{n['interface']:interface}}}


def wait_unit(interface):
    if not __import__('re').fullmatch(r'[a-zA-Z0-9_.:-]{1,32}', interface): raise ValueError('NIC 오류')
    return ('[Service]\nExecStart=\n'
            'ExecStart=/lib/systemd/systemd-networkd-wait-online --ipv4 --timeout=60 -i '+interface+':degraded\n'
            'TimeoutStartSec=60\n')


def status(p):
    print('환경:',p['label'],'프로필:',profiles.path(p['id']))
    print('방식:',p['network']['mode'],'저장 주소:',p['network']['address'] or '(DHCP 자동)')
    for args in (['ip','-4','-br','addr','show','dev',p['network']['interface']],['ip','route'],['resolvectl','status',p['network']['interface']]):
        result = subprocess.run(args,capture_output=True,text=True,timeout=10)
        print(result.stdout[:3500] if result.returncode == 0 else '조회 실패: '+args[0])
    print('현장 전환은 물리 콘솔 권장. netplan 적용은 ct net apply, 복구는 ct net restore.')


def restore():
    journal = profiles.STATE / 'network-pending.json'
    record = profiles.read_json(journal, 1048576)
    for name, content in record['before'].items():
        if Path(name).name != name or not name.endswith('.yaml'): raise ValueError('백업 경로 오류')
        path = profiles.safe_path(NETPLAN / name)
        current = path.read_bytes() if path.exists() else None
        expected = record['after'].get(name)
        old = base64.b64decode(content) if content is not None else None
        if current != old and (current is None or hashlib.sha256(current).hexdigest() != expected):
            raise ValueError('이후 수동 변경 발견; 덮어쓰지 않습니다: '+name)
    for name, content in record['before'].items():
        path = NETPLAN / name
        if content is None: path.unlink(missing_ok=True)
        else: profiles.atomic(path,base64.b64decode(content))
    command(['netplan','generate'],30)
    command(['netplan','apply'],45)
    record['status']='RESTORED'; profiles.atomic(profiles.STATE/'network-last.json',record)
    journal.unlink()
    print('이전 NIC 설정 복구 완료. 실제 주소와 SSH를 확인하세요.')


def apply(p):
    import yaml
    pending = profiles.STATE / 'network-pending.json'
    if pending.exists(): raise ValueError('이전 NIC 적용 미완료: ct net restore로 확인하세요.')
    if p['network']['ssh_port'] != 1023:
        raise ValueError('SSH 리스너 변경은 이 NIC 작업에서 하지 않습니다. 현재 SSH1023을 유지하세요.')
    print('환경:',p['label'],'NIC:',p['network']['interface'],'주소:',p['network']['address'] or 'DHCP')
    print('SSH가 끊길 수 있습니다. 물리 콘솔에서 권장합니다. Web/앱 설정은 별도 적용입니다.')
    if input('이 NIC 설정을 적용하려면 NET 입력: ') != 'NET': return
    if not os.isatty(0): raise ValueError('대화형 콘솔에서 실행하세요.')
    changes = {}
    for path in sorted(NETPLAN.glob('*.yaml')):
        profiles.safe_path(path)
        raw = path.read_bytes()
        if len(raw)>131072: raise ValueError('Netplan 파일 크기 초과')
        doc = yaml.safe_load(raw)
        if not isinstance(doc,dict): raise ValueError('Netplan 문법 오류: '+path.name)
        ether = doc.get('network',{}).get('ethernets',{})
        if p['network']['interface'] in ether:
            del ether[p['network']['interface']]
            changes[path.name] = (raw, (json.dumps(doc,indent=2)+'\n').encode())
    target = NETPLAN/'90-contextra.yaml'
    # Preserve any other interfaces in an existing managed file.
    if target.exists():
        original = target.read_bytes()
        base = yaml.safe_load(changes.get(target.name,(original,original))[1])
        if base.get('network',{}).get('renderer','networkd') != 'networkd': raise ValueError('다른 renderer가 있어 자동 변경하지 않습니다.')
        base.setdefault('network',{}).setdefault('ethernets',{}).update(render(p)['network']['ethernets'])
        desired = (json.dumps(base,indent=2)+'\n').encode()
    else:
        original=None; desired=(json.dumps(render(p),indent=2)+'\n').encode()
    changes[target.name] = (original,desired)
    record={'profile':p['id'],'status':'APPLYING','before':{k:base64.b64encode(v[0]).decode() if v[0] is not None else None for k,v in changes.items()},
            'after':{k:hashlib.sha256(v[1]).hexdigest() for k,v in changes.items()}}
    profiles.atomic(pending,record)
    try:
        for name,(_,raw) in changes.items(): profiles.atomic(NETPLAN/name,raw)
        command(['netplan','generate'],30)
        result=subprocess.run(['netplan','try','--timeout','60'],timeout=90)
        if result.returncode: raise ValueError('Netplan 적용 미확인')
        actual=addresses(p['network']['interface'])
        if not actual or (p['network']['mode']=='static' and ai_ip(p) not in actual): raise ValueError('새 주소 미확인')
    except (Exception,KeyboardInterrupt):
        print('적용 미확인. 도구가 변경한 NIC 파일을 복구합니다.')
        restore()
        raise
    record['status']='APPLIED'; profiles.atomic(profiles.STATE/'network-last.json',record)
    pending.unlink()
    print('NIC 적용 완료. 재부팅 후에도 유지됩니다. 다음: ct config prepare')
