"""
Completion Service for AidePlus
Handles item completion logic for both nominative and global modes
"""

import logging
import re
from datetime import datetime
from flask import session
from peewee import fn, JOIN, Value

from median.models import ItemValide, ListeValide, Stock, Gpao, Product
from median.models import ListeModel, ListeItemModel, UnitDose, Historique, Ucd, Cip
from median.database import mysql_db
from median.constant import (
    TypeServiListe,
    TypeMouvementGpao,
    HistoryType,
    MEDIANWEB_POSTE,
    TypeEtatGpao,
    CONFIG_WEB_CLE,
)
from common.status import (
    HTTP_200_OK,
    HTTP_500_INTERNAL_SERVER_ERROR,
    HTTP_404_NOT_FOUND,
)
from ..utils.data_mapper import DataMapper

logger = logging.getLogger("median.completion")


class CompletionService:
    """Service for handling item completion operations"""

    def __init__(self):
        self.data_mapper = DataMapper()

    def complete_item(self, container_id, loading_id, reference, products, total_quantity, nominative_mode, ward=None):
        """
        Main completion method that delegates to nominative or global mode handlers
        """
        try:
            if nominative_mode:
                return self._complete_nominative_item(container_id, loading_id, reference, products, total_quantity)
            else:
                return self._complete_global_item(container_id, ward, reference, products, total_quantity)
        except Exception as err:
            logger.error(f"Completion error: {str(err)}")
            return {"message": str(err)}, HTTP_500_INTERNAL_SERVER_ERROR

    def _complete_nominative_item(self, container_id, loading_id, reference, products, total_quantity):
        """Handle nominative mode item completion"""
        now = datetime.now()

        try:
            items = (
                ItemValide.select(ItemValide, ListeValide)
                .join(ListeValide, JOIN.INNER, on=(ListeValide.pk == ItemValide.liste_pk))
                .where(
                    (ItemValide.id_pilulier == container_id)
                    & (ListeValide.id_chargement == loading_id)
                    & (ItemValide.reference == reference)
                )
            )

            if items.count() == 0:
                logger.error(
                    f"No items found for container={container_id}, loading={loading_id}, reference={reference}"
                )
                return {"error": "No items found to complete"}, HTTP_404_NOT_FOUND

        except Exception as e:
            logger.error(f"Error fetching nominative items: {str(e)}")
            return {"error": f"Failed to fetch items: {str(e)}"}, HTTP_500_INTERNAL_SERVER_ERROR

        with mysql_db.atomic() as transaction:
            try:
                return self._process_completion_products(items, products, now, False)
            except Exception as err:
                transaction.rollback()
                logger.error(f"Nominative completion transaction error: {str(err)}")
                return {"message": "An error has occured"}, HTTP_500_INTERNAL_SERVER_ERROR

    def _complete_global_item(self, container_id, ward, reference, products, total_quantity):
        """Handle global mode item completion"""
        now = datetime.now()

        try:
            query_liste = (
                ListeItemModel.select(
                    ListeItemModel.pk,
                    ListeItemModel.reference,
                    ListeItemModel.fraction,
                    ListeItemModel.liste,
                    ListeItemModel.qte_dem,
                    ListeItemModel.qte_serv,
                    ListeItemModel.type_servi,
                    ListeItemModel.num_ipp,
                    ListeModel.service,
                    Value("liste_item").alias("source"),
                )
                .join(ListeModel, JOIN.INNER, on=(ListeModel.liste == ListeItemModel.liste))
                .where(
                    (ListeItemModel.reference == reference)
                    & (ListeItemModel.liste.contains("GLOBAL"))
                    & (ListeItemModel.num_ipp.contains("GLOBAL"))
                    & (ListeModel.service == ward["code"])
                    & (fn.DATE(ListeModel.ddeb) == ward["chrono"])
                    & (
                        ListeItemModel.type_servi
                        << [TypeServiListe.GlobaleBoite.value, TypeServiListe.GlobalePilulier.value]
                    )
                )
            )

            query_valide = (
                ItemValide.select(
                    ItemValide.pk,
                    ItemValide.reference,
                    ItemValide.fraction,
                    ListeValide.liste,
                    ItemValide.quantite_dem.alias("qte_dem"),
                    ItemValide.quantite_serv.alias("qte_serv"),
                    ItemValide.type_servi,
                    ItemValide.num_ipp,
                    ListeValide.service,
                    Value("item_valide").alias("source"),
                )
                .join(ListeValide, JOIN.INNER, on=(ListeValide.pk == ItemValide.liste_pk))
                .where(
                    (ItemValide.reference == reference)
                    & (ListeValide.liste.contains("GLOBAL"))
                    & (ListeValide.num_ipp.contains("GLOBAL"))
                    & (ListeValide.service == ward["code"])
                    & (fn.DATE(ListeValide.ddeb) == ward["chrono"])
                    & (
                        ItemValide.type_servi
                        << [TypeServiListe.GlobaleBoite.value, TypeServiListe.GlobalePilulier.value]
                    )
                )
            )

            # Union both queries (same pattern as ward_service.py)
            union_query = query_liste.union(query_valide)

            # Execute and get results
            items_list = list(union_query)

            if len(items_list) == 0:
                logger.error(f"No global items found for ward={ward['code']}, reference={reference}")
                return {"error": "No items found to complete"}, HTTP_404_NOT_FOUND

            # Calculate total demand
            # total_dem = sum(item.qte_dem - item.qte_serv for item in items_list)

        except Exception as e:
            logger.error(f"Error fetching global items: {str(e)}")
            return {"error": f"Failed to fetch items: {str(e)}"}, HTTP_500_INTERNAL_SERVER_ERROR

        # Check if we have enough products to complete the request
        # NOTE: This is commented out, because we want to be able to complete "incomplete" items
        # if total_quantity < total_dem:
        #     logger.error(f"Not enough products scanned. Need {total_dem}, got {total_quantity}")
        #     return {
        #         "error": f"Not enough products scanned. Need {total_dem}, got {total_quantity}"
        #     }, HTTP_400_BAD_REQUEST

        with mysql_db.atomic() as transaction:
            try:
                # In global mode, we need to create ItemValide records first
                new_items = []
                for item in items_list:
                    # Only process ListeItemModel items (source='liste_item'), ItemValide items are already processed
                    if item.source == "liste_item":
                        new_item = self.data_mapper.copy_item_to_valide(item)
                        new_items.append(new_item.pk)
                    else:
                        new_items.append(item.pk)

                items_valide = ItemValide.select(ItemValide).where((ItemValide.pk << new_items))
                return self._process_completion_products(items_valide, products, now, True)
            except Exception as err:
                transaction.rollback()
                logger.error(f"Global completion transaction error: {str(err)}")
                return {"message": str(err)}, HTTP_500_INTERNAL_SERVER_ERROR

    def _process_completion_products(self, items, products, now, is_global_mode):
        """Process the completion of products for both modes"""

        for product in products:
            self._process_item_completion(items, now, product)

        # We might also complete with no products, that's a completion that has been declared empty
        if not products:
            for item in items:
                self._process_item_completion(
                    items.where(ItemValide.pk == item.pk),
                    now,
                    {
                        "quantity": 0,
                        "batch": "",
                        "reference": item.reference,
                        "serial": "0",  # This is a dummy serial, but it will be checked for 0 in the called function
                        "labelSerial": None,
                        "scannedGtin": None,
                        "perem": None,
                    },
                )

        return {"message": "completion.plus.ui.completion_success"}, HTTP_200_OK

    def _process_item_completion(self, items, now, product):
        # TODO: This function is made to process an item completion, but it can also be used to process an
        # empty completion
        # Review the logic to make sure that this won't make the regular completion store bad data
        # (when testing for serial == "0")

        product_quantity = float(product["quantity"])
        product_batch = product["batch"]
        product_reference = product["reference"]
        product_serial = product["serial"]  # Might be overwritten

        is_full_pack = product.get("labelSerial") is not None
        if is_full_pack:
            product_serial = product["labelSerial"]
            product_gtin = product["scannedGtin"]
            product_peremption = self._parse_datetime(product["perem"])

        itemToCompleteQuery = (
            items.where(ItemValide.quantite_dem - ItemValide.quantite_serv > 0)
            .order_by(-(ItemValide.quantite_dem - ItemValide.quantite_serv))
            .limit(1)
        )

        if itemToCompleteQuery.count() == 1:
            itemToComplete = itemToCompleteQuery.get()
            listToComplete = ListeValide.get(ListeValide.pk == itemToComplete.liste_pk)
            itemToComplete.quantite_serv += product_quantity
            itemToComplete.save()

            # Get product info
            ref = (
                Product.select(Product, Ucd, Cip)
                .join(Ucd, JOIN.LEFT_OUTER, on=(Ucd.reference == Product.reference))
                .join(Cip, JOIN.LEFT_OUTER, on=(Cip.ucd == Ucd.ucd))
                .where((Product.reference == product_reference))
                .get()
            )

            unitDose: UnitDose = UnitDose.get_or_none(UnitDose.serial == product_serial)

            if not unitDose and product_serial != "0":  # 0 will be a validated empty completion
                logger.error(f"No container found for the serial={product_serial}")
                raise Exception(f"No container found for serial={product_serial}")

            # STOCK handling
            stock = self._get_stock_for_completion(product_serial, product_batch, unitDose, is_full_pack)

            if not is_full_pack and (not stock or stock.quantite < product_quantity) and product_serial != "0":
                logger.error(
                    f"No stock found or insufficient quantity for serial={product_serial} "
                    f"and container={unitDose.contenant}"
                )
                raise Exception(
                    f"No stock or not enough stock found for serial={product_serial} and container={unitDose.contenant}"
                )

            address = stock.adresse if stock else ""
            quantity = stock.quantite if stock else 0

            if product_serial == "0":
                # If this is an empty completion, we don't have a container
                container = ""
            else:
                # Only the "empty completion" can have no container, otherwise we must have a unitdose (thus container)
                container = unitDose.contenant

            # Create history and GPAO records
            self._create_completion_history(
                itemToComplete,
                listToComplete,
                product,
                now,
                address,
                quantity,
                ref,
                container,
                is_full_pack,
                product_peremption if is_full_pack else None,
            )
            self._create_completion_gpao(
                itemToComplete,
                listToComplete,
                product,
                now,
                quantity,
                ref,
                container,
                is_full_pack,
                product_peremption if is_full_pack else None,
                product_gtin if is_full_pack else None,
            )

            # Decrement the stock
            if stock and not is_full_pack:
                if stock.quantite > product_quantity:
                    stock.quantite -= product_quantity
                    stock.save()
                else:
                    logger.info("Stock depleted, deleting the line.")
                    stock.delete_instance()
            else:
                pass  # A full pack never entered the stock, no decrement needed
                # or this is an empty completion, no stock to decrement

                # Remove from UnitDose
            if not is_full_pack and product_quantity > 1:
                # This is a passbox
                UnitDose.delete().where(UnitDose.contenant == unitDose.contenant).execute()
                logger.info(f"Deleted UnitDoses for container={unitDose.contenant}")
            else:
                # This is a full pack or a unit dose. No serial to delete for a full pack
                if unitDose:
                    unitDose.delete_instance()
                    logger.info(f"Deleted UnitDose for serial={product_serial}")

            logger.info(
                f"Completion : pk={itemToComplete.pk} => {itemToComplete.quantite_serv}/{itemToComplete.quantite_dem}"
            )

        else:
            raise Exception("Couldn't find a ItemValide to complete !")

    def _get_stock_for_completion(self, product_serial, product_batch, unitDose: UnitDose, is_full_pack):
        """Get stock for completion based on serial management configuration"""
        from median.views import RawConfig

        if not unitDose:
            logger.info(f"No UnitDose found for serial={product_serial}, skipping stock retrieval.")
            return None

        serial_management = RawConfig(MEDIANWEB_POSTE, CONFIG_WEB_CLE).read("k_web_serial_management")
        INCORRECT_SERIAL_MANAGEMENT = "incorrect"

        stock: Stock = Stock.get_or_none((Stock.contenant == unitDose.contenant) & (Stock.lot == product_batch))
        if serial_management.value == INCORRECT_SERIAL_MANAGEMENT and not stock:
            # Use the fix method that should be implemented elsewhere
            return self._get_stock_with_fix(product_serial)
        else:
            return stock

    def _get_stock_with_fix(self, serial_code):
        # TODO: This should be removed once the stock management is fixed
        contenantWithMaxStock: Stock = Stock.select(Stock.contenant).where(
            Stock.serial.contains(fn.LEFT(serial_code, 10)) & (Stock.magasin == "SHV")
        )

        lostStock: Stock = (
            Stock.select(Stock)
            .where(
                (Stock.contenant.in_(contenantWithMaxStock))
                & (fn.RIGHT(Stock.serial, 3) >= fn.RIGHT(serial_code, 3))
                & (
                    fn.RIGHT(Stock.serial, 3)
                    < fn.RIGHT(serial_code, 3)
                    + (
                        Stock.select(fn.MAX(Stock.quantite)).where(
                            Stock.serial.contains(fn.LEFT(serial_code, 10)) & (Stock.magasin == "SHV")
                        )
                    ).scalar()
                )
            )
            .order_by(+Stock.serial)
            .get_or_none()
        )

        return lostStock

    def _parse_datetime(self, date_str):
        # Parse expiration date from YYMMDD or YYYY/MM/DD format
        date_peremption = None
        if date_str:
            # Try YYMMDD format
            yymmdd_match = re.match(r"^(\d{2})(\d{2})(\d{2})$", date_str)
            if yymmdd_match:
                year, month, day = yymmdd_match.groups()
                try:
                    date_peremption = datetime(2000 + int(year), int(month), int(day))
                except ValueError as e:
                    logger.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})$", date_str)
                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

        return date_peremption

    def _create_completion_history(
        self,
        itemToComplete,
        listToComplete,
        product,
        now,
        address,
        quantity,
        ref,
        container,
        is_full_pack,
        product_peremption,
    ):
        """Create history record for completion"""
        histo = Historique()
        histo.chrono = now
        histo.reference = product["reference"]
        histo.adresse = address
        histo.adresse_from = address
        histo.quantite_mouvement = float(product["quantity"])
        histo.liste = listToComplete.liste
        histo.quantite_totale = quantity - float(product["quantity"])
        histo.service = listToComplete.service
        histo.type_mouvement = HistoryType.Sortie.value
        histo.lot = product["batch"]
        histo.contenant = container
        histo.poste = MEDIANWEB_POSTE
        histo.id_pilulier = itemToComplete.id_pilulier
        histo.item = itemToComplete.item
        histo.quantite_demande = itemToComplete.quantite_dem
        histo.pk_liste = listToComplete.pk
        histo.serial = product["serial"]
        histo.moment = itemToComplete.moment
        histo.heure = itemToComplete.heure
        histo.date_prise = itemToComplete.dtprise
        histo.id_prescription = itemToComplete.id_presc
        histo.quantite_prescrite = itemToComplete.qte_prescrite
        histo.item_wms = itemToComplete.item_wms
        histo.date_debut = listToComplete.ddeb
        histo.info = f"Sortie pour complement pilulier {itemToComplete.id_pilulier}"
        histo.pk_item = itemToComplete.pk
        histo.id_plateau = itemToComplete.id_plateau
        histo.numero_pilulier = itemToComplete.no_pilulier
        histo.commentaire = itemToComplete.info
        histo.utilisateur = session["username"]
        histo.ipp = listToComplete.num_ipp
        histo.sejour = listToComplete.num_sej

        if not is_full_pack:
            stock = Stock.get_or_none((Stock.contenant == container) & (Stock.lot == product["batch"]))
            if stock:
                histo.date_peremption = stock.date_peremption
                histo.ucd = stock.ucd
                histo.magasin = stock.magasin
                histo.fraction = stock.fraction
        else:
            histo.date_peremption = product_peremption
            histo.ucd = ref.ucd.ucd
            histo.magasin = ""
            histo.fraction = 100
            histo.info = histo.info + " [ARX]"

        histo.save()

    def _create_completion_gpao(
        self,
        itemToComplete: ItemValide,
        listToComplete: ListeValide,
        product,
        now,
        single_stock_quantity,
        ref: Product,
        container,
        is_full_pack,
        product_peremption,
        product_gtin,
    ):
        """Create GPAO record for completion"""
        # get stock before movement
        total_stock_quantity = 0
        if not is_full_pack:
            stock: Stock = (
                Stock.select(fn.SUM(Stock.quantite).alias("quantite"))
                .where((Stock.reference == itemToComplete.reference) & (Stock.fraction == itemToComplete.fraction))
                .group_by(Stock.reference, Stock.fraction)
            )
            if stock:
                total_stock_quantity = stock.get().quantite if stock.count() > 0 else 0

        gpao = Gpao()
        gpao.etat = TypeEtatGpao.DRAFT.value
        gpao.chrono = now
        gpao.poste = MEDIANWEB_POSTE
        gpao.lot = product["batch"]
        gpao.qte_avant = total_stock_quantity
        gpao.qte_apres = total_stock_quantity - float(product["quantity"])
        gpao.ref = product["reference"]
        gpao.qte = float(product["quantity"])
        gpao.type_mvt = TypeMouvementGpao.OUTPUT.value
        gpao.liste = listToComplete.liste
        gpao.dest = listToComplete.service
        gpao.user = session["username"]
        gpao.desig = ref.designation
        gpao.item = itemToComplete.item
        gpao.info = itemToComplete.info
        gpao.item_wms = itemToComplete.item_wms
        # It will be only the quantity of this single movement, not the total of the item
        # that's what the machines do...
        gpao.qte_dem = product["quantity"]
        gpao.qte_disp = single_stock_quantity
        gpao.id_pilulier = itemToComplete.id_pilulier
        gpao.ipp = listToComplete.num_ipp
        gpao.sejour = listToComplete.num_sej
        gpao.serial = product["serial"]
        gpao.contenant = container
        gpao.pk_item = itemToComplete.pk
        gpao.id_robot = ""  # TODO: a completer?
        gpao.id_zone = ""  # TODO: a completer?

        if not is_full_pack:
            stock = Stock.get_or_none((Stock.contenant == container) & (Stock.lot == product["batch"]))
            if stock:
                gpao.ucd = stock.ucd
                gpao.cip = stock.cip
                gpao.fraction = stock.fraction
                gpao.tperemp = stock.date_peremption
                gpao.magasin = stock.magasin
        else:
            gpao.ucd = ref.ucd.ucd
            gpao.cip = product_gtin  # This is the scanned one
            gpao.fraction = 100
            gpao.tperemp = product_peremption
            gpao.magasin = None

        if product['quantity'] == 0:
            # test
            pass

        gpao.save()
