import os
import sys

FILE = os.path.abspath(__file__)
HERE = os.path.dirname(FILE)
ENV_VAR_PREFIX = '_HATCHLING_PORT_ADD_'


def _apply_env_vars(kwargs):
    from ast import literal_eval

    for key, value in os.environ.items():
        if key.startswith(ENV_VAR_PREFIX):
            kwargs[key.replace(ENV_VAR_PREFIX, '', 1).lower()] = literal_eval(value)


def _parse_dependencies(dependency_definition):
    dependencies = []
    for line in dependency_definition.splitlines():
        dependency = line.split(' #', 1)[0].strip()
        if dependency:
            dependencies.append(dependency)

    return dependencies


def _collapse_data(output, data):
    import tomli_w

    expected_output = new_output = tomli_w.dumps(data)
    new_output = new_output.replace('    ', '')
    new_output = new_output.replace('[\n', '[')
    new_output = new_output.replace('",\n]', '"]')

    return output.replace(expected_output, new_output, 1)


def _parse_setup_cfg(kwargs):
    from configparser import ConfigParser

    setup_cfg_file = os.path.join(HERE, 'setup.cfg')
    if not os.path.isfile(setup_cfg_file):
        return

    setup_cfg = ConfigParser()
    setup_cfg.read(setup_cfg_file, encoding='utf-8')

    if not setup_cfg.has_section('metadata'):
        return

    metadata = setup_cfg['metadata']

    if 'name' in metadata and 'name' not in kwargs:
        kwargs['name'] = metadata['name']

    if 'description' in metadata and 'description' not in kwargs:
        kwargs['description'] = metadata['description']

    if 'license' in metadata and 'license' not in kwargs:
        kwargs['license'] = metadata['license']

    if 'author' in metadata and 'author' not in kwargs:
        kwargs['author'] = metadata['author']

    if 'author_email' in metadata and 'author_email' not in kwargs:
        kwargs['author_email'] = metadata['author_email']

    if 'keywords' in metadata and 'keywords' not in kwargs:
        keywords = metadata['keywords'].strip().splitlines()
        kwargs['keywords'] = keywords if len(keywords) > 1 else keywords[0]

    if 'classifiers' in metadata and 'classifiers' not in kwargs:
        kwargs['classifiers'] = metadata['classifiers'].strip().splitlines()

    if 'url' in metadata and 'url' not in kwargs:
        kwargs['url'] = metadata['url']

    if 'download_url' in metadata and 'download_url' not in kwargs:
        kwargs['download_url'] = metadata['download_url']

    if 'project_urls' in metadata and 'project_urls' not in kwargs:
        kwargs['project_urls'] = dict(
            url.replace(' = ', '=', 1).split('=') for url in metadata['project_urls'].strip().splitlines()
        )

    if setup_cfg.has_section('options'):
        options = setup_cfg['options']

        if 'python_requires' in options and 'python_requires' not in kwargs:
            kwargs['python_requires'] = options['python_requires']

        if 'install_requires' in options and 'install_requires' not in kwargs:
            kwargs['install_requires'] = _parse_dependencies(options['install_requires'])

        if 'packages' in options and 'packages' not in kwargs:
            packages = []
            for package_spec in options['packages'].strip().splitlines():
                package = package_spec.replace('find:', '', 1).replace('find_namespace:', '', 1).strip()
                if package:
                    packages.append(package)

            if packages:
                kwargs['packages'] = packages

        if 'package_dir' in options and 'package_dir' not in kwargs:
            kwargs['package_dir'] = dict(
                d.replace(' = ', '=', 1).split('=') for d in options['package_dir'].strip().splitlines()
            )

    if setup_cfg.has_section('options.extras_require') and 'extras_require' not in kwargs:
        kwargs['extras_require'] = {
            feature: _parse_dependencies(dependencies)
            for feature, dependencies in setup_cfg['options.extras_require'].items()
        }

    if setup_cfg.has_section('options.entry_points') and 'entry_points' not in kwargs:
        kwargs['entry_points'] = {
            entry_point: definitions.strip().splitlines()
            for entry_point, definitions in setup_cfg['options.entry_points'].items()
        }


def setup(**kwargs):
    import itertools
    import re

    import tomli_w

    project_metadata = {}
    hatch_metadata = {}
    tool_metadata = {'hatch': hatch_metadata}
    project_data = {
        'build-system': {'requires': ['hatchling'], 'build-backend': 'hatchling.build'},
        'project': project_metadata,
        'tool': tool_metadata,
    }

    _parse_setup_cfg(kwargs)
    _apply_env_vars(kwargs)

    name = kwargs['name']
    project_name = name.replace('_', '-')
    packages = sorted(kwargs.get('packages') or [name.replace('-', '_')])
    package_name = package_path = package_source = packages[0].split('.')[0].lower().strip()

    project_metadata['name'] = project_name

    project_metadata['dynamic'] = ['version']

    if 'description' in kwargs:
        project_metadata['description'] = kwargs['description']

    for readme_file in ('README.md', 'README.rst', 'README.txt'):
        if os.path.isfile(os.path.join(HERE, readme_file)):
            project_metadata['readme'] = readme_file
            break

    project_metadata['license'] = kwargs.get('license', '')

    if 'python_requires' in kwargs:
        project_metadata['requires-python'] = kwargs['python_requires']

    for collaborator in ('author', 'maintainer'):
        collaborators = []
        collaborator_names = []
        collaborator_emails = []
        if collaborator in kwargs:
            collaborator_names.extend(
                collaborator_name.strip() for collaborator_name in kwargs[collaborator].split(',')
            )
        if f'{collaborator}_email' in kwargs:
            collaborator_emails.extend(
                collaborator_email.strip() for collaborator_email in kwargs[f'{collaborator}_email'].split(',')
            )

        for collaborator_name, collaborator_email in itertools.zip_longest(collaborator_names, collaborator_emails):
            data = {}
            if collaborator_name is not None:
                data['name'] = collaborator_name
            if collaborator_email is not None:
                data['email'] = collaborator_email
            if data:
                collaborators.append(data)

        if collaborators:
            project_metadata[f'{collaborator}s'] = collaborators

    if 'keywords' in kwargs:
        keywords = kwargs['keywords']
        if isinstance(keywords, str):
            keywords = keywords.replace(',', ' ').split()
        project_metadata['keywords'] = sorted(keywords)

    if 'classifiers' in kwargs:
        project_metadata['classifiers'] = sorted(kwargs['classifiers'])
        fixed_indices = []
        final_index = 0
        for i, classifier in enumerate(project_metadata['classifiers']):
            if classifier.startswith('Programming Language :: Python :: '):
                final_index = i
                for python_version in ('3.10', '3.11', '3.12'):
                    if classifier.endswith(python_version):
                        fixed_indices.append(i)
                        break
        for i, index in enumerate(fixed_indices):
            project_metadata['classifiers'].insert(final_index, project_metadata['classifiers'].pop(index - i))

    if 'install_requires' in kwargs:
        project_metadata['dependencies'] = sorted(
            [entry.strip() for entry in kwargs['install_requires']]
            if isinstance(kwargs['install_requires'], (list, tuple))
            else _parse_dependencies(kwargs['install_requires']),
            key=lambda d: d.lower(),
        )

    if 'extras_require' in kwargs:
        project_metadata['optional-dependencies'] = {
            group: sorted(
                [entry.strip() for entry in dependencies]
                if isinstance(dependencies, (list, tuple))
                else _parse_dependencies(dependencies),
                key=lambda d: d.lower(),
            )
            for group, dependencies in sorted(kwargs['extras_require'].items())
        }

    if 'entry_points' in kwargs and isinstance(kwargs['entry_points'], dict):
        entry_points = {}
        for entry_point, raw_definitions in kwargs['entry_points'].items():
            definitions = [raw_definitions] if isinstance(raw_definitions, str) else raw_definitions
            definitions = dict(sorted(d.replace(' ', '').split('=', 1) for d in definitions))

            if entry_point == 'console_scripts':
                project_metadata['scripts'] = definitions
            elif entry_point == 'gui_scripts':
                project_metadata['gui-scripts'] = definitions
            else:
                entry_points[entry_point] = definitions
        if entry_points:
            project_metadata['entry-points'] = dict(sorted(entry_points.items()))

    urls = {}
    if 'url' in kwargs:
        urls['Homepage'] = kwargs['url']
    if 'download_url' in kwargs:
        urls['Download'] = kwargs['download_url']
    if 'project_urls' in kwargs:
        urls.update(kwargs['project_urls'])
    if urls:
        project_metadata['urls'] = dict(sorted(urls.items()))

    build_targets = {}
    build_data = {}

    if 'use_scm_version' in kwargs:
        project_data['build-system']['requires'].append('hatch-vcs')
        hatch_metadata['version'] = {'source': 'vcs'}
        build_data['hooks'] = {'vcs': {'version-file': f'{package_path}/_version.py'}}
    else:
        hatch_metadata['version'] = {'path': f'{package_path}/__init__.py'}

    build_data['targets'] = build_targets

    if '' in kwargs.get('package_dir', {}):
        package_source = kwargs['package_dir']['']

        package = (kwargs.get('packages') or [package_name])[0]
        package_path = f'{package_source}/{package}'

        if package_path != f'src/{package_name}':
            build_targets.setdefault('wheel', {})['packages'] = [package_path]

    if kwargs.get('data_files', []):
        shared_data = {}
        for shared_directory, relative_paths in kwargs['data_files']:
            relative_files = {}
            for relative_path in relative_paths:
                relative_directory, filename = os.path.split(relative_path)
                relative_files.setdefault(relative_directory, []).append(filename)

            for relative_directory, files in sorted(relative_files.items()):
                if not os.path.isdir(relative_directory) or set(os.listdir(relative_directory)) != set(files):
                    for filename in sorted(files):
                        local_path = os.path.join(relative_directory, filename).replace('\\', '/')
                        shared_data[local_path] = f'{shared_directory}/{filename}'
                else:
                    shared_data[relative_directory] = shared_directory

        build_targets.setdefault('wheel', {})['shared-data'] = shared_data

    build_targets['sdist'] = {
        'include': [
            f'/{package_source}',
        ]
    }

    hatch_metadata['build'] = build_data

    output = tomli_w.dumps(project_data)
    output = _collapse_data(output, {'requires': project_data['build-system']['requires']})
    output = _collapse_data(output, {'dynamic': project_data['project']['dynamic']})

    project_file = os.path.join(HERE, 'pyproject.toml')
    if os.path.isfile(project_file):
        with open(project_file, encoding='utf-8') as f:
            current_contents = f.read()

        for section in ('build-system', 'project'):
            for pattern in (rf'^\[{section}].*?(?=^\[)', rf'^\[{section}].*'):
                current_contents = re.sub(pattern, '', current_contents, flags=re.MULTILINE | re.DOTALL)

        output += f'\n{current_contents}'

    with open(project_file, 'w', encoding='utf-8') as f:
        f.write(output)


if __name__ == 'setuptools':
    __this_shim = sys.modules.pop('setuptools')
    __current_directory = sys.path.pop(0)

    import setuptools as __real_setuptools

    sys.path.insert(0, __current_directory)
    sys.modules['setuptools'] = __this_shim

    def __getattr__(name):
        return getattr(__real_setuptools, name)

    del __this_shim
    del __current_directory


def migrate(root, setuptools_options, sys_paths):
    import shutil
    import subprocess
    from tempfile import TemporaryDirectory

    with TemporaryDirectory() as temp_dir:
        repo_dir = os.path.join(os.path.realpath(temp_dir), 'repo')
        shutil.copytree(root, repo_dir, ignore=shutil.ignore_patterns('.git', '.tox'), copy_function=shutil.copy)
        shutil.copy(FILE, os.path.join(repo_dir, 'setuptools.py'))
        setup_py = os.path.join(repo_dir, 'setup.py')

        if not os.path.isfile(setup_py):
            # Synthesize a small setup.py file since there is none
            with open(setup_py, 'w', encoding='utf-8') as f:
                f.write('from setuptools import setup\nsetup()\n')

        env = dict(os.environ)
        for arg in setuptools_options:
            key, value = arg.split('=', 1)
            env[f'{ENV_VAR_PREFIX}{key}'] = value

        # When PYTHONSAFEPATH is non-empty, the current directory is not added automatically
        python_paths = [repo_dir]
        python_paths.extend(p for p in sys_paths if p)
        if python_path := env.get('PYTHONPATH', ''):
            python_paths.append(python_path)

        env['PYTHONPATH'] = os.pathsep.join(python_paths)

        subprocess.run([sys.executable, setup_py], env=env, cwd=repo_dir, check=True)

        old_project_file = os.path.join(root, 'pyproject.toml')
        new_project_file = os.path.join(repo_dir, 'pyproject.toml')
        shutil.copyfile(new_project_file, old_project_file)
