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]
            for filename in 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 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()
