"""
Interface to SMBIOS/DMI

(Parsing through dmidecode)

External References
-------------------
| `Desktop Management Interface (DMI) <http://www.dmtf.org/standards/dmi>`_
| `System Management BIOS <http://www.dmtf.org/standards/smbios>`_
| `DMIdecode <http://www.nongnu.org/dmidecode/>`_

"""

import logging
import re
import uuid

# Solve the Chicken and egg problem where grains need to run before any
# of the modules are loaded and are generally available for any usage.
import salt.modules.cmdmod
import salt.utils.path

log = logging.getLogger(__name__)


def __virtual__():
    """
    Only work when dmidecode is installed.
    """
    return (
        bool(salt.utils.path.which_bin(["dmidecode", "smbios"])),
        "The smbios execution module failed to load: neither dmidecode nor smbios in"
        " the path.",
    )


def get(string, clean=True):
    """
    Get an individual DMI string from SMBIOS info

    string
        The string to fetch. DMIdecode supports:
          - ``bios-vendor``
          - ``bios-version``
          - ``bios-release-date``
          - ``system-manufacturer``
          - ``system-product-name``
          - ``system-version``
          - ``system-serial-number``
          - ``system-uuid``
          - ``baseboard-manufacturer``
          - ``baseboard-product-name``
          - ``baseboard-version``
          - ``baseboard-serial-number``
          - ``baseboard-asset-tag``
          - ``chassis-manufacturer``
          - ``chassis-type``
          - ``chassis-version``
          - ``chassis-serial-number``
          - ``chassis-asset-tag``
          - ``processor-family``
          - ``processor-manufacturer``
          - ``processor-version``
          - ``processor-frequency``

    clean
      | Don't return well-known false information
      | (invalid UUID's, serial 000000000's, etcetera)
      | Defaults to ``True``

    CLI Example:

    .. code-block:: bash

        salt '*' smbios.get system-uuid clean=False
    """

    val = _dmidecoder(f"-s {string}").strip()

    # Cleanup possible comments in strings.
    val = "\n".join([v for v in val.split("\n") if not v.startswith("#")])
    if val.startswith("/dev/mem") or clean and not _dmi_isclean(string, val):
        val = None

    return val


def records(rec_type=None, fields=None, clean=True):
    """
    Return DMI records from SMBIOS

    type
        Return only records of type(s)
        The SMBIOS specification defines the following DMI types:

        ====  ======================================
        Type  Information
        ====  ======================================
         0    BIOS
         1    System
         2    Baseboard
         3    Chassis
         4    Processor
         5    Memory Controller
         6    Memory Module
         7    Cache
         8    Port Connector
         9    System Slots
        10    On Board Devices
        11    OEM Strings
        12    System Configuration Options
        13    BIOS Language
        14    Group Associations
        15    System Event Log
        16    Physical Memory Array
        17    Memory Device
        18    32-bit Memory Error
        19    Memory Array Mapped Address
        20    Memory Device Mapped Address
        21    Built-in Pointing Device
        22    Portable Battery
        23    System Reset
        24    Hardware Security
        25    System Power Controls
        26    Voltage Probe
        27    Cooling Device
        28    Temperature Probe
        29    Electrical Current Probe
        30    Out-of-band Remote Access
        31    Boot Integrity Services
        32    System Boot
        33    64-bit Memory Error
        34    Management Device
        35    Management Device Component
        36    Management Device Threshold Data
        37    Memory Channel
        38    IPMI Device
        39    Power Supply
        40    Additional Information
        41    Onboard Devices Extended Information
        42    Management Controller Host Interface
        ====  ======================================

    clean
      | Don't return well-known false information
      | (invalid UUID's, serial 000000000's, etcetera)
      | Defaults to ``True``

    CLI Example:

    .. code-block:: bash

        salt '*' smbios.records clean=False
        salt '*' smbios.records 14
        salt '*' smbios.records 4 core_count,thread_count,current_speed

    """
    if rec_type is None:
        smbios = _dmi_parse(_dmidecoder(), clean, fields)
    else:
        smbios = _dmi_parse(_dmidecoder(f"-t {rec_type}"), clean, fields)

    return smbios


def _dmi_parse(data, clean=True, fields=None):
    """
    Structurize DMI records into a nice list
    Optionally trash bogus entries and filter output
    """
    dmi = []

    # Detect & split Handle records
    dmi_split = re.compile(
        "(handle [0-9]x[0-9a-f]+[^\n]+)\n", re.MULTILINE + re.IGNORECASE
    )
    dmi_raw = iter(re.split(dmi_split, data)[1:])
    for handle, dmi_raw in zip(dmi_raw, dmi_raw):
        handle, htype = [hline.split()[-1] for hline in handle.split(",")][0:2]
        dmi_raw = dmi_raw.split("\n")
        # log.debug('%s record contains %s', handle, dmi_raw)
        log.debug("Parsing handle %s", handle)

        # The first line of a handle is a description of the type
        record = {
            "handle": handle,
            "description": dmi_raw.pop(0).strip(),
            "type": int(htype),
        }

        if not dmi_raw:
            # empty record
            if not clean:
                dmi.append(record)
            continue

        # log.debug('%s record contains %s', record, dmi_raw)
        dmi_data = _dmi_data(dmi_raw, clean, fields)
        if dmi_data:
            record["data"] = dmi_data
            dmi.append(record)
        elif not clean:
            dmi.append(record)

    return dmi


def _dmi_data(dmi_raw, clean, fields):
    """
    Parse the raw DMIdecode output of a single handle
    into a nice dict
    """
    dmi_data = {}

    key = None
    key_data = [None, []]
    for line in dmi_raw:
        if re.match(r"\t[^\s]+", line):
            # Finish previous key
            if key is not None:
                # log.debug('Evaluating DMI key {0}: {1}'.format(key, key_data))
                value, vlist = key_data
                if vlist:
                    if value is not None:
                        # On the rare occasion
                        # (I counted 1 on all systems we have)
                        # that there's both a value <and> a list
                        # just insert the value on top of the list
                        vlist.insert(0, value)
                    dmi_data[key] = vlist
                elif value is not None:
                    dmi_data[key] = value

            # Family: Core i5
            # Keyboard Password Status: Not Implemented
            key, val = line.split(":", 1)
            key = key.strip().lower().replace(" ", "_")
            if (clean and key == "header_and_data") or (fields and key not in fields):
                key = None
                continue
            else:
                key_data = [_dmi_cast(key, val.strip(), clean), []]
        elif key is None:
            continue
        elif re.match(r"\t\t[^\s]+", line):
            # Installable Languages: 1
            #        en-US
            # Characteristics:
            #        PCI is supported
            #        PNP is supported
            val = _dmi_cast(key, line.strip(), clean)
            if val is not None:
                # log.debug('DMI key %s gained list item %s', key, val)
                key_data[1].append(val)

    return dmi_data


def _dmi_cast(key, val, clean=True):
    """
    Simple caster thingy for trying to fish out at least ints & lists from strings
    """
    if clean and not _dmi_isclean(key, val):
        return
    elif not re.match(r"serial|part|asset|product", key, flags=re.IGNORECASE):
        if "," in val:
            val = [el.strip() for el in val.split(",")]
        else:
            try:
                val = int(val)
            except Exception:  # pylint: disable=broad-except
                pass

    return val


def _dmi_isclean(key, val):
    """
    Clean out well-known bogus values
    """
    if not val or re.match("none", val, flags=re.IGNORECASE):
        # log.debug('DMI {0} value {1} seems invalid or empty'.format(key, val))
        return False
    elif "uuid" in key:
        # Try each version (1-5) of RFC4122 to check if it's actually a UUID
        for uuidver in range(1, 5):
            try:
                uuid.UUID(val, version=uuidver)
                return True
            except ValueError:
                continue
        log.trace("DMI %s value %s is an invalid UUID", key, val.replace("\n", " "))
        return False
    elif re.search("serial|part|version", key):
        # 'To be filled by O.E.M.
        # 'Not applicable' etc.
        # 'Not specified' etc.
        # 0000000, 1234667 etc.
        # begone!
        return (
            not re.match(r"^[0]+$", val)
            and not re.match(r"[0]?1234567[8]?[9]?[0]?", val)
            and not re.search(
                r"sernum|part[_-]?number|specified|filled|applicable",
                val,
                flags=re.IGNORECASE,
            )
        )
    elif re.search("asset|manufacturer", key):
        # AssetTag0. Manufacturer04. Begone.
        return not re.search(
            r"manufacturer|to be filled|available|asset|^no(ne|t)",
            val,
            flags=re.IGNORECASE,
        )
    else:
        # map unspecified, undefined, unknown & whatever to None
        return not re.search(
            r"to be filled", val, flags=re.IGNORECASE
        ) and not re.search(
            r"un(known|specified)|no(t|ne)?"
            r" (asset|provided|defined|available|present|specified)",
            val,
            flags=re.IGNORECASE,
        )


def _dmidecoder(args=None):
    """
    Call DMIdecode
    """
    dmidecoder = salt.utils.path.which_bin(["dmidecode", "smbios"])

    if not args:
        out = salt.modules.cmdmod._run_quiet(dmidecoder)
    else:
        out = salt.modules.cmdmod._run_quiet(f"{dmidecoder} {args}")

    return out
