# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
from datetime import datetime
from lxml import etree

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.utils import timezone
from oaipmh import server, common, metadata, error

from api.handlers import WL_BASE
from api.models import Deleted
from catalogue.models import Book
from librarian import WLURI


def make_time_naive(d):
    return timezone.localtime(d).replace(tzinfo=None)

WL_DC_READER_XPATH = '(.|*)/rdf:RDF/rdf:Description/%s/text()'
wl_dc_reader = metadata.MetadataReader(
    fields={
        'title':       ('textList', WL_DC_READER_XPATH % 'dc:title'),
        'creator':     ('textList', WL_DC_READER_XPATH % 'dc:creator'),
        'subject':     ('textList', (WL_DC_READER_XPATH + ' | ' + WL_DC_READER_XPATH + ' | ' + WL_DC_READER_XPATH) %
                        ('dc:subject.period', 'dc:subject.type', 'dc:subject.genre')),
        'description': ('textList', WL_DC_READER_XPATH % 'dc:description'),
        'publisher':   ('textList', WL_DC_READER_XPATH % 'dc:publisher'),
        'contributor': ('textList', (WL_DC_READER_XPATH + ' | ' + WL_DC_READER_XPATH + ' | ' + WL_DC_READER_XPATH) %
                        ('dc:contributor.editor', 'dc:contributor.translator', 'dc:contributor.technical_editor')),
        'date':        ('textList', WL_DC_READER_XPATH % 'dc:date'),
        'type':        ('textList', WL_DC_READER_XPATH % 'dc:type'),
        'format':      ('textList', WL_DC_READER_XPATH % 'dc:format'),
        'identifier':  ('textList', WL_DC_READER_XPATH % 'dc:identifier.url'),
        'source':      ('textList', WL_DC_READER_XPATH % 'dc:source'),
        'language':    ('textList', WL_DC_READER_XPATH % 'dc:language'),
        # 'isPartOf':    ('textList', 'rdf:RDF/rdf:Description/dc:relation.isPartOf/text()'),
        'hasPart':     ('textList', WL_DC_READER_XPATH % 'dc:relation.hasPart'),
        # 'relation':    ('textList', 'rdf:RDF/rdf:Description/dc:relation/text()'),
        # 'coverage':    ('textList', 'rdf:RDF/rdf:Description/dc:coverage/text()'),
        'rights':      ('textList', WL_DC_READER_XPATH % 'dc:rights')
    },
    namespaces={
        'dc': 'http://purl.org/dc/elements/1.1/',
        'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'}
    )


NS_DCTERMS = "http://purl.org/dc/terms/"


def nsdcterms(name):
    return '{%s}%s' % (NS_DCTERMS, name)


class Catalogue(common.ResumptionOAIPMH):
    TAG_CATEGORIES = ['author', 'epoch', 'kind', 'genre']

    def __init__(self, metadata_registry):
        super(Catalogue, self).__init__()
        self.metadata_registry = metadata_registry

        self.oai_id = "oai:" + Site.objects.get_current().domain + ":%s"

        # earliest change
        year_zero = timezone.make_aware(datetime(1990, 1, 1, 0, 0, 0), timezone.utc)

        try:
            earliest_change = Book.objects.filter(findable=True, preview=False).order_by('changed_at')[0].changed_at
        except IndexError:
            earliest_change = year_zero

        try:
            earliest_delete = \
                Deleted.objects.exclude(slug__exact='').order_by('deleted_at')[0].deleted_at
        except IndexError:
            earliest_delete = year_zero

        self.earliest_datestamp = earliest_change if earliest_change <= earliest_delete else earliest_delete

    @staticmethod
    def metadata(book):
        try:
            xml = etree.parse(book.xml_file)
        finally:
            book.xml_file.close()
        md = wl_dc_reader(xml)
        m = md.getMap()
        if book.parent:
            m['isPartOf'] = [str(WLURI(book.parent.slug))]
        return m

    def record_for_book(self, book, headers_only=False):
        meta = None
        identifier = self.slug_to_identifier(book.slug)
        if isinstance(book, Book):
            # setSpec = map(self.tag_to_setspec, book.tags.filter(category__in=self.TAG_CATEGORIES))
            header = common.Header(None, identifier, make_time_naive(book.changed_at), [], False)
            if not headers_only:
                meta = common.Metadata(None, self.metadata(book))
            about = None
        elif isinstance(book, Deleted):
            header = common.Header(None, identifier, make_time_naive(book.deleted_at), [], True)
            if not headers_only:
                meta = common.Metadata(None, {})
            about = None
        else:
            raise TypeError('Unknown book class')
        if headers_only:
            return header
        return header, meta, about

    def identify(self, **kw):
        ident = common.Identify(
            'Wolne Lektury',  # generate
            '%s/oaipmh' % str(WL_BASE),  # generate
            '2.0',  # version
            [m[1] for m in settings.MANAGERS],  # adminEmails
            make_time_naive(self.earliest_datestamp),  # earliest datestamp of any change
            'persistent',  # deletedRecord
            'YYYY-MM-DDThh:mm:ssZ',  # granularity
            ['identity'],  # compression
            []  # descriptions
            )
        return ident

    def books(self, tag, from_, until):
        if tag:
            # we do not support sets, since they are problematic for deleted books.
            raise error.NoSetHierarchyError("Wolne Lektury does not support sets.")
            # books = Book.tagged.with_all([tag])
        else:
            books = Book.objects.filter(findable=True, preview=False)
        deleted = Deleted.objects.exclude(slug__exact='')

        books = books.order_by('changed_at')
        deleted = deleted.order_by('deleted_at')
        if from_:
            books = books.filter(changed_at__gte=from_)
            deleted = deleted.filter(deleted_at__gte=from_)
        if until:
            books = books.filter(changed_at__lte=until)
            deleted = deleted.filter(deleted_at__lte=until)
        return list(books) + list(deleted)

    # @staticmethod
    # def tag_to_setspec(tag):
    #     return "%s:%s" % (tag.category, tag.slug)

    # @staticmethod
    # def setspec_to_tag(s):
    #     if not s: return None
    #     cs = s.split(':')
    #     if len(cs) == 2:
    #         if not cs[0] in Catalogue.TAG_CATEGORIES:
    #             raise error.NoSetHierarchyError("No category part in set")
    #         tag = Tag.objects.get(slug=cs[1], category=cs[0])
    #         return tag
    #     raise error.NoSetHierarchyError("Setspec should have two components: category:slug")

    def getRecord(self, **kw):
        """Returns (header, metadata, about) for given record."""
        slug = self.identifier_to_slug(kw['identifier'])
        try:
            book = Book.objects.get(slug=slug)
            return self.record_for_book(book)
        except Book.DoesNotExist:
            book_type = ContentType.objects.get_for_model(Book)
            try:
                deleted_book = Deleted.objects.get(content_type=book_type, slug=slug)
            except:
                raise error.IdDoesNotExistError("No item for this identifier")
            return self.record_for_book(deleted_book)

    def validate_kw(self, kw):
        if 'resumptionToken' in kw:
            raise error.BadResumptionTokenError("No resumption token support at this point")
        if 'metadataPrefix' in kw and not self.metadata_registry.hasWriter(kw['metadataPrefix']):
            raise error.CannotDisseminateFormatError("This format is not supported")

    def identifier_to_slug(self, ident):
        return ident.split(':')[-1]

    def slug_to_identifier(self, slug):
        return self.oai_id % slug

    def listIdentifiers(self, **kw):
        self.validate_kw(kw)
        records = [self.record_for_book(book, headers_only=True) for
                   book in self.books(None, kw.get('from_'), kw.get('until'))]
        return records, None

    def listRecords(self, **kw):
        """
            can get a resumptionToken kw.
            returns result, token
        """
        self.validate_kw(kw)
        records = [self.record_for_book(book) for
                   book in self.books(None,
                           kw.get('from_', None),
                           kw.get('until', None))]

        return records, None

    def listMetadataFormats(self, **kw):
        formats = [
            ('oai_dc',
             'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
             server.NS_OAIDC),
            ('qdc',
             'http://dublincore.org/schemas/xmls/qdc/2006/01/06/dcterms.xsd',
             NS_DCTERMS)]
        if 'identifier' in kw:
            slug = self.identifier_to_slug(kw['identifier'])
            try:
                Book.objects.get(slug=slug)
                return formats
            except Book.DoesNotExist:
                try:
                    Deleted.objects.get(slug=slug)
                    return []
                except Deleted.DoesNotExist:
                    raise error.IdDoesNotExistError("This id does not exist")
        else:
            return formats

    def listSets(self, **kw):
        raise error.NoSetHierarchyError("Wolne Lektury does not support sets.")
        # tags = []
        # for category in Catalogue.TAG_CATEGORIES:
        #     for tag in Tag.objects.filter(category=category):
        #         tags.append(("%s:%s" % (tag.category, tag.slug),
        #                      tag.name,
        #                      tag.description))
        # return tags, None
