#!/usr/bin/env python3
from __future__ import annotations

import contextlib
import io
import json
import hashlib
import pathlib
import urllib.request
import zipfile


TEMPLATE = '''\
# This file is generated by devscripts/update_ejs.py. DO NOT MODIFY!

VERSION = {version!r}
HASHES = {{
{hash_mapping}
}}
'''
PREFIX = '    "yt-dlp-ejs=='
BASE_PATH = pathlib.Path(__file__).parent.parent
PYPROJECT_PATH = BASE_PATH / 'pyproject.toml'
PACKAGE_PATH = BASE_PATH / 'yt_dlp/extractor/youtube/jsc/_builtin/vendor'
RELEASE_URL = 'https://api.github.com/repos/yt-dlp/ejs/releases/latest'
ASSETS = {
    'yt.solver.lib.js': False,
    'yt.solver.lib.min.js': False,
    'yt.solver.deno.lib.js': True,
    'yt.solver.bun.lib.js': True,
    'yt.solver.core.min.js': False,
    'yt.solver.core.js': True,
}
MAKEFILE_PATH = BASE_PATH / 'Makefile'


def request(url: str):
    return contextlib.closing(urllib.request.urlopen(url))


def makefile_variables(
        version: str | None = None,
        name: str | None = None,
        digest: str | None = None,
        data: bytes | None = None,
        keys_only: bool = False,
) -> dict[str, str | None]:
    assert keys_only or all(arg is not None for arg in (version, name, digest, data))

    return {
        'EJS_VERSION': None if keys_only else version,
        'EJS_WHEEL_NAME': None if keys_only else name,
        'EJS_WHEEL_HASH': None if keys_only else digest,
        'EJS_PY_FOLDERS': None if keys_only else list_wheel_contents(data, 'py', files=False),
        'EJS_PY_FILES': None if keys_only else list_wheel_contents(data, 'py', folders=False),
        'EJS_JS_FOLDERS': None if keys_only else list_wheel_contents(data, 'js', files=False),
        'EJS_JS_FILES': None if keys_only else list_wheel_contents(data, 'js', folders=False),
    }


def list_wheel_contents(
        wheel_data: bytes,
        suffix: str | None = None,
        folders: bool = True,
        files: bool = True,
) -> str:
    assert folders or files, 'at least one of "folders" or "files" must be True'

    with zipfile.ZipFile(io.BytesIO(wheel_data)) as zipf:
        path_gen = (zinfo.filename for zinfo in zipf.infolist())

    filtered = filter(lambda path: path.startswith('yt_dlp_ejs/'), path_gen)
    if suffix:
        filtered = filter(lambda path: path.endswith(f'.{suffix}'), filtered)

    files_list = list(filtered)
    if not folders:
        return ' '.join(files_list)

    folders_list = list(dict.fromkeys(path.rpartition('/')[0] for path in files_list))
    if not files:
        return ' '.join(folders_list)

    return ' '.join(folders_list + files_list)


def main():
    current_version = None
    with PYPROJECT_PATH.open() as file:
        for line in file:
            if not line.startswith(PREFIX):
                continue
            current_version, _, _ = line.removeprefix(PREFIX).partition('"')

    if not current_version:
        print('yt-dlp-ejs dependency line could not be found')
        return

    makefile_info = makefile_variables(keys_only=True)
    prefixes = tuple(f'{key} = ' for key in makefile_info)
    with MAKEFILE_PATH.open() as file:
        for line in file:
            if not line.startswith(prefixes):
                continue
            key, _, val = line.partition(' = ')
            makefile_info[key] = val.rstrip()

    with request(RELEASE_URL) as resp:
        info = json.load(resp)

    version = info['tag_name']
    if version == current_version:
        print(f'yt-dlp-ejs is up to date! ({version})')
        return

    print(f'Updating yt-dlp-ejs from {current_version} to {version}')
    hashes = []
    wheel_info = {}
    for asset in info['assets']:
        name = asset['name']
        is_wheel = name.startswith('yt_dlp_ejs-') and name.endswith('.whl')
        if not is_wheel and name not in ASSETS:
            continue
        with request(asset['browser_download_url']) as resp:
            data = resp.read()

        # verify digest from github
        digest = asset['digest']
        algo, _, expected = digest.partition(':')
        hexdigest = hashlib.new(algo, data).hexdigest()
        assert hexdigest == expected, f'downloaded attest mismatch ({hexdigest!r} != {expected!r})'

        if is_wheel:
            wheel_info = makefile_variables(version, name, digest, data)
            continue

        # calculate sha3-512 digest
        asset_hash = hashlib.sha3_512(data).hexdigest()
        hashes.append(f'    {name!r}: {asset_hash!r},')

        if ASSETS[name]:
            (PACKAGE_PATH / name).write_bytes(data)

    hash_mapping = '\n'.join(hashes)
    for asset_name in ASSETS:
        assert asset_name in hash_mapping, f'{asset_name} not found in release'

    assert all(wheel_info.get(key) for key in makefile_info), 'wheel info not found in release'

    (PACKAGE_PATH / '_info.py').write_text(TEMPLATE.format(
        version=version,
        hash_mapping=hash_mapping,
    ))

    content = PYPROJECT_PATH.read_text()
    updated = content.replace(PREFIX + current_version, PREFIX + version)
    PYPROJECT_PATH.write_text(updated)

    makefile = MAKEFILE_PATH.read_text()
    for key in wheel_info:
        makefile = makefile.replace(f'{key} = {makefile_info[key]}', f'{key} = {wheel_info[key]}')
    MAKEFILE_PATH.write_text(makefile)


if __name__ == '__main__':
    main()
