"""A basic extended attributes (xattr) implementation for Linux, FreeBSD and macOS."""

import errno
import os
import re
import subprocess
import sys
import tempfile

from packaging.version import parse as parse_version

from .helpers import prepare_subprocess_env

from .logger import create_logger

logger = create_logger()

from .platform import listxattr, getxattr, setxattr, ENOATTR

# If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by
# the 'test_extract_capabilities' test, but also allows xattrs to work with fakeroot on Linux in normal use.
# TODO: Check whether fakeroot supports xattrs on all platforms supported below.
# TODO: If that's the case then we can make Borg fakeroot-xattr-compatible on these as well.
XATTR_FAKEROOT = False
if sys.platform.startswith('linux'):
    LD_PRELOAD = os.environ.get('LD_PRELOAD', '')
    preloads = re.split("[ :]", LD_PRELOAD)
    for preload in preloads:
        if preload.startswith("libfakeroot"):
            env = prepare_subprocess_env(system=True)
            fakeroot_output = subprocess.check_output(['fakeroot', '-v'], env=env)
            fakeroot_version = parse_version(fakeroot_output.decode('ascii').split()[-1])
            if fakeroot_version >= parse_version("1.20.2"):
                # 1.20.2 has been confirmed to have xattr support
                # 1.18.2 has been confirmed not to have xattr support
                # Versions in-between are unknown
                XATTR_FAKEROOT = True
            break


def is_enabled(path=None):
    """Determine if xattr is enabled on the filesystem."""
    with tempfile.NamedTemporaryFile(dir=path, prefix='borg-tmp') as f:
        fd = f.fileno()
        name, value = b'user.name', b'value'
        try:
            setxattr(fd, name, value)
        except OSError:
            return False
        try:
            names = listxattr(fd)
        except OSError:
            return False
        if name not in names:
            return False
        return getxattr(fd, name) == value


def get_all(path, follow_symlinks=False):
    """
    Return all extended attributes on *path* as a mapping.

    *path* can either be a path (str or bytes) or an open file descriptor (int).
    *follow_symlinks* indicates whether symlinks should be followed
    and only applies when *path* is not an open file descriptor.

    The returned mapping maps xattr names (bytes) to values (bytes or None).
    None indicates, as an xattr value, an empty value, i.e. a value of length zero.
    """
    if isinstance(path, str):
        path = os.fsencode(path)
    result = {}
    try:
        names = listxattr(path, follow_symlinks=follow_symlinks)
        for name in names:
            try:
                # xattr name is a bytes object; we directly use it.
                # If we get an empty xattr value (b''), we store None into the result dict—
                # Borg has always done it like that.
                result[name] = getxattr(path, name, follow_symlinks=follow_symlinks) or None
            except OSError as e:
                # Note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd
                if e.errno in (ENOATTR, ):  # errors we ignore silently
                    # ENOATTR: a race has happened: xattr names were deleted after listing.
                    pass
                else:  # all others: warn, skip this single xattr name, continue processing other xattrs
                    # EPERM: we were not permitted to read this attribute
                    # EINVAL: maybe the xattr name is invalid or other issue, #6988
                    logger.warning('when getting extended attribute %s: %s', name.decode(errors='replace'), str(e))
    except OSError as e:
        if e.errno in (errno.ENOTSUP, errno.EPERM):
            # if xattrs are not supported on the filesystem, we give up.
            # EPERM might be raised by listxattr.
            pass
        else:
            raise
    return result


def set_all(path, xattrs, follow_symlinks=False):
    """
    Set all extended attributes on *path* from a mapping.

    *path* can either be a path (str or bytes) or an open file descriptor (int).
    *follow_symlinks* indicates whether symlinks should be followed
    and only applies when *path* is not an open file descriptor.
    *xattrs* is a mapping that maps xattr names (bytes) to values (bytes or None).
    None indicates, as an xattr value, an empty value, i.e. a value of length zero.

    Return warning status (True means a non-fatal exception has happened and was dealt with).
    """
    if isinstance(path, str):
        path = os.fsencode(path)
    warning = False
    for k, v in xattrs.items():
        try:
            # the key k is a bytes object due to msgpack unpacking it as such.
            # if we have a None value, it means "empty", so give b'' to setxattr in that case:
            setxattr(path, k, v or b'', follow_symlinks=follow_symlinks)
        except OSError as e:
            # note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd
            warning = True
            if e.errno == errno.E2BIG:
                err_str = 'too big for this filesystem (%s)' % str(e)
            elif e.errno == errno.ENOSPC:
                # ext4 reports ENOSPC when trying to set an xattr with >4 KiB while ext4 can only support 4 KiB xattrs
                # (in this case, this is NOT a "disk full" error, just an ext4 limitation).
                err_str = 'fs full or xattr too big? [xattr len = %d] (%s)' % (len(v), str(e))
            else:
                # generic handler
                # EACCES: permission denied to set this specific xattr (this may happen related to security.* keys)
                # EPERM: operation not permitted
                err_str = str(e)
            logger.warning('when setting extended attribute %s: %s', k.decode(errors='replace'), err_str)
    return warning
