"""
Add friendly names to entities.

"""

from ipaddress import ip_address
import json
import logging
import os
from functools import lru_cache
from typing import Any

from model.model import db, Device, MDNS
from shared.helpers import clean_name, common_substr
from shared.networking_helpers import get_default_route
from shared.oui_parser import get_vendor

logger = logging.getLogger(__name__)


def add_product_info_to_devices() -> None:
    updated_row_count = 0

    # Find all distinct MAC addresses
    with db:
        q = Device.select(Device.mac_addr).group_by(Device.mac_addr)
    mac_addr_list = [device.mac_addr for device in q if device.mac_addr]

    # For each MAC address, find the corresponding vendor name
    inferred_product_name_dict = dict()
    for mac_addr in mac_addr_list:
        oui_vendor = get_vendor(mac_addr)
        if oui_vendor:
            inferred_product_name_dict[mac_addr] = oui_vendor

    # Update the database with the inferred product names into the `mac_vendor` field
    with db.atomic():
        for mac_addr, product_name in inferred_product_name_dict.items():
            row_count = (
                Device.update(mac_vendor=product_name)
                .where(Device.mac_addr == mac_addr)
                .execute()
            )
            updated_row_count += row_count

    # logger.debug(
    #     f"[Friendly Organizer] Updated {updated_row_count} rows of product info."
    # )


def update_mfg_model() -> None:
    """
    Update device friendly names and manufacturers in the database.
    This function processes all devices and updates their metadata fields
    to avoid recalculating at query time.
    """

    with db.atomic():
        # Get all devices with their mdns data
        devices = Device.select(Device)

        for device in devices:
            preferred_name = determine_preferred_name(device)
            preferred_model = determine_preferred_model(device)
            preferred_mfg, known_manufacturer = determine_preferred_manufacturer(
                device, preferred_name
            )
            is_iot = determine_iot_status(
                device,
                known_manufacturer,
                preferred_mfg,
                preferred_model,
                preferred_name,
            )

            # save fields to db
            device.preferred_name = preferred_name
            device.preferred_model = preferred_model
            device.preferred_mfg = preferred_mfg
            device.is_iot = is_iot
            device.is_arp_spoofable = is_iot
            device.is_arp_spoofed = (
                device.is_arp_spoofed if device.is_arp_spoofed is not None else False
            )

            # Update device with new metadata
            device.save()


def determine_preferred_name(device: Device, use_user_name: bool = True) -> str | None:
    """
    Get the preferred name for a device.
    """
    if use_user_name and device.user_name:
        return str(device.user_name)

    mdns_hostname = (
        str(device.mdns_hostname).rstrip(".local") if device.mdns_hostname else None
    )
    mdns_friendly_names = [
        name.friendly_name
        for name in MDNS.select(MDNS.friendly_name).where(MDNS.device == device.id)
        if name and name.friendly_name
    ]
    if len(mdns_friendly_names) > 0:
        mdns_name = fmt_mdns_friendly_name(mdns_friendly_names)
    else:
        mdns_name = None
    preferred_name: str | None = None
    potential_names: list[str | None] = [
        str(device.ha_name) if device.ha_name else None,
        str(device.ssdp_name) if device.ssdp_name else None,
        mdns_name,
        str(mdns_hostname).replace("-", " ") if mdns_hostname else None,
        str(device.dhcp_hostname).replace("-", " ") if device.dhcp_hostname else None,
    ]
    for name in potential_names:
        if name:
            if not preferred_name:
                preferred_name = name
    return preferred_name


def determine_preferred_model(
    device: Device, use_user_model: bool = True
) -> str | None:
    """
    Get the preferred model for a device.
    """
    if use_user_model and device.user_model:
        return str(device.user_model)

    mdns_models = [
        model.device_model
        for model in MDNS.select(MDNS.device_model).where(MDNS.device == device.id)
        if model and model.device_model
    ]
    mdns_model = None
    if len(mdns_models) > 0:
        mdns_model = common_substr(mdns_models)

    preferred_model: str | None = None
    potential_models: list[str | None] = [
        str(device.ha_model) if device.ha_model else None,
        str(device.ssdp_model) if device.ssdp_model else None,
        mdns_model,
    ]

    for model in potential_models:
        if model:
            preferred_model = model
            break

    if preferred_model == "None":
        preferred_model = None

    # use an alternate manufacturer name for the model if relevant
    # e.g. if the devices has a MAC vendor for Nest, which is an alternate name for Google,
    # use "Nest" as the model name (Google is the manufacturer)
    if not preferred_model:
        preferred_mfg = determine_preferred_manufacturer(
            device, use_user_mfg=False, swap_alternate_names=True
        )[0]
        mfg_preferring_altname = determine_preferred_manufacturer(
            device, use_user_mfg=False, swap_alternate_names=False
        )[0]
        if (
            mfg_preferring_altname
            and mfg_preferring_altname.casefold() != preferred_mfg.casefold()
        ):
            # if we have an alternate name for the manufacturer, use it as the model
            preferred_model = mfg_preferring_altname

    return preferred_model


@lru_cache(maxsize=2)
def load_manufacturers() -> tuple[dict[str, Any], dict[str, str]]:
    """
    Load manufacturers data from the manufacturers.json file.
    """
    # load manufacturers data
    iot_manufacturers_file = os.path.join(
        os.path.dirname(os.path.realpath(__file__)),
        "..",
        "data",
        "manufacturers.json",
    )
    with open(iot_manufacturers_file, "r") as file:
        manufacturers = json.load(file)
    # process alternate names
    altnames: dict[str, str] = {}
    for m, data in manufacturers.items():
        if "altnames" in data:
            for name in data["altnames"]:
                altnames[name.casefold()] = data["display_name"]
    return manufacturers, altnames


def determine_preferred_manufacturer(
    device: Device,
    preferred_name: str | None = None,
    use_user_mfg: bool = True,
    swap_alternate_names: bool = True,
) -> tuple[str | None, bool]:
    """
    Get the preferred manufacturer for a device.
    Returns (preferred_mfg, known_manufacturer)
    """
    manufacturers, altnames = load_manufacturers()

    if use_user_mfg and device.user_mfg:
        preferred_mfg = str(device.user_mfg)

    else:
        mdns_mfg = [
            mfg.device_manufacturer
            for mfg in MDNS.select(MDNS.device_manufacturer).where(
                MDNS.device == device.id
            )
            if mfg and mfg.device_manufacturer
        ]
        if len(mdns_mfg) > 0:
            mdns_mfg = common_substr(mdns_mfg)
        else:
            mdns_mfg = None

        preferred_mfg: str | None = None
        potential_mfgs: list[str | None] = [
            str(device.ssdp_mfg) if device.ssdp_mfg else None,
            mdns_mfg,
            str(device.ha_mfg) if device.ha_mfg else None,
            str(device.mac_vendor) if device.mac_vendor else None,
        ]

        # first in preference order is preferred
        for mfg in potential_mfgs:
            if mfg:
                preferred_mfg = mfg
                break

        # manufacturer renamings

        # clean name
        preferred_mfg = clean_name(preferred_mfg)

        # some hardcoded swaps
        if preferred_mfg == "FreeBSD" or preferred_mfg == "Xensource":
            preferred_mfg = "Virtual Machine"

    # check against known manufacturer names
    # if the name is on our hardcoded list, use the name from our list
    # so e.g. suffixes on the name are correctly handled
    known_manufacturer = False
    if preferred_mfg:
        for known_mfg in manufacturers.keys():
            if preferred_mfg and preferred_mfg.casefold().startswith(
                known_mfg.casefold()
            ):
                adjusted_mfg = manufacturers[known_mfg]["display_name"]
                if preferred_mfg != device.user_mfg:
                    preferred_mfg = adjusted_mfg
                known_manufacturer = True
                break

    # if a brand has an "alternate name", set preferred_mfg to the company's real name
    if (
        swap_alternate_names
        and preferred_mfg is not None
        and preferred_mfg.casefold() in altnames
    ):
        # Prefer the parent company name
        if preferred_mfg != device.user_mfg:
            preferred_mfg = altnames[preferred_mfg.casefold()]
        known_manufacturer = True

    # if we haven't found a brand on our iot list, also try matching the device name
    if not known_manufacturer and preferred_name:
        # If no preferred_mfg found, check friendly name
        # Note that this may mean we prefer the device name to the previously-set "preferred_mfg" field
        # This is the case for e.g. Amazon Basics lightbulbs, which have a MAC OUI for Espressif,
        # but a hostname for Amazon
        for known_mfg in manufacturers.keys():
            if preferred_name.casefold().startswith(known_mfg.casefold()):
                adjusted_mfg = manufacturers[known_mfg]["display_name"]
                if preferred_mfg != device.user_mfg:
                    preferred_mfg = adjusted_mfg
                known_manufacturer = True

    return preferred_mfg, known_manufacturer


def determine_iot_status(
    device: Device,
    known_manufacturer: bool,
    preferred_mfg: str | None,
    preferred_model: str | None,
    preferred_name: str | None,
    use_user_name: bool = True,
) -> bool:
    """
    Determine if a device is an IoT device based on various criteria.
    """
    if use_user_name and device.is_iot_user_override:
        return bool(device.is_iot) or False

    mdns_integrations = [
        integration.integration
        for integration in MDNS.select(MDNS.integration).where(MDNS.device == device.id)
        if integration and integration.integration
    ]
    mdns_manufacturers = [
        manufacturer.device_manufacturer
        for manufacturer in MDNS.select(MDNS.device_manufacturer).where(
            MDNS.device == device.id
        )
        if manufacturer and manufacturer.device_manufacturer
    ]

    # get the IP range for the current network
    default_route = get_default_route()
    gateway_ip = default_route.gateway_ip
    host_ip = default_route.host_ip
    subnet = default_route.subnet

    # Base IoT determination
    device_ip = str(device.ip_addr) if device.ip_addr else None
    is_iot = (
        (
            (len(mdns_manufacturers) > 0)  # preferred_mfg from HA data
            or known_manufacturer
            or (len(mdns_integrations) > 0)
        )
        and (device_ip != gateway_ip)  # gateway is never an iot device
        and (device_ip != host_ip)
        and (ip_address(device_ip) in subnet if device_ip else False)
    )

    # except for an "allowlist", where device model/name must contain a certain string
    popular_brand_allowlist = {
        "apple": [
            "Apple TV",
            "AppleTV",
            "HomePod",
            "AudioAccessory",
            "TV",
            "Speaker",
        ],
        "google": [
            "Nest",
            "Chromecast",
            "Home",
            "Hub",
            "TV",
            "Cam",
            "Protect",
            "Doorbell",
        ],
        "belkin": [
            "Wemo",
            "Light",
            "Outlet",
            "Plug",
            "Camera",
            "Doorbell",
        ],
    }
    if (
        preferred_mfg is not None
        and preferred_mfg.casefold() in popular_brand_allowlist
    ):
        # default to assuming non-iot unless the device name/model suggests otherwise
        is_iot = False
        for name in (preferred_model, preferred_name):
            if name and not is_iot:
                for term in popular_brand_allowlist[preferred_mfg.casefold()]:
                    if term.casefold() in name.casefold():
                        is_iot = True
                        break

    # or a denylist, where device model/name must not contain a certain string
    popular_brand_denylist = {
        "samsung": [
            "Galaxy",
            "Phone",
            "Tablet",
            "Book",
            "Tab",
            "Fold",
            "Edge",
        ],
    }
    if preferred_mfg is not None and preferred_mfg.casefold() in popular_brand_denylist:
        # default to assuming iot
        is_iot = True
        for name in (preferred_model, preferred_name):
            if name and is_iot:
                for term in popular_brand_denylist[preferred_mfg.casefold()]:
                    if term.casefold() in name.casefold():
                        is_iot = False
                        break

    # Eero is not IoT
    if preferred_mfg is not None and preferred_mfg.casefold() == "eero":
        # Eero devices are not IoT devices
        # But they are in our dataset so we can note that eero is owned by Amazon
        is_iot = False

    # Any model name with "Mac" is not IoT
    # Sometimes Apple devices are identified through AirPlay mDNS record, but manufacturer name is missing
    for name in (preferred_model, preferred_name):
        if name and "mac" in name.casefold():
            is_iot = False
            break

    return is_iot


def fmt_mdns_friendly_name(mdns_friendly_names: list[str]) -> str | None:
    """
    Format a list of MDNS friendly names into a single string.
    The name "Matter Device" is lower priority than other mDNS names, so we only return it if there are no other names.
    """
    if len(mdns_friendly_names) == 0:
        return None
    mdns_friendly_names = list(filter(None, mdns_friendly_names))
    if "Matter Device" in mdns_friendly_names:
        if len(mdns_friendly_names) == 1:
            return "Matter Device"
        mdns_friendly_names.remove("Matter Device")
    return common_substr(mdns_friendly_names)


def update_device_metadata() -> None:
    add_product_info_to_devices()
    update_mfg_model()
