# coding: utf-8
import os
import json
import pytz
import copy
import logging
import logging.handlers
import datetime
from urllib import request
from datetime import timedelta
from os.path import splitext

from django.core.cache import cache
from sqlalchemy import select, text, desc, func
import pymysql

from seaserv import get_org_id_by_repo_id, seafile_api, get_commit
from seafobj import CommitDiffer, commit_mgr, fs_mgr
from seafobj.commit_differ import DiffEntry
from seafevents.events.db import save_file_audit_event, save_file_update_event, \
        save_perm_audit_event, save_user_activity, save_filehistory, update_user_activity_timestamp, \
        save_repo_trash, restore_repo_trash
from seafevents.app.config import TIME_ZONE
from seafevents.utils import get_opt_from_conf_or_env
from .change_file_path import ChangeFilePathHandler
from .models import Activity, FileTrash
from seafevents.batch_delete_files_notice.utils import get_deleted_files_count, save_deleted_files_msg
from seafevents.batch_delete_files_notice.db import get_deleted_files_total_count, save_deleted_files_count

recent_added_events = {'recent_added_events': []}
EXCLUDED_PATHS = ['/_Internal', '/images/sdoc', '/images/auto-upload']

def _check_ignored_path(path):
    for p in EXCLUDED_PATHS:
        if path.startswith(p):
            return True
    return False


def RepoUpdateEventHandler(config, session, msg):
    try:
        elements = json.loads(msg['content'])
    except:
        logging.warning("got bad message: %s", msg)
        return

    repo_id = elements.get('repo_id')
    commit_id = elements.get('commit_id')

    if not repo_id or not commit_id:
        logging.warning("repo_id: %s, or commit_id: %s invalid.", repo_id, commit_id)
        return

    commit = commit_mgr.load_commit(repo_id, 1, commit_id)
    if commit is None:
        commit = commit_mgr.load_commit(repo_id, 0, commit_id)

    # TODO: maybe handle merge commit.
    if commit is not None and commit.parent_id and not commit.second_parent_id:

        parent = commit_mgr.load_commit(repo_id, commit.version, commit.parent_id)

        if parent is not None:
            description = commit.description
            if description.startswith('Deleted') and 'more' in description:
                differ = CommitDiffer(repo_id, commit.version, parent.root_id, commit.root_id, False, False)
            else:
                differ = CommitDiffer(repo_id, commit.version, parent.root_id, commit.root_id, True, True)
            added_files, deleted_files, added_dirs, deleted_dirs, modified_files, \
            renamed_files, moved_files, renamed_dirs, moved_dirs = differ.diff()

            if renamed_files or renamed_dirs or moved_files or moved_dirs:
                changer = ChangeFilePathHandler(session)
                for r_file in renamed_files:
                    changer.update_db_records(repo_id, r_file.path, r_file.new_path, 0)
                for r_dir in renamed_dirs:
                    changer.update_db_records(repo_id, r_dir.path, r_dir.new_path, 1)
                for m_file in moved_files:
                    changer.update_db_records(repo_id, m_file.path, m_file.new_path, 0)
                for m_dir in moved_dirs:
                    changer.update_db_records(repo_id, m_dir.path, m_dir.new_path, 1)

            users = []
            org_id = get_org_id_by_repo_id(repo_id)
            if org_id > 0:
                users = seafile_api.org_get_shared_users_by_repo(org_id, repo_id)
                owner = seafile_api.get_org_repo_owner(repo_id)
            else:
                users = seafile_api.get_shared_users_by_repo(repo_id)
                owner = seafile_api.get_repo_owner(repo_id)

            if owner not in users:
                users = users + [owner]
            if not users:
                return

            time = datetime.datetime.utcfromtimestamp(msg['ctime'])
            if added_files or deleted_files or added_dirs or deleted_dirs or \
                    modified_files or renamed_files or moved_files or renamed_dirs or moved_dirs:

                fh_enabled = True
                if config.has_option('FILE HISTORY', 'enabled'):
                    fh_enabled = config.getboolean('FILE HISTORY', 'enabled')
                if fh_enabled:
                    records = generate_filehistory_records(added_files, deleted_files,
                                    added_dirs, deleted_dirs, modified_files, renamed_files,
                                    moved_files, renamed_dirs, moved_dirs, commit, repo_id,
                                    parent, time)
                    save_file_histories(config, session, records)

                records, trash_records = generate_activity_records(added_files, deleted_files,
                        added_dirs, deleted_dirs, modified_files, renamed_files,
                        moved_files, renamed_dirs, moved_dirs, commit, repo_id,
                        parent, users, time)

                save_user_activities(session, records)
                save_repo_trashs(session, trash_records)
                # save repo monitor recodes
                records = generate_repo_monitor_records(repo_id, commit,
                                                        added_files, deleted_files,
                                                        added_dirs, deleted_dirs,
                                                        renamed_files, renamed_dirs,
                                                        moved_files, moved_dirs,
                                                        modified_files)
                save_message_to_user_notification(session, records)


            enable_collab_server = False
            if config.has_option('COLLAB_SERVER', 'enabled'):
                enable_collab_server = config.getboolean('COLLAB_SERVER', 'enabled')
            if enable_collab_server:
                send_message_to_collab_server(config, repo_id)

            # deleted files notices
            once_threshold = 0
            total_threshold = 0
            if config.has_option('DELETE FILES NOTICE', 'once_threshold'):
                once_threshold = config.getint('DELETE FILES NOTICE', 'once_threshold')
            if config.has_option('DELETE FILES NOTICE', 'total_threshold'):
                total_threshold = config.getint('DELETE FILES NOTICE', 'total_threshold')

            # cross repo move event will generate added and deleted events,
            # so record the latest 10 added events in memory to filter cross repo move events
            if (added_files or added_dirs) and not commit.description.startswith('Reverted') \
                    and (once_threshold > 0 and total_threshold > 0):
                added_obj_set = set()
                for added_file in added_files:
                    added_obj_set.add(added_file.obj_id)
                for added_dir in added_dirs:
                    added_obj_set.add(added_dir.obj_id)

                if len(recent_added_events['recent_added_events']) < 10:
                    recent_added_events['recent_added_events'].append(added_obj_set)
                else:
                    recent_added_events['recent_added_events'].pop(0)
                    recent_added_events['recent_added_events'].append(added_obj_set)

            if (deleted_files or deleted_dirs) and (once_threshold > 0 and total_threshold > 0):
                deleted_obj_set = set()
                for deleted_file in deleted_files:
                    deleted_obj_set.add(deleted_file.obj_id)
                for deleted_dir in deleted_dirs:
                    deleted_obj_set.add(deleted_dir.obj_id)

                if deleted_obj_set in recent_added_events['recent_added_events']:
                    try:
                        recent_added_events['recent_added_events'].remove(deleted_obj_set)
                    except ValueError:
                        pass
                else:
                    timestamp = datetime.datetime.fromtimestamp(msg['ctime'])
                    deleted_time = datetime.datetime.fromtimestamp(msg['ctime']).strftime('%Y-%m-%d 00:00:00')
                    files_count = get_deleted_files_count(repo_id, commit.version, deleted_files, deleted_dirs)
                    if files_count > 0:
                        save_deleted_files_count(session, repo_id, files_count, deleted_time)

                    if files_count > once_threshold:
                        save_deleted_files_msg(session, owner, repo_id, timestamp)

                    total_count = get_deleted_files_total_count(session, repo_id, deleted_time)
                    if total_count > total_threshold:
                        save_deleted_files_msg(session, owner, repo_id, timestamp)


def send_message_to_collab_server(config, repo_id):
    collab_server = config.get('COLLAB_SERVER', 'server_url')
    collab_key = config.get('COLLAB_SERVER', 'key')
    url = '%s/api/repo-update' % collab_server
    form_data = 'repo_id=%s&key=%s' % (repo_id, collab_key)
    req = request.Request(url, form_data.encode('utf-8'))
    resp = request.urlopen(req)
    ret_code = resp.getcode()
    if ret_code != 200:
        logging.warning('Failed to send message to collab_server %s', collab_server)


def generate_repo_monitor_records(repo_id, commit,
                                  added_files, deleted_files,
                                  added_dirs, deleted_dirs,
                                  renamed_files, renamed_dirs,
                                  moved_files, moved_dirs,
                                  modified_files):

    OP_CREATE = 'create'
    OP_RECOVER = 'recover'
    OP_DELETE = 'delete'
    OP_RENAME = 'rename'
    OP_MOVE = 'move'
    OP_EDIT = 'edit'

    OBJ_FILE = 'file'
    OBJ_DIR = 'dir'

    # {'_dict': {'client_version': '8.0.8',
    #            'commit_id': '436a73b6253de071f29947ad15122e38bfd991eb',
    #            'creator': '3d486d5be28704428d613e86be533ad66286a839',
    #            'creator_name': 'lian@lian.com',
    #            'ctime': 1666689533,
    #            'description': 'Added or modified "1" and 2 more files.\n'
    #                           'Deleted "users.xlsx".\n',
    #            'device_name': 'lian mac work',
    #            'no_local_history': 1,
    #            'parent_id': 'e7dc5161cb00b2b5784fd58e303d5944fbd5d647',
    #            'repo_category': None,
    #            'repo_desc': '',
    #            'repo_id': '31191817-eda3-49f6-b2d9-d7bc534f6c5a',
    #            'repo_name': 'lian lib for test repo monitor',
    #            'root_id': '01e9bea5f8b525bed256afc46d24821d7ce9b8be',
    #            'second_parent_id': None,
    #            'version': 1}}

    repo = seafile_api.get_repo(repo_id)
    if repo.repo_type == 'wiki':
        return []
    base_record = {
        'op_user': getattr(commit, 'creator_name', ''),
        'repo_id': repo_id,
        'repo_name': repo.repo_name,
        'commit_id': commit.commit_id,
        'timestamp': commit.ctime,
        'commit_desc': commit.description,
    }

    records = []

    if added_files:

        added_files_record = copy.copy(base_record)
        added_files_record["commit_diff"] = []

        # {'new_path': None,
        #  'obj_id': '0000000000000000000000000000000000000000',
        #  'path': '/3',
        #  'size': 0}
        for de in added_files:
            if _check_ignored_path(de.path):
                continue
            if commit.description.startswith('Reverted'):
                op_type = OP_RECOVER
            else:
                op_type = OP_CREATE

            commit_diff = dict()
            commit_diff['op_type'] = op_type
            commit_diff['obj_type'] = OBJ_FILE
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.path
            commit_diff['size'] = de.size

            added_files_record["commit_diff"].append(commit_diff)

        records.append(added_files_record)

    if deleted_files:
        deleted_files_record = copy.copy(base_record)
        deleted_files_record["commit_diff"] = []

        for de in deleted_files:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_DELETE
            commit_diff['obj_type'] = OBJ_FILE
            commit_diff['obj_id'] = de.obj_id
            commit_diff['size'] = de.size
            commit_diff['path'] = de.path

            deleted_files_record["commit_diff"].append(commit_diff)

        records.append(deleted_files_record)

    if added_dirs:

        added_dirs_record = copy.copy(base_record)
        added_dirs_record["commit_diff"] = []

        for de in added_dirs:
            if _check_ignored_path(de.path):
                continue
            if commit.description.startswith('Recovered'):
                op_type = OP_RECOVER
            else:
                op_type = OP_CREATE

            commit_diff = dict()
            commit_diff['op_type'] = op_type
            commit_diff['obj_type'] = OBJ_DIR
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.path

            added_dirs_record["commit_diff"].append(commit_diff)

        records.append(added_dirs_record)

    if deleted_dirs:

        deleted_dirs_record = copy.copy(base_record)
        deleted_dirs_record["commit_diff"] = []

        for de in deleted_dirs:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_DELETE
            commit_diff['obj_type'] = OBJ_DIR
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.path

            deleted_dirs_record["commit_diff"].append(commit_diff)

        records.append(deleted_dirs_record)

    if renamed_files:

        renamed_files_record = copy.copy(base_record)
        renamed_files_record["commit_diff"] = []

        for de in renamed_files:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_RENAME
            commit_diff['obj_type'] = OBJ_FILE
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.new_path
            commit_diff['size'] = de.size
            commit_diff['old_path'] = de.path

            renamed_files_record["commit_diff"].append(commit_diff)

        records.append(renamed_files_record)

    if renamed_dirs:

        renamed_dirs_record = copy.copy(base_record)
        renamed_dirs_record["commit_diff"] = []

        for de in renamed_dirs:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_RENAME
            commit_diff['obj_type'] = OBJ_DIR
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.new_path
            commit_diff['size'] = de.size
            commit_diff['old_path'] = de.path

            renamed_dirs_record["commit_diff"].append(commit_diff)

        records.append(renamed_dirs_record)

    if moved_files:

        moved_files_record = copy.copy(base_record)
        moved_files_record["commit_diff"] = []

        for de in moved_files:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_MOVE
            commit_diff['obj_type'] = OBJ_FILE
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.new_path
            commit_diff['size'] = de.size
            commit_diff['old_path'] = de.path

            moved_files_record["commit_diff"].append(commit_diff)

        records.append(moved_files_record)

    if moved_dirs:

        moved_dirs_record = copy.copy(base_record)
        moved_dirs_record["commit_diff"] = []

        for de in moved_dirs:
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = OP_MOVE
            commit_diff['obj_type'] = OBJ_DIR
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.new_path
            commit_diff['size'] = de.size
            commit_diff['old_path'] = de.path

            moved_dirs_record["commit_diff"].append(commit_diff)

        records.append(moved_dirs_record)

    if modified_files:

        modified_files_record = copy.copy(base_record)
        modified_files_record["commit_diff"] = []

        for de in modified_files:
            if commit.description.startswith('Reverted'):
                op_type = OP_RECOVER
            else:
                op_type = OP_EDIT
            if _check_ignored_path(de.path):
                continue
            commit_diff = dict()
            commit_diff['op_type'] = op_type
            commit_diff['obj_type'] = OBJ_FILE
            commit_diff['obj_id'] = de.obj_id
            commit_diff['path'] = de.path
            commit_diff['size'] = de.size

            modified_files_record["commit_diff"].append(commit_diff)

        records.append(modified_files_record)

    filtered_records = []
    for record in records:
        if len(record['commit_diff']) == 0:
            continue
        filtered_records.append(record)
    return filtered_records


def save_message_to_user_notification(session, records):

    if not records:
        return

    repo_id_monitor_users = {}

    for record in records:

        repo_id = record.get('repo_id')
        if not repo_id:
            continue

        monitor_users = cache.get('{}_monitor_users'.format(repo_id))
        if not monitor_users:
            sql = "SELECT email FROM base_usermonitoredrepos where repo_id='{}'".format(repo_id)
            result = session.execute(text(sql))
            monitor_users = result.fetchall()
            monitor_users = [item[0] for item in monitor_users]

        cache_monitor_users = []
        for monitor_user in monitor_users:

            permission = seafile_api.check_permission_by_path(repo_id, '/', monitor_user)
            if permission not in ('r', 'rw'):
                del_sql = "DELETE FROM base_usermonitoredrepos where email='{}' and repo_id='{}'".format(monitor_user, repo_id)
                session.execute(text(del_sql))
                session.commit()
                continue

            cache_monitor_users.append(monitor_user)

        cache.set('{}_monitor_users'.format(repo_id),
                  cache_monitor_users,
                  24 * 60 * 60)

        repo_id_monitor_users[repo_id] = cache_monitor_users

    # process repo update record
    records_should_save = []
    for record in records:

        repo_id = record.get('repo_id')
        if not repo_id or repo_id not in repo_id_monitor_users:
            continue

        op_user = record.get('op_user')
        if not op_user:
            continue

        for monitor_user in repo_id_monitor_users[repo_id]:

            if op_user == monitor_user:
                continue

            info = {
                'record': record,
                'monitor_user': monitor_user
            }

            records_should_save.append(info)

        # {'monitor_user': 'lian@lian.com',
        #  'record': {'commit_desc': 'Deleted "users.xlsx" and 1 more files',
        #             'commit_diff': [{'obj_id': '4e1385391118ad01302a68f5ee1885f76382aa2f',
        #                              'obj_type': 'file',
        #                              'op_type': 'delete',
        #                              'path': '/users.xlsx',
        #                              'size': 8939},
        #                             {'obj_id': '82135d1f2687e56e2a9a7be530baf61f4abb0b5f',
        #                              'obj_type': 'file',
        #                              'op_type': 'delete',
        #                              'path': '/123456.md',
        #                              'size': 13}],
        #             'commit_id': 'b013adb9cf06631e835d42d10f7ccf49a69caad8',
        #             'op_user': 'foo@foo.com',
        #             'repo_id': '31191817-eda3-49f6-b2d9-d7bc534f6c5a',
        #             'repo_name': 'lian lib for test repo monitor',
        #             'timestamp': 1666774731}}

    # save to user notification
    step = 100
    for i in range(0, len(records_should_save), step):

        values = list()
        for item in records_should_save[i: i+step]:
            monitor_user = item['monitor_user']
            record = item['record']

            detail = {
                "commit_id": record["commit_id"],
                "repo_id": record["repo_id"],
                "repo_name": record["repo_name"],
                "op_user": record["op_user"],
                "op_type": record["commit_diff"][0]["op_type"],
                "obj_type": record["commit_diff"][0]["obj_type"],
                "obj_path_list": [item["path"] for item in record["commit_diff"]],
                "old_obj_path_list": [],
            }

            if record["commit_diff"][0].get("old_path"):
                detail["old_obj_path_list"] = [item["old_path"] for item in record["commit_diff"]]

            detail = json.dumps(detail)
            detail = pymysql.converters.escape_string(detail)

            time_zone = pytz.timezone(TIME_ZONE)

            utc_datetime = datetime.datetime.utcfromtimestamp(record['timestamp'])
            utc_datetime = utc_datetime.replace(microsecond=0)
            utc_datetime = pytz.utc.localize(utc_datetime)
            local_datetime = utc_datetime.astimezone(time_zone)
            local_datetime_str = local_datetime.strftime("%Y-%m-%d %H:%M:%S")

            values.append((monitor_user, 'repo_monitor', detail, local_datetime_str, 0))

        sql = """INSERT INTO notifications_usernotification (to_user, msg_type, detail, timestamp, seen)
                 VALUES %s""" % ', '.join(["('%s', '%s', '%s', '%s', %s)" % value for value in values])
        session.execute(text(sql))
        session.commit()


def save_user_activities(session, records):
    if not records:
        return
    # If a file was edited many times by same user in 30 minutes, just update timestamp.
    if len(records) == 1 and records[0]['op_type'] == 'edit':
        record = records[0]
        _timestamp = record['timestamp'] - timedelta(minutes=30)
        stmt = select(Activity).where(Activity.timestamp > _timestamp,
                                      Activity.repo_id == record['repo_id'],
                                      Activity.op_type == record['op_type'],
                                      Activity.op_user == record['op_user'],
                                      Activity.path == record['path']).limit(1)
        row = session.scalars(stmt).first()
        if row:
            activity_id = row.id
            update_user_activity_timestamp(session, activity_id, record)
        else:
            save_user_activity(session, record)
    else:
        for record in records:
            save_user_activity(session, record)


def save_repo_trashs(session, records):
    for record in records:
        if record['op_type'] == 'delete':
            save_repo_trash(session, record)
        if record['op_type'] == 'recover':
            restore_repo_trash(session, record)


def get_delete_records(session, repo_id, show_day, start, limit):
    if show_day == 0:
        return [], 0
    elif show_day == -1:
        stmt = select(FileTrash).where(FileTrash.repo_id == repo_id)
        count_stmt = select(func.count(FileTrash.id)).where(FileTrash.repo_id == repo_id)

    else:
        _timestamp = datetime.datetime.now() - timedelta(days=show_day)
        stmt = select(FileTrash).where(FileTrash.repo_id == repo_id,
                                         FileTrash.delete_time > _timestamp,)
        count_stmt = select(func.count(FileTrash.id)).where(FileTrash.repo_id == repo_id,
                                                            FileTrash.delete_time > _timestamp,)

    stmt = stmt.order_by(desc(FileTrash.delete_time))
    total_count = session.scalar(count_stmt)
    stmt = stmt.slice(start, start + limit)
    res = session.scalars(stmt).all()

    return res, total_count

def generate_activity_records(added_files, deleted_files, added_dirs,
        deleted_dirs, modified_files, renamed_files, moved_files, renamed_dirs,
        moved_dirs, commit, repo_id, parent, related_users, time):

    OP_CREATE = 'create'
    OP_DELETE = 'delete'
    OP_EDIT = 'edit'
    OP_RENAME = 'rename'
    OP_MOVE = 'move'
    OP_RECOVER = 'recover'

    OBJ_FILE = 'file'
    OBJ_DIR = 'dir'

    repo = seafile_api.get_repo(repo_id)
    if repo.repo_type == 'wiki':
        return [], []

    base_record = {
        'commit_id': commit.commit_id,
        'timestamp': time,
        'repo_id': repo_id,
        'related_users': related_users,
        'op_user': getattr(commit, 'creator_name', ''),
        'repo_name': repo.repo_name
    }
    base_trash_record = {
        'commit_id': commit.parent_id,
        'op_user': getattr(commit, 'creator_name', ''),
        'timestamp': time,
        'repo_id': repo_id,
    }
    records = []
    trash_records = []

    for de in added_files:
        record = copy.copy(base_record)
        trash_record = copy.copy(base_trash_record)
        op_type = ''
        if commit.description.startswith('Reverted'):
            op_type = OP_RECOVER
            trash_record['op_type'] = OP_RECOVER
            trash_record['obj_id'] = de.obj_id
            trash_record['obj_name'] = os.path.basename(de.path)
            trash_record['path'] = '/' if os.path.dirname(de.path) == '/' else os.path.dirname(de.path) + '/'
            trash_record['size'] = de.size
            trash_records.append(trash_record)
        else:
            op_type = OP_CREATE
        record['op_type'] = op_type
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_FILE
        record['path'] = de.path
        record['size'] = de.size
        records.append(record)

    for de in deleted_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_DELETE
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_FILE
        record['size'] = de.size
        record['path'] = de.path
        records.append(record)

        trash_record = copy.copy(base_trash_record)
        trash_record['obj_type'] = 'file'
        trash_record['op_type'] = OP_DELETE
        trash_record['obj_id'] = de.obj_id
        trash_record['obj_name'] = os.path.basename(de.path)
        trash_record['path'] = '/' if os.path.dirname(de.path) == '/' else os.path.dirname(de.path) + '/'
        trash_record['size'] = de.size
        trash_records.append(trash_record)
    for de in added_dirs:
        record = copy.copy(base_record)
        trash_record = copy.copy(base_trash_record)
        op_type = ''
        if commit.description.startswith('Recovered'):
            op_type = OP_RECOVER
            trash_record['op_type'] = OP_RECOVER
            trash_record['obj_id'] = de.obj_id
            trash_record['obj_name'] = os.path.basename(de.path)
            trash_record['path'] = '/' if os.path.dirname(de.path) == '/' else os.path.dirname(de.path) + '/'
            trash_record['size'] = de.size
            trash_records.append(trash_record)
        else:
            op_type = OP_CREATE
        record['op_type'] = op_type
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_DIR
        record['path'] = de.path
        records.append(record)

    for de in deleted_dirs:
        record = copy.copy(base_record)
        record['op_type'] = OP_DELETE
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_DIR
        record['path'] = de.path
        records.append(record)

        trash_record = copy.copy(base_trash_record)
        trash_record['obj_type'] = 'dir'
        trash_record['op_type'] = OP_DELETE
        trash_record['obj_id'] = de.obj_id
        trash_record['obj_name'] = os.path.basename(de.path)
        trash_record['path'] = '/' if os.path.dirname(de.path) == '/' else os.path.dirname(de.path) + '/'
        trash_record['size'] = de.size
        trash_records.append(trash_record)
    for de in modified_files:
        record = copy.copy(base_record)
        op_type = ''
        if commit.description.startswith('Reverted'):
            op_type = OP_RECOVER
        else:
            op_type = OP_EDIT
        record['op_type'] = op_type
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_FILE
        record['path'] = de.path
        record['size'] = de.size
        records.append(record)

    for de in renamed_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_RENAME
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_FILE
        record['path'] = de.new_path
        record['size'] = de.size
        record['old_path'] = de.path
        records.append(record)

    for de in moved_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_MOVE
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_FILE
        record['path'] = de.new_path
        record['size'] = de.size
        record['old_path'] = de.path
        records.append(record)

    for de in renamed_dirs:
        record = copy.copy(base_record)
        record['op_type'] = OP_RENAME
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_DIR
        record['path'] = de.new_path
        record['size'] = de.size
        record['old_path'] = de.path
        records.append(record)

    for de in moved_dirs:
        record = copy.copy(base_record)
        record['op_type'] = OP_MOVE
        record['obj_id'] = de.obj_id
        record['obj_type'] = OBJ_DIR
        record['path'] = de.new_path
        record['size'] = de.size
        record['old_path'] = de.path
        records.append(record)

    filtered_records = []
    for record in records:
        if os.path.dirname(record['path']).startswith('/images/auto-upload'):
            continue
        if os.path.dirname(record['path']).startswith('/images/sdoc'):
            continue
        if os.path.dirname(record['path']).startswith('/_Internal'):
            continue
        if 'old_path' in record:
            record['old_path'] = record['old_path'].rstrip('/')
        record['path'] = record['path'].rstrip('/') if record['path'] != '/' else '/'
        filtered_records.append(record)
    return filtered_records, trash_records

def list_file_in_dir(repo_id, dirents, op_type):
    _dirents = copy.copy(dirents)
    files = []
    while True:
        try:
            d = _dirents.pop()
        except IndexError:
            break
        else:
            dir_obj = fs_mgr.load_seafdir(repo_id, 1, d.obj_id)
            new_path = None

            file_list = dir_obj.get_files_list()
            for _file in file_list:
                if op_type in ['rename', 'move']:
                    new_path = os.path.join(d.new_path, _file.name)
                new_file = DiffEntry(os.path.join(d.path, _file.name), _file.id, _file.size, new_path)
                files.append(new_file)

            subdir_list = dir_obj.get_subdirs_list()
            for _dir in subdir_list:
                if op_type in ['rename', 'move']:
                    new_path = os.path.join(d.new_path, _dir.name)
                new_dir = DiffEntry(os.path.join(d.path, _dir.name), _dir.id, new_path=new_path)
                _dirents.append(new_dir)

    return files

def generate_filehistory_records(added_files, deleted_files, added_dirs,
        deleted_dirs, modified_files, renamed_files, moved_files, renamed_dirs,
        moved_dirs, commit, repo_id, parent, time):

    OP_CREATE = 'create'
    OP_DELETE = 'delete'
    OP_EDIT = 'edit'
    OP_RENAME = 'rename'
    OP_MOVE = 'move'
    OP_RECOVER = 'recover'

    OBJ_FILE = 'file'
    OBJ_DIR = 'dir'

    repo = seafile_api.get_repo(repo_id)
    base_record = {
        'commit_id': commit.commit_id,
        'timestamp': time,
        'repo_id': repo_id,
        'op_user': getattr(commit, 'creator_name', '')
    }
    records = []

    _added_files = copy.copy(added_files)
    _added_files.extend(list_file_in_dir(repo_id, added_dirs, 'add'))
    for de in _added_files:
        record = copy.copy(base_record)
        if commit.description.startswith('Reverted') or commit.description.startswith('Recovered'):
            op_type = OP_RECOVER
        else:
            op_type = OP_CREATE
        record['op_type'] = op_type
        record['obj_id'] = de.obj_id
        record['path'] = de.path.rstrip('/')
        record['size'] = de.size
        records.append(record)

    _deleted_files = copy.copy(deleted_files)
    _deleted_files.extend(list_file_in_dir(repo_id, deleted_dirs, 'delete'))
    for de in _deleted_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_DELETE
        record['obj_id'] = de.obj_id
        record['size'] = de.size
        record['path'] = de.path.rstrip('/')
        records.append(record)

    for de in modified_files:
        record = copy.copy(base_record)
        op_type = ''
        if commit.description.startswith('Reverted'):
            op_type = OP_RECOVER
        else:
            op_type = OP_EDIT
        record['op_type'] = op_type
        record['obj_id'] = de.obj_id
        record['path'] = de.path.rstrip('/')
        record['size'] = de.size
        records.append(record)

    _renamed_files = copy.copy(renamed_files)
    _renamed_files.extend(list_file_in_dir(repo_id, renamed_dirs, 'rename'))
    for de in _renamed_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_RENAME
        record['obj_id'] = de.obj_id
        record['path'] = de.new_path.rstrip('/')
        record['size'] = de.size
        record['old_path'] = de.path.rstrip('/')
        records.append(record)

    _moved_files = copy.copy(moved_files)
    _moved_files.extend(list_file_in_dir(repo_id, moved_dirs, 'move'))
    for de in _moved_files:
        record = copy.copy(base_record)
        record['op_type'] = OP_MOVE
        record['obj_id'] = de.obj_id
        record['path'] = de.new_path.rstrip('/')
        record['size'] = de.size
        record['old_path'] = de.path.rstrip('/')
        records.append(record)

    return records


def save_file_histories(config, session, records):
    if not isinstance(records, list):
        return
    for record in records:
        if should_record(config, record):
            fh_threshold = int(get_opt_from_conf_or_env(config, 'FILE HISTORY', 'threshold', default=5))
            save_filehistory(session, fh_threshold, record)


def should_record(config, record):
    """ return True if record['path'] is a specified office file
    """
    suffix = 'md,txt,doc,docx,xls,xlsx,ppt,pptx,sdoc'
    fh_suffix_list = get_opt_from_conf_or_env(config, 'FILE HISTORY', 'suffix', default=suffix.strip(','))
    filename, suffix = splitext(record['path'])
    if suffix[1:] in fh_suffix_list:
        return True

    return False


def FileUpdateEventHandler(config, session, msg):
    try:
        elements = json.loads(msg['content'])
    except:
        logging.warning("got bad message: %s", msg)
        return

    repo_id = elements.get('repo_id')
    commit_id = elements.get('commit_id')
    if not repo_id or not commit_id:
        logging.debug("repo_id: %s, or commit_id: %s invalid.", repo_id, commit_id)
        return

    org_id = get_org_id_by_repo_id(repo_id)

    commit = get_commit(repo_id, 1, commit_id)
    if commit is None:
        commit = get_commit(repo_id, 0, commit_id)
        if commit is None:
            return

    time = datetime.datetime.utcfromtimestamp(msg['ctime'])
    creator_name = getattr(commit, 'creator_name', '')
    if creator_name is None:
        creator_name = ''
    save_file_update_event(session, time, creator_name, org_id,
                           repo_id, commit_id, commit.desc)

def FileAuditEventHandler(config, session, msg):
    try:
        elements = json.loads(msg['content'])
    except:
        logging.warning("got bad message: %s", msg)
        return

    timestamp = datetime.datetime.utcfromtimestamp(msg['ctime'])
    msg_type = elements.get('msg_type')
    user_name = elements.get('user_name')
    ip = elements.get('ip')
    user_agent = elements.get('user_agent')
    repo_id = elements.get('repo_id')
    file_path = elements.get('file_path')
    if not file_path.startswith('/'):
        file_path = '/' + file_path

    org_id = get_org_id_by_repo_id(repo_id)

    save_file_audit_event(session, timestamp, msg_type, user_name, ip,
                          user_agent, org_id, repo_id, file_path)

def PermAuditEventHandler(config, session, msg):
    try:
        elements = json.loads(msg['content'])
    except:
        logging.warning("got bad message: %s", msg)
        return

    timestamp = datetime.datetime.utcfromtimestamp(msg['ctime'])
    etype = elements.get('etype')
    from_user = elements.get('from_user')
    to = elements.get('to')
    repo_id = elements.get('repo_id')
    file_path = elements.get('file_path')
    perm = elements.get('perm')

    org_id = get_org_id_by_repo_id(repo_id)

    save_perm_audit_event(session, timestamp, etype, from_user, to,
                          org_id, repo_id, file_path, perm)


def register_handlers(handlers, enable_audit):
    handlers.add_handler('seaf_server.event:repo-update', RepoUpdateEventHandler)
    if enable_audit:
        handlers.add_handler('seaf_server.event:repo-update', FileUpdateEventHandler)
        handlers.add_handler('seaf_server.event:repo-download-sync', FileAuditEventHandler)
        handlers.add_handler('seaf_server.event:repo-upload-sync', FileAuditEventHandler)
        handlers.add_handler('seaf_server.event:seadrive-download-file', FileAuditEventHandler)

        handlers.add_handler('seahub.audit:file-download-web', FileAuditEventHandler)
        handlers.add_handler('seahub.audit:file-download-api', FileAuditEventHandler)
        handlers.add_handler('seahub.audit:file-download-share-link', FileAuditEventHandler)
        handlers.add_handler('seahub.audit:perm-change', PermAuditEventHandler)
