#!/usr/bin/python
#
# Copyright 2009-2012 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the
# OpenSSL library under certain conditions as described in each
# individual source file, and distribute linked combinations
# including the two.
# You must obey the GNU General Public License in all respects
# for all of the code used other than OpenSSL.  If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so.  If you
# do not wish to do so, delete this exception statement from your
# version.  If you delete this exception statement from all source
# files in the program, then also delete it here.
"""Ubuntu One storage synchronization daemon."""

import sys

if sys.platform != 'win32':
    try:
        from twisted.internet import gireactor
        gireactor.install()
    except ImportError:
        from twisted.internet import glib2reactor
        glib2reactor.install()

    from dbus.mainloop.glib import DBusGMainLoop
    DBusGMainLoop(set_as_default=True)

import atexit
import os
import signal
import sys

from ubuntuone.platform import (
    can_write,
    set_dir_readwrite,
    is_already_running,
    is_root,
    make_dir,
    path_exists,
    recursive_move,
    set_application_name,
)
from ubuntuone.syncdaemon import logger, config
from ubuntuone.syncdaemon.config import (
    get_config_files,
)

from ubuntuone.syncdaemon.main import Main

from twisted.internet import reactor, defer
from ubuntu_sso.xdg_base_directory import (
    xdg_cache_home,
    xdg_data_home,
)


class DeathException(Exception):
    """The process has commited seppuku."""


def die(msg):
    """Write the error message an die."""
    logger.root_logger.warning(msg)
    sys.stderr.write(msg + '\n')
    raise DeathException()


def check_death(failure):
    """Stop the reactor and exit the process."""
    failure.trap(DeathException)
    reactor.callWhenRunning(reactor.stop)


def main(argv):
    """ client entry point. """
    args = argv[1:]
    usage = "Usage: %prog [config file] [extra config files] [options] "
    configs = []
    while len(args) > 0 and not args[0].startswith('-'):
        configs.append(args.pop(0))
    if len(configs) == 0:
        configs.extend(get_config_files())
    (parser, options, argv) = config.configglue(file(configs[0]), *configs[1:],
                               args=args, usage=usage)
    d = async_main(parser, options, argv)
    d.addErrback(check_death)

    # check if we should start a twisted manhole
    if options.debug_manhole:
        startManhole()
    else:
        logger.root_logger.info('not starting twisted.manhole')

    if options.debug_lsprof_file:
        try:
            from bzrlib.lsprof import profile
            ret, stats = profile(reactor.run)
            stats.save(options.debug_lsprof_file)
        except ImportError:
            logger.root_logger.warning('bzrlib.lsprof not available')
            reactor.run()
    else:
        reactor.run()


def startManhole():
    try:
        from twisted.conch import manhole, manhole_ssh
        from twisted.cred.portal import Portal
        from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    except ImportError:
        logger.root_logger.warning('twisted.manhole not available')
    else:
        logger.root_logger.info('starting twisted.manhole')
        realm = manhole_ssh.TerminalRealm()
        getManhole = lambda _: manhole.Manhole(globals())
        realm.chainedProtocolFactory.protocolFactory = getManhole
        portal = Portal(realm)
        checker = InMemoryUsernamePasswordDatabaseDontUse(debug="debug")
        portal.registerChecker(checker)
        manholeFactory = manhole_ssh.ConchFactory(portal)
        reactor.listenTCP(2222, manholeFactory)
        logger.root_logger.info('twisted.manhole started')


@defer.inlineCallbacks
def async_main(parser, options, argv):
    """The client entry point that can yield."""
    logger.init()
    if options.debug:
        logger.set_debug('stdout file')
    else:
        logger.configure_logging(options.logging_level,
                                 options.logging_file_size,
                                 options.logging_backup_count)

    # check we're not running as root, or have explicitely and in
    # length expressed our desire to do so
    if (is_root() and
            not options.im_ok_with_being_root_pretty_please_let_me_be_root):
        die("Please don't run the syncdaemon as root.")

    # check if the user disabled files sync
    # we need to perform this check first because libsyncdaemon uses
    # NameOwnerChanged signal to set up internal syncdaemon structures.
    # In case we check config after acquiring DBus name we'll get LP:759714
    if not config.get_user_config().get_files_sync_enabled():
        die('Files synchronization is disabled.')

    # check if there is another instance running
    is_running = yield is_already_running()

    if is_running:
        die('Another instance is running')

    # check if we are using xdg_data_home and it doesn't exists
    if xdg_data_home in options.data_dir and \
       not path_exists(options.data_dir):
        # if we have metadata in the old xdg_cache, move it!
        old_data_dir = options.data_dir.replace(xdg_data_home, xdg_cache_home)
        if path_exists(old_data_dir):
            parent = os.path.dirname(options.data_dir)
            if path_exists(parent) and not can_write(parent):
                # make the parent dir writable
                set_dir_readwrite(parent)
            elif not path_exists(parent):
                # if it don't exits
                make_dir(parent, recursive=True)
            recursive_move(old_data_dir, options.data_dir)
    if not path_exists(options.data_dir):
        parent = os.path.dirname(options.data_dir)
        if path_exists(parent) and not can_write(parent):
            # make the parent dir writable
            set_dir_readwrite(parent)
        make_dir(options.data_dir, recursive=True)

    # create the partials_dir
    partials_dir = os.path.join(xdg_cache_home, 'ubuntuone', 'partials')
    if not path_exists(partials_dir):
        make_dir(partials_dir, recursive=True)

    logger.rotate_logs()

    assert isinstance(options.root_dir, str)
    assert isinstance(options.shares_dir, str)
    assert isinstance(options.data_dir, str)

    # check if we have oauth credentials 
    oauth_credentials = None
    if options.oauth:
        values = options.oauth.split(':')
        if len(values) == 4 or len(values) == 2:
            oauth_credentials = values
        else:
            msg = "--oauth requires a key and secret together in the form " \
                  "[CONSUMER_KEY:CONSUMER_SECRET:]KEY:SECRET"
            parser.error(msg)

    main = Main(options.root_dir, options.shares_dir, options.data_dir,
                partials_dir, host=options.host, port=int(options.port),
                dns_srv=options.dns_srv, ssl=True,
                disable_ssl_verify=options.disable_ssl_verify,
                mark_interval=options.mark_interval,
                broadcast_events=options.send_events_over_dbus,
                handshake_timeout=options.handshake_timeout,
                shares_symlink_name='Shared With Me',
                read_limit=options.bandwidth_throttling_read_limit,
                write_limit=options.bandwidth_throttling_write_limit,
                throttling_enabled=options.bandwidth_throttling_on,
                ignore_files=options.ignore,
                oauth_credentials=oauth_credentials)

    # override the reactor default signal handlers in order to
    # shutdown properly
    atexit.register(reactor.callFromThread, main.quit)
    def install_handlers():
        """ install our custom signal handler. """
        def handler(signum, frame):
            logger.root_logger.debug("Signal received %s ", str(signum))
            reactor.callFromThread(main.quit)
        for signal_name in ['SIGHUP', 'SIGTERM', 'SIGINT']:
            actual_signal = getattr(signal, signal_name, None)
            # some platforms do not have all the signals, eg: Windows does not
            # have SIGHUP
            if actual_signal is not None:
                signal.signal(actual_signal, handler)

    reactor.callWhenRunning(install_handlers)
    # set the application name
    set_application_name('ubuntuone-syncdaemon')

    # check if we should start the heapy monitor
    if options.debug_heapy_monitor:
        try:
            import guppy.heapy.RM
        except ImportError:
            logger.root_logger.warning('guppy-pe/heapy not available, remote '
                                       'monitor thread not started')
        else:
            guppy.heapy.RM.on()

    main.start()


if __name__ == '__main__':
    try:
        main(sys.argv)
    except Exception:
        logger.root_logger.exception('Unexpected error')
        raise
