# Based on https://github.com/ziglang/zig-pypi/blob/de14cf728fa35c014821f62a4fa9abd9f4bb560e/make_wheels.py
# MIT

from __future__ import annotations

import sys
from email.message import EmailMessage
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipInfo

from wheel.wheelfile import WheelFile


class ReproducibleWheelFile(WheelFile):
    def writestr(self, zinfo, *args, **kwargs):
        if not isinstance(zinfo, ZipInfo):
            raise ValueError("ZipInfo required")
        zinfo.date_time = (1980, 1, 1, 0, 0, 0)
        zinfo.create_system = 3
        super().writestr(zinfo, *args, **kwargs)


def make_message(headers, payload=None):
    msg = EmailMessage()
    for name, value in headers.items():
        if name == "_dependencies":
            for dep in value:
                if isinstance(dep, ExtraRequires):
                    msg["Provides-Extra"] = dep.name
                    for inner_dep in dep.deps:
                        msg["Requires-Dist"] = f"{inner_dep}; extra == '{dep.name}'"
                else:
                    msg["Requires-Dist"] = dep
        elif isinstance(value, list):
            for value_part in value:
                msg[name] = value_part
        else:
            msg[name] = value
    if payload:
        msg.set_payload(payload)
    return msg


def write_wheel_file(filename, contents):
    with ReproducibleWheelFile(filename, "w") as wheel:
        for member_info, member_source in contents.items():
            if not isinstance(member_info, ZipInfo):
                member_info = ZipInfo(member_info)
                member_info.external_attr = 0o644 << 16
            member_info.file_size = len(member_source)
            member_info.compress_type = ZIP_DEFLATED
            wheel.writestr(member_info, bytes(member_source))
    return filename


def write_wheel(
    wheel_path,
    *,
    name,
    version,
    tag,
    metadata,
    description,
    contents,
    entrypoints: list[str] | None = None,
    top_level: list[str] | None = None,
):
    dist_info = f"{name}-{version}.dist-info"
    extra = {}
    if entrypoints:
        entrypoints_joined = "\n".join(entrypoints)
        text = f"[console_scripts]\n{entrypoints_joined}"
        file = f"{dist_info}/entry_points.txt"
        extra[file] = text.encode("utf8")
    if top_level:
        top_level_joined = "\n".join(top_level) + "\n"
        file = f"{dist_info}/top_level.txt"
        extra[file] = top_level_joined.encode("utf8")
    return write_wheel_file(
        wheel_path,
        {
            **contents,
            **extra,
            f"{dist_info}/METADATA": make_message(
                {
                    "Metadata-Version": "2.1",
                    "Name": name,
                    "Version": version,
                    **metadata,
                },
                description,
            ),
            f"{dist_info}/WHEEL": make_message(
                {
                    "Wheel-Version": "1.0",
                    "Generator": "anki write_wheel.py",
                    "Root-Is-Purelib": "false",
                    "Tag": tag,
                }
            ),
        },
    )


def merge_sources(contents, root, exclude):
    root = Path(root)
    for path in root.glob("**/*"):
        if path.is_dir() or exclude(path):
            continue
        path_str = str(path.relative_to(root.parent))
        if path_str.endswith(".pyc"):
            continue
        contents[path_str] = path.read_bytes()


def split_wheel_path(path: str):
    path2 = Path(path)
    components = path2.stem.split("-", maxsplit=2)
    return components


class ExtraRequires:
    def __init__(self, name, deps):
        self.name = name
        self.deps = deps


src_root = sys.argv[1]
generated_root = sys.argv[2]
wheel_path = sys.argv[3]

name, version, tag = split_wheel_path(wheel_path)


def exclude_aqt(path: Path) -> bool:
    if path.suffix in [".ui", ".scss", ".map", ".ts"]:
        return True
    if path.name.startswith("tsconfig"):
        return True
    if "/aqt/data" in str(path):
        return True
    return False


def exclude_nothing(path: Path) -> bool:
    return False


def extract_requirements(path: Path) -> list[str]:
    return path.read_text().splitlines()


if name == "aqt":
    exclude = exclude_aqt
else:
    exclude = exclude_nothing

contents: dict[str, str] = {}
merge_sources(contents, src_root, exclude)
merge_sources(contents, generated_root, exclude)

if name == "anki":
    all_requires = extract_requirements(Path("python/requirements.anki.in")) + [
        ExtraRequires("syncserver", ["flask", "waitress"]),
    ]
    entrypoints = None
    top_level = None
else:
    all_requires = extract_requirements(Path("python/requirements.aqt.in")) + [
        "anki==" + version,
        ExtraRequires(
            "qt5",
            [
                "pyqt5>=5.14",
                "pyqtwebengine",
            ],
        ),
        ExtraRequires(
            "qt6",
            [
                "pyqt6>=6.2",
                "pyqt6-webengine>=6.2",
            ],
        ),
    ]
    entrypoints = ["anki = aqt:run"]
    top_level = ["aqt", "_aqt"]

write_wheel(
    wheel_path,
    name=name,
    version=version,
    tag=tag,
    metadata={
        "License": "AGPL-3",
        "Classifier": [
            "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
        ],
        "Requires-Python": ">=3.9",
        "_dependencies": all_requires,
    },
    description="Please see https://apps.ankiweb.net\n\n",
    contents=contents,
    entrypoints=entrypoints,
    top_level=top_level,
)
