from apps.inm.models import (MIO, MIOItem,
                             MaterialBatch, MaterialBatchA,
                             MIOItemA, MIOItemw)
from rest_framework.exceptions import ParseError
from apps.mtm.models import Material
from apps.wpm.models import WMaterial, BatchSt, BatchLog
from apps.wpm.services_2 import ana_batch_thread
from apps.wpmw.models import Wpr
from apps.qm.models import Ftest, Defect
from django.db import transaction
from django.db.models import Count, Q

@transaction.atomic
def do_out(item: MIOItem, is_reverse: bool = False):
    """
    生产领料到车间
    """
    from apps.inm.models import MaterialBatch
    mio:MIO = item.mio
    belong_dept = mio.belong_dept
    mgroup = mio.mgroup
    do_user = mio.do_user
    material:Material = item.material
    
    # 获取defect
    defect:Defect = None
    if item.wm and item.mb:
        raise ParseError("车间和仓库库存不能同时存在")
    if item.wm:
        defect = item.wm.defect
    elif item.mb:
        defect = item.mb.defect

    action_list = []
    mias = MIOItemA.objects.filter(mioitem=item)
    is_zhj = False # 是否组合件领料
    if mias.exists():
        is_zhj = True
        for itema in mias:
            material = itema.material
            batch = itema.batch
            rate = itema.rate
            new_count = rate * item.count
            action_list.append([material, batch, new_count, None, None])
    else:
        action_list = [[item.material, item.batch, item.count, defect]]
    
    if is_zhj:
        try:
            mb = MaterialBatch.objects.select_for_update().get(
                material=item.material, 
                warehouse=item.warehouse, 
                batch=item.batch,
                state=WMaterial.WM_OK,
                defect=None
            )
        except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e: 
            raise ParseError(f"组合件批次错误!{e}")
        mb.count = mb.count - item.count
        if mb.count < 0:
            raise ParseError("组合件批次库存不足,操作失败")
        else:
            mb.save()

        if material.tracking == Material.MA_TRACKING_SINGLE:
            raise ParseError("组合件暂不支持追踪单件")
    
    xbatches = []
    if is_zhj:
        xbatches = [item.batch]
    for al in action_list:
        xmaterial:Material = al[0]
        xbatch:str = al[1]
        xcount:str = al[2]
        defect:Defect = al[3]

        state = WMaterial.WM_OK
        if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
            state = WMaterial.WM_NOTOK

        xbatches.append(xbatch)
        if xcount <= 0:
            raise ParseError("存在非正数!")
        mb = None
        if not is_zhj:
            try:
                mb = MaterialBatch.objects.select_for_update().get(
                    material=xmaterial,
                    warehouse=item.warehouse,
                    batch=xbatch,
                    state=state,
                    defect=defect
                )
            except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
                raise ParseError(f"{str(xmaterial)}批次{xbatch}错误!{e}")
            mb.count = mb.count - xcount
            if mb.count < 0:
                raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
            else:
                mb.save()

        if xmaterial.into_wm:
            # 领到车间库存(或工段)
            wm, new_create = WMaterial.locked_get_or_create_inventory(
                batch=xbatch, material=xmaterial,
                belong_dept=belong_dept, mgroup=mgroup,
                state=state, defect=defect)
            if new_create:
                wm.create_by = do_user
                wm.batch_ofrom = mb.batch if mb else None
                wm.material_ofrom = mb.material if mb else None
            wm.count = wm.count + xcount
            wm.update_by = do_user
            wm.save()

        # 开始变动wpr
        if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
            if material.into_wm is False:
                raise ParseError("追踪单个物料不支持不进行车间库存的操作")
            mioitemws = MIOItemw.objects.filter(mioitem=item)
            if mioitemws.count() != item.count:
                raise ParseError("出入库与明细数量不一致,操作失败")
            mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct())
            if len(mb_ids) == 1 and mb_ids[0] == mb.id:
                pass
            else:
                raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次')
            for mioitemw in mioitemws:
                Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)

    # 触发批次统计分析
    ana_batch_thread(xbatches)


@transaction.atomic
def do_in(item: MIOItem):
    """
    生产入库后更新车间物料
    """
    mio = item.mio
    wmin:WMaterial = item.wm
    if wmin and wmin.state != WMaterial.WM_OK:
        raise ParseError("非合格物料无法入库")
    belong_dept = mio.belong_dept
    mgroup = mio.mgroup
    do_user = mio.do_user
    material = item.material

    action_list = []
    mias = MIOItemA.objects.filter(mioitem=item)
    is_zhj = False # 是否组合件入仓库

    # 获取defect
    defect:Defect = None
    if item.wm and item.mb:
        raise ParseError("车间和仓库库存不能同时存在")
    if item.wm:
        defect = item.wm.defect
    elif item.mb:
        defect = item.mb.defect

    if mias.exists():  
        is_zhj = True
        for itema in mias:
            material = itema.material
            batch = itema.batch
            rate = itema.rate
            new_count = rate * item.count
            action_list.append([material, batch, new_count, None, None])
    else:
        action_list = [[item.material, item.batch, item.count, defect, item.wm]]

    production_dept = None

    xbatchs = []
    if is_zhj:
        xbatchs = [item.batch]
    for al in action_list:
        xmaterial, xbatch, xcount, defect, xwm = al
        if xcount <= 0:
            raise ParseError("存在非正数!")

        state = WMaterial.WM_OK
        if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
            state = WMaterial.WM_NOTOK

        xbatchs.append(xbatch)
        if xmaterial.into_wm:
            if xwm:
                wm = WMaterial.objects.select_for_update().get(pk=xwm.pk)
            else:
                wm_qs = WMaterial.objects.select_for_update().filter(
                    batch=xbatch,
                    material=xmaterial,
                    belong_dept=belong_dept,
                    mgroup=mgroup,
                    defect=defect,
                    state=state)
                count_x = wm_qs.count()
                if count_x == 1:
                    wm = wm_qs.first()
                elif count_x == 0:
                    raise ParseError(
                        f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
                else:
                    raise ParseError(
                        f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
            
            # 扣减车间库存
            new_count = wm.count - xcount
            if new_count >= 0:
                wm.count = new_count
                wm.update_by = do_user
                wm.save()
            else:
                raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
            
            wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
            if production_dept is None:
                production_dept = wm_production_dept
            elif wm_production_dept and production_dept != wm_production_dept:
                raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
        
        # 增加mb
        if not is_zhj:
            mb, _ = MaterialBatch.objects.get_or_create(
                    material=xmaterial,
                    warehouse=item.warehouse,
                    batch=xbatch,
                    state=state,
                    defect=defect,
                    defaults={
                        "count": 0, 
                        "batch_ofrom": wm.batch_ofrom, 
                        "material_ofrom": wm.material_ofrom,
                        "production_dept": production_dept
                        }
                )
            if mb.production_dept is None:
                mb.production_dept = production_dept
            mb.count = mb.count + xcount
            mb.save()

            # 开始变动wpr
            if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
                if material.into_wm is False:
                    raise ParseError("追踪单个物料不支持不进行车间库存的操作")
                mioitemws = MIOItemw.objects.filter(mioitem=item)
                if mioitemws.count() != item.count:
                    raise ParseError("出入库与明细数量不一致,操作失败")
                wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct())
                if len(wm_ids) == 1 and wm_ids[0] == wm.id:
                    pass
                else:
                    raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次')
                for mioitemw in mioitemws:
                    Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)


    if is_zhj: # 组合件单独处理并且不做追踪单个处理
        mb, is_created = MaterialBatch.objects.get_or_create(
                    material=item.material, 
                    warehouse=item.warehouse, 
                    batch=item.batch,
                    defect=None,
                    state=WMaterial.WM_OK,
                    defaults={"count": 0, "production_dept": production_dept}
        )
        if not is_created:
            raise ParseError("该批次组合件已存在")
        if mb.production_dept is None:
            mb.production_dept = production_dept
        mb.count = mb.count + item.count
        mb.save()
        for mia in mias:
            MaterialBatchA.objects.create(mb=mb, material=mia.material, batch=mia.batch, rate=mia.rate)
    
    # 批次统计分析
    ana_batch_thread(xbatchs)

class InmService:

    @classmethod
    def update_inm(cls, instance: MIO, is_reverse: bool = False):
        """
        更新库存, 支持反向操作
        """
        if is_reverse is False and not MIOItem.objects.filter(mio=instance).exists():
            raise ParseError("出入库记录缺失明细,无法操作")
        
        if instance.type == MIO.MIO_TYPE_PUR_IN:  # 需要更新订单
            # 这里还需要对入厂检验进行处理
            if is_reverse:
                BatchLog.clear(mio=instance)
            else:
                for item in MIOItem.objects.filter(mio=instance):
                    BatchSt.g_create(
                        batch=item.batch, mio=instance, material_start=item.material)
            from apps.pum.services import PumService
            if is_reverse:
                cls.update_mb(instance, -1)
            else:
                cls.update_mb(instance, 1)
            PumService.mio_pur(instance, is_reverse)
        elif instance.type == MIO.MIO_TYPE_PUR_OUT:
            from apps.pum.services import PumService
            if is_reverse:
                cls.update_mb(instance, 1)
            else:
                cls.update_mb(instance, -1)
            PumService.mio_pur(instance, is_reverse)
        elif instance.type == MIO.MIO_TYPE_OTHER_IN:
            if is_reverse:
                BatchLog.clear(mio=instance)
            else:
                for item in MIOItem.objects.filter(mio=instance):
                    BatchSt.g_create(
                        batch=item.batch, mio=instance, material_start=item.material)
            if is_reverse:
                cls.update_mb(instance, -1)
            else:
                cls.update_mb(instance, 1)
        elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]:
            mioitems = MIOItem.objects.filter(mio=instance)
            if is_reverse:
                for item in mioitems:
                    do_out(item)
            else:
                for item in mioitems:
                    do_in(item)
        elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]:
            mioitems = MIOItem.objects.filter(mio=instance)
            if is_reverse:
                for item in mioitems:
                    do_in(item)
            else:
                for item in mioitems:
                    do_out(item)
        elif instance.type == MIO.MIO_TYPE_SALE_OUT:
            from apps.sam.services import SamService
            if is_reverse:
                cls.update_mb(instance, 1)
            else:
                cls.update_mb(instance, -1)
            SamService.mio_saleout(instance, is_reverse)
        elif instance.type == MIO.MIO_TYPE_OTHER_OUT:
            if is_reverse:
                cls.update_mb(instance, 1)
            else:
                cls.update_mb(instance, -1)
        else:
            raise ParseError('不支持该出入库操作')


    @classmethod
    def update_mb(cls, instance: MIO, in_or_out: int = 1):
        """
        更新物料批次
        in = 1
        out = -1
        """
        mioitems = MIOItem.objects.filter(mio=instance)
        for i in mioitems:
           cls.update_mb_item(i,  in_or_out, 'count')

    @classmethod
    def update_mb_item(cls, i: MIOItem, in_or_out: int = 1, field:str='count'):
        """
        更新物料批次(根据明细)
        in = 1
        out = -1
        默认使用count字段做加减
        """
        mio_type = i.mio.type
        material: Material = i.material
        warehouse = i.warehouse
        tracking = material.tracking
        ddict = {"count": 0, "supplier": i.mio.supplier}
        if i.material.type in [Material.MA_TYPE_MAINSO]:
            ddict["batch_ofrom"] = i.batch
            ddict["material_ofrom"] = i.material
        
        if field == "count":
            if i.test_date is not None:
                count_change = i.count - i.count_notok
            else:
                count_change = i.count
        elif field == "count_notok":
            count_change = getattr(i, field)
        else:
            raise ParseError("不支持该字段")
        m_list = [(material, warehouse, i.batch, count_change, None, i)]

        if tracking == Material.MA_TRACKING_SINGLE:
            # 获取所有主要的不合格项
            mw_qs = MIOItemw.objects.filter(mioitem=i)
            defectIds= Ftest.objects.filter(mioitemw_ftest__in=mw_qs).exclude(defect_main=None).values_list("defect_main__id", flat=True).distinct()
            defects_map = {d.id: d for d in Defect.objects.filter(id__in=defectIds)}
            # 过滤并统计相关数据
            filtered_mw_qs = mw_qs.filter(
                ftest__defect_main__id__in=defects_map.keys(),
            ).values('ftest__defect_main__id').annotate(xcount=Count('id'))
            for defect_data in filtered_mw_qs:
                defect_id = defect_data['ftest__defect_main__id']
                xcount = defect_data['xcount']
                if xcount > 0:
                    defect = defects_map[defect_id]
                    m_list.append((material, warehouse, i.batch, xcount, defect, i))

        xbatchs = []
        for material, warehouse, batch, change_count, defect, mioitem in m_list:
            xbatchs.append(batch)
            if change_count < 0:
                raise ParseError("存在负数!")
            state = WMaterial.WM_OK
            if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
                state = WMaterial.WM_NOTOK
            mb, _ = MaterialBatch.objects.get_or_create(
                material=material, 
                warehouse=warehouse, 
                batch=batch, 
                defect=defect,
                state=state,
                defaults=ddict
            )
        
            if in_or_out == 1:
                mb.count = mb.count + change_count
                mb.save()
                if tracking == Material.MA_TRACKING_SINGLE:
                    if defect:
                        mioitemws = MIOItemw.objects.filter(mioitem=i, ftest__defect_main=defect)
                    else:
                        mioitemws = MIOItemw.objects.filter(Q(ftest=None) | Q(ftest__defect_main=None), mioitem=i)
                    if mioitemws.count() != change_count:
                        raise ParseError("出入库与明细数量不一致,操作失败")
                    for mioitemw in mioitemws:
                        if mioitemw.wpr:
                            Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, ftest=mioitemw.ftest)
                        else:
                            wpr = Wpr.change_or_new(number=mioitemw.number, mb=mb, ftest=mioitemw.ftest)
                            mioitemw.wpr = wpr
                            mioitemw.save()

            elif in_or_out == -1:
                mb.count = mb.count - change_count
                if mb.count < 0:
                    raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
                else:
                    mb.save()
                    if tracking == Material.MA_TRACKING_SINGLE:
                        if defect:
                            mioitemws = MIOItemw.objects.filter(mioitem=i, ftest__defect_main=defect)
                        else:
                            mioitemws = MIOItemw.objects.filter(Q(ftest=None) | Q(ftest__defect_main=None), mioitem=i)
                        if mioitemws.count() != change_count:
                            raise ParseError("出入库与明细数量不一致,操作失败")
                        for mioitemw in mioitemws:
                            Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out)
            else:
                raise ParseError("不支持的操作")
    
         # 批次统计分析
        ana_batch_thread(xbatchs)


    @classmethod
    def revert_and_del(cls, mioitem: MIOItem):
        # 锁定 MIO 行，防止多人同时撤销不同明细时并发漏删 MIO
        mio = MIO.objects.select_for_update().get(id=mioitem.mio_id)
        if mio.submit_time is None:
            raise ParseError("未提交的出入库明细不允许撤销")
        if mioitem.test_date is not None:
            raise ParseError("已检验的出入库明细不允许撤销")
        if mio.type == MIO.MIO_TYPE_PUR_IN:
            from apps.pum.services import PumService
            cls.update_mb_item(mioitem, -1)
            BatchLog.clear(mioitem=mioitem)
            PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
            mioitem.delete()
        elif mio.type == MIO.MIO_TYPE_OTHER_IN:
            cls.update_mb_item(mioitem, -1)
            BatchLog.clear(mioitem=mioitem)
            mioitem.delete()
        elif mio.type == MIO.MIO_TYPE_DO_OUT:
            do_in(mioitem)
            BatchLog.clear(mioitem=mioitem)
            mioitem.delete()
        else:
            raise ParseError("不支持该出入库单明细撤销")
        # 若该出入库记录已无明细，自动删除
        if not MIOItem.objects.filter(mio=mio).exists():
            mio.delete()
