import time
import logging
import numpy as np
import pandas as pd
from pathlib import Path
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
from pyrfc import Connection
from typing import Type, Tuple
from lib_utilys import read_json
from lib_sap.purchase_order import po_getdetail, po_getsalesmen, po_confirm

pd.options.mode.copy_on_write = True

logger = logging.getLogger(__name__)


def _to_decimal_or_na(value):
    if value is None or pd.isna(value):
        return pd.NA
    if isinstance(value, Decimal):
        return value
    if isinstance(value, (int, float, np.integer, np.floating)):
        return Decimal(str(value))
    if isinstance(value, str):
        normalized = value.strip().replace("\xa0", "").replace("€", "").replace(" ", "")
        if not normalized:
            return pd.NA
        if "," in normalized and "." in normalized:
            if normalized.rfind(",") > normalized.rfind("."):
                normalized = normalized.replace(".", "").replace(",", ".")
            else:
                normalized = normalized.replace(",", "")
        elif "," in normalized:
            normalized = normalized.replace(",", ".")
        try:
            return Decimal(normalized)
        except (InvalidOperation, ValueError):
            return pd.NA
    return pd.NA


def _coerce_decimal_columns(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame:
    for col in columns:
        if col in df.columns:
            df[col] = df[col].apply(_to_decimal_or_na)
    return df

class OrderRsp:
    def __init__(self, uid, adress, message, business, subject, text, pdf):
        self.uid = uid
        self.adress = adress
        self.message = message
        self.business = business
        self.subject = subject
        self.text = text
        self.pdf = pdf
        self.vendor_order = None
        self.creditor_number = None
        self.gln = None
        self.po_number = None
        self.po_date = None
        self.df = pd.DataFrame()

    def configure_ordersp(self, kv_pairs: dict, UNITMAP_PATH: Path):
        """Configures key-value pairs for the order response."""
        df = pd.DataFrame(kv_pairs.get('Material_list'))
        df = self._parse_units(df, UNITMAP_PATH)
        df = self._set_line_level(df, kv_pairs)
        df = self._calc_missing_price(df)
        df = self._rename_columns(df)
        self.df = self._group_by_material(df)
        self.vendor_order = kv_pairs.get('Vendor_order')
        self.creditor_number = kv_pairs.get('Creditor_number')
        self.gln = kv_pairs.get('Creditor_international_location_number')
        self.po_number = kv_pairs.get('Purchase_order')
        self.po_date = kv_pairs.get('Purchase_order_date')
        self.total_value = kv_pairs.get('Invoice_value')
        self.total_tax = kv_pairs.get('Total_tax')
        self.net_value = kv_pairs.get('Net_value')

    def _parse_units(self, df: pd.DataFrame, UNITMAP_PATH: Path) -> pd.DataFrame:
        """Parses units in the DataFrame based on a unit map."""
        unit_map = read_json(UNITMAP_PATH).get('units')
        df['Unit'] = df['Unit'].apply(lambda x: unit_map.get(x, x))
        return df

    def _calc_missing_price(self, df: pd.DataFrame) -> pd.DataFrame:
        """Calculates missing product line price based on other prices and quantity."""
        df = _coerce_decimal_columns(
            df,
            [
                'Discount',
                'Product_price',
                'Product_line_price',
                'Product_net_price',
                'Quantity',
                'Price_unit',
            ],
        )

        df.loc[df['Product_net_price'].isna() & ~df['Product_price'].isna() & ~df['Discount'].isna(),
               'Product_net_price'] = df['Product_price'] * (1 - (df['Discount'] / 100))
        df.loc[df['Product_net_price'].isna() & ~df['Product_line_price'].isna() & ~df['Quantity'].isna() & ~df['Price_unit'].isna(),
                'Product_net_price'] = df['Product_line_price'] / ( df['Quantity'] / df['Price_unit'] )
        
        df.loc[df['Discount'].isna() & ~df['Product_net_price'].isna() & (df['Product_net_price'] == df['Product_price']), 
               'Discount'] = 0
        
        df.loc[df['Product_line_price'].isna() & ~df['Product_net_price'].isna(),
                'Product_line_price'] = df['Product_net_price'] * ( df['Quantity'] / df['Price_unit'] )
                
        df.loc[df['Discount'].isna() & ~df['Product_line_price'].isna() & (df['Product_net_price'] == (df['Product_line_price'] / (df['Quantity'] / df['Price_unit']))),
               'Discount'] = 0
        
        df.loc[df['Product_line_price'].isna() & ~df['Product_price'].isna() & ~df['Discount'].isna(),
                'Product_line_price'] = (df['Product_price'] * ( df['Quantity'] / df['Price_unit'] ) * (1 - (df['Discount'] / 100)))
        
        df.loc[df['Product_price'].isna() & ~df['Product_net_price'].isna() & ~df['Discount'].isna(),
               'Product_price'] = df['Product_net_price'] / (1 - (df['Discount'] / 100))
        df.loc[df['Product_price'].isna() & ~df['Product_line_price'].isna() & ~df['Discount'].isna(),
               'Product_price'] = df['Product_line_price'] / ( df['Quantity'] / df['Price_unit'] ) / (1 - (df['Discount'] / 100))
        return df

    def _set_line_level(self, df: pd.DataFrame, kv_pairs: dict):
        """Sets line-level details in the order response DataFrame."""
        df.loc[df['Delivery_date'].isna(), 'Delivery_date'] = kv_pairs.get('Delivery_date')
        if kv_pairs.get('Price_unit') is None:
            df.loc[df['Price_unit'].isna(), 'Price_unit'] = Decimal(1)
        else:
            df.loc[df['Price_unit'].isna(), 'Price_unit'] = kv_pairs.get('Price_unit')
        if df['Position_number'].isna().all():
            df['Position_number'] = df['Vendor_position_number']
        return df
    
    def _rename_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        """Renames columns in the DataFrame to match SAP standards."""
        rename_map = {
            'EAN': 'EAN_UPC',
            'Material_number_vendor': 'VEND_MAT',
            'Material_number': 'MATERIAL',
            'Position_number': 'PO_ITEM',
            'Delivery_date': 'DELIV_DATE',
            'Discount': 'DISCOUNT',
            'Quantity': 'QUANTITY',
            'Material_description_vendor': 'SHORT_TEXT',
            'Price_unit': 'PRICE_UNIT',
            'Product_line_price': 'NET_VALUE',
            'Product_net_price': 'NET_PRICE',
            'Product_price': 'GROS_PRICE',
            'Purchase_order_line': 'PO_NUMBER',
            'Purchase_order_line_date': 'PO_DATE',
            'Unit': 'UNIT',
            'Vendor_position_number': 'VEND_PO_ITEM',
        }
        return df.rename(columns=rename_map)

    def _group_by_material(self, df: pd.DataFrame) -> pd.DataFrame:
        """Groups the DataFrame by material and aggregates relevant fields."""
        df = df.groupby('VEND_MAT').agg({
            'DELIV_DATE': 'first',       # TODO: make robust to multiple delivery dates for the same material
            'DISCOUNT': 'first',
            'EAN_UPC': 'first',
            'GROS_PRICE': 'first',
            'MATERIAL': 'first',
            'NET_PRICE': 'first',
            'NET_VALUE': 'sum',
            'PO_DATE': 'first',
            'PO_ITEM': 'first',
            'PO_NUMBER': 'first',
            'PRICE_UNIT': 'first',
            'QUANTITY': 'sum',
            'SHORT_TEXT': 'first',
            'UNIT': 'first',
            'VEND_PO_ITEM': 'first',
        }).reset_index()
        return df

class Order:
    def __init__(self):
        self.po_number = None
        self.vendor = None
        self.co_code = None
        self.df = pd.DataFrame()
    
    def configure_order(self, sapconn: Type[Connection], po_number: int, FIELD_PATH: Path):
        self.po_number = po_number
        po_details = po_getdetail(sapconn, str(po_number))
        df = self._parse_po_details(sapconn, po_details, FIELD_PATH)
        self.df = self._add_additional_columns(df)

    def _parse_po_details(self, sapconn: Type[Connection], po_details: dict, FIELD_PATH: Path) -> pd.DataFrame:
        """Parses purchase order details into a DataFrame."""
        keep_fields = read_json(FIELD_PATH).get('fields')
        
        df_items = pd.DataFrame(po_details['PO_ITEMS'])
        df_items = df_items.loc[:, keep_fields]
        df_items['PO_ITEM_LIST'] = df_items['PO_ITEM'].copy()
        df_items['QUANTITY_LIST_ord'] = df_items['QUANTITY'].copy()
        df_items['NET_VALUE_LIST'] = df_items['NET_VALUE'].copy()
        df_items['GROS_VALUE_LIST'] = df_items['GROS_VALUE'].copy()

        df_schedules = pd.DataFrame(po_details['PO_ITEM_SCHEDULES'])
        df_schedules = df_schedules.loc[:, ['PO_ITEM', 'DELIV_DATE']]
        logger.debug(f"PO_ITEMS: {df_items.shape}, PO_ITEM_SCHEDULES: {df_schedules.shape}")

        df_details = pd.merge(df_items, df_schedules, on='PO_ITEM', how='left')
        logger.debug(f"PO_DETAILS after merge: {df_details.shape}")

        df_history = pd.DataFrame(po_details['PO_ITEM_HISTORY'])
        if not df_history.empty:
            df_history = df_history[df_history['HIST_TYPE'] == 'E']
            df_history.rename({'QUANTITY': 'DELVRD_QUANTITY', 'ENTRY_DATE': 'DELVRD_DATE'}, axis=1, inplace=True)
            df_history = df_history.loc[:, ['PO_ITEM', 'DELVRD_QUANTITY', 'DELVRD_DATE']]
        else:
            df_history = pd.DataFrame(columns=['PO_ITEM', 'DELVRD_QUANTITY', 'DELVRD_DATE'])
        df_details = pd.merge(df_details, df_history, on='PO_ITEM', how='left')
        logger.debug(f"PO_ITEM_HISTORY: {df_history.shape}")
        logger.debug(f"PO_DETAILS after history merge: {df_details.shape}")

        df_account = pd.DataFrame(po_details['PO_ITEM_ACCOUNT_ASSIGNMENT'])
        if not df_account.empty:
            df_account = df_account.loc[:, ['PO_ITEM', 'SD_DOC', 'SDOC_ITEM']]
            df_account.rename({'SD_DOC': 'SALES_ORDER', 'SDOC_ITEM': 'SALES_ITEM'}, axis=1, inplace=True)
            df_account['SALESMEN'] = df_account['SALES_ORDER'].apply(lambda x: po_getsalesmen(sapconn, x))
        else:
            df_account = pd.DataFrame(columns=['PO_ITEM', 'SALES_ORDER', 'SALES_ITEM', 'SALESMEN'])
        df_details = pd.merge(df_details, df_account, on='PO_ITEM', how='left')
        logger.debug(f"PO_ITEM_ACCOUNT_ASSIGNMENT: {df_account.shape}")
        logger.debug(f"PO_DETAILS after account merge: {df_details.shape}")

        df_confirmations = pd.DataFrame(po_details['PO_ITEM_CONFIRMATIONS'])
        if not df_confirmations.empty:
            df_confirmations = df_confirmations.groupby('PO_ITEM').agg({
                'CONF_SER': list,
                'QUANTITY':  list,
                'DELIV_DATE': list
                }).reset_index()
            df_confirmations = df_confirmations.loc[:, ['PO_ITEM', 'CONF_SER', 'QUANTITY', 'DELIV_DATE']]
            df_confirmations.rename({'QUANTITY': 'CONF_QTY', 'DELIV_DATE': 'CONF_DATE'}, axis=1, inplace=True)
        else:
            df_confirmations = pd.DataFrame(columns=['PO_ITEM', 'CONF_SER', 'CONF_QTY', 'CONF_DATE'])
        df_details = pd.merge(df_details, df_confirmations, on='PO_ITEM', how='left')
        logger.debug(f"PO_ITEM_CONFIRMATIONS: {df_confirmations.shape}")
        logger.debug(f"PO_DETAILS after confirmations merge: {df_details.shape}")

        df_details['CONF_QTY'] = df_details['CONF_QTY'].apply(lambda x: x if isinstance(x, list) else [])
        df_details['CONF_SER'] = df_details['CONF_SER'].apply(lambda x: x if isinstance(x, list) else [])
        df_details['CONF_DATE'] = df_details['CONF_DATE'].apply(lambda x: x if isinstance(x, list) else [])
        
        df_details = df_details[df_details['DELETE_IND'] == '']
        df_details = df_details.groupby('MATERIAL').agg({
            'PO_NUMBER': 'first',
            'PO_ITEM': 'first',
            'PO_ITEM_LIST': list,
            'SHORT_TEXT': 'first',
            'CO_CODE': 'first',
            'QUANTITY': 'sum',
            'QUANTITY_LIST_ord': list,
            'UNIT': 'first',
            'NET_PRICE': 'first',
            'PRICE_UNIT': 'first',
            'NET_VALUE': 'sum',
            'NET_VALUE_LIST': list,
            'GROS_VALUE': 'sum',
            'GROS_VALUE_LIST': list,
            'EAN_UPC': 'first',
            'VEND_MAT': 'first',
            'INFO_REC': 'first',
            'ACCTASSCAT': list,
            'DELIV_DATE': 'first',
            'DELVRD_QUANTITY': list,
            'DELVRD_DATE': list,
            'SALES_ORDER': list,
            'SALES_ITEM': list,
            'SALESMEN': list,
            'CONF_SER': list,
            'CONF_QTY': list,
            'CONF_DATE': list,
        }).reset_index()

        df_header = pd.DataFrame(po_details['PO_HEADER'], index=[0])
        self.vendor = df_header['VENDOR'].iloc[0]
        self.co_code = df_header['CO_CODE'].iloc[0]

        return df_details

    def _add_additional_columns(self, df: pd.DataFrame) -> pd.DataFrame:
        """Adds additional columns to the DataFrame."""
        df['GROS_PRICE'] = df['GROS_VALUE'] / df['QUANTITY']
        return df

def match_ordersp_to_order(df_rsp: pd.DataFrame, df_ord: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
    rsp = df_rsp.copy()
    ord_ = df_ord.copy()
    rsp['_rsp_id'] = np.arange(len(rsp))
    ord_['_ord_id'] = np.arange(len(ord_))

    matches_list = []
    unmatched_list = []

    # Rename VEND_MAT to add suffixes before merge so we keep both columns
    rsp_vm = rsp.rename(columns={'VEND_MAT': 'VEND_MAT_rsp'})
    ord_vm = ord_.rename(columns={'VEND_MAT': 'VEND_MAT_ord'})
    
    vm = pd.merge(
        rsp_vm, ord_vm, left_on='VEND_MAT_rsp', right_on='VEND_MAT_ord', how='outer',
        suffixes=('_rsp', '_ord'), indicator='vm_merge'
    )
    vm_matched   = vm[vm['vm_merge'] == 'both'].copy()
    vm_rsp_only_ids  = vm.loc[vm['vm_merge'] == 'left_only',  '_rsp_id'].unique()
    vm_ord_only_ids  = vm.loc[vm['vm_merge'] == 'right_only', '_ord_id'].unique()

    matches_list.append(vm_matched.assign(matched_on='VEND_MAT'))

    rsp_unm = rsp[rsp['_rsp_id'].isin(vm_rsp_only_ids)].copy()
    ord_unm = ord_[ord_['_ord_id'].isin(vm_ord_only_ids)].copy()

    # Rename EAN_UPC to add suffixes before merge so we keep both columns
    rsp_ean = rsp_unm.rename(columns={'EAN_UPC': 'EAN_UPC_rsp'})
    ord_ean = ord_unm.rename(columns={'EAN_UPC': 'EAN_UPC_ord'})
    
    ean = pd.merge(
        rsp_ean, ord_ean, left_on='EAN_UPC_rsp', right_on='EAN_UPC_ord', how='outer',
        suffixes=('_rsp', '_ord'), indicator='ean_merge'
    )
    ean_matched  = ean[ean['ean_merge'] == 'both'].copy()
    ean_rsp_only_ids = ean.loc[ean['ean_merge'] == 'left_only',  '_rsp_id'].unique()
    ean_ord_only_ids = ean.loc[ean['ean_merge'] == 'right_only', '_ord_id'].unique()

    matches_list.append(ean_matched.assign(matched_on='EAN_UPC'))

    rsp_unm = rsp_unm[rsp_unm['_rsp_id'].isin(ean_rsp_only_ids)].copy()
    ord_unm = ord_unm[ord_unm['_ord_id'].isin(ean_ord_only_ids)].copy()

    # Rename MATERIAL to add suffixes before merge so we keep both columns
    rsp_mat = rsp_unm.rename(columns={'MATERIAL': 'MATERIAL_rsp'})
    ord_mat = ord_unm.rename(columns={'MATERIAL': 'MATERIAL_ord'})
    
    mat = pd.merge(
        rsp_mat, ord_mat, left_on='MATERIAL_rsp', right_on='MATERIAL_ord', how='outer',
        suffixes=('_rsp', '_ord'), indicator='mat_merge'
    )
    mat_matched  = mat[mat['mat_merge'] == 'both'].copy()
    mat_rsp_only_ids = mat.loc[mat['mat_merge'] == 'left_only',  '_rsp_id'].unique()
    mat_ord_only_ids = mat.loc[mat['mat_merge'] == 'right_only', '_ord_id'].unique()

    matches_list.append(mat_matched.assign(matched_on='MATERIAL'))

    rsp_unm = rsp_unm[rsp_unm['_rsp_id'].isin(mat_rsp_only_ids)].copy()
    ord_unm = ord_unm[ord_unm['_ord_id'].isin(mat_ord_only_ids)].copy()

    # Rename PO_ITEM to add suffixes before merge so we keep both columns
    rsp_item = rsp_unm.rename(columns={'PO_ITEM': 'PO_ITEM_rsp'})
    ord_item = ord_unm.rename(columns={'PO_ITEM': 'PO_ITEM_ord'})
    
    item = pd.merge(
        rsp_item, ord_item, left_on='PO_ITEM_rsp', right_on='PO_ITEM_ord', how='outer',
        suffixes=('_rsp', '_ord'), indicator='item_merge'
    )
    item_matched  = item[item['item_merge'] == 'both'].copy()
    item_rsp_only = item[item['item_merge'] == 'left_only'].copy()
    item_ord_only = item[item['item_merge'] == 'right_only'].copy()

    matches_list.append(item_matched.assign(matched_on='PO_ITEM'))
    unmatched_list.extend([
        item_rsp_only.assign(matched_on='UNMATCHED_RSP'),
        item_ord_only.assign(matched_on='UNMATCHED_ORD')
    ])

    matches = pd.concat(matches_list, ignore_index=True) if matches_list else pd.DataFrame()
    unmatched = pd.concat(unmatched_list, ignore_index=True) if unmatched_list else pd.DataFrame()

    matches = matches[sorted(matches.columns)]
    unmatched = unmatched[sorted(unmatched.columns)]
    rsp_unmatched = unmatched[unmatched['matched_on'].str.startswith('UNMATCHED_RSP')]
    ord_unmatched = unmatched[unmatched['matched_on'].str.startswith('UNMATCHED_ORD')]

    #matches = _coalesce_join_keys(matches, ['VEND_MAT', 'EAN_UPC', 'MATERIAL'])

    if len(rsp_unmatched) > 0:
        logger.warning(f"{len(rsp_unmatched)} lines in order response could not be matched to order lines.")

    return matches, rsp_unmatched, ord_unmatched

def _coalesce_join_keys(df: pd.DataFrame, fields: list[str]) -> pd.DataFrame:
    for f in fields:
        candidates = [f, f"{f}_rsp", f"{f}_ord"]
        present = [c for c in candidates if c in df.columns]
        if not present:
            continue
        for col in present:
            if pd.api.types.is_string_dtype(df[col]) or pd.api.types.is_object_dtype(df[col]):
                s = df[col].astype("string")
                df[col] = s.where(~s.str.fullmatch(r"\s*"), pd.NA)

        s = df[present[0]] if f in present else None
        for c in [f"{f}_rsp", f"{f}_ord"]:
            if c in df.columns:
                s = df[c] if s is None else s.combine_first(df[c])
        if s is not None:
            df[f] = s

        df.drop(columns=[c for c in (f"{f}_rsp", f"{f}_ord") if c in df.columns],
                inplace=True, errors='ignore')
    return df

def check_ordersp_to_order(merged: pd.DataFrame, tolerance: float=1.0) -> bool:
    """Checks if the order response matches the order."""
    # NOTE: We assume that when the supplier does not provide a delivery date, it is the same as the order date.
    merged['DELIV_DATE_rsp'] = merged['DELIV_DATE_rsp'].fillna(merged['DELIV_DATE_ord'])

    date_explode = merged.explode('CONF_DATE').explode('CONF_DATE')
    rsp_dates_expl = pd.to_datetime(date_explode['DELIV_DATE_rsp'], format='%Y%m%d')
    conf_dates_expl = pd.to_datetime(date_explode['CONF_DATE'], format='%Y%m%d')
    cdt_condition = rsp_dates_expl.eq(conf_dates_expl)
    cdt_condition_impl = cdt_condition.groupby(level=0).all()

    # NOTE: Zevij confirms POs via EDI and sends an email, as this inherently will not satify the condition under else statement,,
    # NOTE: we create a condition that checks if the response quantity and date are exactly the same as the once already confirmed.
    is_confirmed = (merged.loc[:, 'CONF_QTY'].apply(lambda x: sum(sum(lst) for lst in x)) == merged.loc[:, 'QUANTITY_rsp']) #+ cdt_condition_impl
    eq_or_more = merged.loc[:, 'QUANTITY_ord'] >= merged.loc[:, 'QUANTITY_rsp'] + \
        merged.loc[:, 'CONF_QTY'].apply(lambda x: sum(sum(lst) for lst in x))
    qty_condition = is_confirmed | eq_or_more

    #np_condition = merged.loc[:, 'NET_PRICE_ord' : 'NET_PRICE_rsp'].diff(axis=1).iloc[:, -1] <= tolerance
    #npeq_cond = merged.loc[:, 'NET_PRICE_rsp'] == merged.loc[:, 'NET_PRICE_ord']
    
    nv_condition = merged.loc[:, 'NET_VALUE_ord' : 'NET_VALUE_rsp'].diff(axis=1).iloc[:, -1].abs() <= tolerance
    nveq_condition = merged.loc[:, 'NET_VALUE_rsp'] == merged.loc[:, 'NET_VALUE_ord']
    
    rsp_dates = pd.to_datetime(merged['DELIV_DATE_rsp'], format='%Y%m%d') 
    ord_dates = pd.to_datetime(merged['DELIV_DATE_ord'], format='%Y%m%d')
    dt_condition = rsp_dates <= ord_dates + pd.tseries.offsets.BDay(n=4)
    cop_condition = merged.loc[:, 'ACCTASSCAT'].str.join(',').str.contains('M')  # Coupled to sales order
    fyi_condition = (~dt_condition) & (cop_condition)  # Notify sales department

    logger.debug(f"condition shapes: dt_condition={dt_condition.shape}, cop_condition={cop_condition.shape}")
    logger.debug(f"Condition shapes: qty_condition={qty_condition.shape}, np_condition={nv_condition.shape}, fyi_condition={fyi_condition.shape}")

    notify = merged.loc[fyi_condition, :]
    review = merged.loc[(~qty_condition) | (~nv_condition), :]
    perfect = merged.loc[qty_condition & nv_condition & dt_condition, :]
    eligible = merged.loc[qty_condition & nv_condition & (~fyi_condition), :]
    change = merged.loc[qty_condition & nv_condition & (~fyi_condition) & (~nveq_condition), :]

    # TODO: qty_condition is based on confirmed quantities, results in lines being passed to change with quantities lower than quantities from po. 
    # This is not an issue now because this change is only passed to po_change_task once in the beginning where df_item and df_itemx dont exist yet. 

    logger.debug(f"\nPerfect matches: {perfect.shape} \nEligible matches: {eligible.shape} \nFYI matches: {notify.shape} \nReview matches: {review.shape} \nout of total matches {merged.shape}")
    
    fyi_items = notify['PO_ITEM_LIST']
    print(f"FYI items: {fyi_items.tolist()}")
    logger.info(f"The following PO_ITEMS should be notified to sales department: {fyi_items.tolist()}")

    return notify, review, eligible, perfect, change

    # TODO: integrate robustness in unit conversion (e.g. 1 ZAK vs. 100 STUKS))

def create_confirmation(sapconn: Type[Connection], eligible: pd.DataFrame, po_number: int, co_code: str, vendor_order: str, sleep_interval: int = 600):
    df_item, df_itemx = _create_item_attr(eligible, co_code, vendor_order)
    df_confirm, df_confirmx = _create_confirmation_attr(eligible)
    while True:
        result = po_confirm(sapconn, str(po_number), df_item, df_itemx, df_confirm, df_confirmx)
        df_result = pd.DataFrame(result.get('RETURN'))
        is_locked = df_result.get('NUMBER', pd.Series()).eq('006').any()
        if not is_locked:
            logger.info("Confirmation created successfully.")
            break
        user = df_result['MESSAGE_V1'].values[0]
        logger.info("Purchase order %s is locked by %s; retrying in %s seconds",
                    po_number, user, sleep_interval)
        # TODO: Implement email notification to user
        time.sleep(sleep_interval)

def _waterfall_rsp_quantities(row: pd.Series):
    """Distributes response quantities over original order quantities."""
    # NOTE: In the case of multiple PO_ITEMS with the same material, the lowest will be assigned the quantity from the response first.
    # NOTE: If higher PO_ITEMS are assigned response quantity first manually, this will break as we rely on right padding.
    if len(row['QUANTITY_LIST_ord']) > 1:
        conf_list = [sum(lst) for lst in row['CONF_QTY']]
        ord_list = (np.array(row['QUANTITY_LIST_ord']) - np.array(conf_list)).tolist()
        rsp_qty = row['QUANTITY_rsp']
        rsp_list = []

        remainder = rsp_qty
        for q in ord_list:
            take = min(q, remainder)
            rsp_list.append(take)
            remainder -= take
        if remainder > 0:
            rsp_list[-1] += remainder
        
        row['QUANTITY_LIST_rsp'] = rsp_list
    else:
        row['QUANTITY_LIST_rsp'] = [row['QUANTITY_rsp']]
    return row

def explode_to_original_rows(df: pd.DataFrame) -> pd.DataFrame:
    """Divides eligible DataFrame back over the original rows."""
    waterfall_df = df.apply(_waterfall_rsp_quantities, axis=1)
    waterfall_df.to_csv('waterfall_df.csv', index=False)  # Debugging output
    cols_to_explode = ['PO_ITEM_LIST', 'QUANTITY_LIST_ord', 'NET_VALUE_LIST', 
                       'GROS_VALUE_LIST', 'SALESMEN', 'SALES_ITEM', 
                       'SALES_ORDER', 'ACCTASSCAT', 'QUANTITY_LIST_rsp',
                       'CONF_QTY', 'CONF_SER', 'CONF_DATE']

    exp, lode = False, False
    if waterfall_df.apply(lambda row: len(row.loc['DELVRD_DATE']) == len(row.loc['PO_ITEM_LIST']), axis=1).all():
        cols_to_explode.append('DELVRD_DATE')
        exp = True
    if waterfall_df.apply(lambda row: len(row.loc['DELVRD_QUANTITY']) == len(row.loc['PO_ITEM_LIST']), axis=1).all():
        cols_to_explode.append('DELVRD_QUANTITY')
        lode = True

    df_exploded = waterfall_df.explode(cols_to_explode, ignore_index=True)
    df_exploded.loc[:, 'PO_ITEM_ord'] = df_exploded['PO_ITEM_LIST']
    df_exploded.loc[:, 'QUANTITY_ord'] = df_exploded['QUANTITY_LIST_ord']
    df_exploded.loc[:, 'QUANTITY_rsp'] = df_exploded['QUANTITY_LIST_rsp']
    df_exploded.loc[:, 'NET_VALUE_ord'] = df_exploded['NET_VALUE_LIST']
    df_exploded.loc[:, 'GROS_VALUE'] = df_exploded['GROS_VALUE_LIST']
    if exp:
        df_exploded.loc[:, 'DELVRD_DATE'] = df_exploded['DELVRD_DATE']
    if lode:
        df_exploded.loc[:, 'DELVRD_QUANTITY'] = df_exploded['DELVRD_QUANTITY']
    nonzero = df_exploded['QUANTITY_rsp'] != Decimal(0)
    df_exploded.drop(index=df_exploded[~nonzero].index, inplace=True)
    df_exploded.loc[:, 'NET_VALUE_rsp'] = df_exploded['NET_PRICE_rsp'] * df_exploded['QUANTITY_rsp'] / df_exploded['PRICE_UNIT_rsp']
    return df_exploded

def _create_item_attr(eligible: pd.DataFrame, co_code: str, vendor_order: str) -> pd.DataFrame:
    """Creates confirmation attributes for the order response."""
    df_item = eligible.loc[:, ['PO_ITEM_ord', 'MATERIAL', 'VEND_MAT', 'QUANTITY_ord']]
    df_item = df_item.rename(columns={
        'PO_ITEM_ord': 'ITEM_NO',
        'QUANTITY_ord': 'QUANTITY'
    })
    df_item['PLANT'] = co_code
    df_item['ACKNOWL_NO'] = vendor_order
    df_item['ACKN_REQD'] = '0'
    df_itemx = df_item.copy()
    df_itemx.loc[:, ['MATERIAL', 'VEND_MAT', 'PLANT', 'QUANTITY', 'ACKNOWL_NO', 'ACKN_REQD']] = 'X'
    df_itemx['ITEM_NOX'] = 'X'
    df_item = df_item.to_dict(orient='records')
    df_itemx = df_itemx.to_dict(orient='records')
    return df_item, df_itemx

def _create_confirmation_attr(eligible: pd.DataFrame) -> pd.DataFrame:
    """Creates confirmation attributes for the order response."""
    df_appended = eligible.apply(_append_new_confirmation, axis=1)
    df_exploded = df_appended.explode(['CONF_QTY', 'CONF_SER', 'CONF_DATE'], ignore_index=True)
    df_confirm = df_exploded.loc[:, ['PO_ITEM_ord', 'CONF_DATE', 'CONF_QTY', 'CONF_SER']]
    df_confirm = df_confirm.rename(columns={
        'PO_ITEM_ord': 'ITEM_NO',
        'CONF_DATE': 'DELIV_DATE',
        'CONF_QTY': 'QUANTITY'
    })
    df_confirm['DELIV_DATE'] = pd.to_datetime(df_confirm['DELIV_DATE']).dt.strftime('%Y%m%d')
    df_confirm['DELIV_DATE_TYP'] = 'D'
    df_confirm['CONF_CATEGORY'] = 'AB'
    df_confirmx = df_confirm.copy()
    df_confirmx.loc[:, ['DELIV_DATE_TYP', 'DELIV_DATE', 'QUANTITY', 'CONF_CATEGORY']] = 'X'
    df_confirmx['ITEM_NOX'] = 'X'
    df_confirm = df_confirm.to_dict(orient='records')
    df_confirmx = df_confirmx.to_dict(orient='records')
    return df_confirm, df_confirmx

def _append_new_confirmation(row: pd.Series) -> str:
    """Structures confirmations as preparation for correct explosion."""
    def _as_decimal(value) -> Decimal:
        if value is None or pd.isna(value):
            return Decimal(0)
        if isinstance(value, Decimal):
            return value
        return Decimal(str(value))

    conf_ser_list = row['CONF_SER'].copy()
    conf_qty_list = row['CONF_QTY'].copy()
    conf_date_list = row['CONF_DATE'].copy()

    if len(conf_ser_list) == 0:
        conf_ser_list.append('0001')
        conf_qty_list.append(_as_decimal(row['QUANTITY_rsp']))
        conf_date_list.append(row['DELIV_DATE_rsp'])
    else:
        total_conf_qtys = sum((_as_decimal(q) for q in row['CONF_QTY']), Decimal(0))
        last_conf_qty = _as_decimal(row['CONF_QTY'][-1]) if row['CONF_QTY'] else Decimal(0)
        new_conf_qty = _as_decimal(row['QUANTITY_rsp'])
        order_qty = _as_decimal(row['QUANTITY_ord'])
        delvrd_qty = _as_decimal(row['DELVRD_QUANTITY'])

        if order_qty == new_conf_qty + delvrd_qty:
            if delvrd_qty != 0:
                conf_ser_list = ['0001', '0002']
                conf_qty_list = [delvrd_qty, new_conf_qty]
                conf_date_list = [
                    row['DELVRD_DATE'] if not pd.isna(row['DELVRD_DATE']) else conf_date_list[-1],
                    row['DELIV_DATE_rsp']
                ]
            else:
                # NOTE: Overwrites existing confirmations.
                conf_ser_list = ['0001']
                conf_qty_list = [new_conf_qty]
                conf_date_list = [row['DELIV_DATE_rsp']]

        elif new_conf_qty + total_conf_qtys + delvrd_qty <= order_qty:
            conf_ser_list.append(str(int(max(conf_ser_list)) + 1).zfill(4))
            conf_qty_list.append(new_conf_qty)
            conf_date_list.append(row['DELIV_DATE_rsp'])
        
        elif new_conf_qty + total_conf_qtys + delvrd_qty > order_qty:
            if new_conf_qty + delvrd_qty == order_qty:
                conf_ser_list = ['0001', '0002']
                conf_qty_list = [delvrd_qty, new_conf_qty]
                conf_date_list = [
                    row['DELVRD_DATE'] if not pd.isna(row['DELVRD_DATE']) else conf_date_list[-1],
                    row['DELIV_DATE_rsp']
                ]
            else:
                logger.warning("UNSEEN SITUATION")

        elif (new_conf_qty == last_conf_qty) and (new_conf_qty + total_conf_qtys > order_qty):
            conf_ser_list[-1] = str(int(max(conf_ser_list))).zfill(4)
            conf_qty_list[-1] = new_conf_qty
            conf_date_list[-1] = row['DELIV_DATE_rsp']

        else:
            logger.warning("UNSEEN SITUATION")
        
    row['CONF_SER'] = conf_ser_list
    row['CONF_QTY'] = conf_qty_list
    row['CONF_DATE'] = conf_date_list
    return row

def parse_po_details(sapconn: Type[Connection], po_details: dict, FIELD_PATH: Path) -> pd.DataFrame:
    """Parses purchase order details into a DataFrame."""
    keep_fields = read_json(FIELD_PATH).get('fields')
    
    df_items = pd.DataFrame(po_details['PO_ITEMS'])
    df_items = df_items.loc[:, keep_fields]
    df_items['PO_ITEM_LIST'] = df_items['PO_ITEM'].copy()
    df_items['QUANTITY_LIST_ord'] = df_items['QUANTITY'].copy()
    df_items['NET_VALUE_LIST'] = df_items['NET_VALUE'].copy()
    df_items['GROS_VALUE_LIST'] = df_items['GROS_VALUE'].copy()

    df_schedules = pd.DataFrame(po_details['PO_ITEM_SCHEDULES'])
    df_schedules = df_schedules.loc[:, ['PO_ITEM', 'DELIV_DATE']]
    logger.debug(f"PO_ITEMS: {df_items.shape}, PO_ITEM_SCHEDULES: {df_schedules.shape}")

    df_details = pd.merge(df_items, df_schedules, on='PO_ITEM', how='left')
    logger.debug(f"PO_DETAILS after merge: {df_details.shape}")

    df_history = pd.DataFrame(po_details['PO_ITEM_HISTORY'])
    if not df_history.empty:
        df_history = df_history[df_history['HIST_TYPE'] == 'E']
        df_history.rename({'QUANTITY': 'DELVRD_QUANTITY', 'ENTRY_DATE': 'DELVRD_DATE'}, axis=1, inplace=True)
        df_history = df_history.loc[:, ['PO_ITEM', 'DELVRD_QUANTITY', 'DELVRD_DATE']]
    else:
        df_history = pd.DataFrame(columns=['PO_ITEM', 'DELVRD_QUANTITY', 'DELVRD_DATE'])
    df_details = pd.merge(df_details, df_history, on='PO_ITEM', how='left')
    logger.debug(f"PO_ITEM_HISTORY: {df_history.shape}")
    logger.debug(f"PO_DETAILS after history merge: {df_details.shape}")

    df_account = pd.DataFrame(po_details['PO_ITEM_ACCOUNT_ASSIGNMENT'])
    if not df_account.empty:
        df_account = df_account.loc[:, ['PO_ITEM', 'SD_DOC', 'SDOC_ITEM']]
        df_account.rename({'SD_DOC': 'SALES_ORDER', 'SDOC_ITEM': 'SALES_ITEM'}, axis=1, inplace=True)
        df_account['SALESMEN'] = df_account['SALES_ORDER'].apply(lambda x: po_getsalesmen(sapconn, x))
    else:
        df_account = pd.DataFrame(columns=['PO_ITEM', 'SALES_ORDER', 'SALES_ITEM', 'SALESMEN'])
    df_details = pd.merge(df_details, df_account, on='PO_ITEM', how='left')
    logger.debug(f"PO_ITEM_ACCOUNT_ASSIGNMENT: {df_account.shape}")
    logger.debug(f"PO_DETAILS after account merge: {df_details.shape}")

    df_confirmations = pd.DataFrame(po_details['PO_ITEM_CONFIRMATIONS'])
    if not df_confirmations.empty:
        df_confirmations = df_confirmations.groupby('PO_ITEM').agg({
            'CONF_SER': list,
            'QUANTITY':  list,
            'DELIV_DATE': list
            }).reset_index()
        df_confirmations = df_confirmations.loc[:, ['PO_ITEM', 'CONF_SER', 'QUANTITY', 'DELIV_DATE']]
        df_confirmations.rename({'QUANTITY': 'CONF_QTY', 'DELIV_DATE': 'CONF_DATE'}, axis=1, inplace=True)
    else:
        df_confirmations = pd.DataFrame(columns=['PO_ITEM', 'CONF_SER', 'CONF_QTY', 'CONF_DATE'])
    df_details = pd.merge(df_details, df_confirmations, on='PO_ITEM', how='left')
    logger.debug(f"PO_ITEM_CONFIRMATIONS: {df_confirmations.shape}")
    logger.debug(f"PO_DETAILS after confirmations merge: {df_details.shape}")

    df_details['CONF_QTY'] = df_details['CONF_QTY'].apply(lambda x: x if isinstance(x, list) else [])
    df_details['CONF_SER'] = df_details['CONF_SER'].apply(lambda x: x if isinstance(x, list) else [])
    df_details['CONF_DATE'] = df_details['CONF_DATE'].apply(lambda x: x if isinstance(x, list) else [])
    
    df_details = df_details[df_details['DELETE_IND'] == '']

    df_details['DELIV_DATE'] = pd.to_datetime(df_details['DELIV_DATE'], format='%Y%m%d', errors='coerce')
    df_details['CONF_DATE'] = df_details['CONF_DATE'].apply(
        lambda x: [pd.to_datetime(d, format='%Y%m%d', errors='coerce').to_pydatetime() if pd.notna(d) else pd.NaT for d in x] if isinstance(x, list) else [pd.NaT]
    )
    df_details['DELVRD_DATE'] = df_details['DELVRD_DATE'].apply(
        lambda x: [pd.to_datetime(d, format='%Y%m%d', errors='coerce').to_pydatetime() if pd.notna(d) else pd.NaT for d in x] if isinstance(x, list) else [pd.NaT]
    )
    df_details['PRICE_DATE'] = pd.to_datetime(df_details['PRICE_DATE'], format='%Y%m%d', errors='coerce')

    df_details = df_details.groupby('MATERIAL').agg({
        'PO_NUMBER': 'first',
        'PO_ITEM': 'first',
        'PO_ITEM_LIST': list,
        'SHORT_TEXT': 'first',
        'CO_CODE': 'first',
        'QUANTITY': 'sum',
        'QUANTITY_LIST_ord': list,
        'UNIT': 'first',
        'NET_PRICE': 'first',
        'PRICE_UNIT': 'first',
        'NET_VALUE': 'sum',
        'NET_VALUE_LIST': list,
        'GROS_VALUE': 'sum',
        'GROS_VALUE_LIST': list,
        'EAN_UPC': 'first',
        'VEND_MAT': 'first',
        'INFO_REC': 'first',
        'ACCTASSCAT': list,
        'DELIV_DATE': 'first',
        'DELVRD_QUANTITY': list,
        'DELVRD_DATE': list,
        'SALES_ORDER': list,
        'SALES_ITEM': list,
        'SALESMEN': list,
        'CONF_SER': list,
        'CONF_QTY': list,
        'CONF_DATE': list,
        'PLAN_DEL': 'first',
        'PRICE_DATE': 'first',
    }).reset_index()

    df_header = pd.DataFrame(po_details['PO_HEADER'], index=[0])
    vendor = df_header['VENDOR'].iloc[0]
    co_code = df_header['CO_CODE'].iloc[0]
    df_details['CONF_QTY_SUM'] = df_details['CONF_QTY'].apply(lambda x: Decimal(sum(sum(lst) for lst in x)))

    return df_details, vendor, co_code

def add_additional_columns(df: pd.DataFrame) -> pd.DataFrame:
    """Adds additional columns to the DataFrame."""
    df['GROS_PRICE'] = df['GROS_VALUE'] / df['QUANTITY']
    return df

def parse_units_rsp(df: pd.DataFrame, UNITMAP_PATH: Path) -> pd.DataFrame:
    """Parses units in the DataFrame based on a unit map."""
    unit_map = read_json(UNITMAP_PATH).get('units')
    df['Unit'] = df['Unit'].apply(lambda x: unit_map.get(x, x))
    return df

def set_line_level(df: pd.DataFrame, kv_pairs: dict):
    """Sets line-level details in the order response DataFrame."""
    df.loc[df['Delivery_date'].isna(), 'Delivery_date'] = kv_pairs.get('Delivery_date')
    df = _coerce_decimal_columns(df, ['Price_unit'])
    kv_price_unit = _to_decimal_or_na(kv_pairs.get('Price_unit'))
    if kv_pairs.get('Price_unit') is None:
        df.loc[df['Price_unit'].isna(), 'Price_unit'] = Decimal(1)
    else:
        df.loc[df['Price_unit'].isna(), 'Price_unit'] = kv_price_unit
    if df['Position_number'].isna().all():
        df['Position_number'] = df['Vendor_position_number']
    return df

def calc_missing_price(df: pd.DataFrame) -> pd.DataFrame:
    """Calculates missing product line price based on other prices and quantity."""
    df = _coerce_decimal_columns(
        df,
        [
            'Discount',
            'Product_price',
            'Product_line_price',
            'Product_net_price',
            'Quantity',
            'Price_unit',
        ],
    )

    df['Discount'] = df['Discount'].fillna(Decimal(0)) # Assume no discount if not provided

    df.loc[df['Product_net_price'].isna() & ~df['Product_price'].isna() & ~df['Discount'].isna(),
            'Product_net_price'] = df['Product_price'] * (Decimal(1) - (df['Discount'] / Decimal(100)))
    df.loc[df['Product_net_price'].isna() & ~df['Product_line_price'].isna() & ~df['Quantity'].isna() & ~df['Price_unit'].isna(),
            'Product_net_price'] = df['Product_line_price'] / ( df['Quantity'] / df['Price_unit'] )
    
    df.loc[df['Product_line_price'].isna() & ~df['Product_net_price'].isna(),
            'Product_line_price'] = df['Product_net_price'] * ( df['Quantity'] / df['Price_unit'] )
            
    df.loc[df['Product_line_price'].isna() & ~df['Product_price'].isna() & ~df['Discount'].isna(),
            'Product_line_price'] = (df['Product_price'] * ( df['Quantity'] / df['Price_unit'] ) * (Decimal(1) - (df['Discount'] / Decimal(100))))
    
    df.loc[df['Product_price'].isna() & ~df['Product_net_price'].isna() & ~df['Discount'].isna(),
            'Product_price'] = df['Product_net_price'] / (Decimal(1) - (df['Discount'] / Decimal(100)))
    df.loc[df['Product_price'].isna() & ~df['Product_line_price'].isna() & ~df['Discount'].isna(),
            'Product_price'] = df['Product_line_price'] / ( df['Quantity'] / df['Price_unit'] ) / (Decimal(1) - (df['Discount'] / Decimal(100)))
    return df

def rename_columns(df: pd.DataFrame) -> pd.DataFrame:
    """Renames columns in the DataFrame to match SAP standards."""
    rename_map = {
        'EAN': 'EAN_UPC',
        'Material_number_vendor': 'VEND_MAT',
        'Material_number': 'MATERIAL',
        'Position_number': 'PO_ITEM',
        'Delivery_date': 'DELIV_DATE',
        'Discount': 'DISCOUNT',
        'Quantity': 'QUANTITY',
        'Material_description_vendor': 'SHORT_TEXT',
        'Price_unit': 'PRICE_UNIT',
        'Product_line_price': 'NET_VALUE',
        'Product_net_price': 'NET_PRICE',
        'Product_price': 'GROS_PRICE',
        'Purchase_order_line': 'PO_NUMBER',
        'Purchase_order_line_date': 'PO_DATE',
        'Unit': 'UNIT',
        'Vendor_position_number': 'VEND_PO_ITEM',
    }
    return df.rename(columns=rename_map)

def group_by_material(df: pd.DataFrame) -> pd.DataFrame:
    """Groups the DataFrame by material and aggregates relevant fields."""
    df = df.groupby('VEND_MAT').agg({
        'DELIV_DATE': 'first',       # TODO: make robust to multiple delivery dates for the same material
        'DISCOUNT': 'first',
        'EAN_UPC': 'first',
        'GROS_PRICE': 'first',
        'MATERIAL': 'first',
        'NET_PRICE': 'first',
        'NET_VALUE': 'sum',
        'PO_DATE': 'first',
        'PO_ITEM': 'first',
        'PO_NUMBER': 'first',
        'PRICE_UNIT': 'first',
        'QUANTITY': 'sum',
        'SHORT_TEXT': 'first',
        'UNIT': 'first',
        'VEND_PO_ITEM': 'first',
    }).reset_index()
    return df

def create_confirmation_objects(eligible: pd.DataFrame) -> pd.DataFrame:
    """Creates confirmation attributes for the order response."""
    df_appended = eligible.apply(_append_new_confirmation, axis=1)
    df_exploded = df_appended.explode(['CONF_QTY', 'CONF_SER', 'CONF_DATE'], ignore_index=True)
    df_confirm = df_exploded.loc[:, ['PO_ITEM_ord', 'CONF_DATE', 'CONF_QTY', 'CONF_SER']]
    df_confirm = df_confirm.rename(columns={
        'PO_ITEM_ord': 'ITEM_NO',
        'CONF_DATE': 'DELIV_DATE',
        'CONF_QTY': 'QUANTITY'
    })
    df_confirm['DELIV_DATE'] = pd.to_datetime(df_confirm['DELIV_DATE']).dt.strftime('%Y%m%d')
    df_confirm['DELIV_DATE_TYP'] = 'D'
    df_confirm['CONF_CATEGORY'] = 'AB'
    df_confirm['CONF_CATEGORY'] = 'AB'
    df_confirm.drop_duplicates(subset=['ITEM_NO', 'CONF_SER'], keep='last', inplace=True)
    df_confirmx = df_confirm.copy()
    df_confirmx.loc[:, ['DELIV_DATE_TYP', 'DELIV_DATE', 'QUANTITY', 'CONF_CATEGORY']] = 'X'
    df_confirmx['ITEM_NOX'] = 'X'
    df_confirm = df_confirm.to_dict(orient='records')
    df_confirmx = df_confirmx.to_dict(orient='records')
    return df_confirm, df_confirmx

def create_condition_object(df_approved: pd.DataFrame, df_pocond: pd.DataFrame) -> pd.DataFrame:
    df_pbxx = df_pocond.loc[df_pocond['COND_TYPE'] == 'PBXX']
    df_zk0x = df_pocond.loc[df_pocond['COND_TYPE'] == 'ZK0X']
    df_zpnx = df_pocond.loc[df_pocond['COND_TYPE'] == 'ZPNX']

    #df_pbxx_merged = df_pbxx.merge(
    #    df_approved[['PO_ITEM_ord', 'GROS_PRICE_rsp', 'PRICE_UNIT_rsp']],
    #    left_on='ITM_NUMBER',
    #    right_on='PO_ITEM_ord',
    #    how='left',
    #)

    #df_zk0x_merged = df_zk0x.merge(
    #    df_approved[['PO_ITEM_ord', 'DISCOUNT', 'PRICE_UNIT_rsp']],
    #    left_on='ITM_NUMBER',
    #    right_on='PO_ITEM_ord',
    #    how='left',
    #)

    df_zpnx_merged = df_zpnx.merge(
        df_approved[['PO_ITEM_ord', 'NET_PRICE_ord', 'PRICE_UNIT_ord']],
        left_on='ITM_NUMBER',
        right_on='PO_ITEM_ord',
        how='left',
    )

    #df_pbxx_merged['COND_VALUE'] = df_pbxx_merged['GROS_PRICE_rsp'].apply(
    #    lambda x: (Decimal(str(x))/Decimal(10)).quantize(Decimal('0.0001'), ROUND_HALF_UP) if pd.notna(x) else Decimal('0')
    #)
    #df_pbxx_merged['COND_P_UNT'] = df_pbxx_merged['PRICE_UNIT_rsp']
    #df_pbxx_merged['CHANGE_ID'] = 'U'
    #df_pbxx_merged['CONBASEVAL'] = '1'


    #df_zk0x_merged['COND_VALUE'] = df_zk0x_merged['DISCOUNT']
    #df_zk0x_merged['COND_P_UNT'] = df_zk0x_merged['PRICE_UNIT_rsp']
    #df_zk0x_merged['CHANGE_ID'] = 'U'
    #df_zk0x_merged['CONBASEVAL'] = '1'

    print(df_zpnx_merged)

    df_zpnx_merged['COND_VALUE'] = df_zpnx_merged['NET_PRICE_ord'].apply(
        lambda x: (Decimal(str(x))/Decimal(10)).quantize(Decimal('0.0001'), ROUND_HALF_UP) if pd.notna(x) else Decimal('0')
    )
    df_zpnx_merged['COND_P_UNT'] = df_zpnx_merged['PRICE_UNIT_ord']
    df_zpnx_merged['CHANGE_ID'] = (
        df_zpnx_merged['CHANGE_ID']
        .replace(r'^\s*$', pd.NA, regex=True)
        .fillna('U')
    )
    df_zpnx_merged['CONBASEVAL'] = '1'


    #df_cond = pd.concat(
    #    [df_pbxx_merged, df_zk0x_merged, df_zpnx_merged],
    #    ignore_index=True
    #)

    df_cond = df_zpnx_merged

    keep_cols = [
        'ITM_NUMBER', 'COND_TYPE', 'COND_VALUE', 'COND_P_UNT', 'CHANGE_ID', 'CONBASEVAL'
    ]
    df_cond = df_cond.loc[:, keep_cols]
    return df_cond

def create_condition_x_object(df_cond: pd.DataFrame) -> pd.DataFrame:
    df_condx = df_cond.copy()
    df_condx.loc[:, ['COND_TYPE', 'COND_VALUE', 'COND_P_UNT', 'CONBASEVAL']] = 'X'
    return df_condx

def create_item_objects(df: pd.DataFrame, df_poitem: pd.DataFrame) -> pd.DataFrame:
    """Creates confirmation attributes for the order response."""
    df_item = df.merge(
        df_poitem,
        left_on='PO_ITEM_ord',
        right_on='PO_ITEM',
        how='left',
        suffixes=('_df', '_poitem')
    )
    df_item.loc[:, 'QUANTITY'] = df_item['QTY_ord']
    keep_cols = ['PO_ITEM_ord', 'MATERIAL_poitem', 'VEND_MAT_poitem', 'QUANTITY', 'PO_UNIT', 'PLANT', 'MATL_GROUP']
    if 'PO_ITEMX' in df_item.columns:
        keep_cols.append('PO_ITEMX')
    df_item = df_item.loc[:, keep_cols]
    df_item = df_item.rename(columns={
        'PO_ITEM_ord': 'PO_ITEM',
        'MATERIAL_poitem': 'MATERIAL',
        'VEND_MAT_poitem': 'VEND_MAT',
    })
    df_itemx = df_item.copy()
    df_itemx.loc[:, ['MATERIAL', 'VEND_MAT', 'QUANTITY', 'PO_UNIT', 'PLANT', 'MATL_GROUP']] = 'X'
    if 'PO_ITEMX' in df_itemx.columns:
        df_itemx['PO_ITEMX'] = (
            df_itemx['PO_ITEMX']
            .replace(r'^\s*$', pd.NA, regex=True)
            .fillna('X')
        )
        df_item = df_item.drop('PO_ITEMX', axis=1, errors='ignore')
    return df_item, df_itemx

def create_schedule_objects(df: pd.DataFrame, df_posched: pd.DataFrame) -> pd.DataFrame:
    """Creates schedule line attributes for the order response."""
    df_posched.sort_values('PO_ITEM', inplace=True)
    df.sort_values('PO_ITEM_ord', inplace=True)
    df_posched['QUANTITY'] = df['QTY_ord'].values # TODO: monitor closely
    df_schedule = df_posched.copy()
    df_schedulex = df_posched.copy()
    df_schedulex.loc[:, ~df_schedulex.columns.isin(['PO_ITEM', 'SCHED_LINE'])] = 'X'
    if 'PO_ITEMX' in df_schedule.columns:
        df_schedulex['PO_ITEMX'] = (
            df_schedulex['PO_ITEMX']
            .replace(r'^\s*$', pd.NA, regex=True)
            .fillna('X')
        )
        df_schedule.drop('PO_ITEMX', axis=1, inplace=True, errors='ignore')
    return df_schedule, df_schedulex

def duplicate_imploded_df(df: pd.DataFrame, dupdict: dict) -> pd.DataFrame:
    """ Duplicates material in order. """
    new_poitem = dupdict['NEW_POITEM']
    mask = df["MATERIAL"] == dupdict['MATERIAL']
    idx = df.index[mask][0]
    row = df.loc[mask].iloc[0]
    net_value = row['NET_VALUE']
    row['PO_ITEM_LIST'].append(new_poitem)
    row['QUANTITY_LIST_ord'].append(Decimal(str(row['QUANTITY'])))
    df.loc[idx, 'QUANTITY'] = row['QUANTITY'] * 2
    row['NET_VALUE_LIST'].append(Decimal(str(net_value)))
    df.loc[idx, 'NET_VALUE'] = net_value * 2
    row['GROS_VALUE_LIST'].append(Decimal(str(row['GROS_VALUE'])))
    row['ACCTASSCAT'].append('')
    row['DELVRD_QUANTITY'].append(float('nan'))
    row['DELVRD_DATE'].append([pd.NaT])
    row['SALES_ORDER'].append(float('nan'))
    row['SALES_ITEM'].append(float('nan'))
    row['SALESMEN'].append(float('nan'))
    row['CONF_SER'].append([])
    row['CONF_QTY'].append([])
    row['CONF_DATE'].append([])
    df.loc[idx, 'PO_ITEMX'] = 'I'
    return df

def duplicate_exploded_df(df: pd.DataFrame, dupdict: dict) -> pd.DataFrame:
    """ Duplicates material in order. """
    mask = df["MATERIAL"] == dupdict['MATERIAL']
    row = df.loc[mask].iloc[0]
    new_row = row.copy()
    new_row['PO_ITEM'] = dupdict['NEW_POITEM']
    new_row['PO_ITEMX'] = 'I'
    df = pd.concat([df, new_row.to_frame().T], ignore_index=True)
    return df

def duplicate_schedule(df_schedule: pd.DataFrame, dupdict: dict) -> pd.DataFrame:
    """Duplicates schedule lines for the new item."""
    po_item = str(dupdict['PO_ITEM'])
    new_poitem = str(dupdict['NEW_POITEM'])
    row = df_schedule.loc[df_schedule['PO_ITEM'] == po_item].copy()
    row['PO_ITEM'] = new_poitem
    row['PO_ITEMX'] = 'I'
    df_schedule = pd.concat([df_schedule, row], ignore_index=True)
    return df_schedule

def duplicate_conditions(df_cond: pd.DataFrame, dupdict: dict) -> pd.DataFrame:
    """Duplicates conditions for the new item."""
    po_item = str(dupdict['PO_ITEM']).zfill(6)
    new_poitem = str(dupdict['NEW_POITEM']).zfill(6)
    itm_numbers = df_cond["ITM_NUMBER"].astype(str).str.zfill(6)
    rows = df_cond.loc[itm_numbers == po_item].copy()
    if rows.empty:
        return df_cond
    rows.loc[:, 'ITM_NUMBER'] = new_poitem
    rows.loc[:, 'CHANGE_ID'] = 'I'
    df_cond = pd.concat([df_cond, rows], ignore_index=True)
    return df_cond

def delete_imploded_df(df: pd.DataFrame, material: str, po_item: str) -> pd.DataFrame:
    """Removes a PO item from grouped-by-material rows and list columns."""
    mask = df["MATERIAL"] == material
    if not mask.any():
        return df
    idx = df.index[mask][0]
    row = df.loc[mask].iloc[0]
    if "PO_ITEM_LIST" not in row or not isinstance(row["PO_ITEM_LIST"], list):
        return df
    normalized = str(po_item).zfill(5)
    po_item_list = row["PO_ITEM_LIST"]
    remove_indexes = [
        i for i, item in enumerate(po_item_list)
        if str(item).zfill(5) == normalized
    ]
    if not remove_indexes:
        return df
    list_columns = [
        "PO_ITEM_LIST",
        "QUANTITY_LIST_ord",
        "NET_VALUE_LIST",
        "GROS_VALUE_LIST",
        "ACCTASSCAT",
        "DELVRD_QUANTITY",
        "DELVRD_DATE",
        "SALES_ORDER",
        "SALES_ITEM",
        "SALESMEN",
        "CONF_SER",
        "CONF_QTY",
        "CONF_DATE"
    ]
    for col in list_columns:
        if col not in df.columns:
            continue
        col_value = row[col]
        if not isinstance(col_value, list):
            continue
        for remove_index in sorted(remove_indexes, reverse=True):
            if remove_index < len(col_value):
                col_value.pop(remove_index)
        row[col] = col_value
    if len(row["PO_ITEM_LIST"]) == 0:
        df = df.drop(index=idx)
        return df
    df.loc[idx, "PO_ITEM"] = row["PO_ITEM_LIST"][0]
    if "QUANTITY_LIST_ord" in row and isinstance(row["QUANTITY_LIST_ord"], list):
        df.loc[idx, "QUANTITY"] = sum(row["QUANTITY_LIST_ord"])
    if "NET_VALUE_LIST" in row and isinstance(row["NET_VALUE_LIST"], list):
        df.loc[idx, "NET_VALUE"] = sum(row["NET_VALUE_LIST"])
    if "GROS_VALUE_LIST" in row and isinstance(row["GROS_VALUE_LIST"], list):
        df.loc[idx, "GROS_VALUE"] = sum(row["GROS_VALUE_LIST"])
    df.loc[idx, list_columns] = row[list_columns]
    return df