"""Read-only host inventory and explicitly approved fresh-host bootstrap."""
from __future__ import annotations
import ipaddress
import json
import os
from pathlib import Path
import platform
import re
import shutil
import socket
import tarfile
import tempfile
import urllib.request
from .core import absolute, require, KitError, run, docker, read_json, write_json, sha256, now, atomic_write, lock

PACKAGES = ('docker-ce','docker-ce-cli','containerd.io','docker-buildx-plugin','docker-compose-plugin',
            'nvidia-container-toolkit','nvidia-container-toolkit-base','libnvidia-container-tools','libnvidia-container1')
CONFLICTS = ('docker.io','docker-compose','docker-compose-v2','podman-docker','containerd','runc')


def version(value):
    return tuple(int(x) for x in re.findall(r'\d+',value)[:3])


def os_info():
    d={}
    for line in Path('/etc/os-release').read_text().splitlines():
        if '=' in line:
            k,v=line.split('=',1); d[k]=v.strip('"')
    return dict(id=d.get('ID'),version=d.get('VERSION_ID'),arch=platform.machine(),codename=d.get('VERSION_CODENAME'))


def addresses(runner=run):
    _, raw=runner(['ip','-j','address','show'])
    return [{'interface':x['ifname'],'ip':a['local'],'prefix':a['prefixlen']} for x in json.loads(raw)
            if x.get('operstate')=='UP' for a in x.get('addr_info',[])
            if a.get('family')=='inet' and a.get('scope')=='global']


def listening_ports(runner=run):
    _,raw=runner(['ss','-H','-ltn'])
    found=set()
    for line in raw.splitlines():
        fields=line.split()
        if len(fields)>=4:
            try: found.add(int(fields[3].rsplit(':',1)[1]))
            except (ValueError,IndexError): pass
    return sorted(found)


def gpu_info(runner=run):
    _, raw=runner(['nvidia-smi','--query-gpu=name,driver_version,memory.total','--format=csv,noheader,nounits'])
    result=[]
    for row in raw.strip().splitlines():
        values=[x.strip() for x in row.split(',')]
        require(len(values)==3, 'GPU_QUERY', 'GPU 정보 형식을 확인할 수 없습니다.')
        result.append(dict(name=values[0],driver=values[1],memory_mib=int(values[2])))
    return result


def inventory():
    result=dict(time=now(),os=os_info(),python=platform.python_version(),cpus=os.cpu_count(),
                commands={c:shutil.which(c) is not None for c in ('docker','nvidia-ctk','nvidia-smi','ip','ss','findmnt','rsync','openssl','sha256sum')})
    for key,fn in (('addresses',addresses),('ports',listening_ports),('gpu',gpu_info)):
        try: result[key]=fn()
        except (KitError,ValueError): result[key]={'status':'UNKNOWN'}
    try:
        _,v=docker('compose','version','--short'); result['compose_version']=v.strip()
        _,info=docker('info','--format','{{json .Runtimes}}'); result['nvidia_runtime']='nvidia' in json.loads(info)
        result['docker_access']='PASS'
    except (KitError,ValueError): result['docker_access']='UNAVAILABLE_OR_SUDO_REQUIRED'
    try:
        _,text=run(['timedatectl','show','--property=NTPSynchronized','--value'])
        result['clock_synchronized']=text.strip()=='yes'
    except KitError: result['clock_synchronized']=None
    return result


def enforce_requirements(inv, requirements):
    require(inv['os']['id']=='ubuntu' and inv['os']['version']=='24.04' and inv['os']['arch']=='x86_64',
            'HOST_UNSUPPORTED', 'v1 bootstrap의 확인된 대상은 Ubuntu 24.04 x86_64입니다.')
    require(inv.get('docker_access')=='PASS','DOCKER_UNAVAILABLE','Docker 설치/권한을 확인하세요. docker 그룹 가입은 필수가 아닙니다.')
    require(version(inv.get('compose_version','0'))>=version(requirements.get('compose_min','2.24.4')),
            'COMPOSE_VERSION','Compose !override를 지원하는 버전이 필요합니다.')
    require(inv.get('nvidia_runtime') is True,'GPU_RUNTIME_MISSING','NVIDIA Container Toolkit의 Docker 연결이 필요합니다.')
    GPUs=inv.get('gpu')
    require(isinstance(GPUs,list) and GPUs,'GPU_QUERY','GPU/driver를 읽을 수 없습니다.')
    minimum=requirements.get('driver_min')
    require(isinstance(minimum,str) and minimum,'DRIVER_POLICY_MISSING','배포 image의 승인된 driver 최소 조건을 manifest에 기록하세요.')
    require(all(version(g['driver'])>=version(minimum) for g in GPUs),'DRIVER_REVIEW_REQUIRED',
            f'현재 driver가 release 요구값 {minimum}보다 낮습니다. 자동 driver 업그레이드는 하지 않습니다.',
            'Codex가 해당 image/driver 호환성을 검토하고 승인된 Host 유지보수 절차를 진행')


def network_check(site, inv):
    require(any(x['ip']==site['ai_ip'] for x in inv.get('addresses',[]) if isinstance(x,dict)),
            'IP_CHANGED','현재 NIC에서 저장된 AI IP를 찾을 수 없습니다. OS 네트워크는 변경하지 않았습니다.',
            'deployctl configure (현재 IP/관제/인증서 정합성 확인)')


def _download(url):
    with urllib.request.urlopen(url,timeout=15) as r:
        raw=r.read(256*1024+1)
    require(len(raw)<=256*1024,'VENDOR_METADATA_SIZE','저장소 키/설정 응답이 너무 큽니다.')
    return raw


def bootstrap(package_lock=None, *, apply=False):
    plan={'scope':'FRESH_HOST_ONLY','online':True,'packages':list(PACKAGES),
          'not_changed':['NVIDIA driver','docker group','netplan','기존 컨테이너/DB'],
          'requires':'공식 저장소에서 선택한 정확한 package=version lock; sudo --apply'}
    if not apply: return plan
    require(package_lock is not None,'PACKAGE_LOCK','승인된 정확한 package version JSON이 필요합니다. guide bootstrap을 확인하세요.')
    require(os.geteuid()==0,'SUDO_REQUIRED','bootstrap 변경은 sudo가 필요합니다.')
    osdata=os_info()
    require((osdata['id'],osdata['version'],osdata['arch'])==('ubuntu','24.04','x86_64'),
            'HOST_UNSUPPORTED','온라인 bootstrap은 Ubuntu24.04 x86_64에 한정합니다.')
    versions=read_json(package_lock)
    require(set(versions)==set(PACKAGES) and all(isinstance(v,str) and re.fullmatch(r'[A-Za-z0-9.+:~_-]+',v) for v in versions.values()),
            'PACKAGE_LOCK','9개 package의 정확한 버전을 지정하세요. latest 자동 선택은 하지 않습니다.')
    for p in CONFLICTS:
        code,out=run(['dpkg-query','-W','-f=${Status}',p],check=False)
        require(code!=0 or out.strip()!='install ok installed','PACKAGE_CONFLICT',
                f'{p} 충돌 가능성이 있습니다. 기존 패키지를 자동 제거하지 않습니다.')
    if shutil.which('docker'):
        _,raw=docker('ps','-aq')
        require(not raw.strip(),'EXISTING_DOCKER','기존 컨테이너가 있는 host는 별도 유지보수 승인이 필요합니다.')
    state=Path('/var/lib/contextra-host-bootstrap')
    with lock(state):
        intent=state/'intent.json'
        require(not intent.exists(),'BOOTSTRAP_INTENT_EXISTS','앞선 bootstrap 결과를 먼저 확인하세요. 자동 재실행하지 않습니다.')
        write_json(intent,dict(stage='INTENT',versions=versions,os=osdata,time=now()),new=True)
        run(['apt-get','update'],timeout=180,limit=16*1024**2)
        run(['apt-get','install','-y','--no-install-recommends','ca-certificates','curl','gnupg'],timeout=180,limit=16*1024**2)
        keydir=Path('/etc/apt/keyrings'); keydir.mkdir(parents=True,exist_ok=True,mode=0o755)
        dockerkey=keydir/'contextra-docker.asc'
        atomic_write(dockerkey,_download('https://download.docker.com/linux/ubuntu/gpg'),new=True,mode=0o644)
        dockersource=Path('/etc/apt/sources.list.d/contextra-docker.sources')
        atomic_write(dockersource, 'Types: deb\nURIs: https://download.docker.com/linux/ubuntu\nSuites: noble\nComponents: stable\nArchitectures: amd64\nSigned-By: '+str(dockerkey)+'\n',new=True,mode=0o644)
        nvraw=state/'nvidia.asc'; atomic_write(nvraw,_download('https://nvidia.github.io/libnvidia-container/gpgkey'),new=True)
        nvkey=keydir/'contextra-nvidia.gpg'
        require(not nvkey.exists(),'FILE_EXISTS','기존 NVIDIA key를 덮어쓰지 않았습니다.')
        run(['gpg','--batch','--dearmor','--output',str(nvkey),str(nvraw)])
        os.chmod(nvkey,0o644)
        rawlist=_download('https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list').decode()
        active=[]
        for line in rawlist.splitlines():
            if line.startswith('deb https://nvidia.github.io/'):
                active.append(line.replace('deb https://','deb [signed-by='+str(nvkey)+'] https://',1))
        require(active,'VENDOR_REPOSITORY','NVIDIA 공식 repo 형식이 바뀌었습니다. 추측하지 않고 중단합니다.')
        atomic_write(Path('/etc/apt/sources.list.d/contextra-nvidia.list'),'\n'.join(active)+'\n',new=True,mode=0o644)
        run(['apt-get','update'],timeout=180,limit=16*1024**2)
        run(['apt-get','install','-y','--no-install-recommends',*[p+'='+versions[p] for p in PACKAGES]],timeout=600,limit=32*1024**2)
        daemon=Path('/etc/docker/daemon.json')
        if daemon.exists(): atomic_write(state/'daemon.before.json',daemon.read_bytes(),new=True)
        run(['nvidia-ctk','runtime','configure','--runtime=docker'])
        run(['systemctl','restart','docker'],timeout=60)
        write_json(state/'result.json',dict(stage='HOST_PACKAGES_INSTALLED',time=now(),gpu_container_test='NOT_RUN'),new=True)
    return {'status':'HOST_PACKAGES_INSTALLED','gpu_smoke':'NOT_RUN','driver':'UNCHANGED'}


def recovery_pack(packages_dir, output):
    src,out=absolute(packages_dir,no_symlinks=True),absolute(output,no_symlinks=True)
    require(not out.exists() and not out.is_relative_to(src),'RECOVERY_OUTPUT','새 repo 밖 경로를 사용하세요.')
    items={}
    for p in sorted(src.glob('*.deb')):
        require(not p.is_symlink(),'PATH_SYMLINK','package symlink는 허용하지 않습니다.')
        _,raw=run(['dpkg-deb','-f',str(p),'Package','Version','Architecture'])
        require('nvidia-driver-' not in raw,'DRIVER_PACKAGE','driver 변경은 일반 recovery와 분리하세요.')
        items[p.name]=dict(sha256=sha256(p),metadata=raw.strip())
    require(items,'RECOVERY_EMPTY','의존성을 포함한 승인된 .deb 파일이 필요합니다.')
    out.mkdir(parents=True)
    for name in items: shutil.copyfile(src/name,out/name)
    write_json(out/'recovery.json',dict(schema=1,os=os_info(),files=items,closure='NOT_VERIFIED'),new=True)
    return {'path':str(out),'packages':len(items),'dependency_closure':'NOT_VERIFIED_ON_CLEAN_HOST'}


def recovery_install(directory, *, apply=False):
    d=absolute(directory,no_symlinks=True); m=read_json(d/'recovery.json')
    require(m['os']==os_info(),'RECOVERY_OS','복구 package의 OS/arch가 현재 host와 다릅니다.')
    files=[]
    for name,meta in m['files'].items():
        require(Path(name).name==name and name.endswith('.deb'),'PACKAGE_PATH','package 경로 오류')
        p=absolute(d/name,no_symlinks=True)
        require(sha256(p)==meta['sha256'],'PACKAGE_HASH','package checksum 오류')
        files.append(str(p))
    argv=['apt-get','--no-download','--no-install-recommends','install',*files]
    if not apply: return {'plan':argv,'dependency_check':'apt-get --simulate --no-download','network':'NO_DOWNLOAD'}
    require(os.geteuid()==0,'SUDO_REQUIRED','Host recovery에는 sudo가 필요합니다.')
    require(files,'RECOVERY_EMPTY','복구 package 목록이 비었습니다.')
    run(['apt-get','--simulate','--no-download','install',*files],timeout=60,limit=16*1024**2)
    run([*argv[:1],'-y',*argv[1:]],timeout=600,limit=32*1024**2)
    return {'status':'PACKAGES_INSTALLED','runtime_configuration':'기존 /etc/docker 설정과 Toolkit 연결을 별도 검토하세요.'}
