#!/usr/bin/env python3
"""Prepare and operate the coordinated mobile release through the pinned SSH workflow."""
import argparse
import base64
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import subprocess
import tarfile

ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('deployment_client', ROOT / 'scripts/deploy.py')
client = importlib.util.module_from_spec(spec)
spec.loader.exec_module(client)


def prepare(release, candidate):
    if subprocess.check_output(['git', 'status', '--porcelain'], cwd=ROOT, text=True).strip():
        raise ValueError('Commit the reviewed release before preparation')
    if subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip() != release:
        raise ValueError('Release must match reviewed HEAD')
    manifest = json.loads((ROOT / 'integrations/laravel/source-manifest.json').read_text())
    for name in ['app/Services/MobileBlock.php', 'app/Services/MobileDeletion.php']:
        expected = hashlib.sha256((ROOT / 'integrations/laravel' / name).read_bytes()).hexdigest()
        manifest[name] = {'baseline_sha256': None, 'candidate_sha256': expected}
    for name, item in manifest.items():
        if hashlib.sha256((candidate / name).read_bytes()).hexdigest() != item['candidate_sha256']:
            raise ValueError(f'Candidate source differs: {name}')
    folder = ROOT / '.deploy/releases'
    folder.mkdir(parents=True, exist_ok=True, mode=0o700)
    archive = folder / f'{release}-laravel.tar'
    with tarfile.open(archive, 'w') as stream:
        for name in manifest:
            stream.add(candidate / name, arcname=name, recursive=False)
    archive.chmod(0o600)
    metadata = {'release': release, 'sha256': client.digest(archive), 'files': manifest}
    path = folder / f'{release}-laravel.json'
    path.write_text(json.dumps(metadata, indent=2) + '\n')
    path.chmod(0o600)
    print(f'Laravel artifact verified: {release}; sha256={metadata["sha256"]}')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=['prepare', 'stage', 'infra', 'snapshot', 'hold', 'apply', 'resume', 'recover', 'status', 'fetch'])
    parser.add_argument('--release', required=True)
    parser.add_argument('--candidate', type=Path)
    parser.add_argument('--production', action='store_true')
    args = parser.parse_args()
    os.umask(0o077)
    if not re.fullmatch('[a-f0-9]{40}', args.release):
        raise ValueError('Specify the reviewed full commit')
    if args.action == 'prepare':
        if not args.candidate:
            raise ValueError('Specify the isolated candidate')
        prepare(args.release, args.candidate)
        return
    if args.action not in ['status', 'fetch'] and not args.production:
        raise ValueError('Production changes require --production and user authorization')
    config = client.configuration(ROOT / '.deploy/config.json')
    metadata_path = ROOT / '.deploy/releases' / f'{args.release}-laravel.json'
    metadata = json.loads(metadata_path.read_text())
    archive = metadata_path.with_suffix('.tar')
    if metadata['release'] != args.release or client.digest(archive) != metadata['sha256']:
        raise ValueError('Laravel artifact integrity check failed')
    if args.action == 'fetch':
        folder = ROOT / '.deploy/mobile-backups' / args.release
        folder.mkdir(parents=True, exist_ok=True, mode=0o700)
        remote_folder = f'{config["deploy_dir"]}/mobile/{args.release}'
        client.copy(config, f'{remote_folder}/state.json', folder / 'state.json', download=True)
        state = json.loads((folder / 'state.json').read_text())
        checksums = {**state.get('infra_backup_checksums', {}), **state.get('snapshot_checksums', {}),
                     **state.get('backup_checksums', {})}
        allowed = {'tasks-before.json', 'crontab-before.txt', 'panel-before.sqlite', 'task-2.sh', 'task-4.sh',
                   'preflight-database.sql.gz', 'preflight-pm2.json', 'laravel-before.tar.gz', 'node-env-before'}
        if not checksums or not set(checksums) <= allowed:
            raise ValueError('Unexpected coordinated backup manifest')
        for name, expected in checksums.items():
            if not (folder / name).is_file() or client.digest(folder / name) != expected:
                client.copy(config, f'{remote_folder}/{name}', folder / name, download=True)
            (folder / name).chmod(0o600)
            if client.digest(folder / name) != expected:
                raise ValueError('Downloaded coordinated backup checksum differs')
        print(f'Off-server coordinated backups verified: {folder}')
        return
    if args.action == 'stage':
        client.remote(config, 'stage', release=args.release)
        client.copy(config, archive, f'{config["deploy_dir"]}/incoming/{args.release}-laravel.tar')
    payload = base64.b64encode(json.dumps({'action': args.action, 'config': config, 'metadata': metadata}).encode()).decode()
    command = f'python3 - {payload}'
    worker = (ROOT / 'scripts/mobile_cutover_remote.py').read_text()
    subprocess.run(['ssh', '-p', str(config['port']), *client.ssh_options(config),
                    f'{config["user"]}@{config["host"]}', command], input=worker, text=True, check=True)


if __name__ == '__main__':
    try:
        main()
    except (ValueError, OSError, KeyError, subprocess.CalledProcessError) as error:
        print(f'Mobile workflow stopped: {type(error).__name__}', flush=True)
        raise SystemExit(1)
