"""Generate clang-tidy compile commands via the native sdk-nrf toolchain.

Produces a ``compile_commands.json`` for the nrf52/Zephyr clang-tidy
environment **without an ESPHome YAML config**, mirroring
``esphome.espidf.clang_tidy``: generate a minimal Zephyr application, run a
configure-only west build with the native sdk-nrf toolchain, and let
``script/helpers_zephyr.py`` extract idedata from the resulting compile
commands.

* the stub app is C++ so the compile commands carry C++ flags, matching how
  clang-tidy analyzes ESPHome's sources;
* ``prj.conf`` enables the Kconfig superset ESPHome components need (BT, ADC,
  mcumgr, zigbee) so their include paths land in the compile commands;
* the platform defines (USE_ZEPHYR, USE_NRF52) match what a real ESPHome
  nrf52 build adds via its generated project.

``ESPHOME_ZEPHYR_COMPILE_COMMANDS`` may point at an existing build's
``compile_commands.json`` to skip generation (fast iteration).
"""

from __future__ import annotations

import logging
from pathlib import Path

TIDY_PROJECT_NAME = "esphome_tidy"

# Analyzed against the native toolchain's default SDK version
# (RECOMMENDED_SDK_NRF_VERSION), which also keys the CI install cache.
_TIDY_BOARD = "adafruit_itsybitsy_nrf52840"

# Never compiled (the build is configure-only): the file exists only so the
# app target emits a C++ compile command to harvest flags/includes from.
_TIDY_MAIN_CPP = "int main() { return 0; }\n"

# Kconfig superset enabling every subsystem an ESPHome nrf52 component may
# use, so the compile commands carry all of their include paths.
_TIDY_PRJ_CONF = """\
CONFIG_CPP=y
CONFIG_STD_CPP20=y
CONFIG_REQUIRES_FULL_LIBCPP=y
CONFIG_NEWLIB_LIBC=y
CONFIG_BT=y
CONFIG_ADC=y
# posix (time sets POSIX_CLOCK, socket sets POSIX_API); without it the
# Zephyr POSIX headers clash with the libc ones under analysis
CONFIG_POSIX_API=y
#mcumgr begin
CONFIG_NET_BUF=y
CONFIG_ZCBOR=y
CONFIG_MCUMGR=y
CONFIG_MCUMGR_GRP_IMG=y
CONFIG_IMG_MANAGER=y
CONFIG_STREAM_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH=y
CONFIG_IMG_ERASE_PROGRESSIVELY=y
CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y
CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y
CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y
CONFIG_MCUMGR_TRANSPORT_UART=y
#mcumgr end
#zigbee begin
CONFIG_ZIGBEE=y
CONFIG_CRYPTO=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
#zigbee end
"""


def _tidy_cmakelists(library_include_dirs: str) -> str:
    # The defines a real ESPHome nrf52 build puts on the app target.
    # ESPHOME_LOG_LEVEL must be set up front -- otherwise log.h's ``#ifndef``
    # sets it to NONE, a macro-redefined warning across nearly every source.
    return f"""\
# Auto-generated by ESPHome (clang-tidy compile-commands project)
cmake_minimum_required(VERSION 3.20.0)
set(Zephyr_DIR "$ENV{{ZEPHYR_BASE}}/share/zephyr-package/cmake/")
find_package(Zephyr REQUIRED)
project({TIDY_PROJECT_NAME})
target_sources(app PRIVATE main.cpp)
target_compile_definitions(app PRIVATE
  USE_ZEPHYR
  USE_NRF52
  ESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
)
target_include_directories(app PRIVATE
{library_include_dirs}
)
"""


def _parse_lib_deps(platformio_ini: Path) -> list:
    """Parse the nrf52 env's ``lib_deps`` from platformio.ini into Library specs.

    These are the PlatformIO libraries ESPHome components pull in via
    ``cg.add_library`` (ArduinoJson, dlms_parser, ...); their headers must be
    on the tidy translation unit's include path. Mirrors the pio nrf52 env's
    ``lib_deps`` composition (``common.lib_deps_base`` +
    ``common:idf-component-libs``).
    """
    import configparser

    from esphome.core import Library

    parser = configparser.ConfigParser(interpolation=None, strict=False)
    parser.read(platformio_ini)

    tokens: list[str] = []
    for section, key in (
        ("common", "lib_deps_base"),
        ("common:idf-component-libs", "lib_deps"),
    ):
        if parser.has_option(section, key):
            tokens += parser.get(section, key).splitlines()

    libs: list[Library] = []
    for token in tokens:
        token = token.split(";", 1)[0].strip()  # drop trailing ; comment
        if not token or token.startswith(("${", "+<")):
            continue
        if "://" in token or ".git" in token:
            libs.append(Library(token, None, token))  # git repository (with #ref)
        elif "@" in token:
            name, _, version = token.partition("@")
            libs.append(Library(name, version))
    return libs


def _library_include_dirs(platformio_ini: Path) -> list[str]:
    """Resolve the pio libraries and return their include roots."""
    from esphome.platformio.library import LibraryBackend, convert_libraries

    dirs: list[str] = []

    def emit(component) -> None:
        build = component.data.get("build", {})
        candidates = {build.get("includeDir", "include"), build.get("srcDir", "src")}
        candidates.update({"src", "."})
        for candidate in sorted(candidates):
            path = (component.path / candidate).resolve()
            if path.is_dir():
                dirs.append(str(path))

    backend = LibraryBackend(
        platform="nordicnrf52", framework="zephyr", emit=emit, cache_key="zephyr"
    )
    convert_libraries(_parse_lib_deps(platformio_ini), backend)
    return sorted(set(dirs))


def _setup_core(work_dir: Path) -> None:
    """Point CORE at the tidy project + SDK version, without any YAML config."""
    from esphome.components.zephyr.const import KEY_ZEPHYR
    import esphome.config_validation as cv
    from esphome.const import (
        KEY_CORE,
        KEY_FRAMEWORK_VERSION,
        KEY_TARGET_FRAMEWORK,
        KEY_TARGET_PLATFORM,
        PLATFORM_NRF52,
        Toolchain,
    )
    from esphome.core import CORE

    from . import RECOMMENDED_SDK_NRF_VERSION

    CORE.name = TIDY_PROJECT_NAME
    # config_path's parent is the data-dir root for per-run artifacts. The
    # sdk-nrf install is in the global cache dir, independent of this path.
    CORE.config_path = work_dir.parent / "tidy.yaml"
    CORE.build_path = work_dir
    CORE.toolchain = Toolchain.SDK_NRF
    CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = PLATFORM_NRF52
    CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
    CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
        RECOMMENDED_SDK_NRF_VERSION
    )


def generate_compile_commands(work_dir: Path, platformio_ini: Path) -> Path:
    """Generate the tidy Zephyr project and run a configure-only west build.

    Returns the path to the generated ``compile_commands.json``.
    """
    from esphome.core import EsphomeError
    from esphome.framework_helpers import run_command_ok
    from esphome.helpers import rmtree

    from .framework import check_and_install, get_build_env, get_build_paths

    # Surface ESPHome's INFO logs (sdk-nrf download/west update) -- they go
    # through logging, which the clang-tidy script otherwise leaves at
    # WARNING, so the first-run installation looks silent without this.
    logging.basicConfig(level=logging.INFO, format="%(message)s")

    _setup_core(work_dir)
    check_and_install()

    library_include_dirs = "\n".join(
        f'  "{d}"' for d in _library_include_dirs(platformio_ini)
    )
    source_dir = work_dir / "zephyr"
    source_dir.mkdir(parents=True, exist_ok=True)
    (source_dir / "CMakeLists.txt").write_text(
        _tidy_cmakelists(library_include_dirs), encoding="utf-8"
    )
    (source_dir / "main.cpp").write_text(_TIDY_MAIN_CPP, encoding="utf-8")
    (source_dir / "prj.conf").write_text(_TIDY_PRJ_CONF, encoding="utf-8")

    # Always configure from scratch: west can't pristine a dir whose CMake
    # cache is stale/missing, and a configure-only run is cheap.
    build_dir = work_dir / "build"
    if build_dir.is_dir():
        rmtree(build_dir)

    paths = get_build_paths()
    # Build only the generated-headers target (syscall_list.h, offsets.h, ...)
    # on top of the configure: clang-tidy needs those headers to exist, but a
    # full firmware build would be wasted work. --no-sysbuild keeps sdk-nrf
    # 2.9+ from wrapping the build in a multi-image sysbuild project, which
    # would nest the compile commands and hide the headers target.
    west_cmd = [
        str(paths["python_executable"]),
        "-m",
        "west",
        "build",
        "--no-sysbuild",
        "-b",
        _TIDY_BOARD,
        "-d",
        str(build_dir),
        str(source_dir),
        "-t",
        "zephyr_generated_headers",
        "--",
        "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
    ]
    if not run_command_ok(
        west_cmd,
        env=get_build_env(),
        stream_output=True,
        cwd=str(paths["framework_path"]),
    ):
        raise EsphomeError("nRF52 clang-tidy configure failed")

    return build_dir / "compile_commands.json"
