import frappe
from frappe.utils import (
    getdate,
    cstr,
    cint,
    flt,
    get_time,
    get_datetime,
)
from xml.etree import ElementTree
from frappe.utils.nestedset import get_root_of
from typing import List, Dict
from frappe.model.docstatus import DocStatus
import logging
import traceback

# إعداد وحدة التسجيل (Logging)
logging.basicConfig(
    filename='app_errors.log',  # مسار ملف السجل
    filemode='a',               # وضعية الفتح (إلحاق)
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.ERROR         # مستوى التسجيل
)

# تعريف الثوابت
NOT_EXIST = 0
SAME = 1
NOT_SAME = 2
MAX = None  # placeholder for max value

@frappe.whitelist(allow_guest=True)
def login(user, pwd):
    """
    دالة تسجيل الدخول للمستخدمين.
    """
    try:
        login_manager = frappe.auth.LoginManager()
        login_manager.authenticate(user=user, pwd=pwd)
        login_manager.post_login()
        user_doc = frappe.get_doc("User", frappe.session.user)
        api_generate = generate_keys(user_doc)
        return {
            "success_key": 1,
            "message": "success",
            "sid": frappe.session.sid,
            "api_key": user_doc.api_key,
            "api_secret": api_generate,
            "username": user_doc.username,
            "email": user_doc.email,
            "login_after": user_doc.login_after,
            "login_before": user_doc.login_before,
        }
    except frappe.exceptions.AuthenticationError:
        logging.error(f"Authentication failed for user {user}")
        return {
            "success_key": 0,
            "message": "Authentication Failed",
            "Exception_type": "AuthenticationError"
        }
    except Exception as err:
        logging.error(f"Unexpected error during login for user {user}: {str(err)}\n{traceback.format_exc()}")
        return {
            "success_key": 0,
            "message": "Error",
            "Exception_type": type(err).__name__
        }

@frappe.whitelist()
def open_disbursement_session(date):
    """
    دالة فتح جلسة صرف جديدة أو جلب الجلسة السابقة إذا كانت موجودة.
    """
    try:
        today_date = getdate()
        if getdate(date) > today_date:
            frappe.throw("التاريخ لا يمكن أن يكون أكبر من التاريخ الحالي")

        user = frappe.get_doc("User", frappe.session.user)
        retail_warehouse = get_retail_warehouse(user.name)
        list_of_delivered_beneficiaies = []
        disbursement_open = ""

        if retail_warehouse:
            # التحقق من وجود جلسة مسبقة لنفس المستخدم وفي نفس التاريخ
            last_opened_record = get_last_opened_if_exist(user.name, date)
            if last_opened_record:
                # في حالة وجود جلسة مسبقة، يتم جلب المستفيدين الذين تم الصرف لهم
                disbursement_open = last_opened_record.name
                list_of_delivered_beneficiaies = get_disbursement_open_beneficiaries(disbursement_open)
            else:
                # إنشاء جلسة صرف جديدة
                disbursement_open = frappe.get_doc({
                    "doctype": "Disbursement Open",
                    "retail_warehouse": retail_warehouse,
                    "user": user.name,
                    "posting_date": getdate(date),
                })
                disbursement_open.submit()
                frappe.db.commit()

        return {
            "retail_warehouse": retail_warehouse,
            "disbursement_open": disbursement_open,
            "delivered_beneficiaries": list_of_delivered_beneficiaies,
            "length": len(list_of_delivered_beneficiaies),
        }
    except Exception as err:
        logging.error(f"Error in open_disbursement_session with date {date}: {str(err)}\n{traceback.format_exc()}")
        return {
            "success_key": 0,
            "message": "Error",
            "Exception_type": type(err).__name__
        }

def get_retail_warehouse(user_name):
    """
    دالة لجلب مستودع التجزئة المرتبط بالمستخدم.
    """
    try:
        warehouse = frappe.get_value(
            "Free Disbursement Profile",
            {"user": user_name},
            "retail_warehouse"
        )
        return warehouse
    except Exception as err:
        logging.error(f"Error in get_retail_warehouse for user {user_name}: {str(err)}\n{traceback.format_exc()}")
        return None  # أو أي قيمة افتراضية مناسبة

def generate_keys(user):
    """
    دالة لتوليد مفاتيح API للمستخدم.
    """
    try:
        api_secret = frappe.generate_hash(length=15)

        if not user.api_key:
            api_key = frappe.generate_hash(length=15)
            user.api_key = api_key

        user.api_secret = api_secret
        user.save(ignore_permissions=True)
        frappe.db.commit()
        return api_secret
    except Exception as err:
        logging.error(f"Error in generate_keys for user {user.name}: {str(err)}\n{traceback.format_exc()}")
        raise  # إعادة رفع الاستثناء ليتم التعامل معه في الدالة الأعلى

@frappe.whitelist()
def sync_beneficiaries():
    """
    دالة لمزامنة المستفيدين.
    """
    try:
        retail_warehouse = get_retail_warehouse(frappe.session.user)
        if not retail_warehouse:
            frappe.throw("لم يتم العثور على مستودع التجزئة للمستخدم الحالي.")

        beneficiaries = frappe.get_list(
            "Beneficiary",
            fields=[
                "name",
                "person_name",
                "`id` AS `barcode_id`",
                "retail_warehouse",
            ],
            filters={
                "retail_warehouse": retail_warehouse,
                "has_items": True,
                "beneficiary_status": "Active",
            },
        )

        for beneficiary in beneficiaries:
            if cstr(beneficiary.get("barcode_id")).startswith("<svg"):
                try:
                    barcode_xml = ElementTree.fromstring(beneficiary["barcode_id"])
                    bare_code = barcode_xml.attrib.get("data-barcode-value", "")
                    beneficiary["barcode_id"] = bare_code
                except ElementTree.ParseError as parse_err:
                    logging.error(f"XML parsing error for beneficiary {beneficiary['name']}: {str(parse_err)}\n{traceback.format_exc()}")
                    beneficiary["barcode_id"] = ""

            # جلب العناصر المرتبطة بكل مستفيد
            try:
                items = frappe.get_all(
                    "Beneficiary Item",
                    fields=["item", "quantity"],
                    filters={
                        "parent": beneficiary["name"],
                        "parentfield": "beneficiary_items",
                        "parenttype": "Beneficiary",
                    },
                )
                beneficiary["items"] = items
            except Exception as item_err:
                logging.error(f"Error fetching items for beneficiary {beneficiary['name']}: {str(item_err)}\n{traceback.format_exc()}")
                beneficiary["items"] = []

        return beneficiaries
    except Exception as err:
        logging.error(f"Error in sync_beneficiaries: {str(err)}\n{traceback.format_exc()}")
        return {
            "success_key": 0,
            "message": "Error",
            "Exception_type": type(err).__name__
        }

@frappe.whitelist()
def sync_items():
    """
    دالة لمزامنة العناصر.
    """
    try:
        retail_warehouse = get_retail_warehouse(frappe.session.user)
        if not retail_warehouse:
            frappe.throw("لم يتم العثور على مستودع التجزئة للمستخدم الحالي.")

        items = get_items(retail_warehouse)
        disburse_items = get_disburse_items(retail_warehouse)
        result = []

        for item in items:
            item_code = item.get("item_code")
            item["item"] = item_code
            item["qty"] = item.get("actual_qty", 0)
            item["uom"] = item.get("stock_uom")
            item["is_disburse"] = item_code in disburse_items

            if item.get("is_disburse") or flt(item.get("qty")) > 0:
                result.append(item)

        return result
    except Exception as err:
        logging.error(f"Error in sync_items: {str(err)}\n{traceback.format_exc()}")
        return {
            "success_key": 0,
            "message": "Error",
            "Exception_type": type(err).__name__
        }

def get_disburse_items(warehouse):
    """
    دالة لجلب العناصر المخصصة للصرف.
    """
    try:
        query = """
            SELECT
                `tabBeneficiary Item`.`item`
            FROM `tabBeneficiary`
            JOIN `tabBeneficiary Item`
                ON (
                    `tabBeneficiary Item`.parenttype = 'Beneficiary'
                    AND `tabBeneficiary Item`.parent = `tabBeneficiary`.name
                    AND `tabBeneficiary Item`.parentfield = "beneficiary_items"
                )
            WHERE
                `tabBeneficiary`.`retail_warehouse` = %(warehouse)s
                AND `tabBeneficiary`.`has_items` = 1
                AND `tabBeneficiary`.`beneficiary_status` = 'Active'
            ORDER BY `tabBeneficiary Item`.item
        """
        items = frappe.db.sql(query, {"warehouse": warehouse})
        return {item[0] for item in items}
    except Exception as err:
        logging.error(f"Error in get_disburse_items for warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return set()  # إرجاع مجموعة فارغة في حالة الخطأ

def get_items(warehouse, item_group=None):
    """
    دالة لجلب العناصر بناءً على المستودع ومجموعة العناصر.
    """
    try:
        if not item_group or not frappe.db.exists("Item Group", item_group):
            item_group = get_root_of("Item Group")

        lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])

        bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
        bin_join_condition = "AND ((bin.warehouse = %(warehouse)s AND bin.actual_qty > 0) OR (`item`.`is_stock_item` = 0)) "

        query = f"""
            SELECT
                item.name AS item_code,
                item.item_name,
                item.description,
                item.stock_uom,
                item.image AS item_image,
                item.is_stock_item
            FROM
                `tabItem` item {bin_join_selection}
            WHERE
                item.disabled = 0
                AND item.distribution_item = 1
                AND item.is_sales_item = 1
                AND item.is_fixed_asset = 0
                AND item.item_group IN (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
                {bin_join_condition}
            ORDER BY
                item.name ASC
        """

        items_data = frappe.db.sql(
            query,
            {"warehouse": warehouse},
            as_dict=True,
        )

        result = []
        if items_data:
            for item in items_data:
                item_code = item.get("item_code")
                actual_qty, is_stock_item, is_bundle = get_stock_availability(item_code, warehouse)
                sub_items = {}

                if is_bundle:
                    actual_qty, sub_items = actual_qty  # تفكيك النتائج إذا كان العنصر عبارة عن Bundle

                row = {
                    "item_code": item_code,
                    "item_name": item.get("item_name"),
                    "description": item.get("description"),
                    "stock_uom": item.get("stock_uom"),
                    "item_image": item.get("item_image"),
                    "is_stock_item": is_stock_item,
                    "actual_qty": actual_qty,
                    "is_bundle": is_bundle,
                    "sub_items": sub_items,
                }
                result.append(row)

        return result
    except Exception as err:
        logging.error(f"Error in get_items for warehouse {warehouse} and item_group {item_group}: {str(err)}\n{traceback.format_exc()}")
        return []

@frappe.whitelist(methods=["POST"])
def make_disbursement_entry(
    disbursement_open: str,
    transaction_id: str,
    beneficiary: str,
    items: List[Dict],
    posting_date=None,
    posting_time=None,
):
    """
    دالة لإنشاء إدخال صرف جديد مع التحقق من امتلاك المستفيد للصنف.
    """
    try:
        warehouse = frappe.db.get_value(
            "Disbursement Open", disbursement_open, "retail_warehouse"
        )
        if not warehouse:
            frappe.throw("لم يتم العثور على مستودع التجزئة للجلسة المحددة.")

        # التحقق من وجود المستفيد في نقطة التوزيع
        if not beneficiary_at_distribution_point(beneficiary, warehouse):
            frappe.throw("المستفيد غير موجود في نقطة التوزيع")

        list_of_delivered_beneficiaies = get_disbursement_open_beneficiaries(disbursement_open)
        if list_of_delivered_beneficiaies:
            for delivered in list_of_delivered_beneficiaies:
                if beneficiary == delivered.beneficiary:
                    frappe.throw("تم بالفعل إجراء صرف لهذا المستفيد سابقًا")

        # التأكد من أن العناصر هي قائمة
        if isinstance(items, dict):
            items = items.get("list", [items])

        # التحقق من امتلاك المستفيد للصنف قبل الصرف
        unauthorized_items = check_beneficiary_entitlement(beneficiary, items)
        if unauthorized_items:
            message = f"المستفيد ليس له الحق في الحصول على العناصر التالية: {', '.join(unauthorized_items)}"
            logging.error(f"Unauthorized items for beneficiary {beneficiary}: {unauthorized_items}")
            return {
                "status": "fail",
                "type": "unauthorized_items",
                "voucher": None,
                "message": message
            }

        # التحقق مما إذا كان إدخال الصرف موجودًا بالفعل
        exist, voucher = is_disbursement_entry_exists(
            disbursement_open, transaction_id, beneficiary, items
        )

        if exist == NOT_SAME:
            return {"status": "fail", "type": "exist", "voucher": voucher}
        if exist == SAME:
            return {"status": "success", "type": "exist", "voucher": voucher}
        else:
            # إنشاء إدخال صرف جديد
            disbursement_entry = frappe.get_doc({
                "doctype": "Disbursement Entry",
                "disbursement_open": disbursement_open,
                "transaction_id": transaction_id,
                "beneficiary": beneficiary,
                "items": items,
                "posting_date": getdate(posting_date) if posting_date else getdate(),
                "posting_time": get_datetime(posting_time) if posting_time else get_datetime(),
            })
            disbursement_entry.submit()

            return {
                "status": "success",
                "type": "new",
                "voucher": disbursement_entry.name,
            }
    except frappe.exceptions.ValidationError as ve:
        logging.error(f"Validation error in make_disbursement_entry: {str(ve)}\n{traceback.format_exc()}")
        return {
            "status": "fail",
            "type": "validation",
            "voucher": None,
            "message": str(ve)
        }
    except Exception as err:
        logging.error(f"Error in make_disbursement_entry: {str(err)}\n{traceback.format_exc()}")
        return {
            "status": "fail",
            "type": "general",
            "voucher": None,
            "message": "An unexpected error occurred."
        }

def is_disbursement_entry_exists(
    disbursement_open: str,
    transaction_id: str,
    beneficiary: str,
    items: List[Dict]
):
    """
    دالة للتحقق مما إذا كان إدخال الصرف موجودًا بالفعل.
    """
    try:
        exist = frappe.db.exists(
            "Disbursement Entry",
            {"transaction_id": transaction_id, "disbursement_open": disbursement_open},
        )
        if exist:
            disbursement_entry = frappe.get_doc("Disbursement Entry", exist)

            if disbursement_entry.beneficiary != beneficiary:
                return NOT_SAME, disbursement_entry.name

            test_items = items.copy()

            for entry_item in disbursement_entry.items:
                found_qty = 0  # تصحيح: تعيين القيمة الابتدائية ل found_qty إلى 0

                index = 0
                while index < len(test_items):
                    current_item = test_items[index]
                    if current_item.get("item") == entry_item.get("item") and current_item.get("uom") == entry_item.get("uom"):
                        found_qty += flt(current_item.get("qty"))
                        test_items.pop(index)
                    else:
                        index += 1

                if flt(found_qty) != flt(entry_item.get("qty")):
                    return NOT_SAME, disbursement_entry.name

            if test_items:
                return NOT_SAME, disbursement_entry.name

            return SAME, disbursement_entry.name
        else:
            return NOT_EXIST, None
    except Exception as err:
        logging.error(f"Error in is_disbursement_entry_exists: {str(err)}\n{traceback.format_exc()}")
        raise  # إعادة رفع الاستثناء ليتم التعامل معه في الدالة الأعلى

def get_last_opened_if_exist(user, date):
    """
    دالة لجلب آخر جلسة صرف مفتوحة للمستخدم في تاريخ معين.
    """
    try:
        doc = frappe.db.get_list(
            "Disbursement Open",
            filters={
                "user": user,
                "posting_date": getdate(date),
                "docstatus": 1,
            },
            fields=["name"],
            limit=1,
            as_list=False,
        )
        if doc:
            return doc[0]
        return False
    except Exception as err:
        logging.error(f"Error in get_last_opened_if_exist for user {user} and date {date}: {str(err)}\n{traceback.format_exc()}")
        return False

def get_disbursement_open_beneficiaries(disbursement_open):
    """
    دالة لجلب قائمة بالمستفيدين المرتبطين بجلسة صرف معينة.
    """
    try:
        beneficiaries_list = frappe.db.sql(
            """
                SELECT
                    de.`name`,
                    de.`bar_code_id` AS `barcode_id`,
                    de.`disbursement_open`,
                    de.`beneficiary`,
                    ded.`item`,
                    ded.`qty`,
                    ded.`uom`,
                    de.`creation`
                FROM `tabDisbursement Entry` AS de
                JOIN `tabDisbursement Entry Detail` AS ded
                    ON de.name = ded.parent
                WHERE
                    de.`disbursement_open` = %(disbursement_open)s
            """,
            {"disbursement_open": disbursement_open},
            as_dict=1,
        )
        return beneficiaries_list
    except Exception as err:
        logging.error(f"Error in get_disbursement_open_beneficiaries for disbursement_open {disbursement_open}: {str(err)}\n{traceback.format_exc()}")
        return []

def beneficiary_at_distribution_point(beneficiary, warehouse):
    """
    دالة للتحقق مما إذا كان المستفيد موجودًا في نقطة التوزيع المحددة.
    """
    try:
        beneficiaries = frappe.get_all(
            "Beneficiary",
            fields=["name"],
            filters={
                "name": beneficiary,
                "retail_warehouse": warehouse,
                "has_items": True,
                "beneficiary_status": "Active",
            },
        )
        return bool(beneficiaries)
    except Exception as err:
        logging.error(f"Error in beneficiary_at_distribution_point for beneficiary {beneficiary} and warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return False

def get_stock_availability(item_code, warehouse):
    """
    دالة لجلب توافر العنصر في المستودع.
    """
    try:
        is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item")
        if is_stock_item:
            is_bundle = False
            bin_qty = get_bin_qty(item_code, warehouse)
            disburse_qty = get_free_disburse_reserved_qty(item_code, warehouse)
            available_qty = bin_qty - disburse_qty
            return available_qty, True, is_bundle
        else:
            if frappe.db.exists("Product Bundle", item_code):
                is_stock_item = True
                is_bundle = True
                bundle_availability = get_bundle_availability(item_code, warehouse)
                return bundle_availability, is_stock_item, is_bundle
            else:
                is_stock_item = False
                is_bundle = False
                # العنصر هو خدمة أو غير مخزون
                return 0, is_stock_item, is_bundle
    except Exception as err:
        logging.error(f"Error in get_stock_availability for item_code {item_code} and warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return 0, False, False  # إرجاع قيم افتراضية في حالة الخطأ

def get_bundle_availability(bundle_item_code, warehouse):
    """
    دالة لجلب توافر عناصر الباندل (الباقات).
    """
    try:
        product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)
        bundle_bin_qty = MAX
        sub_items = {}
        for item in product_bundle.items:
            item_bin_qty = get_bin_qty(item.item_code, warehouse)
            disburse_qty = get_free_disburse_reserved_qty(item.item_code, warehouse)
            available_qty = item_bin_qty - disburse_qty
            sub_items[item.item_code] = item.qty

            max_available_bundles = available_qty / item.qty
            if is_max_available_bundles(bundle_bin_qty, max_available_bundles) and frappe.db.get_value("Item", item.item_code, "is_stock_item"):
                bundle_bin_qty = max_available_bundles

        disburse_qty = get_free_disburse_reserved_qty(bundle_item_code, warehouse)
        return (bundle_bin_qty - disburse_qty, sub_items)
    except Exception as err:
        logging.error(f"Error in get_bundle_availability for bundle_item_code {bundle_item_code} and warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return 0, {}  # إرجاع قيم افتراضية في حالة الخطأ

def get_free_disburse_reserved_qty(item_code, warehouse):
    """
    دالة لجلب الكمية المحجوزة للصرف الحر.
    """
    try:
        disbursement_open = frappe.get_value(
            "Disbursement Open",
            {"user": frappe.session.user, "retail_warehouse": warehouse},
            "name",
        )
        if not disbursement_open:
            return 0

        reserved_qty = frappe.db.sql(
            """
            SELECT SUM(d_item.qty) AS qty
            FROM `tabDisbursement Entry` d
            JOIN `tabDisbursement Entry Detail` d_item ON d.name = d_item.parent
            WHERE
                d.closed = 0
                AND d.docstatus = 1
                AND d_item.item = %s
                AND d.disbursement_open = %s
            """,
            (item_code, disbursement_open),
            as_dict=1,
        )

        return reserved_qty[0].qty if reserved_qty and reserved_qty[0].qty else 0
    except Exception as err:
        logging.error(f"Error in get_free_disburse_reserved_qty for item_code {item_code} and warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return 0  # إرجاع القيمة الافتراضية في حالة الخطأ

def is_max_available_bundles(val1, val2):
    """
    دالة للتحقق مما إذا كانت الكمية المتاحة للباندل كافية.
    """
    return True if val1 is MAX else val1 > val2

def get_bin_qty(item_code, warehouse):
    """
    دالة لجلب الكمية الموجودة في المستودع لعنصر معين.
    """
    try:
        bin_qty = frappe.db.sql(
            """
            SELECT actual_qty
            FROM `tabBin`
            WHERE item_code = %s AND warehouse = %s
            LIMIT 1
            """,
            (item_code, warehouse),
            as_dict=1,
        )

        return bin_qty[0].actual_qty if bin_qty and bin_qty[0].actual_qty else 0
    except Exception as err:
        logging.error(f"Error in get_bin_qty for item_code {item_code} and warehouse {warehouse}: {str(err)}\n{traceback.format_exc()}")
        return 0  # إرجاع القيمة الافتراضية في حالة الخطأ

def check_beneficiary_entitlement(beneficiary, items: List[Dict]):
    """
    دالة للتحقق مما إذا كان المستفيد يمتلك الصنف المطلوب قبل الصرف.

    :param beneficiary: اسم المستفيد
    :param items: قائمة بالعناصر التي سيتم صرفها
    :return: قائمة بالعناصر غير المصرح بها
    """
    try:
        # جلب العناصر المسموح بها للمستفيد
        allowed_items = frappe.get_all(
            "Beneficiary Item",
            fields=["item", "quantity"],
            filters={
                "parent": beneficiary,
                "parentfield": "beneficiary_items",
                "parenttype": "Beneficiary",
            },
        )

        allowed_items_map = {item['item']: item['quantity'] for item in allowed_items}

        unauthorized_items = []

        for item in items:
            item_code = item.get("item")
            disburse_qty = flt(item.get("qty", 0))

            if item_code not in allowed_items_map:
                unauthorized_items.append(item_code)
            else:
                allowed_qty = flt(allowed_items_map[item_code])
                # يمكن إضافة تحقق من عدم تجاوز الكمية المسموح بها إذا كان ذلك مطلوبًا
                # مثال:
                # if disburse_qty > allowed_qty:
                #     unauthorized_items.append(item_code)

        return unauthorized_items
    except Exception as err:
        logging.error(f"Error in check_beneficiary_entitlement for beneficiary {beneficiary}: {str(err)}\n{traceback.format_exc()}")
        return []  # في حالة الخطأ، نعتبر أنه لا توجد عناصر غير مصرح بها