import logging
import json
import time
import io
from datetime import datetime

from flask import Blueprint, jsonify, request, session
from flask import make_response, send_file
from flask_jwt_extended import jwt_required
from median.constant import TypeServiListe, TypeListe, EcoType, TypeDest
from median.models import ListeItemModel, ListeModel, Magasin
from median.models import Stock, Product, Service, UnitDose
from peewee import JOIN, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict
from ressources.equipments.externe import unload_item
from common.pdf import TablePDF

from common.models import WebLogActions
from common.status import (
    HTTP_200_OK,
    HTTP_204_NO_CONTENT,
    HTTP_400_BAD_REQUEST,
    HTTP_404_NOT_FOUND,
    HTTP_500_INTERNAL_SERVER_ERROR,
)
from common.util import mustHaveRights
from fpdf.errors import FPDFException

external_unload_blueprint = Blueprint("external_unload", __name__)
logger = logging.getLogger("median.external")


RESSOURCE_NAME = "WEB_EXTERNAL_UNLOAD"


@external_unload_blueprint.route("all", methods=["get"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def get_all_lists():
    ext_list = (
        ListeModel.select(ListeModel, Magasin, Service)
        .join(Magasin, JOIN.INNER, on=(ListeModel.zone_deb == Magasin.type_mag))
        .switch(ListeModel)
        .join(Service, JOIN.LEFT_OUTER, on=(ListeModel.service == Service.code))
        .where(
            (Magasin.eco_type == EcoType.Externe.value) & (ListeModel.type_servi == TypeServiListe.ExterneBoite.value)
        )
    )

    # Warning : if the ward code isn't linked to a real ward, it won't be shown

    res = []
    lst: ListeModel
    for lst in ext_list:
        listType = lst.zone_fin or lst.service.code if isinstance(lst.service, Service) else lst.service

        obj = {
            "type": listType,
            "text": lst.liste,
            "id": lst.pk,
            "isSend": lst.selectionne,
            "ward": {"code": lst.service.code, "libelle": lst.service.libelle}
            if isinstance(lst.service, Service)
            else {"code": lst.service, "libelle": "Unknown"},
            "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,
            "zone_deb": lst.zone_deb,
            "zone_fin": lst.zone_fin,
        }

        res.append(obj)

    return {"listes": res}, HTTP_200_OK


@external_unload_blueprint.route("<string:x_pk>/items", methods=["POST"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def get_items(x_pk):
    try:
        items_query = _get_items(x_pk)

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

        return {
            "data": [
                {
                    "pk": i.pk,
                    "list_name": x_pk,
                    "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 items_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


def _get_items(x_pk) -> ListeItemModel:
    logger.info("Récupérer les items de listes de sortie externes : '%s'" % x_pk)
    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,
            Product.risque,
            Product.stup,
            Product.forme,
            ListeItemModel.qte_dem,
            ListeItemModel.qte_serv,
            ListeItemModel.moment,
            ListeItemModel.heure,
            ListeItemModel.readonly,
            Product.pk.alias("product_pk"),
            Product.adr_pref.alias("adr_pref"),
            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_deb),
        )
        .join(Magasin, JOIN.INNER, on=Magasin.type_mag == ListeModel.zone_deb)
        .join(
            Stock,
            JOIN.LEFT_OUTER,
            on=(
                (Stock.reference == ListeItemModel.reference)
                & (Stock.magasin == Magasin.mag)
                & (Stock.fraction == ListeItemModel.fraction)
            ),
        )
        .where(ListeModel.pk == x_pk)
        .group_by(ListeItemModel.pk)
        .order_by(-Product.risque, -Product.stup, -Product.forme, ListeItemModel.item)
    )

    return query


@external_unload_blueprint.route("<string:liste_pk>", methods=["GET"])
@jwt_required()
def get(liste_pk):
    logger.info("Récupérer les listes de sorties")
    return _get_liste(liste_pk)


def _get_liste(list_pk):
    """Retrieve the list information"""
    res = {}
    try:
        lst = (
            ListeModel.select(ListeModel, Service.libelle)
            .join(
                Service,
                JOIN.LEFT_OUTER,
                on=((Service.code == ListeModel.service) & (Service.type_dest == TypeDest.Ward.value)),
            )
            .where((ListeModel.mode == TypeListe.Output.value) & (ListeModel.pk == list_pk))
            .get()
        )

        externals = Magasin.select(Magasin).where(Magasin.eco_type == EcoType.Externe.value)

        res["data"] = model_to_dict(lst, exclude=[ListeModel.service])
        res["data"]["ward_name"] = lst.service.libelle if isinstance(lst.service, Service) else lst.service
        res["selection"] = {}
        res["selection"]["external"] = [model_to_dict(ext) for ext in externals]

        return jsonify(res)
    except DoesNotExist:
        res["error"] = {"message": "List pk %s does not exists" % list_pk}
        return res, HTTP_404_NOT_FOUND
    except Exception as e:
        logger.error("External Unload", str(e))
        return res, HTTP_500_INTERNAL_SERVER_ERROR


@external_unload_blueprint.route("check_content/<string:search_code>/<string:list_pk>", methods=["GET"])
@jwt_required()
def check_content(search_code, list_pk):
    # Given a unit serial or a loader-box code, will find and return the item pk if the searched code
    # is a box containing the item refs, or if the serial is a valid product from the list
    try:
        serial_exists: UnitDose = (
            UnitDose.select(UnitDose).where(UnitDose.serial == search_code).order_by(-UnitDose.pk).first()
        )

        if serial_exists:
            boxExists: Stock = Stock.get_or_none(Stock.contenant == serial_exists.contenant)
        else:
            boxExists: Stock = Stock.get_or_none(Stock.contenant == search_code)

        if boxExists:
            # Try to find the content of a box code

            item = (
                ListeItemModel.select(
                    ListeItemModel.pk,
                    ListeItemModel.reference,
                    ListeItemModel.fraction,
                    Stock.adresse.alias("adresse"),
                    Stock.quantite.alias("quantite"),
                    Stock.contenant.alias("contenant"),
                )
                .join(ListeModel, JOIN.INNER, on=(ListeModel.liste == ListeItemModel.liste))
                .join(Magasin, JOIN.INNER, on=(Magasin.type_mag == ListeModel.zone_deb))
                .join(
                    Stock,
                    JOIN.INNER,
                    on=(
                        (Stock.reference == ListeItemModel.reference)
                        & (Stock.fraction == ListeItemModel.fraction)
                        & (Stock.magasin == Magasin.mag)
                    ),
                )
                .where(
                    (ListeItemModel.mode == TypeListe.Output.value)
                    & (ListeItemModel.liste == list_pk)
                    & (Stock.contenant == boxExists.contenant)
                )
                .objects()
            )

            # We get a list of references, and stock addresses + qty
            results = list(item)
            if not results:
                return {"message": "No matching items found in the list"}, HTTP_404_NOT_FOUND

            # Extract the supposedly only reference
            distinct_refs = set(result.reference for result in results)
            if len(distinct_refs) > 1:
                return {"message": "Multiple different references found in the same box"}, HTTP_400_BAD_REQUEST

            reference = results[0].reference
            stock_data = [
                {"address": result.adresse, "quantity": result.quantite, "contenant": result.contenant}
                for result in results
            ]

            return {"reference": reference, "stock": stock_data}, HTTP_200_OK
        else:
            # Nothing is found !
            return {"message": "Nothing has been found"}, HTTP_200_OK

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


@external_unload_blueprint.route("more_info/<string:item_pk>", methods=["POST"])
@jwt_required()
def fetchMoreInfo(item_pk):
    try:
        data = json.loads(request.data)
        reference = data.get("reference", None)

        if not reference:
            return {"message": "Reference is required"}, HTTP_400_BAD_REQUEST

        item = ListeItemModel.select().where(ListeItemModel.pk == item_pk).get()

        liste = ListeModel.select().where((ListeModel.liste == item.liste) & (ListeModel.mode == item.mode)).get()

        magasin = Magasin.select().where(Magasin.type_mag == liste.zone_deb).get()

        target_mag = magasin.mag

        # Query for all stock lines with this reference in the specific magasin, ordered by expiry date
        stock_query = (
            Stock.select(
                Stock.adresse, Stock.date_peremption, Stock.contenant, fn.SUM(Stock.quantite).alias("quantite")
            )
            .where(
                (Stock.reference == reference)
                & (Stock.quantite > 0)
                & (Stock.bloque == 0)
                & (Stock.magasin == target_mag)
            )
            .group_by(Stock.contenant, Stock.adresse, Stock.date_peremption)
            .order_by(
                Stock.date_peremption.is_null(),
                Stock.date_peremption.asc(),
            )
        )

        result = {"reference": reference, "items": []}

        for stock in stock_query:
            result["items"].append(
                {
                    "contenant": stock.contenant,
                    "address": stock.adresse,
                    "date_peremption": stock.date_peremption.strftime("%Y-%m-%d") if stock.date_peremption else None,
                    "quantite": stock.quantite,
                }
            )

        if not result["items"]:
            result = {"items": [], "reference": reference}

        logger.info(f"Found {len(result['items'])} stock items for reference {reference}")

        logger.info(f"EXTERNAL: fetched earliest expiry info for reference {reference} in magasin {target_mag}")

        return {"result": "ok", "data": result}, HTTP_200_OK

    except DoesNotExist as e:
        logger.error(f"Item or related data not found: {str(e)}")
        return {"message": f"Item or related data not found: {str(e)}"}, HTTP_404_NOT_FOUND
    except Exception as error:
        logger.error(error.args)
        return {"message": str(error.args)}, HTTP_400_BAD_REQUEST


@external_unload_blueprint.route("list/<string:liste_name>", methods=["DELETE"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def delete_list(liste_name):
    try:
        liste: ListeModel = ListeModel.get(
            (ListeModel.liste == liste_name) & (ListeModel.mode == TypeListe.Output.value)
        )
        liste.delete_instance()
        log_ext_unload(session.get("username"), "external_unload", f"Deleted list {liste_name}")
    except DoesNotExist:
        logger.error(f"External Unload : Deletion of list [{liste_name}] failed.")
        return {"message": "This list does not exist!"}, HTTP_400_BAD_REQUEST

    return {}, HTTP_204_NO_CONTENT


@external_unload_blueprint.route("item/<string:item_pk>", methods=["DELETE"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def delete_item(item_pk):
    try:
        item: ListeItemModel = ListeItemModel.get(ListeItemModel.pk == item_pk)
        liste: ListeModel = ListeModel.get(
            (ListeModel.liste == item.liste) & (ListeModel.mode == TypeListe.Output.value)
        )

        item.delete_instance()

        nbItems = (
            ListeItemModel.select()
            .where((ListeItemModel.liste == item.liste) & (ListeItemModel.mode == TypeListe.Output.value))
            .count()
        )
        if nbItems == 0:
            liste.delete_instance()
        else:
            liste.nb_item = nbItems
            liste.save()

        log_ext_unload(session.get("username"), "external_unload", f"Deleted item pk {item_pk} from list {liste.liste}")
    except DoesNotExist:
        logger.error(f"External Unload : Deletion of item [{item_pk}] failed.")
        return {"message": "This list does not exist!"}, HTTP_400_BAD_REQUEST

    return {}, HTTP_204_NO_CONTENT


@external_unload_blueprint.route("unload_item", methods=["POST"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def external_unload_item():
    data = json.loads(request.data)
    item_pk = data.get("item_pk", None)
    boxCode = data.get("box_code", None)

    try:
        result = unload_item(item_pk, boxCode)
        log_ext_unload(session.get("username"), "external_unload", f"Unload box {boxCode} for list item {item_pk}")
        return result
    except Exception as e:
        print(str(e))


@external_unload_blueprint.route("print/<string:list_pk>", methods=["POST"])
@jwt_required()
def print(list_pk):
    data = request.get_json()
    headerTranslations = data.get("headerTranslations", {})
    res = []
    try:
        reappro = ListeModel.get_by_id(list_pk)

        head = [
            ("reference", 80, "L"),
            ("address", 30, "L"),
            ("quantite", 10, "R"),
            ("fraction", 8, "C"),
            ("forme", 8, "C"),
            ("risque", 8, "C"),
            ("stup", 8, "C"),
        ]

        for i, (name, width, align) in enumerate(head):
            if name in headerTranslations:
                head[i] = (headerTranslations[name], width, align)

        lines = _get_items(list_pk)

        # print(lines)
        for li in lines:
            data = [
                "[%s] %s" % (li.reference, li.product.designation),
                li.product.adr_pref if li.product.adr_pref else "Not defined",
                li.qte_dem,
                li.fraction,
                "X" if li.product.forme else "",
                "X" if li.product.risque else "",
                "X" if li.product.stup else "",
            ]

            if len(head) != len(data):
                logger.error("Header and row length mismatch: %s != %s" % (len(head), len(res[0])))
                return {"error": "Header and row length mismatch"}, HTTP_500_INTERNAL_SERVER_ERROR

            # Add a last column to condition the line style, this should not have a header.
            # Only one last column accepted
            res.append(data)
            res[-1].append("N" if li.product.risque else "W" if li.product.stup else "")

        filename = "unload-%s-%s" % (reappro.zone_fin, time.strftime("%Y%m%d_%H%M%S"))
        logger.info(f"Trying to generate PDF for unload {filename} for list {list_pk}")
        file_io = io.BytesIO()

        try:
            pdf_file = TablePDF()
            pdf_file.doc_font(size=7)
            pdf_file.doc_title(reappro.liste)
            pdf_file.grid_header(head)
            pdf_file.grid_rows(res)
            pdf_file.doc_save(file_io)

        except FPDFException as ex:
            logger.error("Error when generating a PDF for replenishment %s" % list_pk)
            logger.error(str(ex))
            error_message = {"error": "replenishment.header.pdf.error", "param": [str(ex)]}
            response = make_response(error_message, HTTP_500_INTERNAL_SERVER_ERROR)
            response.headers["Content-Type"] = "application/json"
            return response

        file_io.seek(0)
        response = make_response(
            send_file(
                file_io, download_name="%s.pdf" % filename, mimetype="application/pdf", as_attachment=True, max_age=0
            )
        )
        response.headers.set("file-name", "%s.pdf" % filename)
        return response

    except DoesNotExist:
        logger.error("Replenishment %s not found" % list_pk)
        error_message = {"alertMessage": "replenishment.header.print.error", "param": [list_pk]}
        response = make_response(error_message, HTTP_500_INTERNAL_SERVER_ERROR)
        response.headers["Content-Type"] = "application/json"
        return response
    except Exception as error:
        logger.error("An error occurred while generating the PDF for replenishment %s: %s" % (list_pk, error))
        error_message = {"alertMessage": "replenishment.header.print.error", "param": [str(error)]}
        response = make_response(error_message, HTTP_500_INTERNAL_SERVER_ERROR)
        response.headers["Content-Type"] = "application/json"
        return response


def log_ext_unload(username: str, action: str, message: str):
    """
    Add new log for replenish (liste)

    :param username: User made the action to log
    :param action:
    :param message: message to log
    """
    logger.info("Replenish[%s](%s)): %s" % (action, username, message))
    wlog = WebLogActions()
    wlog.chrono = datetime.now()
    wlog.username = username
    wlog.equipement_type = EcoType.Externe.value
    wlog.action = action
    wlog.message = message
    wlog.save()
