from datetime import datetime
from lxml.etree import SubElement
from oaipmh import common, server, metadata, error, datestamp

from zope.interface import implements
from zope.component import queryAdapter
from zope.app import zapi
from zope.app.intid.interfaces import IIntIds
from zope.app.component.hooks import getSite
from hurry.workflow.interfaces import IWorkflowState
from hurry.query.interfaces import IQuery
from hurry import query
from hurry.query import set as set_query

from documentlibrary.core import flow
from documentlibrary.core import timezone
from documentlibrary.core.foitopic import vocabulary as foiVocabulary
from documentlibrary.core.category import vocabulary as categoryVocabulary
from documentlibrary.core.history import getUrl # could be moved to another module
from documentlibrary.core.interfaces import IOAIQuery, ICategoryContained
from documentlibrary.core.interfaces import IFOITopicContained
from documentlibrary.core.interfaces import IResponsibleDocumentUsers

PREFIXES = ('oai_dc', 'dl')

NS_DL = 'http://infrae.com/ns/documentlibrary'
NSMAP = {'dl': NS_DL}

def nsdl(name):
    return '{%s}%s' % (NS_DL, name)
        
def write(parent, name, value):
    element = SubElement(parent, nsdl(name), nsmap=NSMAP)
    element.text = value

class MetadataWriter:
    
    def __init__(self, show_private):
        self.show_private = show_private
        self._foi_vocabulary = None
        self._category_vocabulary = None
            
    def __call__(self, element, document):        
        doc_element = SubElement(element, nsdl('dl'), nsmap=NSMAP)
        doc_element.set(
            '{%s}schemaLocation' % server.NS_XSI, 
            '%s %s/dl.xsd' % (NS_DL, NS_DL))

        write(doc_element, 'title', document.title)
        write(doc_element, 'description', document.description)
        write(doc_element, 'versionstring', document.versionstring)
        write(doc_element, 'note', document.note)

        intids = zapi.getUtility(IIntIds, context=document)
        
        if self.show_private:
            write(doc_element, 'category', document.getCategoryName())
    
            chain = []
            obj = document.getCategory()
            while ICategoryContained.providedBy(obj):
                chain.insert(0, intids.getId(obj))
                obj = obj.__parent__

            cat_element = SubElement(
                doc_element, nsdl('categories'), nsmap=NSMAP)   
            for id in chain:
                cat_element = SubElement(
                    cat_element, nsdl('category'), nsmap=NSMAP)
                term = categoryVocabulary(None).getTerm(id)
                cat_element.text = '%s %s' % (
                    '.'.join([str(i+1) for i in term.sequence]), term.title)
        
        write(doc_element, 'foi_topic', document.getFoiTopicName())
        
        if self.show_private:
            topic_ids = []
            obj = document.getFoiTopic()
            while IFOITopicContained.providedBy(obj):
                topic_ids.insert(0, intids.getId(obj))
                obj = obj.__parent__

            topic_element = SubElement(
                doc_element, nsdl('foi_topics'), nsmap=NSMAP)   
            for id in topic_ids:
                topic_element = SubElement(
                    topic_element, nsdl('foi_topic'), nsmap=NSMAP)
                term = foiVocabulary(None).getTerm(id)
                topic_element.text = '%s %s' % (
                    '.'.join([str(i+1) for i in term.sequence]), term.title)
        
        write(
            doc_element, 'modificationdate', datestamp.date_to_datestamp(
                timezone.toNaiveUTC(document.modificationdate)))
        write(
            doc_element, 'availabledate', datestamp.date_to_datestamp(
                timezone.toNaiveUTC(document.availabledate)))
        write(
            doc_element, 'expirydate', datestamp.date_to_datestamp(
                timezone.toNaiveUTC(document.expirydate)))
        write(
            doc_element, 'last_changed', datestamp.datetime_to_datestamp(
                timezone.toNaiveUTC(document.last_changed)))

        authors_element = SubElement(
            doc_element, nsdl('authors'), nsmap=NSMAP)
        for firstname, lastname, emailaddress in document.authors:
            author_element = SubElement(
                authors_element, nsdl('author'), nsmap=NSMAP)
            write(author_element, 'firstname', firstname)
            write(author_element, 'lastname', lastname)
            write(author_element, 'emailaddress', emailaddress)
            write(
                author_element, 'display_name', 
                '%s %s' % (firstname, lastname))

        handle_url = '%s/handle/%s' % (
            getUrl(getSite()), document.handle_id)
        write(doc_element, 'handle', handle_url)
        
        urls_element = SubElement(
            doc_element, nsdl('file_urls'), nsmap=NSMAP)
        if document.file_available:
            write(
                urls_element, 'file_url', 
                '%s/%s' % (handle_url, document.file.filename))
        if document.pdf:
            write(
                urls_element, 'pdf_url', 
                '%s/%s' % (handle_url, document.pdf.filename))
        if document.plaintext:
            write(
                urls_element, 'plaintext_url', 
                '%s/%s' % (handle_url, document.plaintext.filename))

        if self.show_private:
            groups_element = SubElement(
                doc_element, nsdl('groups'), nsmap=NSMAP)
            for name in document.access:
                write(groups_element, 'group', name)
            responsibles = IResponsibleDocumentUsers(
                document).getAllResponsible()
            for name in responsibles:
                write(groups_element, 'group', name)

privateMetadataWriter = MetadataWriter(show_private=True)
publicMetadataWriter = MetadataWriter(show_private=False)
        
def _inUTC(dt):
    if dt is None:
        return None
    return timezone.inUTC(dt)

class PrivateOAIQuery(object):
    
    implements(IOAIQuery)
    
    def __init__(self, context):
        self.context = context

    def getDocumentsForIdentifier(self, identifier):
        return self._getDocumentsForIdentifierHelper(
            identifier, [flow.AVAILABLE, flow.ALERTED])

    def getDeletedDocumentsForIdentifier(self, identifier):
        return self._getDocumentsForIdentifierHelper(
            identifier, [flow.EXPIRED, flow.DELETED])

    def _getDocumentsForIdentifierHelper(self, identifier, states):
        q = zapi.getUtility(IQuery)
        result = q.searchResults(
            query.In(('document_catalog', 'workflow_state'), states) &
            query.Eq(('document_catalog', 'workflow_id'), identifier))
        return self._removeSecurity(result)
    
    def getDocumentsInRange(self, from_, until):
        return self._getDocumentsInRangeHelper(
            _inUTC(from_), _inUTC(until), [flow.AVAILABLE, flow.ALERTED])

    def getDeletedDocumentsInRange(self, from_, until):
        return self._getDocumentsInRangeHelper(
            _inUTC(from_), _inUTC(until), [flow.EXPIRED, flow.DELETED])

    def _getDocumentsInRangeHelper(self, from_, until, states):
        q = zapi.getUtility(IQuery)
        result = q.searchResults(
            query.In(('document_catalog', 'workflow_state'), states) &
            query.Between(
                ('document_catalog', 'last_changed'), from_, until))
        return self._removeSecurity(result)
    
    def _removeSecurity(self, documents):
        # XXX okay, we have to do this to make this public.. ugh..
        from zope.security.proxy import removeSecurityProxy
        for document in documents:
            yield removeSecurityProxy(document)

class PublicOAIQuery(PrivateOAIQuery):
    
    def _getDocumentsForIdentifierHelper(self, identifier, states):
        q = zapi.getUtility(IQuery)
        result = q.searchResults(
            query.In(('document_catalog', 'workflow_state'),states) &
            query.Eq(('document_catalog', 'workflow_id'), identifier) &
            set_query.AnyOf(
                ('document_catalog', 'access'), ['zope.Everybody'])
            )
        return self._removeSecurity(result)

    def _getDocumentsInRangeHelper(self, from_, until, states):
        q = zapi.getUtility(IQuery)
        result = q.searchResults(
            query.In(
                ('document_catalog', 'workflow_state'), states) &
            query.Between(
                ('document_catalog', 'last_changed'), from_, until) &
            set_query.AnyOf(
                ('document_catalog', 'access'), ['zope.Everybody'])
            )
        return self._removeSecurity(result)
    
class DocumentLibraryOAI:
    
    # implements(interfaces.IOAI) # Not a Zope-3 interface (yet)

    def __init__(self, context, baseURL, show_private):
        self.context = context
        self.baseURL = baseURL
        self.show_private = show_private

    def _getQuery(self):
        if self.show_private:
            return queryAdapter(self.context, IOAIQuery, 'private_query')
        return queryAdapter(self.context, IOAIQuery, 'public_query')
    
    def getRecord(self, metadataPrefix, identifier):
        """Get a record for a metadataPrefix and identifier.

        metadataPrefix - identifies metadata set to retrieve
        identifier - repository-unique identifier of record
        
        Should raise error.CannotDisseminateFormatError if
        metadataPrefix is unknown or not supported by identifier.
        
        Should raise error.IdDoesNotExistError if identifier is
        unknown or illegal.

        Returns a header, metadata, about tuple describing the record.
        """
        if metadataPrefix not in PREFIXES:
            raise error.CannotDisseminateFormatError
    
        try:
            identifier = int(identifier)
        except ValueError:
            raise error.IdDoesNotExistError, identifier

        oai_query = self._getQuery()
        results = oai_query.getDocumentsForIdentifier(identifier)
        
        results = list(results)
        is_deleted = False
        if not len(results):
            results = oai_query.getDeletedDocumentsForIdentifier(
                identifier)
            if len(results):
                is_deleted = True
            else:
                raise error.IdDoesNotExistError, (
                    'Unknown identifier: %s' % identifier)

        document = results[0]
        
        stamp = timezone.toNaiveUTC(document.last_changed)
        header = common.Header(str(identifier), stamp, [], is_deleted)
        if not is_deleted:
            metadata = self._getMetadata(metadataPrefix, document)
        else:
            metadata = None
        return header, metadata, None

    def identify(self):
        """Retrieve information about the repository.

        Returns an Identify object describing the repository.
        """
        # XXX should retrieve configuration information from
        # settings
        return common.Identify(
            repositoryName='University of Luton FOI Document Library',
            baseURL=self.baseURL,
            protocolVersion="2.0",
            adminEmails=['faassen@infrae.com'],
            earliestDatestamp=datetime(2004, 1, 1),
            deletedRecord='transient',
            granularity='YYYY-MM-DDThh:mm:ssZ',
            compression=['identity'])
    
    def listMetadataFormats(self, identifier=None):
        """List metadata formats supported by repository or record.

        identifier - identify record for which we want to know all
                     supported metadata formats. if absent, list all metadata
                     formats supported by repository. (optional)

        Should raise error.IdDoesNotExistError if record with
        identifier does not exist.
        
        Should raise error.NoMetadataFormatsError if no formats are
        available for the indicated record.

        Returns an iterable of metadataPrefix, schema, metadataNamespace
        tuples (each entry in the tuple is a string).
        """
        return [
            ('oai_dc', 
            'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', 
            'http://www.openarchives.org/OAI/2.0/oai_dc/'),
            ('dl', '%s/dl.xsd' % NS_DL, NS_DL)
            ]

    def listIdentifiers(self, metadataPrefix, set=None,
                        from_=None, until=None):
        """Get a list of header information on records.

        metadataPrefix - identifies metadata set to retrieve
        set - set identifier; only return headers in set (optional)
        from_ - only retrieve headers from from_ date forward (optional)
        until - only retrieve headers with dates up to and including
                until date (optional)

        Should raise error.CannotDisseminateFormatError if metadataPrefix
        is not supported by the repository.

        Should raise error.NoSetHierarchyError if the repository does not
        support sets.
        
        Returns an iterable of headers.
        """
        for identifier, document, deleted in self._list(
            metadataPrefix, set, from_, until):
            yield common.Header(
                identifier, timezone.toNaiveUTC(document.last_changed), 
                setspec=[], deleted=deleted)

    def listRecords(self, metadataPrefix, set=None, from_=None, until=None):
        """Get a list of header, metadata and about information on records.

        metadataPrefix - identifies metadata set to retrieve
        set - set identifier; only return records in set (optional)
        from_ - only retrieve records from from_ date forward (optional)
        until - only retrieve records with dates up to and including
                until date (optional)

        Should raise error.CannotDisseminateFormatError if metadataPrefix
        is not supported by the repository.

        Should raise error.NoSetHierarchyError if the repository does not
        support sets.

        Returns an iterable of header, metadata, about tuples.
        """
        for identifier, document, deleted in self._list(
            metadataPrefix, set, from_, until):
            header = common.Header(
                identifier, timezone.toNaiveUTC(document.last_changed),
                setspec=[], deleted=deleted)
            if not deleted:
                metadata = self._getMetadata(metadataPrefix, document)
            else:
                metadata = None
            yield header, metadata, None

    def _list(self, metadataPrefix, set=None, from_=None, until=None):
        if set is not None:
            raise error.NoSetHierarchyError
        
        if metadataPrefix not in PREFIXES:
            raise error.CannotDisseminateFormatError

        oai_query = self._getQuery()

        # first expose available/alerted documents
        available_identifiers = {}
        for document in oai_query.getDocumentsInRange(from_, until):
            identifier = str(IWorkflowState(document).getId())
            available_identifiers[identifier] = None
            yield identifier, document, False
    
        # now expose any expired/deleted records
        for document in oai_query.getDeletedDocumentsInRange(from_, until):
            identifier = str(IWorkflowState(document).getId())
            # if there's already an available/alerted version,
            # we don't want to expose this one as deleted! This is
            # not possible for deleted, but is possible for expired
            # documents
            if identifier in available_identifiers:
                continue
            yield identifier, document, True

    def listSets(self):
        """Get a list of sets in the repository.

        Should raise error.NoSetHierarchyError if the repository does not
        support sets.

        Returns an iterable of setSpec, setName tuples (strings).
        """
        raise error.NoSetHierarchyError
    
    def _getMetadata(self, prefix, document):
        if prefix not in PREFIXES:
            raise error.CannotDisseminateFormatError
        
        if prefix == 'oai_dc':
            return common.Metadata({'title': [document.title]})
        if prefix == 'dl':
            return document # metadataWriter knows how to deal with documents
