#!/usr/bin/env python
# encoding: utf-8
"""
Uebernimmt Warenzugangsmeldungen für Lager 100 aus einer Nachrichtenschlange und lagert diese im
myPL ein (EINLAG). Erstellt ausserdem eine Warenbewegung im myPL an einen geeigneten Platz und
erzeugt eine Palettenfahne.

Created by Lars Ronge on 2007-12-12.
Rewritten by Maximillian Dornseif 2008-12, 2009-12
Copyright (c) 2007-2010 HUDORA GmbH.
"""

# From http://superjared.com/entry/django-and-crontab-best-friends/
from django.core.management import setup_environ
import settings
setup_environ(settings)

import mypl.kernel
import cs.masterdata.article
import cs.messaging as messaging
import cs.printing
import cs.zwitscher
import huTools.NetStringIO as NetStringIO
import myplfrontend.belege
import optparse
import signal
import simplejson as json
import sys
import time


process_messages = True
message_count = 0


def display(msg):
    """Display messages based on command line options."""
    if options.debug or (not options.quiet):
        print (msg)
        sys.stdout.flush()


def write_journal(doc):
    """Write journal entrys of incoming messages."""
    logdata = json.dumps(doc, sort_keys=True, separators=(',', ':'))
    if options.debug:
        print logdata
    nstring = NetStringIO.NetStringIO(open(options.log, 'a'))
    nstring.write(logdata)
    nstring.file.close()


def split_quantities(quantity, per_unit):
    """Splits quantity in units not larger than per_unit.

    >>> split_quantities(512.0, 100.0)
    [100, 100, 100, 100, 100, 12]
    >>> split_quantities('512', None)
    [512]
    >>> split_quantities('512.0', 1000)
    [512]
    >>> split_quantities(512, -10)
    [512]
    """

    quantity = int(float(quantity))
    if not per_unit or per_unit < 1:
        return [quantity]
    else:
        per_unit = int(float(per_unit))
        ret = []
        while quantity > 0:
            ret.append(min([quantity, per_unit]))
            quantity -= per_unit
        return ret


def move_units_to_good_location(muis):
    """Moves units to a good location in the warehouse and prints the Palettenfahne
    and the movement-note.
    """

    kernel = mypl.kernel.Kerneladapter()
    movementids = []
    for mui in muis:
        # TODO: move to new HTTP-API
        movementids.append((kernel.init_movement_to_good_location(mui), mui))
    for movement_id, mui in movementids:
        if movement_id:
            # Umlagerbeleg drucken
            pdf = myplfrontend.belege.get_movement_pdf(movement_id)
            cs.printing.print_data(pdf, printer="DruckerTesler")
        else:
            cs.zwitscher.zwitscher('%s: Kein passender Lagerplatz. Palette bleibt auf EINLAG #warning' % mui,
                                   username='mypl')


def umlagerung_to_unit(guid, menge, artnr, batchnr, referenzen):
    """Converts a Umlagerungsposition to an Unit in the Kernel. Returns MUIs generated."""

    muis = []
    if menge < 1:
        msg = 'Umlagerung %s hat Position ohne Menge (0) fuer %s #warn' % (batchnr, artnr)
        cs.zwitscher.zwitscher(msg, username='mypl')
        display(msg.encode('utf-8', 'ignore'))
        return []

    product = cs.masterdata.article.eap(artnr)
    msg = None
    if not product:
        msg = u'%s: Kein Artikelpass. Einlagerung abgebrochen. #error' % artnr
    else:
        if not product.get('palettenfaktor'):
            msg = u'%s: Kein Palettenfaktor. Einlagerung abgebrochen. Übernahme steht #error' % artnr
        if not product.get('pallet_height'):
            msg = u'%s: Palettenhöhe unbekannt. Einlagerung abgebrochen. Übernahme steht #error' % artnr
        elif product.get('pallet_height', 1950) > 2100:
            msg = u'%s: Palettenhöhe (%s) zu hoch. Übernahme steht #error' % (artnr,
                        product['pallet_height'])

    if msg:
        msg = "%s/%s" % (guid, msg)
        cs.zwitscher.zwitscher(msg, username='mypl')
        display(msg.encode('utf-8', 'ignore'))
        # FIXME: Hier wird nichts gelogt, ausser als zwitscher Nachricht. Dh. wir können nicht genau
        #        nachvollziehen, welche Daten zu Problemen geführt haben.
        return [] # this leaves a stale message

    # Einlagerung auf Paletten aufteilen
    elements = [(quantity, artnr, product['pallet_height'])
                for quantity in split_quantities(menge, product['palettenfaktor'])]
    referenzen.update({'komminr': str(batchnr), 'source': 'umlagerung', 'paletten': len(elements)})

    # log file for replaying in case of errors
    write_journal((guid, "EINLAG", elements, referenzen))

    kernel = mypl.kernel.Kerneladapter()
    if not options.dry_run:
        # TODO: move to new HTTP-API
        # mypl.kernel.mypl_kerneladapter.KernelError: error during storing of [(126, '76404', 1850)]: ['error', 'duplicate_id', '3106032-19']
        try:
            muis = kernel.store_at_location_multi(guid, "EINLAG", elements, referenzen)
            doc = messaging.empty_message('wms2mypl_einlagerungen',
                                           guid=guid,
                                           audit_trail='wms2mypl_einlagerungen',
                                           audit_info='Auftrag aus SoftM in den KErnel geschrieben')
            doc.update({'to_location': "EINLAG", 'elements': elements, 'referenzen': referenzen, 'muis': muis})
            messaging.publish(doc, 'log.wms-mypl.warenzugang')
        except mypl.kernel.mypl_kerneladapter.KernelError, msg:
            if "'error', 'duplicate_id'" in str(msg):
                msg = 'Umlagerung %s kommt doppelt im myPL vor #warn' % batchnr
                cs.zwitscher.zwitscher(msg, username='mypl')
                print msg
                return [None] # special-cased um die Nachricht aus der Queue zu nehmen.
            else:
                raise

    if options.debug:
        print muis
    return muis


def handle_message(message):
    """Callback function used by channel.basic_consume for adding 'Warenzugang' to myPL."""
    global message_count

    if options.maxnum and (message_count > options.maxnum):
        cancel_processing()
        return

    body = json.loads(message.body)
    guid, menge, artnr = body['guid'], body['menge'], body['artnr']
    batchnr, referenzen = body['batchnr'], body['referenzen']
    if options.debug:
        print message_count, guid, menge, artnr, batchnr, referenzen
    # umlagerung_to_unit() macht die wirkliche arbeit und überträgt in den kernel.
    muis = umlagerung_to_unit(guid, menge, artnr, batchnr, referenzen)
    if muis:
        channel.basic_ack(message.delivery_tag)
        # Wenn wir move_units_to_good_location() nicht aufrufen, kümmert sich der Kernel später
        # selbst um das wegräumen der Paletten, wir sind also im grunde längst fertig.
        if not options.no_movements:
            if muis != [None]:
                move_units_to_good_location(muis)
        message_count += 1


# Might be used as a signal hander
def cancel_processing():
    """ Stop consuming messages from RabbitMQ """
    global process_messages

    # Do this so we exit our main loop
    process_messages = False
    display("Exiting")
    # Tell the channel you dont want to consume anymore
    for consumer_tag in consumer_tags:
        channel.basic_cancel(consumer_tag)


def main():
    """Setup message consuming."""
    global channel, consumer_tags, options
    consumer_tags = []

    parser = optparse.OptionParser()
    parser.version = "%%prog"
    parser.description = ('Uebernimmt Warenzugangsmeldungen aus einer Warteschlange und lagert diese im'
                         + 'myPL ein (EINLAG). Erstellt ausserdem eine Palettenbewegung an einen geeigneten'
                         + 'Platz und  erzeugt eine Palettenfahne.')
    parser.set_usage('usage: %prog [options]')
    parser.add_option('--lager', type="int", help='Lager, das bearbeitet werden soll')
    parser.add_option('--in', dest="in_queues", action='append',
                      help='Name der Queue, aus der gelesen werden soll. Wird diese Parameter mehrfach '
                      +'angegeben, wird aus mehreren Queues gelesen. Beispiel --in=erp.ic-wms.warenzugang.')
    parser.add_option('--log', default='/var/log/wms2mypl_einlagerungen.log',
                      help='Wo sollen die Nachrichten protokolliert werden? [%default]')
    parser.add_option('--quiet', action='store_true', help='Uebliche Ausgaben unterdruecken')
    parser.add_option('--debug', action='store_true', help='Debugging Informationen ausgeben')
    parser.add_option('--dry-run', action='store_true', help='Keine Nachrichten absenden und loeschen')
    parser.add_option('--no-movements', action='store_true', help='Palleten NICHT von EINLAG weglagern.')
    parser.add_option('--maxnum', type='int',
                      help='Maximale Anzahl der zu transferierenden Datensaetze [default: unlimited]')

    options, args = parser.parse_args()
    if args:
        parser.error("incorrect number of arguments")
    if not options.lager:
        parser.error("'--lager' nicht gesetzt")
    if not options.in_queues:
        parser.error("'--in' nicht gesetzt")

    channel = None
    for qname in options.in_queues:
        inqueuename = "%s#%s" % (qname, options.lager)
        channel = messaging.setup_queue(inqueuename, channel=channel, durable=True)
        consumer_tags.append(channel.basic_consume(queue=inqueuename, callback=handle_message))

    # catch Signals
    signal.signal(signal.SIGTERM, lambda signum, stack_frame: cancel_processing())
    signal.signal(signal.SIGINT, lambda signum, stack_frame: cancel_processing())

    display("waiting for messages ...")

    # Loop while process_messages is True - could be set to False by signal handlers
    last_requeue = 0
    while process_messages:
        # Wait for a message and wait for `handle_message` callback beeing called
        channel.wait()
        if time.time() - last_requeue > 3600:
            # every 3600 seconds Request non-acknowledged messages to be re-delivered
            channel.basic_recover(requeue=True)
            last_requeue = time.time()

    channel.close()
    return 0


if __name__ == '__main__':
    sys.exit(main())
