# SPDX-License-Identifier: LGPL-2.1-or-later
import argparse
import base64
import gzip
import hashlib
import lzma
import os
import shutil
import subprocess
import tarfile
import zipfile
from typing import AnyStr, Dict, Iterable, Optional

from cockpit import __version__

VERSION = __version__ or '0'
PACKAGE = f'cockpit-{VERSION}'
TAG = 'py3-none-any'


def find_sources(*, srcpkg: bool) -> Iterable[str]:
    try:
        subprocess.check_call(['vendor/checkout'], stdout=2)       # Needed for git builds...
    except FileNotFoundError:                                       # ...but not present in tarball...
        pass                                                        # ...and not needed either, because...
    assert os.path.exists('src/cockpit/_vendor/ferny/__init__.py')  # ...the code should exist there already.

    if srcpkg:
        yield from (
            'pyproject.toml',
            'src/build_backend.py',
        )

    for path, _dirs, files in os.walk('src', followlinks=True):
        if '__init__.py' in files:
            yield from [os.path.join(path, file) for file in files]


def copy_sources(distdir: str) -> None:
    for source in find_sources(srcpkg=True):
        destination = os.path.join(distdir, source)
        os.makedirs(os.path.dirname(destination), exist_ok=True)
        shutil.copy(source, destination)


def build_sdist(sdist_directory: str,
                config_settings: Optional[Dict[str, object]] = None) -> str:
    del config_settings
    sdist_filename = f'{PACKAGE}.tar.gz'
    # We do this manually to avoid adding timestamps.  See https://bugs.python.org/issue31526
    with gzip.GzipFile(f'{sdist_directory}/{sdist_filename}', mode='w', mtime=0) as gz:
        # https://github.com/python/typeshed/issues/5491
        with tarfile.open(fileobj=gz, mode='w|', dereference=True) as sdist:  # type: ignore[arg-type,call-overload]
            for filename in sorted(find_sources(srcpkg=True)):
                sdist.add(filename, arcname=f'{PACKAGE}/{filename}', )
    return sdist_filename


def build_wheel(wheel_directory: str,
                config_settings: Optional[Dict[str, object]] = None,
                metadata_directory: Optional[str] = None) -> str:
    del config_settings, metadata_directory
    wheel_filename = f'{PACKAGE}-{TAG}.whl'
    distinfo = {
        'WHEEL': [
            'Wheel-Version: 1.0',
            'Generator: cockpit build_backend',
            'Root-Is-Purelib: true',
            f'Tag: {TAG}',
        ],
        'METADATA': [
            'Metadata-Version: 2.1',
            'Name: cockpit',
            f'Version: {VERSION}',
        ],
        'entry_points.txt': [
            '[console_scripts]',
            'cockpit-askpass = cockpit._vendor.ferny.interaction_client:main',
            'cockpit-bridge = cockpit.bridge:main',
        ],
    }

    with zipfile.ZipFile(f'{wheel_directory}/{wheel_filename}', 'w') as wheel:
        def write(filename: str, data: AnyStr) -> None:
            # we do this manually to avoid adding timestamps
            wheel.writestr(zipfile.ZipInfo(filename), data)

        def beipack_self(main: str, args: str = '') -> bytes:
            from cockpit._vendor.bei import beipack
            contents = {name: wheel.read(name) for name in wheel.namelist()}
            pack = beipack.pack(contents, main, args=args).encode()
            return lzma.compress(pack, preset=lzma.PRESET_EXTREME)

        def write_distinfo(filename: str, lines: Iterable[str]) -> None:
            write(f'{PACKAGE}.dist-info/{filename}', ''.join(f'{line}\n' for line in lines))

        def record_lines() -> Iterable[str]:
            for info in wheel.infolist():
                digest = hashlib.sha256(wheel.read(info.filename)).digest()
                b64_digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
                yield f'{info.filename},sha256={b64_digest},{info.file_size}'
            yield f'{PACKAGE}.dist-info/RECORD,,'

        for filename in sorted(find_sources(srcpkg=False)):
            with open(filename, 'rb') as file:
                write(os.path.relpath(filename, start='src'), file.read())

        write('cockpit/data/cockpit-bridge.beipack.xz', beipack_self('cockpit.bridge:main', 'beipack=True'))

        for filename, lines in distinfo.items():
            write_distinfo(filename, lines)
        write_distinfo('RECORD', record_lines())

    return wheel_filename


def main() -> None:
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--copy', action='store_true')
    group.add_argument('--sdist', action='store_true')
    group.add_argument('--wheel', action='store_true')
    parser.add_argument('srcdir')
    parser.add_argument('destdir')
    args = parser.parse_args()

    # We have to chdir() for PEP 517, so make sure dest is absolute
    destdir = os.path.abspath(args.destdir)
    os.chdir(args.srcdir)

    os.makedirs(destdir, exist_ok=True)

    if args.copy:
        copy_sources(destdir)
    elif args.sdist:
        print(os.path.join(destdir, build_sdist(destdir)))
    else:
        print(os.path.join(destdir, build_wheel(destdir)))


if __name__ == '__main__':
    main()
