# -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
##
# 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.
##

"""
Implements a directory-backed calendar hierarchy.
"""

__all__ = [
    "DirectoryCalendarHomeProvisioningResource",
    "DirectoryCalendarHomeTypeProvisioningResource",
    "DirectoryCalendarHomeUIDProvisioningResource",
    "DirectoryCalendarHomeResource",
]

from twext.python.log import Logger

from txweb2 import responsecode
from txweb2.auth.wrapper import UnauthorizedResponse
from txweb2.dav.util import joinURL
from txweb2.http import HTTPError, RedirectResponse
from txweb2.http_headers import ETag, MimeType

from twisted.internet.defer import succeed, inlineCallbacks, returnValue

from twistedcaldav.config import config
from twistedcaldav.directory.common import uidsResourceName, \
    CommonUIDProvisioningResource, CommonHomeTypeProvisioningResource
from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource, \
    DAVResourceWithChildrenMixin
from twistedcaldav.resource import CalendarHomeResource

from txdav.who.wiki import getWikiACL

from uuid import uuid4
import urllib


log = Logger()


# FIXME: copied from resource.py to avoid circular dependency


class CalDAVComplianceMixIn(object):

    def davComplianceClasses(self):
        return (
            tuple(super(CalDAVComplianceMixIn, self).davComplianceClasses()) +
            config.CalDAVComplianceClasses
        )


class DirectoryCalendarProvisioningResource (
    ReadOnlyResourceMixIn,
    CalDAVComplianceMixIn,
    DAVResourceWithChildrenMixin,
    DAVResource,
):

    def defaultAccessControlList(self):
        return succeed(config.ProvisioningResourceACL)

    def etag(self):
        return succeed(ETag(str(uuid4())))

    def contentType(self):
        return MimeType("httpd", "unix-directory")

    @inlineCallbacks
    def handleMissingTrailingSlash(self, request):
        try:
            _ignore_authnUser, authzUser = yield self.authenticate(request)
        except Exception:
            authzUser = None

        # Turn 301 into 401
        if authzUser is None:
            response = (yield UnauthorizedResponse.makeResponse(
                request.credentialFactories,
                request.remoteAddr
            ))
            returnValue(response)
        else:
            response = RedirectResponse(
                request.unparseURL(
                    path=urllib.quote(
                        urllib.unquote(request.path),
                        safe=':/') + '/'
                )
            )
            returnValue(response)


class DirectoryCalendarHomeProvisioningResource (DirectoryCalendarProvisioningResource):
    """
    Resource which provisions calendar home collections as needed.
    """

    def __init__(self, directory, url, store):
        """
        @param directory: an L{IDirectoryService} to provision calendars from.
        @param url: the canonical URL for the resource.
        """
        assert directory is not None
        assert url.endswith("/"), "Collection URL must end in '/'"

        super(DirectoryCalendarHomeProvisioningResource, self).__init__()

        # MOVE2WHO
        self.directory = directory  # IDirectoryService(directory)
        self._url = url
        self._newStore = store

        #
        # Create children
        #
        # ...just users, locations, and resources though.  If we iterate all of
        # the directory's recordTypes, we also get the proxy sub principal types
        # and other things which don't have calendars.

        self.supportedChildTypes = (
            self.directory.recordType.user,
            self.directory.recordType.location,
            self.directory.recordType.resource,
        )

        for recordType, recordTypeName in [
            (r, self.directory.recordTypeToOldName(r)) for r in
            self.supportedChildTypes
        ]:
            self.putChild(
                recordTypeName,
                DirectoryCalendarHomeTypeProvisioningResource(
                    self, recordTypeName, recordType
                )
            )

        self.putChild(uidsResourceName, DirectoryCalendarHomeUIDProvisioningResource(self))

    def url(self):
        return self._url

    def listChildren(self):
        return [
            self.directory.recordTypeToOldName(r) for r in
            self.supportedChildTypes
        ]

    def principalCollections(self):
        # FIXME: directory.principalCollection smells like a hack
        # See DirectoryPrincipalProvisioningResource.__init__()
        return self.directory.principalCollection.principalCollections()

    def principalForRecord(self, record):
        # FIXME: directory.principalCollection smells like a hack
        # See DirectoryPrincipalProvisioningResource.__init__()
        return self.directory.principalCollection.principalForRecord(record)

    @inlineCallbacks
    def homeForDirectoryRecord(self, record, request):
        uidResource = yield self.getChild(uidsResourceName)
        if uidResource is None:
            returnValue(None)
        else:
            returnValue((yield uidResource.homeResourceForRecord(record, request)))

    ##
    # DAV
    ##

    def isCollection(self):
        return True

    def displayName(self):
        return "calendars"


class DirectoryCalendarHomeTypeProvisioningResource(
    CommonHomeTypeProvisioningResource,
    DirectoryCalendarProvisioningResource
):
    """
    Resource which provisions calendar home collections of a specific
    record type as needed.
    """

    def __init__(self, parent, name, recordType):
        """
        @param parent: the parent of this resource
        @param recordType: the directory record type to provision.
        """
        assert parent is not None
        assert name is not None
        assert recordType is not None

        super(DirectoryCalendarHomeTypeProvisioningResource, self).__init__()

        self.directory = parent.directory
        self.name = name
        self.recordType = recordType
        self._parent = parent

    def url(self):
        return joinURL(self._parent.url(), self.name)

    @inlineCallbacks
    def listChildren(self):
        if config.EnablePrincipalListings:
            children = []
            for record in (yield self.directory.recordsWithRecordType(self.recordType)):
                if record.hasCalendars:
                    for shortName in record.shortNames:
                        children.append(shortName)
            returnValue(children)
        else:
            # Not a listable collection
            raise HTTPError(responsecode.FORBIDDEN)

    def makeChild(self, name):
        return None

    ##
    # DAV
    ##

    def isCollection(self):
        return True

    def displayName(self):
        return self.name

    ##
    # ACL
    ##

    def principalCollections(self):
        return self._parent.principalCollections()

    def principalForRecord(self, record):
        return self._parent.principalForRecord(record)


class DirectoryCalendarHomeUIDProvisioningResource (
    CommonUIDProvisioningResource,
    DirectoryCalendarProvisioningResource
):

    homeResourceTypeName = 'calendars'

    enabledAttribute = 'hasCalendars'

    def homeResourceCreator(self, record, transaction):
        return DirectoryCalendarHomeResource.createHomeResource(
            self, record, transaction)


class DirectoryCalendarHomeResource (CalendarHomeResource):
    """
    Calendar home collection resource.
    """

    @classmethod
    @inlineCallbacks
    def createHomeResource(cls, parent, record, transaction):
        self = yield super(DirectoryCalendarHomeResource, cls).createHomeResource(
            parent, record.uid, transaction)
        self.record = record
        returnValue(self)

    # Special ACLs for Wiki service
    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
        def gotACL(wikiACL):
            if wikiACL is not None:
                # ACL depends on wiki server...
                log.debug("Wiki ACL: {acl}", acl=wikiACL.toxml())
                return succeed(wikiACL)
            else:
                # ...otherwise permissions are fixed, and are not subject to
                # inheritance rules, etc.
                return self.defaultAccessControlList()

        d = getWikiACL(self, request)
        d.addCallback(gotACL)
        return d

    def principalForRecord(self):
        return self.parent.principalForRecord(self.record)
