#!/usr/bin/env python3
"""Container-only, fixed-version downloads verified against official metadata."""
import hashlib
import json
import os
from pathlib import Path
import shutil
import sys
import tempfile
import urllib.request
import xml.etree.ElementTree as ET
import zipfile

SDK_REPOSITORY = 'https://dl.google.com/android/repository/'
SDK_METADATA = SDK_REPOSITORY + 'repository2-1.xml'
SDK_PACKAGE = 'cmdline-tools;12.0'
SDK_FILENAME = 'commandlinetools-linux-11076708_latest.zip'
SDK_SIZE = 153607504
SDK_SHA1 = 'd313adb7aedccf6cf0cfca51ec180f0059f5f8f8'
GRADLE_URL = 'https://services.gradle.org/distributions/gradle-5.1.1-all.zip'
GRADLE_SHA256 = '53b71812f18cdb2777e9f1b2a0f2038683907c90bdc406bc64d8b400e1fb2c3b'
SDK_COMPONENTS = {
    'platforms;android-30': ('platform-30_r03.zip', 52328361, 'e7c6280901dcfa511af098d67dd88c4dfcbc6ea2'),
    'build-tools;30.0.0': ('build-tools_r30-linux.zip', 68819159, '5bdda303b7aba4073dd2aeb8003b6b0b57098021'),
}


def fetch(url):
    with urllib.request.urlopen(url, timeout=120) as response:
        return response.read()


def download(url, target, algorithm, expected):
    checksum = hashlib.new(algorithm)
    sha256 = hashlib.sha256()
    size = 0
    with urllib.request.urlopen(url, timeout=120) as response, target.open('wb') as stream:
        while True:
            block = response.read(1024 * 1024)
            if not block:
                break
            stream.write(block)
            size += len(block)
            checksum.update(block)
            sha256.update(block)
    if checksum.hexdigest() != expected:
        raise RuntimeError(f'Checksum mismatch: {target.name}')
    return {'url': url, 'bytes': size, 'verification_algorithm': algorithm,
            'verification_checksum': expected, 'sha256': sha256.hexdigest()}


def extract(archive, destination):
    # Reject archive traversal rather than trusting any downloaded path.
    with zipfile.ZipFile(archive) as bundle:
        for info in bundle.infolist():
            relative = Path(info.filename)
            if relative.is_absolute() or '..' in relative.parts:
                raise ValueError('Unsafe archive member')
        bundle.extractall(destination)
        for info in bundle.infolist():
            mode = (info.external_attr >> 16) & 0o777
            if mode and not info.is_dir():
                (destination / info.filename).chmod(mode)


def main():
    if sys.platform != 'linux' or os.environ.get('ZHCT_DOCKER_BUILD') != '1':
        raise SystemExit('This bootstrap is container-only; use run.sh prepare after review.')
    metadata = fetch(SDK_METADATA)
    package_root = ET.fromstring(metadata)
    for package, pin in SDK_COMPONENTS.items():
        matches = [p for p in package_root.findall('remotePackage') if p.get('path') == package]
        if len(matches) != 1:
            raise RuntimeError('Fixed Android component unavailable: ' + package)
        archives = [a for a in matches[0].findall('archives/archive') if a.findtext('host-os') in (None, 'linux')]
        if len(archives) != 1:
            raise RuntimeError('Expected one supported component archive: ' + package)
        complete = archives[0].find('complete')
        actual = (complete.findtext('url'), int(complete.findtext('size')), complete.findtext('checksum'))
        if actual != pin:
            raise RuntimeError('Android component metadata changed; review first: ' + package)
    packages = [p for p in package_root.findall('remotePackage')
                if p.get('path') == SDK_PACKAGE]
    if len(packages) != 1:
        raise RuntimeError('Fixed SDK package unavailable in official metadata; stop, do not upgrade')
    candidates = [a for a in packages[0].findall('archives/archive')
                  if a.findtext('host-os') == 'linux']
    if len(candidates) != 1:
        raise RuntimeError('Expected exactly one Linux archive')
    item = candidates[0].find('complete')
    observed = (item.findtext('url'), int(item.findtext('size')), item.findtext('checksum'))
    if observed != (SDK_FILENAME, SDK_SIZE, SDK_SHA1):
        raise RuntimeError('Official SDK metadata changed; review before replacing the pin')
    if fetch(GRADLE_URL + '.sha256').decode().strip() != GRADLE_SHA256:
        raise RuntimeError('Official Gradle checksum changed; review before replacing the pin')
    with tempfile.TemporaryDirectory(prefix='android-toolchain-') as temporary:
        work = Path(temporary)
        sdk_zip = work / SDK_FILENAME
        sdk_receipt = download(SDK_REPOSITORY + SDK_FILENAME, sdk_zip, 'sha1', SDK_SHA1)
        if sdk_receipt['bytes'] != SDK_SIZE:
            raise RuntimeError('SDK archive size differs from official metadata')
        unpacked = work / 'sdk'
        extract(sdk_zip, unpacked)
        cmdline = Path('/opt/android-sdk/cmdline-tools')
        cmdline.mkdir(parents=True, exist_ok=True)
        shutil.move(str(unpacked / 'cmdline-tools'), str(cmdline / '12.0'))
        gradle_zip = work / 'gradle-5.1.1-all.zip'
        gradle_receipt = download(GRADLE_URL, gradle_zip, 'sha256', GRADLE_SHA256)
        extract(gradle_zip, Path('/opt/gradle'))
    Path('/opt/toolchain-receipt.json').write_text(json.dumps({
        'android_metadata_url': SDK_METADATA,
        'android_metadata_sha256': hashlib.sha256(metadata).hexdigest(),
        'cmdline_tools_package': SDK_PACKAGE,
        'cmdline_tools': sdk_receipt,
        'gradle': gradle_receipt,
        'sdkmanager_packages': ['platforms;android-30', 'build-tools;30.0.0'],
        'sdkmanager_component_pins': {key: {'filename': pin[0], 'bytes': pin[1], 'sha1': pin[2]}
                                      for key, pin in SDK_COMPONENTS.items()},
        'boundary': 'No Android platform-tools, emulator, system image or host JDK installed.'
    }, indent=2) + '\n')


if __name__ == '__main__':
    main()
