"""Shared nuclear-data paths for Python and C++.

Set the nuclear-data root once in ``data_root.txt`` beside this file.
Each existing immediate subdirectory becomes a Python variable, for example
``endf-b8.1`` becomes ``ENDF_B81``.

Importing this module reads the data root and discovers libraries, but never
writes files. Running it as a script generates ``openmc_workbench_config.hpp``.

    python openmc_workbench_config.py

Rerun after adding, removing, or renaming nuclear-data directories, or after
moving the repository. Changing only the path inside ``data_root.txt`` does
not require regenerating the header: C++ reads that file at runtime.
"""

from pathlib import Path
import json
import re

CONFIG_FILE = Path(__file__).resolve().with_name("data_root.txt")
HEADER_FILE = Path(__file__).resolve().with_name("openmc_workbench_config.hpp")


def _read_data_root() -> Path:
    try:
        entry = CONFIG_FILE.read_text(encoding="utf-8").strip()
    except FileNotFoundError as exc:
        raise FileNotFoundError(
            f"Create {CONFIG_FILE} and put the nuclear-data root path in it "
            "(one line)."
        ) from exc

    if not entry:
        raise ValueError(f"{CONFIG_FILE} is empty")

    root = Path(entry).expanduser()
    if not root.is_absolute():
        root = CONFIG_FILE.parent / root
    return root.resolve()


def _name_from_directory(directory: str) -> str:
    """Map e.g. 'endf-b8.1' -> 'ENDF_B81'."""
    name = directory.upper().replace("-", "_").replace(".", "")
    name = re.sub(r"[^A-Z0-9_]", "_", name)
    if name and name[0].isdigit():
        name = "_" + name
    return name


def discover_libraries(root: Path) -> dict[str, Path]:
    """Discover existing directories directly under the nuclear-data root."""
    libraries: dict[str, Path] = {}
    if not root.is_dir():
        # Allows the setup notebook to create a new DATA_ROOT.
        return libraries

    for path in sorted(root.iterdir()):
        if not path.is_dir():
            continue
        name = _name_from_directory(path.name)
        if name in {"CONFIG_FILE", "HEADER_FILE", "DATA_ROOT", "LIBRARY_PATHS"}:
            raise ValueError(f"Reserved configuration name: {path.name}")
        if name in libraries:
            raise ValueError(
                f"Library-name collision: {libraries[name].name!r} and "
                f"{path.name!r} both produce {name}"
            )
        libraries[name] = path

    return libraries


DATA_ROOT = _read_data_root()
LIBRARY_PATHS = discover_libraries(DATA_ROOT)
globals().update(LIBRARY_PATHS)
__all__ = ["DATA_ROOT", "LIBRARY_PATHS", *LIBRARY_PATHS]


def _cpp_string(value: str) -> str:
    """Return a quoted C++ UTF-8 string literal."""
    return json.dumps(value, ensure_ascii=False)


def _cpp_header() -> str:
    """Produce the C++ aliases using the same discovered directory names."""
    lines = [
        "// Generated by openmc_workbench_config.py; do not edit.",
        "#pragma once",
        "",
        "#include <cstdlib>",
        "#include <filesystem>",
        "#include <fstream>",
        "#include <stdexcept>",
        "#include <string>",
        "",
        "namespace openmc_workbench_detail {",
        "inline std::filesystem::path read_data_root() {",
        f"    const std::filesystem::path config_file = {_cpp_string(str(CONFIG_FILE))};",
        "    std::ifstream input(config_file);",
        "    if (!input) {",
        '        throw std::runtime_error("Cannot read data_root.txt: " + config_file.string());',
        "    }",
        "",
        "    std::string entry;",
        "    std::getline(input, entry);",
        '    const auto first = entry.find_first_not_of(" \\t\\r\\n");',
        "    if (first == std::string::npos) {",
        '        throw std::runtime_error("data_root.txt is empty");',
        "    }",
        '    const auto last = entry.find_last_not_of(" \\t\\r\\n");',
        "    entry = entry.substr(first, last - first + 1);",
        "",
        '    if (entry == "~" || entry.rfind("~/", 0) == 0) {',
        '        const char* home = std::getenv("HOME");',
        "        if (!home) {",
        '            throw std::runtime_error("HOME is not set");',
        "        }",
        '        entry = (std::filesystem::path(home) / entry.substr(entry == "~" ? 1 : 2)).string();',
        "    }",
        "",
        "    std::filesystem::path root(entry);",
        "    if (root.is_relative()) {",
        "        root = config_file.parent_path() / root;",
        "    }",
        "    return std::filesystem::absolute(root).lexically_normal();",
        "}",
        "}  // namespace openmc_workbench_detail",
        "",
        "inline const std::filesystem::path DATA_ROOT =",
        "    openmc_workbench_detail::read_data_root();",
    ]

    for name, path in LIBRARY_PATHS.items():
        lines += [
            "",
            f"inline const std::filesystem::path {name} =",
            f"    DATA_ROOT / {_cpp_string(path.name)};",
        ]

    lines += [""]
    return "\n".join(lines)


def main() -> None:
    """Generate the C++ header explicitly, without writing on import."""
    if not DATA_ROOT.is_dir():
        raise NotADirectoryError(
            f"Nuclear-data root does not exist: {DATA_ROOT}. "
            "Create it or correct data_root.txt before generating aliases."
        )
    if not LIBRARY_PATHS:
        raise RuntimeError(
            f"No library directories found under {DATA_ROOT}; "
            "run the setup first, then generate the header."
        )

    HEADER_FILE.write_text(_cpp_header(), encoding="utf-8")
    print(f"Generated: {HEADER_FILE}")
    print("Names:", ", ".join(["DATA_ROOT", *LIBRARY_PATHS]))


if __name__ == "__main__":
    main()
