import json
import operator
import os
import uuid
import logging
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from enum import Enum
from functools import reduce
from io import BytesIO
from zipfile import ZipFile

from PIL import Image
from flask import request
from median.models import Dpm, DpmCheck, DpmAction, User, Gtin, Product, Ucd
from median.views import RawConfig
from peewee import DoesNotExist, fn, JOIN
from median.database import mysql_db

from ressources.blueprint.dpm.action_dto import AnalyzeDTO

logger = logging.getLogger("median.webserver.dpm")

ZIP_MIMETYPE = (" application/zip", "application/octet-stream", "application/x-zip-compressed", "multipart/x-zip")


class DpmState(Enum):
    # This is also defined in dpmItemListComponent, make sure these are the same !
    Draft = 0
    ToTest = 1
    ToValidate = 2
    Validated = 3
    Archive = 4


def create_dpm(cip, index, usr, gtin, type_machine, zip_file, data):
    dpm = Dpm()
    dpm.cip = cip
    dpm.type_machine = type_machine
    dpm.dossier = index
    dpm.creation = datetime.now()
    dpm.creator_pk = usr.pk
    dpm.creator = usr.username
    dpm.zip_file = zip_file
    dpm.closing_date = None
    dpm.qt_blister = data["qty_blister"]
    dpm.qt_boite = data["qty_box"]
    dpm.qt_pass = data["qty_pass_box"]
    dpm.DU_boite_pass = data["type_du"]

    if gtin is not None:
        dpm.ucd_cip = gtin.pk
        if not dpm.ucd:
            dpm.ucd = Gtin.get_by_id(gtin.pk).ucd
    else:
        dpm.ucd_cip = None

    dpm.save()

    for nb in range(3):
        actions = 1

        if nb == 0:
            actions = 5
        elif nb == 2:
            actions = 3

        check_tech = DpmCheck()
        check_tech.dpm = dpm
        check_tech.num = nb
        check_tech.profil = "TECH"
        check_tech.save()
        _save_actions(check_obj=check_tech, actions=actions)

        check_pharma = DpmCheck()
        check_pharma.dpm = dpm
        check_pharma.num = nb
        check_pharma.profil = "PHARMACIEN"
        check_pharma.save()
        _save_actions(check_obj=check_pharma, actions=actions)


def upload_dpms(current_request, identity) -> list[dict]:
    result = []
    usr = User.get(User.pk == identity)

    cfg = RawConfig("MEDIANWEB").read("k_eco_dir_dpm")
    folder = cfg.value if cfg is not None else None

    for fname in current_request.files:
        f = current_request.files.get(fname)
        type = current_request.form.get("type", None)
        file_name = None
        if f.mimetype in ZIP_MIMETYPE:
            file_name = f"{uuid.uuid4()}.zip"

        if file_name is not None:
            path = f"{folder}\\{file_name}"
            f.save(path)
            try:
                with ZipFile(path) as dpmzip:
                    # Search in the MSR Zip files
                    info = dpmzip.infolist()
                    xml_files = [f for f in info if f.filename.endswith(".xml")]
                    if not xml_files:
                        raise Exception("No XML file found in MSR")

                    # For Each XML, extract DPM Data
                    for xml_file in xml_files:
                        upload_status = {
                            "status": False,
                            "cip": "",
                            "message": "",
                        }
                        result.append(upload_status)

                        # Check the CIP
                        t = xml_file.filename.split("/")
                        folder_cip = t[0]
                        index = t[1]
                        upload_status["cip"] = folder_cip

                        # Explore the XML data
                        xml_content = dpmzip.read(xml_file)
                        tree = ET.fromstring(xml_content.lower())  # lower, because CIP isn't always in all uppercase.
                        cip_node = tree.find(".//blister//cip")
                        if cip_node is not None:
                            cip_value = cip_node.text
                        else:
                            upload_status["message"] = "CIP Not found in XML"
                            logger.error = f"MSR Upload : {folder_cip} - CIP Not found in XML"
                            continue  # Go to next MSR folder / XML File

                        if folder_cip != cip_value:
                            upload_status["message"] = "Inconsistency in CIP between XML and folder"
                            logger.error = f"MSR Upload : {folder_cip} - Inconsistency in CIP between XML and folder"
                            continue  # Go to next MSR folder / XML File

                        # Get the existing data, if able
                        subquery = (
                            Gtin.select()
                            .join(Ucd, JOIN.LEFT_OUTER, on=Gtin.ucd == Ucd.ucd)
                            .join(Product, JOIN.LEFT_OUTER, on=Ucd.reference == Product.reference)
                            .where((Gtin.cip == cip_value))
                            .order_by(-Gtin.dossier, -Gtin.date_der_coupe)
                        )

                        try:
                            query = subquery.where(Gtin.dossier == index)
                            gtin = query.get()

                            if gtin.last_user == "SYNCHROCIP":
                                upload_status["message"] = "dpm.error.dpm_already_deployed"
                                continue

                        except DoesNotExist:
                            # Fetch a possible older version, only used for the UCD field
                            query = subquery.where(Gtin.cip == cip_value).order_by(-Gtin.dossier).limit(1)
                            gtin = query.get_or_none()

                        # Extract Data
                        blister = tree.find(".//blister")
                        data = {
                            "qty_box": blister.findtext("qte_etui", "0"),
                            "qty_blister": blister.findtext("qte_blister", "0"),
                            "qty_pass_box": blister.findtext("qte_pass", "0"),
                            "type_du": blister.findtext("type_du", "0"),
                        }

                        if not __dpm_valide(cip=cip_value, dossier=index, type_machine=type):
                            target_file = f"{uuid.uuid4()}.zip"
                            target_path = f"{folder}\\{target_file}"
                            with ZipFile(target_path, "a") as target_archive:
                                file_to_saved = filter(
                                    lambda s: (not s.is_dir()) and (s.filename.startswith(f"{cip_value}/{index}")), info
                                )

                                for file in file_to_saved:
                                    target_archive.writestr(file, dpmzip.read(file.filename))

                            with mysql_db.atomic() as transaction:
                                try:
                                    dpm = __get_dpm(cip=cip_value, dossier=index, type_machine=type)
                                    if dpm is None:
                                        create_dpm(
                                            cip=cip_value,
                                            index=index,
                                            gtin=gtin,
                                            usr=usr,
                                            type_machine=type,
                                            zip_file=target_file,
                                            data=data,
                                        )
                                    else:
                                        __update_dpm(dpm=dpm, new_file=target_file, usr=usr, gtin=gtin, data=data)

                                    upload_status["status"] = True

                                except Exception as e:
                                    logger.error(str(e.args))
                                    transaction.rollback()
                                    if os.path.exists(target_path):
                                        os.remove(target_path)
            except Exception as e:
                logger.error(e.args)
            finally:
                if os.path.exists(path):
                    os.remove(path)
    return result


def __update_dpm(dpm: Dpm, new_file: str, usr: User, gtin: Gtin, data: object):
    cfg = RawConfig("MEDIANWEB").read("k_eco_dir_dpm")
    folder = cfg.value if cfg is not None else None

    if folder is not None and os.path.exists(f"{folder}/{dpm.zip_file}"):
        os.remove(f"{folder}/{dpm.zip_file}")

    dpm.zip_file = new_file
    dpm.modification_date = datetime.now()
    dpm.modifier_pk = usr.pk
    dpm.qt_blister = data["qty_blister"]
    dpm.qt_boite = data["qty_box"]
    dpm.qt_pass = data["qty_pass_box"]
    dpm.DU_boite_pass = data["type_du"]

    if gtin is not None:
        dpm.ucd_cip = gtin.pk
        if not dpm.ucd:
            dpm.ucd = Gtin.get_by_id(gtin.pk).ucd
    else:
        dpm.ucd_cip = None

    dpm.save()


def _save_actions(check_obj, actions):
    for action in range(actions):
        action_obj = DpmAction()
        action_obj.num = action
        action_obj.dpm_check = check_obj
        action_obj.save()


def get_state_condition(state):
    thirty_days_ago = datetime.now() - timedelta(days=30)
    if state == DpmState.Archive.value:
        # Validated and older than 30 days
        return Dpm.validation_date.is_null(False) & (Dpm.validation_date < thirty_days_ago)
    elif state == DpmState.Validated.value:
        # Validate and not older than 30 days
        return Dpm.validation_date.is_null(False) & (Dpm.validation_date >= thirty_days_ago)
    elif state == DpmState.ToValidate.value:
        return (Dpm.validation_date.is_null()) & (Gtin.pk.is_null(False)) & (Gtin.qt_coupe > 0)
    elif state == DpmState.ToTest.value:
        return (Dpm.validation_date.is_null()) & (Gtin.pk.is_null(False)) & (Gtin.qt_coupe == 0)
    elif state == DpmState.Draft.value:
        return Dpm.closing_date.is_null() & (Dpm.validation_date.is_null())
    logger.error("DPM States : Unknown state")
    raise Exception("Missing state definitions")


def all_dpms_request():
    data = json.loads(request.data)
    search_list = data.get("criterias", [])
    equipment_types = data.get("equipmentTypes", [])
    states = data.get("types", [])
    # start = data['start']
    # end = data['end']

    creator_alias = User.alias("creator")
    validator_alias = User.alias("validator")
    close_alias = User.alias("close")
    modification_alias = User.alias("modification")

    expr = True  # (Dpm.creation >= start) & (Dpm.creation <= end)

    if len(equipment_types) > 0:
        expr &= Dpm.type_machine << equipment_types

    if len(states) > 0:
        lst = [get_state_condition(state) for state in states]
        search = reduce(operator.or_, lst)
        expr = reduce(operator.and_, [expr, search])

    if len(search_list) > 0:
        lst = list(
            map(
                lambda s: (
                    (Product.designation.contains(s.strip()))
                    | (Product.reference.contains(s.strip()))
                    | (Dpm.cip.contains(s.strip()))
                ),
                search_list,
            )
        )
        search = reduce(operator.and_, lst)
        expr = reduce(operator.and_, [expr, search])

    nb_notchecked_subquery = (
        DpmAction.select(fn.COUNT(DpmAction.pk))
        .join(DpmCheck)
        .where(((~DpmAction.is_checked) | (DpmAction.is_checked.is_null())) & (DpmCheck.dpm == Dpm.pk))
    )

    return (
        (
            Dpm.select(
                Dpm.pk.alias("pk"),
                Dpm.dossier.alias("dossier"),
                Dpm.cip.alias("cip"),
                Dpm.type_machine.alias("equipment_type"),
                Dpm.creator,
                Dpm.creation.alias("creation"),
                Dpm.zip_file,
                creator_alias.avatar.alias("creator_avatar"),
                nb_notchecked_subquery.alias("not_checked"),
                Dpm.validation_date.alias("validation"),
                validator_alias.username.alias("validator"),
                validator_alias.avatar.alias("validator_avatar"),
                Dpm.closing_date.alias("close"),
                close_alias.username.alias("close_user"),
                close_alias.avatar.alias("close_avatar"),
                Dpm.modification_date.alias("modification"),
                modification_alias.username.alias("modification_user"),
                modification_alias.avatar.alias("modification_avatar"),
                Product.reference,
                Product.designation,
                Dpm.ucd,
                Dpm.qt_blister,
                Dpm.qt_boite,
                Dpm.qt_pass,
                Dpm.DU_boite_pass,
                Gtin.pk.alias("gtin_pk"),
                Gtin.qt_coupe,
                Gtin.date_der_coupe,
            )
            .join(creator_alias, JOIN.LEFT_OUTER, on=Dpm.creator_pk == creator_alias.pk)
            .join(validator_alias, JOIN.LEFT_OUTER, on=Dpm.validator_pk == validator_alias.pk)
            .join(close_alias, JOIN.LEFT_OUTER, on=Dpm.closing_user_pk == close_alias.pk)
            .join(modification_alias, JOIN.LEFT_OUTER, on=Dpm.modifier_pk == modification_alias.pk)
            .join(Ucd, JOIN.LEFT_OUTER, on=Ucd.ucd == Dpm.ucd)
            .join(Product, JOIN.LEFT_OUTER, on=Ucd.reference == Product.reference)
            .join(Gtin, JOIN.LEFT_OUTER, on=((Dpm.ucd_cip == Gtin.pk) & (Gtin.dossier == Dpm.dossier)))
        )
        .where(expr)
        .order_by(Dpm.creation.desc())
    )


def analyze_dpms(current_request) -> list[AnalyzeDTO]:
    """Analyse the DPM zip file"""
    analyze_dtos = []
    type = current_request.form.get("type", None)
    for fname in request.files:
        f = current_request.files.get(fname)
        file_name = None
        if f.mimetype in ZIP_MIMETYPE:
            file_name = f"{uuid.uuid4()}.zip"

        cfg = RawConfig("MEDIANWEB").read("k_eco_dir_dpm")
        img_folder = cfg.value if cfg is not None else None
        logger.debug("DPM Folder %s" % img_folder)

        if file_name is not None and img_folder is not None:
            path = f"{img_folder}\\{file_name}"
            if not os.path.exists(img_folder):
                logger.error("DPM Folder not found")
                raise Exception("dpm.analyze.error.folder_not_found")

            f.save(path)
            try:
                with ZipFile(path) as dpmzip:
                    info = dpmzip.infolist()

                    files = filter(lambda s: (not s.is_dir()), info)

                    for file in files:
                        t = file.filename.split("/")
                        cip = t[0]
                        index = t[1]

                        analyze_dto = next(filter(lambda a: a.gtin_code == cip, analyze_dtos), None)

                        if analyze_dto is None:
                            analyze_dto = AnalyzeDTO(cip)
                            analyze_dtos.append(analyze_dto)

                        if len(t) != 3 or cip == index or len(index) > 3 or len(cip) < 3:
                            analyze_dto.structure = False

                        analyze_dto.xml |= ".xml" in file.filename
                        analyze_dto.xlsx |= ".xls" in file.filename

                        if "Image_1" in file.filename:
                            analyze_dto.image_1 = True
                            analyze_dto.dpm = not __dpm_valide(cip=cip, dossier=index, type_machine=type)
                            analyze_dto.gtin = __gtin_exist(cip, index)

                        analyze_dto.image_2 |= "Image_2" in file.filename
            except Exception as e:
                logger.error(str(e.args))
            finally:
                if os.path.exists(path):
                    os.remove(path)

    return analyze_dtos


def __gtin_exist(cip: str, dossier: str) -> bool:
    """Check if GTIN exists on database"""
    res = True
    try:
        Gtin.select(Gtin.pk).join(Ucd, on=Gtin.ucd == Ucd.ucd).join(
            Product, on=Ucd.reference == Product.reference
        ).where((Gtin.cip == cip) & (Gtin.dossier == dossier)).get()
    except DoesNotExist:
        res = False
    return res


def __dpm_valide(cip: str, dossier: str, type_machine: str) -> bool:
    """
    Check if gtin and version exists in the database, and not synchronized with SYNCHROCIP
    """
    synchronized_dpm = Gtin.get_or_none(
        (Gtin.cip == cip) & (Gtin.dossier == dossier) & (Gtin.last_user == "SYNCHROCIP")
    )

    if synchronized_dpm:
        return True

    validated_dpm = Dpm.get_or_none(
        (Dpm.cip == cip)
        & (Dpm.dossier == dossier)
        & (Dpm.type_machine == type_machine)
        & (Dpm.validation_date.is_null(False))
    )

    return validated_dpm is not None


def __get_dpm(cip: str, dossier: str, type_machine: str) -> Dpm:
    """Retrieve the DPM associate to the GTIN and Version"""
    try:
        dpm = (
            Dpm.select()
            .where(
                (Dpm.cip == cip)
                & (Dpm.dossier == dossier)
                & (Dpm.type_machine == type_machine)
                & (Dpm.validation_date.is_null())
            )
            .get()
        )
    except DoesNotExist:
        dpm = None

    return dpm


def is_image(img_buffer) -> bool:
    res = True
    try:
        Image.open(BytesIO(img_buffer))
    except IOError:
        res = False

    return res
