import datetime
import hashlib
import json
from pathlib import Path
import re


ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / 'Contextra_Offline_USB_20261103_r01'
PACKAGE = OUT / 'package'


def sha(path):
    h = hashlib.sha256()
    with path.open('rb') as stream:
        for block in iter(lambda: stream.read(1024**2), b''):
            h.update(block)
    return h.hexdigest()


def write_new(path, text):
    if path.exists():
        if path.read_text() == text:
            return
        if path.name != '00_START_HERE.ko.md' or '준비 중' not in path.read_text():
            raise RuntimeError('Existing delivery file differs: ' + path.name)
    path.write_text(text, encoding='utf-8')


def main():
    seal = json.loads((ROOT / 'reports/sealed-package.json').read_bytes())
    entry = json.loads((ROOT / 'reports/entrypoint-check.json').read_bytes())
    if seal['status'] != 'SEALED_PACKAGE_VERIFIED' or entry['status'] != 'PASS':
        raise RuntimeError('Sealing and standalone entrypoint must pass first')
    digest = sha(PACKAGE / 'package.json')
    if digest != seal['sha256'] or entry['package_manifest_sha256'] != digest:
        raise RuntimeError('Package manifest changed')
    manifest = json.loads((PACKAGE / 'package.json').read_bytes())
    actual = {p.relative_to(PACKAGE).as_posix() for p in PACKAGE.rglob('*') if p.is_file()}
    if actual != set(manifest['files']) | {'package.json'}:
        raise RuntimeError('Package file set changed')
    for name, info in manifest['files'].items():
        path = PACKAGE / name
        if path.is_symlink() or path.stat().st_size != info['size']:
            raise RuntimeError('Package file metadata changed: ' + name)
        if name not in manifest['archives'] and sha(path) != info['sha256']:
            raise RuntimeError('Package source changed: ' + name)
    date = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).strftime('%Y-%m-%d %H:%M')
    for template in sorted((ROOT / 'templates').glob('*.md')):
        value = template.read_text().replace('@DATE@', date).replace('@PACKAGE_SHA@', digest)
        if re.search(r'@[A-Z_]+@', value):
            raise RuntimeError('Template placeholder remains')
        write_new(OUT / template.name, value)
    write_new(OUT / 'PACKAGE_SHA256.txt', digest + '  package/package.json\n')
    validation = f'''# 준비 검증과 미실시 범위

검토: {date} KST. 상태: **봉인 완료 / 사용자 적용 대기**.

| 구분 | 결과 |
| --- | --- |
| 승인 source | commit `{seal['source_commit']}`와 source-lock103파일 일치 |
| 최종 패키지 | payload {seal['files']}파일, archive6개, {seal['bytes']:,}bytes |
| archive 검증 | 이전 반출에서 전체 tar/config image ID/layer diff ID/SHA256 확인, 이번 봉인에서 다시 전체 파일 readback |
| 봉인 manifest SHA256 | `{digest}` |
| 독립 진입 | repo 밖 cwd, PYTHONPATH 제거, 표준 Python의 도움말 및 읽기 전용 Host 검사 PASS |
| 인터넷 비의존 진입 | 별도 network namespace에 loopback만 있는 상태에서 도움말 실행 PASS; host 네트워크는 변경 없음 |
| 실제 대상 | test `contextra-local / compose`, 기존 `artifact-test-r02`, before image5개 일치 |
| 기존 도구 보호 | installed tool 지문 검사 PASS, launcher/backend 미교체 |
| 운영 전후 | 이번 봉인 before/after의 컨테이너·설정 지문 동일 |
| 이전 격리 근거 재사용 | 초기 엔진135+2검사/73subtests·operator38검사, 후속 반출55검사 PASS; 이번에 재실행한 것으로 세지 않음 |
| 복귀 준비 | 이전 Analytics를 포함한6archive와 독립 rollback 구현/격리 근거, 기존ct 보존 |

`BOOTSTRAP_SHA256SUMS`는 작은 코드/문서/manifest와 전체 검사목록을 검증한다.
`SHA256SUMS`의 image 지문은 이번에 전체 읽기 검증을 통과한 package manifest에서 가져왔다.
목록 작성만을 위해64.3GiB를 불필요하게 다시 읽지 않았다. 실제 USB 반입은 stage가 전체를 다시 검사한다.

## 아직 하지 않은 일

- Windows 실제 다운로드/USB 복사·마운트·서버 staging.
- 운영 apply/서비스 정지/재기동/설정 전환과 사용자 rollback 시험.
- cold image load: test에는 image가 이미 있어 적용에서 생략될 수 있다.
- 실제 test Web 목적지에 대한 사용자 확인, 브라우저/실제 알람·썸네일·클립 수용.
- 장시간 안정성·처리속도·10FPS 영향·청계산 현장 호환.
- FAT32용 실제 분할 산출물. 필요하면 별도 분할 운반 단계로 준비한다.

청계산 구형 transaction backend는 이 패키지의 적용 대상이 아니다. 읽기 전용 inventory와
보관 기준 확인 후 별도 호환 후보가 필요하다. test 성공을 현장 수용으로 확대하지 않는다.
기존 source/commit 봉인 보류는31ed63d commit으로 해소됐으며, 이전 UNSEALED 폴더는 historical로 보존한다.
'''
    write_new(OUT / 'VALIDATION.ko.md', validation)
    inventory = dict(release_id=manifest['release_id'], source_commit=manifest['source_commit'],
                     target=manifest['target'], before_images=manifest['before_images'],
                     after_images=manifest['after_images'], archives={
                         name: manifest['files'][name] for name in manifest['archives']})
    write_new(OUT / 'PACKAGE_INVENTORY.json', json.dumps(inventory, ensure_ascii=False, indent=2) + '\n')
    sums = {('package/' + name): info['sha256'] for name, info in manifest['files'].items()}
    sums['package/package.json'] = digest
    outer = [p for p in OUT.iterdir() if p.is_file() and p.name not in ('SHA256SUMS', 'BOOTSTRAP_SHA256SUMS')]
    sums.update({p.name: sha(p) for p in outer})
    write_new(OUT / 'SHA256SUMS', ''.join(f'{value}  {name}\n' for name, value in sorted(sums.items())))
    archive_names = {'package/' + n for n in manifest['archives']}
    boot = {name: value for name, value in sums.items() if name not in archive_names}
    boot['SHA256SUMS'] = sha(OUT / 'SHA256SUMS')
    write_new(OUT / 'BOOTSTRAP_SHA256SUMS', ''.join(f'{value}  {name}\n' for name, value in sorted(boot.items())))
    result = dict(timestamp=date, status='GENERATED_PENDING_FINAL_CHECK', delivery=str(OUT),
                  package_sha256=digest, bootstrap_sha256=sha(OUT / 'BOOTSTRAP_SHA256SUMS'),
                  checksum_entries=len(sums), bootstrap_entries=len(boot),
                  archives=len(archive_names), runtime_applied=False,
                  windows_copy=False, usb_copy=False)
    write_new(ROOT / 'reports/delivery-finalization.json', json.dumps(result, indent=2) + '\n')
    print(json.dumps(result, ensure_ascii=False))


if __name__ == '__main__':
    main()
