from typing import Hashable
from typing import Optional
from typing import Any


def lookup_string_match(ds: dict, key: Hashable) -> Optional[Any]:
    """
    Return the first value whose key matches the input key.
    To this function, a matching dictionary key ``k`` is one for which
    ``m(k) = m(key)`` where ``m`` is the function that converts a value
    to a string and then lowercases that string.
    Also, to clarify further, "first" refers to the first match found
    iterating the dictionary keys.

    Examples:

        >>> ds = {'kEy1': 'v1', 2: 'v2', 'key1': 'v3'}

        >>> lookup_string_match(ds, 'key1')
        'v1'

        >>> lookup_string_match(ds, 'Key1')
        'v1'

        >>> lookup_string_match(ds, '2')
        'v2'

        >>> lookup_string_match(ds, 2)
        'v2'

    :param ds: the data.
    :param key: the key for which to find a match.
    :return: the dictionary value of a matched key if one is found, ``None``
        otherwise. Also return ``None`` when either or both input params are
        ``None``.
    """
    if isinstance(ds, dict) and key is not None:
        key_to_match = str(key).lower()
        for k in ds.keys():
            if str(k).lower() == key_to_match:
                return ds[k]
    return None