# coding: utf-8
from __future__ import print_function, unicode_literals

import re
import os
import sys
import stat
import time
import base64
import hashlib
import threading
from datetime import datetime

from .__init__ import WINDOWS
from .util import (
    IMPLICATIONS,
    uncyg,
    undot,
    unhumanize,
    absreal,
    Pebkac,
    fsenc,
    statdir,
)
from .bos import bos


LEELOO_DALLAS = "leeloo_dallas"


class AXS(object):
    def __init__(self, uread=None, uwrite=None, umove=None, udel=None, uget=None):
        self.uread = {} if uread is None else {k: 1 for k in uread}
        self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
        self.umove = {} if umove is None else {k: 1 for k in umove}
        self.udel = {} if udel is None else {k: 1 for k in udel}
        self.uget = {} if uget is None else {k: 1 for k in uget}

    def __repr__(self):
        return "AXS({})".format(
            ", ".join(
                "{}={!r}".format(k, self.__dict__[k])
                for k in "uread uwrite umove udel uget".split()
            )
        )


class Lim(object):
    def __init__(self):
        self.nups = {}  # num tracker
        self.bups = {}  # byte tracker list
        self.bupc = {}  # byte tracker cache

        self.nosub = False  # disallow subdirectories

        self.smin = None  # filesize min
        self.smax = None  # filesize max

        self.bwin = None  # bytes window
        self.bmax = None  # bytes max
        self.nwin = None  # num window
        self.nmax = None  # num max

        self.rotn = None  # rot num files
        self.rotl = None  # rot depth
        self.rotf = None  # rot datefmt
        self.rot_re = None  # rotf check

    def set_rotf(self, fmt):
        self.rotf = fmt
        r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
        r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
        self.rot_re = re.compile("(^|/)" + r + "$")

    def all(self, ip, rem, sz, abspath):
        self.chk_nup(ip)
        self.chk_bup(ip)
        self.chk_rem(rem)
        if sz != -1:
            self.chk_sz(sz)

        ap2, vp2 = self.rot(abspath)
        if abspath == ap2:
            return ap2, rem

        return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)

    def chk_sz(self, sz):
        if self.smin is not None and sz < self.smin:
            raise Pebkac(400, "file too small")

        if self.smax is not None and sz > self.smax:
            raise Pebkac(400, "file too big")

    def chk_rem(self, rem):
        if self.nosub and rem:
            raise Pebkac(500, "no subdirectories allowed")

    def rot(self, path):
        if not self.rotf and not self.rotn:
            return path, ""

        if self.rotf:
            path = path.rstrip("/\\")
            if self.rot_re.search(path.replace("\\", "/")):
                return path, ""

            suf = datetime.utcnow().strftime(self.rotf)
            if path:
                path += "/"

            return path + suf, suf

        ret = self.dive(path, self.rotl)
        if not ret:
            raise Pebkac(500, "no available slots in volume")

        d = ret[len(path) :].strip("/\\").replace("\\", "/")
        return ret, d

    def dive(self, path, lvs):
        items = bos.listdir(path)

        if not lvs:
            # at leaf level
            return None if len(items) >= self.rotn else ""

        dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
        dirs.sort()

        if not dirs:
            # no branches yet; make one
            sub = os.path.join(path, "0")
            bos.mkdir(sub)
        else:
            # try newest branch only
            sub = os.path.join(path, str(dirs[-1]))

        ret = self.dive(sub, lvs - 1)
        if ret is not None:
            return os.path.join(sub, ret)

        if len(dirs) >= self.rotn:
            # full branch or root
            return None

        # make a branch
        sub = os.path.join(path, str(dirs[-1] + 1))
        bos.mkdir(sub)
        ret = self.dive(sub, lvs - 1)
        if ret is None:
            raise Pebkac(500, "rotation bug")

        return os.path.join(sub, ret)

    def nup(self, ip):
        try:
            self.nups[ip].append(time.time())
        except:
            self.nups[ip] = [time.time()]

    def bup(self, ip, nbytes):
        v = [time.time(), nbytes]
        try:
            self.bups[ip].append(v)
            self.bupc[ip] += nbytes
        except:
            self.bups[ip] = [v]
            self.bupc[ip] = nbytes

    def chk_nup(self, ip):
        if not self.nmax or ip not in self.nups:
            return

        nups = self.nups[ip]
        cutoff = time.time() - self.nwin
        while nups and nups[0] < cutoff:
            nups.pop(0)

        if len(nups) >= self.nmax:
            raise Pebkac(429, "too many uploads")

    def chk_bup(self, ip):
        if not self.bmax or ip not in self.bups:
            return

        bups = self.bups[ip]
        cutoff = time.time() - self.bwin
        mark = self.bupc[ip]
        while bups and bups[0][0] < cutoff:
            mark -= bups.pop(0)[1]

        self.bupc[ip] = mark
        if mark >= self.bmax:
            raise Pebkac(429, "ingress saturated")


class VFS(object):
    """single level in the virtual fs"""

    def __init__(self, log, realpath, vpath, axs, flags):
        self.log = log
        self.realpath = realpath  # absolute path on host filesystem
        self.vpath = vpath  # absolute path in the virtual filesystem
        self.axs = axs  # type: AXS
        self.flags = flags  # config options
        self.nodes = {}  # child nodes
        self.histtab = None  # all realpath->histpath
        self.dbv = None  # closest full/non-jump parent
        self.lim = None  # type: Lim  # upload limits; only set for dbv

        if realpath:
            self.histpath = os.path.join(realpath, ".hist")  # db / thumbcache
            self.all_vols = {vpath: self}  # flattened recursive
            self.aread = {}
            self.awrite = {}
            self.amove = {}
            self.adel = {}
            self.aget = {}
        else:
            self.histpath = None
            self.all_vols = None
            self.aread = None
            self.awrite = None
            self.amove = None
            self.adel = None
            self.aget = None

    def __repr__(self):
        return "VFS({})".format(
            ", ".join(
                "{}={!r}".format(k, self.__dict__[k])
                for k in "realpath vpath axs flags".split()
            )
        )

    def get_all_vols(self, outdict):
        if self.realpath:
            outdict[self.vpath] = self

        for v in self.nodes.values():
            v.get_all_vols(outdict)

    def add(self, src, dst):
        """get existing, or add new path to the vfs"""
        assert not src.endswith("/")  # nosec
        assert not dst.endswith("/")  # nosec

        if "/" in dst:
            # requires breadth-first population (permissions trickle down)
            name, dst = dst.split("/", 1)
            if name in self.nodes:
                # exists; do not manipulate permissions
                return self.nodes[name].add(src, dst)

            vn = VFS(
                self.log,
                os.path.join(self.realpath, name) if self.realpath else None,
                "{}/{}".format(self.vpath, name).lstrip("/"),
                self.axs,
                self._copy_flags(name),
            )
            vn.dbv = self.dbv or self
            self.nodes[name] = vn
            return vn.add(src, dst)

        if dst in self.nodes:
            # leaf exists; return as-is
            return self.nodes[dst]

        # leaf does not exist; create and keep permissions blank
        vp = "{}/{}".format(self.vpath, dst).lstrip("/")
        vn = VFS(self.log, src, vp, AXS(), {})
        vn.dbv = self.dbv or self
        self.nodes[dst] = vn
        return vn

    def _copy_flags(self, name):
        flags = {k: v for k, v in self.flags.items()}
        hist = flags.get("hist")
        if hist and hist != "-":
            flags["hist"] = "{}/{}".format(hist.rstrip("/"), name)

        return flags

    def bubble_flags(self):
        if self.dbv:
            for k, v in self.dbv.flags.items():
                if k not in ["hist"]:
                    self.flags[k] = v

        for v in self.nodes.values():
            v.bubble_flags()

    def _find(self, vpath):
        """return [vfs,remainder]"""
        vpath = undot(vpath)
        if vpath == "":
            return [self, ""]

        if "/" in vpath:
            name, rem = vpath.split("/", 1)
        else:
            name = vpath
            rem = ""

        if name in self.nodes:
            return self.nodes[name]._find(rem)

        return [self, vpath]

    def can_access(self, vpath, uname):
        # type: (str, str) -> tuple[bool, bool, bool, bool]
        """can Read,Write,Move,Delete,Get"""
        vn, _ = self._find(vpath)
        c = vn.axs
        return [
            uname in c.uread or "*" in c.uread,
            uname in c.uwrite or "*" in c.uwrite,
            uname in c.umove or "*" in c.umove,
            uname in c.udel or "*" in c.udel,
            uname in c.uget or "*" in c.uget,
        ]

    def get(
        self,
        vpath,
        uname,
        will_read,
        will_write,
        will_move=False,
        will_del=False,
        will_get=False,
    ):
        # type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str]
        """returns [vfsnode,fs_remainder] if user has the requested permissions"""
        vn, rem = self._find(vpath)
        c = vn.axs

        for req, d, msg in [
            [will_read, c.uread, "read"],
            [will_write, c.uwrite, "write"],
            [will_move, c.umove, "move"],
            [will_del, c.udel, "delete"],
            [will_get, c.uget, "get"],
        ]:
            if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
                m = "you don't have {}-access for this location"
                raise Pebkac(403, m.format(msg))

        return vn, rem

    def get_dbv(self, vrem):
        # type: (str) -> tuple[VFS, str]
        dbv = self.dbv
        if not dbv:
            return self, vrem

        vrem = [self.vpath[len(dbv.vpath) :].lstrip("/"), vrem]
        vrem = "/".join([x for x in vrem if x])
        return dbv, vrem

    def canonical(self, rem, resolve=True):
        """returns the canonical path (fully-resolved absolute fs path)"""
        rp = self.realpath
        if rem:
            rp += "/" + rem

        return absreal(rp) if resolve else rp

    def ls(self, rem, uname, scandir, permsets, lstat=False):
        # type: (str, str, bool, list[list[bool]], bool) -> tuple[str, str, dict[str, VFS]]
        """return user-readable [fsdir,real,virt] items at vpath"""
        virt_vis = {}  # nodes readable by user
        abspath = self.canonical(rem)
        real = list(statdir(self.log, scandir, lstat, abspath))
        real.sort()
        if not rem:
            # no vfs nodes in the list of real inodes
            real = [x for x in real if x[0] not in self.nodes]

            for name, vn2 in sorted(self.nodes.items()):
                ok = False
                axs = vn2.axs
                axs = [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget]
                for pset in permsets:
                    ok = True
                    for req, lst in zip(pset, axs):
                        if req and uname not in lst and "*" not in lst:
                            ok = False
                    if ok:
                        break

                if ok:
                    virt_vis[name] = vn2

        return [abspath, real, virt_vis]

    def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
        """
        recursively yields from ./rem;
        rel is a unix-style user-defined vpath (not vfs-related)
        """

        fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
        dbv, vrem = self.get_dbv(rem)

        if (
            seen
            and (not fsroot.startswith(seen[-1]) or fsroot == seen[-1])
            and fsroot in seen
        ):
            m = "bailing from symlink loop,\n  prev: {}\n  curr: {}\n  from: {}/{}"
            self.log("vfs.walk", m.format(seen[-1], fsroot, self.vpath, rem), 3)
            return

        seen = seen[:] + [fsroot]
        rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
        rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]

        rfiles.sort()
        rdirs.sort()

        yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt

        for rdir, _ in rdirs:
            if not dots and rdir.startswith("."):
                continue

            wrel = (rel + "/" + rdir).lstrip("/")
            wrem = (rem + "/" + rdir).lstrip("/")
            for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
                yield x

        for n, vfs in sorted(vfs_virt.items()):
            if not dots and n.startswith("."):
                continue

            wrel = (rel + "/" + n).lstrip("/")
            for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat):
                yield x

    def zipgen(self, vrem, flt, uname, dots, scandir):
        if flt:
            flt = {k: True for k in flt}

        f1 = "{0}.hist{0}up2k.".format(os.sep)
        f2a = os.sep + "dir.txt"
        f2b = "{0}.hist{0}".format(os.sep)

        # if multiselect: add all items to archive root
        # if single folder: the folder itself is the top-level item
        folder = "" if flt else (vrem.split("/")[-1] or "top")

        g = self.walk(folder, vrem, [], uname, [[True]], dots, scandir, False)
        for _, _, vpath, apath, files, rd, vd in g:
            if flt:
                files = [x for x in files if x[0] in flt]

                rm = [x for x in rd if x[0] not in flt]
                [rd.remove(x) for x in rm]

                rm = [x for x in vd.keys() if x not in flt]
                [vd.pop(x) for x in rm]

                flt = None

            # print(repr([vpath, apath, [x[0] for x in files]]))
            fnames = [n[0] for n in files]
            vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
            apaths = [os.path.join(apath, n) for n in fnames]
            files = list(zip(vpaths, apaths, files))

            if not dots:
                # dotfile filtering based on vpath (intended visibility)
                files = [x for x in files if "/." not in "/" + x[0]]

                rm = [x for x in rd if x[0].startswith(".")]
                for x in rm:
                    rd.remove(x)

                rm = [k for k in vd.keys() if k.startswith(".")]
                for x in rm:
                    del vd[x]

            # up2k filetring based on actual abspath
            files = [
                x
                for x in files
                if f1 not in x[1] and (not x[1].endswith(f2a) or f2b not in x[1])
            ]

            for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
                yield f


if WINDOWS:
    re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
else:
    re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")


class AuthSrv(object):
    """verifies users against given paths"""

    def __init__(self, args, log_func, warn_anonwrite=True):
        self.args = args
        self.log_func = log_func
        self.warn_anonwrite = warn_anonwrite
        self.line_ctr = 0

        self.mutex = threading.Lock()
        self.reload()

    def log(self, msg, c=0):
        if self.log_func:
            self.log_func("auth", msg, c)

    def laggy_iter(self, iterable):
        """returns [value,isFinalValue]"""
        it = iter(iterable)
        prev = next(it)
        for x in it:
            yield prev, False
            prev = x

        yield prev, True

    def _parse_config_file(self, fd, acct, daxs, mflags, mount):
        # type: (any, str, dict[str, AXS], any, str) -> None
        vol_src = None
        vol_dst = None
        self.line_ctr = 0
        for ln in [x.decode("utf-8").strip() for x in fd]:
            self.line_ctr += 1
            if not ln and vol_src is not None:
                vol_src = None
                vol_dst = None

            if not ln or ln.startswith("#"):
                continue

            if vol_src is None:
                if ln.startswith("u "):
                    u, p = ln[2:].split(":", 1)
                    acct[u] = p
                else:
                    vol_src = ln
                continue

            if vol_src and vol_dst is None:
                vol_dst = ln
                if not vol_dst.startswith("/"):
                    raise Exception('invalid mountpoint "{}"'.format(vol_dst))

                # cfg files override arguments and previous files
                vol_src = bos.path.abspath(vol_src)
                vol_dst = vol_dst.strip("/")
                mount[vol_dst] = vol_src
                daxs[vol_dst] = AXS()
                mflags[vol_dst] = {}
                continue

            try:
                lvl, uname = ln.split(" ", 1)
            except:
                lvl = ln
                uname = "*"

            if lvl == "a":
                m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
                self.log(m, 1)

            self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])

    def _read_vol_str(self, lvl, uname, axs, flags):
        # type: (str, str, AXS, any) -> None
        if lvl.strip("crwmdg"):
            raise Exception("invalid volume flag: {},{}".format(lvl, uname))

        if lvl == "c":
            try:
                # volume flag with arguments, possibly with a preceding list of bools
                uname, cval = uname.split("=", 1)
            except:
                # just one or more bools
                cval = True

            while "," in uname:
                # one or more bools before the final flag; eat them
                n1, uname = uname.split(",", 1)
                self._read_volflag(flags, n1, True, False)

            self._read_volflag(flags, uname, cval, False)
            return

        if uname == "":
            uname = "*"

        for un in uname.split(","):
            if "r" in lvl:
                axs.uread[un] = 1

            if "w" in lvl:
                axs.uwrite[un] = 1

            if "m" in lvl:
                axs.umove[un] = 1

            if "d" in lvl:
                axs.udel[un] = 1

            if "g" in lvl:
                axs.uget[un] = 1

    def _read_volflag(self, flags, name, value, is_list):
        if name not in ["mtp"]:
            flags[name] = value
            return

        if not is_list:
            value = [value]
        elif not value:
            return

        flags[name] = flags.get(name, []) + value

    def reload(self):
        """
        construct a flat list of mountpoints and usernames
        first from the commandline arguments
        then supplementing with config files
        before finally building the VFS
        """

        acct = {}  # username:password
        daxs = {}  # type: dict[str, AXS]
        mflags = {}  # mountpoint:[flag]
        mount = {}  # dst:src (mountpoint:realpath)

        if self.args.a:
            # list of username:password
            for x in self.args.a:
                try:
                    u, p = x.split(":", 1)
                    acct[u] = p
                except:
                    m = '\n  invalid value "{}" for argument -a, must be username:password'
                    raise Exception(m.format(x))

        if self.args.v:
            # list of src:dst:permset:permset:...
            # permset is <rwmdg>[,username][,username] or <c>,<flag>[=args]
            for v_str in self.args.v:
                m = re_vol.match(v_str)
                if not m:
                    raise Exception("invalid -v argument: [{}]".format(v_str))

                src, dst, perms = m.groups()
                if WINDOWS:
                    src = uncyg(src)

                # print("\n".join([src, dst, perms]))
                src = bos.path.abspath(src)
                dst = dst.strip("/")
                mount[dst] = src
                daxs[dst] = AXS()
                mflags[dst] = {}

                for x in perms.split(":"):
                    lvl, uname = x.split(",", 1) if "," in x else [x, ""]
                    self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])

        if self.args.c:
            for cfg_fn in self.args.c:
                with open(cfg_fn, "rb") as f:
                    try:
                        self._parse_config_file(f, acct, daxs, mflags, mount)
                    except:
                        m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
                        self.log(m.format(cfg_fn, self.line_ctr), 1)
                        raise

        # case-insensitive; normalize
        if WINDOWS:
            cased = {}
            for k, v in mount.items():
                cased[k] = absreal(v)

            mount = cased

        if not mount:
            # -h says our defaults are CWD at root and read/write for everyone
            axs = AXS(["*"], ["*"], None, None)
            vfs = VFS(self.log_func, bos.path.abspath("."), "", axs, {})
        elif "" not in mount:
            # there's volumes but no root; make root inaccessible
            vfs = VFS(self.log_func, None, "", AXS(), {})
            vfs.flags["d2d"] = True

        maxdepth = 0
        for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
            depth = dst.count("/")
            assert maxdepth <= depth  # nosec
            maxdepth = depth

            if dst == "":
                # rootfs was mapped; fully replaces the default CWD vfs
                vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
                continue

            v = vfs.add(mount[dst], dst)
            v.axs = daxs[dst]
            v.flags = mflags[dst]
            v.dbv = None

        vfs.all_vols = {}
        vfs.get_all_vols(vfs.all_vols)

        for perm in "read write move del get".split():
            axs_key = "u" + perm
            unames = ["*"] + list(acct.keys())
            umap = {x: [] for x in unames}
            for usr in unames:
                for mp, vol in vfs.all_vols.items():
                    axs = getattr(vol.axs, axs_key)
                    if usr in axs or "*" in axs:
                        umap[usr].append(mp)
                umap[usr].sort()
            setattr(vfs, "a" + perm, umap)

        all_users = {}
        missing_users = {}
        for axs in daxs.values():
            for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget]:
                for usr in d.keys():
                    all_users[usr] = 1
                    if usr != "*" and usr not in acct:
                        missing_users[usr] = 1

        if missing_users:
            self.log(
                "you must -a the following users: "
                + ", ".join(k for k in sorted(missing_users)),
                c=1,
            )
            raise Exception("invalid config")

        if LEELOO_DALLAS in all_users:
            raise Exception("sorry, reserved username: " + LEELOO_DALLAS)

        promote = []
        demote = []
        for vol in vfs.all_vols.values():
            hid = hashlib.sha512(fsenc(vol.realpath)).digest()
            hid = base64.b32encode(hid).decode("ascii").lower()
            vflag = vol.flags.get("hist")
            if vflag == "-":
                pass
            elif vflag:
                vol.histpath = uncyg(vflag) if WINDOWS else vflag
            elif self.args.hist:
                for nch in range(len(hid)):
                    hpath = os.path.join(self.args.hist, hid[: nch + 1])
                    bos.makedirs(hpath)

                    powner = os.path.join(hpath, "owner.txt")
                    try:
                        with open(powner, "rb") as f:
                            owner = f.read().rstrip()
                    except:
                        owner = None

                    me = fsenc(vol.realpath).rstrip()
                    if owner not in [None, me]:
                        continue

                    if owner is None:
                        with open(powner, "wb") as f:
                            f.write(me)

                    vol.histpath = hpath
                    break

            vol.histpath = absreal(vol.histpath)
            if vol.dbv:
                if bos.path.exists(os.path.join(vol.histpath, "up2k.db")):
                    promote.append(vol)
                    vol.dbv = None
                else:
                    demote.append(vol)

        # discard jump-vols
        for v in demote:
            vfs.all_vols.pop(v.vpath)

        if promote:
            msg = [
                "\n  the following jump-volumes were generated to assist the vfs.\n  As they contain a database (probably from v0.11.11 or older),\n  they are promoted to full volumes:"
            ]
            for vol in promote:
                msg.append(
                    "  /{}  ({})  ({})".format(vol.vpath, vol.realpath, vol.histpath)
                )

            self.log("\n\n".join(msg) + "\n", c=3)

        vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}

        for vol in vfs.all_vols.values():
            lim = Lim()
            use = False

            if vol.flags.get("nosub"):
                use = True
                lim.nosub = True

            v = vol.flags.get("sz")
            if v:
                use = True
                lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]

            v = vol.flags.get("rotn")
            if v:
                use = True
                lim.rotn, lim.rotl = [int(x) for x in v.split(",")]

            v = vol.flags.get("rotf")
            if v:
                use = True
                lim.set_rotf(v)

            v = vol.flags.get("maxn")
            if v:
                use = True
                lim.nmax, lim.nwin = [int(x) for x in v.split(",")]

            v = vol.flags.get("maxb")
            if v:
                use = True
                lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]

            if use:
                vol.lim = lim

        for vol in vfs.all_vols.values():
            fk = vol.flags.get("fk")
            if fk:
                vol.flags["fk"] = int(fk) if fk is not True else 8

        for vol in vfs.all_vols.values():
            if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
                vol.flags["gz"] = False  # def.pk

            if "scan" in vol.flags:
                vol.flags["scan"] = int(vol.flags["scan"])
            elif self.args.re_maxage:
                vol.flags["scan"] = self.args.re_maxage

        all_mte = {}
        errors = False
        for vol in vfs.all_vols.values():
            if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
                vol.flags["e2ds"] = True

            if self.args.e2d or "e2ds" in vol.flags:
                vol.flags["e2d"] = True

            for ga, vf in [["no_hash", "nohash"], ["no_idx", "noidx"]]:
                if vf in vol.flags:
                    ptn = vol.flags.pop(vf)
                else:
                    ptn = getattr(self.args, ga)

                if ptn:
                    vol.flags[vf] = re.compile(ptn)

            for k in ["e2t", "e2ts", "e2tsr"]:
                if getattr(self.args, k):
                    vol.flags[k] = True

            for k1, k2 in IMPLICATIONS:
                if k1 in vol.flags:
                    vol.flags[k2] = True

            # default tag cfgs if unset
            if "mte" not in vol.flags:
                vol.flags["mte"] = self.args.mte
            elif vol.flags["mte"].startswith("+"):
                vol.flags["mte"] = ",".join(
                    x for x in [self.args.mte, vol.flags["mte"][1:]] if x
                )
            if "mth" not in vol.flags:
                vol.flags["mth"] = self.args.mth

            # append parsers from argv to volume-flags
            self._read_volflag(vol.flags, "mtp", self.args.mtp, True)

            # d2d drops all database features for a volume
            for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"]]:
                if not vol.flags.get(grp, False):
                    continue

                vol.flags["d2t"] = True
                vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}

            # mt* needs e2t so drop those too
            for grp, rm in [["e2t", "mt"]]:
                if vol.flags.get(grp, False):
                    continue

                vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}

            # verify tags mentioned by -mt[mp] are used by -mte
            local_mtp = {}
            local_only_mtp = {}
            tags = vol.flags.get("mtp", []) + vol.flags.get("mtm", [])
            tags = [x.split("=")[0] for x in tags]
            tags = [y for x in tags for y in x.split(",")]
            for a in tags:
                local_mtp[a] = True
                local = True
                for b in self.args.mtp or []:
                    b = b.split("=")[0]
                    if a == b:
                        local = False

                if local:
                    local_only_mtp[a] = True

            local_mte = {}
            for a in vol.flags.get("mte", "").split(","):
                local = True
                all_mte[a] = True
                local_mte[a] = True
                for b in self.args.mte.split(","):
                    if not a or not b:
                        continue

                    if a == b:
                        local = False

            for mtp in local_only_mtp.keys():
                if mtp not in local_mte:
                    m = 'volume "/{}" defines metadata tag "{}", but doesnt use it in "-mte" (or with "cmte" in its volume-flags)'
                    self.log(m.format(vol.vpath, mtp), 1)
                    errors = True

        tags = self.args.mtp or []
        tags = [x.split("=")[0] for x in tags]
        tags = [y for x in tags for y in x.split(",")]
        for mtp in tags:
            if mtp not in all_mte:
                m = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volume-flag)'
                self.log(m.format(mtp), 1)
                errors = True

        if errors:
            sys.exit(1)

        vfs.bubble_flags()

        m = "volumes and permissions:\n"
        for v in vfs.all_vols.values():
            if not self.warn_anonwrite:
                break

            m += '\n\033[36m"/{}"  \033[33m{}\033[0m'.format(v.vpath, v.realpath)
            for txt, attr in [
                ["  read", "uread"],
                [" write", "uwrite"],
                ["  move", "umove"],
                ["delete", "udel"],
                ["   get", "uget"],
            ]:
                u = list(sorted(getattr(v.axs, attr).keys()))
                u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
                u = u if u else "\033[36m--none--\033[0m"
                m += "\n|  {}:  {}".format(txt, u)
            m += "\n"

        if self.warn_anonwrite and not self.args.no_voldump:
            self.log(m)

        try:
            v, _ = vfs.get("/", "*", False, True)
            if self.warn_anonwrite and os.getcwd() == v.realpath:
                self.warn_anonwrite = False
                msg = "anyone can read/write the current directory: {}"
                self.log(msg.format(v.realpath), c=1)
        except Pebkac:
            self.warn_anonwrite = True

        with self.mutex:
            self.vfs = vfs
            self.acct = acct
            self.iacct = {v: k for k, v in acct.items()}

            self.re_pwd = None
            pwds = [re.escape(x) for x in self.iacct.keys()]
            if pwds:
                self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")

    def dbg_ls(self):
        users = self.args.ls
        vols = "*"
        flags = []

        try:
            users, vols = users.split(",", 1)
        except:
            pass

        try:
            vols, flags = vols.split(",", 1)
            flags = flags.split(",")
        except:
            pass

        if users == "**":
            users = list(self.acct.keys()) + ["*"]
        else:
            users = [users]

        for u in users:
            if u not in self.acct and u != "*":
                raise Exception("user not found: " + u)

        if vols == "*":
            vols = ["/" + x for x in self.vfs.all_vols.keys()]
        else:
            vols = [vols]

        for v in vols:
            if not v.startswith("/"):
                raise Exception("volumes must start with /")

            if v[1:] not in self.vfs.all_vols:
                raise Exception("volume not found: " + v)

        self.log({"users": users, "vols": vols, "flags": flags})
        m = "/{}: read({}) write({}) move({}) del({}) get({})"
        for k, v in self.vfs.all_vols.items():
            vc = v.axs
            self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget))

        flag_v = "v" in flags
        flag_ln = "ln" in flags
        flag_p = "p" in flags
        flag_r = "r" in flags

        n_bads = 0
        for v in vols:
            v = v[1:]
            vtop = "/{}/".format(v) if v else "/"
            for u in users:
                self.log("checking /{} as {}".format(v, u))
                try:
                    vn, _ = self.vfs.get(v, u, True, False, False, False, False)
                except:
                    continue

                atop = vn.realpath
                g = vn.walk(
                    vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False
                )
                for _, _, vpath, apath, files, _, _ in g:
                    fnames = [n[0] for n in files]
                    vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
                    vpaths = [vtop + x for x in vpaths]
                    apaths = [os.path.join(apath, n) for n in fnames]
                    files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths))

                    if flag_ln:
                        files = [x for x in files if not x[1].startswith(atop + os.sep)]
                        n_bads += len(files)

                    if flag_v:
                        msg = [
                            '# user "{}", vpath "{}"\n{}'.format(u, vp, ap)
                            for vp, ap in files
                        ]
                    else:
                        msg = [x[1] for x in files]

                    if msg:
                        self.log("\n" + "\n".join(msg))

                if n_bads and flag_p:
                    raise Exception("found symlink leaving volume, and strict is set")

        if not flag_r:
            sys.exit(0)
