import os
import errno
import logging
from contextlib import contextmanager

import cephfs

from .snapshot_util import mksnap, rmsnap
from .charmap_util import charmap_get, charmap_set, charmap_rm
from .pin_util import pin
from .template import GroupTemplate
from ..fs_util import listdir, listsnaps, get_ancestor_xattr, create_base_dir, has_subdir
from ..exception import VolumeException

log = logging.getLogger(__name__)


class Group(GroupTemplate):
    # Reserved subvolume group name which we use in paths for subvolumes
    # that are not assigned to a group (i.e. created with group=None)
    NO_GROUP_NAME = "_nogroup"

    def __init__(self, fs, vol_spec, groupname):
        if groupname == Group.NO_GROUP_NAME:
            raise VolumeException(-errno.EPERM, "Operation not permitted for group '{0}' as it is an internal group.".format(groupname))
        if groupname in vol_spec.INTERNAL_DIRS:
            raise VolumeException(-errno.EINVAL, "'{0}' is an internal directory and not a valid group name.".format(groupname))
        self.fs = fs
        self.user_id = None
        self.group_id = None
        self.vol_spec = vol_spec
        self.groupname = groupname if groupname else Group.NO_GROUP_NAME

    @property
    def path(self):
        return os.path.join(self.vol_spec.base_dir.encode('utf-8'), self.groupname.encode('utf-8'))

    @property
    def group_name(self):
        return self.groupname

    @property
    def uid(self):
        return self.user_id

    @uid.setter
    def uid(self, val):
        self.user_id = val

    @property
    def gid(self):
        return self.group_id

    @gid.setter
    def gid(self, val):
        self.group_id = val

    def is_default_group(self):
        return self.groupname == Group.NO_GROUP_NAME

    def list_subvolumes(self):
        try:
            return listdir(self.fs, self.path)
        except VolumeException as ve:
            # listing a default group when it's not yet created
            if ve.errno == -errno.ENOENT and self.is_default_group():
                return []
            raise

    def has_subvolumes(self):
        try:
            return has_subdir(self.fs, self.path)
        except VolumeException as ve:
            # listing a default group when it's not yet created
            if ve.errno == -errno.ENOENT and self.is_default_group():
                return False
            raise

    def pin(self, pin_type, pin_setting):
        return pin(self.fs, self.path, pin_type, pin_setting)

    def charmap_set(self, setting, value):
        return charmap_set(self.fs, self.path, setting, value)

    def charmap_rm(self):
        return charmap_rm(self.fs, self.path)

    def charmap_get(self, setting):
        return charmap_get(self.fs, self.path, setting)

    def create_snapshot(self, snapname):
        snappath = os.path.join(self.path,
                                self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
                                snapname.encode('utf-8'))
        mksnap(self.fs, snappath)

    def remove_snapshot(self, snapname):
        snappath = os.path.join(self.path,
                                self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
                                snapname.encode('utf-8'))
        rmsnap(self.fs, snappath)

    def list_snapshots(self):
        try:
            dirpath = os.path.join(self.path,
                                   self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
            return listsnaps(self.fs, self.vol_spec, dirpath, filter_inherited_snaps=True)
        except VolumeException as ve:
            if ve.errno == -errno.ENOENT:
                return []
            raise

    def info(self):
        st = self.fs.statx(self.path, cephfs.CEPH_STATX_BTIME | cephfs.CEPH_STATX_SIZE
                           | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID | cephfs.CEPH_STATX_MODE
                           | cephfs.CEPH_STATX_ATIME | cephfs.CEPH_STATX_MTIME | cephfs.CEPH_STATX_CTIME,
                           cephfs.AT_SYMLINK_NOFOLLOW)
        usedbytes = st["size"]
        try:
            nsize = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
        except cephfs.NoData:
            nsize = 0

        try:
            data_pool = self.fs.getxattr(self.path, 'ceph.dir.layout.pool').decode('utf-8')
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

        try:
            normalization = self.fs.getxattr(self.path, 'ceph.dir.normalization').decode('utf-8')
        except cephfs.NoData:
            normalization = "none"

        try:
            casesensitive = self.fs.getxattr(self.path, 'ceph.dir.casesensitive').decode('utf-8')
            casesensitive = casesensitive == "1"
        except cephfs.NoData:
            casesensitive = True

        return {'uid': int(st["uid"]),
                'gid': int(st["gid"]),
                'atime': str(st["atime"]),
                'mtime': str(st["mtime"]),
                'ctime': str(st["ctime"]),
                'mode': int(st["mode"]),
                'data_pool': data_pool,
                'created_at': str(st["btime"]),
                'bytes_quota': "infinite" if nsize == 0 else nsize,
                'bytes_used': int(usedbytes),
                'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0),
                'normalization': normalization,
                'casesensitive': casesensitive,
        }

    def resize(self, newsize, noshrink):
        try:
            newsize = int(newsize)
            if newsize <= 0:
                raise VolumeException(-errno.EINVAL, "Invalid subvolume group size")
        except ValueError:
            newsize = newsize.lower()
            if not (newsize == "inf" or newsize == "infinite"):
                raise (VolumeException(-errno.EINVAL, "invalid size option '{0}'".format(newsize)))
            newsize = 0
            noshrink = False

        try:
            maxbytes = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
        except cephfs.NoData:
            maxbytes = 0
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

        group_stat = self.fs.stat(self.path)
        if newsize > 0 and newsize < group_stat.st_size:
            if noshrink:
                raise VolumeException(-errno.EINVAL, "Can't resize the subvolume group. The new size"
                                      " '{0}' would be lesser than the current used size '{1}'"
                                      .format(newsize, group_stat.st_size))

        if not newsize == maxbytes:
            try:
                self.fs.setxattr(self.path, 'ceph.quota.max_bytes', str(newsize).encode('utf-8'), 0)
            except cephfs.Error as e:
                raise (VolumeException(-e.args[0],
                                       "Cannot set new size for the subvolume group. '{0}'".format(e.args[1])))
        return newsize, group_stat.st_size

def set_group_attrs(fs, path, attrs):
    # set subvolume group attrs
    # set size
    quota = attrs.get("quota")
    if quota is not None:
        try:
            fs.setxattr(path, 'ceph.quota.max_bytes', str(quota).encode('utf-8'), 0)
        except cephfs.InvalidValue:
            raise VolumeException(-errno.EINVAL, "invalid size specified: '{0}'".format(quota))
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

    # set pool layout
    pool = attrs.get("data_pool")
    if not pool:
        pool = get_ancestor_xattr(fs, path, "ceph.dir.layout.pool")
    try:
        fs.setxattr(path, 'ceph.dir.layout.pool', pool.encode('utf-8'), 0)
    except cephfs.InvalidValue:
        raise VolumeException(-errno.EINVAL,
                              "Invalid pool layout '{0}'. It must be a valid data pool".format(pool))

    # set uid/gid
    uid = attrs.get("uid")
    if uid is None:
        uid = 0
    else:
        try:
            uid = int(uid)
            if uid < 0:
                raise ValueError
        except ValueError:
            raise VolumeException(-errno.EINVAL, "invalid UID")

    gid = attrs.get("gid")
    if gid is None:
        gid = 0
    else:
        try:
            gid = int(gid)
            if gid < 0:
                raise ValueError
        except ValueError:
            raise VolumeException(-errno.EINVAL, "invalid GID")
    fs.chown(path, uid, gid)

    # set mode
    mode = attrs.get("mode", None)
    if mode is not None:
        fs.lchmod(path, mode)

    normalization = attrs.get("normalization")
    if normalization is not None:
        try:
            fs.setxattr(path, "ceph.dir.normalization", normalization.encode('utf-8'), 0)
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

    casesensitive = attrs.get("casesensitive")
    if casesensitive is False:
        try:
            fs.setxattr(path, "ceph.dir.casesensitive", "0".encode('utf-8'), 0)
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

def create_group(fs, vol_spec, groupname, size, pool, mode, uid, gid, normalization, casesensitive):
    """
    create a subvolume group.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :param size: In bytes, or None for no size limit
    :param pool: the RADOS pool where the data objects of the subvolumes will be stored
    :param mode: the user permissions
    :param uid: the user identifier
    :param gid: the group identifier
    :param normalization: the unicode normalization form to use (nfd, nfc, nfkd or nfkc)
    :param casesensitive: whether to make the subvolume case insensitive or not
    :return: None
    """
    group = Group(fs, vol_spec, groupname)
    path = group.path
    vol_spec_base_dir = group.vol_spec.base_dir.encode('utf-8')

    # create vol_spec base directory with default mode(0o755) if it doesn't exist
    create_base_dir(fs, vol_spec_base_dir, vol_spec.DEFAULT_MODE)
    fs.mkdir(path, mode)
    try:
        attrs = {
            'uid': uid,
            'gid': gid,
            'data_pool': pool,
            'quota': size,
            'normalization': normalization,
            'casesensitive': casesensitive,
        }
        set_group_attrs(fs, path, attrs)
    except (cephfs.Error, VolumeException) as e:
        try:
            # cleanup group path on best effort basis
            log.debug("cleaning up subvolume group path: {0}".format(path))
            fs.rmdir(path)
        except cephfs.Error as ce:
            log.debug("failed to clean up subvolume group {0} with path: {1} ({2})".format(groupname, path, ce))
        if isinstance(e, cephfs.Error):
            e = VolumeException(-e.args[0], e.args[1])
        raise e


def remove_group(fs, vol_spec, groupname):
    """
    remove a subvolume group.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :return: None
    """
    group = Group(fs, vol_spec, groupname)
    try:
        fs.rmdir(group.path)
    except cephfs.Error as e:
        if e.args[0] == errno.ENOENT:
            raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
        elif e.args[0] == errno.ENOTEMPTY:
            raise VolumeException(-errno.ENOTEMPTY, f"subvolume group {groupname} contains subvolume(s) "
                                  "or retained snapshots of deleted subvolume(s)")
        raise VolumeException(-e.args[0], e.args[1])


@contextmanager
def open_group(fs, vol_spec, groupname):
    """
    open a subvolume group. This API is to be used as a context manager.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :return: yields a group object (subclass of GroupTemplate)
    """
    group = Group(fs, vol_spec, groupname)
    try:
        st = fs.stat(group.path)
        group.uid = int(st.st_uid)
        group.gid = int(st.st_gid)
    except cephfs.Error as e:
        if e.args[0] == errno.ENOENT:
            if not group.is_default_group():
                raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
        else:
            raise VolumeException(-e.args[0], e.args[1])
    yield group


@contextmanager
def open_group_unique(fs, vol_spec, groupname, c_group, c_groupname):
    if groupname == c_groupname:
        yield c_group
    else:
        with open_group(fs, vol_spec, groupname) as group:
            yield group
