"""Utilities for computing entity object_id from API data.

When object_id is empty in the API response, it can be computed client-side
using the algorithm in this module.
"""

from __future__ import annotations

from dataclasses import replace
from typing import TYPE_CHECKING, TypeVar

if TYPE_CHECKING:
    from .model import DeviceInfo, EntityInfo

_EntityInfoT = TypeVar("_EntityInfoT", bound="EntityInfo")


def _to_snake_case_char(c: str) -> str:
    """Convert a single character to snake_case equivalent."""
    if c == " ":
        return "_"
    if "A" <= c <= "Z":
        return c.lower()
    return c


def _to_sanitized_char(c: str) -> str:
    """Convert a single character to sanitized equivalent."""
    # Keep lowercase letters, digits, underscore, and hyphen
    if "a" <= c <= "z" or "0" <= c <= "9" or c in "_-":
        return c
    return "_"


def snake_case(name: str) -> str:
    """Convert a name to snake_case."""
    return "".join(_to_snake_case_char(c) for c in name)


def sanitize(name: str) -> str:
    """Sanitize a name to only contain [a-z0-9_-]."""
    return "".join(_to_sanitized_char(c) for c in name)


def compute_object_id(name: str) -> str:
    """Compute object_id from a name using snake_case + sanitize."""
    return sanitize(snake_case(name))


def _infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool:
    """Infer name_add_mac_suffix from device name ending with MAC suffix."""
    # Guard against missing or malformed MAC addresses
    cleaned_mac = device_info.mac_address.replace(":", "")
    if len(cleaned_mac) < 6:
        return False
    mac_suffix = cleaned_mac[-6:].lower()
    return device_info.name.endswith(f"-{mac_suffix}")


def _get_name_for_object_id(
    entity: EntityInfo,
    device_info: DeviceInfo,
    device_id_to_name: dict[int, str],
) -> str:
    """Get the name used for object_id computation.

    Args:
        entity: The entity to get name for
        device_info: Device info from the API
        device_id_to_name: Mapping of device_id to device name for sub-devices

    Returns:
        The name to use for object_id computation

    """
    if entity.name:
        return entity.name
    if entity.device_id != 0:
        sub_device_name = device_id_to_name.get(entity.device_id)
        if sub_device_name is not None:
            return sub_device_name
        # device_id references a sub-device absent from device_info.devices
        # (firmware/version skew): fall through to main-device naming rather
        # than raising KeyError and aborting the whole entity list.
    # If friendly_name is set, always use it
    if device_info.friendly_name:
        return device_info.friendly_name
    # Only compute MAC suffix when friendly_name is empty
    if _infer_name_add_mac_suffix(device_info):
        return ""  # Bug-for-bug compat: MAC suffix + no friendly_name = empty
    return device_info.name


def compute_entity_object_id(
    entity: EntityInfo,
    device_info: DeviceInfo,
    device_id_to_name: dict[int, str],
) -> str:
    """Compute object_id for an entity.

    Args:
        entity: The entity to compute object_id for
        device_info: Device info from the API
        device_id_to_name: Mapping of device_id to device name for sub-devices

    Returns:
        The computed object_id string

    """
    name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name)
    return compute_object_id(name_for_id)


def fill_missing_object_ids(
    entities: list[_EntityInfoT],
    device_info: DeviceInfo,
) -> list[_EntityInfoT]:
    """Fill in missing object_id values for entities.

    When object_id is empty in the API response, this function computes it
    using the same algorithm as ESPHome.

    Args:
        entities: List of entities to process
        device_info: Device info from the API

    Returns:
        A new list of entities with object_id filled in where missing

    """
    # Build device_id -> name lookup from sub-devices
    device_id_to_name = {d.device_id: d.name for d in device_info.devices}

    result: list[_EntityInfoT] = []
    for entity in entities:
        if not entity.object_id:
            # Compute object_id and create new entity with it filled in
            object_id = compute_entity_object_id(entity, device_info, device_id_to_name)
            # All EntityInfo subclasses are frozen dataclasses
            entity = replace(entity, object_id=object_id)  # type: ignore[type-var]
        result.append(entity)
    return result
