"""ESP-IDF direct build API for ESPHome."""

import json
import logging
import os
from pathlib import Path
import shutil
import subprocess

from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE
from esphome.core import CORE, EsphomeError

_LOGGER = logging.getLogger(__name__)


def _get_idf_path() -> Path | None:
    """Get IDF_PATH from environment or common locations."""
    # Check environment variable first
    if "IDF_PATH" in os.environ:
        path = Path(os.environ["IDF_PATH"])
        if path.is_dir():
            return path

    # Check common installation locations
    common_paths = [
        Path.home() / "esp" / "esp-idf",
        Path.home() / ".espressif" / "esp-idf",
        Path("/opt/esp-idf"),
    ]

    for path in common_paths:
        if path.is_dir() and (path / "tools" / "idf.py").is_file():
            return path

    return None


def _get_idf_env() -> dict[str, str]:
    """Get environment variables needed for ESP-IDF build.

    Requires the user to have sourced export.sh before running esphome.
    """
    env = os.environ.copy()

    idf_path = _get_idf_path()
    if idf_path is None:
        raise EsphomeError(
            "ESP-IDF not found. Please install ESP-IDF and source export.sh:\n"
            "  git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n"
            "  cd ~/esp-idf && ./install.sh\n"
            "  source ~/esp-idf/export.sh\n"
            "See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/"
        )

    env["IDF_PATH"] = str(idf_path)
    return env


def run_idf_py(
    *args, cwd: Path | None = None, capture_output: bool = False
) -> int | str:
    """Run idf.py with the given arguments."""
    idf_path = _get_idf_path()
    if idf_path is None:
        raise EsphomeError("ESP-IDF not found")

    env = _get_idf_env()
    idf_py = idf_path / "tools" / "idf.py"

    cmd = ["python", str(idf_py)] + list(args)

    if cwd is None:
        cwd = CORE.build_path

    _LOGGER.debug("Running: %s", " ".join(cmd))
    _LOGGER.debug("  in directory: %s", cwd)

    if capture_output:
        result = subprocess.run(
            cmd,
            cwd=cwd,
            env=env,
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode != 0:
            _LOGGER.error("idf.py failed:\n%s", result.stderr)
        return result.stdout
    result = subprocess.run(
        cmd,
        cwd=cwd,
        env=env,
        check=False,
    )
    return result.returncode


def run_reconfigure() -> int:
    """Run cmake reconfigure only (no build)."""
    return run_idf_py("reconfigure")


def has_outdated_files():
    """Check if the build configuration is stale.

    Returns True if required build files are missing or if configuration inputs
    are newer than the generated CMake/Ninja build artifacts.
    """
    cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt")

    cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt")
    cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt")
    build_config_path = CORE.relative_build_path("build/config")
    sdkconfig_internal_path = CORE.relative_build_path(
        f"sdkconfig.{CORE.name}.esphomeinternal"
    )
    dependency_lock_path = CORE.relative_build_path("dependencies.lock")
    build_ninja_path = CORE.relative_build_path("build/build.ninja")

    if not os.path.isdir(build_config_path) or not os.listdir(build_config_path):
        return True
    if not os.path.isfile(cmakecache_txt_path):
        return True
    if not os.path.isfile(build_ninja_path):
        return True
    if os.path.isfile(dependency_lock_path) and os.path.getmtime(
        dependency_lock_path
    ) > os.path.getmtime(build_ninja_path):
        return True

    cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path)
    return any(
        os.path.getmtime(f) > cmakecache_txt_mtime
        for f in [
            _get_idf_path(),
            cmakelists_txt_build_path,
            cmakelists_txt_src_path,
            sdkconfig_internal_path,
            build_config_path,
        ]
        if f and os.path.exists(f)
    )


def need_reconfigure() -> bool:
    from esphome.build_gen.espidf import has_discovered_components

    # We need to reconfigure either if the files are outdated or if there is no component discovered
    return has_outdated_files() or not has_discovered_components()


def run_compile(config, verbose: bool) -> int:
    """Compile the ESP-IDF project.

    Uses two-phase configure to auto-discover available components:
    1. If no previous build, configure with minimal REQUIRES to discover components
    2. Regenerate CMakeLists.txt with discovered components
    3. Run full build
    """
    from esphome.build_gen.espidf import write_project

    # Check if we need to do discovery phase
    if need_reconfigure():
        _LOGGER.info("Discovering available ESP-IDF components...")
        write_project(minimal=True)
        rc = run_reconfigure()
        if rc != 0:
            _LOGGER.error("Component discovery failed")
            return rc
        _LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
        write_project(minimal=False)

    # Build
    args = []

    if verbose:
        args.append("-v")

    args.append("build")

    # Set the sdkconfig file
    sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
    if sdkconfig_path.is_file():
        args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"])

    return run_idf_py(*args)


def get_firmware_path() -> Path:
    """Get the path to the compiled firmware binary."""
    build_dir = CORE.relative_build_path("build")
    return build_dir / f"{CORE.name}.bin"


def get_factory_firmware_path() -> Path:
    """Get the path to the factory firmware (with bootloader)."""
    build_dir = CORE.relative_build_path("build")
    return build_dir / f"{CORE.name}.factory.bin"


def create_factory_bin() -> bool:
    """Create factory.bin by merging bootloader, partition table, and app."""
    build_dir = CORE.relative_build_path("build")
    flasher_args_path = build_dir / "flasher_args.json"

    if not flasher_args_path.is_file():
        _LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
        return False

    try:
        with open(flasher_args_path, encoding="utf-8") as f:
            flash_data = json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        _LOGGER.error("Failed to read flasher_args.json: %s", e)
        return False

    # Get flash size from config
    flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]

    # Build esptool merge command
    sections = []
    for addr, fname in sorted(
        flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
    ):
        file_path = build_dir / fname
        if file_path.is_file():
            sections.extend([addr, str(file_path)])
        else:
            _LOGGER.warning("Flash file not found: %s", file_path)

    if not sections:
        _LOGGER.warning("No flash sections found")
        return False

    output_path = get_factory_firmware_path()
    chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")

    cmd = [
        "python",
        "-m",
        "esptool",
        "--chip",
        chip,
        "merge_bin",
        "--flash_size",
        flash_size,
        "--output",
        str(output_path),
    ] + sections

    _LOGGER.info("Creating factory.bin...")
    result = subprocess.run(cmd, capture_output=True, text=True, check=False)

    if result.returncode != 0:
        _LOGGER.error("Failed to create factory.bin: %s", result.stderr)
        return False

    _LOGGER.info("Created: %s", output_path)
    return True


def create_ota_bin() -> bool:
    """Copy the firmware to .ota.bin for ESPHome OTA compatibility."""
    firmware_path = get_firmware_path()
    ota_path = firmware_path.with_suffix(".ota.bin")

    if not firmware_path.is_file():
        _LOGGER.warning("Firmware not found: %s", firmware_path)
        return False

    shutil.copy(firmware_path, ota_path)
    _LOGGER.info("Created: %s", ota_path)
    return True
