"""Private, round-trip checked Compose snapshots; never restore rotated secrets.

Compose parses a saved config again at `up`. Its config JSON already escapes
literal dollars; preserve that representation and all volume/network identities.
"""
from __future__ import annotations
import copy
import os
from pathlib import Path
from .core import absolute, require, canonical, read_json, write_json, sha256, docker


def command_environment():
    # Shell env has higher priority than --env-file. Do not inherit app settings.
    allowed = ('PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'DOCKER_CONFIG',
               'XDG_RUNTIME_DIR', 'XDG_CONFIG_HOME')
    return {k: os.environ[k] for k in allowed if k in os.environ} | {'LC_ALL': 'C'}


def escape_values(value):
    if isinstance(value, dict):
        return {k: escape_values(v) for k, v in value.items()}
    if isinstance(value, list):
        return [escape_values(v) for v in value]
    return value.replace('$', '$$') if isinstance(value, str) else value


def normalized(doc, project):
    result = copy.deepcopy(doc)
    require(result.get('name', project) == project, 'PROJECT_SCOPE',
            'Compose project가 등록한 이름과 다릅니다.')
    result['name'] = project
    for key in ('volumes', 'networks'):
        for name, spec in result.get(key, {}).items():
            require(isinstance(spec, dict) and isinstance(spec.get('name'), str)
                    and spec['name'], 'RESOURCE_NAME_REQUIRED',
                    f'{key}/{name}: 정규화된 실제 resource 이름이 필요합니다.')
    return result


def seal(doc, path, project, *, runner=None):
    """Input is REAL Compose config JSON, not a raw user-authored configuration."""
    runner = docker if runner is None else runner
    p = absolute(path, no_symlinks=True)
    expected = normalized(doc, project)
    # Doubling dollars again corrupts healthchecks and credentials on Compose v5.
    write_json(p, expected, new=True)
    _, raw = runner('compose', '--project-name', project, '-f', str(p),
                    'config', '--format', 'json', timeout=30, limit=8*1024**2,
                    environment=command_environment())
    from .core import parse_json
    actual = normalized(parse_json(raw), project)
    require(canonical(actual) == canonical(expected), 'COMPOSE_ROUNDTRIP',
            'Compose 재해석 결과가 달라졌습니다. $ 값·경로·volume 이름을 검토하세요.')
    write_json(p.with_name('compose.normalized.json'), actual, new=True)
    return p


def document(path, expected_hash=None):
    p = absolute(path, no_symlinks=True).with_name('compose.normalized.json')
    if expected_hash:
        require(p.is_file() and sha256(p) == expected_hash, 'SNAPSHOT_CHANGED',
                '정규화된 Compose snapshot이 변경됐습니다.')
    return read_json(p, 8*1024**2)


# Credential/session/TLS paths are live, externally owned state. We do not copy,
# restore or enumerate them. Runtime's own checks remain authoritative.
EXTERNAL_STATE_TARGETS = ('/run/bridge', '/run/bridge-state', '/run/ai-tls')


def file_bindings(doc):
    records = {}
    for cfg in doc['services'].values():
        for v in cfg.get('volumes', []):
            if v.get('type') != 'bind' or not v.get('read_only'):
                continue
            target = v['target']
            if any(target == t or target.startswith(t+'/') for t in EXTERNAL_STATE_TARGETS):
                continue
            p = absolute(v['source'], no_symlinks=True)
            if p.is_file():
                require(p.stat().st_size <= 64*1024**2, 'CONFIG_SIZE',
                        '설정 파일이 너무 큽니다. model은 별도 receipt로 검증하세요.')
                records[str(p)] = sha256(p)
            elif p.is_dir() and target == '/etc/contextra':
                members = {}
                for f in sorted(p.rglob('*')):
                    require(not f.is_symlink(), 'PATH_SYMLINK', '설정 디렉터리의 링크를 허용하지 않습니다.')
                    if f.is_file():
                        require(len(members) < 1000 and f.stat().st_size <= 4*1024**2,
                                'CONFIG_SIZE', '설정 디렉터리 크기 상한을 초과했습니다.')
                        members[f.relative_to(p).as_posix()] = sha256(f)
                records[str(p)] = {'directory': members}
    return records


def verify_bindings(records):
    for path, expected in records.items():
        p = absolute(path, no_symlinks=True)
        if isinstance(expected, str):
            same = p.is_file() and sha256(p) == expected
        else:
            require(isinstance(expected, dict) and set(expected) == {'directory'},
                    'SNAPSHOT_CHANGED', '파일 binding 기록이 잘못됐습니다.')
            files = {}
            require(p.is_dir(), 'CONFIG_CHANGED', '기존 설정 디렉터리가 없습니다.')
            for f in p.rglob('*'):
                require(not f.is_symlink(), 'PATH_SYMLINK', '설정 디렉터리에 링크가 생겼습니다.')
                if f.is_file():
                    require(len(files) < 1000 and f.stat().st_size <= 4*1024**2,
                            'CONFIG_SIZE', '설정 크기가 바뀌었습니다.')
                    files[f.relative_to(p).as_posix()] = sha256(f)
            same = files == expected['directory']
        require(same, 'CONFIG_CHANGED', '이전 release의 mount 설정 파일이 바뀌었습니다. 자동 덮어쓰기/복원하지 않습니다.')
