

from __future__ import absolute_import

import errno
import grp
import logging
import os
import pwd
import shutil
import stat
import subprocess
import sys

import six

from vdsm import constants
from vdsm import utils
from vdsm.common.network import address
from vdsm.storage import constants as sc

log = logging.getLogger('storage.fileUtils')

MIN_PORT = 1
MAX_PORT = 65535


class TarCopyFailed(RuntimeError):
    pass


def tarCopy(src, dst, exclude=()):
    excludeArgs = ["--exclude=%s" % path for path in exclude]

    tsrc = subprocess.Popen([constants.EXT_TAR, "cf", "-"] +
                            excludeArgs + ["-C", src, "."],
                            stdout=subprocess.PIPE)
    tdst = subprocess.Popen([constants.EXT_TAR, "xf", "-", "-C", dst,
                             "--touch"],
                            stdin=tsrc.stdout, stderr=subprocess.PIPE,
                            stdout=subprocess.PIPE)
    tsrc.stdout.close()
    out, err = tdst.communicate()
    tsrc.wait()

    if tdst.returncode != 0 or tsrc.returncode != 0:
        raise TarCopyFailed(tsrc.returncode, tdst.returncode, out, err)


def transformPath(remotePath):
    return remotePath.replace('_', '__').replace('/', '_')


def normalize_path(path):
    if path.startswith("/") or ":" not in path:
        return normpath(path)

    host, tail = address.hosttail_split(path)
    if ":" in tail:
        port, path = tail.split(':', 1)
        if is_port(port):
            tail = port + ":" + normpath(path)
        else:
            tail = normpath(tail)
    else:
        tail = normpath(tail)
    return address.hosttail_join(host, tail)


def normpath(path):
    path = os.path.normpath(path)
    if path.startswith('//'):
        path = path[1:]
    return path


def is_port(port_str):
    if port_str.startswith('0'):
        return False
    try:
        port = int(port_str)
        return MIN_PORT <= port <= MAX_PORT
    except ValueError:
        return False


def validateAccess(targetPath, perms=(os.R_OK | os.W_OK | os.X_OK)):
    if not os.access(targetPath, perms):
        log.warning("Permission denied for directory: %s with permissions: %s",
                    targetPath, perms)
        raise OSError(errno.EACCES, os.strerror(errno.EACCES))


def validateQemuReadable(targetPath):
    gids = (grp.getgrnam(constants.DISKIMAGE_GROUP).gr_gid,
            grp.getgrnam(constants.METADATA_GROUP).gr_gid)
    st = os.stat(targetPath)
    if not (st.st_gid in gids and st.st_mode & stat.S_IRGRP or
            st.st_mode & stat.S_IROTH):
        raise OSError(errno.EACCES, os.strerror(errno.EACCES))


def pathExists(filename, writable=False):
    check = os.R_OK

    if writable:
        check |= os.W_OK

    if os.access(filename, check):
        return True

    return os.access(filename, check)


def cleanupdir(dirPath, ignoreErrors=True):
    cleanupdir_errors = []

    def logit(func, path, exc_info):
        cleanupdir_errors.append('%s: %s' % (func.__name__, exc_info[1]))

    log.info("Removing directory: %s", dirPath)
    shutil.rmtree(dirPath, onerror=logit)
    if not ignoreErrors and cleanupdir_errors:
        raise RuntimeError("%s %s" % (dirPath, cleanupdir_errors))


def createdir(dirPath, mode=None):
    if mode is not None:
        mode = stat.S_IMODE(mode)
        params = (dirPath, mode)
    else:
        params = (dirPath,)

    log.info("Creating directory: %s mode: %s", dirPath,
             mode if mode is None else oct(mode))
    try:
        os.makedirs(*params)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
        statinfo = os.stat(dirPath)
        if not stat.S_ISDIR(statinfo.st_mode):
            raise OSError(errno.ENOTDIR, "Not a directory %s" % dirPath)
        log.debug("Using existing directory: %s", dirPath)
        if mode is not None:
            curMode = stat.S_IMODE(statinfo.st_mode)
            if curMode != mode:
                raise OSError(errno.EPERM,
                              ("Existing %s permissions %o are not as "
                               "requested %o") % (dirPath, curMode, mode))


def resolveUid(user):
    if isinstance(user, six.string_types):
        uid = pwd.getpwnam(user).pw_uid
    else:
        uid = int(user)
    return uid


def resolveGid(group):
    if isinstance(group, six.string_types):
        gid = grp.getgrnam(group).gr_gid
    else:
        gid = int(group)
    return gid


def chown(path, user=-1, group=-1):
    uid = resolveUid(user)
    gid = resolveGid(group)

    stat = os.stat(path)
    currentUid = stat.st_uid
    currentGid = stat.st_gid

    if ((uid == currentUid or user == -1) and
            (gid == currentGid or group == -1)):
        return True
    log.info("Changing owner for %s, to (%s:%s)", path, uid, gid)
    os.chown(path, uid, gid)
    return True


def atomic_symlink(target, name):
    log.info("Linking %r to %r", target, name)
    try:
        current_target = os.readlink(name)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise
    else:
        if current_target == target:
            log.debug("link %r exists", name)
            return
        log.debug("Replacing stale link to %r", current_target)

    tmp_name = name + ".tmp"
    try:
        log.debug("Creating symlink from %r to %r", target, tmp_name)
        os.symlink(target, tmp_name)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
        log.info("Removing stale temporary link %r", tmp_name)
        os.unlink(tmp_name)
        log.debug("Creating symlink from %r to %r", target, tmp_name)
        os.symlink(target, tmp_name)

    try:
        log.debug("Renaming %r to %r", tmp_name, name)
        os.rename(tmp_name, name)
    except:
        exc_info = sys.exc_info()
        log.debug("Unlinking %r", tmp_name)
        try:
            os.unlink(tmp_name)
        except OSError as e:
            log.error("Cannot remove temporary link %r: %s", tmp_name, e)
        six.reraise(*exc_info)


def fsyncPath(path):
    fd = os.open(path, os.O_RDONLY)
    try:
        os.fsync(fd)
    finally:
        os.close(fd)


def copyUserModeToGroup(path):
    mode = os.stat(path).st_mode
    newGroupMode = userMode >> 3
        newMode = (mode & 0o707) | newGroupMode
        os.chmod(path, newMode)


def padToBlockSize(path):
    with open(path, 'a') as f:
        size = os.fstat(f.fileno()).st_size
        newSize = utils.round(size, sc.BLOCK_SIZE_4K)
        log.info("Truncating file %s to %d bytes", path, newSize)
        os.ftruncate(f.fileno(), newSize)
