import errno
import hashlib
import os
import posixpath
import re
import select
import stat
import subprocess
import sys
import textwrap
import time
from pathlib import Path

import platformdirs

from .errors import Error

from .process import prepare_subprocess_env
from ..platformflags import is_win32

from ..constants import *  # NOQA

from ..logger import create_logger

logger = create_logger()


def ensure_dir(path, mode=stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO, pretty_deadly=True):
    """
    Ensure that the directory exists with the correct permissions.

    1) Create the directory in a race-free manner.
    2) If mode is not None and the directory was created, set the correct
       permissions on the leaf directory (masking out the current umask first).
    3) If pretty_deadly is True, catch exceptions and re-raise them with a clearer
       message.

    Returns normally if the directory exists (with correct permissions) or was created.
    Raises an exception otherwise. If a fatal exception occurs, it is re-raised.
    """
    try:
        Path(path).mkdir(mode=mode, parents=True, exist_ok=True)
    except OSError as e:
        if pretty_deadly:
            raise Error(str(e))
        else:
            raise


def get_base_dir():
    """Get base directory for Borg.

    We only care for BORG_BASE_DIR here, as it can be used to override the base dir
    and not use any more or less platform-specific way to determine the base dir.
    The borg 1.x logic (HOME/USER/~ guessing) lives in borg.legacy.fs.
    """
    return os.environ.get("BORG_BASE_DIR")


def join_base_dir(*paths):
    base_dir = get_base_dir()
    return None if base_dir is None else str(Path(base_dir).joinpath(*paths))


def get_keys_dir(*, create=True):
    """Determine where to store repository keys."""
    keys_dir = os.environ.get("BORG_KEYS_DIR")
    if keys_dir is None:
        # note: do not just give this as default to the environment.get(), see issue #5979.
        return get_config_dir("keys", create=create)
    if create:
        ensure_dir(keys_dir)
    return keys_dir


def get_security_dir(repository_id=None, *, create=True):
    """Determine where to store local security information."""
    security_dir = os.environ.get("BORG_SECURITY_DIR")
    if security_dir is None:
        # note: do not just give this as default to the environment.get(), see issue #5979.
        subdirs = ("security", repository_id) if repository_id else ("security",)
        return get_data_dir(*subdirs, create=create)
    if repository_id:
        security_dir = str(Path(security_dir) / repository_id)
    if create:
        ensure_dir(security_dir)
    return security_dir


def get_data_dir(*subdirs, create=True):
    """Determine where to store borg changing data on the client.

    Any additional path components are appended to the data directory.
    """
    data_dir = os.environ.get(
        "BORG_DATA_DIR", join_base_dir(".local", "share", "borg") or platformdirs.user_data_dir("borg")
    )
    if subdirs:
        data_dir = str(Path(data_dir).joinpath(*subdirs))
    if create:
        ensure_dir(data_dir)
    return data_dir


def get_runtime_dir(*subdirs, create=True):
    """Determine where to store runtime files, like sockets, PID files, ...

    Any additional path components are appended to the runtime directory.
    """
    runtime_dir = os.environ.get(
        "BORG_RUNTIME_DIR", join_base_dir(".cache", "borg") or platformdirs.user_runtime_dir("borg")
    )
    if subdirs:
        runtime_dir = str(Path(runtime_dir).joinpath(*subdirs))
    if create:
        ensure_dir(runtime_dir)
    return runtime_dir


def get_cache_dir(*subdirs, create=True):
    """Determine where to store Borg cache data.

    Any additional path components are appended to the cache directory.
    """
    cache_dir = os.environ.get("BORG_CACHE_DIR", join_base_dir(".cache", "borg") or platformdirs.user_cache_dir("borg"))
    full_dir = str(Path(cache_dir).joinpath(*subdirs)) if subdirs else cache_dir
    if create:
        ensure_dir(full_dir)
        # the CACHEDIR.TAG applies to the whole tree below it, so it goes into the cache root
        cache_tag_fn = Path(cache_dir) / CACHE_TAG_NAME
        if not cache_tag_fn.exists():
            cache_tag_contents = (
                CACHE_TAG_CONTENTS
                + textwrap.dedent(
                    """
            # This file is a cache directory tag created by Borg.
            # For information about cache directory tags, see:
            #       https://www.bford.info/cachedir/spec.html
            """
                ).encode("ascii")
            )
            from ..platform import SaveFile

            with SaveFile(cache_tag_fn, binary=True) as fd:
                fd.write(cache_tag_contents)
    return full_dir


def get_config_dir(*subdirs, create=True):
    """Determine where to store the configuration.

    Any additional path components are appended to the config directory.
    """
    config_dir = os.environ.get(
        "BORG_CONFIG_DIR", join_base_dir(".config", "borg") or platformdirs.user_config_dir("borg")
    )
    if subdirs:
        config_dir = str(Path(config_dir).joinpath(*subdirs))
    if create:
        ensure_dir(config_dir)
    return config_dir


def dir_is_cachedir(path=None, dir_fd=None):
    """Determines whether the specified directory is a cache directory (and
    therefore should potentially be excluded from the backup) according to
    the CACHEDIR.TAG protocol (https://www.bford.info/cachedir/spec.html).

    If dir_fd is provided, operations will be based on the directory file descriptor.
    Otherwise (path is provided), operations will be based on the directory path.
    """
    tag_fd = None
    try:
        if dir_fd is not None:
            tag_fd = os.open(CACHE_TAG_NAME, os.O_RDONLY, dir_fd=dir_fd)
        else:
            tag_fd = os.open(str(Path(path) / CACHE_TAG_NAME), os.O_RDONLY)
        return os.read(tag_fd, len(CACHE_TAG_CONTENTS)) == CACHE_TAG_CONTENTS
    except (FileNotFoundError, OSError):
        return False
    finally:
        if tag_fd is not None:
            os.close(tag_fd)


def dir_is_tagged(path=None, exclude_caches=None, exclude_if_present=None, dir_fd=None):
    """Determines whether the specified path is excluded by being a cache
    directory or containing user-specified tag files/directories. Returns a
    list of the names of the tag files/directories (either CACHEDIR.TAG or the
    matching user-specified files/directories).

    If dir_fd is provided, operations will be based on the directory file descriptor.
    Otherwise (path is provided), operations will be based on the directory path.
    """
    tag_names = []

    if dir_fd is not None:
        # Use file descriptor-based operations
        if exclude_caches and dir_is_cachedir(dir_fd=dir_fd):
            tag_names.append(CACHE_TAG_NAME)
        if exclude_if_present is not None:
            for tag in exclude_if_present:
                try:
                    os.stat(tag, dir_fd=dir_fd)
                    tag_names.append(tag)
                except FileNotFoundError:
                    pass
    else:
        # Use path-based operations (for backward compatibility)
        if exclude_caches and dir_is_cachedir(path=path):
            tag_names.append(CACHE_TAG_NAME)
        if exclude_if_present is not None:
            for tag in exclude_if_present:
                tag_path = Path(path) / tag
                if tag_path.exists():
                    tag_names.append(tag)

    return tag_names


def make_path_safe(path):
    """
    Make path safe by making it relative and normalized.

    `path` is sanitized by making it relative, removing
    consecutive slashes (e.g. '//'), removing '.' elements,
    and removing trailing slashes.

    For reasons of security, a ValueError is raised should
    `path` contain any '..' elements.
    """
    if "\\.." in path or "..\\" in path:
        raise ValueError(f"unexpected '..' element in path {path!r}")

    path = slashify(path)

    if is_win32 and len(path) >= 2 and path[1] == ":":
        # Handle drive letters: C:/path -> C/path
        path = path[0].upper() + path[2:]

    path = map_chars(path)

    path = path.lstrip("/")
    if path.startswith("../") or "/../" in path or path.endswith("/..") or path == "..":
        raise ValueError(f"unexpected '..' element in path {path!r}")
    path = posixpath.normpath(path)
    return path


def slashify(path):
    """
    Replace backslashes with forward slashes if running on Windows.

    Use case: we always want to use forward slashes, even on Windows.
    """
    return path.replace("\\", "/") if is_win32 else path


# Bijective mapping to Unicode Private Use Area (like cifs mapchars)
WINDOWS_MAP_CHARS = str.maketrans(
    {
        "<": "\uF03C",
        ">": "\uF03E",
        ":": "\uF03A",
        '"': "\uF022",
        "\\": "\uF05C",
        "|": "\uF07C",
        "?": "\uF03F",
        "*": "\uF02A",
    }
)


def map_chars(path):
    """
    Map reserved characters if running on Windows.

    Use case: if an archived path contains reserved characters (that are not reserved on POSIX)
    we need to replace them with replacements to make the path usable on Windows.
    """
    if not is_win32:
        return path

    return path.translate(WINDOWS_MAP_CHARS)


def get_strip_prefix(path):
    # similar to how rsync does it, we allow users to give paths like:
    # /this/gets/stripped/./this/is/kept
    # the whole path is what is used to read from the fs,
    # the strip_prefix will be /this/gets/stripped/ and
    # this/is/kept is the path being archived.
    pos = path.find("/./")  # detect slashdot hack
    if pos > 0:
        # found a prefix to strip! make sure it ends with one "/"!
        return posixpath.normpath(path[:pos]) + "/"
    else:
        # no or empty prefix, nothing to strip!
        return None


_dotdot_re = re.compile(r"^(\.\./)+")


def remove_dotdot_prefixes(path):
    """
    Remove '../'s at the beginning of `path`. Additionally, the path is made relative.

    `path` is expected to be normalized already (e.g. via `posixpath.normpath()`).
    """
    if is_win32:
        if len(path) > 1 and path[1] == ":":
            path = path.replace(":", "", 1)

    path = path.lstrip("/")
    path = _dotdot_re.sub("", path)
    if path in ["", ".."]:
        return "."
    return path


def assert_sanitized_path(path):
    assert isinstance(path, str)
    # `path` should have been sanitized earlier. Some features,
    # like pattern matching rely on a sanitized path. As a
    # precaution we check here again.
    if make_path_safe(path) != path:
        raise ValueError(f"path {path!r} is not sanitized")
    return path


def to_sanitized_path(path):
    assert isinstance(path, str)
    # Legacy versions of Borg still allowed non-sanitized paths
    # to be stored. So, we sanitize them when reading.
    #
    # Borg 2 ensures paths are safe before storing them. Thus, when
    # support for reading Borg 1 archives is dropped, this should be
    # changed to a simple check to verify paths aren't malicious.
    # Namely, absolute paths and paths containing '..' elements must
    # be rejected.
    #
    # Also checks for '..' elements in `path` for reasons of security.
    return make_path_safe(path)


class HardLinkManager:
    """
    Manage hard links (and avoid code duplication doing so).

    A) When creating a borg2 archive from the filesystem, we have to maintain a mapping like:
       (dev, ino) -> (hlid, chunks)  # for fs_hl_targets
       If we encounter the same (dev, ino) again later, we'll just re-use the hlid and chunks list.

    B) When extracting a borg2 archive to the filesystem, we have to maintain a mapping like:
       hlid -> path
       If we encounter the same hlid again later, we hard link to the path of the already extracted
       content of same hlid.

    C) When transferring from a borg1 archive, we need:
       path -> chunks_correct  # for borg1_hl_targets, chunks_correct must be either from .chunks_healthy or .chunks.
       If we encounter a regular file item with source == path later, we reuse chunks_correct
       and create the same hlid = hardlink_id_from_path(source).

    D) When importing a tar file (simplified 1-pass way for now, not creating borg hard link items):
       path -> chunks
       If we encounter a LNK tar entry later with linkname==path, we re-use the chunks and create a regular file item.
       For better hard link support (including the very first hard link item for each group of same-target hard links),
       we would need a 2-pass processing, which is not yet implemented.
    """

    def __init__(self, *, id_type, info_type):
        self._map = {}
        self.id_type = id_type
        self.info_type = info_type  # can be a single type or a tuple of types

    def hardlink_id_from_path(self, path):
        """compute a hard link id from a path"""
        assert isinstance(path, str)
        return hashlib.sha256(path.encode("utf-8", errors="surrogateescape")).digest()

    def hardlink_id_from_inode(self, *, ino, dev):
        """compute a hard link id from an inode"""
        assert isinstance(ino, int)
        assert isinstance(dev, int)
        return hashlib.sha256(f"{ino}/{dev}".encode()).digest()

    def remember(self, *, id, info):
        """
        remember stuff from a (usually contentful) item.

        :param id: some id used to reference to the contentful item, could be:
                   a path (tar style, old borg style) [bytes]
                   a hlid (new borg style) [bytes]
                   a (dev, inode) tuple (filesystem)
        :param info: information to remember, could be:
                     chunks list
                     hlid
        """
        assert isinstance(id, self.id_type), f"id is {id!r}, not of type {self.id_type}"
        assert isinstance(info, self.info_type), f"info is {info!r}, not of type {self.info_type}"
        self._map[id] = info

    def retrieve(self, id, *, default=None):
        """
        retrieve stuff to use it in a (usually contentless) item.
        """
        assert isinstance(id, self.id_type), f"id is {id!r}, not of type {self.id_type}"
        return self._map.get(id, default)


def scandir_keyfunc(dirent):
    try:
        return (0, dirent.inode())
    except OSError as e:
        # maybe a permission denied error while doing a stat() on the dirent
        logger.debug("scandir_inorder: Unable to stat %s: %s", dirent.path, e)
        # order this dirent after all the others lexically by file name
        # we may not break the whole scandir just because of an exception in one dirent
        # ignore the exception for now, since another stat will be done later anyways
        # (or the entry will be skipped by an exclude pattern)
        return (1, dirent.name)


def scandir_inorder(*, path, fd=None):
    arg = fd if fd is not None else path
    return sorted(os.scandir(arg), key=scandir_keyfunc)


def secure_erase(path, *, avoid_collateral_damage):
    """Attempt to erase a file securely by writing random data over it before deleting it.

    If avoid_collateral_damage is True, we only secure erase if the total link count is 1,
    otherwise we just do a normal "delete" (unlink) without first overwriting it with random.
    This avoids other hard links pointing to same inode as <path> getting damaged, but might be less secure.
    A typical scenario where this is useful are quick "hard link copies" of bigger directories.

    If avoid_collateral_damage is False, we always secure erase.
    If there are hard links pointing to the same inode as <path>, they will contain random garbage afterwards.
    """
    path_obj = Path(path)
    with path_obj.open("r+b") as fd:
        st = os.stat(fd.fileno())
        if not (st.st_nlink > 1 and avoid_collateral_damage):
            fd.write(os.urandom(st.st_size))
            fd.flush()
            os.fsync(fd.fileno())
    path_obj.unlink()


def safe_unlink(path):
    """
    Safely unlink (delete) *path*.

    If we run out of space while deleting the file, we try truncating it first.
    BUT we truncate only if path is the only hard link referring to this content.

    Use this when deleting potentially large files when recovering
    from a VFS error such as ENOSPC. It can help a full file system
    recover. Refer to the "File system interaction" section
    in legacy/repository.py for further explanations.
    """
    path_obj = Path(path)
    try:
        path_obj.unlink()
    except OSError as unlink_err:
        if unlink_err.errno != errno.ENOSPC:
            # not free space related, give up here.
            raise
        # we ran out of space while trying to delete the file.
        st = path_obj.stat()
        if st.st_nlink > 1:
            # rather give up here than cause collateral damage to the other hard link.
            raise
        # no other hard link! try to recover free space by truncating this file.
        try:
            # Do not create *path* if it does not exist, open for truncation in r+b mode (=O_RDWR|O_BINARY).
            with open(path, "r+b") as fd:
                fd.truncate()
        except OSError:
            # truncate didn't work, so we still have the original unlink issue - give up:
            raise unlink_err
        else:
            # successfully truncated the file, try again deleting it:
            path_obj.unlink()


def dash_open(path, mode):
    assert "+" not in mode  # the streams are either r or w, but never both
    if path == "-":
        stream = sys.stdin if "r" in mode else sys.stdout
        return stream.buffer if "b" in mode else stream
    else:
        return open(path, mode)


def O_(*flags):
    result = 0
    for flag in flags:
        result |= getattr(os, "O_" + flag, 0)
    return result


flags_base = O_("BINARY", "NOCTTY", "RDONLY")
flags_special = flags_base | O_("NOFOLLOW")  # BLOCK == wait when reading devices or fifos
flags_special_follow = flags_base  # BLOCK == wait when reading symlinked devices or fifos
flags_normal = flags_base | O_("NONBLOCK", "NOFOLLOW")
flags_normal_follow = flags_base | O_("NONBLOCK")  # follow a symlinked recursion root, see #4737
flags_noatime = flags_normal | O_("NOATIME")
flags_dir = O_("DIRECTORY", "RDONLY", "NOFOLLOW")
flags_dir_follow = O_("DIRECTORY", "RDONLY")  # follow a symlinked recursion root, see #4737


def os_open(*, flags, path=None, parent_fd=None, name=None, noatime=False):
    """
    Use os.open to open a fs item.

    If parent_fd and name are given, they are preferred and openat will be used,
    path is not used in this case.

    :param path: full (but not necessarily absolute) path
    :param parent_fd: open directory file descriptor
    :param name: name relative to parent_fd
    :param flags: open flags for os.open() (int)
    :param noatime: True if access time shall be preserved
    :return: file descriptor
    """
    if name and parent_fd is not None:
        # name is neither None nor empty, parent_fd given.
        fname = name  # use name relative to parent_fd
    else:
        fname, parent_fd = path, None  # just use the path
    if is_win32 and os.path.isdir(fname):
        # Directories can not be opened on Windows.
        return None
    _flags_normal = flags
    if noatime:
        _flags_noatime = _flags_normal | O_("NOATIME")
        try:
            # if we have O_NOATIME, this likely will succeed if we are root or owner of file:
            fd = os.open(fname, _flags_noatime, dir_fd=parent_fd)
        except PermissionError:
            if _flags_noatime == _flags_normal:
                # we do not have O_NOATIME, no need to try again:
                raise
            # Was this EPERM due to the O_NOATIME flag? Try again without it:
            fd = os.open(fname, _flags_normal, dir_fd=parent_fd)
        except OSError as exc:
            # O_NOATIME causes EROFS when accessing a volume shadow copy in WSL1
            from . import workarounds

            if "retry_erofs" in workarounds and exc.errno == errno.EROFS and _flags_noatime != _flags_normal:
                fd = os.open(fname, _flags_normal, dir_fd=parent_fd)
            else:
                raise
    else:
        fd = os.open(fname, _flags_normal, dir_fd=parent_fd)
    return fd


def os_stat(*, path=None, parent_fd=None, name=None, follow_symlinks=False):
    """
    Use os.stat to open a fs item.

    If parent_fd and name are given, they are preferred and statat will be used,
    path is not used in this case.

    :param path: full (but not necessarily absolute) path
    :param parent_fd: open directory file descriptor
    :param name: name relative to parent_fd
    :return: stat info
    """
    if name and parent_fd is not None:
        # name is neither None nor empty, parent_fd given.
        fname = name  # use name relative to parent_fd
    else:
        fname, parent_fd = path, None  # just use the path
    return os.stat(fname, dir_fd=parent_fd, follow_symlinks=follow_symlinks)


class SpecialFileReader:
    """
    File-like reader for special files (fifos / character devices) opened with O_NONBLOCK,
    enforcing a timeout while waiting for data.

    The timeout limits how long a read waits for data to *arrive*: as long as data keeps
    flowing with gaps shorter than the timeout, reading never times out. When the timeout
    is exceeded, OSError(ETIMEDOUT) is raised.

    This also covers waiting for a fifo's writer to connect: opening a fifo read-only with
    O_NONBLOCK succeeds immediately even when there is no writer (yet) and until one connects,
    os.read() returns b"" (which otherwise means EOF). Thus, b"" only counts as EOF after some
    data was received before - until then, it means "no writer connected (yet)" and we keep
    waiting. Consequence: a writer connecting, but closing without writing anything, results
    in a timeout, not in empty file content.
    """

    NO_WRITER_POLL_INTERVAL = 0.1  # [s] how often to re-check a fifo nobody has opened for writing

    def __init__(self, fd, timeout):
        self.fd = fd  # OS-level file descriptor, opened with O_RDONLY | O_NONBLOCK
        self.timeout = timeout  # [s] maximum time to wait for data to arrive
        self.got_data = False  # whether any data was received yet

    def read(self, size):
        # Loop until we have <size> bytes, EOF or a timeout: the chunker's FileReader expects
        # short reads only at EOF, and a single os.read from a fifo returns at most the pipe
        # buffer's current content.
        parts = []
        remaining = size
        deadline = time.monotonic() + self.timeout
        while remaining:
            try:
                data = os.read(self.fd, remaining)
            except BlockingIOError:
                data = None  # a writer is connected, but no data is available right now
            if data:
                parts.append(data)
                remaining -= len(data)
                self.got_data = True
                deadline = time.monotonic() + self.timeout
                continue
            if data == b"" and self.got_data:
                break  # EOF - all writers closed after having sent data.
            wait = deadline - time.monotonic()
            if wait <= 0.0:
                raise OSError(errno.ETIMEDOUT, f"no data received for more than {self.timeout} seconds")
            if data is None:
                # wait for data to arrive (select returns early when it does or when writers close)
                select.select([self.fd], [], [], wait)
            else:
                # fifo without a connected writer: there is nothing to select() on that would
                # signal a writer connecting, so re-check periodically.
                time.sleep(min(self.NO_WRITER_POLL_INTERVAL, wait))
        return parts[0] if len(parts) == 1 else b"".join(parts)

    def seek(self, pos, whence=os.SEEK_SET):
        # Special files are not seekable - raise what lseek on a fifo fd would raise
        # (the sparse file support probes seekability and handles OSError).
        raise OSError(errno.ESPIPE, os.strerror(errno.ESPIPE))


# input map range states (borg create --map), see #4363.
MAP_DATA = "data"  # range contains data, read and store it
MAP_ZERO = "zero"  # range is known to read as all-zero, store a hole without reading it
MAP_SAME = "same"  # range is known unchanged vs. a reference archive, reuse its chunks (--reuse-from)


def read_input_map(path, *, allow_same=False):
    """
    Read and validate an input map file (borg create --map), see #4363.

    Format: one range per line: "START LENGTH STATE" (decimal or 0x-prefixed hex),
    STATE being data, zero or same. '#' starts a comment, empty lines are ignored.
    The ranges must be sorted, non-overlapping and contiguous, starting at offset 0.

    Returns a list of (start, length, state) tuples.
    """
    states = {MAP_DATA, MAP_ZERO} | ({MAP_SAME} if allow_same else set())
    input_map = []
    expected_start = 0
    with open(path) as f:
        for lineno, line in enumerate(f, start=1):
            line = line.split("#", 1)[0].strip()
            if not line:
                continue
            err = f"{path}:{lineno}: invalid input map line"
            try:
                start_s, length_s, state = line.split()
                start, length = int(start_s, 0), int(length_s, 0)
            except ValueError:
                raise Error(f"{err}: expected 'START LENGTH STATE', got: {line!r}") from None
            if state not in states:
                raise Error(f"{err}: invalid state {state!r} (expected: {', '.join(sorted(states))})")
            if length <= 0:
                raise Error(f"{err}: LENGTH must be positive")
            if start != expected_start:
                raise Error(
                    f"{err}: ranges must be sorted, contiguous and start at offset 0 "
                    f"(expected START {expected_start}, got {start})"
                )
            input_map.append((start, length, state))
            expected_start = start + length
    if not input_map:
        raise Error(f"{path}: empty input map")
    return input_map


def input_map_check_size(input_map, size):
    """Check that *input_map* covers [0, size) exactly - a mismatch means the map does not belong to this input."""
    covered = input_map[-1][0] + input_map[-1][1]
    if covered != size:
        raise Error(f"input map covers {covered} bytes, but the input has {size} bytes")


def umount(mountpoint):
    from . import set_ec

    env = prepare_subprocess_env(system=True)
    try:
        rc = subprocess.call(["fusermount", "-u", mountpoint], env=env)  # nosec B603, B607
    except FileNotFoundError:
        rc = subprocess.call(["umount", mountpoint], env=env)  # nosec B603, B607
    set_ec(rc)


# below is a slightly modified tempfile.mkstemp that has an additional mode parameter.
# see https://github.com/borgbackup/borg/issues/6933 and https://github.com/borgbackup/borg/issues/6400

import os as _os  # NOQA
import sys as _sys  # NOQA
import errno as _errno  # NOQA
from tempfile import _sanitize_params, _get_candidate_names  # type: ignore[attr-defined] # NOQA
from tempfile import TMP_MAX, _text_openflags, _bin_openflags  # type: ignore[attr-defined] # NOQA


def _mkstemp_inner(dir, pre, suf, flags, output_type, mode=0o600):
    """Code common to mkstemp, TemporaryFile, and NamedTemporaryFile."""

    dir = _os.path.abspath(dir)
    names = _get_candidate_names()
    if output_type is bytes:
        names = map(_os.fsencode, names)

    for seq in range(TMP_MAX):
        name = next(names)
        file = _os.path.join(dir, pre + name + suf)
        _sys.audit("tempfile.mkstemp", file)
        try:
            fd = _os.open(file, flags, mode)
        except FileExistsError:
            continue  # try again
        except PermissionError:
            # This exception is thrown when a directory with the chosen name
            # already exists on windows.
            if _os.name == "nt" and _os.path.isdir(dir) and _os.access(dir, _os.W_OK):
                continue
            else:
                raise
        return fd, file

    raise FileExistsError(_errno.EEXIST, "No usable temporary file name found")


def mkstemp_mode(suffix=None, prefix=None, dir=None, text=False, mode=0o600):
    """User-callable function to create and return a unique temporary
    file.  The return value is a pair (fd, name) where fd is the
    file descriptor returned by os.open, and name is the filename.
    If 'suffix' is not None, the file name will end with that suffix,
    otherwise there will be no suffix.
    If 'prefix' is not None, the file name will begin with that prefix,
    otherwise a default prefix is used.
    If 'dir' is not None, the file will be created in that directory,
    otherwise a default directory is used.
    If 'text' is specified and true, the file is opened in text
    mode.  Else (the default) the file is opened in binary mode.
    If any of 'suffix', 'prefix' and 'dir' are not None, they must be the
    same type.  If they are bytes, the returned name will be bytes; str
    otherwise.
    The file is readable and writable only by the creating user ID.
    If the operating system uses permission bits to indicate whether a
    file is executable, the file is executable by no one. The file
    descriptor is not inherited by children of this process.
    Caller is responsible for deleting the file when done with it.
    """

    prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)

    if text:
        flags = _text_openflags
    else:
        flags = _bin_openflags

    return _mkstemp_inner(dir, prefix, suffix, flags, output_type, mode)
