import logging
import os
import select
import time
from threading import Event as TEvent
from threading import Lock, Thread
from typing import NamedTuple, Sequence

from adjustor.core.fan import FanInfo, fan_worker, get_fan_info
from adjustor.decky import disable_decky_plugins, find_decky_plugins
from adjustor.i18n import _
from hhd.plugins import Config, Context, Event, HHDPlugin, load_relative_yaml

from .const import get_profile_units

logger = logging.getLogger(__name__)

APPLY_DELAY = 0.7
TDP_DELAY = 0.1
SLEEP_DELAY = 4
PP_POLL_DELAY = 500

#
# Platform profiles interface
#

DEFAULT_MODE = "balanced"

PP_PATH = "/sys/class/platform-profile/"

PP_KNOWN = {
    "amd-dptc": {
        "low-power": _("Low Power"),
        "quiet": _("Quiet"),
        "balanced": _("Balanced"),
        "performance": _("Performance"),
        "custom": _("Custom"),
    },
    "lenovo-wmi-gamezone": {
        "low-power": _("Quiet"),
        "balanced": _("Balanced"),
        "performance": _("Performance"),
        "custom": _("Custom"),
    },
}


class PPData(NamedTuple):
    fn: str
    pp: str
    provider: str
    has_custom: bool
    profiles: tuple[tuple[str, str], ...]


def get_profiles() -> PPData | None:
    if not os.path.isdir(PP_PATH):
        return None

    for pp in os.listdir(PP_PATH):
        if not pp.startswith("platform-profile"):
            continue

        try:
            with open(os.path.join(PP_PATH, pp, "name"), "r") as f:
                fn = f.read().strip()
        except Exception:
            continue

        provider = None
        for known in PP_KNOWN:
            if fn.startswith(known):
                provider = known
                break
        if not provider:
            continue
        try:
            with open(os.path.join(PP_PATH, pp, "choices"), "r") as f:
                choices = f.read().strip().split(" ")
        except Exception:
            logger.error(f"Could not read choices for PP provider {fn}")
            continue

        has_custom = "custom" in choices and "custom" in PP_KNOWN[provider]

        profiles = tuple(
                (k, v) for k, v in PP_KNOWN[provider].items() if k in choices
            )

        if not profiles:
            logger.info(f"Found no profiles for: '{pp}', '{fn}'")
            continue

        return PPData(
            fn=fn,
            pp=pp,
            provider=provider,
            has_custom=has_custom,
            profiles=profiles,
        )


def setup_modes(data: PPData, obj: dict):
    has_default = False
    for sys, pretty in data.profiles:
        obj["modes"][sys] = {
            "type": "container",
            "title": pretty,
            "children": {},
        }
        if sys == DEFAULT_MODE:
            has_default = True
    obj["default"] = DEFAULT_MODE if has_default else data.profiles[-1][0]


def setup_mode_units(units: dict[str, int], obj: dict):
    for mode, mode_obj in obj["modes"].items():
        if mode == "custom":
            continue

        unit_mode = mode
        if mode == "quiet" and mode not in units:
            unit_mode = "low-power"
        if unit_mode in units:
            mode_obj["unit"] = f"{units[unit_mode]}W"


def set_mode(data: PPData, profile: str):
    try:
        logger.info(
            f"Setting platform profile for '{data.fn}'{' (\'' + data.provider + '\')' if data.fn != data.provider else ''} to '{profile}'"
        )
        with open(os.path.join(PP_PATH, data.pp, "profile"), "w") as f:
            f.write(profile)
        return True
    except Exception as e:
        logger.error(f"Could not set platform profile with error:\n{e}")
        return False


def profile_worker(data: PPData, emit, should_exit: TEvent):
    profile_fn = os.path.join(PP_PATH, data.pp, "profile")
    try:
        with open(profile_fn, "r") as f:
            # Reading before polling clears the initial sysfs notification.
            profile = f.read().strip()
            poll = select.poll()
            poll.register(f.fileno(), select.POLLPRI | select.POLLERR)

            logger.info(f"Starting platform profile listener for '{data.fn}'.")
            while not should_exit.is_set():
                if not poll.poll(PP_POLL_DELAY):
                    continue

                f.seek(0)
                new_profile = f.read().strip()
                if not new_profile or new_profile == profile:
                    continue

                logger.info(
                    f"Platform profile for '{data.fn}' changed from "
                    f"'{profile}' to '{new_profile}'."
                )
                profile = new_profile
                emit({"type": "platform_profile", "profile": new_profile})
    except Exception as e:
        if not should_exit.is_set():
            logger.error(f"Platform profile listener failed with error:\n{e}")


#
# Firmware Attributes interface
#

SYS_ATTR_KNOWN = ["amd-dptc", "lenovo-wmi-other"]
SYS_ATTR_PATH = "/sys/class/firmware-attributes/"
SYS_ATTR_MID = "attributes"
TDP_PL3_FN = "ppt_pl3_fppt"
TDP_PL2_FN = "ppt_pl2_sppt"
TDP_PL1_FN = "ppt_pl1_spl"
FAN_FULL_SPEED_FN = "fan_full_speed"

MIN_VAL = "min_value"
MAX_VAL = "max_value"
DEF_VAL = "default_value"
CUR_VAL = "current_value"

PL1_TO_ENERGY_MAP = [
    (15, (6, 12)),
    (30, (10, 20)),
    (80, (25, 45)),
    (130, (30, 60)),
]


class FwattrData(NamedTuple):
    fn: str
    provider: str
    pl1: tuple[int, int | None, int]
    pl2: tuple[int, int | None, int] | None = None
    pl3: tuple[int, int | None, int] | None = None


class Fwattr(NamedTuple):
    provider: str
    attr: str
    fn: str


def get_fwattr(attr: str) -> Fwattr | None:
    """Find a writable firmware attribute without assuming its provider."""
    if not os.path.isdir(SYS_ATTR_PATH):
        return
    for provider in sorted(os.listdir(SYS_ATTR_PATH)):
        fn = os.path.join(SYS_ATTR_PATH, provider, SYS_ATTR_MID, attr, CUR_VAL)
        if os.path.isfile(fn):
            return Fwattr(provider=provider, attr=attr, fn=fn)


def get_fwattr_value(data: Fwattr) -> int | None:
    try:
        with open(data.fn) as f:
            return int(f.read().strip())
    except Exception as e:
        logger.error(
            f"Failed reading firmware attribute '{data.attr}' with error:\n{e}"
        )


def set_fwattr(data: Fwattr, val: int):
    logger.info(
        f"Setting firmware attribute '{data.attr}' to {val} by writing to:\n{data.fn}"
    )
    try:
        with open(data.fn, "w") as f:
            f.write(f"{val}\n")
        return True
    except Exception as e:
        logger.error(f"Failed writing firmware attribute with error:\n{e}")
        return False


def get_tdp_values(mode_provider: str | None = None):
    for fn in os.listdir(SYS_ATTR_PATH):
        # WMI convention means we have cruft -N suffix
        provider = None
        for s in SYS_ATTR_KNOWN:
            if fn.startswith(s):
                provider = s
                break
        if not provider:
            continue

        data = {}
        for v, tdp_fn in [
            ("pl1", TDP_PL1_FN),
            ("pl2", TDP_PL2_FN),
            ("pl3", TDP_PL3_FN),
        ]:
            try:
                with open(
                    os.path.join(SYS_ATTR_PATH, fn, SYS_ATTR_MID, tdp_fn, MIN_VAL)
                ) as f:
                    min_val = int(f.read().strip())
                with open(
                    os.path.join(SYS_ATTR_PATH, fn, SYS_ATTR_MID, tdp_fn, MAX_VAL)
                ) as f:
                    max_val = int(f.read().strip())
            except Exception:
                continue

            try:
                with open(
                    os.path.join(SYS_ATTR_PATH, fn, SYS_ATTR_MID, tdp_fn, DEF_VAL)
                ) as f:
                    def_val = int(f.read().strip())
            except Exception:
                def_val = None

            data[v] = (min_val, def_val, max_val)

        if "pl1" not in data:
            logger.error(f"Provider '{fn}' has partial TDP data: {data}")
            return

        return FwattrData(fn=fn, provider=provider, **data)


def setup_tdp_values(data: FwattrData, obj):
    obj["unit"] = f"→ {data.pl1[2]}"
    obj["children"] = load_relative_yaml("./tdp.yml")
    obj["children"]["tdp"]["min"] = data.pl1[0]
    obj["children"]["tdp"]["default"] = data.pl1[1] or 15  # not the best
    obj["children"]["tdp"]["max"] = data.pl1[2]


def set_tdp(tdp_fn: str, data: FwattrData, val: int):
    fn = os.path.join(SYS_ATTR_PATH, data.fn, SYS_ATTR_MID, tdp_fn, CUR_VAL)
    logger.info(f"Setting tdp value '{tdp_fn}' to {val} by writing to:\n{fn}")
    try:
        with open(fn, "w") as f:
            f.write(f"{val}\n")
        return True
    except Exception as e:
        logger.error(f"Failed writing value with error:\n{e}")
        return False


#
# Hwmon managed fan curve
#

HWMON = "/sys/class/hwmon"

DEFAULT_EDGE = {
    40: 45,
    45: 45,
    50: 45,
    55: 45,
    60: 55,
    65: 60,
    70: 70,
    80: 85,
    90: 100,
}
DEFAULT_TCTL = {
    40: 40,
    50: 45,
    60: 50,
    70: 80,
    80: 90,
    90: 100,
    100: 100,
}

FanCurve = tuple[tuple[int, int], ...]


class ManagedFan(NamedTuple):
    info: FanInfo
    edge: FanCurve
    tctl: FanCurve | None


def get_managed_fan() -> ManagedFan | None:
    info = get_fan_info()
    if not info:
        return
    return ManagedFan(
        info=info,
        edge=tuple((k, v) for k, v in DEFAULT_EDGE.items()),
        tctl=tuple((k, v) for k, v in DEFAULT_EDGE.items()) if info["tctl"] else None,
    )


def setup_managed_fan(data: ManagedFan, obj):
    obj.update(load_relative_yaml("./fan/managed.yml"))
    for target, curve in [
        ("manual_edge", data.edge),
        ("manual_junction", data.tctl),
    ]:
        if curve is None:
            del obj["modes"][target]
            continue
        temps = {}
        for temp, cc in curve:
            temps[f"st{temp}"] = {
                "tags": ["slim"],
                "type": "int",
                "min": 0,
                "max": 100,
                "step": 2,
                "unit": "%",
                "title": f"{temp}C",
                "default": cc,
            }
        obj["modes"][target]["children"] = {
            "info": obj["modes"][target]["children"]["info"],
            **temps,
            "reset": obj["modes"][target]["children"]["reset"],
        }


#
# Unmanaged fan curve
#

HWMON_ATTRS = {
    "name": "name",
    "enable": "pwm{fan}_enable",
    "pwm": "pwm{fan}_auto_point{point}_pwm",
    "temp": "pwm{fan}_auto_point{point}_temp",
}

HWMON_DEFAULT_CURVES = {
    "lenovo_wmi_gamezone_fan": tuple(
        zip(
            (10, 20, 30, 40, 50, 60, 70, 80, 90, 100),
            (44, 48, 55, 60, 71, 79, 87, 87, 100, 100),
        )
    ),
    "asus_custom_fan_curve": tuple(
        zip(
            (30, 40, 50, 60, 70, 80, 90, 100),
            (5, 10, 20, 35, 55, 75, 75, 75),
        )
    ),
}


class HwmonFan(NamedTuple):
    name: str
    path: str
    fans: tuple[int, ...]
    curve: FanCurve
    write_temps: bool


def get_hwmon_fan() -> HwmonFan | None:
    if not os.path.isdir(HWMON):
        return
    for hwmon in sorted(os.listdir(HWMON)):
        path = os.path.join(HWMON, hwmon)
        try:
            with open(os.path.join(path, HWMON_ATTRS["name"])) as f:
                name = f.read().strip()
        except Exception:
            continue

        curve = HWMON_DEFAULT_CURVES.get(name)
        if curve is None:
            continue

        fans = []
        fan = 1
        while os.path.isfile(
            os.path.join(path, HWMON_ATTRS["pwm"].format(fan=fan, point=1))
        ):
            if not all(
                os.path.isfile(
                    os.path.join(
                        path,
                        HWMON_ATTRS[attr].format(fan=fan, point=point),
                    )
                )
                for point in range(1, len(curve) + 1)
                for attr in ("pwm", "temp")
            ):
                logger.warning(
                    f"Ignoring partial fan curve from hwmon provider '{name}'"
                )
                fans.clear()
                break
            fans.append(fan)
            fan += 1

        if fans:
            return HwmonFan(
                name=name,
                path=path,
                fans=tuple(fans),
                curve=curve,
                write_temps=name == "asus_custom_fan_curve",
            )


def get_fan() -> ManagedFan | HwmonFan | None:
    hwmon = get_hwmon_fan()
    if hwmon:
        return hwmon
    return get_managed_fan()


def setup_fan(data: ManagedFan | HwmonFan, obj):
    if isinstance(data, ManagedFan):
        setup_managed_fan(data, obj)
        return

    obj.update(load_relative_yaml("./fan/hwmon.yml"))
    points = {}
    for temp, speed in data.curve:
        points[f"st{temp}"] = {
            "tags": ["slim"],
            "type": "int",
            "min": 0,
            "max": 100,
            "step": 2,
            "unit": "%",
            "title": f"{temp}C",
            "default": speed,
        }
    enabled = obj["modes"]["enabled"]["children"]
    obj["modes"]["enabled"]["children"] = {
        **points,
        "reset": enabled["reset"],
    }


def set_hwmon_fan(data: HwmonFan, curve: Sequence[int]):
    if len(curve) != len(data.curve) or any(
        speed < 0 or speed > 100 for speed in curve
    ):
        raise ValueError("Fan curve must contain one 0-100 speed per temperature")

    point_str = ",".join(f"{temp:>4d} C" for temp, _ in data.curve)
    curve_str = ",".join(f"{speed:>4d} %" for speed in curve)
    logger.info(f"Setting '{data.name}' fan curve:\n{point_str}\n{curve_str}")

    for fan in data.fans:
        for point, ((temp, _), speed) in enumerate(zip(data.curve, curve), 1):
            if data.write_temps:
                temp_fn = os.path.join(
                    data.path,
                    HWMON_ATTRS["temp"].format(fan=fan, point=point),
                )
                with open(temp_fn, "w") as f:
                    f.write(str(temp))

            pwm_fn = os.path.join(
                data.path,
                HWMON_ATTRS["pwm"].format(fan=fan, point=point),
            )
            with open(pwm_fn, "w") as f:
                f.write(str(round(speed * 255 / 100)))

    for fan in data.fans:
        enable_fn = os.path.join(data.path, HWMON_ATTRS["enable"].format(fan=fan))
        if not os.path.isfile(enable_fn):
            continue
        with open(enable_fn, "w") as f:
            f.write("1")
        if fan != data.fans[-1]:
            time.sleep(TDP_DELAY)

    return True


def disable_hwmon_fan(data: HwmonFan):
    found = False
    for fan in data.fans:
        enable_fn = os.path.join(data.path, HWMON_ATTRS["enable"].format(fan=fan))
        if not os.path.isfile(enable_fn):
            continue
        found = True
        with open(enable_fn, "w") as f:
            f.write("2")
        if fan != data.fans[-1]:
            time.sleep(TDP_DELAY)

    if found:
        logger.info(f"Disabled '{data.name}' fan curve")
    return found


class UnifiedDriverPlugin(HHDPlugin):
    def __init__(self) -> None:
        self.name = f"adjustor_unified"
        self.priority = 6
        self.log = "adju"
        self.enabled = False
        self.initialized = False
        self.init = False
        self.failed = False
        self.has_decky = False
        self.action_enabled = False
        self.tdp_set = None
        self.old_conf = None
        self.startup = True

        self.profiles = get_profiles()
        if self.profiles and self.profiles.has_custom:
            self.tdp = get_tdp_values(self.profiles.fn)
        else:
            self.tdp = None
        self.full_fan = get_fwattr(FAN_FULL_SPEED_FN)
        self.fan = get_fan()

        self.mode = None
        self.new_mode = None
        self.system_mode = None
        self.new_tdp = None
        self.queue_tdp = None
        self.queue_fan = None
        self.old_target = None
        self.sys_tdp = False

        # Platform profile listener
        self.profile_t = None
        self.profile_should_exit = TEvent()

        # Managed Fan
        self.fan_t = None
        self.fan_should_exit = TEvent()
        self.fan_junction = TEvent()
        self.fan_lock = Lock()
        self.fan_curve = {}
        self.fan_state = {}

    def is_supported(self):
        return self.profiles is not None

    def settings(self):
        base = load_relative_yaml("../../settings.yml")
        del base["hhd"]["children"]["enforce_limits"]
        out = {
            "hhd": {"settings": base["hhd"], "steamos": base["steamos"]},
        }
        if os.environ.get("HHD_ADJ_ENABLE_TDP"):
            out["hhd"]["settings"]["children"]["tdp_enable"]["default"] = True

        if not self.enabled or self.failed:
            self.initialized = False
            self.action_enabled = True
            out["tdp"] = {"tdp": base["tdp"]}
            if not self.has_decky:
                del out["tdp"]["tdp"]["children"]["decky_info"]
                del out["tdp"]["tdp"]["children"]["decky_remove"]
            return out

        self.action_enabled = False
        logger.info(f"Profile data: {self.profiles}\nTDP data: {self.tdp}")

        self.initialized = True
        out["tdp"] = {"unified": load_relative_yaml("settings.yml")}

        assert self.profiles
        setup_modes(self.profiles, out["tdp"]["unified"]["children"]["tdp"])
        if profile_units := get_profile_units():
            setup_mode_units(
                profile_units,
                out["tdp"]["unified"]["children"]["tdp"],
            )
        if self.tdp:
            setup_tdp_values(
                self.tdp,
                out["tdp"]["unified"]["children"]["tdp"]["modes"]["custom"],
            )
        else:
            out["tdp"]["unified"]["children"]["tdp"]["modes"].pop(
                "custom", None
            )
        if not self.full_fan:
            del out["tdp"]["unified"]["children"]["fan_full_speed"]
        if self.fan:
            setup_fan(self.fan, out["tdp"]["unified"]["children"]["fan"])
        else:
            del out["tdp"]["unified"]["children"]["fan"]

        return out

    def open(
        self,
        emit,
        context: Context,
    ):
        self.emit = emit
        assert self.profiles
        self.profile_should_exit.clear()
        self.profile_t = Thread(
            target=profile_worker,
            args=(self.profiles, self.emit, self.profile_should_exit),
            name="platform-profile",
        )
        self.profile_t.start()

    def _publish_steamos(self, conf: Config):
        if self.tdp:
            conf["hhd.steamos.tdp_status"] = "enabled"
            conf["hhd.steamos.tdp_min"] = self.tdp.pl1[0]
            conf["hhd.steamos.tdp_default"] = self.tdp.pl1[1] or 15
            conf["hhd.steamos.tdp_max"] = self.tdp.pl1[2]
        else:
            # Exit status 2 prevents steamos-manager from selecting a fallback
            # TDP driver while unified continues to manage platform profiles.
            conf["hhd.steamos.tdp_status"] = "conflict"
            conf["hhd.steamos.tdp_min"] = None
            conf["hhd.steamos.tdp_default"] = None
            conf["hhd.steamos.tdp_max"] = None

        if self.tdp_set is not None:
            conf["hhd.steamos.tdp_set"] = self.tdp_set

    def _update_init(self, conf: Config):
        if (
            self.action_enabled
            and self.has_decky
            and conf.get_action("tdp.tdp.decky_remove")
        ):
            logger.warning("Removing Decky plugins")
            conf["hhd.settings.tdp_enable"] = True
            self.has_decky = False
            self.failed = False
            disable_decky_plugins()
            logger.warning("Enabling TDP controls.")
            self.emit({"type": "settings"})

        if self.action_enabled and conf.get_action("tdp.tdp.tdp_enable"):
            conf["hhd.settings.tdp_enable"] = True
            self.failed = False

        enabled = conf.get("hhd.settings.tdp_enable", False)
        if not enabled:
            conf["hhd.settings.tdp_ready"] = False
            conf["hhd.steamos.tdp_status"] = "disabled"
            conf["hhd.steamos.tdp_min"] = None
            conf["hhd.steamos.tdp_default"] = None
            conf["hhd.steamos.tdp_max"] = None
            self.init = False

        if self.enabled != enabled:
            self.emit({"type": "settings"})
        self.enabled = enabled

        if not enabled:
            return False

        if self.init or self.failed:
            return self.init and not self.failed

        decky_plugins = find_decky_plugins()
        if decky_plugins:
            plugin = decky_plugins[0]
            err = f'Found "{plugin.name}" at:\n{plugin.path}\n' + _(
                "Disable Decky TDP plugins using the button below to continue."
            )
            self.emit({"type": "settings"})
            self.has_decky = True
            conf["tdp.tdp.tdp_error"] = err
            conf["hhd.settings.tdp_ready"] = False
            conf["hhd.steamos.tdp_status"] = "conflict"
            logger.error(err)
            self.failed = True
            self.enabled = False
            return False

        self.failed = False
        self.init = True
        conf["hhd.settings.tdp_ready"] = True
        conf["tdp.tdp.tdp_error"] = ""
        return True

    def update(self, conf: Config):
        if not self._update_init(conf):
            self.old_conf = None
            self.startup = True
            return

        self._publish_steamos(conf)

        if not self.initialized:
            self.old_conf = None
            self.startup = True
            return

        if self.startup and self.full_fan:
            full_fan = get_fwattr_value(self.full_fan)
            if full_fan is not None:
                conf["tdp.unified.fan_full_speed"] = bool(full_fan)

        # If not old config, exit, as values can not be set
        if not self.old_conf:
            self.old_conf = conf["tdp.unified"]
            return

        curr = time.perf_counter()

        if self.full_fan:
            full_fan = conf["tdp.unified.fan_full_speed"].to(bool)
            if full_fan != self.old_conf["fan_full_speed"].to(bool):
                set_fwattr(self.full_fan, int(full_fan))

        #
        # TDP
        #
        new_target = None
        new_tdp = self.new_tdp
        self.new_tdp = None
        new_mode = self.new_mode
        self.new_mode = None
        system_mode = self.system_mode
        self.system_mode = None
        mode_from_system = False
        assert self.profiles
        if new_tdp:
            mode = "custom"
            conf["tdp.unified.tdp.mode"] = mode
        elif system_mode:
            mode = system_mode
            mode_from_system = True
            self.sys_tdp = False
            conf["tdp.unified.tdp.mode"] = mode
        elif new_mode:
            mode = new_mode
            conf["tdp.unified.tdp.mode"] = mode
        else:
            mode = conf["tdp.unified.tdp.mode"].to(str)
        self.mode = mode

        tdp_reset = False
        if mode is not None and mode != self.old_conf["tdp.mode"].to(str):
            if not new_tdp:
                self.sys_tdp = False
            tdp_reset = True

        if mode is not None and self.startup:
            tdp_reset = True

        # Show steam message
        if self.sys_tdp:
            conf["tdp.unified.sys_tdp"] = _("Steam is controlling TDP")
        else:
            conf["tdp.unified.sys_tdp"] = ""

        #
        # TDP Management
        #

        # Set TDP and handle EPP
        if tdp_reset and mode != "custom":
            if not mode_from_system:
                set_mode(self.profiles, mode)
            match mode:
                case "quiet" | "low-power":
                    new_target = "power"
                case "balanced":
                    new_target = "balanced"
                case _:  # "performance":
                    new_target = "performance"

        # In custom mode, re-apply settings with debounce
        tdp_set = False
        if mode == "custom" and self.tdp:
            # Check user changed values
            if new_tdp:
                steady = new_tdp
                conf["tdp.unified.tdp.custom.tdp"] = steady
            else:
                steady = conf["tdp.unified.tdp.custom.tdp"].to(int)

            # Bounds check
            if steady < self.tdp.pl1[0]:
                steady = self.tdp.pl1[0]
                conf["tdp.unified.tdp.custom.tdp"] = steady
            elif steady > self.tdp.pl1[2]:
                steady = self.tdp.pl1[2]
                conf["tdp.unified.tdp.custom.tdp"] = steady

            # Update steam text
            steady_updated = steady and steady != self.old_conf["tdp.custom.tdp"].to(
                int
            )
            if not new_tdp and steady_updated:
                self.sys_tdp = False

            steady_updated |= tdp_reset
            boost = conf["tdp.unified.tdp.custom.boost"].to(bool)
            boost_updated = boost != self.old_conf["tdp.custom.boost"].to(bool)

            # If yes, queue an update
            # Debounce
            if self.startup or steady_updated or boost_updated:
                self.queue_tdp = curr + APPLY_DELAY

            tdp_set = self.queue_tdp and self.queue_tdp < curr
            if tdp_set:
                # Fixup EPP
                # Find target mapping for energy settings
                max_pl1 = self.tdp.pl1[2]
                for max_val, (balance, performance) in PL1_TO_ENERGY_MAP:
                    if max_pl1 <= max_val:
                        balance_min = balance
                        performance_min = performance
                        break
                else:
                    balance_min, performance_min = PL1_TO_ENERGY_MAP[-1][1]

                if steady < balance_min:
                    new_target = "power"
                elif steady < performance_min:
                    new_target = "balanced"
                else:
                    new_target = "performance"

                self.queue_tdp = None
                set_mode(self.profiles, "custom")
                set_tdp(TDP_PL1_FN, self.tdp, steady)
                if boost:
                    if self.tdp.pl2:
                        pl2 = min(steady + 2, self.tdp.pl2[2])
                        set_tdp(TDP_PL2_FN, self.tdp, pl2)
                    if self.tdp.pl3:
                        # Interpolate here
                        pl3 = min(
                            int(steady * self.tdp.pl3[2] / self.tdp.pl1[2]), self.tdp.pl3[2]
                        )
                        set_tdp(TDP_PL3_FN, self.tdp, pl3)
                else:
                    if self.tdp.pl2:
                        set_tdp(TDP_PL2_FN, self.tdp, steady)
                    if self.tdp.pl3:
                        set_tdp(TDP_PL3_FN, self.tdp, steady)

        # Apply new target
        if new_target and new_target != self.old_target:
            self.old_target = new_target
            self.emit({"type": "energy", "status": new_target})

        #
        # Fan Settings for Managed fans
        #

        if self.fan and isinstance(self.fan, ManagedFan):
            mode = conf["tdp.unified.fan.mode"].to(str)
            if mode != "disabled":
                with self.fan_lock:
                    if conf[f"tdp.unified.fan.{mode}.reset"].to(bool):
                        conf[f"tdp.unified.fan.{mode}.reset"] = False
                        curve = self.fan.edge if "edge" in mode else self.fan.tctl
                        assert curve, f"Curve is missing for mode '{mode}'. This should not be possible."
                        for k, v in curve:
                            if f"tdp.unified.fan.{mode}.st{k}" in conf:
                                conf[f"tdp.unified.fan.{mode}.st{k}"] = v

                    for k, v in conf[f"tdp.unified.fan.{mode}"].to(dict).items():
                        if not k.startswith("st"):
                            continue
                        self.fan_curve[int(k[2:])] = v / 100
                    if self.fan_state:
                        s = self.fan_state
                        fan_speed = (
                            f"{s['v_curr']*100:.1f}% @ {s['t_target']}C"
                            if s["in_setpoint"]
                            else f"{s['v_curr']*100:.1f}% → {s['v_target']*100:.1f}%"
                        )
                        conf[f"tdp.unified.fan.{mode}.info"] = (
                            f"{fan_speed} ({', '.join(map(str, s['v_rpm']))} RPM)\n"
                            + (
                                f"Tctl: {s['t_junction']:.2f}C, "
                                if s["t_junction"] is not None
                                else ""
                            )
                            + f"Edge: {s['t_edge']:.2f}C\n"
                        )
                    if "junction" in mode:
                        self.fan_junction.set()
                    else:
                        self.fan_junction.clear()

                if not self.fan_t:
                    self.fan_should_exit.clear()
                    self.fan_t = Thread(
                        target=fan_worker,
                        args=(
                            self.fan.info,
                            self.fan_should_exit,
                            self.fan_lock,
                            self.fan_curve,
                            self.fan_state,
                            self.fan_junction,
                        ),
                    )
                    self.fan_t.start()
            else:
                if self.fan_t:
                    self.fan_should_exit.set()
                    self.fan_t.join()
                    self.fan_t = None
                    self.fan_state = {}

        #
        # Firmware-managed hwmon fan curves
        #

        elif self.fan and isinstance(self.fan, HwmonFan):
            fan_mode = conf["tdp.unified.fan.mode"].to(str)
            fan_enabled = fan_mode == "enabled"

            if fan_enabled:
                if conf["tdp.unified.fan.enabled.reset"].to(bool):
                    conf["tdp.unified.fan.enabled.reset"] = False
                    for temp, speed in self.fan.curve:
                        conf[f"tdp.unified.fan.enabled.st{temp}"] = speed

                if self.startup or tdp_reset or tdp_set:
                    self.queue_fan = curr + APPLY_DELAY

                if fan_mode != self.old_conf["fan.mode"].to(str):
                    self.queue_fan = curr + APPLY_DELAY

                for temp, _default_speed in self.fan.curve:
                    if conf[f"tdp.unified.fan.enabled.st{temp}"].to(
                        int
                    ) != self.old_conf[f"fan.enabled.st{temp}"].to(int):
                        self.queue_fan = curr + APPLY_DELAY

                if self.queue_fan and self.queue_fan < curr:
                    try:
                        set_hwmon_fan(
                            self.fan,
                            [
                                conf[f"tdp.unified.fan.enabled.st{temp}"].to(int)
                                for temp, _default_speed in self.fan.curve
                            ],
                        )
                    except Exception as e:
                        logger.error(f"Could not set fan curve. Error:\n{e}")
                    self.queue_fan = None
            elif fan_mode != self.old_conf["fan.mode"].to(str):
                try:
                    if not disable_hwmon_fan(self.fan):
                        # Quickly switch modes to reset the curve
                        reset_mode = next(
                            (
                                profile
                                for profile, _ in self.profiles.profiles
                                if profile != mode
                            ),
                            None,
                        )
                        if reset_mode:
                            logger.info(
                                f"Disabling fan curve by switching to '{reset_mode}' and back."
                            )
                            set_mode(self.profiles, reset_mode)
                            time.sleep(TDP_DELAY)
                            set_mode(self.profiles, mode)

                            # Switching profiles resets custom TDP values. Since TDP
                            # handling already ran this sync, apply them on the next one.
                            if mode == "custom" and self.tdp:
                                self.queue_tdp = curr
                        else:
                            logger.warning(
                                "Could not disable fan curve: no alternate profile available."
                            )
                except Exception as e:
                    logger.error(f"Could not disable fan curve. Error:\n{e}")
                self.queue_fan = None

        # Finish
        self.old_conf = conf["tdp.unified"]
        if self.startup:
            self.startup = False

    def notify(self, events: Sequence[Event]):
        for ev in events:
            if ev["type"] == "tdp":
                self.new_tdp = ev["tdp"]
                self.sys_tdp = ev["tdp"] is not None
                if ev["tdp"] is None:
                    self.tdp_set = False
                elif self.tdp:
                    self.tdp_set = ev["tdp"] != self.tdp.pl1[2]
                else:
                    self.tdp_set = True
            elif ev["type"] == "ppd":
                assert self.profiles
                match ev["status"]:
                    case "power":
                        for p, _ in self.profiles.profiles:
                            if p in ("low-power", "quiet"):
                                self.new_mode = p
                                break
                    case "balanced":
                        self.new_mode = "balanced"
                    case "performance":
                        self.new_mode = "performance"
            elif ev["type"] == "platform_profile":
                assert self.profiles
                mode = ev["profile"]
                if mode not in (p for p, _ in self.profiles.profiles):
                    logger.warning(
                        f"Unknown platform profile '{mode}' for "
                        f"'{self.profiles.fn}'. Ignoring it."
                    )
                    continue
                if mode in (self.mode, self.system_mode):
                    continue

                self.system_mode = mode
                if self.mode is not None:
                    notify_mode = "quiet" if mode == "low-power" else mode
                    self.emit(
                        {"type": "special", "event": f"tdp_cycle_{notify_mode}"}
                    )
            elif ev["type"] == "special" and ev.get("event", None) == "wakeup":
                logger.info(
                    f"Waking up from sleep, resetting TDP after {SLEEP_DELAY} seconds."
                )
                self.queue_tdp = time.perf_counter() + SLEEP_DELAY
            elif (
                ev["type"] == "acpi"
                and ev["event"] in ("ac", "dc")
            ):
                logger.info(
                    f"Power adapter status switched to '{ev['event']}', resetting TDP."
                )
                assert self.profiles
                self.tdp = get_tdp_values(self.profiles.fn)
                self.emit({"type": "settings"})
                self.initialized = False

                if not self.queue_tdp:
                    self.queue_tdp = time.perf_counter() + APPLY_DELAY
            elif ev["type"] == "special" and ev["event"] == "tdp_cycle":
                match self.mode:
                    case "quiet":
                        self.new_mode = "balanced"
                        event = "tdp_cycle_balanced"
                    case "balanced":
                        self.new_mode = "performance"
                        event = "tdp_cycle_performance"
                    case "performance":
                        self.new_mode = "custom"
                        event = "tdp_cycle_custom"
                    case "custom":
                        self.new_mode = "quiet"
                        event = "tdp_cycle_quiet"
                    case _:
                        self.new_mode = "balanced"
                        event = "tdp_cycle_balanced"

                logger.info(f"Cycling TDP to '{self.new_mode}'")
                if self.emit:
                    self.emit({"type": "special", "event": event})

    def close(self):
        if self.profile_t:
            self.profile_should_exit.set()
            self.profile_t.join()
            self.profile_t = None
        if self.fan_t:
            self.fan_should_exit.set()
            self.fan_t.join()
            self.fan_t = None
            self.fan_state = {}
