import json
import logging
import datetime
import requests
from flask import Blueprint, request, session, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from webargs import fields
from webargs.flaskparser import use_args
from peewee import DoesNotExist, JOIN
from median.models import (
    ListeModel, ListeItemModel, Historique, Gpao, Magasin, Stock,
    FListe, ListeErrorModel, Endpoint, Product, User, Printer, Service)
from median.views import RawConfig, PrinterView, PrinterViewException
from median.constant import (
    HistoryType, TypeServiListe, TypeListe, RIEDL_PRIORITY, EtatListe,
    MEDIANWEB_POSTE, PatientGlobal, TypeEtatGpao)
from common.status import (
    HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR,
    HTTP_403_FORBIDDEN, HTTP_204_NO_CONTENT)
from common.log import log_riedl
from common.util import get_counter
from common.riedl import get_url_dispatcher
from common.templating import render_template
from common.exception import RiedlPrinterException, PrinterException, RiedlDispatcherException

from peewee import fn


riedl_output_blueprint = Blueprint('riedl_output', __name__)

logger = logging.getLogger('median')


@riedl_output_blueprint.errorhandler(400)
def handle_error(err):
    """
    When an error 400, return a formated custom error on the frontend
    """
    headers = err.data.get("headers", None)
    messages = err.data.get("messages", ["Invalid request."])
    if headers:
        return jsonify({"message": messages}), err.code, headers
    else:
        return jsonify({"message": messages}), err.code


@riedl_output_blueprint.route('endpoints', methods=['GET'])
@jwt_required()
def get_endpoints():
    endpoints = Endpoint.select(Endpoint.pk, Endpoint.libelle, Endpoint.secteur, Endpoint.type_peigne) \
        .where(Endpoint.type_dest == 'RIEDL').order_by(Endpoint.type_peigne)

    return {'list': [
        {'pk': item.pk,
         'label': item.libelle,
         'riedl_code': item.secteur,
         'value': item.type_peigne} for item in endpoints]}, HTTP_200_OK


@riedl_output_blueprint.route('priorities', methods=['GET'])
@jwt_required()
def get_priorities():
    return {'list': [{'value': i[0], 'name': i[1]} for i in RIEDL_PRIORITY]}, HTTP_200_OK


@riedl_output_blueprint.route('all', methods=['POST'])
@jwt_required()
def get_all():
    data = json.loads(request.data)

    stores = data.get('stores', [])
    states = data.get('states', [])

    andexpr = ((FListe.mode == TypeListe.Output.value) &
               (FListe.type_servi << [TypeServiListe.GlobaleBoite.value, TypeServiListe.RiedlBoite.value,
                                      TypeServiListe.Nominatif.value, TypeServiListe.RetraitLot.value,
                                      TypeServiListe.Perimee.value]) &
               (FListe.zone_deb == 'RIEDL'))

    if len(stores) > 0:
        andexpr = andexpr & (FListe.zone_fin << stores)

    if len(states) > 0:
        andexpr = andexpr & (FListe.etat << states)

    rdl_list = FListe.select(FListe, Magasin.libelle,
                             ListeErrorModel.message.alias('error_message')) \
        .join(ListeErrorModel, JOIN.LEFT_OUTER,
              on=(FListe.liste == ListeErrorModel.liste) & (FListe.mode == ListeErrorModel.mode)) \
        .switch(FListe) \
        .join(Magasin, JOIN.LEFT_OUTER, on=FListe.zone_fin == Magasin.type_mag) \
        .where(andexpr).order_by(FListe.service)

    res = []
    for lst in rdl_list:

        type_id = None

        if lst.type_servi in {TypeServiListe.GlobaleBoite.value, TypeServiListe.RiedlBoite.value}:
            type_id = 'outglobal'
        elif lst.type_servi in {TypeServiListe.Nominatif.value}:
            type_id = 'outnominative'
        elif lst.type_servi in {TypeServiListe.RetraitLot.value}:
            type_id = 'outlot'
        elif lst.type_servi in {TypeServiListe.Perimee.value}:
            type_id = 'outperime'

        obj = {
            'type': type_id,
            'text': '%s' % (lst.liste,),
            'id': 'model-%s' % (lst.liste,),
            'isSend': lst.selectionne,
            'ward': lst.service,
            'mag': {
                'label': lst.magasin.libelle if hasattr(lst, 'magasin') and lst.magasin is not None else '',
                'type': lst.zone_fin,
            },
            'username': lst.username or '',
            'etat': lst.etat,
            'message': lst.listeerrormodel.error_message if hasattr(lst, 'listeerrormodel') else '',
            'date_create': str(lst.date_creation) if lst.date_creation is not None else '',
            'order_num': lst.interface,
            'items_num': lst.nb_item,
            'date_update': str(lst.ddeb) if lst.ddeb is not None else '',
            'output': lst.no_pilulier,
            'priority': lst.pos_pilulier
        }

        res.append(obj)

    return {'listes': res}, HTTP_200_OK


@riedl_output_blueprint.route('deleteitem', methods=['DELETE'])
@jwt_required()
def deleteItem():
    """Delete one item by PK"""
    # logger.info(request.data)
    data = json.loads(request.data)

    try:
        itm = ListeItemModel.get(pk=data['item_pk'])
        lst = ListeModel.get(mode="S", liste=itm.liste)

        mag = Magasin.select(Magasin).where(Magasin.type_mag == lst.zone_fin).order_by(Magasin.pk)
        qte_tot = Stock.select(fn.SUM(Stock.quantite)).where(Stock.reference == itm.reference).scalar()
        qte_rdl = Stock.select(fn.SUM(Stock.quantite)).where(
            Stock.reference == itm.reference, Stock.magasin == (mag and mag[0].mag or 'L01')
        ).scalar()

        # Create a GPAO line
        logger.info('Create a GPAO line for item %i in list [%s]' % (data['item_pk'], itm.liste))
        Gpao.create(
            chrono=datetime.datetime.now(),
            poste=MEDIANWEB_POSTE,
            etat=TypeEtatGpao.DRAFT.value,
            ref=itm.reference,
            qte=0,
            type_mvt=TypeListe.Output.value,
            liste=itm.liste,
            dest=itm.dest,
            user=session['username'],
            item=itm.item,
            info=itm.info,
            solde=1,
            item_wms=itm.item_wms,
            fraction=itm.fraction,
            qte_dem=itm.qte_dem,
            ipp=itm.num_ipp or PatientGlobal.Ipp.value,
            sejour=itm.num_sej or PatientGlobal.Sejour.value,
            id_robot=mag and mag[0].id_robot or "",
            id_zone=mag and mag[0].id_zone or "",
            magasin=mag and mag[0].mag or 'L01',
            qte_avant=qte_tot,
            qte_apres=qte_tot,
            pk_item=itm.pk,
            qte_disp=qte_rdl,
        )

        # Create an history
        logger.info('Create a GPAO line for item %i' % data['item_pk'])
        Historique.create(
            chrono=datetime.datetime.now(),
            reference=itm.reference,
            adresse=mag and mag[0].mag or 'L01',
            magasin=mag and mag[0].mag or 'L01',
            quantite_mouvement=0,
            quantite_totale=qte_tot,
            quantite_demande=itm.qte_dem,
            service=itm.dest,
            liste=itm.liste,
            item=itm.item,
            type_mouvement=HistoryType.Sortie.value,
            pmp=0,
            poste='MEDIANWEB',
            ipp=itm.num_ipp or PatientGlobal.Ipp.value,
            sejour=itm.num_sej or PatientGlobal.Sejour.value,
            fraction=itm.fraction,
            item_wms=itm.item_wms,
            utilisateur=session['username'],
            info="MedianWeb -> Supp item",
            commentaire="Manual",
        )

        logger.info("RIEDL: delete item [%s]: f_item.pk %i" % (data['item'], data['item_pk']))
        log_riedl(
            session['username'], 'out_del_item',
            'suppression item %s (%s) liste %s ' % (itm.item, itm.pk, itm.liste)
        )
        itm.etat = EtatListe.Annuler.value
        itm.save()

        return {'result': 'ok'}, HTTP_200_OK

    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST


@riedl_output_blueprint.route('force_done', methods=['POST'])
@use_args({
    "liste": fields.Str(required=True)
}, location="json", error_status_code=400, error_headers={"validation": "error"})
def force_done(args):
    """
    We retrieve all items to generate the HISTO and GPAO
    After we put lines and list as done
    """
    try:
        lst = ListeModel.get(mode="S", liste=args['liste'])
        mag = Magasin.get(type_mag=lst.zone_fin)
    except DoesNotExist as error:
        logger.error(error.args)
        return {'message': "List %s does not exists" % args['liste']}, HTTP_400_BAD_REQUEST
    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST

    for item in ListeItemModel.select(ListeItemModel).where(
        ListeItemModel.mode == lst.mode, ListeItemModel.liste == lst.liste
    ):
        new_gpao = Gpao()
        new_gpao.chrono = datetime.datetime.now()
        new_gpao.poste = MEDIANWEB_POSTE
        new_gpao.dest = lst.service
        new_gpao.etat = "A"
        new_gpao.liste = item.liste
        new_gpao.ref = item.reference
        new_gpao.qte = item.qte_dem - item.qte_serv
        new_gpao.lot = item.lot
        new_gpao.type_mvt = "S"
        new_gpao.tperemp = "0000-00-00 00:00:00"
        new_gpao.fraction = item.fraction
        new_gpao.cip = item.cip
        new_gpao.ucd = item.ucd
        new_gpao.magasin = mag.mag
        new_gpao.info = item.info
        new_gpao.qte_dem = item.qte_dem
        new_gpao.contenant = ""
        new_gpao.item = item.item
        new_gpao.pk_item = item.pk
        new_gpao.item_wms = item.item_wms
        new_gpao.ipp = lst.num_ipp
        new_gpao.sejour = lst.num_sej
        new_gpao.solde = 1
        new_gpao.save()

        item.qte_serv = item.qte_dem
        item.etat = "S"
        item.commentaire_web = "Solde manuel"
        item.save()

    comments = ListeErrorModel()
    comments.liste = lst.liste
    comments.mode = lst.mode
    comments.message = "Liste soldée manuellement"
    comments.save()

    lst.etat = "S"
    lst.save()

    return {}, HTTP_200_OK


@riedl_output_blueprint.route('report', methods=['POST'])
@use_args({
    "liste": fields.Str(required=True)
}, location="json", error_status_code=400, error_headers={"validation": "error"})
def report(args):

    try:
        lst = ListeModel.get(mode="S", liste=args['liste'])
    except DoesNotExist as error:
        logger.error(error.args)
        return {'message': "List %s does not exists" % args['liste']}, HTTP_400_BAD_REQUEST
    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST

    mag = None
    try:
        mag = Magasin.get(Magasin.type_mag == lst.zone_fin, Magasin.eco_type == 'L')
    except DoesNotExist:
        logger.error("No warehouse found for %s" % lst.zone_fin)
        return {'message': "No warehouse found for %s" % lst.zone_fin}, HTTP_400_BAD_REQUEST

    print("-- Call the dispatcher API")
    url = ''
    try:
        url = get_url_dispatcher(mag.type_mag)
    except RiedlDispatcherException:
        return {'message': "Dispatcher URL not found"}, HTTP_400_BAD_REQUEST

    print('URL dispatcher %s -> %s' % (mag.type_mag, url))
    # send list to the dispatcher.
    try:
        print('Call RiedlOCommand list id %s' % str(lst.pk))
        resp = requests.post('%s/RiedlOCommand' % url, json={
            "warehouseCode": mag.mag,
            "orderNumber": str(lst.interface),
            "requesterNumber": "987"
        })
        print('Code retour dipatcher %s' % resp.status_code)
    except Exception:
        print("Error when call the the dispatcher")
        return {'message': "Error when call the Dispatcher URL"}, HTTP_400_BAD_REQUEST

    return {}, HTTP_200_OK


@riedl_output_blueprint.route('printcontainer', methods=['POST'])
@use_args({
    "liste": fields.Str(required=True)
}, location="json", error_status_code=400, error_headers={"validation": "error"})
@jwt_required()
def printContainer(args):
    """Print the container label"""
    try:
        # Load the output list and check if container
        lst = ListeModel.get(mode="S", liste=args['liste'])
        if not lst.id_plateau:
            # Retrieve prefix
            tray_prefix = ""
            try:
                cfg = RawConfig('TOUS')
                tray_prefix = cfg.read('k_rdl_prefix_container').value
            except Exception:
                tray_prefix = ""
            # Retrieve code
            lst.id_plateau = '%s%010d' % (tray_prefix, int(get_counter("RIEDL_CONTAINER")))
            lst.save()

        mag = None
        try:
            mag = Magasin.get(Magasin.type_mag == lst.zone_fin, Magasin.eco_type == 'L')
        except DoesNotExist:
            logger.error("No warehouse found for %s" % lst.zone_fin)
        try:
            # Print the label
            default_printer: Printer = PrinterView.default_printer(mag)
            logger.info(f"Default printer for {mag.mag} is {default_printer.name}")
            with PrinterView(default_printer, 3, session["user_id"]) as p:
                tmpl, _ = p.printer_template("container")
                zpl_content = render_template(tmpl, _compose_container_dict(lst, 0, 0))
                p.send(zpl_content)

            # TODO: in the future, use render template with 3 arguments
        except PrinterViewException as e:
            logger.error(f"PrinterViewException: {str(e)}")
        except RiedlPrinterException:
            logger.error("Error when retrieve printer configuration!")
        except PrinterException:
            logger.error("send to printer failed !")
        except Exception:
            logger.error("Config Key k_rdl_print_container cannot be retrieve")

        return {'result': 'ok', 'args': args}, HTTP_200_OK
    except DoesNotExist as error:
        logger.error(error.args)
        return {'message': "List %s does not eixsts" % args['liste']}, HTTP_400_BAD_REQUEST
    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST


@riedl_output_blueprint.route('forcefullboxes/<string:list_pk>', methods=['GET'])
@jwt_required()
def force_full_boxes(list_pk):
    logger.info("force list %s to move on full boxes" % list_pk)
    try:
        lst = ListeModel.get(mode="S", pk=list_pk)
        query = ListeItemModel.update({ListeItemModel.num_face: 1}).where(
            ListeItemModel.mode == "S", ListeItemModel.liste == lst.liste)
        query.execute()
        lst.num_face = 1
        lst.save()
        logger.info("Force list %s to use a full boxes" % lst.liste)
        log_riedl(
            session['username'], 'out_update_list',
            'Force la sortie des boites complètes de la liste %s' % (lst.liste,)
        )
        return {'result': 'ok', 'args': [list_pk]}, HTTP_200_OK
    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST


@riedl_output_blueprint.route('validate/<string:item_pk>', methods=['PATCH'])
@jwt_required()
def validate(item_pk):
    try:
        data = json.loads(request.data)
        value = data.get('readonly', False)
        itm = (ListeItemModel.select(ListeItemModel)
               .where(ListeItemModel.pk == item_pk)).get()

        itm.readonly = value
        itm.save()
        logger.info("RIEDL: update item [%s]: f_item.pk %i" % (itm.item, itm.pk))
        log_riedl(
            session['username'], 'out_update_item',
            'mise à jour item %s (%s) liste %s ' % (itm.item, itm.pk, itm.liste)
        )

        return {'result': 'ok'}, HTTP_200_OK

    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST


@riedl_output_blueprint.route('<string:x_liste>/items', methods=['POST'])
@jwt_required()
def get_items(x_liste):
    logger.info("Récupérer les items de listes riedl : '%s'" % x_liste)
    try:

        lst = ListeModel.get_or_none(liste=x_liste, mode='S')
        mag = Magasin.get_or_none(type_mag=lst.zone_fin)

        blocked_stock = Stock.select(
            Stock.pk, Magasin.type_mag.alias('type_mag'), Stock.reference.alias('reference'),
            fn.SUM(Stock.quantite).alias('qty_blocked_units'),
            fn.COUNT(Stock.quantite).alias('qty_blocked_boxes')
        ).join(
            Magasin, on=Magasin.mag == Stock.magasin
        ).where(
            Stock.bloque > 0
        ).group_by(
            Stock.reference,
            Magasin.type_mag
        )

        query = (ListeItemModel.select(ListeItemModel.pk, ListeItemModel.item,
                                       ListeItemModel.etat, ListeItemModel.reference,
                                       ListeItemModel.fraction, Product.designation,
                                       ListeItemModel.qte_dem, ListeItemModel.qte_serv,
                                       ListeItemModel.moment, ListeItemModel.heure,
                                       ListeItemModel.readonly, Product.pk.alias('product_pk'),
                                       fn.IFNULL(blocked_stock.c.qty_blocked_units, 0).alias('qty_blocked_units'),
                                       fn.IFNULL(blocked_stock.c.qty_blocked_boxes, 0).alias('qty_blocked_boxes'),
                                       fn.SUM(Stock.quantite).alias('qte_stock')
                                       )
                 .join(Product, on=ListeItemModel.reference == Product.reference)
                 .join(ListeModel,
                       on=(ListeModel.liste == ListeItemModel.liste) &
                          (ListeModel.mode == ListeItemModel.mode))
                 .join(blocked_stock, JOIN.LEFT_OUTER,
                       on=(blocked_stock.c.reference == ListeItemModel.reference) &
                          (blocked_stock.c.type_mag == ListeModel.zone_fin))
                 .join(Stock, on=((Stock.reference == ListeItemModel.reference) & (Stock.magasin == mag.mag)))
                 .where((ListeItemModel.mode == 'S') & (ListeItemModel.liste == x_liste))
                 .group_by(Stock.reference)
                 .order_by(ListeItemModel.item.asc()))

        if query.count() == 0:
            logger.error('No list with name %s' % x_liste)
            return {'data': []}, HTTP_200_OK

        return {'data': [{
            'pk': i.pk,
            'list_name': x_liste,
            'item': i.item,
            'etat': i.etat,
            'reference': i.reference,
            'fraction': i.fraction,
            'designation': i.product.designation,
            'qte_demandee': i.qte_dem,
            'qte_servie': i.qte_serv,
            'moment': i.moment,
            'heure': i.heure,
            'readonly': i.readonly,
            'reference_pk': i.product.product_pk,
            'qte_stock': i.qte_stock,
            'qty_blocked_units': i.qty_blocked_units,
            'qty_blocked_boxes': i.qty_blocked_boxes
        } for i in query]}, HTTP_200_OK

    except Exception as error:
        logger.error(('Get reappro items Datatables raised an exception: ', error.args))
        return {'message': error.args}, HTTP_500_INTERNAL_SERVER_ERROR


@riedl_output_blueprint.route('<string:x_liste>/init', methods=['PUT'])
@jwt_required()
def reinit_list(x_liste):
    authorized_profil = ['DEENOVA', 'ECO-DEX', 'PHARMACIEN']
    user_id = get_jwt_identity()
    try:
        User.select(User.profil).where((user_id == User.pk) &
                                       (User.profil << authorized_profil)).get()
    except DoesNotExist:
        return {'message': 'rield.output.list.error.forbidden',
                }, HTTP_403_FORBIDDEN

    try:
        riedl_lst = ListeModel.select().where(ListeModel.pk == x_liste).get()
    except DoesNotExist:
        return {'message': 'rield.output.list.error.not_exist',
                }, HTTP_500_INTERNAL_SERVER_ERROR

    try:
        riedl_lst.interface = ''
        riedl_lst.etat = EtatListe.Vierge.value
        riedl_lst.selectionne = 0
        riedl_lst.save()
    except Exception as error:
        logger.error(('Reinit_list raised an exception: ', error.args))
        return {'message': error.args}, HTTP_500_INTERNAL_SERVER_ERROR

    return {}, HTTP_204_NO_CONTENT


def _compose_container_dict(list_obj: ListeModel, label_shift: int = 0, label_top: int = 0) -> dict:
    label_datas = {
        'label_shift': label_shift,
        'label_top': label_top,
        'container_code': list_obj.id_plateau,
        'ward_code': list_obj.service,
        'ward_label': '-',
    }
    try:
        serv = Service.get(code=list_obj.service, type_dest='SERVICE')
        label_datas['ward_label'] = serv.libelle
    except DoesNotExist:
        label_datas['ward_label'] = 'Inconnu'
    return label_datas
