from pathlib import Path, PurePosixPath
import os, re, shutil, subprocess, tarfile

home = Path('/home/justrep-ssh')
web = home / 'www'
name = 'justrec-password-reset-20261012-v2'
archive_path = home / (name + '.tar.gz')
staging = home / (name + '-stage')
backup = home / (name + '-backup')
assert web.is_dir(), 'Web root missing'
assert archive_path.is_file(), 'Deployment archive missing'
assert not staging.exists() and not backup.exists(), 'Deployment path already exists'
staging.mkdir(mode=0o700)
backup.mkdir(mode=0o700)
with tarfile.open(archive_path, 'r:gz') as archive:
    members = archive.getmembers()
    paths = []
    for member in members:
        relative = PurePosixPath(member.name)
        assert not relative.is_absolute() and '..' not in relative.parts, 'Unsafe archive path'
        assert member.isfile(), 'Archive must contain regular files only'
        paths.append(relative)
    assert PurePosixPath('index.html') in paths, 'Frontend index missing'
    assert PurePosixPath('backend/index.php') in paths, 'API router missing'
    assert PurePosixPath('backend/objects/password-reset/password-reset.php') in paths, 'Password-reset handler missing'
    archive.extractall(staging)
php_files = [p for p in paths if p.suffix == '.php']
for relative in php_files:
    result = subprocess.run(['php', '-l', str(staging.joinpath(*relative.parts))], capture_output=True, text=True)
    assert result.returncode == 0, result.stdout + result.stderr
index = (staging / 'index.html').read_text()
assets = set(re.findall(r'(?:src|href)="(/assets/[^\"]+)"', index))
assert assets and all((staging / asset.lstrip('/')).is_file() for asset in assets), 'Index references missing assets'
files = sorted(paths, key=lambda p: (
    4 if p.as_posix() == 'backend/index.php' else
    3 if p.suffix == '.html' else
    2 if p.parts[0] == 'backend' else 0,
    p.as_posix()))
backed_up, published = [], []
try:
    for relative in files:
        target = web.joinpath(*relative.parts)
        if target.is_file():
            backup_target = backup.joinpath(*relative.parts)
            backup_target.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(target, backup_target)
            backed_up.append(relative)
    for relative in files:
        source = staging.joinpath(*relative.parts)
        target = web.joinpath(*relative.parts)
        target.parent.mkdir(parents=True, exist_ok=True)
        temp = target.with_name(target.name + '.justrec-next')
        shutil.copyfile(source, temp)
        temp.chmod((target.stat().st_mode & 0o777) if target.exists() else 0o644)
        os.replace(temp, target)
        published.append(relative)
    live_index = (web / 'index.html').read_text()
    live_assets = set(re.findall(r'(?:src|href)="(/assets/[^\"]+)"', live_index))
    assert live_assets and all((web / asset.lstrip('/')).is_file() for asset in live_assets), 'Live index references missing assets'
except Exception:
    for relative in reversed(published):
        target = web.joinpath(*relative.parts)
        backup_target = backup.joinpath(*relative.parts)
        if backup_target.is_file():
            rollback = target.with_name(target.name + '.rollback')
            shutil.copy2(backup_target, rollback)
            os.replace(rollback, target)
        elif target.is_file():
            target.unlink()
    raise
print(f'Published {len(files)} files ({len(php_files)} PHP files); backup={backup}', flush=True)
