#!/usr/bin/env python3
import argparse
import configparser
import json
import logging
import os
import shutil
import threading
import time
import traceback
import webbrowser
from functools import partial
from logging import handlers

import bottle
import pyinotify
from bottle import ServerAdapter
from boxsdk import Client
from cherrypy import wsgiserver
from cherrypy.wsgiserver import ssl_builtin

from diycrate.cache_utils import redis_key, redis_get, r_c, redis_set
from diycrate.file_operations import EventHandler, wm, in_moved_to, in_moved_from, mask
from diycrate.gui import notify_user_with_gui
from diycrate.item_queue_io import upload_queue_processor, download_queue_processor, download_queue_monitor, \
    upload_queue_monitor, download_queue, upload_queue
from diycrate.oauth_utils import setup_oauth, store_tokens_callback
from diycrate.path_utils import re_walk

crate_logger = logging.getLogger('diy_crate_logger')
crate_logger.setLevel(logging.DEBUG)

l_handler = handlers.SysLogHandler(address='/dev/log')

crate_logger.addHandler(l_handler)

log_format = 'diycrate' + ' %(levelname)-9s %(name)-15s %(threadName)-14s +%(lineno)-4d %(message)s'
log_format = logging.Formatter(log_format)
l_handler.setFormatter(log_format)

cloud_provider_name = 'Box'

bottle_app = bottle.Bottle()

# The watch manager stores the watches and provides operations on watches

# keep the lint-ing & introspection from complaining that these attributes don't exist before run-time.

BOX_DIR = os.path.expanduser('~/box')

download_thread = threading.Thread(target=download_queue_processor)
upload_thread = threading.Thread(target=upload_queue_processor)

trash_directory = os.path.expanduser('~/.local/share/Trash/files')

upload_mon_thread = threading.Thread(target=upload_queue_monitor)

download_mon_thread = threading.Thread(target=download_queue_monitor)

handler = EventHandler(upload_queue=upload_queue)

notifier = pyinotify.ThreadedNotifier(wm, handler, read_freq=10)
notifier.coalesce_events()


def long_poll_event_listener():
    """
    Receive and process remote cloud item events in real-time
    :return:
    """
    client = Client(oauth=handler.oauth)
    while True:
        try:
            stream_position = client.events().get_latest_stream_position()
            for event in client.events().generate_events_with_long_polling(stream_position=stream_position):
                crate_logger.debug('{} happened!'.format(json.dumps(event)))
                if event.get('message', '').lower() == 'reconnect':
                    break
                if event['event_type'] == 'ITEM_CREATE':
                    obj_id = event['source']['id']
                    obj_type = event['source']['type']
                    if obj_type == 'folder':
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        file_path = os.path.join(path, event['source']['name'])
                        if not os.path.isdir(file_path):
                            os.makedirs(file_path)
                            redis_set(r_c, client.folder(folder_id=obj_id), os.path.getmtime(file_path),
                                      BOX_DIR, True, path)
                if event['event_type'] == 'ITEM_MOVE':
                    obj_id = event['source']['id']
                    obj_type = event['source']['type']
                    if obj_type in ('file', 'folder'):
                        if r_c.exists(redis_key(obj_id)):
                            item_info = json.loads(str(r_c.get(redis_key(obj_id)),
                                                       encoding='utf-8',
                                                       errors='strict'))
                            src_file_path = json.loads(str(r_c.get(redis_key(obj_id)),
                                                           encoding='utf-8',
                                                           errors='strict'))['file_path']
                            if int(event['source']['path_collection']['total_count']) > 1:
                                path = '{}'.format(os.path.sep).join([folder['name']
                                                                      for folder in
                                                                      event['source']['path_collection']['entries'][
                                                                      1:]])
                            else:
                                path = ''
                            path = os.path.join(BOX_DIR, path)
                            file_path = os.path.join(path, event['source']['name'])
                            if os.path.exists(src_file_path):
                                to_set = []
                                if obj_type == 'folder':
                                    for sub_id in get_sub_ids(obj_id):
                                        if r_c.exists(redis_key(sub_id)):
                                            sub_item_info = json.loads(str(r_c.get(redis_key(sub_id)),
                                                                           encoding='utf-8',
                                                                           errors='strict'))
                                            orig_len = len(src_file_path.split(os.path.sep))
                                            tail = os.path.sep.join(
                                                sub_item_info['file_path'].split(os.path.sep)[orig_len:])
                                            new_sub_path = os.path.join(file_path, tail)
                                            sub_item_info['file_path'] = new_sub_path
                                            to_set.append(
                                                partial(r_c.set, redis_key(sub_id), json.dumps(sub_item_info)))
                                shutil.move(src_file_path, file_path)
                                for item in to_set:
                                    item()
                                item_info['file_path'] = file_path
                                item_info['etag'] = event['source']['etag']
                                r_c.set(redis_key(obj_id), json.dumps(item_info))
                if event['event_type'] == 'ITEM_RENAME':
                    obj_id = event['source']['id']
                    obj_type = event['source']['type']
                    if obj_type == 'file':
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        file_path = os.path.join(path, event['source']['name'])
                        file_obj = client.file(file_id=obj_id).get()
                        src_file_path = None if not r_c.exists(redis_key(obj_id)) else redis_get(r_c, file_obj)[
                            'file_path']
                        if src_file_path and os.path.exists(src_file_path):
                            version_info = redis_get(r_c, obj=file_obj)
                            os.rename(src_file_path, file_path)
                            version_info['file_path'] = file_path
                            version_info['etag'] = file_obj['etag']
                            r_c.set(redis_key(obj_id), json.dumps(version_info))
                            r_c.set('diy_crate.last_save_time_stamp', int(time.time()))
                        else:
                            download_queue.put([file_obj, file_path, client._oauth])
                    elif obj_type == 'folder':
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        file_path = os.path.join(path, event['source']['name'])
                        folder_obj = client.folder(folder_id=obj_id).get()
                        src_file_path = None if not r_c.exists(redis_key(obj_id)) else redis_get(r_c, folder_obj)[
                            'file_path']
                        if src_file_path and os.path.exists(src_file_path):
                            os.rename(src_file_path, file_path)
                            version_info = redis_get(r_c, obj=folder_obj)
                            version_info['file_path'] = file_path
                            version_info['etag'] = folder_obj['etag']
                            r_c.set(redis_key(obj_id), json.dumps(version_info))
                            r_c.set('diy_crate.last_save_time_stamp', int(time.time()))
                elif event['event_type'] == 'ITEM_UPLOAD':
                    obj_id = event['source']['id']
                    obj_type = event['source']['type']
                    if obj_type == 'file':
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        if not os.path.exists(path):  # just in case this is a file in a new subfolder
                            os.makedirs(path)
                        download_queue.put(
                            [client.file(file_id=obj_id).get(), os.path.join(path, event['source']['name']),
                             client._oauth])
                        # was_versioned = r_c.exists(redis_key(obj_id))
                        # if not was_versioned:
                        #     if int(event['source']['path_collection']['total_count']) > 1:
                        #         path = '{}'.format(os.path.sep).join([folder['name']
                        #                                                   for folder in
                        #                                                   event['source']['path_collection']['entries'][1:]])
                        #     else:
                        #         path = ''
                        #     path = os.path.join(BOX_DIR, path)
                        #     download_queue.put([client.file(file_id=obj_id).get(), os.path.join(path, event['source']['name'])])
                        #     # with open(os.path.join(path, event['source']['name']), 'w') as new_file_handler:
                        #     #     client.file(file_id=obj_id).get().download_to(new_file_handler)
                        #     #
                        #     # r_c.set(redis_key([obj_id]), json.dumps({'etag': event['source']['etag'],
                        #     #                         'fresh_download': True,
                        #     #                         'time_stamp': time.time()}))
                        # else:
                        #     pass
                    elif obj_type == 'folder':
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        if not os.path.exists(path):  # just in case this is a file in a new subfolder
                            os.makedirs(path)
                        file_path = os.path.join(path, event['source']['name'])
                        box_message = 'new version'
                        if not os.path.exists(file_path):
                            os.makedirs(file_path)
                            redis_set(r_c, client.folder(folder_id=obj_id), os.path.getmtime(file_path),
                                      BOX_DIR, True, path)
                            box_message = 'new folder'
                        notify_user_with_gui('Box message: {} {}'.format(box_message, file_path), crate_logger)
                elif event['event_type'] == 'ITEM_TRASH':
                    obj_id = event['source']['id']
                    obj_type = event['source']['type']
                    if obj_type == 'file':
                        item_info = r_c.get(redis_key(obj_id))
                        if item_info:
                            item_info = json.loads(str(item_info, encoding='utf-8', errors='strict'))
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        file_path = os.path.join(path, event['source']['name']) if not item_info else item_info[
                            'file_path']
                        if os.path.exists(file_path):
                            os.unlink(file_path)
                        if r_c.exists(redis_key(obj_id)):
                            r_c.delete(redis_key(obj_id))
                            r_c.set('diy_crate.last_save_time_stamp', int(time.time()))
                        notify_user_with_gui('Box message: Deleted {}'.format(file_path), crate_logger)
                    elif obj_type == 'folder':
                        item_info = r_c.get(redis_key(obj_id))
                        if item_info:
                            item_info = json.loads(str(item_info, encoding='utf-8', errors='strict'))
                        if int(event['source']['path_collection']['total_count']) > 1:
                            path = '{}'.format(os.path.sep).join([folder['name']
                                                                  for folder in
                                                                  event['source']['path_collection']['entries'][
                                                                  1:]])
                        else:
                            path = ''
                        path = os.path.join(BOX_DIR, path)
                        file_path = os.path.join(path, event['source']['name']) if not item_info else item_info[
                            'file_path']
                        if os.path.isdir(file_path):
                            # still need to get the parent_id
                            for box_id in get_sub_ids(obj_id):
                                r_c.delete(redis_key(box_id))
                            r_c.delete(redis_key(obj_id))
                            shutil.rmtree(file_path)
                            parent_id = r_c.get(redis_key(obj_id)).get('parent_id')
                            if parent_id:
                                parent_folder = r_c.get(redis_key(parent_id))
                                sub_ids = parent_folder.get('sub_ids', [])
                                if sub_ids:
                                    sub_ids.remove(obj_id)
                                    r_c.set(redis_key(parent_id), json.dumps(parent_folder))
                                r_c.set('diy_crate.last_save_time_stamp', int(time.time()))
                            notify_user_with_gui('Box message: Deleted {}'.format(file_path), crate_logger)

                elif event['event_type'] == 'ITEM_DOWNLOAD':
                    pass
        except Exception:
            crate_logger.debug(traceback.format_exc())


def get_sub_ids(box_id):
    """
    Retrieve the box item ids that are stored in this sub path
    :param box_id:
    :return:
    """
    ids = []
    item_info = json.loads(str(r_c.get(redis_key(box_id)), encoding='utf-8', errors='strict'))
    for sub_id in item_info['sub_ids']:
        sub_item_info = json.loads(str(r_c.get(redis_key(sub_id)), encoding='utf-8', errors='strict'))
        if os.path.isdir(sub_item_info['file_path']):
            ids.extend(get_sub_ids(sub_id))
        ids.append(sub_id)
    return ids


long_poll_thread = threading.Thread(target=long_poll_event_listener)
long_poll_thread.daemon = True

walk_thread = threading.Thread(target=re_walk)
walk_thread.daemon = True


@bottle_app.route('/')
def oauth_handler():
    """
    RESTful end-point for the oauth handling
    :return:
    """
    assert bottle_app.csrf_token == bottle.request.GET['state']
    #  we actually store these values in redis as an explicit part of the callback, so we basically throw these away
    # access_token, refresh_token = bottle_app.oauth.authenticate(bottle.request.GET['code'])
    bottle_app.oauth.authenticate(bottle.request.GET['code'])
    start_cloud_threads(bottle_app.oauth)

    return 'OK'


def start_cloud_threads(client_oauth):
    """

    :return:
    """
    client = Client(client_oauth)
    handler.oauth = client_oauth
    wm.add_watch(BOX_DIR, mask, rec=False)
    box_folder = client.folder(folder_id='0').get()
    download_thread.daemon = True
    download_thread.start()
    upload_thread.daemon = True
    upload_thread.start()
    # local trash can
    wm.add_watch(trash_directory, mask=in_moved_to | in_moved_from)
    if not long_poll_thread.is_alive():  # start before doing anything else
        long_poll_thread.start()
    # walk_and_notify_and_download_tree(BOX_DIR, box_folder, client)
    walk_thread._args = (BOX_DIR, box_folder, client, client_oauth)
    walk_thread.start()


# Create our own sub-class of Bottle's ServerAdapter
# so that we can specify SSL. Using just server='cherrypy'
# uses the default cherrypy server, which doesn't use SSL
class SSLCherryPyServer(ServerAdapter):
    """
    Custom server adapter using cherry-py with ssl
    """

    def run(self, server_handler):
        """
        Overrides super to setup Cherry py with ssl and start the server.
        :param server_handler: originating server type
        :type server_handler:
        """
        server = wsgiserver.CherryPyWSGIServer((self.host, self.port), server_handler)
        # Uses the following github page's recommendation for setting up the cert:
        # https://github.com/nickbabcock/bottle-ssl
        server.ssl_adapter = ssl_builtin.BuiltinSSLAdapter(conf_obj['ssl']['cacert_pem_path'],
                                                           conf_obj['ssl']['privkey_pem_path'])
        try:
            server.start()
        finally:
            server.stop()


if __name__ == '__main__':
    conf_obj = configparser.ConfigParser()
    conf_dir = os.path.abspath(os.path.expanduser('~/.config/diycrate'))
    if not os.path.isdir(conf_dir):
        os.mkdir(conf_dir)
    cloud_credentials_file_path = os.path.join(conf_dir, 'box.ini')
    if not os.path.isfile(cloud_credentials_file_path):
        open(cloud_credentials_file_path, 'w').write('')
    conf_obj.read(cloud_credentials_file_path)
    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument('--client_id', type=str, help='Client ID provided by {}'.format(cloud_provider_name),
                            default='')
    arg_parser.add_argument('--client_secret', type=str,
                            help='Client Secret provided by {}'.format(cloud_provider_name), default='')
    arg_parser.add_argument('--box_dir', type=str, help='directory for the root of box', default='~/box')
    arg_parser.add_argument('--cacert_pem_path', type=str, help='filepath to where the cacert.pem is located',
                            default='')
    arg_parser.add_argument('--privkey_pem_path', type=str, help='filepath to where the privkey.pem is located',
                            default='')
    args = arg_parser.parse_args()
    had_oauth2 = conf_obj.has_section('oauth2')
    if not had_oauth2:
        conf_obj.add_section('oauth2')
    conf_obj['oauth2'] = {
        'client_id': args.client_id or conf_obj['oauth2']['client_id'],
        'client_secret': args.client_secret or conf_obj['oauth2']['client_secret']
    }
    if 'ssl' not in conf_obj:
        if not args.cacert_pem_path:
            raise ValueError('Need a valid cacert_pem_path')
        if not args.privkey_pem_path:
            raise ValueError('Need a valid privkey_pem_path')
        conf_obj['ssl'] = {
            'cacert_pem_path': os.path.abspath(os.path.expanduser(args.cacert_pem_path)),
            'privkey_pem_path': os.path.abspath(os.path.expanduser(args.privkey_pem_path)),
        }
    conf_obj['ssl'] = {
        'cacert_pem_path': os.path.abspath(os.path.expanduser(args.cacert_pem_path)) if args.cacert_pem_path else
        conf_obj['ssl']['cacert_pem_path'],
        'privkey_pem_path': os.path.abspath(os.path.expanduser(args.privkey_pem_path)) if args.privkey_pem_path else
        conf_obj['ssl']['privkey_pem_path'],
    }

    conf_obj.write(open(cloud_credentials_file_path, 'w'))
    had_box = conf_obj.has_section('box')
    if not had_box:
        conf_obj.add_section('box')

    conf_obj['box'] = {
        'directory': args.box_dir or conf_obj['box']['directory']
    }
    conf_obj.write(open(cloud_credentials_file_path, 'w'))
    BOX_DIR = os.path.expanduser(conf_obj['box']['directory'])
    if not os.path.isdir(BOX_DIR):
        os.mkdir(BOX_DIR)
    if not r_c.exists('diy_crate.auth.access_token') and not r_c.exists('diy_crate.auth.refresh_token'):
        bottle_app.oauth = setup_oauth(r_c, conf_obj, store_tokens_callback)
        auth_url, bottle_app.csrf_token = bottle_app.oauth.get_authorization_url('https://localhost:8080/')
        webbrowser.open_new_tab(auth_url)  # make it easy for the end-user to start auth
    else:
        oauth = setup_oauth(r_c, conf_obj, store_tokens_callback)
        start_cloud_threads(oauth)
    notifier.start()
    # notifier_thread = threading.Thread(target=notifier.loop)
    # notifier_thread.daemon = True
    # notifier_thread.start()
    if not r_c.exists('diy_crate.auth.access_token') and not r_c.exists('diy_crate.auth.refresh_token'):
        bottle_app.run(server=SSLCherryPyServer)
