# edited by glg
import sqlite3
import threading
import time
from datetime import datetime
from typing import Callable, Tuple

from pypos.core.utils.worker_pool_utils import submit_ui_query_task_keyed


class TransaksiPenjualanHotspotUseCaseService:
    """
    Memecah hotspot di controller transaksi penjualan agar lebih testable.
    """

    def run_async_settlement_status_check(
        self,
        *,
        controller,
        request_id,
        include_today=False,
        enforce_kasir=False,
        non_fatal_exceptions: Tuple[type, ...] = tuple(),
        submit_task_fn: Callable = None,
    ):
        controller._settlement_check_inflight = True
        started_event = threading.Event()
        submitter = submit_task_fn if callable(submit_task_fn) else submit_ui_query_task_keyed

        def _worker():
            started_event.set()

            payload = {
                "data": [],
                "current_kasir": None,
                "has_other_kasir": False,
                "ok": True,
                "error": "",
                "error_code": "",
                "reason": "",
                "trace_id": f"settlement-check-{int(request_id or 0)}",
            }
            try:
                data = controller.model_settle.cek_transaksi_settlement(include_today=include_today)

                current_kasir = None
                has_other_kasir = False

                if enforce_kasir:
                    current_kasir = controller.user_info.get("nama") if controller.user_info else None
                    has_other_kasir = controller.transaksi_payment_state_service.detect_other_kasir(
                        data,
                        current_kasir=current_kasir,
                    )

                payload["data"] = data or []
                payload["current_kasir"] = current_kasir
                payload["has_other_kasir"] = bool(has_other_kasir)
            except (TypeError, ValueError) as exc:
                payload["ok"] = False
                payload["error_code"] = "SETTLEMENT_CHECK_DATA_ERROR"
                payload["reason"] = "settlement_check_data_error"
                payload["error"] = str(exc)
            except RuntimeError as exc:
                payload["ok"] = False
                payload["error_code"] = "SETTLEMENT_CHECK_RUNTIME_ERROR"
                payload["reason"] = "settlement_check_runtime_error"
                payload["error"] = str(exc)
            except tuple(non_fatal_exceptions or tuple()) as exc:
                payload["ok"] = False
                payload["error_code"] = "SETTLEMENT_CHECK_UNEXPECTED_ERROR"
                payload["reason"] = "settlement_check_unexpected_error"
                payload["error"] = str(exc)

            if hasattr(controller.view, "emit_settlement_payload"):
                controller.view.emit_settlement_payload(request_id, payload)
                return

            controller._on_settlement_payload_ready(request_id, payload)

        key = f"transaksi-settlement-check:{id(controller)}"
        future = submitter(key, _worker)
        if future is None:
            controller._settlement_check_inflight = False
            return

        def _on_done(_):
            if not started_event.is_set():
                controller._settlement_check_inflight = False

        try:
            future.add_done_callback(_on_done)
        except (RuntimeError, TypeError, AttributeError):
            if not started_event.is_set():
                controller._settlement_check_inflight = False

    def handle_jumlah_berubah(self, *, controller, row, column):
        if not controller._ensure_settlement_guard():
            return

        if column != 4:
            return

        controller.log_debug("masuk ke handle jumlah berubah controller")
        try:
            id_item = controller.view.table_barang.item(row, 0)
            if not id_item:
                return

            id_barang = str(id_item.text())
            prev_qty = 1
            try:
                prev_qty = int(controller.data_barang_cache.get(id_barang, {}).get("jumlah", 1))
            except (TypeError, ValueError):
                prev_qty = 1

            jumlah_item_widget = controller.view.table_barang.item(row, 4)
            if not jumlah_item_widget:
                return

            plan = controller._get_transaksi_barang_mutation_use_case_service().build_manual_qty_plan(
                id_barang=id_barang,
                prev_qty=prev_qty,
                qty_text=jumlah_item_widget.text(),
                normalize_manual_qty_callback=controller.transaksi_barang_input_service.normalize_manual_qty,
                ensure_admin_authorized_callback=controller._ensure_admin_qty_besar_authorized,
                find_barang_detail_callback=controller.model.cari_barang_by_id,
                is_valid_harga_callback=controller._is_valid_harga_jual,
            )

            if not bool(plan.get("ok")):
                reason = str(plan.get("reason") or "")
                if reason == "invalid_qty":
                    controller.view.show_warning("Input Tidak Valid", "Jumlah tidak boleh nol atau negatif.")
                    controller.view.set_jumlah_row(row, int(plan.get("revert_qty") or 1))
                    return
                if reason == "no_change":
                    return
                if reason == "invalid_harga":
                    controller._warn_harga_jual_invalid(plan.get("barang_detail"), id_barang)
                if reason in {"admin_rejected", "invalid_harga"}:
                    controller.view.set_jumlah_row(row, int(plan.get("revert_qty") or prev_qty))
                return

            barang_detail = plan.get("barang_detail") if isinstance(plan.get("barang_detail"), dict) else {}
            controller.data_barang_cache[id_barang] = barang_detail

            controller.view.table_barang.blockSignals(True)
            controller.view.update_row_barang(row, barang_detail)
            controller.view.table_barang.blockSignals(False)
            controller.view.update_info_diskon(barang_detail)
            controller.view.update_ringkasan()

        except (TypeError, ValueError, RuntimeError, KeyError, AttributeError) as exc:
            controller.log_error(f"Gagal update jumlah: {exc}")
            controller.view.table_barang.blockSignals(False)

    # edited by glg
    def buka_dialog_pembayaran(self, *, controller, dialog_accepted):
        if not controller._ensure_settlement_guard():
            return

        row_count = controller.view.table_barang.rowCount()
        is_processing = (
            hasattr(controller.view, "_sedang_proses_pembayaran")
            and controller.view._sedang_proses_pembayaran
        )
        if row_count == 0:
            controller.view.show_warning(
                "Keranjang Kosong",
                "Tidak ada barang di keranjang.\nSilakan tambahkan barang terlebih dahulu.",
            )
            return

        if not controller.transaksi_payment_state_service.can_open_payment_dialog(row_count, is_processing):
            controller.log_warning("Dialog pembayaran sudah terbuka, abaikan klik ganda")
            return

        controller.transaksi_payment_state_service.start_payment_process(controller.view)
        controller.log_info("Membuka dialog Multi-Payment (F8)")

        info = controller._build_info_transaksi_from_table()
        controller.log_debug(
            f"info pembayaran jenis_item={info.jenis_item} total_qty={info.total_qty} total_belanja={info.total_belanja}"
        )

        result_code, hasil = controller.view.show_payment_dialog(info, multi_payment_mode=True)
        dialog_state = controller.transaksi_payment_flow_service.classify_dialog_result(
            result_code,
            hasil,
            dialog_accepted,
        )

        if dialog_state == "cancel":
            controller.log_info("Pembayaran dibatalkan oleh user (klik Batal/Esc)")
            controller.transaksi_payment_state_service.finish_payment_process(controller.view, success=False)
            return

        if dialog_state == "empty":
            controller.log_warning("Pembayaran dikonfirmasi tapi result None")
            controller.transaksi_payment_state_service.finish_payment_process(controller.view, success=False)
            return

        if dialog_state == "multi":
            controller.log_info(f"Pembayaran kombinasi: {len(hasil)} metode")
            success = controller.simpan_transaksi_multi_payment(hasil)
        else:
            controller.log_info(f"Pembayaran dikonfirmasi: {hasil.metode}")
            success = controller.simpan_transaksi_dengan_pembayaran(hasil)

        controller.transaksi_payment_state_service.finish_payment_process(controller.view, success=success)

    # edited by glg
    def process_barang_text_input(self, *, controller, text, barcode_only=False):
        scan_started = controller.performance_service.mark()

        barang_detail, error_message = controller.transaksi_barang_input_service.find_barang(
            model=controller.model,
            text=text,
            barcode_only=barcode_only,
        )
        if not barang_detail:
            controller.performance_service.record(
                "scan_loop",
                scan_started,
                threshold_ms=100,
                context=f"found=0 barcode_only={1 if barcode_only else 0}",
            )
            if error_message:
                # Gunakan judul yang mudah dipahami kasir untuk kasus harga belum diatur.
                warning_title = "Barang Tidak Ditemukan"
                error_lower = str(error_message or "").strip().lower()
                if (
                    "harga belum diatur" in error_lower
                    or "harganya belum diatur" in error_lower
                    or "belum memiliki harga jual aktif" in error_lower
                ):
                    warning_title = "Harga Barang Belum Diatur"

                controller.view.show_warning(warning_title, error_message)
            return False

        ok_qty, jumlah = controller.transaksi_barang_input_service.resolve_input_qty(
            popup_enabled=controller.view.popup_checkbox.isChecked(),
            input_dialog_callback=controller.view.input_jumlah_dialog,
        )
        if not ok_qty:
            controller.performance_service.record(
                "scan_loop",
                scan_started,
                threshold_ms=100,
                context=f"found=1 qty_cancel=1 barcode_only={1 if barcode_only else 0}",
            )
            return False

        success = controller._apply_barang_by_id(str(barang_detail["id"]), jumlah)
        controller.performance_service.record(
            "scan_loop",
            scan_started,
            threshold_ms=100,
            context=f"found=1 success={1 if success else 0} barcode_only={1 if barcode_only else 0}",
        )
        return success

    # edited by glg
    def refresh_barang_autocomplete(self, *, controller, keyword, async_mode=False):
        normalized = controller.autocomplete_service.normalize_keyword(keyword)

        if not controller.autocomplete_service.should_lookup(normalized):
            controller.barang_mapping = {}
            controller.view.set_barang_autocomplete([])
            return

        if bool(async_mode):
            request_id = controller._next_autocomplete_request_id()
            controller._run_async_autocomplete_fetch(request_id, normalized)
            return

        lookup_started = controller.performance_service.mark()
        barang_list, mapping = controller.model.get_produk_autocomplete(
            keyword=normalized,
            limit=controller.autocomplete_service.get_limit(),
        )

        controller.barang_mapping = mapping
        controller.view.set_barang_autocomplete(barang_list)
        context = f"keyword_len={len(normalized)} result={len(barang_list)}"
        controller.performance_service.record(
            "autocomplete_lookup",
            lookup_started,
            threshold_ms=80,
            context=context,
        )
        if normalized:
            controller.performance_service.record(
                "first_search",
                lookup_started,
                threshold_ms=None,
                context=context,
                once_key="first_search",
            )

    # edited by glg
    def buka_penyimpanan_dialog(self, *, controller, save_error_type):
        if not controller._ensure_settlement_guard():
            return

        try:
            if hasattr(controller.view, "table_barang") and controller.view.table_barang.rowCount() == 0:
                controller.view.show_warning(
                    "Keranjang Kosong",
                    "Tidak ada barang di keranjang.\nSilakan tambahkan barang terlebih dahulu sebelum menyimpan transaksi.",
                )
                return
        except (RuntimeError, TypeError, AttributeError) as exc:
            controller.log_warning(f"Gagal memeriksa rowCount keranjang sebelum buka penyimpanan: {exc}")

        confirmed = controller.view.show_confirmation(
            "Simpan Transaksi?",
            "Apakah Anda ingin menyimpan kondisi transaksi sekarang?",
        )

        if not confirmed:
            return

        transaksi_data, detail_data, _transaksi_data_dict = controller.view.kumpulkan_data_transaksi()
        transaksi_data, detail_data = controller.transaksi_preorder_service.prepare_save_payload(
            transaksi_data,
            detail_data,
        )
        if not transaksi_data:
            return

        try:
            transaksi_id = controller.model.simpan_transaksi_f9(transaksi_data, detail_data)
            controller.view.reset_form()
            controller._clear_preorder_payload()
            controller.view.show_info("Berhasil", f"Transaksi disimpan dengan ID: {transaksi_id}")
        except save_error_type as exc:
            code = str(getattr(exc, "code", "TRX_SAVE_F9_ERROR") or "TRX_SAVE_F9_ERROR")
            controller.view.show_error(f"Gagal menyimpan transaksi:\n{exc}\nKode: {code}")
        except (TypeError, ValueError, RuntimeError) as exc:
            controller.view.show_error(f"Gagal menyimpan transaksi:\n{exc}")

    # edited by glg
    def load_transaksi_ke_view(
        self,
        *,
        controller,
        detail_rows,
        cust_id,
        cust_nama,
        diskon,
        ppn,
        total_harga,
        preorder_payload=None,
    ):
        _ = cust_id
        controller.view.reset_form()
        controller.row_index_service.clear()

        table = getattr(controller.view, "table_barang", None)
        if table:
            table.setUpdatesEnabled(False)
            table.blockSignals(True)
        try:
            for item in detail_rows:
                produk_id, nama, harga, jumlah, diskon_persen, satuan = item
                controller.view.tambah_barang_ke_tabel(
                    {
                        "id": produk_id,
                        "nama": nama,
                        "harga": harga,
                        "jumlah": jumlah,
                        "diskon_persen": diskon_persen,
                        "satuan": satuan,
                    }
                )
                controller._register_row_index(
                    str(produk_id),
                    controller.view.table_barang.rowCount() - 1,
                )
        finally:
            if table:
                table.blockSignals(False)
                table.setUpdatesEnabled(True)
                table.viewport().update()

        index = controller.view.customer_combo.findText(cust_nama)
        if index != -1:
            controller.view.customer_combo.setCurrentIndex(index)

        controller.view.diskon_input.setValue(int(diskon))
        controller.view.ppn.setText(f"{ppn:,.0f}")
        controller.view.total_bayar.setText(f"{total_harga:,.0f}")
        controller.view.update_ringkasan()

        controller._preorder_payload = None
        controller._preorder_payload = controller.transaksi_preorder_service.build_restore_payload(
            preorder_payload,
            detail_rows,
        )

    # edited by glg
    def reset_form_transaksi(self, *, controller, view):
        """Reset form transaksi penjualan."""
        view.table_barang.setRowCount(0)
        controller.row_index_service.clear()

        # Reset cache otorisasi qty besar saat sesi keranjang direset.
        controller.high_qty_authorization_service.reset()

        view.diskon_input.setValue(0)
        view.total_label.setText("0")

        if hasattr(view, "info_total_produk_label"):
            view.info_total_produk_label.setText("0")
        if hasattr(view, "info_diskon_label"):
            view.info_diskon_label.setText("0")
        if hasattr(view, "info_diskon_member_persen_label"):
            view.info_diskon_member_persen_label.setText("0.00 %")
        if hasattr(view, "info_diskon_customer_label"):
            view.info_diskon_customer_label.setText("0")
        if hasattr(view, "info_additional_diskon_label"):
            view.info_additional_diskon_label.setText("0")
        if hasattr(view, "info_grand_total_label"):
            view.info_grand_total_label.setText("0")
        if hasattr(view, "info_cashback_label"):
            view.info_cashback_label.setText("0")
        if hasattr(view, "info_point_label"):
            view.info_point_label.setText("0")
        if hasattr(view, "info_free_item_label"):
            view.info_free_item_label.setText("-")

        view.ppn.setText("")
        view.total_bayar.setText("")
        view._sedang_proses_pembayaran = False
        view.button_simpan.setEnabled(False)

        if hasattr(view, "cart_sku_input"):
            view.cart_sku_input.clear()
        if hasattr(view, "cart_qty_input"):
            view.cart_qty_input.setValue(1)
        if hasattr(view, "barang_input"):
            view.barang_input.clear()

        if hasattr(view, "diskon_groupbox_map"):
            for groupbox, _ in view.diskon_groupbox_map.values():
                groupbox.setParent(None)
            view.diskon_groupbox_map.clear()

        while view.info_diskon_layout.count():
            item = view.info_diskon_layout.takeAt(0)
            widget = item.widget()
            if widget:
                widget.deleteLater()

    # edited by glg
    def update_ringkasan_from_view(
        self,
        *,
        controller,
        view,
        parse_rupiah_callback,
        parse_int_callback,
    ):
        total_harga, items = controller.transaksi_table_input_service.collect_ringkasan_items(
            table_barang=view.table_barang,
            data_barang_cache=controller.data_barang_cache,
            parse_rupiah_callback=parse_rupiah_callback,
            parse_int_callback=parse_int_callback,
        )

        diskon_persen = view.diskon_input.value()
        customer_id = view.customer_combo.currentData() if hasattr(view, "customer_combo") else 0

        diskon_customer_ctx = controller.diskon_customer_model.calculate_benefits(total_harga, customer_id)
        info_pembayaran = controller.pembayaran_info_service.hitung_info(
            items,
            diskon_persen,
            diskon_customer_ctx.get("diskon_nilai", 0.0),
        )

        total_setelah_diskon = info_pembayaran["total_bayar"]
        ppn_value = controller.penjualan_config_service.get_ppn_percent()
        ppn_mode = controller.penjualan_config_service.get_ppn_mode(default="include")
        if ppn_mode == "exclude":
            ppn = total_setelah_diskon * (ppn_value / 100)
            total_bayar = total_setelah_diskon + ppn
        else:
            # Mode include: PPN tetap informatif dan tidak menambah total bayar.
            ppn = total_setelah_diskon * (ppn_value / (100 + ppn_value)) if ppn_value > 0 else 0.0
            total_bayar = total_setelah_diskon

        view.total_label.setText(view.format_rupiah(total_bayar))
        if hasattr(view, "ppn"):
            view.ppn.setText(view.format_rupiah(ppn))
        if hasattr(view, "total_bayar"):
            view.total_bayar.setText(view.format_rupiah(total_bayar))
        if hasattr(view, "update_info_pembayaran"):
            view.update_info_pembayaran(
                total_produk=info_pembayaran["total_produk"],
                diskon_produk=info_pembayaran["diskon_produk"],
                free_items=info_pembayaran["free_items"],
                total_bayar=total_bayar,
                diskon_customer=info_pembayaran.get("diskon_customer", 0),
                cashback=diskon_customer_ctx.get("cashback_nilai", 0),
                point=diskon_customer_ctx.get("point_nilai", 0),
                diskon_member_persen=diskon_customer_ctx.get("diskon_persen", 0),
                additional_diskon=info_pembayaran.get("diskon_tambahan", 0),
                grand_total=total_bayar,
            )

        controller._diskon_customer_ctx = diskon_customer_ctx

    # edited by glg
    def kumpulkan_data_transaksi_view(self, *, controller, view):
        if view.table_barang.rowCount() == 0:
            controller.show_warning("Validasi", "Belum ada barang ditambahkan.", view=view)
            return None

        # Guard kualitas payload transaksi:
        # - mode default: kompatibel legacy (baris invalid di-skip + warning terbatas)
        # - strict mode (opsional via config): fail-fast agar tidak ada silent data loss.
        payload_strict_mode = controller.penjualan_config_service.is_transaksi_payload_strict_mode_enabled(default=0)
        payload_error_log_limit = controller.penjualan_config_service.get_transaksi_payload_error_log_limit(default=20)
        payload_row_errors = []

        def _on_payload_row_error(event):
            data = event if isinstance(event, dict) else {}
            stage = str(data.get("stage") or "payload").strip()
            row_index = int(data.get("row_index") or 0)
            reason = str(data.get("reason") or data.get("error") or "unknown").strip()
            produk_id = str(data.get("produk_id") or "").strip()
            produk_nama = str(data.get("produk_nama") or "").strip()
            payload_row_errors.append(
                {
                    "stage": stage,
                    "row_index": row_index,
                    "reason": reason,
                    "produk_id": produk_id,
                    "produk_nama": produk_nama,
                }
            )
            suffix = ""
            if produk_id or produk_nama:
                suffix = f" produk_id={produk_id or '-'} produk_nama={produk_nama or '-'}"
            controller.log_warning(
                f"[PayloadGuard] stage={stage} row={row_index} reason={reason}{suffix}"
            )

        try:
            rows = controller.transaksi_table_input_service.collect_payload_rows(
                view.table_barang,
                data_barang_cache=controller.data_barang_cache,
                on_row_error=_on_payload_row_error,
                strict_mode=payload_strict_mode,
                error_log_limit=payload_error_log_limit,
            )
            detail_data, total_harga = controller.transaksi_payload_service.build_detail_data(
                rows,
                on_row_error=_on_payload_row_error,
                strict_mode=payload_strict_mode,
                error_log_limit=payload_error_log_limit,
            )
        except (TypeError, ValueError, KeyError, RuntimeError) as exc:
            controller.log_warning(f"Kumpulkan payload transaksi gagal: {exc}")
            controller.show_warning(
                "Validasi",
                "Data transaksi tidak valid. Periksa kembali item pada keranjang.",
                view=view,
            )
            return None

        if not detail_data:
            controller.show_warning("Validasi", "Detail transaksi tidak valid.", view=view)
            return None

        # Jangan izinkan silent row-drop; semua baris invalid wajib diperbaiki sebelum simpan.
        if payload_row_errors:
            sample_rows = []
            for err in payload_row_errors[:3]:
                sample_rows.append(f"row={int(err.get('row_index') or 0)} ({err.get('reason') or 'unknown'})")
            detail_hint = ", ".join(sample_rows)
            controller.show_warning(
                "Validasi",
                (
                    f"Ditemukan {len(payload_row_errors)} baris item tidak valid.\n"
                    "Perbaiki data item terlebih dahulu sebelum simpan transaksi.\n"
                    f"Contoh: {detail_hint}"
                ),
                view=view,
            )
            return None

        diskon_input = view.diskon_input.value()
        customer_id = view.customer_combo.currentData()
        diskon_customer_ctx = controller.diskon_customer_model.calculate_benefits(total_harga, customer_id)
        ppn_value = controller.penjualan_config_service.get_ppn_percent()
        ppn_mode = controller.penjualan_config_service.get_ppn_mode(default="include")
        customer_nama_asli = view.controller.get_customer_nama_by_id(customer_id) if customer_id else "tunai"

        return controller.transaksi_payload_service.build_transaksi_payload(
            detail_data=detail_data,
            total_harga=total_harga,
            diskon_input=diskon_input,
            customer_id=customer_id,
            customer_text=view.customer_combo.currentText(),
            customer_nama_asli=customer_nama_asli,
            user_info=view.controller.user_info,
            diskon_customer_ctx=diskon_customer_ctx,
            ppn_percent=ppn_value,
            ppn_mode=ppn_mode,
        )

    def _handle_scanned_barcode(self, *, controller, barcode, source="unknown"):

        barcode = str(barcode or "").strip()

        if not barcode:

            return False

        if not controller._ensure_settlement_guard():

            return True



        if hasattr(controller.view, "cart_sku_input"):

            controller.view.cart_sku_input.clear()

        if hasattr(controller.view, "barang_input"):

            controller.view.barang_input.clear()

        if hasattr(controller.view, "cart_qty_input"):

            controller.view.cart_qty_input.setValue(1)

        controller._process_barang_text_input(barcode, barcode_only=True)

        if hasattr(controller.view, "cart_sku_input"):

            controller.view.cart_sku_input.setFocus()

        return True

    def _ensure_settlement_guard(self, *, controller):

        if not getattr(controller, "_settlement_state_initialized", False):
            if not bool(getattr(controller, "_settlement_check_inflight", False)):
                controller.cek_status_settlement(async_mode=True)
            controller._notify_settlement_check_pending()
            return False

        if not controller.is_settlement_blocked():

            return True

        controller.show_warning(

            "Settlement Diperlukan",

            "Masih ada transaksi hari sebelumnya yang belum settlement.\nSelesaikan settlement terlebih dahulu.",

            view=controller.view,

        )

        return False

    def _notify_settlement_check_pending(self, *, controller):
        now_ts = time.monotonic()
        last_ts = float(getattr(controller, "_settlement_pending_notice_ts", 0.0) or 0.0)
        if (now_ts - last_ts) < 2.0:
            return
        controller._settlement_pending_notice_ts = now_ts
        view = getattr(controller, "view", None)
        if view is not None and hasattr(view, "show_toast"):
            try:
                view.show_toast(
                    "Memeriksa status settlement, silakan tunggu sesaat.",
                    duration_ms=1800,
                    level="information",
                )
                return
            except (RuntimeError, TypeError, AttributeError):
                pass
        controller.log_info("[Settlement] Menunggu hasil pengecekan settlement async.")

    def _restore_preorder_payload(self, *, controller):

        payload = controller._preorder_payload

        if not payload:

            return False

        if controller._preorder_exists(payload.get("nomer")):

            return True

        try:

            controller.model.simpan_transaksi_f9(payload["transaksi_data"], payload["detail_data"])

            return True

        except TransaksiSaveError as exc:
            code = str(getattr(exc, "code", "TRX_SAVE_F9_ERROR") or "TRX_SAVE_F9_ERROR")
            controller.view.show_error(f"Gagal mengembalikan transaksi pre order:\n{exc}\nKode: {code}")
            return False
        except (TypeError, ValueError, RuntimeError) as exc:
            controller.view.show_error(f"Gagal mengembalikan transaksi pre order:\n{exc}")
            return False

    def handle_clear_cart(self, *, controller):

        if not controller._preorder_payload:

            controller.view.reset_form()

            return



        confirmed_delete = controller.ask_confirm(

            "Konfirmasi",

            "Transaksi ini berasal dari pre order.\n"

            "Apakah yakin ingin menghapus transaksi tersimpan?",

            view=controller.view,

            yes_label="Hapus",

            no_label="Batal",

            default_no=True,

        )



        if confirmed_delete:

            controller.view.reset_form()

            controller._clear_preorder_payload()

            return



        restored = controller._restore_preorder_payload()

        controller.view.reset_form()

        if restored:

            controller.view.show_info("Pre Order", "Transaksi dikembalikan ke daftar pre order.")

        else:

            controller.view.show_warning("Pre Order", "Transaksi gagal dikembalikan ke pre order.")

        controller._clear_preorder_payload()

    def buka_history_penjualan(self, *, controller):

        """

        Membuka dialog/history penjualan untuk user yang login hari ini.

        """

        if not controller._ensure_settlement_guard():

            return

        if controller.parent_window and hasattr(controller.parent_window, "buka_page_history"):

            controller.parent_window.buka_page_history()

            return

        user_id = controller.user_info.get('id', None)

        today = datetime.now().strftime('%Y-%m-%d')

        dialog = controller.controller_factory.create_load_transaksi_controller(

            controller.view,

            controller,

            controller.model.db_path,

        )

        dialog.set_filter(user_id=user_id, tanggal=today)

        dialog.view.setWindowModality(2)

        dialog.view.setWindowTitle("History Penjualan Hari Ini")

        dialog.view.exec()

    def cek_status_settlement(self, *, controller, include_today=False, enforce_kasir=False, async_mode=False):

        if bool(async_mode):

            request_id = controller._next_settlement_request_id()

            controller._run_async_settlement_status_check(

                request_id=request_id,

                include_today=include_today,

                enforce_kasir=enforce_kasir,

            )

            return

        data = controller.model_settle.cek_transaksi_settlement(include_today=include_today)

        current_kasir = None

        has_other_kasir = False

        if enforce_kasir:

            current_kasir = controller.user_info.get("nama") if controller.user_info else None

            has_other_kasir = controller.transaksi_payment_state_service.detect_other_kasir(
                data,
                current_kasir=current_kasir,
            )

        controller.settlement_result(data, has_other_kasir=has_other_kasir, current_kasir=current_kasir)

    def _on_settlement_payload_ready(self, *, controller, request_id, payload):
        controller._settlement_check_inflight = False

        if not controller._is_latest_settlement_request(request_id):

            return

        data = payload if isinstance(payload, dict) else {}
        if not bool(data.get("ok", True)):
            error_code = str(data.get("error_code") or "SETTLEMENT_CHECK_FAILED")
            controller.log_warning(
                f"Gagal cek status settlement async [{error_code}]: {str(data.get('error') or 'unknown_error')}"
            )
            # edited by glg
            # Jika check settlement gagal, jangan kunci permanen.
            # Tetap blok aksi transaksi sampai status tervalidasi ulang, lalu jadwalkan retry.
            controller._settlement_state_initialized = False
            controller._set_settlement_blocked(True)
            controller._schedule_settlement_recheck(delay_ms=1800)
            controller._notify_settlement_check_pending()
            return

        controller.settlement_result(

            data.get("data") or [],

            has_other_kasir=bool(data.get("has_other_kasir")),

            current_kasir=data.get("current_kasir"),

        )

    def buka_customer_dialog(self, *, controller):

        if not controller._ensure_settlement_guard():

            return

        controller.log_debug("buka_customer_dialog dipanggil")

        dialog = controller.controller_factory.create_customer_search_controller(parent=controller.view)

        selected_id = dialog.show()

        controller.log_debug(f"dialog.show() selesai, selected_id={selected_id}")

        if selected_id:

            controller.log_info(f"Customer dipilih: {selected_id}")

            if hasattr(controller.view, 'set_customer_by_id'):

                controller.view.set_customer_by_id(selected_id)

    def simpan_transaksi_multi_payment(self, *, controller, payment_list):

        controller.log_info(f"Simpan transaksi dengan {len(payment_list)} metode pembayaran")

        data_ui = controller._collect_transaksi_data_for_save()

        if not data_ui:

            return False



        transaksi_data, detail_data, transaksi_data_dict = data_ui

        transaksi_data_dict, metode_text, _, _ = controller.model.apply_multi_payment(

            transaksi_data_dict,

            payment_list

        )

        return controller._persist_transaksi_with_payment(

            transaksi_data=transaksi_data,

            detail_data=detail_data,

            transaksi_data_dict=transaksi_data_dict,

            metode_text=metode_text,

            audit_message=f"Transaksi Multi-Payment [{metode_text}] oleh {transaksi_data_dict['oleh_nama']}",

            voucher_callback=lambda transaksi_id, conn=None: controller._apply_voucher_usage_multi(
                transaksi_id,
                payment_list,
                conn=conn,
            ),

        )

    def simpan_transaksi_dengan_pembayaran(self, *, controller, hasil_pembayaran):

        controller.log_info(f"Simpan transaksi dengan pembayaran: {hasil_pembayaran}")

        data_ui = controller._collect_transaksi_data_for_save()

        if not data_ui:

            return False



        transaksi_data, detail_data, transaksi_data_dict = data_ui

        transaksi_data_dict, metode_text = controller.model.apply_single_payment(transaksi_data_dict, hasil_pembayaran)



        return controller._persist_transaksi_with_payment(

            transaksi_data=transaksi_data,

            detail_data=detail_data,

            transaksi_data_dict=transaksi_data_dict,

            metode_text=metode_text,

            audit_message=f"Transaksi [{metode_text}] oleh {transaksi_data_dict['oleh_nama']}",

            voucher_callback=lambda transaksi_id, conn=None: controller._apply_voucher_usage_single(
                transaksi_id,
                hasil_pembayaran,
                conn=conn,
            ),

        )

    def proses_pilih_barang(self, *, controller, display_text):

        if not controller._ensure_settlement_guard():

            return

        if getattr(controller, '_proses_barang_aktif', False):

            return



        controller._proses_barang_aktif = True



        try:

            barang_data = controller.barang_mapping.get(display_text)

            if not barang_data:

                return

            jumlah, ok = controller.view.input_jumlah_dialog()

            if not ok:

                return

            controller._apply_barang_by_id(str(barang_data["id"]), jumlah)



        finally:

            controller._proses_barang_aktif = False

    def _update_quota_free_produk(self, *, controller, arr_free_produk):

        if not arr_free_produk:

            return

        result = controller.transaksi_service.update_quota_free_produk(arr_free_produk)
        trace_id = result.get("trace_id") if isinstance(result, dict) else ""

        if result.get("status") == 1:

            if trace_id:
                controller.log_info(f"Kuota free produk berhasil diupdate ke server. trace_id={trace_id}")
            else:
                controller.log_info("Kuota free produk berhasil diupdate ke server")

        else:

            if trace_id:
                controller.log_error(f"Gagal update free produk: {result.get('reason')} | trace_id={trace_id}")
            else:
                controller.log_error(f"Gagal update free produk: {result.get('reason')}")

    def _dispatch_free_produk_dead_alert(self, *, controller, alert_payload):
        alert_service = getattr(controller, "free_produk_outbox_alert_service", None)
        if alert_service is None:
            alert_service = FreeProdukOutboxAlertService()
        payload = alert_service.build_channel_payload(alert_payload)
        dead_count = int(payload.get("dead_count") or 0)
        top_items = str(payload.get("top_items_text") or "-")

        controller.log_warning(
            f"[ALERT][OUTBOX_DEAD] dead_count={dead_count} top={top_items}"
        )

        dispatched = False
        view = getattr(controller, "view", None)
        if view is not None and hasattr(view, "show_toast"):
            try:
                view.show_toast(
                    alert_service.build_toast_message(payload),
                    duration_ms=2600,
                    level="warning",
                )
                dispatched = True
            except (RuntimeError, TypeError, AttributeError) as exc:
                controller.log_warning(f"[ALERT][OUTBOX_DEAD] gagal kirim toast: {exc}")

        parent_window = getattr(controller, "parent_window", None) or getattr(view, "parent_window", None)
        callback_names = (
            "on_free_produk_outbox_dead_alert",
            "handle_outbox_alert",
            "on_outbox_alert",
        )
        for callback_name in callback_names:
            if parent_window is None or not hasattr(parent_window, callback_name):
                continue
            try:
                getattr(parent_window, callback_name)(payload)
                dispatched = True
                break
            except (RuntimeError, TypeError, AttributeError) as exc:
                controller.log_warning(
                    f"[ALERT][OUTBOX_DEAD] gagal kirim callback {callback_name}: {exc}"
                )

        if not dispatched:
            controller.log_warning(
                "[ALERT][OUTBOX_DEAD] channel aktif tidak tersedia, fallback ke log warning."
            )

    def _log_free_produk_outbox_backlog(self, *, controller):
        now_ts = time.monotonic()
        last_ts = float(getattr(controller, "_free_produk_outbox_notice_ts", 0.0) or 0.0)
        if (now_ts - last_ts) < 20.0:
            return
        snapshot = controller._get_free_produk_outbox_backlog_snapshot()
        pending_count = int(snapshot.get("pending") or 0)
        inflight_count = int(snapshot.get("inflight") or 0)
        dead_count = int(snapshot.get("dead") or 0)
        if pending_count <= 0 and inflight_count <= 0 and dead_count <= 0:
            return
        controller._free_produk_outbox_notice_ts = now_ts
        level_log = controller.log_warning if dead_count > 0 else controller.log_info
        level_log(
            f"Backlog outbox free produk pending={pending_count} inflight={inflight_count} dead={dead_count}"
        )
        if dead_count > 0:
            last_dead_alert_ts = float(getattr(controller, "_free_produk_dead_alert_notice_ts", 0.0) or 0.0)
            if (now_ts - last_dead_alert_ts) < 60.0:
                return
            controller._free_produk_dead_alert_notice_ts = now_ts
            try:
                alert_payload = controller.free_produk_outbox_service.build_dead_letter_alert(limit=3)
            except sqlite3.Error as exc:
                controller.log_warning(f"[ALERT][OUTBOX_DEAD] gagal memuat ringkasan dead-letter: {exc}")
                return
            controller._dispatch_free_produk_dead_alert(alert_payload)

    def _drain_free_produk_outbox_async(self, *, controller, max_items=3):
        lock = getattr(controller, "_free_produk_drain_lock", None)
        if lock is None:
            controller._free_produk_drain_lock = threading.Lock()
            lock = controller._free_produk_drain_lock
        with lock:
            if bool(getattr(controller, "_free_produk_drain_inflight", False)):
                return False
            controller._free_produk_drain_inflight = True

        def _on_done(processed):
            try:
                controller.log_debug(f"Outbox free produk diproses batch={int(processed or 0)}")
                controller._log_free_produk_outbox_backlog()
            finally:
                controller._finish_free_produk_outbox_drain()

        def _on_error(exc):
            try:
                controller.log_warning(f"Outbox free produk async gagal: {exc}")
                controller._log_free_produk_outbox_backlog()
            finally:
                controller._finish_free_produk_outbox_drain()

        try:
            controller.background_task_service.run(
                controller._drain_free_produk_outbox_once,
                max_items,
                on_done=_on_done,
                on_error=_on_error,
            )
            return True
        except (RuntimeError, TypeError, AttributeError) as exc:
            controller.log_warning(f"Gagal menjadwalkan outbox free produk async: {exc}")
            controller._finish_free_produk_outbox_drain()
            return False

    def handle_input_barcode(self, *, controller):

        if not controller._ensure_settlement_guard():

            return

        controller.log_debug("masuk ke handle input barcode")

        barcode_input = controller.view.barang_input.text().strip()

        if not barcode_input:

            return



        controller.view.barang_input.clear()
        scanner = getattr(controller, "scanner_controller", None)
        normalized = barcode_input
        if scanner is not None:
            accepted = scanner.accept_manual_barcode(barcode_input)
            if not accepted:
                return
            normalized = accepted
        controller._handle_scanned_barcode(normalized, source="manual_barcode")

    def handle_barang_input(self, *, controller):

        if not controller._ensure_settlement_guard():

            return

        controller.log_debug("masuk ke handle barang input")

        text = controller.view.barang_input.text().strip()

        controller.view.barang_input.clear()



        if not text:

            return



        controller._process_barang_text_input(text, barcode_only=False)

    def _apply_barang_by_id(self, *, controller, id_barang, tambahan_qty):
        plan = controller._get_transaksi_barang_mutation_use_case_service().build_apply_plan(
            id_barang=id_barang,
            tambahan_qty=tambahan_qty,
            find_barang_row_callback=controller.find_barang_row_by_id,
            compute_total_qty_callback=controller.transaksi_barang_input_service.compute_total_qty,
            ensure_admin_authorized_callback=controller._ensure_admin_qty_besar_authorized,
            find_barang_detail_callback=controller.model.cari_barang_by_id,
            is_valid_harga_callback=controller._is_valid_harga_jual,
        )

        if not bool(plan.get("ok")):
            if str(plan.get("reason") or "") == "invalid_harga":
                controller._warn_harga_jual_invalid(plan.get("barang_detail"), id_barang)
            return False

        pid = str(plan.get("id_barang") or str(id_barang or ""))
        barang_detail = plan.get("barang_detail") if isinstance(plan.get("barang_detail"), dict) else {}
        existing_row = plan.get("existing_row")
        controller.data_barang_cache[pid] = barang_detail

        if existing_row is not None:
            controller.view.update_row_barang(existing_row, barang_detail)
        else:
            controller.view.tambah_barang_ke_tabel(barang_detail)
            controller._register_row_index(pid, controller.view.table_barang.rowCount() - 1)

        controller.view.update_info_diskon(barang_detail)
        controller.view.update_ringkasan()
        return True

    def simpan_transaksi(self, *, controller):

        if not controller._ensure_settlement_guard():

            return False

        controller.log_debug("masuk simpan transaksi")

        data_ui = controller._collect_transaksi_data_for_save()

        if not data_ui:

            return False



        transaksi_data, detail_data, transaksi_data_dict = data_ui

        metode_text = str(transaksi_data_dict.get("metode_bayar", "TUNAI"))

        return controller._persist_transaksi_with_payment(

            transaksi_data=transaksi_data,

            detail_data=detail_data,

            transaksi_data_dict=transaksi_data_dict,

            metode_text=metode_text,

            audit_message=f"Transaksi simpan oleh {transaksi_data_dict['oleh_nama']}",

            voucher_callback=None,

            success_callback=lambda transaksi_id: controller.view.notifikasi_sukses(transaksi_id),

            show_success_dialog=False,

        )

    def cetak_struk_terakhir(self, *, controller):

        """

        Cetak ulang struk transaksi terakhir dengan

        Menggunakan HTML engine yang sama

        Menampilkan informasi pembayaran lengkap (metode, jumlah dibayar, kembalian)

        """

        controller.log_info("Cetak struk berdasarkan transaksi terakhir")



        transaksi = controller.model.get_transaksi_terakhir()

        if not transaksi:

            controller.view.show_warning("Cetak Struk", "Transaksi terakhir tidak ditemukan.")

            return



        detail_rows = controller.model.get_detail_transaksi(transaksi["id"])

        detail_data = controller.model.map_detail_rows(detail_rows)



        transaksi_data_dict = controller.model.build_transaksi_dict_from_row(transaksi)



        controller.log_debug(f"REPRINT transaksi_id={transaksi['id']} nomer={transaksi['nomer']}")

        controller.log_debug(f"REPRINT settlement_id={transaksi_data_dict.get('settlement_id')}")

        controller.log_debug(f"REPRINT jumlah_dibayar={transaksi_data_dict.get('transaksi_dibayar')}")

        controller.log_debug(f"REPRINT kembalian={transaksi_data_dict.get('transaksi_dibayar_return')}")



        controller.print_controller.print_struk_by_mode(

            None,

            detail_data,

            transaksi_data_dict,

            index_printer=None,

            parent=controller.view

        )

        controller.log_info("Struk terakhir diproses sesuai mode cetak")

    # edited by glg
    def dispose_controller(self, *, controller, super_dispose_callback):
        controller._stop_free_produk_outbox_timer()
        controller._free_produk_drain_inflight = False
        try:
            background_task_service = getattr(controller, "background_task_service", None)
            if background_task_service is not None:
                background_task_service.shutdown(wait=False)
        except (RuntimeError, TypeError, AttributeError):
            pass

        try:
            scanner_controller = getattr(controller, "scanner_controller", None)
            if bool(getattr(controller, "_owns_scanner_controller", False)) and scanner_controller:
                scanner_controller.dispose()
        except (RuntimeError, TypeError, AttributeError):
            pass
        return super_dispose_callback()

    # edited by glg
    def refresh_master_data(
        self,
        *,
        controller,
        load_all_customers_callback=None,
        non_fatal_exceptions=tuple(),
    ):
        exception_types = tuple(non_fatal_exceptions or (RuntimeError, TypeError, ValueError, KeyError, AttributeError))
        try:
            current_customer_id = None
            if hasattr(controller.view, "customer_combo"):
                current_customer_id = controller.view.customer_combo.currentData()
            customer_loader = load_all_customers_callback if callable(load_all_customers_callback) else (lambda: [])
            controller.customer_list = customer_loader() or []
            controller.view.populate_customer_combo(controller.customer_list)
            if current_customer_id is not None:
                try:
                    idx = controller.view.customer_combo.findData(current_customer_id)
                    if idx >= 0:
                        controller.view.customer_combo.setCurrentIndex(idx)
                except exception_types as exc:
                    controller.log_warning(f"Gagal restore customer combo setelah refresh: {exc}")

            keyword = controller.view.barang_input.text() if hasattr(controller.view, "barang_input") else ""
            controller.refresh_barang_autocomplete(keyword)
            controller.data_barang_cache.clear()
        except exception_types as exc:
            controller.log_warning(f"refresh_master_data gagal: {exc}")

    # edited by glg
    def log_lookup_query_plan(self, *, controller, non_fatal_exceptions=tuple()):
        exception_types = tuple(non_fatal_exceptions or (RuntimeError, TypeError, ValueError, KeyError, AttributeError))
        try:
            plan_map = controller.model.get_lookup_query_plan()
            summary = controller.query_plan_service.summarize(plan_map)
            for key, payload in summary.items():
                details = " | ".join(payload.get("details") or ["-"])
                if payload.get("full_scan"):
                    controller.log_warning(f"QueryPlan {key}: {details}")
                else:
                    controller.log_info(f"QueryPlan {key}: {details}")
        except exception_types as exc:
            controller.log_warning(f"Gagal analisis query plan lookup barang: {exc}")

    # edited by glg
    def handle_key_press_event(self, *, controller, event, qt_module):
        key = event.key()
        modifiers = event.modifiers()
        action_map = {
            qt_module.Key_F1: controller.view.handle_second_monitor,
            qt_module.Key_F2: controller.buka_history_penjualan,
            qt_module.Key_F3: controller.buka_customer_dialog,
            qt_module.Key_F4: controller.view.handle_config_dialog,
            qt_module.Key_F6: controller.view.handle_diskon_dialog,
            qt_module.Key_F8: controller.view.handle_return_dialog,
            qt_module.Key_F9: controller.buka_penyimpanan_dialog,
            qt_module.Key_F10: controller.view.handle_print_struk,
            qt_module.Key_F11: controller.view.handle_fullscreen_toggle,
        }
        handler = action_map.get(key)
        if handler is not None:
            handler()
            return True
        if key == qt_module.Key_F7 and modifiers == qt_module.ControlModifier:
            controller.view.handle_history_dialog()
            return True
        if key == qt_module.Key_D and modifiers == qt_module.ControlModifier:
            controller.view.handle_debug_dialog()
            return True
        return False

    # edited by glg
    def settlement_result(self, *, controller, data, has_other_kasir=False):
        payload_data = data or []
        controller.log_debug(f"masuk settlement result, jumlah data = {len(payload_data)}")
        controller._settlement_state_initialized = True
        controller._settlement_check_inflight = False
        if controller.transaksi_payment_state_service.has_pending_settlement(payload_data):
            controller._set_settlement_blocked(True)
            message = controller.transaksi_payment_state_service.build_settlement_warning_message(has_other_kasir)
            controller.view.show_warning("Perhatian", message)
            controller.view.buka_modal_settlement()
            return
        controller._set_settlement_blocked(False)

    # edited by glg
    def persist_transaksi_with_payment(
        self,
        *,
        controller,
        transaksi_data,
        detail_data,
        transaksi_data_dict,
        metode_text,
        audit_message,
        voucher_callback=None,
        success_callback=None,
        show_success_dialog=True,
    ):
        persist_service = controller._resolve_persist_service()
        persist_ui_use_case_service = controller._resolve_persist_ui_use_case_service()
        payment_persist_use_case_service = controller._resolve_payment_persist_use_case_service()
        return payment_persist_use_case_service.run_persist(
            persist_service=persist_service,
            persist_ui_use_case_service=persist_ui_use_case_service,
            transaksi_data=transaksi_data,
            detail_data=detail_data,
            transaksi_data_dict=transaksi_data_dict,
            metode_text=metode_text,
            audit_message=audit_message,
            voucher_callback=voucher_callback,
            success_callback=success_callback,
            show_success_dialog=show_success_dialog,
            update_quota_callback=controller._update_quota_free_produk_async,
            schedule_print_callback=controller._schedule_cetak_struk,
            log_error_callback=controller.log_error,
            log_info_callback=controller.log_info,
            show_error_callback=lambda title, message: controller.show_error(title, message, view=controller.view),
            show_info_callback=controller.show_info,
            reset_form_callback=getattr(controller.view, "reset_form", None),
            clear_preorder_callback=controller._clear_preorder_payload,
        )

    # edited by glg
    def cetak_struk_transaksi(self, *, controller, transaksi_data, detail_data, transaksi_data_dict):
        try:
            controller.print_controller.print_struk_by_mode(
                transaksi_data,
                detail_data,
                transaksi_data_dict,
                index_printer=None,
                parent=controller.view,
            )
        except (RuntimeError, TypeError, AttributeError) as exc:
            controller.log_warning(f"Error cetak struk: {exc}")

    # edited by glg
    def update_quota_free_produk_async(self, *, controller, arr_free_produk, parse_int_callback):
        if not arr_free_produk:
            return
        try:
            enqueue_result = controller.free_produk_outbox_service.enqueue_payload(
                arr_free_produk,
                reason="transaksi_save_async",
            ) or {}
            queued = bool(enqueue_result.get("queued", False))
            queued_id = parse_int_callback(enqueue_result.get("id"), 0)
            if queued and queued_id > 0:
                controller.log_info(f"Outbox free produk tersimpan id={queued_id}")
            elif queued_id > 0:
                reason = str(enqueue_result.get("reason") or "duplicate_active_payload")
                controller.log_info(
                    f"Outbox free produk dedup aktif id={queued_id} reason={reason}"
                )
        except (sqlite3.Error, RuntimeError, TypeError, ValueError) as exc:
            controller.log_warning(f"Gagal enqueue outbox free produk: {exc}")
            return
        controller._drain_free_produk_outbox_async(max_items=3)

    # edited by glg
    def record_admin_qty_approval_trail(self, *, controller, id_barang, qty, approved, approval_name):
        service = getattr(controller, "enterprise_control_service", None)
        if service is None:
            return
        actor_name = str((controller.user_info or {}).get("nama") or "").strip()
        status = "approved" if bool(approved) else "rejected"
        try:
            service.record_approval_trail(
                action_name="qty_besar_transaksi",
                actor_name=actor_name,
                approval_name=str(approval_name or ""),
                approval_status=status,
                trace_id=f"qty-auth-{datetime.now().strftime('%H%M%S%f')[-8:]}",
                payload={
                    "produk_id": str(id_barang or ""),
                    "qty": int(qty or 0),
                    "qty_limit": int(getattr(controller, "qty_verify_threshold", 99) or 99),
                },
            )
        except (sqlite3.Error, RuntimeError, TypeError, ValueError) as exc:
            controller.log_warning(f"Gagal catat approval trail qty besar: {exc}")
