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)
