#!/usr/bin/env python3
"""Download a release pinned by the public build and install it on its owner's Pi."""
import argparse
import hashlib
import ipaddress
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, build_opener
import zipfile

PACKAGE_URL = 'https://tonypitastic.netlify.app/downloads/tonypitastic-bridge.zip'
PACKAGE_SHA256 = '406c03a08e97ed45ebfa1266b4e240635c9f9fec2e0014e32d2097d8f6022300'
ALLOWED_DOWNLOAD_ORIGINS = {'https://tonypitastic.netlify.app', 'https://tonypitastic.com', 'https://www.tonypitastic.com'}

def validate_download_url(value):
    try:
        target = urlsplit(value)
        port = target.port
    except ValueError:
        raise SystemExit('Unexpected package download destination') from None
    origin = 'https://' + (target.hostname or '').lower()
    if (target.scheme != 'https' or target.username or target.password or port not in (None, 443)
            or origin not in ALLOWED_DOWNLOAD_ORIGINS or target.path != '/downloads/tonypitastic-bridge.zip'
            or target.query or target.fragment):
        raise SystemExit('Unexpected package download destination')
    return value

class ReleaseRedirectHandler(HTTPRedirectHandler):
    def redirect_request(self, request, file, code, message, headers, new_url):
        # Check before urllib can send a request to an unapproved destination.
        validate_download_url(new_url)
        return super().redirect_request(request, file, code, message, headers, new_url)

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--host', required=True, help='Your Pi\'s current LAN IPv4 address')
    parser.add_argument('--port', type=int, default=8765)
    parser.add_argument('--activate-hardware', action='store_true')
    parser.add_argument('--no-start', action='store_true')
    parser.add_argument('--dry-run', action='store_true')
    args = parser.parse_args()
    host = ipaddress.IPv4Address(args.host)
    if host.is_unspecified or host.is_multicast or host.is_reserved or not 1024 <= args.port <= 65535:
        raise SystemExit('Use this Pi\'s specific IPv4 address and a port from 1024 to 65535')
    if not re.fullmatch(r'[0-9a-f]{64}', PACKAGE_SHA256):
        raise SystemExit('This source template must be pinned by the public release builder')
    validate_download_url(PACKAGE_URL)
    with tempfile.TemporaryDirectory(prefix='tonypitastic-install-') as folder:
        package = Path(folder) / 'tonypitastic-bridge.zip'
        digest = hashlib.sha256()
        total = 0
        opener = build_opener(ReleaseRedirectHandler())
        with opener.open(PACKAGE_URL, timeout=30) as response, package.open('wb') as output:
            validate_download_url(response.geturl())
            while True:
                raw = response.read(65536)
                if not raw:
                    break
                total += len(raw)
                if total > 32_000_000:
                    raise SystemExit('Package exceeds the release size limit')
                output.write(raw)
                digest.update(raw)
        if digest.hexdigest() != PACKAGE_SHA256:
            raise SystemExit('Release checksum mismatch; no installer was executed. Download the current installer again.')
        with zipfile.ZipFile(package) as archive:
            entries = [entry for entry in archive.infolist() if entry.filename == 'tonypi-web/install_pi.py']
            if len(entries) != 1 or entries[0].file_size > 200_000:
                raise SystemExit('Invalid installer entry')
            source = archive.read(entries[0])
        installer = Path(folder) / 'install_pi.py'
        installer.write_bytes(source)
        command = [sys.executable, str(installer), '--archive', str(package), '--sha256', PACKAGE_SHA256, '--host', str(host), '--port', str(args.port)]
        for option in ('activate_hardware', 'no_start', 'dry_run'):
            if getattr(args, option):
                command.append('--' + option.replace('_', '-'))
        return subprocess.run(command, check=False).returncode

if __name__ == '__main__':
    raise SystemExit(main())
