#!/usr/bin/env python
# -*- test-case-name: calendarserver.tools.test.test_calverify -*-
##
# Copyright (c) 2011-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.
##
from __future__ import print_function

"""
This tool allows data in the database to be directly inspected using a set
of simple commands.
"""

from calendarserver.tools import tables
from calendarserver.tools.cmdline import utilityMain, WorkerService
from pycalendar.datetime import DateTime

from twext.enterprise.dal.syntax import Select, Parameter, Count, Delete, \
    ALL_COLUMNS
from twext.who.idirectory import RecordType

from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twisted.python.filepath import FilePath
from twisted.python.text import wordWrap
from twisted.python.usage import Options

from twistedcaldav import caldavxml
from twistedcaldav.config import config
from twistedcaldav.directory import calendaruserproxy
from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE

from txdav.caldav.datastore.query.filter import Filter
from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
from txdav.who.idirectory import RecordType as CalRecordType

from uuid import UUID
import os
import sys
import traceback


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


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


class DBInspectOptions(Options):
    """
    Command-line options for 'calendarserver_dbinspect'
    """

    synopsis = description

    optFlags = [
        ['verbose', 'v', "Verbose logging."],
        ['debug', 'D', "Debug logging."],
        ['purging', 'p', "Enable Purge command."],
    ]

    optParameters = [
        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
    ]

    def __init__(self):
        super(DBInspectOptions, self).__init__()
        self.outputName = '-'


@inlineCallbacks
def UserNameFromUID(txn, uid):
    record = yield txn.directoryService().recordWithUID(uid)
    returnValue(record.shortNames[0] if record else "(%s)" % (uid,))


@inlineCallbacks
def UIDFromInput(txn, value):
    try:
        returnValue(str(UUID(value)).upper())
    except (ValueError, TypeError):
        pass

    record = yield txn.directoryService().recordWithShortName(RecordType.user, value)
    if record is None:
        record = yield txn.directoryService().recordWithShortName(CalRecordType.location, value)
    if record is None:
        record = yield txn.directoryService().recordWithShortName(CalRecordType.resource, value)
    if record is None:
        record = yield txn.directoryService().recordWithShortName(RecordType.group, value)
    returnValue(record.uid if record else None)


class Cmd(object):

    _name = None

    @classmethod
    def name(cls):
        return cls._name

    def doIt(self, txn):
        raise NotImplementedError


class TableSizes(Cmd):

    _name = "Show Size of each Table"

    @inlineCallbacks
    def doIt(self, txn):

        results = []
        for dbtable in schema.model.tables:  # @UndefinedVariable
            dbtable = getattr(schema, dbtable.name)
            count = yield self.getTableSize(txn, dbtable)
            results.append((dbtable.model.name, count,))

        # Print table of results
        table = tables.Table()
        table.addHeader(("Table", "Row Count"))
        for dbtable, count in sorted(results):
            table.addRow((
                dbtable,
                count,
            ))

        print("\n")
        print("Database Tables (total=%d):\n" % (len(results),))
        table.printTable()

    @inlineCallbacks
    def getTableSize(self, txn, dbtable):
        rows = (yield Select(
            [Count(ALL_COLUMNS), ],
            From=dbtable,
        ).on(txn))
        returnValue(rows[0][0])


class CalendarHomes(Cmd):

    _name = "List Calendar Homes"

    @inlineCallbacks
    def doIt(self, txn):

        uids = yield self.getAllHomeUIDs(txn)

        # Print table of results
        missing = 0
        table = tables.Table()
        table.addHeader(("Owner UID", "Short Name"))
        for uid in sorted(uids):
            shortname = yield UserNameFromUID(txn, uid)
            if shortname.startswith("("):
                missing += 1
            table.addRow((
                uid,
                shortname,
            ))

        print("\n")
        print("Calendar Homes (total=%d, missing=%d):\n" % (len(uids), missing,))
        table.printTable()

    @inlineCallbacks
    def getAllHomeUIDs(self, txn):
        ch = schema.CALENDAR_HOME
        rows = (yield Select(
            [ch.OWNER_UID, ],
            From=ch,
        ).on(txn))
        returnValue(tuple([row[0] for row in rows]))


class CalendarHomesSummary(Cmd):

    _name = "List Calendar Homes with summary information"

    @inlineCallbacks
    def doIt(self, txn):

        uids = yield self.getCalendars(txn)

        results = {}
        for uid, calname, count in sorted(uids, key=lambda x: x[0]):
            totalname, totalcount = results.get(uid, (0, 0,))
            if calname != "inbox":
                totalname += 1
                totalcount += count
                results[uid] = (totalname, totalcount,)

        # Print table of results
        table = tables.Table()
        table.addHeader(("Owner UID", "Short Name", "Calendars", "Resources"))
        totals = [0, 0, 0]
        for uid in sorted(results.keys()):
            shortname = yield UserNameFromUID(txn, uid)
            table.addRow((
                uid,
                shortname,
                results[uid][0],
                results[uid][1],
            ))
            totals[0] += 1
            totals[1] += results[uid][0]
            totals[2] += results[uid][1]
        table.addFooter(("Total", totals[0], totals[1], totals[2]))
        table.addFooter((
            "Average",
            "",
            "%.2f" % ((1.0 * totals[1]) / totals[0] if totals[0] else 0,),
            "%.2f" % ((1.0 * totals[2]) / totals[0] if totals[0] else 0,),
        ))

        print("\n")
        print("Calendars with resource count (total=%d):\n" % (len(results),))
        table.printTable()

    @inlineCallbacks
    def getCalendars(self, txn):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                ch.OWNER_UID,
                cb.CALENDAR_RESOURCE_NAME,
                Count(co.RESOURCE_ID),
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="left", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
            GroupBy=(ch.OWNER_UID, cb.CALENDAR_RESOURCE_NAME)
        ).on(txn))
        returnValue(tuple(rows))


class Calendars(Cmd):

    _name = "List Calendars"

    @inlineCallbacks
    def doIt(self, txn):

        uids = yield self.getCalendars(txn)

        # Print table of results
        table = tables.Table()
        table.addHeader(("Owner UID", "Short Name", "Calendar", "Resources"))
        for uid, calname, count in sorted(uids, key=lambda x: (x[0], x[1])):
            shortname = yield UserNameFromUID(txn, uid)
            table.addRow((
                uid,
                shortname,
                calname,
                count
            ))

        print("\n")
        print("Calendars with resource count (total=%d):\n" % (len(uids),))
        table.printTable()

    @inlineCallbacks
    def getCalendars(self, txn):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                ch.OWNER_UID,
                cb.CALENDAR_RESOURCE_NAME,
                Count(co.RESOURCE_ID),
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="left", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
            GroupBy=(ch.OWNER_UID, cb.CALENDAR_RESOURCE_NAME)
        ).on(txn))
        returnValue(tuple(rows))


class CalendarsByOwner(Cmd):

    _name = "List Calendars for Owner UID/Short Name"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("Owner UID/Name: ")
        uid = yield UIDFromInput(txn, uid)
        uids = yield self.getCalendars(txn, uid)

        # Print table of results
        table = tables.Table()
        table.addHeader(("Owner UID", "Short Name", "Calendars", "ID", "Resources"))
        totals = [0, 0, ]
        for uid, calname, resid, count in sorted(uids, key=lambda x: x[1]):
            shortname = yield UserNameFromUID(txn, uid)
            table.addRow((
                uid if totals[0] == 0 else "",
                shortname if totals[0] == 0 else "",
                calname,
                resid,
                count,
            ))
            totals[0] += 1
            totals[1] += count
        table.addFooter(("Total", "", totals[0], "", totals[1]))

        print("\n")
        print("Calendars with resource count (total=%d):\n" % (len(uids),))
        table.printTable()

    @inlineCallbacks
    def getCalendars(self, txn, uid):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                ch.OWNER_UID,
                cb.CALENDAR_RESOURCE_NAME,
                co.CALENDAR_RESOURCE_ID,
                Count(co.RESOURCE_ID),
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="left", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
            Where=(ch.OWNER_UID == Parameter("UID")),
            GroupBy=(ch.OWNER_UID, cb.CALENDAR_RESOURCE_NAME, co.CALENDAR_RESOURCE_ID)
        ).on(txn, **{"UID": uid}))
        returnValue(tuple(rows))


class Events(Cmd):

    _name = "List Events"

    @inlineCallbacks
    def doIt(self, txn):

        uids = yield self.getEvents(txn)

        # Print table of results
        table = tables.Table()
        table.addHeader(("Owner UID", "Short Name", "Calendar", "ID", "Type", "UID"))
        for uid, calname, id, caltype, caluid in sorted(uids, key=lambda x: (x[0], x[1])):
            shortname = yield UserNameFromUID(txn, uid)
            table.addRow((
                uid,
                shortname,
                calname,
                id,
                caltype,
                caluid
            ))

        print("\n")
        print("Calendar events (total=%d):\n" % (len(uids),))
        table.printTable()

    @inlineCallbacks
    def getEvents(self, txn):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                ch.OWNER_UID,
                cb.CALENDAR_RESOURCE_NAME,
                co.RESOURCE_ID,
                co.ICALENDAR_TYPE,
                co.ICALENDAR_UID,
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
        ).on(txn))
        returnValue(tuple(rows))


class EventsByCalendar(Cmd):

    _name = "List Events for a specific calendar"

    @inlineCallbacks
    def doIt(self, txn):

        rid = raw_input("Resource-ID: ")
        try:
            int(rid)
        except ValueError:
            print('Resource ID must be an integer')
            returnValue(None)
        uids = yield self.getEvents(txn, rid)

        # Print table of results
        table = tables.Table()
        table.addHeader(("Type", "UID", "Resource Name", "Resource ID",))
        for caltype, caluid, rname, rid in sorted(uids, key=lambda x: x[1]):
            table.addRow((
                caltype,
                caluid,
                rname,
                rid,
            ))

        print("\n")
        print("Calendar events (total=%d):\n" % (len(uids),))
        table.printTable()

    @inlineCallbacks
    def getEvents(self, txn, rid):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                co.ICALENDAR_TYPE,
                co.ICALENDAR_UID,
                co.RESOURCE_NAME,
                co.RESOURCE_ID,
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
            Where=(co.CALENDAR_RESOURCE_ID == Parameter("RID")),
        ).on(txn, **{"RID": rid}))
        returnValue(tuple(rows))


class EventDetails(Cmd):
    """
    Base class for common event details commands.
    """
    @inlineCallbacks
    def printEventDetails(self, txn, details):
        owner, calendar, resource_id, resource, created, modified, data = details
        shortname = yield UserNameFromUID(txn, owner)
        table = tables.Table()
        table.addRow(("Owner UID:", owner,))
        table.addRow(("User Name:", shortname,))
        table.addRow(("Calendar:", calendar,))
        table.addRow(("Resource Name:", resource))
        table.addRow(("Resource ID:", resource_id))
        table.addRow(("Created", created))
        table.addRow(("Modified", modified))
        print("\n")
        table.printTable()
        print(data)

    @inlineCallbacks
    def getEventData(self, txn, whereClause, whereParams):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        rows = (yield Select(
            [
                ch.OWNER_UID,
                cb.CALENDAR_RESOURCE_NAME,
                co.RESOURCE_ID,
                co.RESOURCE_NAME,
                co.CREATED,
                co.MODIFIED,
                co.ICALENDAR_TEXT,
            ],
            From=ch.join(
                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
                    cb.BIND_MODE == _BIND_MODE_OWN)).join(
                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID)),
            Where=whereClause,
        ).on(txn, **whereParams))
        returnValue(tuple(rows))


class Event(EventDetails):

    _name = "Get Event Data by Resource-ID"

    @inlineCallbacks
    def doIt(self, txn):

        rid = raw_input("Resource-ID: ")
        try:
            int(rid)
        except ValueError:
            print('Resource ID must be an integer')
            returnValue(None)
        result = yield self.getData(txn, rid)
        if result:
            yield self.printEventDetails(txn, result[0])
        else:
            print("Could not find resource")

    def getData(self, txn, rid):
        co = schema.CALENDAR_OBJECT
        return self.getEventData(txn, (co.RESOURCE_ID == Parameter("RID")), {"RID": rid})


class EventsByUID(EventDetails):

    _name = "Get Event Data by iCalendar UID"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("UID: ")
        rows = yield self.getData(txn, uid)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, uid):
        co = schema.CALENDAR_OBJECT
        return self.getEventData(txn, (co.ICALENDAR_UID == Parameter("UID")), {"UID": uid})


class EventsByName(EventDetails):

    _name = "Get Event Data by resource name"

    @inlineCallbacks
    def doIt(self, txn):

        name = raw_input("Resource Name: ")
        rows = yield self.getData(txn, name)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, name):
        co = schema.CALENDAR_OBJECT
        return self.getEventData(txn, (co.RESOURCE_NAME == Parameter("NAME")), {"NAME": name})


class EventsByOwner(EventDetails):

    _name = "Get Event Data by Owner UID/Short Name"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("Owner UID/Name: ")
        uid = yield UIDFromInput(txn, uid)
        rows = yield self.getData(txn, uid)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, uid):
        ch = schema.CALENDAR_HOME
        return self.getEventData(txn, (ch.OWNER_UID == Parameter("UID")), {"UID": uid})


class EventsByOwnerCalendar(EventDetails):

    _name = "Get Event Data by Owner UID/Short Name and calendar name"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("Owner UID/Name: ")
        uid = yield UIDFromInput(txn, uid)
        name = raw_input("Calendar resource name: ")
        rows = yield self.getData(txn, uid, name)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, uid, name):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        return self.getEventData(txn, (ch.OWNER_UID == Parameter("UID")).And(cb.CALENDAR_RESOURCE_NAME == Parameter("NAME")), {"UID": uid, "NAME": name})


class EventsByPath(EventDetails):

    _name = "Get Event Data by HTTP Path"

    @inlineCallbacks
    def doIt(self, txn):

        path = raw_input("Path: ")
        pathbits = path.split("/")
        if len(pathbits) != 6:
            print("Not a valid calendar object resource path")
            returnValue(None)
        homeName = pathbits[3]
        calendarName = pathbits[4]
        resourceName = pathbits[5]
        rows = yield self.getData(txn, homeName, calendarName, resourceName)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, homeName, calendarName, resourceName):
        ch = schema.CALENDAR_HOME
        cb = schema.CALENDAR_BIND
        co = schema.CALENDAR_OBJECT
        return self.getEventData(
            txn,
            (
                ch.OWNER_UID == Parameter("HOME")).And(
                cb.CALENDAR_RESOURCE_NAME == Parameter("CALENDAR")).And(
                co.RESOURCE_NAME == Parameter("RESOURCE")
            ),
            {"HOME": homeName, "CALENDAR": calendarName, "RESOURCE": resourceName}
        )


class EventsByContent(EventDetails):

    _name = "Get Event Data by Searching its Text Data"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("Search for: ")
        rows = yield self.getData(txn, uid)
        if rows:
            for result in rows:
                yield self.printEventDetails(txn, result)
        else:
            print("Could not find icalendar data")

    def getData(self, txn, text):
        co = schema.CALENDAR_OBJECT
        return self.getEventData(txn, (co.ICALENDAR_TEXT.Contains(Parameter("TEXT"))), {"TEXT": text})


class EventsInTimerange(Cmd):

    _name = "Get Event Data within a specified time range"

    @inlineCallbacks
    def doIt(self, txn):

        uid = raw_input("Owner UID/Name: ")
        start = raw_input("Start Time (UTC YYYYMMDDTHHMMSSZ or YYYYMMDD): ")
        if len(start) == 8:
            start += "T000000Z"
        end = raw_input("End Time (UTC YYYYMMDDTHHMMSSZ or YYYYMMDD): ")
        if len(end) == 8:
            end += "T000000Z"

        try:
            start = DateTime.parseText(start)
        except ValueError:
            print("Invalid start value")
            returnValue(None)
        try:
            end = DateTime.parseText(end)
        except ValueError:
            print("Invalid end value")
            returnValue(None)
        timerange = caldavxml.TimeRange(start=start.getText(), end=end.getText())

        home = yield txn.calendarHomeWithUID(uid)
        if home is None:
            print("Could not find calendar home")
            returnValue(None)

        yield self.eventsForEachCalendar(home, uid, timerange)

    @inlineCallbacks
    def eventsForEachCalendar(self, home, uid, timerange):

        calendars = yield home.calendars()
        for calendar in calendars:
            if calendar.name() == "inbox":
                continue
            yield self.eventsInTimeRange(calendar, uid, timerange)

    @inlineCallbacks
    def eventsInTimeRange(self, calendar, uid, timerange):

        # Create fake filter element to match time-range
        filter = caldavxml.Filter(
            caldavxml.ComponentFilter(
                caldavxml.ComponentFilter(
                    timerange,
                    name=("VEVENT",),
                ),
                name="VCALENDAR",
            )
        )
        filter = Filter(filter)
        filter.settimezone(None)

        matches = yield calendar.search(filter, useruid=uid, fbtype=False)
        if matches is None:
            returnValue(None)
        for name, _ignore_uid, _ignore_type in matches:
            event = yield calendar.calendarObjectWithName(name)
            ical_data = yield event.componentForUser()
            ical_data.stripStandardTimezones()

            table = tables.Table()
            table.addRow(("Calendar:", calendar.name(),))
            table.addRow(("Resource Name:", name))
            table.addRow(("Resource ID:", event._resourceID))
            table.addRow(("Created", event.created()))
            table.addRow(("Modified", event.modified()))
            print("\n")
            table.printTable()
            print(ical_data.getTextWithoutTimezones())


class Purge(Cmd):

    _name = "Purge all data from tables"

    @inlineCallbacks
    def doIt(self, txn):

        if raw_input("Do you really want to remove all data [y/n]: ")[0].lower() != 'y':
            print("No data removed")
            returnValue(None)

        wipeout = (
            # These are ordered in such a way as to ensure key constraints are not
            # violated as data is removed

            schema.RESOURCE_PROPERTY,

            schema.CALENDAR_OBJECT_REVISIONS,

            schema.CALENDAR,
            # schema.CALENDAR_BIND, - cascades
            # schema.CALENDAR_OBJECT, - cascades
            # schema.TIME_RANGE, - cascades
            # schema.TRANSPARENCY, - cascades


            schema.CALENDAR_HOME,
            # schema.CALENDAR_HOME_METADATA - cascades
            schema.ATTACHMENT,

            schema.ADDRESSBOOK_OBJECT_REVISIONS,

            schema.ADDRESSBOOK_HOME,
            # schema.ADDRESSBOOK_HOME_METADATA, - cascades
            # schema.ADDRESSBOOK_BIND, - cascades
            # schema.ADDRESSBOOK_OBJECT, - cascades

            schema.NOTIFICATION_HOME,
            schema.NOTIFICATION,
            # schema.NOTIFICATION_OBJECT_REVISIONS - cascades,
        )

        for tableschema in wipeout:
            yield self.removeTableData(txn, tableschema)
            print("Removed rows in table %s" % (tableschema,))

        if calendaruserproxy.ProxyDBService is not None:
            calendaruserproxy.ProxyDBService.clean()  # @UndefinedVariable
            print("Removed all proxies")
        else:
            print("No proxy database to clean.")

        fp = FilePath(config.AttachmentsRoot)
        if fp.exists():
            for child in fp.children():
                child.remove()
            print("Removed attachments.")
        else:
            print("No attachments path to delete.")

    @inlineCallbacks
    def removeTableData(self, txn, tableschema):
        yield Delete(
            From=tableschema,
            Where=None  # Deletes all rows
        ).on(txn)


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

    def __init__(self, store, options, reactor, config):
        super(DBInspectService, self).__init__(store)
        self.options = options
        self.reactor = reactor
        self.config = config
        self.commands = []
        self.commandMap = {}

    def doWork(self):
        """
        Start the service.
        """

        # Register commands
        self.registerCommand(TableSizes)
        self.registerCommand(CalendarHomes)
        self.registerCommand(CalendarHomesSummary)
        self.registerCommand(Calendars)
        self.registerCommand(CalendarsByOwner)
        self.registerCommand(Events)
        self.registerCommand(EventsByCalendar)
        self.registerCommand(Event)
        self.registerCommand(EventsByUID)
        self.registerCommand(EventsByName)
        self.registerCommand(EventsByOwner)
        self.registerCommand(EventsByOwnerCalendar)
        self.registerCommand(EventsByPath)
        self.registerCommand(EventsByContent)
        self.registerCommand(EventsInTimerange)
        self.doDBInspect()

        return succeed(None)

    def postStartService(self):
        """
        Don't quit right away
        """
        pass

    def registerCommand(self, cmd):
        self.commands.append(cmd.name())
        self.commandMap[cmd.name()] = cmd

    @inlineCallbacks
    def runCommandByPosition(self, position):
        try:
            yield self.runCommandByName(self.commands[position])
        except IndexError:
            print("Position %d not available" % (position,))
            returnValue(None)

    @inlineCallbacks
    def runCommandByName(self, name):
        try:
            yield self.runCommand(self.commandMap[name])
        except IndexError:
            print("Unknown command: '%s'" % (name,))

    @inlineCallbacks
    def runCommand(self, cmd):
        txn = self.store.newTransaction()
        try:
            yield cmd().doIt(txn)
            yield txn.commit()
        except Exception, e:
            traceback.print_exc()
            print("Command '%s' failed because of: %s" % (cmd.name(), e,))
            yield txn.abort()

    def printCommands(self):

        print("\n<---- Commands ---->")
        for ctr, name in enumerate(self.commands):
            print("%d. %s" % (ctr + 1, name,))
        if self.options["purging"]:
            print("P. Purge\n")
        print("Q. Quit\n")

    @inlineCallbacks
    def doDBInspect(self):
        """
        Poll for commands, stopping the reactor when done.
        """

        while True:
            self.printCommands()
            cmd = raw_input("Command: ")
            if cmd.lower() == 'q':
                break
            if self.options["purging"] and cmd.lower() == 'p':
                yield self.runCommand(Purge)
            else:
                try:
                    position = int(cmd)
                except ValueError:
                    print("Invalid command. Try again.\n")
                    continue

                yield self.runCommandByPosition(position - 1)

        self.reactor.stop()

    def stopService(self):
        """
        Stop the service.  Nothing to do; everything should be finished by this
        time.
        """
        # TODO: stopping this service mid-export should really stop the export
        # loop, but this is not implemented because nothing will actually do it
        # except hitting ^C (which also calls reactor.stop(), so that will exit
        # anyway).


def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
    """
    Do the export.
    """
    if reactor is None:
        from twisted.internet import reactor
    options = DBInspectOptions()
    options.parseOptions(argv[1:])

    def makeService(store):
        return DBInspectService(store, options, reactor, config)
    utilityMain(options['config'], makeService, reactor, verbose=options['debug'])

if __name__ == '__main__':
    main()
