"""
A module written originally by Armin Ronacher to manage file transfers in an
atomic way
"""

import errno
import os
import pathlib
import random
import shutil
import sys
import tempfile
import time

import salt.utils.files
import salt.utils.win_dacl

CAN_RENAME_OPEN_FILE = False
if os.name == "nt":  # pragma: no cover

    def _rename(src, dst):
        return False

    def _rename_atomic(src, dst):
        return False

    try:
        import ctypes

        _MOVEFILE_REPLACE_EXISTING = 0x1
        _MOVEFILE_WRITE_THROUGH = 0x8
        _MoveFileEx = ctypes.windll.kernel32.MoveFileExW  # pylint: disable=C0103

        def _rename(src, dst):  # pylint: disable=E0102
            if isinstance(src, pathlib.Path):
                src = str(src)
            if isinstance(dst, pathlib.Path):
                dst = str(dst)
            if not isinstance(src, str):
                src = str(src, sys.getfilesystemencoding())
            if not isinstance(dst, str):
                dst = str(dst, sys.getfilesystemencoding())
            if _rename_atomic(src, dst):
                return True
            retry = 0
            rval = False
            while not rval and retry < 100:
                rval = _MoveFileEx(
                    src, dst, _MOVEFILE_REPLACE_EXISTING | _MOVEFILE_WRITE_THROUGH
                )
                if not rval:
                    time.sleep(0.001)
                    retry += 1
            return rval

        # new in Vista and Windows Server 2008
        # pylint: disable=C0103
        _CreateTransaction = ctypes.windll.ktmw32.CreateTransaction
        _CommitTransaction = ctypes.windll.ktmw32.CommitTransaction
        _MoveFileTransacted = ctypes.windll.kernel32.MoveFileTransactedW
        _CloseHandle = ctypes.windll.kernel32.CloseHandle
        # pylint: enable=C0103
        CAN_RENAME_OPEN_FILE = True

        def _rename_atomic(src, dst):  # pylint: disable=E0102
            tra = _CreateTransaction(None, 0, 0, 0, 0, 1000, "Atomic rename")
            if tra == -1:
                return False
            try:
                retry = 0
                rval = False
                while not rval and retry < 100:
                    rval = _MoveFileTransacted(
                        src,
                        dst,
                        None,
                        None,
                        _MOVEFILE_REPLACE_EXISTING | _MOVEFILE_WRITE_THROUGH,
                        tra,
                    )
                    if rval:
                        rval = _CommitTransaction(tra)
                        break
                    else:
                        time.sleep(0.001)
                        retry += 1
                return rval
            finally:
                _CloseHandle(tra)

    except Exception:  # pylint: disable=broad-except
        pass

    def atomic_rename(src, dst):
        # Try atomic or pseudo-atomic rename
        if _rename(src, dst):
            return
        # Fall back to "move away and replace"
        try:
            os.rename(src, dst)
        except OSError as err:
            if err.errno != errno.EEXIST:
                raise
            old = f"{dst}-{random.randint(0, sys.maxint):08x}"
            os.rename(dst, old)
            os.rename(src, dst)
            try:
                os.unlink(old)
            except Exception:  # pylint: disable=broad-except
                pass

else:
    atomic_rename = os.rename  # pylint: disable=C0103
    CAN_RENAME_OPEN_FILE = True


class _AtomicWFile:
    """
    Helper class for :func:`atomic_open`.
    """

    def __init__(self, fhanle, tmp_filename, filename):
        self._fh = fhanle
        self._tmp_filename = tmp_filename
        self._filename = filename

    def __getattr__(self, attr):
        return getattr(self._fh, attr)

    def __enter__(self):
        return self

    def close(self):
        if self._fh.closed:
            return
        # Flush user-space buffers and fsync the file's data + metadata
        # before the atomic rename. Without this, a crash after the rename
        # can expose a written-but-unsynced (truncated/partial) file:
        # POSIX does not require an implicit fsync on rename, and the
        # fsync-on-rename heuristic of ext4 et al. is not universal. The
        # fileno() / fsync() pair is guarded because some file-like objects
        # do not expose a real fd (e.g. an in-memory wrapper); in that case
        # there is nothing useful to sync at the fd level.
        try:
            self._fh.flush()
            os.fsync(self._fh.fileno())
        except (AttributeError, OSError, ValueError):
            pass
        self._fh.close()
        if salt.utils.win_dacl.HAS_WIN32:
            if os.path.isfile(self._filename):
                salt.utils.win_dacl.copy_security(
                    source=self._filename, target=self._tmp_filename
                )
        else:
            if os.path.isfile(self._filename):
                shutil.copymode(self._filename, self._tmp_filename)
                st = os.stat(self._filename)
                os.chown(self._tmp_filename, st.st_uid, st.st_gid)
            else:
                # chmod file to default mode based on umask
                os.chmod(self._tmp_filename, 0o666 & ~salt.utils.files.get_umask())
        atomic_rename(self._tmp_filename, self._filename)

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.close()
        else:
            self._fh.close()
            try:
                os.remove(self._tmp_filename)
            except OSError:
                pass

    def __repr__(self):
        return "<{} {}{}, mode {}>".format(
            self.__class__.__name__,
            self._fh.closed and "closed " or "",
            self._filename,
            self._fh.mode,
        )


def atomic_open(filename, mode="w"):
    """
    Works like a regular `open()` but writes updates into a temporary
    file instead of the given file and moves it over when the file is
    closed.  The file returned behaves as if it was a regular Python
    """
    if mode in ("r", "rb", "r+", "rb+", "a", "ab"):
        raise TypeError("Read or append modes don't work with atomic_open")
    kwargs = {
        "prefix": ".___atomic_write",
        "dir": os.path.dirname(filename),
        "delete": False,
    }
    if "b" not in mode:
        kwargs["newline"] = ""
    ntf = tempfile.NamedTemporaryFile(mode, **kwargs)
    return _AtomicWFile(ntf, ntf.name, filename)


def safe_atomic_write(dst, data, backup_mode="", cachedir=""):
    """
    Create a temporary file with only user r/w perms, write the
    data and atomically copy it to the destination. Supports the
    Salt file backup mechanism.

    dst
        The path to write to.

    data
        String or bytes of data to write.

    backup_mode
        Optional parameter to override the configured
        :ref:`backup mode <file-state-backups>` explicitly.

    cachedir
        Optional parameter to override the configured
        cachedir explicitly. Backups are written into
        a subdirectory of this path called ``file_backup``.
    """
    mode = "wb" if isinstance(data, bytes) else "w"
    tmp = salt.utils.files.mkstemp(prefix=salt.utils.files.TEMPFILE_PREFIX)
    with salt.utils.files.fopen(tmp, mode) as tmp_:
        tmp_.write(data)
    salt.utils.files.copyfile(tmp, dst, backup_mode, cachedir)
    salt.utils.files.safe_rm(tmp)
