#!/usr/bin/env python
# -*- test-case-name: calendarserver.tools.test.test_trash -*-
##
# Copyright (c) 2015-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

from argparse import ArgumentParser, RawTextHelpFormatter
import datetime
from calendarserver.tools.cmdline import utilityMain, WorkerService
from calendarserver.tools.util import prettyRecord, displayNameForCollection, agoString, locationString
from pycalendar.datetime import DateTime, Timezone
from twext.python.log import Logger
from twisted.internet.defer import inlineCallbacks, returnValue

log = Logger()


class TrashRestorationService(WorkerService):

    operation = None
    operationArgs = []

    def doWork(self):
        return self.operation(self.store, *self.operationArgs)


def main():

    parser = ArgumentParser(
        formatter_class=RawTextHelpFormatter,
        description="List or restore deleted events / collections from trash",
        epilog="""
Examples:
* List deleted collections (and their resource IDs) for a given user 
by UID:
    calendarserver_trash -p 10000000-0000-0000-0000-000000000001 -c
* Restore a deleted collection by resource ID (use -c / -e to find resource ID)
for the same user:
    calendarserver_trash -p 10000000-0000-0000-0000-000000000001 -c -r <rid>
        """)
    parser.add_argument('-f', '--config', dest='configFileName', metavar='CONFIGFILE', help='caldavd.plist configuration file path')
    parser.add_argument('-d', '--debug', action='store_true', help='show debug logging')
    parser.add_argument('-p', '--principal', dest='principal', help='the principal to use (uid)')
    parser.add_argument('-e', '--events', action='store_true', help='list or restore trashed events')
    parser.add_argument('-c', '--collections', action='store_true', help='list or restore trashed collections for principal (uid)')
    parser.add_argument('-r', '--recover', dest='resourceID', type=int,
        help='recover trashed collection or event (by resource ID).\nWithout this option, items are listed but not recovered.')
    parser.add_argument('--empty', action='store_true', help='empty the principal\'s trash')
    parser.add_argument('--days', type=int, default=0, help='number of past days to retain')

    args = parser.parse_args()

    if not args.principal:
        print("--principal missing")
        return

    if args.empty:
        operation = emptyTrashForPrincipal
        operationArgs = [args.principal, args.days]
    elif args.collections:
        if args.resourceID:
            operation = restoreTrashedCollection
            operationArgs = [args.principal, args.resourceID]
        else:
            operation = listTrashedCollectionsForPrincipal
            operationArgs = [args.principal]
    elif args.events:
        if args.resourceID:
            operation = restoreTrashedEvent
            operationArgs = [args.principal, args.resourceID]
        else:
            operation = listTrashedEventsForPrincipal
            operationArgs = [args.principal]
    else:
        operation = listTrashedCollectionsForPrincipal
        operationArgs = [args.principal]

    TrashRestorationService.operation = operation
    TrashRestorationService.operationArgs = operationArgs

    utilityMain(
        args.configFileName,
        TrashRestorationService,
        verbose=args.debug,
        loadTimezones=True
    )


@inlineCallbacks
def listTrashedCollectionsForPrincipal(service, store, principalUID):
    directory = store.directoryService()
    record = yield directory.recordWithUID(principalUID)
    if record is None:
        print("No record found for:", principalUID)
        returnValue(None)

    @inlineCallbacks
    def doIt(txn):
        home = yield txn.calendarHomeWithUID(principalUID)
        if home is None:
            print("No home for principal")
            returnValue(None)

        trash = yield home.getTrash()
        if trash is None:
            print("No trash available")
            returnValue(None)

        trashedCollections = yield home.children(onlyInTrash=True)
        if len(trashedCollections) == 0:
            print("No trashed collections for:", prettyRecord(record))
            returnValue(None)

        print("Trashed collections for:", prettyRecord(record))

        nowDT = datetime.datetime.utcnow()

        for collection in trashedCollections:
            displayName = displayNameForCollection(collection)
            whenTrashed = collection.whenTrashed()
            ago = nowDT - whenTrashed
            print()
            print("   Trashed {}:".format(agoString(ago)))
            print(
                "      \"{}\" (collection)  Recovery ID = {}".format(
                    displayName.encode("utf-8"), collection._resourceID
                )
            )
            startTime = whenTrashed - datetime.timedelta(minutes=5)
            children = yield trash.trashForCollection(
                collection._resourceID, start=startTime
            )
            print("         ...containing events:")
            for child in children:
                component = yield child.component()
                summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
                print("            \"{}\"".format(summary))

    yield store.inTransaction(label="List trashed collections", operation=doIt)


def startString(pydt):
    return pydt.getLocaleDateTime(DateTime.FULLDATE, False, True, pydt.getTimezoneID())


@inlineCallbacks
def printEventDetails(event):
    nowPyDT = DateTime.getNowUTC()
    nowDT = datetime.datetime.utcnow()
    oneYearInFuture = DateTime.getNowUTC()
    oneYearInFuture.offsetDay(365)

    component = yield event.component()
    mainSummary = component.mainComponent().propertyValue("SUMMARY", u"<no title>")
    whenTrashed = event.whenTrashed()
    ago = nowDT - whenTrashed
    print("   Trashed {}:".format(agoString(ago)))

    if component.isRecurring():
        print(
            "      \"{}\" (repeating)  Recovery ID = {}".format(
                mainSummary, event._resourceID
            )
        )
        print("         ...upcoming instances:")
        instances = component.cacheExpandedTimeRanges(oneYearInFuture)
        instances = sorted(instances.instances.values(), key=lambda x: x.start)
        limit = 3
        count = 0
        for instance in instances:
            if instance.start >= nowPyDT:
                summary = instance.component.propertyValue("SUMMARY", u"<no title>")
                location = locationString(instance.component)
                tzid = instance.component.getProperty("DTSTART").parameterValue("TZID", None)
                dtstart = instance.start
                if tzid is not None:
                    timezone = Timezone(tzid=tzid)
                    dtstart.adjustTimezone(timezone)
                print("            \"{}\" {} {}".format(summary, startString(dtstart), location))
                count += 1
                limit -= 1
            if limit == 0:
                break
        if not count:
            print("            (none)")

    else:
        print(
            "      \"{}\" (non-repeating)  Recovery ID = {}".format(
                mainSummary, event._resourceID
            )
        )
        dtstart = component.mainComponent().propertyValue("DTSTART")
        location = locationString(component.mainComponent())
        print("         {} {}".format(startString(dtstart), location))


@inlineCallbacks
def listTrashedEventsForPrincipal(service, store, principalUID):
    directory = store.directoryService()
    record = yield directory.recordWithUID(principalUID)
    if record is None:
        print("No record found for:", principalUID)
        returnValue(None)

    @inlineCallbacks
    def doIt(txn):
        home = yield txn.calendarHomeWithUID(principalUID)
        if home is None:
            print("No home for principal")
            returnValue(None)

        trash = yield home.getTrash()
        if trash is None:
            print("No trash available")
            returnValue(None)

        untrashedCollections = yield home.children(onlyInTrash=False)
        if len(untrashedCollections) == 0:
            print("No untrashed collections for:", prettyRecord(record))
            returnValue(None)

        for collection in untrashedCollections:
            displayName = displayNameForCollection(collection)
            children = yield trash.trashForCollection(collection._resourceID)
            if len(children) == 0:
                continue

            print("Trashed events in calendar \"{}\":".format(displayName.encode("utf-8")))
            for child in children:
                print()
                yield printEventDetails(child)
            print("")

    yield store.inTransaction(label="List trashed events", operation=doIt)


@inlineCallbacks
def restoreTrashedCollection(service, store, principalUID, resourceID):
    directory = store.directoryService()
    record = yield directory.recordWithUID(principalUID)
    if record is None:
        print("No record found for:", principalUID)
        returnValue(None)

    @inlineCallbacks
    def doIt(txn):
        home = yield txn.calendarHomeWithUID(principalUID)
        if home is None:
            print("No home for principal")
            returnValue(None)

        collection = yield home.childWithID(resourceID, onlyInTrash=True)
        if collection is None:
            print("Collection {} is not in the trash".format(resourceID))
            returnValue(None)

        yield collection.fromTrash(
            restoreChildren=True, delta=datetime.timedelta(minutes=5), verbose=True
        )

    yield store.inTransaction(label="Restore trashed collection", operation=doIt)


@inlineCallbacks
def restoreTrashedEvent(service, store, principalUID, resourceID):
    directory = store.directoryService()
    record = yield directory.recordWithUID(principalUID)
    if record is None:
        print("No record found for:", principalUID)
        returnValue(None)

    @inlineCallbacks
    def doIt(txn):
        home = yield txn.calendarHomeWithUID(principalUID)
        if home is None:
            print("No home for principal")
            returnValue(None)

        trash = yield home.getTrash()
        if trash is None:
            print("No trash available")
            returnValue(None)

        child = yield trash.objectResourceWithID(resourceID)
        if child is None:
            print("Event not found")
            returnValue(None)

        component = yield child.component()
        summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
        print("Restoring \"{}\"".format(summary))
        yield child.fromTrash()

    yield store.inTransaction(label="Restore trashed event", operation=doIt)


@inlineCallbacks
def emptyTrashForPrincipal(service, store, principalUID, days, txn=None, verbose=True):
    directory = store.directoryService()
    record = yield directory.recordWithUID(principalUID)
    if record is None:
        if verbose:
            print("No record found for:", principalUID)
        returnValue(None)

    @inlineCallbacks
    def doIt(txn):
        home = yield txn.calendarHomeWithUID(principalUID)
        if home is None:
            if verbose:
                print("No home for principal")
            returnValue(None)

        yield home.emptyTrash(days=days, verbose=verbose)

    if txn is None:
        yield store.inTransaction(label="Empty trash", operation=doIt)
    else:
        yield doIt(txn)


if __name__ == "__main__":
    main()
