#!/usr/bin/env python
# -*- test-case-name: calendarserver.tools.test.test_upgrade -*-
##
# Copyright (c) 2006-2017 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
This tool allows any necessary upgrade to complete, then exits.
"""

import os
import sys
import time

from txdav.common.datastore.sql import CommonDataStore

from twisted.internet.defer import succeed
from twisted.logger import LogLevel, formatEvent, formatTime
from twisted.logger import FilteringLogObserver, LogLevelFilterPredicate
from twisted.python.text import wordWrap
from twisted.python.usage import Options, UsageError

from twext.python.log import Logger

from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE

from calendarserver.tools.cmdline import utilityMain, WorkerService
from calendarserver.tap.caldav import CalDAVServiceMaker

log = Logger()


def usage(e=None):
    if e:
        print(e)
        print("")
    try:
        UpgradeOptions().opt_help()
    except SystemExit:
        pass
    if e:
        sys.exit(64)
    else:
        sys.exit(0)


description = '\n'.join(
    wordWrap(
        """
        Usage: calendarserver_upgrade [options] [input specifiers]\n
        """ + __doc__,
        int(os.environ.get('COLUMNS', '80'))
    )
)


class UpgradeOptions(Options):
    """
    Command-line options for 'calendarserver_upgrade'

    @ivar upgraders: a list of L{DirectoryUpgradeer} objects which can identify the
        calendars to upgrade, given a directory service.  This list is built by
        parsing --record and --collection options.
    """

    synopsis = description

    optFlags = [
        ['status', 's', "Check database status and exit."],
        ['check', 'c', "Check current database schema and exit."],
        ['postprocess', 'p', "Perform post-database-import processing."],
        ['debug', 'D', "Print log messages to STDOUT."],
    ]

    optParameters = [
        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
        ['prefix', 'x', "", "Only upgrade homes with the specified GUID prefix - partial upgrade only."],
    ]

    def __init__(self):
        super(UpgradeOptions, self).__init__()
        self.upgradeers = []
        self.outputName = '-'
        self.merge = False

    def opt_output(self, filename):
        """
        Specify output file path (default: '-', meaning stdout).
        """
        self.outputName = filename

    opt_o = opt_output

    def opt_merge(self):
        """
        Rather than skipping homes that exist on the filesystem but not in the
        database, merge their data into the existing homes.
        """
        self.merge = True

    opt_m = opt_merge

    def openOutput(self):
        """
        Open the appropriate output file based on the '--output' option.
        """
        if self.outputName == '-':
            return sys.stdout
        else:
            return open(self.outputName, 'wb')


class UpgraderService(WorkerService, object):
    """
    Service which runs, exports the appropriate records, then stops the reactor.
    """

    started = False

    def __init__(self, store, options, output, reactor, config):
        super(UpgraderService, self).__init__(store)
        self.options = options
        self.output = output
        self.reactor = reactor
        self.config = config
        self._directory = None

    def doWork(self):
        """
        Immediately stop.  The upgrade will have been run before this.
        """
        # If we get this far the database is OK
        if self.options["status"]:
            self.output.write("Database OK.\n")
        elif self.options["check"]:
            checkResult = getattr(CommonDataStore, "checkSchemaResults", None)
            if checkResult is not None:
                if checkResult:
                    self.output.write("Database check FAILED:\n{}\n".format("\n".join(checkResult)))
                else:
                    self.output.write("Database check OK.\n")
            else:
                self.output.write("Database check FAILED.\n")
        else:
            self.output.write("Upgrade complete, shutting down.\n")
        UpgraderService.started = True
        return succeed(None)

    def doWorkWithoutStore(self):
        """
        Immediately stop.  The upgrade will have been run before this and failed.
        """
        if self.options["status"]:
            self.output.write("Upgrade needed.\n")
        elif self.options["check"]:
            checkResult = getattr(CommonDataStore, "checkSchemaResults", None)
            if checkResult is not None:
                if checkResult:
                    self.output.write("Database check FAILED:\n{}\n".format("\n".join(checkResult)))
                else:
                    self.output.write("Database check OK.\n")
            else:
                self.output.write("Database check FAILED.\n")
        else:
            self.output.write("Upgrade failed.\n")
        UpgraderService.started = True
        return succeed(None)

    def postStartService(self):
        """
        Quit right away
        """
        from twisted.internet import reactor
        from twisted.internet.error import ReactorNotRunning
        try:
            reactor.stop()
        except ReactorNotRunning:
            # I don't care.
            pass

    def stopService(self):
        """
        Stop the service.  Nothing to do; everything should be finished by this
        time.
        """


def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
    """
    Do the export.
    """
    from twistedcaldav.config import config
    if reactor is None:
        from twisted.internet import reactor

    options = UpgradeOptions()
    try:
        options.parseOptions(argv[1:])
    except UsageError, e:
        usage(e)

    try:
        output = options.openOutput()
    except IOError, e:
        stderr.write("Unable to open output file for writing: %s\n" % (e))
        sys.exit(1)

    if options.merge:
        def setMerge(data):
            data.MergeUpgrades = True
        config.addPostUpdateHooks([setMerge])

    def makeService(store):
        return UpgraderService(store, options, output, reactor, config)

    def onlyUpgradeEvents(eventDict):
        text = formatEvent(eventDict)
        output.write(formatTime(eventDict.get("log_time", time.time())) + " " + text + "\n")
        output.flush()

    if not options["status"] and not options["check"]:
        # When doing an upgrade always send L{LogLevel.warn} logging to the tool output
        log.observer.addObserver(FilteringLogObserver(
            onlyUpgradeEvents,
            [LogLevelFilterPredicate(defaultLogLevel=LogLevel.warn), ]
        ))

    def customServiceMaker():
        customService = CalDAVServiceMaker()
        customService.doPostImport = options["postprocess"]
        return customService

    def _patchConfig(config):
        config.FailIfUpgradeNeeded = options["status"] or options["check"]
        config.CheckExistingSchema = options["check"]
        if options["prefix"]:
            config.UpgradeHomePrefix = options["prefix"]
        if not options["status"] and not options["check"]:
            config.DefaultLogLevel = "debug"

    def _onShutdown():
        if not UpgraderService.started:
            print("Failed to start service.")

    utilityMain(options["config"], makeService, reactor, customServiceMaker, patchConfig=_patchConfig, onShutdown=_onShutdown, verbose=options["debug"])


if __name__ == '__main__':
    main()
