"""
Support to interface with Alexa Devices.

SPDX-License-Identifier: Apache-2.0

For more details about this platform, please refer to the documentation at
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
"""

import asyncio
from datetime import datetime, timedelta
from json import JSONDecodeError, loads
import logging
import os
import random
import time
from typing import Optional
from urllib.parse import urlparse

import aiohttp
from alexapy import (
    AlexaAPI,
    AlexaLogin,
    AlexapyConnectionError,
    AlexapyLoginError,
    HTTP2EchoClient,
    __version__ as alexapy_version,
    hide_serial,
    obfuscate,
)
from alexapy.errors import AlexapyTooManyRequestsError
from alexapy.helpers import delete_cookie as alexapy_delete_cookie
import async_timeout
from homeassistant.components.persistent_notification import (
    async_create as async_create_persistent_notification,
    async_dismiss as async_dismiss_persistent_notification,
)
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
    CONF_EMAIL,
    CONF_PASSWORD,
    CONF_URL,
    EVENT_HOMEASSISTANT_STARTED,
    EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import UnknownFlow
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.loader import async_get_integration
from homeassistant.util import dt, slugify
import voluptuous as vol

from .alexa_entity import AlexaEntityData, get_entity_data, parse_alexa_entities
from .config_flow import in_progress_instances
from .const import (
    ALEXA_COMPONENTS,
    CONF_ACCOUNTS,
    CONF_DEBUG,
    CONF_EXCLUDE_DEVICES,
    CONF_EXTENDED_ENTITY_DISCOVERY,
    CONF_INCLUDE_DEVICES,
    CONF_OAUTH,
    CONF_OTPSECRET,
    CONF_PUBLIC_URL,
    CONF_QUEUE_DELAY,
    CONF_SCAN_INTERVAL,
    DATA_ALEXAMEDIA,
    DATA_LISTENER,
    DEFAULT_EXTENDED_ENTITY_DISCOVERY,
    DEFAULT_PUBLIC_URL,
    DEFAULT_QUEUE_DELAY,
    DEFAULT_SCAN_INTERVAL,
    DEPENDENT_ALEXA_COMPONENTS,
    DOMAIN,
    HTTP2_ERROR_THRESHOLD,
    ISSUE_URL,
    LAST_CALLED_429_BACKOFF_INITIAL_S,
    LAST_CALLED_429_BACKOFF_MAX_S,
    LAST_CALLED_COALESCE_WINDOW_MS,
    LAST_CALLED_CONN_BACKOFF_S,
    LAST_CALLED_DEBOUNCE_S,
    LAST_CALLED_ITEMS,
    LAST_CALLED_LOGIN_BACKOFF_S,
    LAST_CALLED_LOOKBACK_MS,
    LAST_CALLED_RETRY_DELAY_S,
    LAST_CALLED_RETRY_LIMIT,
    LAST_CALLED_STALE_FUDGE_MS,
    LAST_CALLED_SUCCESS_PACE_S,
    LAST_PING_MAX_AGE_SECONDS,
    LAST_PUSH_INACTIVITY_SECONDS,
    MIN_TIME_BETWEEN_FORCED_SCANS,
    MIN_TIME_BETWEEN_SCANS,
    NOTIFICATION_COOLDOWN,
    NOTIFY_REFRESH_BACKOFF,
    NOTIFY_REFRESH_MAX_RETRIES,
    SCAN_INTERVAL,
    STARTUP_MESSAGE,
)
from .coordinator import AlexaMediaCoordinator
from .exceptions import TimeoutException
from .helpers import (
    _catch_login_errors,
    _existing_serials,
    alarm_just_dismissed,
    calculate_uuid,
    hide_email,
    report_relogin_required,
    safe_get,
)
from .metrics import AlexaMetrics, get_metrics
from .notify import async_unload_entry as notify_async_unload_entry
from .runtime_data import AlexaRuntimeData
from .services import AlexaMediaServices

_LOGGER = logging.getLogger(__name__)


ACCOUNT_CONFIG_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_EMAIL): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Required(CONF_URL): cv.string,
        vol.Optional(CONF_INCLUDE_DEVICES, default=[]): vol.All(
            cv.ensure_list, [cv.string]
        ),
        vol.Optional(CONF_EXCLUDE_DEVICES, default=[]): vol.All(
            cv.ensure_list, [cv.string]
        ),
        vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
        vol.Optional(CONF_QUEUE_DELAY, default=DEFAULT_QUEUE_DELAY): cv.positive_float,
        vol.Optional(CONF_EXTENDED_ENTITY_DISCOVERY, default=False): cv.boolean,
        vol.Optional(CONF_DEBUG, default=False): cv.boolean,
    }
)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {
                vol.Optional(CONF_ACCOUNTS): vol.All(
                    cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]
                )
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)


def _valid_voice_summary(summary: object) -> bool:
    """Return True if summary looks like a real spoken utterance."""
    if not isinstance(summary, str):
        return False
    summary = summary.strip()
    return bool(summary) and any(ch.isalnum() for ch in summary)


def _queue_last_called_activity(
    account: dict,
    *,
    device_serial: str,
    customer_id: str | None,
    activity_ts: int | None,
    command: str,
) -> None:
    """Queue or refresh a last_called activity candidate keyed by device/customer."""
    if not device_serial:
        return

    try:
        ts = int(activity_ts) if activity_ts is not None else 0
    except (TypeError, ValueError):
        ts = 0

    queue: list[dict] = account.setdefault("last_called_activity_queue", [])

    for item in queue:
        if (
            item.get("serial") == device_serial
            and item.get("customer_id") == customer_id
        ):
            # Keep earliest timestamp like alexa-remote does for the queued burst.
            prev_ts = int(item.get("activity_ts") or 0)
            if ts and (prev_ts == 0 or ts < prev_ts):
                item["activity_ts"] = ts
            item["command"] = command
            return

    queue.append(
        {
            "serial": device_serial,
            "customer_id": customer_id,
            "activity_ts": ts,
            "command": command,
        }
    )


def _snapshot_last_called_activity_queue(account: dict) -> list[dict]:
    """Return a shallow snapshot of queued activity entries."""
    queue = account.get("last_called_activity_queue") or []
    return [dict(item) for item in queue if isinstance(item, dict)]


def _remove_last_called_activity_queue_entries(
    account: dict, resolved_keys: set[tuple[str, str | None]]
) -> None:
    """Remove resolved queue entries by (serial, customer_id)."""
    queue = account.get("last_called_activity_queue") or []
    account["last_called_activity_queue"] = [
        item
        for item in queue
        if (
            item.get("serial"),
            item.get("customer_id"),
        )
        not in resolved_keys
    ]


def _valid_utterance_type(record: dict) -> bool:
    """Filter utterance types similar to Node-RED/alexa-remote logic."""
    utterance_type = record.get("utteranceType")
    if utterance_type in {
        "DEVICE_ARBITRATION",
        "ASR_TIMEOUT",
        "WAKE_WORD_ONLY",
    }:
        return False
    return True


def _push_healthy(account: dict) -> bool:
    """Return True if HTTP2 push is likely usable (enough to skip polling last_called)."""
    http2 = account.get("http2")
    if not http2:
        return False

    # Hard negative: the underlying transport is closed.
    client = getattr(http2, "client", None)
    if client is not None and getattr(client, "is_closed", False):
        return False

    # If alexapy has already driven error count to "give up", treat as down.
    if int(account.get("http2error") or 0) >= HTTP2_ERROR_THRESHOLD:
        return False

    last_push = float(account.get("last_push_activity") or 0.0)
    if last_push and (time.time() - last_push) > LAST_PUSH_INACTIVITY_SECONDS:
        return False

    # If we have a recent ping, that's a strong positive.
    last_ping_dt = getattr(http2, "_last_ping", None)  # private, best-effort
    if last_ping_dt:
        try:
            age = time.time() - last_ping_dt.timestamp()
            # ping is ~299s; allow generous slack for scheduler jitter.
            if age <= LAST_PING_MAX_AGE_SECONDS:
                return True
            # If ping is *very* stale, treat as suspicious but not definitive.
            # Don't force False here unless you also have other negative signals.
        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.debug("Could not evaluate http2 ping age: %s", exc)

    # Unknown state: object exists and client isn't closed -> assume usable.
    return True


def _network_allowed(login_obj) -> bool:
    if login_obj.close_requested:
        return False
    if login_obj.session.closed:
        return False
    if not login_obj.status.get("login_successful"):
        return False
    return True


def _entity_backed_device_identifiers(account_dict: dict) -> set[str]:
    """Collect device identifier strings for devices that are backed by entities.

    Alexa Media Player historically prunes stale HA Device Registry entries by comparing
    device identifiers against the current *media_player* serials. That works for Echoes,
    but fails for entity-only devices (e.g., Amazon Indoor Air Quality Monitor) which have
    no media_player entity. Those devices would get pruned unless we also consider the
    identifiers referenced by entities we created.
    """
    identifiers: set[str] = set()

    def _collect_from_device_info(device_info) -> None:
        if not device_info:
            return
        try:
            # dr.DeviceInfo
            di_idents = getattr(device_info, "identifiers", None)
            if di_idents:
                for ident in di_idents:
                    if isinstance(ident, tuple) and len(ident) == 2:
                        identifiers.add(ident[1])
                return
        except Exception as exc:  # pylint: disable=broad-except
            _LOGGER.debug("Could not extract identifiers from device_info: %s", exc)
        # dict-style device_info
        if isinstance(device_info, dict):
            di_idents = device_info.get("identifiers")
            if di_idents:
                for ident in di_idents:
                    if isinstance(ident, tuple) and len(ident) == 2:
                        identifiers.add(ident[1])

    # Recursively walk nested entity structures (dict/list/tuple/set) and collect any device_info found.
    def _walk(obj) -> None:
        if obj is None:
            return

        # Entity-ish object
        _collect_from_device_info(getattr(obj, "device_info", None))

        if isinstance(obj, dict):
            for v in obj.values():
                _walk(v)
        elif isinstance(obj, (list, tuple, set)):
            for v in obj:
                _walk(v)

    _walk(account_dict.get("entities", {}))
    return identifiers


def _entity_backed_serials(account: dict) -> set[str]:
    """Return serials that exist only because we created entity-backed devices.

    These serials may not be discoverable via media player inventory, but should
    still be considered 'current' so we don't prune/ignore them.
    """
    serials: set[str] = set()
    entities = account.get("entities")
    if not isinstance(entities, dict):
        return serials

    # Sensors are stored keyed by serial; this is where AIAQM lives.
    sensors = entities.get("sensor")
    if isinstance(sensors, dict):
        serials.update(s for s in sensors.keys() if isinstance(s, str) and s)

    return serials


def _select_last_called_payload_from_records(
    records: list[dict],
    queue_snapshot: list[dict],
    account: dict,
    existing_serials_local: set[str],
) -> tuple[dict | None, set[tuple[str, str | None]]]:
    """Select the best last_called payload from raw customer history records."""
    if not records or not queue_snapshot:
        return None, set()

    watermark = int(account.get("last_called_customer_history_ts") or 0)
    last_pushed_activity = account.get("last_called_last_pushed_activity") or {}

    queue_by_key = {
        (item.get("serial"), item.get("customer_id")): item
        for item in queue_snapshot
        if item.get("serial")
    }

    def _record_ts(record: dict) -> int:
        try:
            return int(record.get("creationTimestamp") or 0)
        except (TypeError, ValueError):
            return 0

    sorted_records = sorted(
        (r for r in records if isinstance(r, dict)),
        key=_record_ts,
        reverse=True,
    )

    for record in sorted_records:
        serial = record.get("deviceSerialNumber")
        if not serial or serial not in existing_serials_local:
            continue

        if not _valid_utterance_type(record):
            continue

        summary = ((record.get("description") or {}).get("summary") or "").strip()
        if not _valid_voice_summary(summary):
            continue

        try:
            ts = int(record.get("creationTimestamp") or 0)
        except (TypeError, ValueError):
            continue

        if ts <= watermark:
            continue

        if ts <= int(last_pushed_activity.get(serial) or 0):
            continue

        for key, queued in queue_by_key.items():
            queued_serial, _queued_customer = key
            queued_ts = int(queued.get("activity_ts") or 0)

            if serial != queued_serial:
                continue

            # Future enhancement: customer/user matching.
            # The push payload includes destinationUserId, but the current
            # get_customer_history_records() helper does not expose a matching
            # user identifier from the raw RVH records, so this check is currently
            # ineffective. Leave in place in case the API helper is extended later.
            #
            # if queued_customer and record.get("customerId") not in (None, queued_customer):
            #    continue

            if queued_ts and ts < (queued_ts - LAST_CALLED_STALE_FUDGE_MS):
                continue

            payload = {
                "serialNumber": serial,
                "timestamp": ts,
                "summary": summary,
                "response": (record.get("alexaResponse") or "").strip() or None,
            }
            return payload, {key}

    return None, set()


async def async_setup(hass, config):
    """Set up the Alexa domain."""
    # Initialize metrics
    hass.data.setdefault(DOMAIN, {})
    hass.data[DOMAIN]["metrics"] = AlexaMetrics(hass)
    metrics = hass.data[DOMAIN]["metrics"]
    metrics.start_boot_tracking()

    integration = await async_get_integration(hass, DOMAIN)
    integration_name = integration.name or "<not available>"
    _LOGGER.info(
        STARTUP_MESSAGE.format(
            name=integration_name,
            ISSUE_URL=ISSUE_URL,
            DOMAIN=DOMAIN,
            version=integration.version,
            alexapy_version=alexapy_version,
        )
    )
    metrics.record_boot_stage("domain_setup")
    if DOMAIN in config:
        async_create_issue(
            hass,
            DOMAIN,
            "deprecated_yaml_configuration",
            is_fixable=False,
            issue_domain=DOMAIN,
            severity=IssueSeverity.ERROR,
            translation_key="deprecated_yaml_configuration",
            learn_more_url="https://github.com/alandtse/alexa_media_player/wiki/Configuration#configurationyaml",
        )
        _LOGGER.error(
            "YAML configuration of Alexa Media Player is no longer supported. "
            "Please remove `alexa_media` from your configuration, "
            "restart Home Assistant and use the UI to configure it instead. "
            "Settings > Devices & services > Integrations > ADD INTEGRATION"
        )
        return False
    return True


def _store_and_dispatch_last_called(
    hass: HomeAssistant,
    email: str,
    last_called: dict,
    force: bool = False,
) -> None:
    """Store last_called data and dispatch change event if needed.

    Shared helper used by both the closure-based update_last_called
    and the module-level _async_update_last_called_global.
    """
    accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
    if email not in accounts:
        _LOGGER.debug("%s: Account removed during update, skipping", hide_email(email))
        return
    stored_data = accounts[email]
    payload = dict(last_called) if isinstance(last_called, dict) else {}

    ts_raw = payload.get("timestamp")
    try:
        ts = int(ts_raw or 0)
    except (TypeError, ValueError):
        ts = 0

    if 0 < ts < 10_000_000_000:
        ts *= 1000
    if ts > 0:
        payload["timestamp"] = ts

    prev = stored_data.get("last_called")
    changed = prev != payload

    if ts > 0:
        stored_data["last_called_customer_history_ts"] = max(
            int(stored_data.get("last_called_customer_history_ts") or 0),
            ts,
        )

    stored_data["last_called"] = payload

    if (
        force
        or (prev is None and last_called is not None)
        or (prev is not None and changed)
    ):
        _LOGGER.debug(
            "%s: last_called changed",
            hide_email(email),
        )
        async_dispatcher_send(
            hass,
            f"{DOMAIN}_{hide_email(email)}"[0:32],
            {"last_called_change": payload},
        )


async def _async_update_last_called_global(
    hass: HomeAssistant,
    login_obj,
    email: str,
    last_called: dict | None = None,
    force: bool = False,
) -> None:
    """Update the last called device globally (standalone function).

    This version is defined at module level for use in background tasks.
    It delegates storage/dispatch to _store_and_dispatch_last_called.
    """
    if not _network_allowed(login_obj):
        return

    accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
    account = accounts.get(email)

    # If we're doing a "refresh" (no payload) and not forcing, prefer the probe worker
    # whenever it's available, regardless of http2 connection.
    if (
        (not isinstance(last_called, dict) or not last_called.get("summary"))
        and not force
        and account
    ):
        trigger = account.get("last_called_probe_trigger")
        if callable(trigger):
            trigger("GLOBAL_REFRESH", None)
            return

    if not isinstance(last_called, dict) or not last_called.get("summary"):
        try:
            # Do not timebox this call; alexapy may back off/sleep on 429.
            # Serialize RVH calls per account to avoid parallel rate-limited requests.
            api_lock = None
            if account:
                api_lock = account.get("last_called_api_lock")
            if api_lock is None:
                last_called = await AlexaAPI.get_last_device_serial(login_obj)
            else:
                async with api_lock:
                    last_called = await AlexaAPI.get_last_device_serial(login_obj)
        except asyncio.CancelledError:
            # Task cancelled during unload/shutdown; propagate cancellation.
            raise
        except AlexapyTooManyRequestsError:
            _LOGGER.debug(
                "%s: Rate limited during last_called update; skipping",
                hide_email(email),
            )
            return
        except AlexapyConnectionError as exc:
            _LOGGER.debug(
                "%s: Connection error during last_called update: %s",
                hide_email(email),
                exc,
            )
            return
        except AlexapyLoginError:
            _LOGGER.debug(
                "%s: Login error during last_called update", hide_email(email)
            )
            report_relogin_required(hass, login_obj, email)
            return
        except TypeError:
            _LOGGER.debug(
                "%s: Error updating last_called: %s",
                hide_email(email),
                repr(last_called),
            )
            return
        if not isinstance(last_called, dict):
            _LOGGER.debug(
                "%s: Error updating last_called: unexpected response %s",
                hide_email(email),
                repr(last_called),
            )
            return

    if not _valid_voice_summary(last_called.get("summary")):
        _LOGGER.debug(
            "%s: Ignoring last_called with invalid summary",
            hide_email(email),
        )
        return

    _LOGGER.debug(
        "%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)
    )
    _store_and_dispatch_last_called(hass, email, last_called, force)


# @retry_async(limit=5, delay=5, catch_exceptions=True)
async def async_setup_entry(hass, config_entry):
    """Set up Alexa Media Player as config entry.

    This function uses the new runtime_data pattern for type-safe data storage.
    Legacy hass.data[DATA_ALEXAMEDIA] is maintained for backward compatibility
    during the migration period.
    """
    _boot_start = time.monotonic()

    async def close_alexa_media(event=None) -> None:
        """Clean up Alexa connections."""
        _LOGGER.debug("Received shutdown request: %s", event)
        if accounts := safe_get(hass.data, [DATA_ALEXAMEDIA, "accounts"], {}):
            for email, _ in accounts.items():
                await close_connections(hass, email)

    async def complete_startup(event=None) -> None:
        # pylint: disable=unused-argument
        """Run final tasks after startup."""
        _LOGGER.debug("Completing remaining startup tasks.")
        await asyncio.sleep(10)
        if hass.data[DATA_ALEXAMEDIA].get("notify_service"):
            notify = hass.data[DATA_ALEXAMEDIA].get("notify_service")
            _LOGGER.debug("Refreshing notify targets")
            await notify.async_register_services()

    async def relogin(event=None) -> None:
        """Relogin to Alexa."""
        if hide_email(email) == event.data.get("email"):
            _LOGGER.debug("%s: Received relogin request: %s", hide_email(email), event)
            login_obj: AlexaLogin = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
                "login_obj"
            )
            uuid = (await calculate_uuid(hass, email, url))["uuid"]
            if login_obj is None:
                login_obj = AlexaLogin(
                    url=url,
                    email=email,
                    password=password,
                    outputpath=hass.config.path,
                    debug=account.get(CONF_DEBUG),
                    otp_secret=account.get(CONF_OTPSECRET, ""),
                    oauth=account.get(CONF_OAUTH, {}),
                    uuid=uuid,
                    oauth_login=True,
                )
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login_obj
            else:
                login_obj.oauth_login = True
            await login_obj.reset()
            # await login_obj.login()
            if await test_login_status(hass, config_entry, login_obj):
                await setup_alexa(hass, config_entry, login_obj)

    async def login_success(event=None) -> None:
        """Relogin to Alexa."""
        if hide_email(email) == event.data.get("email"):
            _LOGGER.debug("Received Login success: %s", event)
            login_obj: AlexaLogin = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
                "login_obj"
            )
            await setup_alexa(hass, config_entry, login_obj)

    hass.data.setdefault(DATA_ALEXAMEDIA, {})
    hass.data[DATA_ALEXAMEDIA].setdefault("accounts", {})
    hass.data[DATA_ALEXAMEDIA].setdefault("config_flows", {})
    hass.data[DATA_ALEXAMEDIA].setdefault("notify_service", None)
    account = config_entry.data
    email = account.get(CONF_EMAIL)
    password = account.get(CONF_PASSWORD)
    url = account.get(CONF_URL)
    hass.data[DATA_ALEXAMEDIA]["accounts"].setdefault(
        email,
        {
            "coordinator": None,
            "config_entry": config_entry,
            "setup_alexa": setup_alexa,
            "devices": {
                "media_player": {},
                "switch": {},
                "guard": [],
                "light": [],
                "binary_sensor": [],
                "temperature": [],
                "smart_switch": [],
            },
            "entities": {
                "media_player": {},
                "switch": {},
                "sensor": {},
                "light": [],
                "binary_sensor": [],
                "alarm_control_panel": {},
                "smart_switch": [],
            },
            "excluded": {},
            "new_devices": True,
            "http2_lastattempt": 0,
            "http2error": 0,
            "http2_commands": {},
            "http2_activity": {"serials": {}, "refreshed": {}},
            "http2": None,
            "auth_info": None,
            "second_account_index": 0,
            "should_get_network": True,
            "first_run": True,
            "notifications": {},  # already used for the raw notifications dict
            "notifications_pending": set(),  # doppler serials that need a refresh
            "notifications_refresh_task": None,  # running task or None
            "notifications_retry_count": 0,  # simple backoff counter
            "options": {
                CONF_INCLUDE_DEVICES: config_entry.data.get(CONF_INCLUDE_DEVICES, ""),
                CONF_EXCLUDE_DEVICES: config_entry.data.get(CONF_EXCLUDE_DEVICES, ""),
                CONF_QUEUE_DELAY: config_entry.data.get(
                    CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY
                ),
                CONF_SCAN_INTERVAL: config_entry.data.get(
                    CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
                ),
                CONF_PUBLIC_URL: config_entry.data.get(
                    CONF_PUBLIC_URL, DEFAULT_PUBLIC_URL
                ),
                CONF_EXTENDED_ENTITY_DISCOVERY: config_entry.data.get(
                    CONF_EXTENDED_ENTITY_DISCOVERY, DEFAULT_EXTENDED_ENTITY_DISCOVERY
                ),
                CONF_DEBUG: config_entry.data.get(CONF_DEBUG, False),
            },
            DATA_LISTENER: [config_entry.add_update_listener(update_listener)],
        },
    )
    uuid_dict = await calculate_uuid(hass, email, url)
    uuid = uuid_dict["uuid"]
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["second_account_index"] = uuid_dict[
        "index"
    ]
    login: AlexaLogin = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
        "login_obj",
        AlexaLogin(
            url=url,
            email=email,
            password=password,
            outputpath=hass.config.path,
            debug=account.get(CONF_DEBUG),
            otp_secret=account.get(CONF_OTPSECRET, ""),
            oauth=account.get(CONF_OAUTH, {}),
            uuid=uuid,
            oauth_login=True,
        ),
    )
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_push_activity"] = 0

    # Create runtime_data for optimized architecture
    # This provides type-safe access to account data
    if not hasattr(config_entry, "runtime_data") or config_entry.runtime_data is None:
        config_entry.runtime_data = AlexaRuntimeData(
            login_obj=login,
            config_entry=config_entry,
            second_account_index=uuid_dict["index"],
        )
    if not hass.data[DATA_ALEXAMEDIA]["accounts"][email]["second_account_index"]:
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_alexa_media)
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, complete_startup)
    hass.bus.async_listen("alexa_media_relogin_required", relogin)
    hass.bus.async_listen("alexa_media_relogin_success", login_success)
    try:
        _t = time.monotonic()
        cookies = await login.load_cookie()
        cookie_login_ok = False
        if cookies:
            try:
                if login._session is None or getattr(login._session, "closed", False):
                    login._create_session(True)
                async with login._session.get(
                    "https://alexa.amazon.com/api/bootstrap",
                    cookies=cookies,
                    ssl=login._ssl,
                    allow_redirects=False,
                ) as response:
                    if response.status == 200:
                        data = loads(await response.text())
                        auth = (data or {}).get("authentication") or {}
                        customer_email = (auth.get("customerEmail") or "").lower()
                        if (
                            auth.get("authenticated")
                            and customer_email == email.lower()
                        ):
                            _LOGGER.debug(
                                "[BOOT] Cookie auth confirmed via /api/bootstrap"
                            )
                            login.status["login_successful"] = True
                            login.customer_id = auth.get("customerId")
                            login.stats["login_timestamp"] = datetime.now()
                            login.stats["api_calls"] = 0
                            await login.check_domain()
                            await login.finalize_login()
                            cookie_login_ok = True
            except (JSONDecodeError, ValueError, aiohttp.ClientError) as ex:
                _LOGGER.debug("[BOOT] Bootstrap cookie auth check failed: %s", ex)
        if not cookie_login_ok:
            await login.login(cookies=cookies)
        _LOGGER.debug("[BOOT] login completed in %.2fs", time.monotonic() - _t)
        _t = time.monotonic()
        if await test_login_status(hass, config_entry, login):
            _LOGGER.debug("[BOOT] test_login_status in %.2fs", time.monotonic() - _t)
            _t = time.monotonic()
            await setup_alexa(hass, config_entry, login)
            _LOGGER.debug(
                "[BOOT] setup_entry total: %.2fs", time.monotonic() - _boot_start
            )
            return True
        return False
    except AlexapyConnectionError as err:
        raise ConfigEntryNotReady(str(err) or "Connection Error during login") from err


async def setup_alexa(hass, config_entry, login_obj: AlexaLogin):
    # pylint: disable=too-many-statements,too-many-locals
    """Set up a alexa api based on host parameter."""

    debug = config_entry.data.get(CONF_DEBUG, False)

    # Record metrics
    metrics = get_metrics(hass)
    email = login_obj.email
    if metrics:
        metrics.record_boot_stage(f"setup_alexa_start_{hide_email(email)}")

    # Initialize throttling state and lock
    dnd_update_lock = asyncio.Lock()
    last_dnd_update_times: dict[str, datetime] = {}
    pending_dnd_updates: dict[str, bool] = {}
    scheduled_dnd_tasks: dict[str, asyncio.Task] = {}

    async def async_update_data() -> Optional[AlexaEntityData]:
        # noqa pylint: disable=too-many-branches
        """Fetch data from API endpoint.

        This is the place to pre-process the data to lookup tables
        so entities can quickly look up their data.

        This will ping Alexa API to identify all devices, bluetooth, and the last
        called device.

        If any guards, sensors, switches or lights are configured, their current state will be acquired.
        This data is returned directly so that it is available on the coordinator.

        This will add new devices and services when discovered. By default this
        runs every SCAN_INTERVAL seconds unless another method calls it. if
        push is connected, it will increase the delay 10-fold between updates.
        While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to
        reduce the number of runs to avoid flooding. Slow changing states
        should be checked here instead of in spawned components like
        media_player since this object is one per account.
        Each AlexaAPI call generally results in two webpage requests.
        """
        email = config.get(CONF_EMAIL)
        accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
        account = accounts.get(email)
        if not account:
            return None

        login_obj = account.get("login_obj")
        if not login_obj or not _network_allowed(login_obj):
            return None
        account = hass.data[DATA_ALEXAMEDIA]["accounts"][email]
        existing_serials = set(_existing_serials(hass, login_obj))
        existing_serials |= _entity_backed_serials(account)
        existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
            "media_player"
        ].values()
        auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("auth_info")
        new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]
        extended_entity_discovery = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "options"
        ].get(CONF_EXTENDED_ENTITY_DISCOVERY)
        should_get_network = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "should_get_network"
        ]
        first_run = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["first_run"]
        devices = {}
        bluetooth = {}
        preferences = {}
        dnd = {}
        entity_state = {}

        # Try to get cached data for faster boot
        entry_id = config_entry.entry_id if config_entry else ""
        cache_key_prefix = f"{email}_{entry_id}"
        cached_devices = None
        _used_cached_devices = False
        if metrics:
            cached_devices = metrics.api_cache.get(f"{cache_key_prefix}_devices")

        if cached_devices and not new_devices:
            _LOGGER.debug("%s: Using cached devices data", hide_email(email))
            # NOTE: DataCache returns direct references. We intentionally enrich device dicts
            # in-place each refresh cycle (bluetooth_state/locale/dnd/etc.).
            devices = cached_devices
            _used_cached_devices = True
            # Still need fresh bluetooth, preferences, and DND
            tasks = [
                AlexaAPI.get_bluetooth(login_obj),
                AlexaAPI.get_device_preferences(login_obj),
                AlexaAPI.get_dnd_state(login_obj),
            ]
        else:
            tasks = [
                AlexaAPI.get_devices(login_obj),
                AlexaAPI.get_bluetooth(login_obj),
                AlexaAPI.get_device_preferences(login_obj),
                AlexaAPI.get_dnd_state(login_obj),
            ]
        if new_devices:
            tasks.append(AlexaAPI.get_authentication(login_obj))

        entities_to_monitor = set()

        # Temperature sensors (stored as entities["sensor"][serial]["Temperature"] = sensor)
        for per_serial in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
            "sensor"
        ].values():
            if not isinstance(per_serial, dict):
                continue

            temp = per_serial.get("Temperature")
            if temp and temp.enabled:
                entities_to_monitor.add(temp.alexa_entity_id)

            # Air Quality sensors:
            # entities["sensor"][serial]["Air_Quality"][unique_id] = sensor
            airq = per_serial.get("Air_Quality")
            if isinstance(airq, dict):
                for aq_sensor in airq.values():
                    if aq_sensor and aq_sensor.enabled:
                        entities_to_monitor.add(aq_sensor.alexa_entity_id)
            elif airq and getattr(airq, "enabled", False):
                # Backwards compat if some installs still have a single sensor stored
                entities_to_monitor.add(airq.alexa_entity_id)

        for light in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"].get(
            "light", []
        ):
            if light.enabled:
                entities_to_monitor.add(light.alexa_entity_id)

        for binary_sensor in hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "entities"
        ].get("binary_sensor", []):
            if binary_sensor.enabled:
                entities_to_monitor.add(binary_sensor.alexa_entity_id)

        for guard in (
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"]
            .get("alarm_control_panel", {})
            .values()
        ):
            if guard.enabled:
                entities_to_monitor.add(guard.unique_id)

        for smart_switch in hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "entities"
        ].get("smart_switch", []):
            if smart_switch.enabled:
                entities_to_monitor.add(smart_switch.alexa_entity_id)

        if entities_to_monitor:
            tasks.append(get_entity_data(login_obj, list(entities_to_monitor)))

        if should_get_network:
            tasks.append(AlexaAPI.get_network_details(login_obj))

        optional_task_results = []
        try:
            if tasks:
                # Note: asyncio.TimeoutError and aiohttp.ClientError are already
                # handled by the data update coordinator.
                # Increase timeout from 30s to 45s to permit
                # get_network_details() retries which could up to 30s.
                async with async_timeout.timeout(45):
                    start_fetch = time.monotonic()
                    if _used_cached_devices:
                        (
                            bluetooth,
                            preferences,
                            dnd,
                            *optional_task_results,
                        ) = await asyncio.gather(*tasks)
                    else:
                        (
                            devices,
                            bluetooth,
                            preferences,
                            dnd,
                            *optional_task_results,
                        ) = await asyncio.gather(*tasks)

                    fetch_time = time.monotonic() - start_fetch
                    _LOGGER.debug(
                        "[BOOT] API fetch (%d tasks, cached=%s) in %.2fs",
                        len(tasks) if tasks else 0,
                        _used_cached_devices,
                        fetch_time,
                    )
                    # Record API call metrics
                    if metrics:
                        metrics.record_api_call("initial_fetch", fetch_time)
                        # Cache the devices for faster next boot (only freshly fetched)
                        if not _used_cached_devices:
                            metrics.api_cache.cache_set(
                                f"{cache_key_prefix}_devices", devices
                            )

                    _t_post = time.monotonic()
                    if should_get_network and optional_task_results:
                        # First run is a special case. Get the state of all entities(including disabled)
                        # This ensures all entities have state during startup without needing to request coordinator refresh

                        _LOGGER.info(
                            "%s: Network Discovery: Checking", hide_email(email)
                        )
                        api_devices = optional_task_results.pop()
                        if not api_devices:
                            _LOGGER.warning(
                                "%s: Network Discovery: AlexaAPI returned an unexpected response. Retrying on next polling cycle",
                                hide_email(email),
                            )
                        else:
                            _LOGGER.debug(
                                "%s: Network Discovery: Success, processing response",
                                hide_email(email),
                            )
                            # Only process this once after success
                            hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                                "should_get_network"
                            ] = False

                            # Discard the entities_to_monitor results since we now have full network details
                            if entities_to_monitor and optional_task_results:
                                optional_task_results.pop()
                                entities_to_monitor.clear()

                        alexa_entities = parse_alexa_entities(
                            api_devices,
                            debug=hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                                "options"
                            ].get(CONF_DEBUG, False),
                        )
                        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"].update(
                            alexa_entities
                        )

                        _entities_to_monitor = set()
                        for type_of_entity, entities in alexa_entities.items():
                            if (
                                type_of_entity
                                in {"guard", "temperature", "air_quality", "aiaqm"}
                                or extended_entity_discovery
                            ):
                                for entity in entities:
                                    _entities_to_monitor.add(entity.get("id"))
                                    _LOGGER.debug("Monitoring: %s", entity.get("name"))
                        _LOGGER.debug(
                            "%s: Network Discovery: %s entities will be monitored",
                            hide_email(email),
                            len(list(_entities_to_monitor)),
                        )
                        # Use shorter timeout for entity data to avoid blocking
                        _t_ed = time.monotonic()
                        try:
                            entity_state = await asyncio.wait_for(
                                get_entity_data(login_obj, list(_entities_to_monitor)),
                                timeout=10.0,
                            )
                        except asyncio.TimeoutError:
                            _LOGGER.warning(
                                "%s: get_entity_data timed out after 10s, "
                                "entity states will be fetched on next cycle",
                                hide_email(email),
                            )
                        _LOGGER.debug(
                            "[BOOT] get_entity_data (network) in %.2fs",
                            time.monotonic() - _t_ed,
                        )

                if entities_to_monitor and optional_task_results:
                    entity_state = optional_task_results.pop()
                    _LOGGER.debug(
                        "%s: Processing %s entities to monitor",
                        hide_email(email),
                        len(list(entities_to_monitor)),
                    )

                if new_devices and optional_task_results:
                    auth_info = optional_task_results.pop()
                    _LOGGER.debug(
                        "%s: Found %s devices, %s bluetooth",
                        hide_email(email),
                        len(devices) if devices is not None else "",
                        (
                            len(bluetooth.get("bluetoothStates", []))
                            if bluetooth is not None
                            else ""
                        ),
                    )

                # Process notifications in background to avoid blocking boot
                # (process_notifications has a 4s sleep + API call)
                _LOGGER.debug(
                    "[BOOT] post-fetch processing in %.2fs", time.monotonic() - _t_post
                )

                existing_notif_task = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
                    "notifications_init_task"
                )
                if existing_notif_task and not existing_notif_task.done():
                    _LOGGER.debug(
                        "%s: Notifications background task already running, skipping",
                        hide_email(email),
                    )
                else:

                    async def _bg_process_notifications():
                        try:
                            await process_notifications(login_obj)
                        except (
                            AlexapyConnectionError,
                            AlexapyLoginError,
                            asyncio.TimeoutError,
                            JSONDecodeError,
                        ):
                            _LOGGER.debug(
                                "%s: Background notifications failed, retrying once",
                                hide_email(email),
                            )
                            try:
                                await asyncio.sleep(5)
                                await process_notifications(login_obj)
                            except (
                                AlexapyConnectionError,
                                AlexapyLoginError,
                                asyncio.TimeoutError,
                                JSONDecodeError,
                            ):
                                _LOGGER.debug(
                                    "%s: Background notifications retry failed",
                                    hide_email(email),
                                )

                    hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                        "notifications_init_task"
                    ] = hass.async_create_background_task(
                        _bg_process_notifications(),
                        f"{DOMAIN}_notifications_init",
                    )

        except (AlexapyLoginError, JSONDecodeError):
            _LOGGER.debug(
                "%s: Alexa API disconnected; attempting to relogin : status %s",
                hide_email(email),
                login_obj.status,
            )
            if login_obj.status:
                hass.bus.async_fire(
                    "alexa_media_relogin_required",
                    event_data={"email": hide_email(email), "url": login_obj.url},
                )
            return None
        except asyncio.CancelledError:
            # Task cancelled during unload/shutdown; propagate cancellation.
            raise

        _t_proc = time.monotonic()
        new_alexa_clients = []  # list of newly discovered device names
        exclude_filter = []
        include_filter = []

        for device in devices:
            serial = device["serialNumber"]
            dev_name = device["accountName"]
            if include and dev_name not in include:
                include_filter.append(dev_name)
                if "appDeviceList" in device:
                    for app in device["appDeviceList"]:
                        (
                            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][
                                app["serialNumber"]
                            ]
                        ) = device
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][
                    serial
                ] = device
                continue
            if exclude and dev_name in exclude:
                exclude_filter.append(dev_name)
                if "appDeviceList" in device:
                    for app in device["appDeviceList"]:
                        (
                            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][
                                app["serialNumber"]
                            ]
                        ) = device
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][
                    serial
                ] = device
                continue

            if (
                dev_name not in include_filter
                and device.get("capabilities")
                and not any(
                    x in device["capabilities"]
                    for x in ["MUSIC_SKILL", "TIMERS_AND_ALARMS", "REMINDERS"]
                )
            ):
                # skip devices without music or notification skill
                _LOGGER.debug("Excluding %s for lacking capability", dev_name)
                continue

            if bluetooth is not None and "bluetoothStates" in bluetooth:
                for b_state in bluetooth["bluetoothStates"]:
                    if serial == b_state["deviceSerialNumber"]:
                        device["bluetooth_state"] = b_state
                        break

            if preferences is not None and "devicePreferences" in preferences:
                for dev in preferences["devicePreferences"]:
                    if dev["deviceSerialNumber"] == serial:
                        device["locale"] = dev["locale"]
                        device["timeZoneId"] = dev["timeZoneId"]
                        _LOGGER.debug(
                            "%s: Locale %s timezone %s",
                            dev_name,
                            device["locale"],
                            device["timeZoneId"],
                        )
                        break

            if dnd is not None and "doNotDisturbDeviceStatusList" in dnd:
                for dev in dnd["doNotDisturbDeviceStatusList"]:
                    if dev["deviceSerialNumber"] == serial:
                        device["dnd"] = dev["enabled"]
                        _LOGGER.debug("%s: DND %s", dev_name, device["dnd"])
                        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
                            "switch"
                        ].setdefault(serial, {"dnd": True})
                        break

            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["auth_info"] = device[
                "auth_info"
            ] = auth_info
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"]["media_player"][
                serial
            ] = device

            if serial not in existing_serials:
                new_alexa_clients.append(dev_name)
            elif (
                serial in existing_serials
                and hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
                    "media_player"
                ].get(serial)
                and hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
                    "media_player"
                ]
                .get(serial)
                .enabled
            ):
                await (
                    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
                        "media_player"
                    ]
                    .get(serial)
                    .refresh(device, skip_api=True)
                )
        _LOGGER.debug(
            "%s: Existing: %s New: %s;"
            " Filtered out by not being in include: %s "
            "or in exclude: %s",
            hide_email(email),
            list(existing_entities),
            new_alexa_clients,
            include_filter,
            exclude_filter,
        )

        _LOGGER.debug("[BOOT] device processing in %.2fs", time.monotonic() - _t_proc)

        if new_alexa_clients:
            cleaned_config = config.copy()
            cleaned_config.pop(CONF_PASSWORD, None)
            # CONF_PASSWORD contains sensitive info which is no longer needed
            # Load multiple platforms in parallel using async_forward_entry_setups
            _LOGGER.debug("Loading platforms: %s", ", ".join(ALEXA_COMPONENTS))
            try:
                _t = time.monotonic()
                await hass.config_entries.async_forward_entry_setups(
                    config_entry, ALEXA_COMPONENTS
                )
                _LOGGER.debug("[BOOT] platform loading in %.2fs", time.monotonic() - _t)
                if metrics:
                    metrics.record_boot_stage(f"platforms_loaded_{hide_email(email)}")
            except (asyncio.TimeoutError, TimeoutException) as ex:
                _LOGGER.error(f"Error while loading platforms: {ex}")
                raise ConfigEntryNotReady(
                    f"Timeout while loading platforms: {ex}"
                ) from ex

        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False
        # prune stale devices
        device_registry = dr.async_get(hass)
        entity_backed_ids = _entity_backed_device_identifiers(
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]
        )
        for device_entry in dr.async_entries_for_config_entry(
            device_registry, config_entry.entry_id
        ):
            for _, identifier in device_entry.identifiers:
                if (
                    identifier
                    in hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
                        "media_player"
                    ]
                    or identifier
                    in map(
                        lambda x: slugify(f"{x}_{email}"),
                        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
                            "media_player"
                        ].keys(),
                    )
                    or identifier in entity_backed_ids
                ):
                    break
            else:
                device_registry.async_remove_device(device_entry.id)
                _LOGGER.debug(
                    "%s: Removing stale device %s",
                    hide_email(email),
                    device_entry.name,
                )

        await login_obj.save_cookiefile()
        if login_obj.access_token:
            hass.config_entries.async_update_entry(
                config_entry,
                data={
                    **config_entry.data,
                    CONF_OAUTH: {
                        "access_token": login_obj.access_token,
                        "refresh_token": login_obj.refresh_token,
                        "expires_in": login_obj.expires_in,
                        "mac_dms": login_obj.mac_dms,
                        "code_verifier": login_obj.code_verifier,
                        "authorization_code": login_obj.authorization_code,
                    },
                },
            )

        if first_run or not _push_healthy(account):
            if _network_allowed(login_obj):
                trigger = account.get("last_called_probe_trigger")
                if callable(trigger):
                    trigger("POLL_REFRESH", None)
                else:
                    # fallback if probe not initialized for some reason
                    hass.async_create_background_task(
                        _async_update_last_called_global(hass, login_obj, email),
                        f"{DOMAIN}_last_called_poll_{hide_email(email)}",
                    )
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["first_run"] = False

        return entity_state

    @_catch_login_errors
    async def process_notifications(login_obj, raw_notifications=None) -> bool:
        """Process raw notifications json.

        Returns True if notifications were updated, False if we skipped
        (e.g. due to cooldown or alexapy returned None).
        """
        email: str = login_obj.email
        account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][email]

        if raw_notifications is None:
            now = time.time()
            last = account_dict.get("last_notif_poll", 0.0)
            delta = now - last

            if delta < NOTIFICATION_COOLDOWN:
                _LOGGER.debug(
                    "%s: Skipping get_notifications; last poll %.1fs ago "
                    "(cooldown %ss).",
                    hide_email(email),
                    delta,
                    NOTIFICATION_COOLDOWN,
                )
                return False

            account_dict["last_notif_poll"] = now

            # Small delay to let Alexa settle if we're polling explicitly
            await asyncio.sleep(4)
            raw_notifications = await AlexaAPI.get_notifications(login_obj)

        previous = account_dict.get("notifications", {})
        notifications = {"process_timestamp": dt.utcnow()}

        if raw_notifications is not None:
            for notification in raw_notifications:
                n_dev_id = notification.get("deviceSerialNumber")
                if n_dev_id is None:
                    # skip notifications untied to a device for now
                    # https://github.com/alandtse/alexa_media_player/issues/633#issuecomment-610705651
                    continue
                n_type = notification.get("type")
                if n_type is None:
                    continue
                if n_type == "MusicAlarm":
                    n_type = "Alarm"
                n_id = notification["notificationIndex"]
                if n_type == "Alarm":
                    n_date = notification.get("originalDate")
                    n_time = notification.get("originalTime")
                    notification["date_time"] = (
                        f"{n_date} {n_time}" if n_date and n_time else None
                    )
                    previous_alarm = safe_get(previous, [n_dev_id, "Alarm", n_id], {})
                    if previous_alarm and alarm_just_dismissed(
                        notification,
                        previous_alarm.get("status"),
                        previous_alarm.get("version"),
                    ):
                        hass.bus.async_fire(
                            "alexa_media_alarm_dismissal_event",
                            event_data={
                                "device": {"id": n_dev_id},
                                "event": notification,
                            },
                        )

                if n_dev_id not in notifications:
                    notifications[n_dev_id] = {}
                if n_type not in notifications[n_dev_id]:
                    notifications[n_dev_id][n_type] = {}
                notifications[n_dev_id][n_type][n_id] = notification

        account_dict["notifications"] = notifications
        _LOGGER.debug(
            "%s: Updated %s notifications for %s devices at %s",
            hide_email(email),
            len(raw_notifications) if raw_notifications is not None else 0,
            len(notifications),
            dt.as_local(account_dict["notifications"]["process_timestamp"]),
        )
        # Notify sensors that the notifications snapshot has been refreshed
        async_dispatcher_send(
            hass,
            f"{DOMAIN}_{hide_email(email)}"[0:32],
            {"notifications_refreshed": True},
        )
        return True

    # ---------------------------------------------------------------------
    # Near-real-time last_called probing (customer history), single worker
    # Initialize ONCE per account (setup_alexa), not inside http2_handler.
    # ---------------------------------------------------------------------
    def _init_last_called_probe_worker(account: dict) -> None:
        """Initialize per-account last_called probe worker trigger function."""
        account.setdefault("last_called_api_lock", asyncio.Lock())
        account.setdefault(
            "last_called_customer_history_ts", 0
        )  # ms epoch last applied
        account.setdefault("last_called_probe_backoff_s", 0.0)
        account.setdefault("last_called_probe_event", asyncio.Event())
        account.setdefault("last_called_probe_next_allowed", 0.0)  # monotonic seconds
        account.setdefault("last_called_probe_task", None)
        account.setdefault("last_called_probe_trigger_cmd", "")
        account.setdefault("last_called_probe_trigger_serial", None)
        account.setdefault("last_called_probe_trigger_ts", 0)  # newest push ts (ms)
        account.setdefault("last_called_probe_trigger", None)
        account.setdefault("last_called_activity_queue", [])
        account.setdefault("last_called_last_pushed_activity", {})
        account.setdefault("last_volumes", {})
        account.setdefault("last_equalizer", {})

        if callable(account.get("last_called_probe_trigger")):
            return

        async def _last_called_probe_worker() -> None:
            """Single worker per account: debounce bursts, then quick-retry until history >= trigger_ts."""
            skip_debounce = False
            try:
                while True:
                    accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
                    account_live = accounts.get(email)
                    if not account_live:
                        return

                    def _debug(msg, *args):
                        if debug:
                            _LOGGER.debug("%s: " + msg, hide_email(email), *args)

                    login_live = account_live.get("login_obj")
                    if not login_live or not _network_allowed(login_live):
                        await asyncio.sleep(5)
                        continue
                    # Stop this worker if the login object has been torn down (entry
                    # reload/unload) so it cannot orphan onto a dead login and keep
                    # hammering the API across reloads.
                    if getattr(login_live, "close_requested", False) or (
                        getattr(login_live, "session", None) is not None
                        and login_live.session.closed
                    ):
                        return
                    # Skip probing while the login is unhealthy: the request would be
                    # built with a missing csrf / None header and raise a serialization
                    # TypeError, and hammering the history API while logged-out can
                    # hinder re-auth. Poll quietly until the login recovers.
                    _status = getattr(login_live, "status", None)
                    if not (_status and _status.get("login_successful")):
                        await asyncio.sleep(15)
                        continue
                    # Bounded wait so that if the login is torn down while we are
                    # idle here, the next iteration's teardown guard exits promptly
                    # instead of parking on the event forever.
                    try:
                        await asyncio.wait_for(
                            account_live["last_called_probe_event"].wait(),
                            timeout=15,
                        )
                    except asyncio.TimeoutError:
                        continue
                    account_live["last_called_probe_event"].clear()

                    if not skip_debounce:
                        await asyncio.sleep(
                            LAST_CALLED_DEBOUNCE_S
                            + random.uniform(0.0, 0.05)  # nosec B311  # noqa: S311
                        )
                        if account_live["last_called_probe_event"].is_set():
                            continue
                    skip_debounce = False

                    preempted = False
                    while True:
                        now = time.monotonic()
                        next_allowed = float(
                            account_live.get("last_called_probe_next_allowed", 0.0)
                            or 0.0
                        )
                        delay = max(0.0, next_allowed - now)
                        if delay <= 0:
                            break
                        try:
                            await asyncio.wait_for(
                                account_live["last_called_probe_event"].wait(),
                                timeout=delay,
                            )
                            account_live["last_called_probe_event"].clear()
                            preempted = True
                            break
                        except asyncio.TimeoutError:
                            break
                    if preempted:
                        continue

                    trigger_cmd = str(
                        account_live.get("last_called_probe_trigger_cmd") or "push"
                    )

                    for attempt in range(LAST_CALLED_RETRY_LIMIT + 1):
                        if account_live["last_called_probe_event"].is_set():
                            break

                        try:
                            async with account_live["last_called_api_lock"]:
                                queue_snapshot = _snapshot_last_called_activity_queue(
                                    account_live
                                )
                                if not queue_snapshot:
                                    if trigger_cmd in (
                                        "GLOBAL_REFRESH",
                                        "SERVICE_REFRESH",
                                        "POLL_REFRESH",
                                    ):
                                        last_called = (
                                            await AlexaAPI.get_last_device_serial(
                                                login_live, items=LAST_CALLED_ITEMS
                                            )
                                        )
                                        if isinstance(
                                            last_called, dict
                                        ) and _valid_voice_summary(
                                            last_called.get("summary")
                                        ):
                                            await update_last_called(
                                                login_live, last_called
                                            )
                                        account_live["last_called_probe_trigger_ts"] = 0
                                    break

                                earliest_ts = min(
                                    (
                                        int(item.get("activity_ts") or 0)
                                        for item in queue_snapshot
                                        if item.get("activity_ts")
                                    ),
                                    default=0,
                                )

                                rvh_window_ms = max(
                                    LAST_CALLED_LOOKBACK_MS, 15 * 60 * 1000
                                )
                                start_time = (
                                    max(0, earliest_ts - rvh_window_ms)
                                    if earliest_ts
                                    else int((time.time() * 1000) - rvh_window_ms)
                                )
                                end_time = int(time.time() * 1000) + rvh_window_ms
                                max_record_size = max(
                                    LAST_CALLED_ITEMS, len(queue_snapshot) + 2
                                )

                            try:
                                records = await AlexaAPI.get_customer_history_records(
                                    login_live,
                                    start_time=start_time,
                                    end_time=end_time,
                                    max_record_size=max_record_size,
                                )
                            except TypeError as exc:
                                # A serialization TypeError (e.g. a None header key
                                # when the login is half-torn-down) is not transient
                                # within a session: back off and do NOT re-arm. The
                                # old behavior crash-looped ~every 11s, hammering the
                                # API. Wait for the next genuine push trigger instead.
                                backoff = max(LAST_CALLED_CONN_BACKOFF_S, 60.0)
                                account_live["last_called_probe_next_allowed"] = (
                                    time.monotonic() + backoff
                                )
                                _LOGGER.warning(
                                    "%s: last_called probe API TypeError (%s): %s; "
                                    "backing off, not re-arming",
                                    hide_email(email),
                                    trigger_cmd,
                                    exc,
                                )
                                break

                            if records is None:
                                _LOGGER.warning(
                                    "%s: last_called probe API returned None (%s)",
                                    hide_email(email),
                                    trigger_cmd,
                                )
                                records = []

                            # 🔎 DEBUG: inspect raw history result
                            _LOGGER.debug(
                                "%s: last_called probe retrieved %s history records",
                                hide_email(email),
                                len(records or []),
                            )
                            if isinstance(records, list):
                                _debug(
                                    "last_called probe raw records (%s): %s",
                                    trigger_cmd,
                                    [
                                        {
                                            "summary": (
                                                item.get("description") or {}
                                            ).get("summary"),
                                            "response": item.get("alexaResponse"),
                                            "serial": item.get("deviceSerialNumber"),
                                            "ts": item.get("creationTimestamp"),
                                            "utteranceType": item.get("utteranceType"),
                                        }
                                        for item in records[:10]
                                        if isinstance(item, dict)
                                    ],
                                )
                            else:
                                _debug(
                                    "last_called probe returned unexpected type (%s): %r",
                                    trigger_cmd,
                                    records,
                                )

                        except asyncio.CancelledError:
                            raise
                        except AlexapyTooManyRequestsError:
                            uk_floor = random.uniform(  # noqa: S311
                                30.0, 63.0
                            )  # nosec B311
                            prev = float(
                                account_live.get("last_called_probe_backoff_s", 0.0)
                                or 0.0
                            )
                            backoff = (
                                LAST_CALLED_429_BACKOFF_INITIAL_S
                                if prev <= 0.0
                                else min(prev * 2.0, LAST_CALLED_429_BACKOFF_MAX_S)
                            )
                            backoff = max(backoff, uk_floor)
                            jitter = random.uniform(  # noqa: S311
                                0.0, min(5.0, backoff * 0.1)
                            )  # nosec B311

                            account_live["last_called_probe_backoff_s"] = backoff
                            account_live["last_called_probe_next_allowed"] = (
                                time.monotonic() + backoff + jitter
                            )

                            _LOGGER.debug(
                                "%s: last_called probe rate-limited (%s); backing off %.1fs (%.1fs jitter) then self-retry",
                                hide_email(email),
                                trigger_cmd,
                                backoff,
                                jitter,
                            )
                            skip_debounce = True
                            account_live["last_called_probe_event"].set()
                            break
                        except AlexapyLoginError:
                            account_live["last_called_probe_next_allowed"] = (
                                time.monotonic() + LAST_CALLED_LOGIN_BACKOFF_S
                            )
                            _LOGGER.debug(
                                "%s: last_called probe login error (%s); skipping",
                                hide_email(email),
                                trigger_cmd,
                            )
                            report_relogin_required(hass, login_live, email)
                            break
                        except AlexapyConnectionError as exc:
                            account_live["last_called_probe_next_allowed"] = (
                                time.monotonic() + LAST_CALLED_CONN_BACKOFF_S
                            )
                            _LOGGER.debug(
                                "%s: last_called probe connection error (%s): %s",
                                hide_email(email),
                                trigger_cmd,
                                exc,
                            )
                            break

                        existing_serials_local = set(
                            _existing_serials(hass, login_live)
                        )
                        existing_serials_local |= _entity_backed_serials(account_live)

                        payload, resolved_keys = (
                            _select_last_called_payload_from_records(
                                records,
                                queue_snapshot,
                                account_live,
                                existing_serials_local,
                            )
                        )

                        _debug(
                            "last_called queue match (%s): queued=%s matched=%s resolved=%s",
                            trigger_cmd,
                            [
                                {
                                    "serial": item.get("serial"),
                                    "customer_id": item.get("customer_id"),
                                    "activity_ts": item.get("activity_ts"),
                                    "command": item.get("command"),
                                }
                                for item in queue_snapshot
                            ],
                            (
                                {
                                    "serialNumber": payload.get("serialNumber"),
                                    "timestamp": payload.get("timestamp"),
                                    "summary": payload.get("summary"),
                                    "response": payload.get("response"),
                                }
                                if payload
                                else None
                            ),
                            sorted(resolved_keys),
                        )

                        if not payload:
                            if attempt < LAST_CALLED_RETRY_LIMIT:
                                await asyncio.sleep(LAST_CALLED_RETRY_DELAY_S)
                                continue

                            if trigger_cmd in (
                                "GLOBAL_REFRESH",
                                "SERVICE_REFRESH",
                                "POLL_REFRESH",
                            ):
                                _LOGGER.debug(
                                    "%s: queued activity unresolved after retries; falling back to direct refresh",
                                    hide_email(email),
                                )

                                unresolved_keys = {
                                    (item.get("serial"), item.get("customer_id"))
                                    for item in queue_snapshot
                                    if item.get("serial")
                                }
                                _remove_last_called_activity_queue_entries(
                                    account_live, unresolved_keys
                                )

                                try:
                                    async with account_live["last_called_api_lock"]:
                                        last_called = (
                                            await AlexaAPI.get_last_device_serial(
                                                login_live,
                                                items=LAST_CALLED_ITEMS,
                                            )
                                        )
                                    if isinstance(
                                        last_called, dict
                                    ) and _valid_voice_summary(
                                        last_called.get("summary")
                                    ):
                                        await update_last_called(
                                            login_live, last_called
                                        )
                                except asyncio.CancelledError:
                                    raise
                                except AlexapyLoginError as exc:
                                    _LOGGER.debug(
                                        "%s: fallback last_called refresh failed (%s): %s",
                                        hide_email(email),
                                        trigger_cmd,
                                        exc,
                                    )
                                    report_relogin_required(hass, login_live, email)
                                except (
                                    AlexapyTooManyRequestsError,
                                    AlexapyConnectionError,
                                ) as exc:
                                    _LOGGER.debug(
                                        "%s: fallback last_called refresh failed (%s): %s",
                                        hide_email(email),
                                        trigger_cmd,
                                        exc,
                                    )
                                account_live["last_called_probe_trigger_ts"] = 0
                                account_live["last_called_probe_event"].clear()

                            break

                        account_live["last_called_probe_backoff_s"] = 0.0
                        account_live["last_called_probe_next_allowed"] = (
                            time.monotonic()
                            + LAST_CALLED_SUCCESS_PACE_S
                            + random.uniform(0.0, 0.25)  # nosec B311  # noqa: S311
                        )

                        trigger_serial = account_live.get(
                            "last_called_probe_trigger_serial"
                        )
                        _LOGGER.debug(
                            "%s: Updating last_called via %s (triggered by %s): %s",
                            hide_email(email),
                            trigger_cmd,
                            (
                                hide_serial(trigger_serial)
                                if trigger_serial
                                else "unknown"
                            ),
                            hide_serial(payload["serialNumber"]),
                        )

                        await update_last_called(login_live, payload)
                        account_live["last_called_last_pushed_activity"][
                            payload["serialNumber"]
                        ] = payload["timestamp"]
                        _remove_last_called_activity_queue_entries(
                            account_live, resolved_keys
                        )
                        account_live["last_called_probe_trigger_ts"] = 0
                        account_live["last_called_probe_event"].clear()
                        break
            except asyncio.CancelledError:
                raise

            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception(
                    "%s: last_called probe worker crashed",
                    hide_email(email),
                )

        def _trigger_last_called_probe(
            trigger_command: str, trigger_ts_ms: int | None
        ) -> None:
            """Record newest trigger + wake worker. Does NOT cancel running worker."""
            accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
            account_live = accounts.get(email)
            if not account_live:
                return

            if trigger_ts_ms is not None:
                try:
                    ts = int(trigger_ts_ms)
                except (TypeError, ValueError):
                    ts = 0

                prev = int(account_live.get("last_called_probe_trigger_ts") or 0)
                if ts > prev:
                    account_live["last_called_probe_trigger_ts"] = ts
                account_live["last_called_probe_trigger_cmd"] = trigger_command
            else:
                # Manual refresh triggers clear any push watermark
                if trigger_command in (
                    "GLOBAL_REFRESH",
                    "SERVICE_REFRESH",
                    "POLL_REFRESH",
                ):
                    account_live["last_called_probe_trigger_ts"] = 0
                    account_live["last_called_probe_trigger_serial"] = None
                account_live["last_called_probe_trigger_cmd"] = trigger_command

            task = account_live.get("last_called_probe_task")
            if task is None or task.done():
                account_live["last_called_probe_task"] = (
                    hass.async_create_background_task(
                        _last_called_probe_worker(),
                        name=f"{DOMAIN}_last_called_probe_{hide_email(email)}",
                    )
                )

            account_live["last_called_probe_event"].set()

        # Store the trigger on the live account as well (so reload swaps don't strand it)
        account["last_called_probe_trigger"] = _trigger_last_called_probe

    def _is_dnd_voice_toggle(last_called: dict) -> bool:
        summary = " ".join(((last_called.get("summary") or "").strip().lower()).split())
        response = " ".join(
            ((last_called.get("response") or "").strip().lower()).split()
        )

        # Normalize apostrophes (replace smart quotes with ASCII)
        summary = summary.replace("\u2019", "'").replace("\u2018", "'")
        response = response.replace("\u2019", "'").replace("\u2018", "'")

        return (
            "do not disturb" in summary
            or "won't disturb you" in response
            or "do not disturb is now off" in response
        )

    @_catch_login_errors
    async def update_last_called(login_obj, last_called=None, force=False):
        """Update the last called device for the login_obj.

        Stores the last_called in hass.data and fires an event to notify listeners.
        Delegates storage/dispatch to the module-level _store_and_dispatch_last_called helper.
        """
        if not isinstance(last_called, dict) or not last_called.get("summary"):
            try:
                # Serialize calls per account to avoid parallel rate-limited requests.
                account = (
                    hass.data.get(DATA_ALEXAMEDIA, {})
                    .get("accounts", {})
                    .get(email, {})
                )
                api_lock = account.get("last_called_api_lock") if account else None

                if api_lock is None:
                    last_called = await AlexaAPI.get_last_device_serial(login_obj)
                else:
                    async with api_lock:
                        last_called = await AlexaAPI.get_last_device_serial(login_obj)

            except asyncio.CancelledError:
                raise

            except AlexapyTooManyRequestsError:
                _LOGGER.debug(
                    "%s: Rate limited during last_called update; skipping",
                    hide_email(email),
                )
                return

            except AlexapyLoginError:
                _LOGGER.debug(
                    "%s: Login error during last_called update",
                    hide_email(email),
                )
                report_relogin_required(hass, login_obj, email)
                return

            except AlexapyConnectionError as exc:
                _LOGGER.debug(
                    "%s: Connection error during last_called update: %s",
                    hide_email(email),
                    exc,
                )
                return

            except TypeError:
                _LOGGER.debug(
                    "%s: Error updating last_called: %s",
                    hide_email(email),
                    repr(last_called),
                )
                return

            if not isinstance(last_called, dict):
                _LOGGER.debug(
                    "%s: Error updating last_called: unexpected response %s",
                    hide_email(email),
                    repr(last_called),
                )
                return

        # Central voice-only gate
        if not _valid_voice_summary(last_called.get("summary")):
            _LOGGER.debug(
                "%s: Ignoring last_called with invalid/non-voice summary: %s",
                hide_email(email),
                repr(last_called.get("summary")),
            )
            return

        _LOGGER.debug(
            "%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)
        )
        _store_and_dispatch_last_called(hass, email, last_called, force)

        if _is_dnd_voice_toggle(last_called):
            _LOGGER.debug(
                "%s: last_called indicates DND voice toggle", hide_email(email)
            )
            await update_dnd_state(login_obj)

    @_catch_login_errors
    async def update_bluetooth_state(login_obj, device_serial):
        """Update the bluetooth state on ws bluetooth event."""
        bluetooth = await AlexaAPI.get_bluetooth(login_obj)
        device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
            "media_player"
        ][device_serial]

        if bluetooth is not None and "bluetoothStates" in bluetooth:
            for b_state in bluetooth["bluetoothStates"]:
                if device_serial == b_state["deviceSerialNumber"]:
                    _LOGGER.debug(
                        "%s: setting value for: %s to %s",
                        hide_email(email),
                        hide_serial(device_serial),
                        hide_serial(b_state),
                    )
                    device["bluetooth_state"] = b_state
                    return device["bluetooth_state"]
        _LOGGER.debug(
            "%s: get_bluetooth for: %s failed with %s",
            hide_email(email),
            hide_serial(device_serial),
            hide_serial(bluetooth),
        )
        return None

    async def schedule_update_dnd_state(email: str) -> None:
        """Run one deferred DND refresh after the cooldown expires."""
        try:
            while True:
                async with dnd_update_lock:
                    if not pending_dnd_updates.get(email, False):
                        scheduled_dnd_tasks.pop(email, None)
                        return

                    last_run = last_dnd_update_times.get(email)
                    now = datetime.utcnow()

                    remaining = 0.0
                    if last_run is not None:
                        elapsed = now - last_run
                        if elapsed < MIN_TIME_BETWEEN_SCANS:
                            remaining = (
                                MIN_TIME_BETWEEN_SCANS - elapsed
                            ).total_seconds()

                if remaining > 0:
                    _LOGGER.debug(
                        "%s: Deferred DND update sleeping %.3fs until cooldown expires",
                        hide_email(email),
                        remaining,
                    )
                    await asyncio.sleep(remaining)

                async with dnd_update_lock:
                    if not pending_dnd_updates.get(email, False):
                        scheduled_dnd_tasks.pop(email, None)
                        return

                    last_run = last_dnd_update_times.get(email)
                    now = datetime.utcnow()
                    if (
                        last_run is not None
                        and (now - last_run) < MIN_TIME_BETWEEN_SCANS
                    ):
                        # Another update snuck in or timing was slightly early; loop and re-evaluate.
                        continue

                    pending_dnd_updates[email] = False
                    scheduled_dnd_tasks.pop(email, None)

                login_obj = (
                    hass.data.get(DATA_ALEXAMEDIA, {})
                    .get("accounts", {})
                    .get(email, {})
                    .get("login_obj")
                )
                if not login_obj:
                    _LOGGER.debug(
                        "%s: Skipping scheduled forced DND update: login_obj missing",
                        hide_email(email),
                    )
                    return

                _LOGGER.debug(
                    "%s: Executing scheduled forced DND update",
                    hide_email(email),
                )
                await update_dnd_state(login_obj)
                return

        except asyncio.CancelledError:
            _LOGGER.debug("%s: Deferred DND update task cancelled", hide_email(email))
            raise
        finally:
            async with dnd_update_lock:
                task = scheduled_dnd_tasks.get(email)
                if task is asyncio.current_task():
                    scheduled_dnd_tasks.pop(email, None)

    @_catch_login_errors
    async def update_dnd_state(login_obj) -> None:
        """Update the DND state on websocket DND combo event."""
        email = login_obj.email
        now = datetime.utcnow()

        async with dnd_update_lock:
            last_run = last_dnd_update_times.get(email)

            if last_run is not None and (now - last_run) < MIN_TIME_BETWEEN_SCANS:
                pending_dnd_updates[email] = True

                if (
                    email not in scheduled_dnd_tasks
                    or scheduled_dnd_tasks[email].done()
                ):
                    _LOGGER.debug(
                        "%s: Throttling active; scheduling deferred DND update.",
                        hide_email(email),
                    )
                    scheduled_dnd_tasks[email] = asyncio.create_task(
                        schedule_update_dnd_state(email)
                    )
                else:
                    _LOGGER.debug(
                        "%s: Throttling active; deferred DND update already scheduled.",
                        hide_email(email),
                    )
                return

            last_dnd_update_times[email] = now

        _LOGGER.debug("%s: Updating DND state", hide_email(email))
        try:
            dnd = await AlexaAPI.get_dnd_state(login_obj)
        except asyncio.TimeoutError:
            _LOGGER.error(
                "%s: Timeout occurred while fetching DND state",
                hide_email(email),
            )
            return
        except Exception as err:  # pylint: disable=broad-except
            _LOGGER.error(
                "%s: Unexpected error while fetching DND state: %s",
                hide_email(email),
                err,
            )
            return

        if dnd is not None and "doNotDisturbDeviceStatusList" in dnd:
            async_dispatcher_send(
                hass,
                f"{DOMAIN}_{hide_email(email)}"[0:32],
                {"dnd_update": dnd["doNotDisturbDeviceStatusList"]},
            )
            return

        _LOGGER.debug("%s: get_dnd_state failed: dnd:%s", hide_email(email), dnd)

    def _schedule_notifications_refresh(
        hass,
        email: str,
        device_serial: str | None = None,
        reason: str = "",
    ) -> None:
        """Mark notifications as needing refresh and ensure worker task is running.

        device_serial is just for debug; we track a set of pending devices but
        we always refresh the full notifications payload once.
        """

        account = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {}).get(email)
        if not account:
            return

        if device_serial:
            account["notifications_pending"].add(device_serial)
        else:
            # Special marker for "global" changes if you want one
            account["notifications_pending"].add("*")

        if reason:
            _LOGGER.debug(
                "%s: Scheduling notifications refresh (reason=%s, pending=%s)",
                hide_email(email),
                reason,
                account["notifications_pending"],
            )

        task = account.get("notifications_refresh_task")
        if task is not None and not task.done():
            # Already have a running worker; it'll see the new pending set
            return

        # Start new worker
        account["notifications_refresh_task"] = hass.async_create_task(
            _run_notifications_refresh(hass, email)
        )

    async def _run_notifications_refresh(hass, email: str) -> None:
        """Worker task: refresh notifications for an account if pending.

        - Uses alexapy.AlexaAPI.get_notifications(login)
        - Retries a few times if we only get None (cooldown/throttle)
        - Clears notifications_pending when successful or when we give up
        """
        account = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {}).get(email)
        if not account:
            return
        login = account.get("login_obj")
        if not login:
            return

        try:
            retries = 0
            while (
                account.get("notifications_pending", set())
                and retries <= NOTIFY_REFRESH_MAX_RETRIES
            ):
                try:
                    data = await AlexaAPI.get_notifications(login)
                except Exception as ex:
                    _LOGGER.warning(
                        "%s: get_notifications raised %s; treating as None. This may indicate an unexpected error.",
                        hide_email(email),
                        ex,
                    )
                    data = None

                if data is not None:
                    # Success: update through the normal processing path
                    await process_notifications(login, raw_notifications=data)
                    account["notifications_retry_count"] = 0
                    account["notifications_pending"].clear()

                    _LOGGER.debug(
                        "%s: Refreshed notifications snapshot (pending cleared)",
                        hide_email(email),
                    )
                    return

                # If we get here, alexapy side returned None (cooldown / throttle)
                retries += 1
                account["notifications_retry_count"] = retries

                if not account["notifications_pending"]:
                    # Nothing to do anymore, bail early
                    break

                _LOGGER.debug(
                    "%s: Notifications refresh returned None (retry %s/%s); "
                    "pending=%s; sleeping %.1fs",
                    hide_email(email),
                    retries,
                    NOTIFY_REFRESH_MAX_RETRIES,
                    account["notifications_pending"],
                    NOTIFY_REFRESH_BACKOFF,
                )
                await asyncio.sleep(NOTIFY_REFRESH_BACKOFF)

            # If we fall through, give up for now but leave pending set alone
            if account["notifications_pending"]:
                _LOGGER.debug(
                    "%s: Giving up notifications refresh after %s attempts; "
                    "still pending=%s",
                    hide_email(email),
                    retries,
                    account["notifications_pending"],
                )

        finally:
            # Always clear the task pointer so future pushes can schedule again
            account["notifications_refresh_task"] = None

    async def http2_connect() -> HTTP2EchoClient:
        """Open HTTP2 Push connection.

        This will only attempt one login before failing.
        """
        http2: Optional[HTTP2EchoClient] = None
        email = login_obj.email
        try:
            if login_obj.session.closed:
                _LOGGER.debug(
                    "%s: HTTP2 creation aborted. Session is closed.",
                    hide_email(email),
                )
                return
            http2 = HTTP2EchoClient(
                login_obj,
                msg_callback=http2_handler,
                open_callback=http2_open_handler,
                close_callback=http2_close_handler,
                error_callback=http2_error_handler,
                loop=hass.loop,
            )
            _LOGGER.debug("%s: Starting HTTP2: %s", hide_email(email), http2)
            await http2.async_run()
        except AlexapyLoginError as exception_:
            _LOGGER.debug(
                "%s: Login Error detected from http2: %s",
                hide_email(email),
                exception_,
            )
            hass.bus.async_fire(
                "alexa_media_relogin_required",
                event_data={"email": hide_email(email), "url": login_obj.url},
            )
            return
        except BaseException as exception_:  # pylint: disable=broad-except
            _LOGGER.debug(
                "%s: HTTP2 creation failed: %s", hide_email(email), exception_
            )
            return
        _LOGGER.debug("%s: HTTP2 created: %s", hide_email(email), http2)
        return http2

    @callback
    async def http2_handler(message_obj):
        # pylint: disable=too-many-branches,too-many-statements
        """Handle http2 push messages.

        This allows push notifications from Alexa to update last_called and media state.
        """

        coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("coordinator")
        account = hass.data[DATA_ALEXAMEDIA]["accounts"][email]

        def _now_ms() -> int:
            return int(time.time() * 1000)

        def simulate_activity(
            device_serial: str,
            customer_id: str | None,
            trigger_command: str,
            trigger_ts_ms: int | None,
        ) -> None:
            _queue_last_called_activity(
                account,
                device_serial=device_serial,
                customer_id=customer_id,
                activity_ts=trigger_ts_ms,
                command=trigger_command,
            )
            account["last_called_probe_trigger_serial"] = device_serial
            trigger = account.get("last_called_probe_trigger")
            if callable(trigger):
                trigger(trigger_command, trigger_ts_ms)

        def _handle_volume_change_activity(
            serial: str,
            json_payload: dict,
            trigger_ts_ms: int | None,
        ) -> None:
            """Mirror alexa-remote.ts PUSH_VOLUME_CHANGE -> simulateActivity() conditions."""
            last_volumes: dict = account["last_volumes"]
            last_equalizer: dict = account["last_equalizer"]

            vol = json_payload.get("volumeSetting")
            muted = json_payload.get("isMuted")

            prev = last_volumes.get(serial)
            eq_prev = last_equalizer.get(serial)

            should_simulate = (
                eq_prev is not None
                and abs(_now_ms() - int(eq_prev.get("updated", 0)))
                < LAST_CALLED_COALESCE_WINDOW_MS
            )

            if should_simulate:
                _LOGGER.debug(
                    "[_handle_volume_change_activity] Simulating activity",
                )
                simulate_activity(
                    serial,
                    json_payload.get("destinationUserId"),
                    "PUSH_VOLUME_CHANGE",
                    trigger_ts_ms,
                )
            else:
                _LOGGER.debug(
                    "[_handle_volume_change_activity] Not simulating activity",
                )

            last_volumes[serial] = {
                "volumeSetting": vol,
                "isMuted": muted,
                "updated": _now_ms(),
            }

        def _handle_equalizer_change_activity(
            serial: str,
            json_payload: dict,
            trigger_ts_ms: int | None,
        ) -> None:
            """Mirror alexa-remote.ts PUSH_EQUALIZER_STATE_CHANGE -> simulateActivity() conditions."""
            last_volumes: dict = account["last_volumes"]
            last_equalizer: dict = account["last_equalizer"]

            bass = json_payload.get("bass")
            treble = json_payload.get("treble")
            midrange = json_payload.get("midrange")

            prev = last_equalizer.get(serial)
            vol_prev = last_volumes.get(serial)

            should_simulate = (
                prev is not None
                and prev.get("bass") == bass
                and prev.get("treble") == treble
                and prev.get("midrange") == midrange
            ) or (
                vol_prev is not None
                and abs(_now_ms() - int(vol_prev.get("updated", 0)))
                < LAST_CALLED_COALESCE_WINDOW_MS
            )

            if should_simulate:
                simulate_activity(
                    serial,
                    json_payload.get("destinationUserId"),
                    "PUSH_EQUALIZER_STATE_CHANGE",
                    trigger_ts_ms,
                )

            last_equalizer[serial] = {
                "bass": bass,
                "treble": treble,
                "midrange": midrange,
                "updated": _now_ms(),
            }

        # ---------------------------------------------------------------------
        # Main http2push parsing / dispatch
        # ---------------------------------------------------------------------
        updates = (
            message_obj.get("directive", {})
            .get("payload", {})
            .get("renderingUpdates", [])
        )
        existing_serials = set(_existing_serials(hass, login_obj))
        existing_serials |= _entity_backed_serials(account)
        for item in updates:
            try:
                resource = loads(item.get("resourceMetadata", ""))
            except JSONDecodeError:
                continue

            command = (
                resource["command"]
                if isinstance(resource, dict) and "command" in resource
                else None
            )
            try:
                json_payload = (
                    loads(resource["payload"])
                    if isinstance(resource, dict) and "payload" in resource
                    else None
                )
            except (JSONDecodeError, TypeError):
                _LOGGER.debug(
                    "%s: Skipping malformed push payload for command %s",
                    hide_email(email),
                    command,
                )
                continue
            seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                "http2_commands"
            ]

            if command and json_payload:
                _LOGGER.debug(
                    "%s: Received http2push command: %s : %s",
                    hide_email(email),
                    command,
                    hide_serial(json_payload),
                )

                account["last_push_activity"] = time.time()
                serial = None
                command_time = time.time()
                if command not in seen_commands:
                    _LOGGER.debug(
                        "Adding %s to seen_commands: %s", command, seen_commands
                    )
                seen_commands[command] = command_time

                if (
                    "dopplerId" in json_payload
                    and "deviceSerialNumber" in json_payload["dopplerId"]
                ):
                    serial = json_payload["dopplerId"]["deviceSerialNumber"]
                elif (
                    "key" in json_payload
                    and "entryId" in json_payload["key"]
                    and json_payload["key"]["entryId"].find("#") != -1
                ):
                    serial = (json_payload["key"]["entryId"]).split("#")[2]
                    json_payload["key"]["serialNumber"] = serial
                else:
                    serial = None

                if command in (
                    "PUSH_AUDIO_PLAYER_STATE",
                    "PUSH_MEDIA_CHANGE",
                    "PUSH_MEDIA_PROGRESS_CHANGE",
                    "NotifyMediaSessionsUpdated",
                    "NotifyNowPlayingUpdated",
                ):
                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating media_player: %s", hide_serial(json_payload)
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"player_state": json_payload},
                        )
                    elif command == "NotifyNowPlayingUpdated":
                        _LOGGER.debug("Send NowPlaying: %s", hide_serial(json_payload))
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"now_playing": json_payload},
                        )

                elif command == "PUSH_VOLUME_CHANGE":
                    try:
                        ts = resource.get("timeStamp")
                        ts_ms = int(ts) if ts else None

                        if serial and isinstance(json_payload, dict):
                            _handle_volume_change_activity(serial, json_payload, ts_ms)

                    except Exception:
                        _LOGGER.exception(
                            "%s: http2_handler failed processing %s",
                            hide_email(email),
                            command,
                        )

                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating media_player volume: %s",
                            hide_serial(json_payload),
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"player_state": json_payload},
                        )

                elif command == "PUSH_DOPPLER_CONNECTION_CHANGE":
                    # Player availability update
                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating media_player availability %s",
                            hide_serial(json_payload),
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"player_state": json_payload},
                        )

                elif command == "PUSH_EQUALIZER_STATE_CHANGE":
                    # Player equalizer update
                    try:
                        ts = resource.get("timeStamp")
                        ts_ms = int(ts) if ts else None

                        if serial and isinstance(json_payload, dict):
                            _handle_equalizer_change_activity(
                                serial, json_payload, ts_ms
                            )

                    except Exception:
                        _LOGGER.exception(
                            "%s: http2_handler failed processing %s",
                            hide_email(email),
                            command,
                        )

                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating media_player equalizer state %s",
                            hide_serial(json_payload),
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"player_state": json_payload},
                        )

                elif command == "PUSH_BLUETOOTH_STATE_CHANGE":
                    # Player bluetooth update
                    bt_event = (
                        json_payload.get("bluetoothEvent")
                        if isinstance(json_payload, dict)
                        else None
                    )
                    _LOGGER.debug("bt_event: %s", bt_event)
                    bt_success = (
                        json_payload.get("bluetoothEventSuccess")
                        if isinstance(json_payload, dict)
                        else None
                    )
                    _LOGGER.debug("bt_success: %s", bt_success)
                    if (
                        serial
                        and serial in existing_serials
                        and bt_success
                        and bt_event
                        and bt_event
                        in {
                            "DEVICE_CONNECTED",
                            "DEVICE_DISCONNECTED",
                            "STREAMING_STATE_CHANGED",
                        }
                    ):
                        _LOGGER.debug(
                            "Updating media_player bluetooth %s",
                            hide_serial(json_payload),
                        )
                        bluetooth_state = await update_bluetooth_state(
                            login_obj, serial
                        )
                        _LOGGER.debug(
                            "bluetooth_state %s", hide_serial(bluetooth_state)
                        )
                        if bluetooth_state:
                            async_dispatcher_send(
                                hass,
                                f"{DOMAIN}_{hide_email(email)}"[0:32],
                                {"bluetooth_change": bluetooth_state},
                            )

                elif command == "PUSH_MEDIA_QUEUE_CHANGE":
                    # Player availability update
                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating media_player queue %s",
                            hide_serial(json_payload),
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"queue_state": json_payload},
                        )

                elif command == "PUSH_NOTIFICATION_CHANGE":
                    # Notification/alarm state changed on this device.
                    # Queue a refresh with backoff to ride out alexa-side cooldowns.
                    _schedule_notifications_refresh(
                        hass,
                        email,
                        device_serial=serial,
                        reason="PUSH_NOTIFICATION_CHANGE",
                    )

                    if serial and serial in existing_serials:
                        _LOGGER.debug(
                            "Updating mediaplayer notifications: %s",
                            hide_serial(json_payload),
                        )
                        async_dispatcher_send(
                            hass,
                            f"{DOMAIN}_{hide_email(email)}"[0:32],
                            {"notification_update": json_payload},
                        )

                elif command in [
                    "PUSH_DELETE_DOPPLER_ACTIVITIES",  # Delete Alexa history,
                ]:
                    pass

                elif command in [
                    "PUSH_TODO_CHANGE",  # Update To-Do List
                    "PUSH_LIST_CHANGE",  # Clear a shopping list https://github.com/alandtse/alexa_media_player/issues/1190
                    "PUSH_LIST_ITEM_CHANGE",  # Update shopping list
                ]:
                    # To-do
                    _LOGGER.debug("%s currently not supported", command)
                    pass

                elif command in [
                    "PUSH_CONTENT_FOCUS_CHANGE",  # Likely prime related refocus
                    "PUSH_DEVICE_SETUP_STATE_CHANGE",  # Likely device changes mid setup
                ]:
                    _LOGGER.debug("%s currently not supported", command)
                    pass

                elif command in [
                    "PUSH_MEDIA_PREFERENCE_CHANGE",  # Disliking or liking songs, https://github.com/alandtse/alexa_media_player/issues/1599
                ]:
                    _LOGGER.debug("%s currently not supported", command)
                    pass

                elif command in [
                    "MATTER_SETUP_NOTIFICATION",  # New command observed 2026-02-20
                ]:
                    _LOGGER.debug("%s: New command; currently not supported", command)
                    pass

                else:
                    _LOGGER.debug(
                        "Unhandled command: %s with data %s. Please report at %s",
                        command,
                        hide_serial(json_payload),
                        ISSUE_URL,
                    )

                # Preserve existing http2 activity tracking + new-device discovery
                if serial in existing_serials:
                    history = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                        "http2_activity"
                    ]["serials"].get(serial)
                    if history is None or (
                        history and command_time - history[len(history) - 1][1] > 2
                    ):
                        history = [(command, command_time)]
                    else:
                        history.append([command, command_time])

                    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2_activity"][
                        "serials"
                    ][serial] = history

                    events = []
                    for old_command, old_command_time in history:
                        if (
                            old_command
                            in {"PUSH_VOLUME_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE"}
                            and command_time - old_command_time < 0.25
                        ):
                            events.append(
                                (old_command, round(command_time - old_command_time, 2))
                            )
                        elif old_command in {"PUSH_AUDIO_PLAYER_STATE"}:
                            events = []

                    if len(events) >= 4:
                        _LOGGER.debug(
                            "%s: Detected potential DND http2push change with %s events %s",
                            hide_serial(serial),
                            len(events),
                            events,
                        )
                        await update_dnd_state(login_obj)

                if (
                    serial
                    and serial not in existing_serials
                    and serial
                    not in hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                        "excluded"
                    ].keys()
                ):
                    _LOGGER.debug("Discovered new media_player %s", hide_serial(serial))
                    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = True
                    if coordinator:
                        await coordinator.async_request_refresh()

    @callback
    async def http2_open_handler():
        """Handle http2 open."""

        email: str = login_obj.email
        _LOGGER.debug("%s: HTTP2push successfully connected", hide_email(email))
        hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "http2error"
        ] = 0  # set errors to 0
        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2_lastattempt"] = time.time()

    @callback
    async def http2_close_handler():
        """Handle http2 close.

        This should attempt to reconnect up to 5 times
        """
        email: str = login_obj.email
        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2"] = None
        if login_obj.close_requested:
            _LOGGER.debug(
                "%s: Close requested; will not reconnect http2", hide_email(email)
            )
            return
        if not login_obj.status.get("login_successful"):
            _LOGGER.debug(
                "%s: Login error; will not reconnect http2", hide_email(email)
            )
            return
        errors: int = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"]
        delay: int = 5 * 2**errors
        last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "http2_lastattempt"
        ]
        now = time.time()
        if (now - last_attempt) < delay:
            return
        http2_client = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2"]
        http2_enabled = bool(http2_client)
        while errors < 5 and not (http2_enabled):
            _LOGGER.debug(
                "%s: HTTP2 push closed; reconnect #%i in %is",
                hide_email(email),
                errors,
                delay,
            )
            hass.data[DATA_ALEXAMEDIA]["accounts"][email][
                "http2_lastattempt"
            ] = time.time()
            http2_client = await http2_connect()
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2"] = http2_client
            http2_enabled = bool(http2_client)
            if http2_enabled:
                break
            errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"] = (
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"] + 1
            )
            delay = 5 * 2**errors
            errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"]
            await asyncio.sleep(delay)
        if not http2_enabled:
            _LOGGER.debug(
                "%s: HTTP2Push connection closed; retries exceeded; polling",
                hide_email(email),
            )
        coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("coordinator")
        if coordinator:
            coordinator.update_interval = timedelta(
                seconds=scan_interval * 10 if http2_enabled else scan_interval
            )
            _LOGGER.debug(
                "HTTP2push: %s, Polling interval: %s",
                http2_enabled,
                coordinator.update_interval,
            )
            await coordinator.async_request_refresh()

    @callback
    async def http2_error_handler(message):
        """Handle http2push error.

        This currently logs the error.  In the future, this should invalidate
        the http2push and determine if a reconnect should be done.
        """
        email: str = login_obj.email
        errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"]
        _LOGGER.debug(
            "%s: Received http2push error #%i %s: type %s",
            hide_email(email),
            errors,
            message,
            type(message),
        )
        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2"] = None
        if not login_obj.close_requested and (
            login_obj.session.closed or isinstance(message, AlexapyLoginError)
        ):
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"] = 5
            _LOGGER.debug("%s: Login error detected.", hide_email(email))
            hass.bus.async_fire(
                "alexa_media_relogin_required",
                event_data={"email": hide_email(email), "url": login_obj.url},
            )
            return
        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2error"] = errors + 1

    _LOGGER.debug("Setting up Alexa devices for %s", hide_email(login_obj.email))
    config = config_entry.data
    email = config.get(CONF_EMAIL)
    include = (
        cv.ensure_list_csv(config[CONF_INCLUDE_DEVICES])
        if config[CONF_INCLUDE_DEVICES]
        else ""
    )
    _LOGGER.debug("include: %s", include)
    exclude = (
        cv.ensure_list_csv(config[CONF_EXCLUDE_DEVICES])
        if config[CONF_EXCLUDE_DEVICES]
        else ""
    )
    _LOGGER.debug("exclude: %s", exclude)
    scan_interval: float = (
        config.get(CONF_SCAN_INTERVAL).total_seconds()
        if isinstance(config.get(CONF_SCAN_INTERVAL), timedelta)
        else config.get(CONF_SCAN_INTERVAL)
    )
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login_obj

    # Initialize the per-account probe worker exactly once here (not per push message).
    _init_last_called_probe_worker(hass.data[DATA_ALEXAMEDIA]["accounts"][email])

    _t = time.monotonic()
    http2_enabled = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["http2"] = (
        await http2_connect()
    )
    _LOGGER.debug("[BOOT] http2_connect in %.2fs", time.monotonic() - _t)
    coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("coordinator")

    # Get runtime_data for optimized coordinator
    runtime_data = (
        config_entry.runtime_data if hasattr(config_entry, "runtime_data") else None
    )

    if coordinator is None:
        _LOGGER.debug("%s: Creating optimized coordinator", hide_email(email))

        # Use optimized coordinator with debouncing
        coordinator = AlexaMediaCoordinator(
            hass=hass,
            runtime_data=runtime_data,
            update_method=async_update_data,
            scan_interval=scan_interval,
        )

        hass.data[DATA_ALEXAMEDIA]["accounts"][email]["coordinator"] = coordinator
        # Set correct interval now that http2 status is known
        coordinator.set_http2_status(bool(http2_enabled))

        # Also store in runtime_data for type-safe access
        if runtime_data:
            runtime_data.coordinator = coordinator
    else:
        _LOGGER.debug("%s: setup_alexa: Reusing coordinator", hide_email(email))
        # Use the optimized set_http2_status method if available
        if isinstance(coordinator, AlexaMediaCoordinator):
            coordinator.set_http2_status(bool(http2_enabled))
        else:
            coordinator.update_interval = timedelta(
                seconds=scan_interval * 10 if http2_enabled else scan_interval
            )
    # Fetch initial data
    _LOGGER.debug("%s: setup_alexa: Starting coordinator refresh", hide_email(email))
    _t = time.monotonic()
    await coordinator.async_config_entry_first_refresh()
    _LOGGER.debug("[BOOT] first_refresh in %.2fs", time.monotonic() - _t)

    # Register services (fast - just registers callbacks)
    hass.data[DATA_ALEXAMEDIA]["services"] = alexa_services = AlexaMediaServices(
        hass, functions={"update_last_called": update_last_called}
    )
    await alexa_services.register()

    # Update last_called in background to avoid blocking
    _LOGGER.debug("%s: setup_alexa: Scheduling last_called update", hide_email(email))
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_called_init_task"] = (
        hass.async_create_background_task(
            _async_update_last_called_background(hass, login_obj, email),
            f"{DOMAIN}_last_called_init",
        )
    )

    return True


async def _async_update_last_called_background(
    hass: HomeAssistant, login_obj, email: str
) -> None:
    """Update last_called in background to avoid blocking startup."""
    try:
        await _async_update_last_called_global(hass, login_obj, email)
        _LOGGER.debug("%s: Background last_called update completed", hide_email(email))

        # Record metrics
        metrics = get_metrics(hass)
        if metrics:
            metrics.record_boot_stage(f"last_called_{hide_email(email)}")
    except asyncio.CancelledError:
        # Task cancelled during unload/shutdown; propagate cancellation.
        raise
    except Exception as err:
        _LOGGER.debug(
            "%s: Background last_called update failed: %s", hide_email(email), err
        )


async def async_unload_entry(hass, entry) -> bool:
    """Unload a config entry"""
    email = entry.data["email"]
    _LOGGER.debug("Unloading entry: %s", hide_email(email))
    for task_key in (
        "notifications_refresh_task",
        "notifications_init_task",
        "last_called_init_task",
        "service_update_last_called_task",
    ):
        accounts = hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
        account = accounts.get(email)
        if not account:
            return True
        task = account.get(task_key)
        if task and not task.done():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                # Expected during unload/shutdown
                pass
    last_called_task = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
        "last_called_probe_task"
    )
    if last_called_task and not last_called_task.done():
        last_called_task.cancel()
        try:
            await last_called_task
        except asyncio.CancelledError:
            # Expected during unload/shutdown
            pass
        except Exception as err:  # pragma: no cover
            _LOGGER.debug(
                "%s: Exception while cancelling last_called_probe_task: %s",
                hide_email(email),
                err,
            )
    hass.data[DATA_ALEXAMEDIA]["accounts"][email]["last_called_probe_task"] = None

    debouncer = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
        "confirm_refresh_debouncer"
    )
    if debouncer:
        debouncer.async_cancel()
        hass.data[DATA_ALEXAMEDIA]["accounts"][email][
            "confirm_refresh_debouncer"
        ] = None

    for component in ALEXA_COMPONENTS + DEPENDENT_ALEXA_COMPONENTS:
        try:
            if component == "notify":
                await notify_async_unload_entry(hass, entry)
            else:
                _LOGGER.debug("Forwarding unload entry to %s", component)
                await hass.config_entries.async_forward_entry_unload(entry, component)
        except Exception:
            _LOGGER.error("Error unloading: %s", component)
    await close_connections(hass, email)
    for listener in hass.data[DATA_ALEXAMEDIA]["accounts"][email][DATA_LISTENER]:
        listener()
    hass.data[DATA_ALEXAMEDIA]["accounts"].pop(email)
    # Clean up config flows in progress
    flows_to_remove = []
    if hass.data[DATA_ALEXAMEDIA].get("config_flows"):
        for key, flow in hass.data[DATA_ALEXAMEDIA]["config_flows"].items():
            if key.startswith(email) and flow:
                _LOGGER.debug("Aborting flow %s %s", key, flow)
                flows_to_remove.append(key)
                try:
                    hass.config_entries.flow.async_abort(flow.get("flow_id"))
                except UnknownFlow:
                    pass
        for flow in flows_to_remove:
            hass.data[DATA_ALEXAMEDIA]["config_flows"].pop(flow)
    # Clean up hass.data
    if not hass.data[DATA_ALEXAMEDIA].get("accounts"):
        _LOGGER.debug("Removing accounts data and services")
        hass.data[DATA_ALEXAMEDIA].pop("accounts")
        alexa_services = hass.data[DATA_ALEXAMEDIA].get("services")
        if alexa_services:
            await alexa_services.unregister()
            hass.data[DATA_ALEXAMEDIA].pop("services")
    if hass.data[DATA_ALEXAMEDIA].get("config_flows") == {}:
        _LOGGER.debug("Removing config_flows data")
        async_dismiss_persistent_notification(
            hass, f"alexa_media_{slugify(email)}{slugify((entry.data['url'])[7:])}"
        )
        hass.data[DATA_ALEXAMEDIA].pop("config_flows")
    if not hass.data[DATA_ALEXAMEDIA]:
        _LOGGER.debug("Removing alexa_media data structure")
        if hass.data.get(DATA_ALEXAMEDIA):
            hass.data.pop(DATA_ALEXAMEDIA)
    else:
        _LOGGER.debug(
            "Unable to remove alexa_media data structure: %s",
            hass.data.get(DATA_ALEXAMEDIA),
        )
    _LOGGER.debug("Unloaded entry for %s", hide_email(email))
    return True


async def async_remove_entry(hass, entry) -> bool:
    """Handle removal of an entry."""
    email = entry.data["email"]
    obfuscated_email = hide_email(email)
    _LOGGER.debug("Removing config entry: %s", hide_email(email))
    login_obj = AlexaLogin(
        url="",
        email=email,
        password="",  # nosec
        outputpath=hass.config.path,
    )
    # Delete cookiefile
    cookiefile = hass.config.path(f".storage/{DOMAIN}.{email}.pickle")
    obfuscated_cookiefile = hass.config.path(
        f".storage/{DOMAIN}.{obfuscated_email}.pickle"
    )
    if callable(getattr(AlexaLogin, "delete_cookiefile", None)):
        try:
            await login_obj.delete_cookiefile()
            _LOGGER.debug("Cookiefile %s deleted.", obfuscated_cookiefile)
        except Exception as ex:
            _LOGGER.error(
                "delete_cookiefile() exception: %s;"
                " Manually delete cookiefile before re-adding the integration: %s",
                ex,
                obfuscated_cookiefile,
            )
    else:
        if os.path.exists(cookiefile):
            try:
                await alexapy_delete_cookie(cookiefile)
                _LOGGER.debug(
                    "Successfully deleted cookiefile: %s", obfuscated_cookiefile
                )
            except (OSError, EOFError, TypeError, AttributeError) as ex:
                _LOGGER.error(
                    "alexapy_delete_cookie() exception: %s;"
                    " Manually delete cookiefile before re-adding the integration: %s",
                    ex,
                    obfuscated_cookiefile,
                )
        else:
            _LOGGER.error("Cookiefile not found: %s", obfuscated_cookiefile)
    _LOGGER.debug("Config entry %s removed.", obfuscated_email)
    return True


async def close_connections(hass, email: str) -> None:
    """Clear open aiohttp connections for email."""
    if (
        email not in hass.data[DATA_ALEXAMEDIA]["accounts"]
        or "login_obj" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]
    ):
        return
    account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][email]
    login_obj = account_dict["login_obj"]
    await login_obj.save_cookiefile()
    await login_obj.close()
    _LOGGER.debug(
        "%s: Connection closed: %s", hide_email(email), login_obj.session.closed
    )


async def update_listener(hass, config_entry):
    """Update when config_entry options update."""
    account = config_entry.data
    email = account.get(CONF_EMAIL)
    reload_needed: bool = False
    for key, old_value in hass.data[DATA_ALEXAMEDIA]["accounts"][email][
        "options"
    ].items():
        new_value = config_entry.data.get(key)
        if new_value is not None and new_value != old_value:
            hass.data[DATA_ALEXAMEDIA]["accounts"][email]["options"][key] = new_value
            _LOGGER.debug(
                "Option %s changed from %s to %s",
                key,
                old_value,
                hass.data[DATA_ALEXAMEDIA]["accounts"][email]["options"][key],
            )
            reload_needed = True
    if reload_needed:
        await hass.config_entries.async_reload(config_entry.entry_id)
        _LOGGER.debug(
            "%s options reloaded",
            hass.data[DATA_ALEXAMEDIA]["accounts"][email],
        )


async def test_login_status(hass, config_entry, login) -> bool:
    """Test the login status and spawn requests for info."""

    _LOGGER.debug("Testing login status: %s", login.status)
    if login.status and login.status.get("login_successful"):
        return True
    account = config_entry.data
    _LOGGER.debug("Logging in: %s %s", obfuscate(account), in_progress_instances(hass))
    _LOGGER.debug("Login stats: %s", login.stats)
    message: str = (
        f"Reauthenticate {login.email} on the [Integrations](/config/integrations) page. "
    )
    if login.stats.get("login_timestamp") != datetime(1, 1, 1):
        elaspsed_time: str = str(datetime.now() - login.stats.get("login_timestamp"))
        api_calls: int = login.stats.get("api_calls")
        message += f"Relogin required after {elaspsed_time} and {api_calls} api calls."
    host = urlparse(login.url).hostname or login.url
    async_create_persistent_notification(
        hass,
        title="Alexa Media Reauthentication Required",
        message=message,
        notification_id=f"alexa_media_{slugify(login.email)}_{slugify(host)}",
    )
    flow = hass.data[DATA_ALEXAMEDIA]["config_flows"].get(
        f"{account[CONF_EMAIL]} - {account[CONF_URL]}"
    )
    if flow:
        if flow.get("flow_id") in in_progress_instances(hass):
            _LOGGER.debug("Existing config flow detected")
            return False
        _LOGGER.debug("Stopping orphaned config flow %s", flow.get("flow_id"))
        try:
            hass.config_entries.flow.async_abort(flow.get("flow_id"))
        except UnknownFlow:
            pass
        hass.data[DATA_ALEXAMEDIA]["config_flows"][
            f"{account[CONF_EMAIL]} - {account[CONF_URL]}"
        ] = None
    _LOGGER.debug("Creating new config flow to login")
    config_entry.async_start_reauth(
        hass,
        context={"source": SOURCE_REAUTH},
        data={
            CONF_EMAIL: account[CONF_EMAIL],
            CONF_PASSWORD: account[CONF_PASSWORD],
            CONF_URL: account[CONF_URL],
            CONF_DEBUG: account[CONF_DEBUG],
            CONF_INCLUDE_DEVICES: account[CONF_INCLUDE_DEVICES],
            CONF_EXCLUDE_DEVICES: account[CONF_EXCLUDE_DEVICES],
            CONF_SCAN_INTERVAL: (
                account[CONF_SCAN_INTERVAL].total_seconds()
                if isinstance(account[CONF_SCAN_INTERVAL], timedelta)
                else account[CONF_SCAN_INTERVAL]
            ),
            CONF_OTPSECRET: account.get(CONF_OTPSECRET, ""),
        },
    )
    try:
        flow_obj = config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}).__next__()
        hass.data[DATA_ALEXAMEDIA]["config_flows"][
            f"{account[CONF_EMAIL]} - {account[CONF_URL]}"
        ] = flow_obj
    except StopIteration:
        _LOGGER.debug("A new config flow could not be created.")
    return False
