"""Private-package boundaries shared by creation and configuration import.

Only an explicit, checksum-covered file list is read. Never enumerate a user's
home directory. Outputs must be private before any credential bytes are written;
existing destination directories/files are not silently repurposed.
"""
import hashlib
import os
from pathlib import Path
import stat
import subprocess
import tempfile
import zipfile


def digest(data):
    return hashlib.sha256(data).hexdigest()


def private_directory(path):
    path = path.absolute()
    if path.is_symlink():
        raise ValueError('private output directory must not be a symlink')
    existed = path.exists()
    path.mkdir(parents=True, mode=0o700, exist_ok=True)
    if os.name != 'nt':
        if path.stat().st_uid != os.getuid() or stat.S_IMODE(path.stat().st_mode) & 0o077:
            raise ValueError('private output directory must be owned by this user with mode 0700')
    else:
        # Modify ACLs only on a newly created directory. For an existing one,
        # validate every allow ACE rather than widening or rewriting user ACLs.
        script = "param([string]$Directory)\n$ErrorActionPreference='Stop'; $p=$Directory; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value; "
        if not existed:
            script += "& icacls.exe $p /inheritance:r /grant:r ('*'+$sid+':(OI)(CI)F') '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null; if($LASTEXITCODE){throw 'ACL update failed'}; "
        # Python's private Windows mkdir may add OWNER RIGHTS (S-1-3-4).
        # It grants rights to this directory's owner, not to another user; allow
        # it only after independently checking that the owner is in the keep set.
        script += "$allowed=@($sid,'S-1-5-18','S-1-5-32-544'); $acl=Get-Acl -LiteralPath $p; if($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin $allowed){throw 'Unexpected private-directory owner'}; $allowed+='S-1-3-4'; foreach($a in $acl.Access){if($a.AccessControlType -eq 'Allow' -and $a.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -notin $allowed){throw 'Output directory is not private'}}"
        # -File binds the path as a parameter; -Command would reinterpret appended
        # arguments as PowerShell source and mishandle spaces/metacharacters.
        with tempfile.TemporaryDirectory(prefix='sshmcp-acl-') as temporary:
            script_path = Path(temporary) / 'private-directory.ps1'
            script_path.write_text(script, encoding='utf-8-sig')
            subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
                            '-File', str(script_path), '-Directory', str(path)], check=True, capture_output=True)
    return path


def verified_members(root):
    members = {}
    for line in (root / 'SHA256SUMS').read_text(encoding='utf-8').splitlines():
        expected, name = line.split('  ', 1)
        relative = Path(name)
        if relative.is_absolute() or '..' in relative.parts or name in members:
            raise ValueError('unsafe or duplicate package manifest path')
        path = root / relative
        if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()) or not path.is_file() or path.stat().st_size > 128 * 1024 * 1024:
            raise ValueError('package manifest references an unsupported file')
        data = path.read_bytes()
        if digest(data) != expected:
            raise ValueError('package checksum mismatch: ' + name)
        members[name] = data
    if not members:
        raise ValueError('package manifest is empty')
    return members


def write_zip(output, entries):
    private_directory(output.parent)
    if output.exists():
        raise ValueError('refusing to overwrite an existing private package')
    manifest = ''.join(f'{digest(data)}  {name}\n' for name, data in sorted(entries.items()))
    entries = {**entries, 'SHA256SUMS': manifest.encode()}
    with tempfile.NamedTemporaryFile(dir=output.parent, prefix='.sshmcp-private-', delete=False) as temporary:
        staged = Path(temporary.name)
    try:
        with zipfile.ZipFile(staged, 'w', zipfile.ZIP_DEFLATED) as archive:
            directories = {'SSHMCP-Private/'}
            for name in entries:
                parent = Path('SSHMCP-Private') / Path(name).parent
                directories.update(p.as_posix() + '/' for p in [parent, *parent.parents] if p != Path('.'))
            for name in sorted(directories):
                info = zipfile.ZipInfo(name)
                info.external_attr = (0o40700 << 16) | 0x10
                archive.writestr(info, b'')
            for name, data in sorted(entries.items()):
                info = zipfile.ZipInfo('SSHMCP-Private/' + name)
                executable = name.startswith('linux-x86_64/') and not name.endswith('SHA256SUMS')
                info.external_attr = (0o100700 if executable else 0o100600) << 16
                info.compress_type = zipfile.ZIP_DEFLATED
                archive.writestr(info, data)
        # Both files share a private directory/filesystem. Publishing a hard link
        # is atomic and refuses a name created after preflight, without exposing a
        # partially copied archive or replacing a user's existing package.
        os.link(staged, output)
        if os.name != 'nt':
            output.chmod(0o600)
    finally:
        staged.unlink(missing_ok=True)
    return digest(output.read_bytes())
