import json
import logging
from datetime import datetime
from peewee import JOIN, DoesNotExist, fn
from flask import request, session

from median.constant import EcoType, EtatAdresse, EquipmentType, HistoryType, MEDIANWEB_POSTE, EtatListe
from median.models import Adresse, Magasin, Config
from median.models import Printer, PrinterCommand, PrinterLabel, PrinterCommandLabel
from median.models import ListeItemModel, ListeModel, WorkItemModel, Format
from median.models import Stock, Gpao, Historique, UnitDose, Product, Cip
from median.views import PrinterViewException, PrinterView, RawConfig

from ressources.equipments.store_utils import init_equipment, add_poste_espace, remove_equipment
from ressources.equipments.store_utils import create_equipment_topo
from ressources.blueprint.product_blueprint import find_product_with_gtin

from median.base import mysql_db
from common.models import WebLogActions
from common.templating import render_template
from common.util import get_counter, test_printer_connection
from common.status import (
    HTTP_200_OK,
    HTTP_400_BAD_REQUEST,
    HTTP_500_INTERNAL_SERVER_ERROR,
)
import re

loggerExt = logging.getLogger("median.external")
loggerPrint = logging.getLogger("median.print")


def generate_external(data):
    type = data["type"]
    version = data["version"]
    equipment = None
    if type == EquipmentType.EXTERNE.value:
        depths = int(data.get("PROFONDEUR", 1))
        lines = int(data.get("LIGNE", 1))
        columns = int(data.get("COLONNE", 1))

        equipment = init_equipment(data)
        equipment.eco_type = EcoType.Externe.value
        equipment.nb_dim = 4
        equipment.lib_2 = "LIGNE"
        equipment.dim_2 = lines
        equipment.lib_3 = "COLONNE"
        equipment.dim_3 = columns
        equipment.lib_4 = "PROFONDEUR"
        equipment.dim_4 = depths
        equipment.save()

        create_equipment_topo(equipment=equipment, numversion=version)

        try:
            for line in range(lines):
                for col in range(columns):
                    for depth in range(depths):
                        add_external_adresse(
                            equipment=equipment,
                            address_label=f"{equipment.mag}.{line + 1:03}.{col + 1:03}.{depth + 1:03}",
                        )

            add_poste_espace(equipment)

        except Exception:
            remove_equipment(equipment)

    return equipment


def add_external_adresse(equipment, address_label):
    address = Adresse()
    address.adresse = address_label
    address.magasin = equipment.mag
    address.etat = EtatAdresse.Libre.value
    address.nb_div = 1
    address.dim_x = 1
    address.dim_y = 1
    address.princ = 1
    address.save()


def printLabels():
    """
    We'll print labels :
    - create stock lines : f_stock + f_gpao (type E, state A) + f_histo (type Ent)
    for each label/product :
        - Create a serial number, store it in f_sachet + f_item_w
    """
    print("printing labels!")

    data = json.loads(request.data)
    printer_pk = data.get("printer", None)
    list_pk = data.get("list", None)
    item_pk = data.get("item", None)
    print_quantity = int(data.get("quantity", None))
    scannedBox = data.get("scannedBox", None)
    scannedProduct = data.get("scannedProduct", None)

    try:
        # Get all needed objects
        printer: Printer = Printer.get_or_none(Printer.pk == printer_pk)
        item: ListeItemModel = ListeItemModel.get_or_none(ListeItemModel.pk == item_pk)
        currentList: ListeModel = ListeModel.get_or_none(ListeModel.pk == list_pk)
        box: Format = Format.get_or_none((Format.typeBac == scannedBox["typeBac"]))
        product = find_product_with_gtin(scannedProduct["gtin"])

        # Parse the CIP/GTIN
        gtin = scannedProduct["gtin"]
        gtin = gtin[1:] if len(gtin) == 14 and gtin.startswith("0") else gtin

        # Fetching the GTIN having the UCD with the less characters (priorising CNK)
        # TODO: Removed the "index = 0" by internal decision, watch out if this is used for quantities
        currentGtin: Cip = Cip.select().where((Cip.cip == gtin)).order_by(-fn.LENGTH(Cip.ucd), -Cip.dossier).get()

        # Get the address or use a placeholder
        SHV_ADR = product["adr_pre"] or "SHV"

        # Get the active store of this list
        magasin: Magasin = Magasin.get_or_none(Magasin.type_mag == currentList.zone_fin)

        # Set only one entry datetime
        entry_datetime = datetime.now()

        # Parse expiration date from YYMMDD or YYYY/MM/DD format
        date_peremption = None
        if scannedProduct["perem"]:
            # Try YYMMDD format
            yymmdd_match = re.match(r"^(\d{2})(\d{2})(\d{2})$", scannedProduct["perem"])
            if yymmdd_match:
                year, month, day = yymmdd_match.groups()
                try:
                    date_peremption = datetime(2000 + int(year), int(month), int(day))
                except ValueError as e:
                    loggerExt.error("Datamatrix exp. date error : " + str(e))
                    date_peremption = None

            # Try YYYY/MM/DD format
            else:
                yyyy_mm_dd_match = re.match(r"^(\d{4})[/\-](\d{2})[/\-](\d{2})$", scannedProduct["perem"])
                if yyyy_mm_dd_match:
                    year, month, day = yyyy_mm_dd_match.groups()
                    try:
                        date_peremption = datetime(int(year), int(month), int(day))
                    except ValueError:
                        date_peremption = None

    except Exception as err:
        loggerPrint.error("Print Labels : Error while preparing data")
        loggerPrint.error(str(err))
        return {
            "message": "Print Labels : Error while preparing data",
            "error": str(err),
        }, HTTP_500_INTERNAL_SERVER_ERROR

    with mysql_db.atomic() as transaction:
        try:
            test_printer_connection(printer.address, 2)

            # Start list and item (states will be saved at the end)
            currentList.etat = EtatListe.EnCours.value
            item.etat = EtatListe.EnCours.value
            item.qte_serv += float(print_quantity)

            if item.qte_serv > item.qte_dem:
                raise Exception("Error in the served quantity (serv > dem) !")

            # Get the batch counter
            if not currentList.id_chargement:
                currentList.id_chargement = get_counter(f"EXT_CHARGEMENT_{magasin.mag}")
                currentList.save()

            # Check existing stock for this container
            existing_stock = (
                Stock.select()
                .where((Stock.reference == item.reference) & (Stock.contenant == scannedBox["boxSerial"]))
                .order_by(-Stock.id_fifo)
            )

            if existing_stock.count() > 0:
                id_fifo = int(existing_stock.get().id_fifo) + 1
            else:
                id_fifo = 0

            # Stock = Box
            stock = Stock()
            stock.reference = item.reference
            stock.quantite = print_quantity
            stock.lot = scannedProduct["lot"]
            stock.date_peremption = date_peremption
            stock.contenant = scannedBox["boxSerial"]
            stock.adresse = SHV_ADR
            stock.magasin = magasin.mag
            stock.ucd = currentGtin.ucd
            stock.zone_admin = currentList.zone_fin
            stock.date_entree = entry_datetime
            stock.fraction = item.fraction
            stock.capa = box.nb_div
            stock.cip = gtin
            stock.id_fifo = id_fifo
            # + last serial number (see at end of process)
            stock.save()

            # GPAO = Box
            gpao = Gpao()
            gpao.etat = "A"
            gpao.ref = item.reference
            gpao.qte = print_quantity
            gpao.lot = scannedProduct["lot"]
            gpao.type_mvt = "E"
            gpao.tperemp = date_peremption
            gpao.fraction = item.fraction
            gpao.cip = gtin
            gpao.ucd = currentGtin.ucd
            gpao.magasin = magasin.mag
            gpao.info = item.info
            gpao.qte_dem = item.qte_dem
            gpao.contenant = scannedBox["boxSerial"]
            gpao.item = item.item
            gpao.pk_item = item.pk
            gpao.item_wms = item.item_wms
            gpao.id_zone = magasin.id_zone
            gpao.id_robot = magasin.id_robot
            gpao.tentree = entry_datetime
            gpao.chrono = datetime.now()
            gpao.poste = MEDIANWEB_POSTE
            gpao.user = session["username"]
            gpao.liste = currentList.liste
            # + last serial number (see at end of process)
            gpao.save()

            # f_histo = Box
            histo = Historique()
            histo.chrono = datetime.now()
            histo.reference = item.reference
            histo.quantite_mouvement = print_quantity
            histo.quantite_totale = stock.quantite
            histo.adresse = SHV_ADR
            histo.info = "CREATION BAC TAMPON"
            histo.commentaire = f"CREATION BAC TAMPON VIA {MEDIANWEB_POSTE}"
            histo.liste = currentList.liste
            histo.item = item.item
            histo.item_wms = item.item_wms
            histo.type_mouvement = HistoryType.Entree.value
            histo.lot = scannedProduct["lot"]
            histo.date_peremption = date_peremption
            histo.magasin = magasin.mag
            histo.quantite_demande = item.qte_dem
            histo.contenant = scannedBox["boxSerial"]
            histo.poste = MEDIANWEB_POSTE
            histo.utilisateur = session["username"]
            histo.ucd = currentGtin.ucd
            histo.fraction = item.fraction
            histo.pk_item = item.pk

            histo.save()

            # Item quantity served update. Close it if serv = dem
            if item.qte_dem == item.qte_serv:
                item.etat = EtatListe.Solde.value
            item.save()

            # Update list state
            nbOpenItems = (
                ListeItemModel.select()
                .where((ListeItemModel.liste == currentList.liste) & (ListeItemModel.etat != EtatListe.Solde.value))
                .count()
            )
            if nbOpenItems > 0:
                currentList.etat = EtatListe.EnCours.value
            else:
                currentList.etat = EtatListe.Solde.value
            currentList.save()

            created_serials = []

            for n_job in range(1, print_quantity + 1):
                # Process PAR DOSE (starting with 1, for the serial)
                current_date = datetime.now().strftime("%y%m%d")

                # Warn if the mag letter is empty
                if not magasin.lettre:
                    loggerExt.error(f"Magazine letter is empty for magazine {magasin.mag} (ID: {magasin.pk})")

                existing_serials = [
                    i.serial
                    for i in UnitDose.select(UnitDose.serial).where(
                        (UnitDose.serial.startswith(f"{current_date}{magasin.lettre}"))
                    )
                ]

                for n in range(9999):
                    serial = (
                        f"{current_date}{magasin.lettre}"
                        f"{str(currentList.id_chargement).rjust(3, '0')}"
                        f"{str(n + n_job).rjust(4, '0')}"
                    )

                    # Testing if the serial is free, else increment the counter until we find a free one.
                    if serial not in existing_serials:
                        break

                created_serials.append(serial)

                # f_sachet
                dose = UnitDose()
                dose.serial = serial
                dose.contenant = scannedBox["boxSerial"]
                dose.chrono = entry_datetime
                dose.save()

                # item_w
                itemw = WorkItemModel()
                itemw.liste = currentList.liste
                itemw.mode = "E"
                itemw.item = item.item
                itemw.reference = item.reference
                itemw.quantite_dem = 1
                itemw.quantite_serv = 1
                itemw.adresse = SHV_ADR
                itemw.lot = scannedProduct["lot"]
                itemw.date_peremption = date_peremption
                itemw.info = item.info
                itemw.utilisateur = session["username"]
                itemw.contenant = scannedBox["boxSerial"]
                itemw.poste = MEDIANWEB_POSTE
                itemw.item_wms = item.item_wms
                itemw.ucd = currentGtin.ucd
                itemw.fraction = item.fraction
                itemw.pk_item = item.pk
                itemw.cip = gtin
                itemw.capa = box.nb_div
                itemw.serial = serial
                itemw.urgent = 0  # Used to say if this itemw has been printed or not
                itemw.save()

                # print(itemw.serial)  # This not printer related! (console)

            # Adding the last generated serial to stock and gpao
            stock.serial = itemw.serial
            stock.save()
            histo.serial = itemw.serial
            histo.save()
            gpao.serial = itemw.serial
            gpao.save()

        except Exception as err:
            transaction.rollback()
            # print(str(err))
            loggerExt.error(str(err))
            return {"error": str(err)}, HTTP_400_BAD_REQUEST

    _print_labels(printer, created_serials)

    return {"message": "printing finished"}, HTTP_200_OK


def _render_ud_label(printer: Printer, serial: str):
    try:
        itemw: WorkItemModel = WorkItemModel.get_or_none(WorkItemModel.serial == serial)

        if not itemw:
            raise Exception(f"Print Label: WorkItem not found with serial {serial}")

        product: Product = Product.get_or_none(Product.reference == itemw.reference)
        if not product:
            raise Exception(f"Print Label: Product not found for reference {itemw.reference}")

        # get label and command info
        try:
            query = (
                Printer.select(Printer, PrinterLabel, PrinterCommandLabel)
                .join(PrinterCommandLabel, JOIN.INNER, on=(Printer.current_label_id == PrinterCommandLabel.label_id))
                .join(PrinterLabel, JOIN.INNER, on=(PrinterLabel.pk == Printer.current_label_id))
                .join(PrinterCommand, JOIN.INNER, on=(PrinterCommand.pk == PrinterCommandLabel.command_id))
                .where((Printer.pk == printer.pk) & (PrinterCommand.code == "external_ud_label"))
            )
        except Exception as e:
            loggerPrint.error(f"Print Label: Database query error: {str(e)}")
            raise Exception(f"Print Label: Error in printer query: {str(e)}")

        if query.count() == 1:
            try:
                cfg: Config = RawConfig().read("k_eco_client")

                external_ud_dict = {
                    "product_desig": product.designation,
                    "product_desig_bis": product.desig_bis or "",
                    "product_gtin": itemw.cip,
                    "date_perem": itemw.date_peremption,
                    "batch": itemw.lot,
                    "quantity": 1,
                    "serial": itemw.serial,
                    "stock_adr": itemw.adresse,
                    "dci": product.dci or "",
                    "comment": product.com_med or "",
                    "fraction": itemw.fraction,
                    "client_header": cfg.value if cfg else "Deenova",
                }

                printer_data = query[0]
                zpl_code = printer_data.printercommandlabel.print_code
                zpl_user_dict = json.loads(printer_data.printercommandlabel.print_dict)
                zpl_dict = {**zpl_user_dict, **external_ud_dict}

                # remove newlines
                zpl_code = zpl_code.replace("\n", "")

                rendered_zpl = render_template(zpl_code, zpl_dict)
                return rendered_zpl
            except json.JSONDecodeError as e:
                loggerPrint.error(f"Print Label: JSON parsing error: {str(e)}")
                raise Exception(f"Print Label: Error parsing ZPL dictionary: {str(e)}")
            except Exception as e:
                loggerPrint.error(f"Print Label: Template rendering error: {str(e)}")
                raise Exception(f"Print Label: Error rendering template: {str(e)}")
        else:
            loggerPrint.error("Print Label: No matching printer configuration found")
            raise Exception("Print Label: Error while fetching ZPL Code and Dict")

    except DoesNotExist as e:
        loggerPrint.error(f"Print Label: Database record not found: {str(e)}")
        raise Exception(f"Print Label: Required data not found: {str(e)}")
    except Exception as e:
        loggerPrint.error(f"Print Label: Unexpected error: {str(e)}")
        raise Exception(f"Print Label: {str(e)}")


def _print_labels(printer: Printer, serials: list):
    print(f"Sending : {serials}")

    try:
        with PrinterView(printer, 3, session["user_id"]) as p:
            for serial in serials:
                rendered_label = _render_ud_label(printer, serial)
                print_result = _print_label(p, rendered_label, serial)
                if print_result:
                    itemw: WorkItemModel = WorkItemModel.get(WorkItemModel.serial == serial)
                    itemw.urgent = 1
                    itemw.save()
                else:
                    loggerPrint.error(f"Failed to print label with serial {serial}")

    except PrinterViewException as e:
        loggerPrint.error(f"PrinterView: {e}")
    except Exception as e:
        loggerPrint.error(str(e))


def _print_label(p: PrinterView, zpl: str, serial: str):
    print_result = p.send(zpl, serial)
    loggerPrint.debug(f"Print result for printer {p.printer.address}: {print_result}")
    return print_result


def reprintLabel(serial: str, printer_pk: int):
    """Reprint a label given its serial number and printer ID"""
    try:
        printer: Printer = Printer.get_or_none(Printer.pk == printer_pk)
        if not printer:
            loggerPrint.error(f"Printer pk {printer_pk} not found")
            return {"error": "Printer not found"}, HTTP_400_BAD_REQUEST

        rendered_label = _render_ud_label(printer, serial)

        with PrinterView(printer, 3, session["user_id"]) as p:
            print_result = _print_label(p, rendered_label, serial)
            if print_result:
                loggerPrint.info("Reprinted the label {serial} with success")
                log_external(session.get("username"), "external_unload", f"Reprinted label {serial}")
                itemw: WorkItemModel = WorkItemModel.get(WorkItemModel.serial == serial)
                itemw.urgent = 1
                itemw.save()
                return {"message": f"Label {serial} reprinted successfully"}, HTTP_200_OK
            else:
                loggerPrint.error("Error trying reprinting the label {serial}")
            return {"error": "Failed to print label"}, HTTP_500_INTERNAL_SERVER_ERROR
    except Exception as err:
        loggerPrint.error(f"Error reprinting label {serial}: {str(err)}")
        return {"error": str(err)}, HTTP_500_INTERNAL_SERVER_ERROR


def unload_item(item_pk, box_code):
    # Use an ListeItem pk + container code to move the stockline, from zone_deb to zone_fin (tampon)

    try:
        item: ListeItemModel = ListeItemModel.get(ListeItemModel.pk == item_pk)
        liste_obj: ListeModel = ListeModel.get(ListeModel.liste == item.liste)
        stockLines = Stock.select(
            Stock.pk, Stock.reference, Stock.contenant, Stock.adresse, Stock.ucd, Stock.cip, Stock.capa, Stock.quantite
        ).where((Stock.reference == item.reference) & (Stock.fraction == item.fraction) & (Stock.contenant == box_code))

        distinctBoxes = stockLines.select(Stock.contenant, fn.SUM(Stock.quantite)).distinct()
        if distinctBoxes.count() != 1:
            return {"message": "External unload : fetching the right stock line failed"}, HTTP_500_INTERNAL_SERVER_ERROR
        else:
            destination_mag: Magasin = Magasin.get(Magasin.type_mag == liste_obj.zone_fin)

        with mysql_db.atomic() as transaction:
            try:
                for stock in stockLines:
                    # For intellisense
                    stock: Stock = stock

                    # item_w
                    itemw = WorkItemModel()
                    itemw.liste = item.liste
                    itemw.mode = item.mode
                    itemw.item = item.item
                    itemw.reference = item.reference
                    itemw.quantite_dem = item.qte_dem
                    itemw.quantite_serv = item.qte_serv
                    itemw.adresse = stock.adresse
                    itemw.lot = stock.lot
                    itemw.date_peremption = stock.date_peremption
                    itemw.info = item.info
                    itemw.utilisateur = session["username"]
                    itemw.contenant = stock.contenant
                    itemw.poste = MEDIANWEB_POSTE
                    itemw.item_wms = item.item_wms
                    itemw.ucd = stock.ucd
                    itemw.fraction = item.fraction
                    itemw.pk_item = item.pk
                    itemw.cip = stock.cip
                    itemw.capa = stock.capa
                    itemw.serial = stock.serial
                    itemw.save()

                    loggerExt.info("Save the movement to the history table")

                    # Histo
                    histo: Historique = Historique()
                    histo.chrono = datetime.now()
                    histo.reference = item.reference
                    histo.adresse = stock.adresse
                    histo.adresse_from = stock.adresse
                    histo.adresse_from = item.adresse_pref
                    histo.quantite_mouvement = item.qte_serv
                    histo.liste = item.liste
                    histo.quantite_totale = item.qte_serv  # We only move this line
                    histo.service = liste_obj.service
                    histo.type_mouvement = HistoryType.Transfert.value
                    histo.lot = item.lot
                    histo.date_peremption = stock.date_peremption
                    histo.contenant = item.contenant
                    histo.poste = "MEDIANWEB"
                    histo.ucd = stock.ucd
                    histo.magasin = item.magasin
                    histo.fraction = item.fraction
                    histo.id_pilulier = item.id_pilulier
                    histo.item = item.item
                    histo.quantite_demande = item.qte_dem
                    histo.pk_liste = liste_obj.pk
                    # histo.serial = this is not yet known before the printing
                    histo.id_prescription = item.id_presc
                    histo.quantite_prescrite = item.qte_prescrite
                    histo.item_wms = item.item_wms
                    # histo.date_debut = liste_obj.ddeb
                    histo.info = f"Deplacement vers {liste_obj.zone_fin}"
                    histo.pk_item = item.pk
                    histo.id_plateau = item.id_plateau
                    histo.numero_pilulier = item.no_pilulier
                    histo.commentaire = item.info
                    histo.utilisateur = session["username"]
                    histo.save()

                    # Move the item !
                    rows_updated = (
                        Stock.update(
                            magasin=destination_mag.mag,
                            adresse=destination_mag.mag,
                            zone_admin=destination_mag.type_mag,
                        )
                        .where((Stock.pk == stock.pk))
                        .execute()
                    )

                    if rows_updated == 0:
                        loggerExt.warning(f"No stock records were updated for item {item_pk}")
                    else:
                        loggerExt.info(f"Moved stock pk: {stock.pk} for item {item_pk} to {destination_mag.mag}")

                    # And update the list
                    # TODO: We won't divide the box content. Can it be possible to have serv > dem ?
                    item.qte_serv += stock.quantite
                    if item.qte_serv >= item.qte_dem:
                        item.etat = EtatListe.Solde.value
                    else:
                        item.etat = EtatListe.EnCours.value
                    item.save()

                    nbNonFinishedItems = (
                        ListeItemModel.select()
                        .where((ListeItemModel.liste == item.liste) & (ListeItemModel.etat != "S"))
                        .count()
                    )
                    if nbNonFinishedItems > 0:
                        liste_obj.etat = EtatListe.EnCours.value
                    else:
                        liste_obj.etat = EtatListe.Solde.value
                    liste_obj.save()

            except Exception as err:
                transaction.rollback()
                print(str(err))
                loggerExt.error(str(err))
                return {"error": str(err)}, HTTP_400_BAD_REQUEST

        return {"message": "Item moved successfully"}, HTTP_200_OK
    except DoesNotExist as e:
        loggerExt.error(f"External unload: DoesNotExist error - {str(e)}")
        return {"error": f"Item or stock not found: {str(e)}"}, HTTP_400_BAD_REQUEST
    except Exception as e:
        loggerExt.error(f"External unload: Unexpected error - {str(e)}")
        return {"error": f"An unexpected error occurred: {str(e)}"}, HTTP_500_INTERNAL_SERVER_ERROR


def generate_external_box_code():
    counter = get_counter("EXT_SHELVEBOX")
    newBoxCode = f"E{str(counter).rjust(9, '0')}"
    return newBoxCode


def log_external(username: str, action: str, message: str):
    """
    Add new log for external

    :param username: User made the action to log
    :param action:
    :param message: message to log
    """
    loggerExt.info("External[%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()
