from __future__ import annotations

import asyncio
from collections import deque
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
import contextlib
import json
import logging
import ssl
from typing import Any

import certifi
from aiohttp import ClientSession, TCPConnector

from solixapi.apitypes import API_ENDPOINTS
from solixapi.helpers import get_solix_product_code
from solixapi.mqtt import AnkerSolixMqttSession
from solixapi.mqttcmdmap import SolixMqttCommands
from solixapi.session import AnkerSolixClientSession

MODEL_A2345 = "A2345"
PORT_LABELS = {
    "usbc_1": "USB-C 1",
    "usbc_2": "USB-C 2",
    "usbc_3": "USB-C 3",
    "usbc_4": "USB-C 4",
    "usba": "USB-A Group",
}
PORT_SELECT = {
    "usbc_1": (SolixMqttCommands.usbc_1_port_switch, 0),
    "usbc_2": (SolixMqttCommands.usbc_2_port_switch, 1),
    "usbc_3": (SolixMqttCommands.usbc_3_port_switch, 2),
    "usbc_4": (SolixMqttCommands.usbc_4_port_switch, 3),
    "usba": (SolixMqttCommands.usba_port_switch, 4),
}


class StandaloneApi:
    """HA-free cloud adapter built from the existing A2345 integration logic."""

    def __init__(self, email: str, password: str, country: str, logger: logging.Logger) -> None:
        ssl_context = ssl.create_default_context(cafile=certifi.where())
        self._websession = ClientSession(connector=TCPConnector(ssl=ssl_context))
        self.session = AnkerSolixClientSession(
            email=email,
            password=password,
            countryId=country,
            websession=self._websession,
            logger=logger,
        )
        self.devices: dict[str, dict[str, Any]] = {}
        self.logger = logger

    async def close(self) -> None:
        await self._websession.close()

    def clear_authentication(self) -> None:
        self.session.clear_authentication()

    async def login(self, restart: bool = False) -> None:
        if not await self.session.async_authenticate(restart=restart):
            raise RuntimeError("Anker authentication failed")

    async def get_devices(self) -> list[dict[str, Any]]:
        await self.login(restart=False)
        candidates: dict[str, dict[str, Any]] = {}
        for endpoint in (
            "bind_devices",
            "user_devices",
            "charging_devices",
            "homepage",
            "get_auto_upgrade",
        ):
            try:
                response = await self.session.request("post", API_ENDPOINTS[endpoint])
            except Exception as err:
                self.logger.debug("Discovery endpoint %s failed: %s", endpoint, err)
                continue
            for device in _walk_devices(response.get("data") or {}):
                normalized = self._normalize_device(device)
                if normalized:
                    candidates[normalized["device_sn"]] = normalized
        try:
            response = await self.session.request("post", API_ENDPOINTS["site_list"])
            for site in (response.get("data") or {}).get("site_list") or []:
                site_id = site.get("site_id")
                for device in _walk_devices(site):
                    normalized = self._normalize_device(device)
                    if normalized:
                        candidates[normalized["device_sn"]] = normalized
                if site_id:
                    for endpoint in ("scene_info", "site_detail"):
                        detail = await self.session.request(
                            "post",
                            API_ENDPOINTS[endpoint],
                            json={"site_id": site_id},
                        )
                        for device in _walk_devices(detail.get("data") or {}):
                            normalized = self._normalize_device(device)
                            if normalized:
                                candidates[normalized["device_sn"]] = normalized
        except Exception as err:
            self.logger.debug("site_list discovery failed: %s", err)
        self.devices = candidates
        return list(candidates.values())

    async def status_request(self, device_sn: str) -> dict[str, Any] | None:
        device = self.devices.get(device_sn, {})
        payload = {"device_sn": device_sn, "device_model": MODEL_A2345}
        if product_code := device.get("device_pn"):
            payload["product_code"] = product_code
        try:
            response = await self.session.request(
                "post",
                API_ENDPOINTS["charger_get_device_setting"],
                json=payload,
            )
            return response.get("data")
        except Exception as err:
            self.logger.debug("Cloud fallback status request failed: %s", err)
            return None

    async def get_connected_device_information(self, device_sn: str) -> dict[str, Any]:
        payloads = (
            {"device_sn": device_sn, "device_model": MODEL_A2345, "product_code": MODEL_A2345},
            {"device_sn": device_sn},
        )
        endpoints = (
            "charger_get_device_identity_status",
            "charger_get_device_identity_default_status",
        )
        collected: dict[str, Any] = {}
        for endpoint in endpoints:
            for payload in payloads:
                try:
                    response = await self.session.request(
                        "post",
                        API_ENDPOINTS[endpoint],
                        json=payload,
                    )
                except Exception as err:
                    self.logger.debug("Identity endpoint %s failed: %s", endpoint, err)
                    continue
                data = response.get("data")
                if data:
                    collected[endpoint] = data
                    break
        return _extract_connected_device_information(collected)

    def build_manual_device(self, device_sn: str, name: str | None = None) -> dict[str, Any]:
        return {
            "device_sn": device_sn,
            "device_pn": MODEL_A2345,
            "product_code": MODEL_A2345,
            "device_name": name or "Anker Prime Charger (A2345)",
            "alias_name": name,
            "device_img": None,
            "main_version": None,
            "is_admin": True,
            "mqtt_supported": True,
            "raw": {"manual": True, "device_sn": device_sn, "product_code": MODEL_A2345},
        }

    def _normalize_device(self, raw: dict[str, Any]) -> dict[str, Any] | None:
        if not isinstance(raw, dict):
            return None
        device_sn = str(raw.get("device_sn") or raw.get("sn") or "")
        if not device_sn:
            return None
        pn = str(
            raw.get("device_pn")
            or raw.get("product_code")
            or raw.get("device_model")
            or get_solix_product_code(device_sn)
            or ""
        )
        if pn != MODEL_A2345:
            return None
        return {
            "device_sn": device_sn,
            "device_pn": MODEL_A2345,
            "product_code": MODEL_A2345,
            "device_name": raw.get("device_name") or raw.get("alias_name") or "Anker Prime Charger (A2345)",
            "alias_name": raw.get("alias_name"),
            "device_img": raw.get("device_img") or raw.get("img_url"),
            "main_version": raw.get("main_version") or raw.get("sw_version"),
            "is_admin": raw.get("is_admin", True),
            "mqtt_supported": True,
            "raw": raw,
        }


class PrimeChargerRuntime:
    """Persistent helper runtime exposed over JSON IPC."""

    def __init__(self, emit_event: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
        self.emit_event = emit_event
        self.logger = logging.getLogger("anker_prime_bridge")
        self.logger.setLevel(logging.INFO)
        self.loop: asyncio.AbstractEventLoop | None = None
        self.api: StandaloneApi | None = None
        self.device: dict[str, Any] | None = None
        self.device_sn: str | None = None
        self.snapshot_data: dict[str, Any] = {}
        self.mqtt: PrimeChargerMqttManager | None = None
        self.polling_enabled = True
        self.polling_interval = 300
        self._poll_task: asyncio.Task | None = None

    async def close(self) -> None:
        await self._stop_active_connection()
        if self.api:
            await self.api.close()
            self.api = None

    async def logout(self) -> bool:
        await self._stop_active_connection()
        if self.api:
            self.api.clear_authentication()
            await self.api.close()
        self.api = None
        self.device = None
        self.device_sn = None
        self.snapshot_data = {}
        await self.emit_event("devices", [])
        await self.emit_event("status", {"message": "Signed out"})
        return True

    async def _stop_active_connection(self) -> None:
        if self._poll_task:
            self._poll_task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await self._poll_task
            self._poll_task = None
        if self.mqtt:
            await self.mqtt.stop()
            self.mqtt = None

    async def login(
        self,
        email: str,
        password: str,
        country: str,
        debug: bool = False,
        restart: bool = False,
    ) -> list[dict[str, Any]]:
        if self.api:
            await self.api.close()
        self.logger.setLevel(logging.DEBUG if debug else logging.INFO)
        self.api = StandaloneApi(email=email, password=password, country=country, logger=self.logger)
        if restart:
            await self.api.login(restart=True)
            devices = await self.api.get_devices()
        else:
            devices = await self.api.get_devices()
        payload = [_device_summary(device) for device in devices]
        await self.emit_event("devices", payload)
        return payload

    async def connect_device(
        self,
        device_sn: str,
        refresh_interval: int,
        reconnect_interval: int,
        polling_fallback: bool,
        debug: bool = False,
    ) -> dict[str, Any]:
        if not self.api:
            raise RuntimeError("Login is required before connecting to a device")
        self.loop = asyncio.get_running_loop()
        self.device = self.api.devices.get(device_sn) or self.api.build_manual_device(device_sn)
        self.device_sn = device_sn
        self.snapshot_data = {
            "device_name": self.device.get("device_name"),
            "device_sn": device_sn,
            "firmware_version": self.device.get("main_version"),
            "model": self.device.get("device_pn"),
            "online": False,
            "realtime_status": "connecting",
        }
        if self.mqtt:
            await self.mqtt.stop()
        self.mqtt = PrimeChargerMqttManager(
            api=self.api,
            device=self.device,
            on_update=self._handle_mqtt_update,
            realtime_interval=refresh_interval,
            keepalive=reconnect_interval,
            stale_after=max(60, min(180, refresh_interval // 2)),
            debug_logging=debug,
        )
        await self.mqtt.start()
        self.polling_enabled = polling_fallback
        self.polling_interval = max(60, refresh_interval)
        self._restart_poller()
        await self.refresh()
        return self._build_snapshot()

    async def refresh(self) -> dict[str, Any]:
        if not self.api or not self.device_sn:
            raise RuntimeError("No active device connection")
        payload = await self.api.status_request(self.device_sn)
        if isinstance(payload, dict):
            self.snapshot_data.update(_flatten_payload(payload))
            self.snapshot_data["last_cloud_poll"] = _iso_now()
        info = await self.api.get_connected_device_information(self.device_sn)
        self.snapshot_data.update(info)
        if self.mqtt:
            await self.mqtt.trigger_realtime()
            await self.mqtt.status_request()
        snapshot = self._build_snapshot()
        await self.emit_event("snapshot", snapshot)
        return snapshot

    async def reconnect(self) -> dict[str, Any]:
        if not self.mqtt:
            raise RuntimeError("No MQTT runtime is active")
        await self.mqtt.restart()
        return await self.refresh()

    async def set_port_switch(self, port_key: str, enabled: bool) -> dict[str, Any]:
        if not self.device or not self.mqtt or not self.mqtt.session:
            raise RuntimeError("No active MQTT session")
        if port_key not in PORT_SELECT:
            raise RuntimeError(f"Unsupported port key: {port_key}")
        command, port_select = PORT_SELECT[port_key]
        self.snapshot_data[f"{port_key}_switch"] = 1 if enabled else 0
        session = self.mqtt.session
        for _ in range(2):
            hexdata = session.get_command_data(
                command=command,
                parameters={
                    "set_port_switch_select": port_select,
                    "set_port_switch": 1 if enabled else 0,
                },
                model=self.device.get("device_pn", MODEL_A2345),
            )
            if not hexdata:
                raise RuntimeError(f"Command data missing for {port_key}")
            _, response = session.publish(self.device, hexdata)
            await asyncio.to_thread(response.wait_for_publish, 5)
            await asyncio.sleep(1.2)
        await self.mqtt.trigger_realtime()
        await self.mqtt.status_request()
        snapshot = self._build_snapshot()
        await self.emit_event("snapshot", snapshot)
        return snapshot

    def _handle_mqtt_update(self, values: dict[str, Any]) -> None:
        if not self.loop:
            return
        self.loop.call_soon_threadsafe(self._apply_mqtt_update, values)

    def _apply_mqtt_update(self, values: dict[str, Any]) -> None:
        self.snapshot_data.update(values)
        snapshot = self._build_snapshot()
        if self.loop:
            self.loop.create_task(self.emit_event("snapshot", snapshot))

    def _build_snapshot(self) -> dict[str, Any]:
        device_name = self.snapshot_data.get("device_name") or self.device.get("device_name") if self.device else "Anker Prime Charger"
        diagnostics = {
            "mqttConnected": bool(self.snapshot_data.get("mqtt_connected")),
            "realtimeStatus": str(self.snapshot_data.get("realtime_status") or "idle"),
            "mqttReconnectCount": int(self.snapshot_data.get("mqtt_reconnect_count") or 0),
            "mqttRecoveryCount": int(self.snapshot_data.get("mqtt_recovery_count") or 0),
            "lastError": self.snapshot_data.get("mqtt_last_error"),
            "lastSeen": _stringify(self.snapshot_data.get("last_seen")),
            "lastPayloadAt": _stringify(self.snapshot_data.get("last_payload_time")),
            "lastCloudPoll": _stringify(self.snapshot_data.get("last_cloud_poll")),
            "lastTopic": (self.mqtt.last_payload or {}).get("topic") if self.mqtt and self.mqtt.last_payload else None,
            "extractedValueCount": len(((self.mqtt.last_payload or {}).get("values") or {})) if self.mqtt and self.mqtt.last_payload else 0,
            "payloadDataType": (self.mqtt.last_payload or {}).get("data_type") if self.mqtt and self.mqtt.last_payload else None,
            "lastEnvelopePreview": _envelope_preview(self.mqtt.last_payload if self.mqtt else None),
            "payloadPreview": _payload_preview(self.mqtt.last_payload if self.mqtt else None),
        }
        return {
            "deviceSN": self.device_sn or "",
            "deviceName": device_name,
            "productCode": (self.device or {}).get("device_pn", MODEL_A2345),
            "firmwareVersion": self.snapshot_data.get("firmware_version") or self.snapshot_data.get("sw_version"),
            "online": bool(self.snapshot_data.get("online")),
            "connectionPhase": _phase_for(self.snapshot_data),
            "totalPower": _computed_total_power(self.snapshot_data),
            "lastUpdated": _stringify(self.snapshot_data.get("last_seen") or self.snapshot_data.get("last_cloud_poll")),
            "ports": _build_ports(self.snapshot_data),
            "diagnostics": diagnostics,
            "rawValues": {key: _stringify(value) for key, value in sorted(self.snapshot_data.items()) if isinstance(value, (str, int, float, bool))},
        }

    def _restart_poller(self) -> None:
        if self._poll_task:
            self._poll_task.cancel()
        if not self.polling_enabled:
            self._poll_task = None
            return
        self._poll_task = asyncio.create_task(self._poll_loop(), name="anker_prime_cloud_poll")

    async def _poll_loop(self) -> None:
        while True:
            await asyncio.sleep(self.polling_interval)
            if self._has_recent_payload():
                continue
            try:
                await self.refresh()
            except Exception as err:
                self.logger.warning("Background refresh failed: %s", err)

    def _has_recent_payload(self) -> bool:
        last_payload = self.snapshot_data.get("last_payload_time")
        if not isinstance(last_payload, datetime):
            return False
        return (datetime.now(UTC) - last_payload).total_seconds() < 120


class PrimeChargerMqttManager:
    """Standalone variant of the HA MQTT recovery loop."""

    def __init__(
        self,
        api: StandaloneApi,
        device: dict[str, Any],
        on_update: Callable[[dict[str, Any]], None],
        realtime_interval: int,
        keepalive: int,
        stale_after: int,
        debug_logging: bool,
    ) -> None:
        self.api = api
        self.device = device
        self.device_sn = device["device_sn"]
        self.on_update = on_update
        self.realtime_interval = realtime_interval
        self.keepalive = keepalive
        self.stale_after = stale_after
        self.debug_logging = debug_logging
        self.session: AnkerSolixMqttSession | None = None
        self.task: asyncio.Task | None = None
        self.running = False
        self.connected = False
        self.reconnect_count = 0
        self.last_payload_time: datetime | None = None
        self.last_seen: datetime | None = None
        self.last_payload: dict[str, Any] | None = None
        self.payload_history: deque[dict[str, Any]] = deque(maxlen=30)
        self.last_values: dict[str, Any] = {}
        self.realtime_status = "stopped"
        self.last_realtime_trigger: datetime | None = None
        self.last_status_request: datetime | None = None
        self.last_error: str | None = None
        self.recovery_count = 0
        self._stale_recovery_attempts = 0

    async def start(self) -> None:
        if self.task:
            return
        self.running = True
        self.task = asyncio.create_task(self._run(), name="anker_prime_mqtt")

    async def stop(self) -> None:
        self.running = False
        if self.task:
            self.task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await self.task
            self.task = None
        if self.session:
            await asyncio.to_thread(self.session.cleanup)
            self.session = None
        self.connected = False

    async def restart(self) -> None:
        await self.stop()
        await self.start()

    async def trigger_realtime(self) -> None:
        if not self.session or not self.session.is_connected():
            return
        try:
            await asyncio.to_thread(self.session.realtime_trigger, self.device, 10, 2)
            self.last_realtime_trigger = datetime.now(UTC)
            self.realtime_status = "triggered"
        except Exception as err:
            self.last_error = str(err)
            self.realtime_status = "trigger_failed"
        finally:
            self._publish_state()

    async def status_request(self) -> None:
        if not self.session or not self.session.is_connected():
            return
        try:
            await asyncio.to_thread(self.session.status_request, self.device, 2)
            self.last_status_request = datetime.now(UTC)
            self.realtime_status = "status_requested"
        except Exception as err:
            self.last_error = str(err)
        finally:
            self._publish_state()

    async def _run(self) -> None:
        backoff = 5
        next_realtime = 0.0
        next_status_request = 0.0
        while self.running:
            try:
                await self._ensure_connected()
                now = asyncio.get_running_loop().time()
                if now >= next_realtime:
                    await self.trigger_realtime()
                    next_realtime = now + self.realtime_interval
                if self._payload_stale():
                    self.realtime_status = "stale_payload"
                    self._stale_recovery_attempts += 1
                    if now >= next_status_request:
                        await self.trigger_realtime()
                        await self.status_request()
                        next_status_request = now + min(60, self.stale_after)
                    next_realtime = min(next_realtime, now + 30)
                    if self._should_reconnect_for_stale_payload():
                        self.recovery_count += 1
                        raise RuntimeError("MQTT payload watchdog forced reconnect")
                    self._publish_state()
                else:
                    self._stale_recovery_attempts = 0
                backoff = 5
                await asyncio.sleep(10)
            except asyncio.CancelledError:
                raise
            except Exception as err:
                self.connected = False
                self.realtime_status = "reconnecting"
                self.reconnect_count += 1
                self.last_error = str(err)
                self._publish_state()
                if self.session:
                    await asyncio.to_thread(self.session.cleanup)
                    self.session = None
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 300)

    async def _ensure_connected(self) -> None:
        if self.session and self.session.is_connected():
            if not self.connected:
                self.connected = True
                self.reconnect_count += 1
                await self.trigger_realtime()
                self._publish_state()
            return
        if self.session:
            await asyncio.to_thread(self.session.cleanup)
        self.session = AnkerSolixMqttSession(self.api.session)
        self.session.message_callback(self._handle_message)
        client = await self.session.connect_client_async(keepalive=self.keepalive)
        if not client or not client.is_connected():
            raise RuntimeError("MQTT connection was not established")
        topic = f"{self.session.get_topic_prefix(self.device)}#"
        self.session.subscribe(topic)
        self.connected = True
        self.reconnect_count += 1
        self.realtime_status = "connected"
        self.last_error = None
        await self.trigger_realtime()
        self._publish_state()

    def _handle_message(
        self,
        _session: Any,
        topic: str,
        message: Any,
        data: bytes | dict[str, Any],
        model: str,
        device_sn: str,
        extracted_values: dict[str, Any],
    ) -> None:
        if device_sn != self.device_sn or model != MODEL_A2345:
            return
        now = datetime.now(UTC)
        self.last_seen = now
        self.last_payload_time = now
        self.last_payload = {
            "topic": topic,
            "message": _safe_message(message),
            "data_type": type(data).__name__,
            "values": extracted_values,
            "received_at": _stringify(now),
        }
        self.payload_history.append(self.last_payload)
        self._apply_port_switch_echo(extracted_values)
        self.last_values.update(extracted_values)
        self.realtime_status = "receiving"
        self._publish_state()

    def _apply_port_switch_echo(self, values: dict[str, Any]) -> None:
        if "set_port_switch_select" not in values or "set_port_switch" not in values:
            return
        port_map = {0: "usbc_1_switch", 1: "usbc_2_switch", 2: "usbc_3_switch", 3: "usbc_4_switch", 4: "usba_switch"}
        try:
            selected = int(values["set_port_switch_select"])
        except (TypeError, ValueError):
            return
        if key := port_map.get(selected):
            values[key] = values["set_port_switch"]

    def _payload_stale(self) -> bool:
        if not self.last_payload_time:
            return True
        return (datetime.now(UTC) - self.last_payload_time).total_seconds() > self.stale_after

    def _should_reconnect_for_stale_payload(self) -> bool:
        if not self.last_payload_time:
            return self._stale_recovery_attempts >= 3
        age = (datetime.now(UTC) - self.last_payload_time).total_seconds()
        return age > max(self.stale_after * 3, self.realtime_interval + 60)

    def _publish_state(self) -> None:
        values = dict(self.last_values)
        values.update(
            {
                "mqtt_connected": self.connected,
                "mqtt_reconnect_count": self.reconnect_count,
                "last_seen": self.last_seen,
                "last_payload_time": self.last_payload_time,
                "realtime_status": self.realtime_status,
                "last_realtime_trigger": self.last_realtime_trigger,
                "last_status_request": self.last_status_request,
                "mqtt_recovery_count": self.recovery_count,
                "mqtt_last_error": self.last_error,
                "online": bool(self.last_seen and (datetime.now(UTC) - self.last_seen).total_seconds() <= self.stale_after * 2),
            }
        )
        self.on_update(values)


def _walk_devices(value: Any) -> list[dict[str, Any]]:
    found: list[dict[str, Any]] = []
    if isinstance(value, dict):
        if value.get("device_sn") or value.get("sn"):
            found.append(value)
        for child in value.values():
            found.extend(_walk_devices(child))
    elif isinstance(value, list):
        for item in value:
            found.extend(_walk_devices(item))
    return found


def _walk_dicts(value: Any) -> list[dict[str, Any]]:
    found: list[dict[str, Any]] = []
    if isinstance(value, dict):
        found.append(value)
        for child in value.values():
            found.extend(_walk_dicts(child))
    elif isinstance(value, list):
        for child in value:
            found.extend(_walk_dicts(child))
    return found


def _extract_connected_device_information(payload: dict[str, Any]) -> dict[str, Any]:
    port_devices: dict[str, str] = {}
    loose_names: list[str] = []
    for item in _walk_dicts(payload):
        name = _first_string(
            item,
            (
                "charging_device_name",
                "device_name",
                "device_model",
                "display_name",
                "identify_name",
                "identity_name",
                "model",
                "product_name",
                "model_name",
                "name",
                "remark",
                "title",
            ),
        )
        if not name:
            continue
        port = _normalize_port(_first_value(item, ("port", "port_id", "port_index", "port_no", "port_num", "port_type", "usb_type")))
        if port:
            port_devices[port] = name
        elif name not in loose_names:
            loose_names.append(name)
    summary_parts = [f"{PORT_LABELS.get(port, port)}: {name}" for port, name in sorted(port_devices.items()) if name]
    summary_parts.extend(name for name in loose_names if name not in port_devices.values())
    result: dict[str, Any] = {"device_information": ", ".join(summary_parts) or None}
    for port, name in port_devices.items():
        result[f"{port}_device_information"] = name
    return result


def _first_string(item: dict[str, Any], keys: tuple[str, ...]) -> str | None:
    for key, value in item.items():
        lowered = str(key).lower()
        if any(lowered == wanted or lowered.endswith(f"_{wanted}") for wanted in keys):
            if isinstance(value, str) and value.strip():
                return value.strip()
    return None


def _first_value(item: dict[str, Any], keys: tuple[str, ...]) -> Any:
    for key, value in item.items():
        lowered = str(key).lower()
        if any(lowered == wanted or lowered.endswith(f"_{wanted}") for wanted in keys):
            return value
    return None


def _normalize_port(value: Any) -> str | None:
    aliases = {
        "0": "usbc_1",
        "1": "usbc_2",
        "2": "usbc_3",
        "3": "usbc_4",
        "4": "usba",
        "5": "usba_1",
        "6": "usba_2",
        "c1": "usbc_1",
        "c2": "usbc_2",
        "c3": "usbc_3",
        "c4": "usbc_4",
        "usb_c_1": "usbc_1",
        "usb_c_2": "usbc_2",
        "usb_c_3": "usbc_3",
        "usb_c_4": "usbc_4",
        "usbc1": "usbc_1",
        "usbc2": "usbc_2",
        "usbc3": "usbc_3",
        "usbc4": "usbc_4",
        "usba": "usba",
        "usb_a": "usba",
        "usb_a_1": "usba_1",
        "usb_a_2": "usba_2",
    }
    if value is None:
        return None
    key = str(value).strip().lower().replace("-", "_").replace(" ", "_")
    return aliases.get(key)


def _flatten_payload(payload: dict[str, Any]) -> dict[str, Any]:
    flattened: dict[str, Any] = {}
    for key, value in payload.items():
        if isinstance(value, dict):
            flattened.update(_flatten_payload(value))
        elif isinstance(value, list):
            for index, item in enumerate(value):
                if isinstance(item, dict):
                    flattened.update(_flatten_payload(item))
                elif isinstance(item, (int, float, str, bool)):
                    flattened[f"{key}_{index}"] = item
        else:
            flattened[key] = value
    total_output = sum(
        _number(flattened.get(key)) or 0
        for key in ("usbc_1_power", "usbc_2_power", "usbc_3_power", "usbc_4_power", "usba_1_power", "usba_2_power")
    )
    if total_output:
        flattened["total_power"] = round(total_output, 2)
    return flattened


def _build_ports(data: dict[str, Any]) -> list[dict[str, Any]]:
    ports: list[dict[str, Any]] = []
    for key in ("usbc_1", "usbc_2", "usbc_3", "usbc_4"):
        ports.append(_build_port(data, key, PORT_LABELS[key]))
    usba_power = sum(_number(data.get(item)) or 0 for item in ("usba_1_power", "usba_2_power"))
    usba_current = sum(_number(data.get(item)) or 0 for item in ("usba_1_current", "usba_2_current"))
    usba_voltage = max(
        [_number(data.get(item)) or 0 for item in ("usba_1_voltage", "usba_2_voltage")],
        default=0,
    )
    ports.append(
        {
            "key": "usba",
            "name": PORT_LABELS["usba"],
            "power": round(usba_power, 2) if usba_power else None,
            "voltage": round(usba_voltage, 2) if usba_voltage else None,
            "current": round(usba_current, 2) if usba_current else None,
            "status": _status_text(data.get("usba_1_status") or data.get("usba_2_status")),
            "switchState": _bool_value(data.get("usba_switch")),
            "deviceInformation": data.get("usba_device_information") or _join_present(
                data.get("usba_1_device_information"), data.get("usba_2_device_information")
            ),
        }
    )
    return ports


def _build_port(data: dict[str, Any], key: str, name: str) -> dict[str, Any]:
    return {
        "key": key,
        "name": name,
        "power": _number(data.get(f"{key}_power")),
        "voltage": _number(data.get(f"{key}_voltage")),
        "current": _number(data.get(f"{key}_current")),
        "status": _status_text(data.get(f"{key}_status")),
        "switchState": _bool_value(data.get(f"{key}_switch")),
        "deviceInformation": data.get(f"{key}_device_information"),
    }


def _device_summary(device: dict[str, Any]) -> dict[str, Any]:
    return {
        "deviceSN": device["device_sn"],
        "name": device.get("device_name") or "Anker Prime Charger",
        "productCode": device.get("device_pn") or MODEL_A2345,
        "firmwareVersion": device.get("main_version"),
        "isAdmin": bool(device.get("is_admin", True)),
    }


def _phase_for(data: dict[str, Any]) -> str:
    realtime_status = str(data.get("realtime_status") or "")
    if "auth" in str(data.get("mqtt_last_error") or "").lower():
        return "auth_failed"
    if realtime_status in {"reconnecting"}:
        return "reconnecting"
    if realtime_status in {"stale_payload"}:
        return "stale"
    if bool(data.get("online")) and bool(data.get("mqtt_connected")):
        return "connected"
    if bool(data.get("mqtt_connected")):
        return "connecting"
    if realtime_status in {"stopped", "disconnected"}:
        return "offline"
    return "idle"


def _stringify(value: Any) -> str | None:
    if value is None:
        return None
    if isinstance(value, datetime):
        return value.astimezone(UTC).isoformat()
    if isinstance(value, (dict, list)):
        return json.dumps(value, default=str)
    return str(value)


def _payload_preview(payload: dict[str, Any] | None) -> str | None:
    if not payload:
        return None
    values = payload.get("values")
    if not isinstance(values, dict):
        return None
    keys = sorted(values.keys())[:8]
    return ", ".join(f"{key}={values[key]}" for key in keys)


def _envelope_preview(payload: dict[str, Any] | None) -> str | None:
    if not payload:
        return None
    message = payload.get("message")
    if not isinstance(message, dict):
        return None
    payload_text = message.get("payload")
    if isinstance(payload_text, str):
        preview = payload_text[:220]
        return preview + ("..." if len(payload_text) > 220 else "")
    return _stringify(message)[:220]


def _safe_message(message: Any) -> Any:
    if isinstance(message, dict):
        redacted = {}
        for key, value in message.items():
            lowered = str(key).lower()
            if any(token in lowered for token in ("token", "certificate", "private", "password")):
                redacted[key] = "**REDACTED**"
            elif isinstance(value, dict):
                redacted[key] = _safe_message(value)
            else:
                redacted[key] = value
        return redacted
    return message


def _status_text(value: Any) -> str | None:
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return str(value)


def _number(value: Any) -> float | None:
    try:
        if value is None or value == "":
            return None
        return round(float(value), 2)
    except (TypeError, ValueError):
        return None


def _bool_value(value: Any) -> bool | None:
    if value is None:
        return None
    try:
        return bool(int(value))
    except (TypeError, ValueError):
        if isinstance(value, bool):
            return value
        return None


def _join_present(*items: Any) -> str | None:
    values = [str(item) for item in items if item]
    return ", ".join(values) if values else None


def _iso_now() -> str:
    return datetime.now(UTC).isoformat()


def _computed_total_power(data: dict[str, Any]) -> float | None:
    direct = _number(data.get("total_power") or data.get("total_output"))
    if direct is not None:
        return direct
    values = [
        _number(data.get(key))
        for key in (
            "usbc_1_power",
            "usbc_2_power",
            "usbc_3_power",
            "usbc_4_power",
            "usba_1_power",
            "usba_2_power",
        )
    ]
    numeric = [value for value in values if value is not None]
    if not numeric:
        return None
    return round(sum(numeric), 2)
