"""
Module which handles all the routing mechanics to convert UnroutedNotifications into either
RoutedNotifications or FailedNotifications
"""

from octopus.lib import dates
from octopus.modules.store import store
from service import packages, models
import esprit
from service.web import app
from flask import url_for
from copy import deepcopy
import uuid

class RoutingException(Exception):
    """
    Generic exception to be raised when errors with routing are encountered
    """
    pass

def route(unrouted):
    """
    Route an UnroutedNotification to the appropriate repositories

    The function will extract all the metadata and match data from any binary content associated with
    the notification, and in combination from match data taken from the notification metadata itself will
    determine if there is a RepositoryConfig whose criteria it matches.

    If there is a match to one or more of the criteria, MatchProvenance objects will be created for
    each matching repository, and persisted for later inspection.

    If one or more repositories are matched, a RoutedNotification will be created and enhanced with any
    metadata extracted from the associated package (if present), then persisted.

    If no repositories match, a FailedNotification will be created and enhanced with any
    metadata extracted from the associated package (if present), then persisted.

    :param unrouted: an UnroutedNotification object
    :return: True if the notification was routed to a repository, False if there were no matches
    """
    app.logger.debug(u"Routing - Notification:{y}".format(y=unrouted.id))

    # first get the packaging system to load and retrieve all the metadata
    # and match data from the content file (if it exists)
    try:
        metadata, pmd = packages.PackageManager.extract(unrouted.id, unrouted.packaging_format)
    except packages.PackageException as e:
        app.logger.debug(u"Routing - Notification:{y} failed with error '{x}'".format(y=unrouted.id, x=e.message))
        raise RoutingException(e.message)

    # extract the match data from the notification and combine it with the match data from the package
    match_data = unrouted.match_data()
    if pmd is not None:
        match_data.merge(pmd)

    # iterate through all the repository configs, collecting match provenance and
    # id information
    # FIXME: at the moment this puts all the provenance in memory and then writes it all
    # in one go later.  Probably that's OK, but it will depend on the number of fields the
    # repository matches and the number of repositories as to how big this gets.
    match_provenance = []
    match_ids = []
    try:
        for rc in models.RepositoryConfig.scroll(page_size=10, keepalive="1m"):
            prov = models.MatchProvenance()
            prov.repository = rc.repository
            prov.notification = unrouted.id
            app.logger.debug(u"Routing - Notification:{y} matching against Repository:{x}".format(y=unrouted.id, x=rc.repository))
            match(match_data, rc, prov)
            if len(prov.provenance) > 0:
                match_provenance.append(prov)
                match_ids.append(rc.repository)
                app.logger.debug(u"Routing - Notification:{y} successfully matched Repository:{x}".format(y=unrouted.id, x=rc.repository))
            else:
                app.logger.debug(u"Routing - Notification:{y} did not match Repository:{x}".format(y=unrouted.id, x=rc.repository))

    except esprit.tasks.ScrollException as e:
        app.logger.error(u"Routing - Notification:{y} failed with error '{x}'".format(y=unrouted.id, x=e.message))
        raise RoutingException(e.message)

    app.logger.debug(u"Routing - Notification:{y} matched to {x} repositories".format(y=unrouted.id, x=len(match_ids)))

    # write all the match provenance out to the index (could be an empty list)
    for p in match_provenance:
        p.save()
        app.logger.debug(u"Routing - Provenance:{z} written for Notification:{y} for match to Repisitory:{x}".format(x=p.repository, y=unrouted.id, z=p.id))

    # if there are matches then the routing is successful, and we want to finalise the
    # notification for the routed index and its content for download
    if len(match_ids) > 0:
        # repackage the content that came with the unrouted notification (if necessary) into
        # the formats required by the repositories for which there was a match
        pack_links = repackage(unrouted, match_ids)

        # update the record with the information, and then
        # write it to the index
        routed = unrouted.make_routed()
        for pl in pack_links:
            routed.add_link(pl.get("url"), pl.get("type"), pl.get("format"), pl.get("access"), pl.get("packaging"))
        routed.repositories = match_ids
        routed.analysis_date = dates.now()
        if metadata is not None:
            enhance(routed, metadata)
        links(routed)
        routed.save()
        app.logger.debug(u"Routing - Notification:{y} successfully routed".format(y=unrouted.id))
        retu
rn True
    else:
        # log the failure
        app.logger.error(u"Routing - Notification:{y} was not routed".format(y=unrouted.id))

        # if config says so, convert the unrouted notification to a failed notification, enhance and save
        # for later diagnosis
        if app.config.get("KEEP_FAILED_NOTIFICATIONS", False):
            failed = unrouted.make_failed()
            failed.analysis_date = dates.now()
            if metadata is not None:
                enhance(failed, metadat
a)
            failed.save()
            app.logger.debug(u"Routing - Notification:{y} as stored as a Failed Notification".format(y=unrouted.id))

        return False

    # Note that we don't delete the unrouted notification here - that's for the caller to decide

def match(notification_data, repository_config, provenance):
    """
    Match the incoming notification data, to the repository config and determine
    if there is a match.

    If there is a match, all criteria for the match will be added to the provenance
    object

    :param notification_data:   models.RoutingMetadata
    :param repository_config:   models.RepositoryConfig
    :return:  True if there was a match, False if not
    """
    # just to give us a short-hand without compromising the useful names in the method sig
    md = notification_data
    rc = repository_config

    match_algorithms = {
        "domains" : {
            "urls" : domain_url,
            "emails" : domain_email
        },
        "name_variants" : {
            "affiliations" : exact_substring
        },
        "author_emails" : {
            "emails" : exact
        },
        "author_ids" : {
            "author_ids" : author_match
        },
        "postcodes" : {
            "postcodes" : postcode_match
        },
        "grants" : {
            "grants" : exact
        },
        "strings" : {
            "urls" : domain_url,
            "emails" : exact,
            "affiliations" : exact_substring,
            "author_ids" : author_string_match,
            "postcodes" : postcode_match,
            "grants" : exact
        }
    }

    repo_property_values = {
        "author_ids" : author_id_string
    }

    match_property_values = {
        "author_ids" : author_id_string
    }

    # do the required matches
    matched = False
    for repo_property, sub in match_algorithms.iteritems():
        for match_property, fn in sub.iteritems():
            for rprop in getattr(rc, repo_property):
                for mprop in getattr(md, match_property):
                    m = fn(rprop, mprop)
                    if m is not False:  # it will be a string then
                        matched = True

                        # convert the values that have matched to string values suitable for provenance
                        rval = repo_property_values.get(repo_property)(rprop) if repo_property in repo_property_values else rprop
                        mval = match_property_values.get(match_property)(mprop) if match_property in match_property_values else mprop

                        