"""Filesystem quota functions"""

import os
import errno
import pwd
from typing import Union
import ctypes
import enum
import psutil


class QuotaError(OSError):
    """Base exception for errors raised running quotactl.

    Subclasses read errno as defined in:
    https://man7.org/linux/man-pages/man2/quotactl.2.html"""

    __module__ = 'rads'


class QuotasDisabled(QuotaError, ProcessLookupError):
    # ProcessLookupError is the usual Python OSError on errno.ESRCH
    """No disk quota is found for the indicated user. Quotas have
    not been turned on for this filesystem"""
    __module__ = 'rads'


class QuotaNoUser(QuotaError, KeyError):
    """Raised when trying to fetch quota for a non-existing user"""

    __module__ = 'rads'


class VFSDqblk(ctypes.Structure):
    """dqblk struct for VFS quota data, as defined in sys/quota.h"""

    _fields_ = [
        ("bhardlimit", ctypes.c_uint64),  # Absolute limit on disk blocks
        ("bsoftlimit", ctypes.c_uint64),  # Preferred limit on disk blocks
        ("curspace", ctypes.c_uint64),  # Current occupied space (bytes)
        ("ihardlimit", ctypes.c_uint64),  # Maximum number of inodes
        ("isoftlimit", ctypes.c_uint64),  # Preferred inode limit
        ("curinodes", ctypes.c_uint64),  # Current number of inodes
        ("btime", ctypes.c_uint64),  # Time limit for excessive disk use
        ("itime", ctypes.c_uint64),  # Time limit for excessive files
        ("valid", ctypes.c_uint32),  # Bit mask of QIF constants
    ]


class XfsDiskQuota(ctypes.Structure):
    """fs_disk_quota struct for XFS quota data, as defined in xfs/xqm.h"""

    # All the blk units are in BBs (Basic Blocks) of 512 bytes
    _fields_ = [
        ("version", ctypes.c_int8),  # Version of this structure
        ("flags", ctypes.c_int8),  # XFS_{USER,PROJ,GROUP}_QUOTA
        ("fieldmask", ctypes.c_uint16),  # Field specifier
        ("id", ctypes.c_uint32),  # User, project, or group ID
        ("blk_hardlimit", ctypes.c_uint64),  # Absolute limit on disk blocks
        ("blk_softlimit", ctypes.c_uint64),  # Preferred limit on disk blocks
        ("ino_hardlimit", ctypes.c_uint64),  # Maximum allocates inodes
        ("ino_softlimit", ctypes.c_uint64),  # Preferred inode limit
        ("bcount", ctypes.c_uint64),  # disk blocks owned by the user
        ("icount", ctypes.c_uint64),  # inodes owned by the user
        ("itimer", ctypes.c_int32),  # Zero if within inode limits
        ("btimer", ctypes.c_int32),  # Similar to above, for disk blocks
        ("iwarns", ctypes.c_uint16),  # warnings issued for inode count
        ("bwarns", ctypes.c_uint16),  # warnings issued for block count
        ("padding2", ctypes.c_int32),  # for future use
        ("rtb_hardlimit", ctypes.c_uint64),  # hard limit on Realtime(RT) blocks
        ("rtb_softlimit", ctypes.c_uint64),  # soft limit on RT blocks
        ("rtbcount", ctypes.c_uint64),  # realtime blocks owned
        ("rtbtimer", ctypes.c_int32),  # similar to above; for RT blocks
        ("rtbwarns", ctypes.c_uint16),  # warnings for RT blocks
        ("padding3", ctypes.c_int16),  # for future use
        ("padding4", ctypes.c_char * 8),  # for future use
    ]


def xqm_cmd(cmd: int) -> int:
    """Mimics the XQM_CMD macro from quotaio_xfs.h:
    #define XQM_CMD(cmd)    ( ('X'<<8)+(cmd) )"""
    return (ord('X') << 8) + cmd


class XfsCommands(enum.Enum):
    """Quotactl commands for XFS"""

    Q_XQUOTAON = xqm_cmd(0x1)  # enable quota accounting/enforcement
    Q_XQUOTAOFF = xqm_cmd(0x2)  # disable quota accounting/enforcement
    Q_XGETQUOTA = xqm_cmd(0x3)  # get disk limits & usage
    Q_XSETQLIM = xqm_cmd(0x4)  # set disk limits only
    Q_XGETQSTAT = xqm_cmd(0x5)  # returns fs_quota_stat_t struct
    Q_XQUOTARM = xqm_cmd(0x6)  # free quota files' space
    Q_XGETNEXTQUOTA = xqm_cmd(0x9)  # get disk limits and usage >= ID


class VfsCommands(enum.Enum):
    """Quotactl commands for VFS"""

    USRQUOTA = 0
    Q_SYNC = 0x800001  # sync disk copy of a filesystems quotas
    Q_QUOTAON = 0x800002  # turn quotas on
    Q_QUOTAOFF = 0x800003  # turn quotas off
    Q_GETFMT = 0x800004  # get quota format used on given filesystem
    Q_GETINFO = 0x800005  # get information about quota files
    Q_SETINFO = 0x800006  # set information about quota files
    Q_GETQUOTA = 0x800007  # get user quota structure
    Q_SETQUOTA = 0x800008  # set user quota structure
    Q_GETNEXTQUOTA = 0x800009  # get disk limits and usage >= ID


class QTypes(enum.Enum):
    """Quota type constants"""

    # The dqblk_xfs.h header file defines its own XQM_USRQUOTA, XQM_GRPQUOTA,
    # and XQM_PRJQUOTA constants for XFS, but they have the same value as below
    USRQUOTA = 0
    GRPQUOTA = 1
    PRJQUOTA = 2


def qcmd(cmd: Union[VfsCommands, XfsCommands], qtype: QTypes):
    """Mimics the QCMD macro from quota.h:
    #define QCMD(cmd, type)  (((cmd) << SUBCMDSHIFT) | ((type) & SUBCMDMASK))"""
    return (cmd.value << 8) | (qtype.value & 0x00FF)


class XfsQuotaFlags(enum.Enum):
    """Flags for XfsDiskQuota.flags"""

    XFS_USER_QUOTA = 1  # User quota type
    XFS_PROJ_QUOTA = 2  # Project quota type
    XFS_GROUP_QUOTA = 4  # Group quota type


def blkdev(path: str):
    """Get the block device for the mount for a path

    Args:
        path: folder to get the block device for
    Returns:
        psutil._common.sdiskpart: device info
    """
    path = os.path.realpath(path)
    # /dev/vzfs will be filtered out on ovz if disk_partitions(all=False)
    mounts = {
        x.mountpoint: x
        for x in psutil.disk_partitions(all=True)
        if x.device.startswith('/')
    }
    while path != '/':
        if path in mounts:
            return mounts[path]
        path = os.path.dirname(path)
    return mounts[path]


blkdev.__module__ = 'rads'


class QuotaCtl:
    """Allows using the quotactl() syscall"""

    __module__ = 'rads'

    def __init__(self):
        self.quotactl = ctypes.CDLL('libc.so.6', use_errno=True).quotactl
        self.devices = {}

    def getquota(self, user: Union[str, pwd.struct_passwd]) -> int:
        """Get the current usage of a user from the quotactl() syscall in bytes

        Args:
            user: username (or pwd struct) to fetch filesystem quota for
        Raises:
            QuotaNoUser: if you pass user as a str and the user does not exist
            QuotasDisabled: if filesystem quotas are disabled
            QuotaError: base exception for any other error
        Returns:
            disk usage of a user in bytes
        """
        if not isinstance(user, pwd.struct_passwd):
            try:
                user = pwd.getpwnam(user)
            except KeyError as exc:
                raise QuotaNoUser(exc) from exc
        if user.pw_dir in self.devices:
            device_info = self.devices[user.pw_dir]
        else:
            device_info = blkdev(user.pw_dir)
            self.devices[user.pw_dir] = device_info
        dev = bytes(device_info.device, 'ascii')
        if device_info.fstype == 'xfs':
            mem = XfsDiskQuota()
            cmd = qcmd(XfsCommands.Q_XGETQUOTA, QTypes.USRQUOTA)
        else:
            mem = VFSDqblk()
            cmd = qcmd(VfsCommands.Q_GETQUOTA, QTypes.USRQUOTA)
        if self.quotactl(cmd, dev, user.pw_uid, ctypes.byref(mem)) == -1:
            # quotactl returned an error
            err = ctypes.get_errno()
            if err == errno.ESRCH:
                raise QuotasDisabled('Filesystem quotas are not enabled')
            try:
                error = errno.errorcode[38]
            except KeyError:
                msg = f'quotactl failed for {user.pw_name} with errno {err}'
            else:
                msg = (
                    f'quotactl failed for {user.pw_name} with error {error}. '
                    'See https://man7.org/linux/man-pages/man2/quotactl.2.html'
                )
            raise QuotaError(err, msg)
        if device_info.fstype == 'xfs':
            return mem.bcount * 512
        return mem.curspace
