#!/usr/bin/env -S  python3 -u

from __future__ import with_statement

import os
import sys
import errno

from datetime import datetime, timedelta, date
import time

import fileinput
import argparse
import gzip
import shutil
import json
import traceback

import random
import string

from pathlib import Path

try:
    from fuse import FUSE, FuseOSError, Operations
except:
    try:
        from fusepy import FUSE, FuseOSError, Operations
    except ModuleNotFoundError as e:
        print("please install fusepy", e)
        raise


class WorkdirFS(Operations):
    def __init__(self, args):
        self.args = args

        # init configdir and configlast
        self.confdir = os.path.join(os.environ['HOME'], '.local', 'workdirfs')
        self.configlast = os.path.join(self.confdir, 'yesterday')

        self._checkdir(self.confdir)

        # init archivbase and xdg_archive
        if self.args.archive.startswith("/"):
            self.archivpathbase = self.args.archive
            self.xdgarchivpathbase = self.args.archive
        else:
            self.archivpathbase = os.path.join(os.environ['HOME'], self.args.archive)
            self.xdgarchivpathbase = os.path.join('$HOME', self.args.archive)

        self._xdg()
        self._give_me_today()
        self.todaypath = self._checkdir(self._give_me_archivpath())
        self.yesterdaypath = self._checkdir(self._give_me_archivpath(self._give_me_yesterday()))

        print("initial yesterdaypath is {}".format(self.yesterdaypath))
        print("initial todaypath is {}".format(self.todaypath))

    # Helpers
    # =======

    def _xdg(self):
        foundarchive=False
        foundwork=False
        xdgdir = os.path.join(os.environ['HOME'], '.config')
        xdguserdirs = os.path.join(xdgdir, 'user-dirs.dirs')
        if not os.path.exists(xdgdir):
            os.mkdir(xdgdir)
        try:
            with fileinput.input(xdguserdirs, inplace=True) as fh:
                for line in fh:
                    if line.startswith('XDG_ARCHIVE_DIR'):
                        print('XDG_ARCHIVE_DIR="' + self.xdgarchivpathbase + '"', end='\n')
                        foundarchive=True
                    elif line.startswith('XDG_WORK_DIR'):
                        print('XDG_WORK_DIR="$HOME/' + self.args.mountpoint + '"', end='\n')
                        foundwork=True
                    else:
                         print(line, end='')
        except:
            print("File not existing, create it: {}".format(xdguserdirs))

        if not foundarchive:
            with open(xdguserdirs, 'a') as fh:
                fh.write("XDG_ARCHIVE_DIR=" + self.xdgarchivpathbase + '\n')
        if not foundwork:
            with open(xdguserdirs, 'a') as fh:
                fh.write('XDG_WORK_DIR=\"$HOME/' + self.args.mountpoint + '"\n')

    def _give_me_today(self):
        self.today = datetime.now() - timedelta(hours=self.args.timeoffset)
        return self.today

    def _give_me_yesterday(self):
        if os.path.exists(self.configlast):
            with open(self.configlast, 'r') as fh:
                yesterday = fh.readline().strip()
            if len(yesterday) == 0:
                self.yesterday = self.today
            else:
                self.yesterday = datetime.strptime(yesterday, "%Y-%m-%d")
        else:
            self.yesterday = self.today
            self._checkdir(self.confdir)
            if not os.path.isdir(self.confdir):
                os.mkdir(self.confdir)
        with open(self.configlast, 'w') as fh:
            fh.write(self.yesterday.date().strftime("%Y-%m-%d"))
        return self.yesterday

    def _give_me_archivpath(self, _date=None):
        archivpathbase = self.archivpathbase
        if _date == None:
            _date = self.today
        if self.args.yearlydir:
            archivpathbase = os.path.join(archivpathbase, _date.strftime("%Y"))
        if self.args.monthlydir:
            archivpathbase = os.path.join(archivpathbase, _date.strftime("%m"))
        if self.args.weeklydir:
            archivpathbase = os.path.join(archivpathbase, int(_date.strftime("%W"))+1)
        self.archivpath = os.path.join(archivpathbase, _date.strftime("%Y-%m-%d"))
        return self.archivpath

    def _checkdir(self, path):
        if not os.path.exists(path) and not os.path.isdir(path):
            try:
                os.makedirs(path, exist_ok=True)
                print("Created directory {}".format(path), flush=True)
            except Exception as e:
                print("[-] Error while check dir and zip files: ", e)
        return path


    def _full_path(self, partial):

        #self._give_me_today()
        path = self._give_me_archivpath()
        self._give_me_yesterday()

        if partial.startswith("/"):
            partial = partial[1:]


        path = os.path.join(
                self._checkdir( self._give_me_archivpath(self._give_me_today())),
                partial
            )

        if self.today.date() > self.yesterday.date():
            self._cleanup_dirs()
            self.yesterday = self.today
            self.yesterdaypath = self.todaypath
            with open(self.configlast, 'w') as fh:
                fh.write(self.today.strftime("%Y-%m-%d"))

        return path

    def _check_fileext(self, filename, zipext):
        if type(zipext) is str:
            return zipext in filename
        elif type(zipext) is list:
            for e in zipext:
                if (lambda x: x in filename)(e):
                    return True
        return False

    def _cleanup_dirs(self):

        print("Cleanup dir", self.yesterdaypath)
        # zip_fileext is used for output
        zip_fileext=".gz"
        # zip_fileexts is a single string or a list of strings, which files
        # should not be ceompressed
        zip_fileexts=[".gz","tgz",".gz.tar"]
        zip_compressionlevel=5
        #for root, dirs, files in os.walk(self.yesterdaypath, topdown=False):
        print("Archivepath",self.archivpathbase)
        for root, dirs, files in os.walk(self.archivpathbase, topdown=False):
            for d in dirs:
                print("cleanup",os.path.join(root, d))
                if not d == self.todaypath and not os.listdir(os.path.join(root, d)):
                    print("Directory is empty -> remove it",
                          os.path.join(root, d))
                    os.rmdir(os.path.join(root, d))
            if self.args.compress:
                for f in files:
                    print("compress", os.path.join(root, f))
                    #if zip_fileext not in f:
                    if not self._check_fileext(f,zip_fileexts):
                        try:
                            with open(os.path.join(root, f), 'rb') as f_in:
                                with gzip.open(
                                        os.path.join(root, f+zip_fileext),
                                        'wb',
                                        compresslevel=zip_compressionlevel) as f_out:
                                    shutil.copyfileobj(f_in, f_out)

                        except Exception as e:
                            print("Error during zipping file {}".format(os.path.join(root, f)), e)
                            traceback.print_exc()
                        else:
                            os.remove(os.path.join(root, f))


    # Filesystem methods
    # ==================

    def access(self, path, mode):
        full_path = self._full_path(path)
        if not os.access(full_path, mode):
            raise FuseOSError(errno.EACCES)

    def chmod(self, path, mode):
        full_path = self._full_path(path)
        return os.chmod(full_path, mode)

    def chown(self, path, uid, gid):
        full_path = self._full_path(path)
        return os.chown(full_path, uid, gid)

    def getattr(self, path, fh=None):
        full_path = self._full_path(path)
        st = os.lstat(full_path)
        return dict((key, getattr(st, key)) for key in ('st_atime',
                                                        'st_ctime',
                                                        'st_gid',
                                                        'st_mode',
                                                        'st_mtime',
                                                        'st_nlink',
                                                        'st_size',
                                                        'st_uid'))

    def readdir(self, path, fh):
        full_path = self._full_path(path)

        dirents = ['.', '..']
        if os.path.isdir(full_path):
            dirents.extend(os.listdir(full_path))
        for r in dirents:
            yield r

    def readlink(self, path):
        pathname = os.readlink(self._full_path(path))
        if pathname.startswith("/"):
            # Path name is absolute, sanitize it.
            return os.path.relpath(pathname, os.path.join(os.environ['HOME'],
                self.args.archive))
        else:
            return pathname

    def mknod(self, path, mode, dev):
        return os.mknod(self._full_path(path), mode, dev)

    def rmdir(self, path):
        full_path = self._full_path(path)
        return os.rmdir(full_path)

    def mkdir(self, path, mode):
        return os.mkdir(self._full_path(path), mode)

    def statfs(self, path):
        full_path = self._full_path(path)
        stv = os.statvfs(full_path)
        return dict((key, getattr(stv, key)) for key in ('f_bavail',
                                                         'f_bfree',
                                                         'f_blocks',
                                                         'f_bsize',
                                                         'f_favail',
                                                         'f_ffree',
                                                         'f_files',
                                                         'f_flag',
                                                         'f_frsize',
                                                         'f_namemax'))

    def unlink(self, path):
        return os.unlink(self._full_path(path))

    def symlink(self, name, target):
        return os.symlink(target, self._full_path(name))

    def rename(self, old, new):
        return os.rename(self._full_path(old), self._full_path(new))

    def link(self, target, name):
        return os.link(self._full_path(name), self._full_path(target))

    def utimens(self, path, times=None):
        return os.utime(self._full_path(path), times)

    # File methods
    # ============

    def open(self, path, flags):
        full_path = self._full_path(path)
        return os.open(full_path, flags)

    def create(self, path, mode, fi=None):
        full_path = self._full_path(path)
        return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode)

    def read(self, path, length, offset, fh):
        os.lseek(fh, offset, os.SEEK_SET)
        return os.read(fh, length)

    def write(self, path, buf, offset, fh):
        os.lseek(fh, offset, os.SEEK_SET)
        return os.write(fh, buf)

    def truncate(self, path, length, fh=None):
        full_path = self._full_path(path)
        with open(full_path, 'r+') as f:
            f.truncate(length)

    def flush(self, path, fh):
        return os.fsync(fh)

    def release(self, path, fh):
        return os.close(fh)

    def fsync(self, path, fdatasync, fh):
        return self.flush(path, fh)

def main(args):
    #FUSE(WorkdirFS(root), mountpoint, nothreads=True, foreground=True)
    # start FUSE filesystem
    mountpoint = Path(os.path.join(os.environ['HOME'], args.mountpoint))
    if mountpoint.is_symlink():
        mountpoint.unlink();
    if mountpoint.exists() and not mountpoint.is_mount():
        if mountpoint.is_dir():
            if os.listdir(mountpoint):
                mountpoint.rename(str(mountpoint) + "-" + datetime.now().strftime("%Y-%m-%d") 
                        + "-"
                        + ''.join(random.choice(
                                string.ascii_uppercase + string.digits) for _ in range(6))
                        + ".bak")
        else:
            mountpoint.rename(str(mountpoint) + "-" + datetime.now().strftime("%Y-%m-%d") 
                    + "-"
                    + ''.join(random.choice(
                            string.ascii_uppercase + string.digits) for _ in range(6))
                    + ".bak")

    mountpoint.mkdir(mode=0o700, parents=True, exist_ok=True)

#    if not (os.path.isdir(mountpoint) or os.path.exists(mountpoint)):
#        if  Path(mountpoint).is_symlink():
#            os.path.remove
#        os.mkdir(mountpoint, 0o744)
    FUSE(WorkdirFS(args), os.path.join(os.environ['HOME'], args.mountpoint),
            nothreads=True, foreground=True, allow_root=True)

if __name__ == '__main__':
    #main(sys.argv[2], sys.argv[1])
    parser = argparse.ArgumentParser()
    parser.add_argument("-a", "--archive",
            default='archive/workdir', help="""Path to archivedir-base. When path
            starts with "/", it's an absolute path, else it is handled as path
            relative to users home.
            Defaults to »archive/workdir« """)
    parser.add_argument("-m", "--mountpoint",
            default='Work', help="""Path to Workdir. This path is always
            relative to users homedir. "/" at the begin get removed.
            Defaults to »Work«""")
    parser.add_argument("-t", "--timeoffset", type=int, default=4, help="""If you're working
            all day till 3 o'clock in the morning, set it to 4, so next day
            archive-dir will be created 4 hours after midnight. You have 1h
            tolerance, if you're working one day a little bit longer.
            Defaults to »4«""")
    parser.add_argument("-Y", "--yearlydir", action="store_true",
            help="""Create a yearly directory - named YYYY - under »archive«.
    Defaults to »False«""")
    parser.add_argument("-M", "--monthlydir", action="store_true",
            help="""Create a monthly directory - named MM - under »archive«.
    Defaults to »False«""")
    parser.add_argument("-W", "--weeklydir", action="store_true",
            help="""Create a weekly directory - named WW - under »archive«.
    Defaults to »False«""")
    parser.add_argument("-C", "--compress", action="store_false",
            help="""Compress each file in archive
            Defaults to »True«""")
    args = parser.parse_args()
    print(args)
    #root = os.environ['HOME']+'/archive'
    #mountpoint = os.environ['HOME']+'/Work'
    main(args)
