import logging
import textwrap

import esphome.codegen as cg
from esphome.components.const import CONF_IGNORE_NOT_FOUND
from esphome.components.esp32 import (
    CONF_CPU_FREQUENCY,
    CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
    VARIANT_ESP32,
    VARIANT_ESP32C5,
    VARIANT_ESP32C61,
    VARIANT_ESP32P4,
    VARIANT_ESP32S2,
    VARIANT_ESP32S3,
    add_idf_sdkconfig_option,
    get_esp32_variant,
    idf_version,
)
import esphome.config_validation as cv
from esphome.const import (
    CONF_ADVANCED,
    CONF_DISABLED,
    CONF_FRAMEWORK,
    CONF_ID,
    CONF_MODE,
    CONF_SPEED,
    PLATFORM_ESP32,
)
from esphome.core import CORE
import esphome.final_validate as fv

CODEOWNERS = ["@esphome/core"]
DOMAIN = "psram"

DEPENDENCIES = [PLATFORM_ESP32]

# PSRAM availability tracking for cross-component coordination
KEY_PSRAM_GUARANTEED = "psram_guaranteed"

_LOGGER = logging.getLogger(__name__)

psram_ns = cg.esphome_ns.namespace(DOMAIN)
PsramComponent = psram_ns.class_("PsramComponent", cg.Component)

TYPE_QUAD = "quad"
TYPE_OCTAL = "octal"
TYPE_HEX = "hex"

SDK_MODES = {TYPE_QUAD: "QUAD", TYPE_OCTAL: "OCT", TYPE_HEX: "HEX"}

CONF_ENABLE_ECC = "enable_ecc"

SPIRAM_MODES = {
    VARIANT_ESP32: (TYPE_QUAD,),
    VARIANT_ESP32C5: (TYPE_QUAD,),
    VARIANT_ESP32C61: (TYPE_QUAD,),
    VARIANT_ESP32S2: (TYPE_QUAD,),
    VARIANT_ESP32S3: (TYPE_QUAD, TYPE_OCTAL),
    VARIANT_ESP32P4: (TYPE_HEX,),
}


SPIRAM_SPEEDS = {
    VARIANT_ESP32: (40, 80, 120),
    VARIANT_ESP32C5: (40, 80, 120),
    VARIANT_ESP32C61: (40, 80),
    VARIANT_ESP32S2: (40, 80, 120),
    VARIANT_ESP32S3: (40, 80, 120),
    VARIANT_ESP32P4: (20, 100, 200),
}


def supported() -> bool:
    if not CORE.is_esp32:
        return False
    variant = get_esp32_variant()
    return variant in SPIRAM_MODES


def is_guaranteed() -> bool:
    """Check if PSRAM is guaranteed to be available.

    Returns True when PSRAM is configured with both 'disabled: false' and
    'ignore_not_found: false', meaning the device will fail to boot if PSRAM
    is not found. This ensures safe use of high buffer configurations that
    depend on PSRAM.

    This function should be called during code generation (to_code phase) by
    components that need to know PSRAM availability for configuration decisions.

    Returns:
        bool: True if PSRAM is guaranteed, False otherwise
    """
    return CORE.data.get(KEY_PSRAM_GUARANTEED, False)


def validate_psram_mode(config):
    esp32_config = fv.full_config.get()[PLATFORM_ESP32]
    if config[CONF_SPEED] == "120MHZ":
        if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
            raise cv.Invalid(
                "PSRAM 120MHz requires 240MHz CPU frequency (set in esp32 component)"
            )
        if config[CONF_MODE] == TYPE_OCTAL:
            if (
                esp32_config[CONF_FRAMEWORK]
                .get(CONF_ADVANCED, {})
                .get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
            ):
                _LOGGER.warning(
                    "120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
                )
            else:
                raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
    if config[CONF_MODE] != TYPE_OCTAL and config[CONF_ENABLE_ECC]:
        raise cv.Invalid("ECC is only available in octal mode.")
    if config[CONF_MODE] == TYPE_OCTAL:
        variant = get_esp32_variant()
        if variant != VARIANT_ESP32S3:
            raise cv.Invalid(
                f"Octal PSRAM is only supported on ESP32-S3, not {variant}"
            )
    return config


def get_config_schema(config):
    variant = get_esp32_variant()
    speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])]
    if not speeds:
        raise cv.Invalid("PSRAM is not supported on this chip")
    modes = SPIRAM_MODES[variant]
    if CONF_MODE not in config and len(modes) != 1:
        raise (
            cv.Invalid(
                textwrap.dedent(
                    f"""
                        {variant} requires PSRAM mode selection; one of {", ".join(modes)}
                        Selection of the wrong mode for the board will cause a runtime failure to initialise PSRAM
                    """
                )
            )
        )
    return cv.Schema(
        {
            cv.GenerateID(): cv.declare_id(PsramComponent),
            cv.Optional(CONF_MODE, default=modes[0]): cv.one_of(*modes, lower=True),
            cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean,
            cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True),
            cv.Optional(CONF_DISABLED, default=False): cv.boolean,
            cv.Optional(CONF_IGNORE_NOT_FOUND, default=True): cv.boolean,
        }
    )(config)


CONFIG_SCHEMA = get_config_schema


def _store_psram_guaranteed(config):
    """Store PSRAM guaranteed status in CORE.data for other components.

    PSRAM is "guaranteed" when it will fail if not found, ensuring safe use
    of high buffer configurations in network/wifi components.

    Called during final validation to ensure the flag is available
    before any to_code() functions run.
    """
    psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND]
    CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed
    return config


FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed)


async def to_code(config):
    if config[CONF_DISABLED]:
        return
    if CORE.using_arduino:
        cg.add_build_flag("-DBOARD_HAS_PSRAM")
        if config[CONF_MODE] == TYPE_OCTAL:
            cg.add_platformio_option("board_build.arduino.memory_type", "qio_opi")

    add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True)
    add_idf_sdkconfig_option("CONFIG_SPIRAM", True)
    add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True)
    add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True)
    add_idf_sdkconfig_option(
        "CONFIG_SPIRAM_IGNORE_NOTFOUND", config[CONF_IGNORE_NOT_FOUND]
    )

    add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True)

    # Remove MHz suffix, convert to int
    speed = int(config[CONF_SPEED][:-3])
    add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True)
    add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed)
    if speed == 120:
        variant = get_esp32_variant()
        # On chips with MSPI timing tuning, FLASH and PSRAM share the core
        # clock so flash frequency must match PSRAM frequency.
        # ESP32 and ESP32-S2 don't have this constraint.
        if variant not in (VARIANT_ESP32, VARIANT_ESP32S2):
            add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
        if config[CONF_MODE] == TYPE_OCTAL and idf_version() >= cv.Version(5, 4, 0):
            add_idf_sdkconfig_option(
                "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR",
                True,
            )
    if config[CONF_ENABLE_ECC]:
        add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True)

    cg.add_define("USE_PSRAM")

    var = cg.new_Pvariable(config[CONF_ID])
    await cg.register_component(var, config)
