# edited by glg

import time
import threading

from PySide6.QtCore import QTimer, Qt

from PySide6.QtWidgets import QDialog, QLineEdit, QComboBox, QPushButton, QCheckBox, QSpinBox



from pypos.core.base_controller import BaseController

from pypos.core.utils.myhelper import parse_rupiah
from pypos.modules.penjualan.errors import TransaksiSaveError

from pypos.modules.penjualan.views.pembatalan_view import PembatalanView

from pypos.modules.penjualan.services.controller_factory_service import PenjualanControllerFactoryService

from pypos.modules.penjualan.models.transaksi_model import TransaksiModel

from pypos.modules.penjualan.models.diskon_customer_model import DiskonCustomerModel

from pypos.modules.penjualan.models.voucher_model import VoucherModel

from pypos.modules.penjualan.views.transaksi_penjualan_view import TransaksiPenjualanView

from pypos.modules.penjualan.models.settlement_model import SettlementModel

from pypos.modules.penjualan.services.pembayaran_info_service import PembayaranInfoService

from pypos.modules.penjualan.services.transaksi_service import TransaksiService

from pypos.modules.penjualan.services.penjualan_config_service import PenjualanConfigService

from pypos.modules.penjualan.services.transaksi_payload_service import TransaksiPayloadService

from pypos.modules.penjualan.services.transaksi_row_index_service import TransaksiRowIndexService

from pypos.modules.penjualan.services.transaksi_save_service import TransaksiSaveService

from pypos.modules.penjualan.services.transaksi_preorder_service import TransaksiPreorderService
from pypos.modules.penjualan.services.transaksi_penjualan_hotspot_use_case_service import (
    TransaksiPenjualanHotspotUseCaseService,
)

from pypos.modules.penjualan.services.transaksi_persist_flow_service import (
    TransaksiPersistFlowService,
)
from pypos.modules.penjualan.services.transaksi_persist_ui_use_case_service import (
    TransaksiPersistUiUseCaseService,
)
from pypos.modules.penjualan.services.transaksi_payment_persist_use_case_service import (
    TransaksiPaymentPersistUseCaseService,
)

from pypos.modules.penjualan.services.transaksi_payment_info_service import TransaksiPaymentInfoService

from pypos.modules.penjualan.services.transaksi_payment_flow_service import TransaksiPaymentFlowService

from pypos.modules.penjualan.services.transaksi_payment_state_service import TransaksiPaymentStateService

from pypos.modules.penjualan.services.transaksi_harga_validation_service import TransaksiHargaValidationService

from pypos.modules.penjualan.services.transaksi_barang_input_service import TransaksiBarangInputService

from pypos.modules.penjualan.services.transaksi_table_input_service import TransaksiTableInputService

from pypos.modules.penjualan.services.transaksi_autocomplete_service import TransaksiAutocompleteService
from pypos.modules.penjualan.services.transaksi_autocomplete_async_use_case_service import (
    TransaksiAutocompleteAsyncUseCaseService,
)
from pypos.modules.penjualan.services.transaksi_barang_mutation_use_case_service import (
    TransaksiBarangMutationUseCaseService,
)

from pypos.modules.penjualan.services.transaksi_background_task_service import TransaksiBackgroundTaskService
from pypos.modules.penjualan.services.free_produk_sync_outbox_service import (
    FreeProdukSyncOutboxService,
)
from pypos.modules.penjualan.services.free_produk_outbox_drain_use_case_service import (
    FreeProdukOutboxDrainUseCaseService,
)
from pypos.modules.penjualan.services.free_produk_outbox_alert_service import (
    FreeProdukOutboxAlertService,
)

from pypos.modules.penjualan.services.transaksi_query_plan_service import TransaksiQueryPlanService

from pypos.modules.penjualan.services.transaksi_performance_service import TransaksiPerformanceService

from pypos.modules.penjualan.services.transaksi_enterprise_control_service import (
    TransaksiEnterpriseControlService,
)

from pypos.modules.penjualan.services.admin_authorization_service import AdminAuthorizationService

from pypos.modules.penjualan.services.pembayaran_admin_dialog_service import PembayaranAdminDialogService

from pypos.modules.penjualan.services.high_qty_authorization_service import HighQtyAuthorizationService

from pypos.modules.printer.controllers.print_controller import PrintController

from pypos.modules.printer.controllers.printer_settings_controller import PrinterSettingsController

from pypos.modules.scanner.controllers.scanner_controller import ScannerController

from pypos.core.utils.db_helper import parse_int_from_text, parse_float_safely
from pypos.core.utils.worker_pool_utils import submit_ui_query_task_keyed

PENJUALAN_NON_FATAL_EXCEPTIONS = (
    TypeError,
    ValueError,
    KeyError,
    AttributeError,
    RuntimeError,
    OSError,
    LookupError,
    ArithmeticError,
    ImportError,
)



class TransaksiPenjualanController(BaseController):
    # edited by glg
    # Default di level kelas agar aman jika ada akses sebelum __init__ selesai.
    _settlement_blocked = False
    _settlement_state_initialized = False
    _settlement_check_inflight = False
    _settlement_pending_notice_ts = 0.0
    # edited by glg
    # Guard retry check settlement agar tidak menjadwalkan timer berulang.
    _settlement_retry_scheduled = False
    # edited by glg
    # Guard outbox drain agar sinkronisasi free produk tidak overlap.
    _free_produk_drain_inflight = False
    _free_produk_outbox_notice_ts = 0.0
    _free_produk_dead_alert_notice_ts = 0.0
    # edited by glg
    # Throttle log saat drain outbox ditunda karena sinkronisasi global.
    _free_produk_sync_gate_notice_ts = 0.0



    def pembatalan_transaksi(self):

        if not self._ensure_settlement_guard():

            return

        parent_window = getattr(self.view, "parent_window", None)

        if parent_window and hasattr(parent_window, "buka_page_pembatalan"):

            try:
                parent_window.buka_page_pembatalan()
                return
            except PENJUALAN_NON_FATAL_EXCEPTIONS as exc:
                # edited by glg
                # Jangan biarkan error pembukaan halaman naik jadi unhandled exception.
                self.log_warning(f"Gagal buka pembatalan via parent window: {exc}")
            except Exception as exc:
                # edited by glg
                # Guard tambahan untuk memastikan shortcut F6 tetap aman.
                self.log_error(f"Error tidak terduga saat buka pembatalan via parent window: {exc}")

        self._open_pembatalan_standalone()

    # edited by glg
    def _open_pembatalan_standalone(self):
        try:
            pembatalan_controller = self.controller_factory.create_pembatalan_transaksi_controller(
                user_info=self.user_info,
                db_path=self.db_path,
            )
            return_controller = self.controller_factory.create_return_controller()
            return_history_controller = self.controller_factory.create_return_history_controller(
                return_model=getattr(return_controller, "model", None),
                pembatalan_model=getattr(pembatalan_controller, "model", None),
            )
            controllers = (
                pembatalan_controller,
                return_controller,
                return_history_controller,
            )
            if any(ctrl is None for ctrl in controllers):
                raise ValueError("PembatalanView membutuhkan controller instance lengkap")
            page = PembatalanView(
                parent=self.view,
                pembatalan_controller=pembatalan_controller,
                return_controller=return_controller,
                return_history_controller=return_history_controller,
            )
            page.show()
            return True
        except PENJUALAN_NON_FATAL_EXCEPTIONS as exc:
            self.log_error(f"Gagal membuka halaman pembatalan (standalone): {exc}")
        except Exception as exc:
            self.log_error(f"Error tidak terduga saat membuka pembatalan (standalone): {exc}")
        self.show_error(
            "Gagal Membuka Pembatalan",
            "Halaman pembatalan gagal dimuat. Silakan coba lagi.",
            view=self.view,
        )
        return False



    # edited by glg
    def _reset_startup_guard_state(self):

        self._settlement_blocked = False

        self._settlement_state_initialized = False
        self._settlement_check_inflight = False
        self._settlement_pending_notice_ts = 0.0
        self._settlement_retry_scheduled = False
        self._free_produk_drain_inflight = False
        self._free_produk_outbox_notice_ts = 0.0
        self._free_produk_dead_alert_notice_ts = 0.0
        # edited by glg
        self._free_produk_sync_gate_notice_ts = 0.0

    # edited by glg
    def _init_primary_components(self, db_path, user_info, parent_window):

        self.model = TransaksiModel(db_path)

        self.db_path = db_path

        self.parent_window = parent_window

        self.controller_factory = PenjualanControllerFactoryService()

        self.diskon_customer_model = DiskonCustomerModel(db_path)

        self.voucher_model = VoucherModel(db_path)

        self.view = TransaksiPenjualanView(self, user_info, db_path, parent_window=parent_window)

    # edited by glg
    def _init_async_payload_guards(self):

        self._autocomplete_request_seq = 0
        self._autocomplete_request_lock = threading.Lock()
        self._settlement_request_seq = 0
        self._settlement_request_lock = threading.Lock()
        if hasattr(self.view, "set_autocomplete_payload_handler"):
            self.view.set_autocomplete_payload_handler(self._on_autocomplete_payload_ready)
        if hasattr(self.view, "set_settlement_payload_handler"):
            self.view.set_settlement_payload_handler(self._on_settlement_payload_ready)

    # edited by glg
    def _init_security_components(self, db_path):

        self.model_settle = SettlementModel(db_path)

        self.admin_authorization_service = AdminAuthorizationService(self.model_settle)

        self.admin_dialog_service = PembayaranAdminDialogService(

            validator=self.admin_authorization_service.validate_settlement_admin

        )

    # edited by glg
    def _init_penjualan_core_services(self, db_path, scanner_controller):
        self.pembayaran_info_service = PembayaranInfoService()
        self.transaksi_service = TransaksiService()
        self.penjualan_config_service = PenjualanConfigService(db_path=db_path)
        self.qty_verify_threshold = self.penjualan_config_service.get_admin_qty_verify_threshold(default=99)
        self.high_qty_authorization_service = HighQtyAuthorizationService(max_qty=self.qty_verify_threshold)
        self.performance_service = TransaksiPerformanceService(
            enabled=self.penjualan_config_service.is_performance_profile_enabled(),
            log_info=self.log_info,
            log_warning=self.log_warning,
        )
        self._owns_scanner_controller = scanner_controller is None
        self.scanner_controller = scanner_controller or ScannerController()
        self.scanner_controller.reload_settings()
        self.background_task_service = TransaksiBackgroundTaskService()

    # edited by glg
    def _init_penjualan_input_services(self):
        self.transaksi_payload_service = TransaksiPayloadService()
        self.transaksi_payment_info_service = TransaksiPaymentInfoService()
        self.transaksi_payment_flow_service = TransaksiPaymentFlowService()
        self.transaksi_payment_state_service = TransaksiPaymentStateService()
        self.transaksi_harga_validation_service = TransaksiHargaValidationService()
        self.transaksi_barang_input_service = TransaksiBarangInputService()
        self.transaksi_barang_mutation_use_case_service = TransaksiBarangMutationUseCaseService()
        self.transaksi_table_input_service = TransaksiTableInputService()
        self.autocomplete_service = TransaksiAutocompleteService(
            limit=self.penjualan_config_service.get_autocomplete_limit(),
            min_keyword=self.penjualan_config_service.get_autocomplete_min_keyword(),
        )
        self.transaksi_autocomplete_async_use_case_service = TransaksiAutocompleteAsyncUseCaseService()

    # edited by glg
    def _init_penjualan_outbox_services(self, db_path):
        self.free_produk_outbox_service = FreeProdukSyncOutboxService(db_path=db_path)
        self.free_produk_outbox_drain_use_case_service = FreeProdukOutboxDrainUseCaseService(
            free_produk_outbox_service=self.free_produk_outbox_service,
            sync_quota_callback=self.transaksi_service.update_quota_free_produk,
            parse_int_callback=parse_int_from_text,
        )
        self.free_produk_outbox_alert_service = FreeProdukOutboxAlertService()
        self._free_produk_drain_lock = threading.Lock()
        self._free_produk_outbox_timer = None
        self.query_plan_service = TransaksiQueryPlanService()

    # edited by glg
    def _init_penjualan_persist_services(self, db_path):
        self.transaksi_save_service = TransaksiSaveService(
            db_path=db_path,
            transaksi_model=self.model,
            voucher_model=self.voucher_model,
        )
        self.enterprise_control_service = TransaksiEnterpriseControlService(
            db_path=db_path
        )
        self.transaksi_persist_flow_service = TransaksiPersistFlowService(
            self.transaksi_save_service,
            enterprise_control_service=self.enterprise_control_service,
        )
        self.transaksi_persist_ui_use_case_service = TransaksiPersistUiUseCaseService()
        self.transaksi_payment_persist_use_case_service = TransaksiPaymentPersistUseCaseService()
        self.transaksi_preorder_service = TransaksiPreorderService()
        self.transaksi_penjualan_hotspot_use_case_service = TransaksiPenjualanHotspotUseCaseService()

    # edited by glg
    def _init_penjualan_print_services(self):
        self.printer_settings_controller = PrinterSettingsController()
        self.print_controller = PrintController(self.printer_settings_controller)

    # edited by glg
    def _init_penjualan_services(self, db_path, scanner_controller):
        self._init_penjualan_core_services(db_path, scanner_controller)
        self._init_penjualan_input_services()
        self._init_penjualan_outbox_services(db_path)
        self._init_penjualan_persist_services(db_path)
        self._init_penjualan_print_services()

    # edited by glg
    def _get_transaksi_penjualan_hotspot_use_case_service(self):
        service = getattr(self, "transaksi_penjualan_hotspot_use_case_service", None)
        if service is None:
            service = TransaksiPenjualanHotspotUseCaseService()
            self.transaksi_penjualan_hotspot_use_case_service = service
        return service

    # edited by glg
    def _init_runtime_state(self, customer_list, user_info):

        self.diskon_controller = self.controller_factory.create_diskon_controller()

        self.customer_list = customer_list

        self.user_info = user_info

        self.barang_mapping = {}

        self.data_barang_cache = {}

        self.row_index_service = TransaksiRowIndexService(parse_qty_callback=parse_int_from_text)

        self._preorder_payload = None

    # edited by glg
    def _setup_startup_view_state(self, customer_list):

        self.view.populate_customer_combo(customer_list)

        self.view.configure_barang_autocomplete(

            debounce_ms=self.penjualan_config_service.get_autocomplete_debounce_ms()

        )

        self.view.bind_barang_autocomplete_handler(self.handle_barang_keyword_changed)

        self.refresh_barang_autocomplete("")

        QTimer.singleShot(0, self._log_lookup_query_plan)
        self._start_free_produk_outbox_timer(interval_ms=12000)
        QTimer.singleShot(2500, lambda: self._drain_free_produk_outbox_async(max_items=3))

        QTimer.singleShot(100, self.view.cart_sku_input.setFocus)

    # edited by glg
    def _record_startup_metrics(self, init_started_at):

        self.performance_service.record(

            "startup_transaksi_controller",

            init_started_at,

            threshold_ms=180,

            context="init lengkap",

            once_key="startup_transaksi_controller",

        )

    def __init__(self, customer_list, user_info, db_path, parent_window=None, scanner_controller=None):

        super().__init__()

        init_started_at = time.perf_counter()

        self._reset_startup_guard_state()
        self._init_primary_components(db_path, user_info, parent_window)
        self._init_async_payload_guards()
        self._init_security_components(db_path)
        self._init_penjualan_services(db_path, scanner_controller)
        self._init_runtime_state(customer_list, user_info)
        self._setup_startup_view_state(customer_list)
        self._record_startup_metrics(init_started_at)



    def reload_scanner_detector_settings(self, payload=None):
        if hasattr(self, "scanner_controller") and self.scanner_controller:
            self.scanner_controller.reload_settings(payload)



    def get_scanner_runtime_status(self):
        if hasattr(self, "scanner_controller") and self.scanner_controller:
            return self.scanner_controller.get_runtime_status()
        return {}



    def process_scanner_key_event(self, event):
        scanner = getattr(self, "scanner_controller", None)
        if scanner is None:
            return False
        barcode = scanner.consume_qt_key_event(event)
        if not barcode:
            return False
        return self._handle_scanned_barcode(barcode, source="scanner")



    def consume_pending_scanner_barcode(self):
        scanner = getattr(self, "scanner_controller", None)
        if scanner is None:
            return False
        barcode = scanner.poll_pending_barcode()
        if not barcode:
            return False
        return self._handle_scanned_barcode(str(barcode), source="scanner")



    def _handle_scanned_barcode(self, barcode, source="unknown"):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._handle_scanned_barcode(
            controller=self,
            barcode=barcode,
            source=source,
        )




    def is_settlement_blocked(self):

        return bool(getattr(self, "_settlement_blocked", False))


    # edited by glg
    def _start_free_produk_outbox_timer(self, interval_ms=12000):
        timer = getattr(self, "_free_produk_outbox_timer", None)
        if timer is None:
            timer = QTimer(self.view if hasattr(self, "view") else None)
            timer.setSingleShot(False)
            timer.timeout.connect(lambda: self._drain_free_produk_outbox_async(max_items=3))
            self._free_produk_outbox_timer = timer
        interval = max(4000, int(interval_ms or 0))
        timer.setInterval(interval)
        if not timer.isActive():
            timer.start()

    # edited by glg
    def _stop_free_produk_outbox_timer(self):
        timer = getattr(self, "_free_produk_outbox_timer", None)
        if timer is None:
            return
        try:
            timer.stop()
        except (RuntimeError, TypeError, AttributeError):
            pass
        self._free_produk_outbox_timer = None



    def dispose(self):
        parent_dispose = super().dispose
        return self._get_transaksi_penjualan_hotspot_use_case_service().dispose_controller(
            controller=self,
            super_dispose_callback=parent_dispose,
        )



    def _set_settlement_blocked(self, blocked):

        self._settlement_blocked = bool(blocked)

        if self._settlement_blocked:

            self.nonaktifkan_form()

        else:

            self.aktifkan_form()



    def _ensure_settlement_guard(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._ensure_settlement_guard(
            controller=self,
        )


    # edited by glg
    def _notify_settlement_check_pending(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._notify_settlement_check_pending(
            controller=self,
        )




    # edited by glg
    def _load_all_customers(self):
        from pypos.modules.customer.controllers.customer_controller import CustomerController

        return CustomerController().load_all_customers()



    def refresh_master_data(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().refresh_master_data(
            controller=self,
            load_all_customers_callback=self._load_all_customers,
            non_fatal_exceptions=PENJUALAN_NON_FATAL_EXCEPTIONS,
        )



    def handle_barang_keyword_changed(self, keyword):

        self.refresh_barang_autocomplete(keyword, async_mode=True)



    def refresh_barang_autocomplete(self, keyword, async_mode=False):
        self._get_transaksi_penjualan_hotspot_use_case_service().refresh_barang_autocomplete(
            controller=self,
            keyword=keyword,
            async_mode=async_mode,
        )



    # edited by glg
    def _next_autocomplete_request_id(self):

        with self._autocomplete_request_lock:

            self._autocomplete_request_seq += 1

            return int(self._autocomplete_request_seq)



    # edited by glg
    def _is_latest_autocomplete_request(self, request_id):

        with self._autocomplete_request_lock:

            return int(request_id or 0) == int(self._autocomplete_request_seq or 0)

    # edited by glg
    def _get_transaksi_autocomplete_async_use_case_service(self) -> TransaksiAutocompleteAsyncUseCaseService:
        service = getattr(self, "transaksi_autocomplete_async_use_case_service", None)
        if service is None:
            service = TransaksiAutocompleteAsyncUseCaseService()
            self.transaksi_autocomplete_async_use_case_service = service
        return service



    # edited by glg
    def _run_async_autocomplete_fetch(self, request_id, normalized_keyword):

        lookup_started = self.performance_service.mark()

        emit_callback = getattr(self.view, "emit_autocomplete_payload", None)
        self._get_transaksi_autocomplete_async_use_case_service().submit_async_fetch(
            owner_id=id(self),
            request_id=request_id,
            keyword=normalized_keyword,
            limit=self.autocomplete_service.get_limit(),
            lookup_started=lookup_started,
            fetch_callback=lambda keyword, limit: self.model.get_produk_autocomplete(
                keyword=keyword,
                limit=limit,
            ),
            emit_callback=emit_callback if callable(emit_callback) else None,
            on_ready_callback=self._on_autocomplete_payload_ready,
            submit_task_callback=submit_ui_query_task_keyed,
        )



    # edited by glg
    def _on_autocomplete_payload_ready(self, request_id, payload):
        self._get_transaksi_autocomplete_async_use_case_service().apply_ready_payload(
            request_id=request_id,
            payload=payload,
            is_latest_request_callback=self._is_latest_autocomplete_request,
            set_mapping_callback=lambda mapping: setattr(self, "barang_mapping", mapping),
            set_autocomplete_callback=self.view.set_barang_autocomplete,
            record_perf_callback=self.performance_service.record,
            log_warning_callback=self.log_warning,
        )



    def _log_lookup_query_plan(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().log_lookup_query_plan(
            controller=self,
            non_fatal_exceptions=PENJUALAN_NON_FATAL_EXCEPTIONS,
        )



    def get_customer_nama_by_id(self, customer_id):

        """Ambil nama asli customer dari database berdasarkan ID"""

        for customer in self.customer_list:

            if customer["id"] == customer_id:

                return customer["nama"]

        return "tunai"



    def reset_form_transaksi(self, view):
        self._get_transaksi_penjualan_hotspot_use_case_service().reset_form_transaksi(
            controller=self,
            view=view,
        )



    def update_tombol_simpan(self, view):

        """Atur state tombol simpan berdasarkan isi keranjang dan status proses pembayaran."""

        ada_barang = view.table_barang.rowCount() > 0

        sedang_proses = getattr(view, "_sedang_proses_pembayaran", False)

        view.button_simpan.setEnabled(ada_barang and not sedang_proses)



    def _clear_preorder_payload(self):

        self._preorder_payload = None



    def _register_row_index(self, produk_id, row_index):

        self.row_index_service.register(produk_id, row_index)



    def _rebuild_row_index_map(self):

        self.row_index_service.rebuild(getattr(self.view, "table_barang", None))



    def _shift_row_index_after_remove(self, removed_row, removed_produk_id=None):

        self.row_index_service.shift_after_remove(removed_row, removed_produk_id)



    def _preorder_exists(self, nomer):

        return self.model.preorder_exists(nomer)



    def _restore_preorder_payload(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._restore_preorder_payload(
            controller=self,
        )




    def handle_clear_cart(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().handle_clear_cart(
            controller=self,
        )




    def update_ringkasan_from_view(self, view):
        self._get_transaksi_penjualan_hotspot_use_case_service().update_ringkasan_from_view(
            controller=self,
            view=view,
            parse_rupiah_callback=parse_rupiah,
            parse_int_callback=parse_int_from_text,
        )



    def kumpulkan_data_transaksi_view(self, view):
        return self._get_transaksi_penjualan_hotspot_use_case_service().kumpulkan_data_transaksi_view(
            controller=self,
            view=view,
        )



    def keyPressEvent(self, event):
        handled = self._get_transaksi_penjualan_hotspot_use_case_service().handle_key_press_event(
            controller=self,
            event=event,
            qt_module=Qt,
        )
        if not handled:
            super().keyPressEvent(event)



    def buka_history_penjualan(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().buka_history_penjualan(
            controller=self,
        )




    def cek_status_settlement(self, include_today=False, enforce_kasir=False, async_mode=False):
        return self._get_transaksi_penjualan_hotspot_use_case_service().cek_status_settlement(
            controller=self,
            include_today=include_today,
            enforce_kasir=enforce_kasir,
            async_mode=async_mode,
        )




    # edited by glg
    def _next_settlement_request_id(self):

        with self._settlement_request_lock:

            self._settlement_request_seq += 1

            return int(self._settlement_request_seq)



    # edited by glg
    def _is_latest_settlement_request(self, request_id):

        with self._settlement_request_lock:

            return int(request_id or 0) == int(self._settlement_request_seq or 0)



    # edited by glg
    def _run_async_settlement_status_check(self, request_id, include_today=False, enforce_kasir=False):
        self._get_transaksi_penjualan_hotspot_use_case_service().run_async_settlement_status_check(
            controller=self,
            request_id=request_id,
            include_today=include_today,
            enforce_kasir=enforce_kasir,
            non_fatal_exceptions=PENJUALAN_NON_FATAL_EXCEPTIONS,
            submit_task_fn=submit_ui_query_task_keyed,
        )



    # edited by glg
    def _on_settlement_payload_ready(self, request_id, payload):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._on_settlement_payload_ready(
            controller=self,
            request_id=request_id,
            payload=payload,
        )


    # edited by glg
    def _schedule_settlement_recheck(self, delay_ms=1800):
        if bool(getattr(self, "_settlement_retry_scheduled", False)):
            return
        self._settlement_retry_scheduled = True

        def _run():
            self._settlement_retry_scheduled = False
            if self.is_disposed:
                return
            if bool(getattr(self, "_settlement_check_inflight", False)):
                return
            self.cek_status_settlement(async_mode=True)

        QTimer.singleShot(max(300, int(delay_ms or 0)), _run)



    def settlement_result(self, data, has_other_kasir=False, current_kasir=None):
        return self._get_transaksi_penjualan_hotspot_use_case_service().settlement_result(
            controller=self,
            data=data,
            has_other_kasir=has_other_kasir,
        )



    def nonaktifkan_form(self):

        self.log_debug("nonaktifkan_form() dipanggil - disabling input widgets")

        widget_types = [QLineEdit, QComboBox, QPushButton, QCheckBox, QSpinBox]

        for widget_type in widget_types:

            for widget in self.view.findChildren(widget_type):

                if widget.property("always_enabled") == True:

                    continue

                widget.setEnabled(False)



    def aktifkan_form(self):

        self.log_debug("aktifkan_form() dipanggil - enabling input widgets")

        widget_types = [QLineEdit, QComboBox, QPushButton, QCheckBox, QSpinBox]

        for widget_type in widget_types:

            for widget in self.view.findChildren(widget_type):

                widget.setEnabled(True)



    def buka_customer_dialog(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().buka_customer_dialog(
            controller=self,
        )




    def buka_dialog_pembayaran(self):
        self._get_transaksi_penjualan_hotspot_use_case_service().buka_dialog_pembayaran(
            controller=self,
            dialog_accepted=QDialog.Accepted,
        )



    def _build_info_transaksi_from_table(self):
        return self._get_transaksi_payment_info_service().build_from_table(
            table_widget=getattr(self.view, "table_barang", None),
            parse_qty_callback=parse_int_from_text,
            parse_rupiah_callback=parse_rupiah,
        )

    # edited by glg
    def show_payment_dialog_from_view(self, *, parent, info_transaksi, multi_payment_mode=False):
        pembayaran_ctrl = self.controller_factory.create_pembayaran_controller(
            parent=parent,
            info_transaksi=info_transaksi,
            multi_payment_mode=multi_payment_mode,
        )
        result_code = pembayaran_ctrl.show()
        return result_code, getattr(pembayaran_ctrl, "result", None)

    # edited by glg
    def open_settlement_dialog_from_view(self, parent_window=None):
        settlement_controller = self.controller_factory.create_settlement_controller(
            db_path=self.db_path,
            user_info=self.user_info,
            parent_window=parent_window or self.view,
        )
        dialog = settlement_controller.view
        dialog.exec()
        self.cek_status_settlement(async_mode=True)
        if hasattr(self, "is_settlement_blocked") and not self.is_settlement_blocked():
            if hasattr(self.view, "force_enable_main_inputs"):
                self.view.force_enable_main_inputs()

    def simpan_transaksi_multi_payment(self, payment_list):
        return self._get_transaksi_penjualan_hotspot_use_case_service().simpan_transaksi_multi_payment(
            controller=self,
            payment_list=payment_list,
        )




    def simpan_transaksi_dengan_pembayaran(self, hasil_pembayaran):
        return self._get_transaksi_penjualan_hotspot_use_case_service().simpan_transaksi_dengan_pembayaran(
            controller=self,
            hasil_pembayaran=hasil_pembayaran,
        )




    def _collect_transaksi_data_for_save(self):

        data_ui = self.view.kumpulkan_data_transaksi()

        if data_ui:

            return data_ui

        self.show_warning("Validasi", "Tidak ada data transaksi untuk disimpan.", view=self.view)

        return None



    def _resolve_persist_service(self):
        service = getattr(self, "transaksi_persist_flow_service", None)
        if service is not None:
            return service
        return TransaksiPersistFlowService(
            getattr(self, "transaksi_save_service", None),
            enterprise_control_service=getattr(self, "enterprise_control_service", None),
        )

    # edited by glg
    def _resolve_persist_ui_use_case_service(self):
        service = getattr(self, "transaksi_persist_ui_use_case_service", None)
        if service is not None:
            return service
        return TransaksiPersistUiUseCaseService()

    # edited by glg
    def _resolve_payment_persist_use_case_service(self):
        service = getattr(self, "transaksi_payment_persist_use_case_service", None)
        if service is not None:
            return service
        return TransaksiPaymentPersistUseCaseService()

    # edited by glg
    # Compat shim untuk guard KPI v100: contract error envelope tetap terdokumentasi di controller.
    def _handle_persist_error(self, result):
        error_payload = self._resolve_persist_ui_use_case_service().normalize_error_payload(result)
        error_code = error_payload["error_code"]
        error_message = error_payload["error_message"]
        reason = error_payload["reason"]
        trace_id = error_payload["trace_id"]
        raw_error = error_payload["raw_error"]
        if trace_id:
            self.log_error(
                f"Gagal simpan transaksi pembayaran [{error_code}] trace_id={trace_id}: "
                f"{raw_error or error_message} reason={reason}"
            )
        else:
            self.log_error(
                f"Gagal simpan transaksi pembayaran [{error_code}]: "
                f"{raw_error or error_message} reason={reason}"
            )
        self.show_error("Simpan Transaksi", f"{error_message}\nKode: {error_code}", view=self.view)
        return False

    def _persist_transaksi_with_payment(
        self,
        transaksi_data,
        detail_data,
        transaksi_data_dict,
        metode_text,
        audit_message,
        voucher_callback=None,
        success_callback=None,
        show_success_dialog=True,
    ):
        return self._get_transaksi_penjualan_hotspot_use_case_service().persist_transaksi_with_payment(
            controller=self,
            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,
        )



    def _schedule_cetak_struk(self, transaksi_data, detail_data, transaksi_data_dict):

        QTimer.singleShot(

            0,

            lambda: self.cetak_struk_transaksi(

                transaksi_data=transaksi_data,

                detail_data=detail_data,

                transaksi_data_dict=transaksi_data_dict,

            ),

        )



    def cetak_struk_transaksi(self, transaksi_data, detail_data, transaksi_data_dict):
        return self._get_transaksi_penjualan_hotspot_use_case_service().cetak_struk_transaksi(
            controller=self,
            transaksi_data=transaksi_data,
            detail_data=detail_data,
            transaksi_data_dict=transaksi_data_dict,
        )



    # edited by glg
    # Wajib raise saat gagal agar transaksi + voucher usage rollback atomik.
    def _apply_voucher_usage_single(self, transaksi_id, hasil_pembayaran, conn=None):

        ok, msg = self.transaksi_save_service.apply_voucher_usage_single(
            transaksi_id,
            hasil_pembayaran,
            conn=conn,
        )

        if not ok:

            raise ValueError(f"Pemakaian voucher gagal: {msg}")



    # edited by glg
    # Wajib raise saat gagal agar transaksi + voucher usage rollback atomik.
    def _apply_voucher_usage_multi(self, transaksi_id, payment_list, conn=None):

        ok, msg = self.transaksi_save_service.apply_voucher_usage_multi(
            transaksi_id,
            payment_list,
            conn=conn,
        )

        if not ok:

            raise ValueError(f"Pemakaian voucher gagal: {msg}")



    def buka_penyimpanan_dialog(self):
        self._get_transaksi_penjualan_hotspot_use_case_service().buka_penyimpanan_dialog(
            controller=self,
            save_error_type=TransaksiSaveError,
        )



    def load_transaksi_tersimpan(self):

        if not self._ensure_settlement_guard():

            return

        dialog = self.controller_factory.create_load_transaksi_controller(

            self.view,

            self,

            self.model.db_path,

        )

        dialog.show()



    def load_transaksi_ke_view(self, detail_rows, cust_id, cust_nama, diskon, ppn, total_harga, preorder_payload=None):
        self._get_transaksi_penjualan_hotspot_use_case_service().load_transaksi_ke_view(
            controller=self,
            detail_rows=detail_rows,
            cust_id=cust_id,
            cust_nama=cust_nama,
            diskon=diskon,
            ppn=ppn,
            total_harga=total_harga,
            preorder_payload=preorder_payload,
        )



    def proses_pilih_barang(self, display_text):
        return self._get_transaksi_penjualan_hotspot_use_case_service().proses_pilih_barang(
            controller=self,
            display_text=display_text,
        )




    def cek_kuota_free_produk(self, barang):

        return self.transaksi_service.cek_kuota_free_produk(barang, self.user_info)



    def _update_quota_free_produk(self, arr_free_produk):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._update_quota_free_produk(
            controller=self,
            arr_free_produk=arr_free_produk,
        )


    # edited by glg
    def _get_free_produk_outbox_drain_use_case_service(self):
        service = getattr(self, "free_produk_outbox_drain_use_case_service", None)
        outbox_service = getattr(self, "free_produk_outbox_service", None)
        transaksi_service = getattr(self, "transaksi_service", None)
        sync_quota_callback = getattr(transaksi_service, "update_quota_free_produk", None)
        if (
            service is None
            or getattr(service, "free_produk_outbox_service", None) is not outbox_service
            or getattr(service, "sync_quota_callback", None) is not sync_quota_callback
        ):
            service = FreeProdukOutboxDrainUseCaseService(
                free_produk_outbox_service=outbox_service,
                sync_quota_callback=sync_quota_callback,
                parse_int_callback=parse_int_from_text,
            )
            self.free_produk_outbox_drain_use_case_service = service
        return service

    # edited by glg
    def _drain_free_produk_outbox_once(self, max_items=3):
        return self._get_free_produk_outbox_drain_use_case_service().drain_once(
            max_items=max_items,
            log_info=self.log_info,
            log_warning=self.log_warning,
        )

    # edited by glg
    def _finish_free_produk_outbox_drain(self):
        lock = getattr(self, "_free_produk_drain_lock", None)
        if lock is None:
            self._free_produk_drain_inflight = False
            return
        with lock:
            self._free_produk_drain_inflight = False

    # edited by glg
    def _get_free_produk_outbox_backlog_snapshot(self):
        return self._get_free_produk_outbox_drain_use_case_service().build_backlog_snapshot()

    # edited by glg
    def _dispatch_free_produk_dead_alert(self, alert_payload):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._dispatch_free_produk_dead_alert(
            controller=self,
            alert_payload=alert_payload,
        )


    # edited by glg
    def _log_free_produk_outbox_backlog(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._log_free_produk_outbox_backlog(
            controller=self,
        )


    # edited by glg
    def _drain_free_produk_outbox_async(self, max_items=3):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._drain_free_produk_outbox_async(
            controller=self,
            max_items=max_items,
        )


    def _update_quota_free_produk_async(self, arr_free_produk):
        return self._get_transaksi_penjualan_hotspot_use_case_service().update_quota_free_produk_async(
            controller=self,
            arr_free_produk=arr_free_produk,
            parse_int_callback=parse_int_from_text,
        )



    def find_barang_row_by_id(self, id_barang: str):

        return self.row_index_service.find_row(self.view.table_barang, id_barang)



    def handle_barang_row_removed(self, removed_row, removed_produk_id):

        self._shift_row_index_after_remove(removed_row, removed_produk_id)

        pid = str(removed_produk_id or "").strip()

        if pid:

            self.data_barang_cache.pop(pid, None)

            # edited by glg
            # Produk dihapus dari keranjang -> hapus token otorisasi qty besar produk tersebut.
            self.high_qty_authorization_service.clear_produk(pid)



    def handle_input_barcode(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().handle_input_barcode(
            controller=self,
        )




    def handle_barang_input(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().handle_barang_input(
            controller=self,
        )




    def _process_barang_text_input(self, text, barcode_only=False):
        return self._get_transaksi_penjualan_hotspot_use_case_service().process_barang_text_input(
            controller=self,
            text=text,
            barcode_only=barcode_only,
        )



    def _apply_barang_by_id(self, id_barang, tambahan_qty):
        return self._get_transaksi_penjualan_hotspot_use_case_service()._apply_barang_by_id(
            controller=self,
            id_barang=id_barang,
            tambahan_qty=tambahan_qty,
        )




    # edited by glg
    def _get_transaksi_harga_validation_service(self) -> TransaksiHargaValidationService:
        service = getattr(self, "transaksi_harga_validation_service", None)
        if service is None:
            service = TransaksiHargaValidationService()
            self.transaksi_harga_validation_service = service
        return service

    # edited by glg
    def _get_transaksi_barang_mutation_use_case_service(self) -> TransaksiBarangMutationUseCaseService:
        service = getattr(self, "transaksi_barang_mutation_use_case_service", None)
        if service is None:
            service = TransaksiBarangMutationUseCaseService()
            self.transaksi_barang_mutation_use_case_service = service
        return service



    # edited by glg
    def _get_transaksi_payment_info_service(self) -> TransaksiPaymentInfoService:
        service = getattr(self, "transaksi_payment_info_service", None)
        if service is None:
            service = TransaksiPaymentInfoService()
            self.transaksi_payment_info_service = service
        return service



    # edited by glg
    def _resolve_harga_jual(self, barang_detail) -> float:
        return self._get_transaksi_harga_validation_service().resolve_harga_jual(
            barang_detail,
            parse_float_callback=parse_float_safely,
        )



    # edited by glg
    def _is_valid_harga_jual(self, barang_detail) -> bool:
        return self._get_transaksi_harga_validation_service().is_valid_harga_jual(
            barang_detail,
            parse_float_callback=parse_float_safely,
        )



    # edited by glg
    def _warn_harga_jual_invalid(self, barang_detail, id_barang):
        self.log_warning(
            self._get_transaksi_harga_validation_service().build_invalid_harga_log(
                id_barang=id_barang,
                barang_detail=barang_detail,
            )
        )

        if hasattr(self, "view") and self.view:
            self.view.show_warning(
                "Harga Barang Belum Diatur",
                self._get_transaksi_harga_validation_service().build_invalid_harga_warning_message(),
            )



    def handle_jumlah_berubah(self, row, column):
        self._get_transaksi_penjualan_hotspot_use_case_service().handle_jumlah_berubah(
            controller=self,
            row=row,
            column=column,
        )



    # edited by glg
    def _ensure_admin_qty_besar_authorized(self, id_barang, qty) -> bool:
        pid = str(id_barang or "").strip()
        qty_limit = int(getattr(self, "qty_verify_threshold", 99) or 99)

        if not self.transaksi_barang_input_service.is_over_max_qty(qty, max_qty=qty_limit):
            return True

        if not self.high_qty_authorization_service.needs_authorization(pid, qty):
            return True

        if not self._verifikasi_admin_qty_besar(id_barang=pid, qty=qty):
            return False

        self.high_qty_authorization_service.mark_authorized(pid)
        return True

    def _record_admin_qty_approval_trail(self, *, id_barang, qty, approved, approval_name):
        return self._get_transaksi_penjualan_hotspot_use_case_service().record_admin_qty_approval_trail(
            controller=self,
            id_barang=id_barang,
            qty=qty,
            approved=approved,
            approval_name=approval_name,
        )

    def _verifikasi_admin_qty_besar(self, id_barang="", qty=0):
        approval_name = self.admin_dialog_service.verify(

            parent=self.view,

            warn_callback=lambda title, message, dialog: self.show_warning(title, message, view=dialog),
            return_admin_name=True,

        )
        approved = bool(str(approval_name or "").strip())
        self._record_admin_qty_approval_trail(
            id_barang=id_barang,
            qty=qty,
            approved=approved,
            approval_name=approval_name,
        )
        return approved



    def simpan_transaksi(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().simpan_transaksi(
            controller=self,
        )




    def cetak_struk_terakhir(self):
        return self._get_transaksi_penjualan_hotspot_use_case_service().cetak_struk_terakhir(
            controller=self,
        )










