# SPDX-License-Identifier: BSD-2-Clause

# Copyright (c) 2025 Phil Thompson <phil@riverbankcomputing.com>


import base64
import glob
import hashlib
import os
import shutil
import sys

from ..exceptions import UserException
from ..pyproject import PyProject
from ..version import SIP_VERSION_STR


# The wheel format defined in PEP 427.
WHEEL_VERSION = '1.0'


def distinfo(name, console_scripts, gui_scripts, sbom_files, generator,
        generator_version, inventory, metadata_overrides, prefix, project_root,
        requires_dists, wheel_tag):
    """ Create and populate a .dist-info directory from an inventory file. """

    if prefix is None:
        prefix = ''

    # Read the list of installed files.
    with open(inventory) as inventory_f:
        installed_lines = inventory_f.read().strip()
        installed = installed_lines.split('\n') if installed_lines else []

    # Get the pyproject.toml file.
    saved = os.getcwd()
    os.chdir(project_root)
    pyproject = PyProject()
    os.chdir(saved)

    # Get the metadata and update it from the command line.
    metadata = pyproject.get_metadata()

    if metadata_overrides is not None:
        for oride in metadata_overrides:
            parts = oride.split('=', maxsplit=1)
            or_name = parts[0].strip()
            or_value = parts[1].strip() if len(parts) == 2 else ''
            metadata[or_name] = or_value

    # Create the directory.
    create_distinfo(name, wheel_tag, installed, metadata, requires_dists,
            project_root, console_scripts, gui_scripts, sbom_files,
            prefix_dir=prefix, generator=generator,
            generator_version=generator_version)


def create_distinfo(distinfo_dir, wheel_tag, installed, metadata,
        requires_dists, project_root, console_scripts, gui_scripts, sbom_files,
        prefix_dir='', generator=None, generator_version=None):
    """ Create and populate a .dist-info directory. """

    if generator is None:
        generator = 'sipbuild'

    if generator_version is None:
        generator_version = SIP_VERSION_STR

    # The prefix directory corresponds to DESTDIR or INSTALL_ROOT.
    real_distinfo_dir = prefix_dir + distinfo_dir

    # Make sure we have an empty dist-info directory.  Handle exceptions as the
    # user may be trying something silly with a system directory.
    if os.path.exists(real_distinfo_dir):
        try:
            shutil.rmtree(real_distinfo_dir)
        except Exception as e:
            raise UserException(
                    "unable remove old dist-info directory '{}'".format(
                            real_distinfo_dir),
                    str(e))

    try:
        os.mkdir(real_distinfo_dir)
    except Exception as e:
        raise UserException(
                "unable create dist-info directory '{}'".format(
                        real_distinfo_dir),
                str(e))

    # Copy any license and sbom files.
    saved = os.getcwd()
    os.chdir(project_root)

    _install_files(metadata.get('license-file'), prefix_dir,
            os.path.join(distinfo_dir, 'licenses'), installed)
    _install_files(sbom_files, prefix_dir, os.path.join(distinfo_dir, 'sboms'),
            installed)

    os.chdir(saved)

    # Reproducable builds.
    installed.sort()

    if wheel_tag is None:
        # Create the INSTALLER file.
        installer_fn = os.path.join(distinfo_dir, 'INSTALLER')
        installed.append(installer_fn)

        with open(prefix_dir + installer_fn, 'w') as installer_f:
            print(generator, file=installer_f)
    else:
        # Define any entry points.
        if console_scripts or gui_scripts:
            eps_fn = os.path.join(distinfo_dir, 'entry_points.txt')
            installed.append(eps_fn)

            with open(prefix_dir + eps_fn, 'w') as eps_f:
                if console_scripts:
                    eps_f.write(
                            '[console_scripts]\n' + '\n'.join(
                                    console_scripts) + '\n')

                if gui_scripts:
                    eps_f.write(
                            '[gui_scripts]\n' + '\n'.join(gui_scripts) + '\n')

        # Create the WHEEL file.
        WHEEL = '''Wheel-Version: {}
Generator: {} {}
Root-Is-Purelib: false
Tag: {}
'''

        wheel_fn = os.path.join(distinfo_dir, 'WHEEL')
        installed.append(wheel_fn)

        with open(prefix_dir + wheel_fn, 'w') as wheel_f:
            wheel_f.write(
                    WHEEL.format(WHEEL_VERSION, generator, generator_version,
                            wheel_tag))

    # Create the METADATA file.
    metadata_fn = os.path.join(distinfo_dir, 'METADATA')
    write_metadata(metadata, requires_dists, metadata_fn, project_root,
            prefix_dir=prefix_dir)
    installed.append(metadata_fn)

    # Create the RECORD file.
    record_fn = os.path.join(distinfo_dir, 'RECORD')

    distinfo_path, distinfo_base = os.path.split(distinfo_dir)
    real_distinfo_path = os.path.normcase(prefix_dir + distinfo_path)

    with open(prefix_dir + record_fn, 'w') as record_f:
        for name in installed:
            real_name = prefix_dir + name
            if os.path.isdir(real_name):
                all_fns = []

                for root, dirs, files in os.walk(real_name):
                    # Reproducable builds.
                    dirs.sort()
                    files.sort()

                    for f in files:
                        all_fns.append(os.path.join(root, f))

                    if '__pycache__' in dirs:
                        dirs.remove('__pycache__')
            else:
                all_fns = [real_name]

            for fn in all_fns:
                norm_fn = os.path.normcase(fn)

                if norm_fn.startswith(real_distinfo_path):
                    fn_name = fn[len(real_distinfo_path) + 1:].replace('\\', '/')
                elif norm_fn.startswith(prefix_dir + sys.prefix):
                    fn_name = os.path.relpath(
                            fn, real_distinfo_path).replace('\\', '/')
                else:
                    fn_name = fn[len(prefix_dir):]

                fn_f = open(fn, 'rb')
                data = fn_f.read()
                fn_f.close()

                digest = base64.urlsafe_b64encode(
                        hashlib.sha256(data).digest()).rstrip(b'=').decode('ascii')

                record_f.write(
                        '{},sha256={},{}\n'.format(fn_name, digest, len(data)))

        record_f.write('{}/RECORD,,\n'.format(distinfo_base))


def write_metadata(metadata, requires_dists, metadata_fn, project_root,
        prefix_dir=''):
    """ Write the meta-data, with additional requirements to a file. """

    if requires_dists:
        rd = metadata.get('requires-dist', [])
        if isinstance(rd, str):
            rd = [rd]

        metadata['requires-dist'] = requires_dists + rd

    with open(prefix_dir + metadata_fn, 'w') as metadata_f:
        description = None

        # Do these first for cosmetic reasons.
        for name in ('metadata-version', 'name', 'version', 'requires-python'):
            _write_metadata_item(name, metadata.pop(name), metadata_f)

        for name, value in metadata.items():
            if name == 'description-file':
                description = value
            else:
                _write_metadata_item(name, value, metadata_f)

        if description is not None:
            metadata_f.write('\n')

            # The description file uses posix separators.
            description = description.replace('/', os.sep)

            with open(os.path.join(project_root, description)) as description_f:
                metadata_f.write(description_f.read())


def _install_files(files, prefix_dir, target_dir, installed):
    """ Install a list of files into a subdirectory of the .dist-info
        directory and update the list of installed files.
    """

    if files:
        for patt in files:
            for file in glob.glob(patt):
                file_fn = os.path.join(target_dir, file)
                installed.append(file_fn)

                real_dir = prefix_dir + os.path.dirname(file_fn)
                os.makedirs(real_dir, exist_ok=True)
                shutil.copy(file, real_dir)


def _write_metadata_item(name, value, metadata_f):
    """ Write a single metadata item. """

    if isinstance(value, str):
        value = [value]

    for v in value:
        metadata_f.write('{}: {}\n'.format(name.title(), v))
