# This file is part of the faebryk project
# SPDX-License-Identifier: MIT

import logging
from pathlib import Path

from faebryk.libs.util import (
    ConfigFlag,
    FileChangedWatcher,
    at_exit,
    find_file,
    global_lock,
    is_editable_install,
    least_recently_modified_file,
)

logger = logging.getLogger(__name__)


LEAK_WARNINGS = ConfigFlag("CPP_LEAK_WARNINGS", default=False)
DEBUG_BUILD = ConfigFlag("CPP_DEBUG_BUILD", default=False)
PRINTF_DEBUG = ConfigFlag("CPP_PRINTF_DEBUG", default=False)

EDITABLE_MODULE_NAME = "faebryk_core_cpp_editable"

_thisfile = Path(__file__)
_thisdir = _thisfile.parent
_cmake_dir = _thisdir
_build_dir = _cmake_dir / "build"
_pyi_source = _build_dir / f"{EDITABLE_MODULE_NAME}.pyi"
_src_files = [_thisdir / "src", _thisdir / "include", _thisdir / "CMakeLists.txt"]


def compile():
    import platform
    import shutil
    import sys

    from faebryk.libs.header import formatted_file_contents, get_header
    from faebryk.libs.util import run_live

    logger.warning("Recompiling C++ code")

    pyi_watcher = FileChangedWatcher(_pyi_source, FileChangedWatcher.CheckMethod.HASH)

    # check for cmake binary existing
    if not shutil.which("cmake"):
        raise RuntimeError(
            "cmake not found, needed for compiling c++ code in editable mode"
        )

    other_flags = []

    # On OSx we've had some issues with building for the right architecture
    if sys.platform == "darwin":  # macOS
        arch = platform.machine()
        if arch in ["arm64", "x86_64"]:
            other_flags += [f"-DCMAKE_OSX_ARCHITECTURES={arch}"]

    if DEBUG_BUILD:
        other_flags += ["-DCMAKE_BUILD_TYPE=Debug"]
    other_flags += [f"-DGLOBAL_PRINTF_DEBUG={int(bool(PRINTF_DEBUG))}"]

    run_parallel = ["-j"]
    if sys.platform == "win32":
        # Use /m for parallel builds with MSVC
        run_parallel = ["/m"]

    with global_lock(_build_dir / "lock", timeout_s=60):
        run_live(
            [
                "cmake",
                "-S",
                str(_cmake_dir),
                "-B",
                str(_build_dir),
                "-DEDITABLE=1",
                "-DPython_EXECUTABLE=" + sys.executable,
                *other_flags,
            ],
            stdout=logger.debug,
            stderr=logger.error,
        )
        run_live(
            [
                "cmake",
                "--build",
                str(_build_dir),
                "--",
                *run_parallel,
            ],
            stdout=logger.debug,
            stderr=logger.error,
        )

    # move autogenerated type stub file to source directory
    if pyi_watcher.has_changed():
        pyi_out = _thisfile.with_suffix(".pyi")
        pyi_out.write_text(
            formatted_file_contents(
                get_header()
                + "\n"
                + "# This file is auto-generated by nanobind.\n"
                + "# Do not edit this file directly; edit the corresponding\n"
                + "# C++ file instead.\n"
                # + "from typing import overload\n"
                + _pyi_source.read_text(encoding="utf-8"),
                is_pyi=True,
            ),
            encoding="utf-8",
        )
        run_live(
            [sys.executable, "-m", "ruff", "check", "--fix", pyi_out],
            stdout=logger.debug,
            stderr=logger.error,
        )


def load():
    import sys

    # Search the module in the build directory and its subdirectories
    if not _build_dir.exists():
        raise RuntimeError("build directory not found")

    module_dir = _build_dir
    if sys.platform == "win32":
        # Find the actual directory containing the compiled module (.pyd on Windows)
        # Check common locations: build dir itself, Debug, Release subdirs
        module_name_start = EDITABLE_MODULE_NAME
        search_paths = [_build_dir, _build_dir / "Debug", _build_dir / "Release"]
        module_path = None
        for potential_dir in search_paths:
            module_path = next(
                find_file(potential_dir, f"{module_name_start}*.pyd"), None
            )
            if module_path:
                module_dir = module_path.parent
                break
        if not module_path:
            raise RuntimeError(
                f"Could not find {module_name_start}*.pyd in "
                "build directory or subdirectories."
            )

    # Add the directory containing the .pyd file to sys.path
    sys.path.append(str(module_dir))


def compile_and_load():
    """
    Forces C++ to compile into faebryk_core_cpp_editable module which is then loaded
    into _cpp.
    """

    # Force recompile
    # subprocess.run(["rm", "-rf", str(build_dir)], check=True)

    # check if source files are newer than build files
    build_date_info = least_recently_modified_file(_build_dir)
    src_date_info = least_recently_modified_file(*_src_files)
    if (
        build_date_info is None
        or src_date_info is None
        or build_date_info[1] < src_date_info[1]
    ):
        compile()

    # load into sys.path
    load()


# Re-export c++ with type hints provided by __init__.pyi
if is_editable_install():
    compile_and_load()
    from faebryk_core_cpp_editable import *  # type: ignore # noqa: E402, F403
else:
    from faebryk_core_cpp import *  # type: ignore # noqa: E402, F403


def cleanup():
    if LEAK_WARNINGS:
        print("\n--- Nanobind leakage analysis ".ljust(80, "-"))
        # nanobind automatically prints leaked objects at exit
    from faebryk.core.cpp import set_leak_warnings

    set_leak_warnings(bool(LEAK_WARNINGS))


at_exit(cleanup, on_exception=False)
