#!/usr/bin/env python3
"""Standard-library downloader with SHA-256 checks and HTTP Range resume."""
import argparse
import hashlib
from pathlib import Path
import time
import urllib.parse
import urllib.request


def sha256(path):
    h = hashlib.sha256()
    with path.open('rb') as f:
        for block in iter(lambda: f.read(4 * 1024 * 1024), b''):
            h.update(block)
    return h.hexdigest()


def download(base, name, expected, output):
    dest = output / name
    if dest.exists():
        if sha256(dest) == expected:
            print(f'校验通过，跳过 {name}', flush=True)
            return
        raise RuntimeError(f'已有文件校验失败，请移走或删除后重试: {dest}')
    partial = dest.with_name(dest.name + '.part')
    url = urllib.parse.urljoin(base, urllib.parse.quote(name))
    # A fully transferred .part can remain after an interruption before rename.
    if partial.exists() and sha256(partial) == expected:
        partial.replace(dest)
        return
    for attempt in range(1, 6):
        offset = partial.stat().st_size if partial.exists() else 0
        headers = {'Range': f'bytes={offset}-'} if offset else {}
        try:
            with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) as response:
                append = bool(offset and response.status == 206)
                if append and not response.headers.get('Content-Range', '').startswith(f'bytes {offset}-'):
                    raise RuntimeError(f'服务器续传偏移不匹配: {name}')
                if response.status not in (200, 206):
                    raise RuntimeError(f'HTTP {response.status}: {name}')
                written = offset if append else 0
                last = time.monotonic()
                with partial.open('ab' if append else 'wb') as f:
                    while True:
                        block = response.read(4 * 1024 * 1024)
                        if not block:
                            break
                        f.write(block)
                        written += len(block)
                        if time.monotonic() - last > 5:
                            print(f'{name}: {written / 1e9:.2f} GB', flush=True)
                            last = time.monotonic()
            if sha256(partial) != expected:
                raise RuntimeError(f'校验失败: {partial}。删除此文件后重新下载。')
            partial.replace(dest)
            print(f'下载并校验完成 {name}', flush=True)
            return
        except (OSError, urllib.error.URLError) as exc:
            if attempt == 5:
                raise
            print(f'{name}: 网络中断，保留进度，准备重试 ({attempt}/5): {exc}', flush=True)
            time.sleep(min(attempt * 2, 10))


def main():
    parser = argparse.ArgumentParser(description='下载课堂数据，支持续传与SHA-256校验')
    parser.add_argument('--base-url', default='http://10.108.10.64:8080/')
    parser.add_argument('--output', type=Path, default=Path('classroom_iq'))
    args = parser.parse_args()
    base = args.base_url.rstrip('/') + '/'
    args.output.mkdir(parents=True, exist_ok=True)
    with urllib.request.urlopen(urllib.parse.urljoin(base, 'SHA256SUMS.txt'), timeout=30) as response:
        content = response.read().decode('utf-8')
    (args.output / 'SHA256SUMS.txt').write_text(content, encoding='utf-8')
    entries = []
    for line in content.splitlines():
        expected, name = line.split(maxsplit=1)
        name = name.strip()
        if len(expected) != 64 or Path(name).name != name or name in ('.', '..') or '/' in name or '\\' in name:
            raise ValueError('校验清单包含非法文件名或哈希')
        entries.append((name, expected))
    for name, expected in entries:
        download(base, name, expected, args.output)
    print('全部文件已下载并通过SHA-256校验。请按DOWNLOAD_GUIDE.md解压。')


if __name__ == '__main__':
    main()
