#!/usr/bin/env python3
"""Create a source-only Neworia release; optionally upload/deploy with a personal API key."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import sys
import time
import urllib.error
import urllib.request
import zipfile

BASE = 'https://deploy.neworia.net'
LIMIT = 32 * 1024 * 1024


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def request(path, token, method='GET', payload=None, archive=None):
    headers = {'Authorization': 'Bearer ' + token, 'Accept': 'application/json'}
    if archive is not None:
        body = archive.read_bytes()
        headers.update({'Content-Type': 'application/zip', 'X-Filename': 'release.zip'})
    elif payload is not None:
        body = json.dumps(payload).encode()
        headers['Content-Type'] = 'application/json'
    else:
        body = None
    req = urllib.request.Request(BASE + path, data=body, headers=headers, method=method)
    try:
        with urllib.request.build_opener(NoRedirect()).open(req, timeout=120) as response:
            return json.load(response)
    except urllib.error.HTTPError as exc:
        # Never print request bodies, headers, credential values or arbitrary server output.
        raise SystemExit('Panel request failed (HTTP %s).' % exc.code) from None
    except (OSError, ValueError):
        raise SystemExit('Panel request failed; check connectivity and the personal API key.') from None


def pack(root, output):
    root = root.resolve(strict=True)
    files = [root / 'package.json', root / 'package-lock.json']
    for directory in ('src', 'scripts', 'test'):
        start = root / directory
        if start.is_symlink():
            raise SystemExit('Symbolic links are not allowed: ' + directory)
        if start.exists():
            for item in start.rglob('*'):
                if item.is_symlink():
                    raise SystemExit('Symbolic links are not allowed.')
                if item.is_file():
                    files.append(item)
    if not (root / 'src/index.js').is_file():
        raise SystemExit('Missing src/index.js.')
    total = 0
    for item in files:
        relative = item.relative_to(root)
        if item.is_symlink() or any(p.startswith('.') or p in ('node_modules', '__pycache__') for p in relative.parts):
            raise SystemExit('Hidden files, dependencies and symbolic links must not be included.')
        total += item.stat().st_size
    if total > 128 * 1024 * 1024 or len(files) > 3000:
        raise SystemExit('Source is too large.')
    output = output.resolve()
    if output.exists():
        raise SystemExit('Output already exists; choose a new --out path.')
    with zipfile.ZipFile(output, 'x', compression=zipfile.ZIP_DEFLATED) as archive:
        for item in sorted(files):
            archive.write(item, item.relative_to(root).as_posix())
    if output.stat().st_size > LIMIT:
        output.unlink()
        raise SystemExit('ZIP exceeds 32 MiB.')
    print('Created:', output)
    print('SHA256:', hashlib.sha256(output.read_bytes()).hexdigest())


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('source', type=Path, help='Local checked-out bot source directory')
    parser.add_argument('--out', type=Path, default=Path('neworia-release.zip'))
    parser.add_argument('--upload', action='store_true', help='Upload the ZIP without activation')
    parser.add_argument('--deploy', action='store_true', help='Upload, deploy live, and wait for the result')
    args = parser.parse_args()
    pack(args.source, args.out)
    if not (args.upload or args.deploy):
        return
    token = os.environ.get('NEWORIA_DEPLOY_TOKEN', '')
    if not token or any(c.isspace() for c in token):
        raise SystemExit('Set NEWORIA_DEPLOY_TOKEN to your personal panel API key first.')
    release_response = request('/api/releases', token, 'POST', archive=args.out)
    release = release_response.get('release', release_response)
    release_id = release.get('id')
    if not isinstance(release_id, str) or len(release_id) != 32 or any(c not in '0123456789abcdef' for c in release_id):
        raise SystemExit('Unexpected upload response; inspect the release in the panel.')
    print('Uploaded release:', release_id)
    if not args.deploy:
        return
    job_response = request('/api/jobs', token, 'POST', payload={'kind': 'deploy', 'release_id': release_id})
    job = job_response.get('job', job_response)
    job_id = job.get('id')
    if not isinstance(job_id, str) or len(job_id) != 32 or any(c not in '0123456789abcdef' for c in job_id):
        raise SystemExit('Unexpected job response; inspect the panel before retrying.')
    print('Deployment job:', job_id)
    previous = None
    for _ in range(240):
        result = request('/api/jobs/' + job_id, token)
        job = result.get('job', result)
        status = job.get('status')
        if status not in ('queued', 'running', 'succeeded', 'failed'):
            raise SystemExit('Unexpected status; inspect the panel before retrying.')
        if status != previous:
            print('Status:', status, flush=True)
            previous = status
        if status == 'succeeded':
            return
        if status == 'failed':
            raise SystemExit('Deployment failed. Inspect the job in the panel.')
        time.sleep(5)
    raise SystemExit('Still processing. Inspect the panel; do not submit a duplicate job.')


if __name__ == '__main__':
    main()
