"""Sealed directories with exact committed tool provenance and immutable images."""
import hashlib
from pathlib import Path
import re
import shutil

from .. import bundle
from ..core import absolute, canonical, identifier, read_json, relative, require, sha256, write_json

SCHEMA = 'contextra.offline.full.v1'
ROLES = {'analytics', 'ai-control-sidecar', 'ai-gateway', 'redis', 'postgresql'}


def validate(m):
    require(isinstance(m, dict) and set(m) == {'schema', 'release_id', 'source_commit', 'target',
            'before_images', 'after_images', 'archives', 'files', 'policy'},
            'PACKAGE_SCHEMA', '지원하는 전체 배포 manifest가 아닙니다.')
    require(m['schema'] == SCHEMA, 'PACKAGE_SCHEMA', '다른 패키지 세대입니다.')
    identifier(m['release_id'])
    require(re.fullmatch(r'[0-9a-f]{40}', m['source_commit'] or ''), 'SOURCE_COMMIT', '승인 source commit이 필요합니다.')
    require(set(m['target']) == {'server', 'backend', 'definition_sha256'} and m['target']['backend'] == 'compose',
            'TARGET_BACKEND', '이번 적용은 검증한 Compose 설치만 지원합니다. 현장판은 정보 수집 후 검토하세요.')
    identifier(m['target']['server'])
    require(re.fullmatch(r'[0-9a-f]{64}', m['target']['definition_sha256']), 'TARGET_HASH', '서버 정의 지문이 필요합니다.')
    for key in ('before_images', 'after_images'):
        require(set(m[key]) == ROLES and all(re.fullmatch(r'sha256:[0-9a-f]{64}', v) for v in m[key].values()),
                'IMAGE_CONTRACT', '5개 서비스의 정확한 image ID가 필요합니다.')
    require(all(m['before_images'][r] == m['after_images'][r] for r in ROLES - {'analytics'}),
            'SERVICE_SCOPE', '이번 패키지는 Analytics만 교체합니다.')
    require(m['policy'] == {'fight_engine': 'r5', 'fall_engine': 'r2', 'fight_display_seconds': 3},
            'POLICY_SCOPE', '검증된 엔진 선택/표시 정책만 허용합니다.')
    require(isinstance(m['files'], dict) and 0 < len(m['files']) <= 20000, 'FILE_LIMIT', '파일 목록을 확인하세요.')
    total = 0
    for name, v in m['files'].items():
        relative(name)
        require(name != 'package.json' and isinstance(v, dict) and set(v) == {'sha256', 'size'},
                'FILE_SPEC', '잘못된 파일 항목입니다.')
        require(re.fullmatch(r'[0-9a-f]{64}', v['sha256']) and type(v['size']) is int and 0 <= v['size'] <= 128*1024**3,
                'FILE_SPEC', '파일 크기/지문을 확인하세요.')
        total += v['size']
    require(total <= 128*1024**3, 'FILE_LIMIT', '패키지 크기가 상한을 넘습니다.')
    require(isinstance(m['archives'], list) and m['archives'], 'ARCHIVE_REQUIRED', '완성 image archive가 필요합니다.')
    for name in m['archives']:
        require(name.startswith('images/') and name in m['files'], 'ARCHIVE_REQUIRED', 'image 파일이 누락됐습니다.')
    return m


def verify(root, expected):
    root = absolute(root, no_symlinks=True)
    require(re.fullmatch(r'[0-9a-f]{64}', expected or ''), 'APPROVED_HASH', '별도로 전달받은 manifest SHA256를 입력하세요.')
    require(sha256(root/'package.json') == expected, 'PACKAGE_HASH', '승인된 manifest와 다릅니다.')
    m = validate(read_json(root/'package.json', 8*1024**2))
    actual = set()
    for p in root.rglob('*'):
        require(not p.is_symlink(), 'PACKAGE_LINK', '패키지의 링크를 허용하지 않습니다.')
        if p.is_file(): actual.add(p.relative_to(root).as_posix())
        else: require(p.is_dir(), 'PACKAGE_SPECIAL', '특수파일을 허용하지 않습니다.')
    require(actual == set(m['files']) | {'package.json'}, 'PACKAGE_FILES', '파일이 추가/누락됐습니다.')
    for name, v in m['files'].items():
        p = root/name
        require(p.stat().st_size == v['size'] and sha256(p) == v['sha256'], 'PACKAGE_BYTES', '파일 지문이 다릅니다: '+name)
    return m


def create(repo, output, spec, source_paths, assets):
    """No dirty snapshot fallback: a new tool must be committed before sealing."""
    repo, output = absolute(repo, no_symlinks=True), absolute(output, no_symlinks=True)
    require(not output.exists(), 'OUTPUT_EXISTS', '새 패키지 폴더만 사용하세요.')
    tracked = bundle.committed_files(repo, spec['source_commit'])
    payload = {}
    for name in source_paths:
        relative(name)
        require(name in tracked, 'SOURCE_UNCOMMITTED', '새 도구를 승인 commit으로 고정한 뒤 봉인하세요: '+name)
        require(name in ('ct-offline', 'tools/secure_bridge_tls.py', 'control_sidecar/__init__.py',
                         'control_sidecar/config.py', 'control_sidecar/protocol.py') or
                name.startswith(('deployment_kit/', 'tools/icerti_offline/maintenance/', 'docs/deployment/')),
                'SOURCE_SCOPE', '배포 도구/안내서만 source로 반입합니다.')
        require(not name.endswith(('.pyc', '.env')) and '/test_' not in name, 'SOURCE_SCOPE', '시험/비공개 파일은 제외합니다.')
        payload[name] = bundle.checked_source(repo, name, tracked[name])
    for name, info in assets.items():
        relative(name)
        require(name.startswith('images/') and name not in payload, 'ASSET_SCOPE', 'image asset만 별도 반입합니다.')
        p = absolute(info['source'], no_symlinks=True)
        require(sha256(p) == info['sha256'], 'ASSET_HASH', '검증된 image archive와 다릅니다.')
        payload[name] = p
    m = dict(spec, schema=SCHEMA, files={n: {'sha256': sha256(p), 'size': p.stat().st_size} for n,p in payload.items()})
    validate(m)
    output.mkdir(mode=0o755, parents=True)
    for name, p in payload.items():
        dst = output/name; dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(p, dst); dst.chmod(0o644)
    write_json(output/'package.json', m, new=True, mode=0o644)
    digest = sha256(output/'package.json')
    verify(output, digest)
    return digest
