# edited by glg
from pypos.core.base_controller import BaseController
from pypos.modules.penjualan.views.settlement_dialog_view import SettlementDialogView
from pypos.modules.penjualan.views.settlement_detail_view import SettlementDetailView
from pypos.modules.penjualan.models.settlement_model import SettlementModel
from pypos.modules.penjualan.services.settlement_orchestrator_service import SettlementOrchestratorService
from pypos.modules.penjualan.services.settlement_history_reprint_service import SettlementHistoryReprintService
from pypos.modules.penjualan.services.settlement_view_data_service import SettlementViewDataService
from pypos.modules.penjualan.services.settlement_state_service import SettlementStateService
from pypos.modules.penjualan.services.settlement_input_service import SettlementInputService
from pypos.modules.penjualan.services.async_stale_safe_result_service import (
    AsyncStaleSafeResultService,
)
from pypos.core.utils.worker_pool_utils import (
    is_worker_queue_dropped,
    submit_ui_preview_task,
    submit_ui_query_task_keyed,
)
from pypos.modules.printer.controllers.print_controller import PrintController
from pypos.modules.printer.controllers.printer_settings_controller import PrinterSettingsController
from PySide6.QtCore import QTimer
import time
import threading

class SettlementController(BaseController):
    def __init__(
        self,
        db_path,
        user_info,
        parent_window=None,
        as_page=False,
        on_close_requested=None,
        on_settlement_completed=None,
    ):
        super().__init__()
        self.model = SettlementModel(db_path)
        self.user_info = user_info
        self.db_path = db_path
        self.parent_window = parent_window
        # edited by glg
        # Callback opsional untuk aksi lanjutan (contoh: trigger export setelah settlement sukses).
        self.on_settlement_completed = on_settlement_completed

        self.printer_settings_controller = PrinterSettingsController()
        self.print_controller = PrintController(self.printer_settings_controller)
        self.orchestrator_service = SettlementOrchestratorService(self.model, self.print_controller)
        self.view_data_service = SettlementViewDataService()
        # edited by glg
        self.history_reprint_service = SettlementHistoryReprintService(
            settlement_model=self.model,
            print_controller=self.print_controller,
            view_data_service=self.view_data_service,
        )
        self.state_service = SettlementStateService()
        self.input_service = SettlementInputService()
        self.async_stale_result_service = AsyncStaleSafeResultService(
            log_info=self.log_info,
            log_warning=self.log_warning,
        )
        # edited by glg
        # Sequence request async detail item transaksi untuk mencegah hasil stale.
        self._preview_detail_request_seq = 0
        self._preview_detail_request_lock = threading.Lock()
        # edited by glg
        # Sequence request async detail history settlement untuk mencegah hasil stale.
        self._history_detail_request_seq = 0
        self._history_detail_request_lock = threading.Lock()
        # edited by glg
        # Sequence request async refresh settlement page agar respons klik/menu tetap ringan.
        self._refresh_request_seq = 0
        self._refresh_request_lock = threading.Lock()
        self.view = SettlementDialogView(
            self,
            parent=parent_window,
            as_page=as_page,
            on_close_requested=on_close_requested,
        )
        if hasattr(self.view, "set_refresh_payload_handler"):
            self.view.set_refresh_payload_handler(self._on_refresh_payload_ready)
        if hasattr(self.view, "set_preview_detail_payload_handler"):
            self.view.set_preview_detail_payload_handler(self._on_preview_detail_payload_ready)
        if hasattr(self.view, "set_history_detail_payload_handler"):
            self.view.set_history_detail_payload_handler(self._on_history_detail_payload_ready)
        self.detail_view = SettlementDetailView(self.view)

        self.view.setup_controller_events(self)

        self.log_debug("masuk controller settle")

        # load data awal
        self.refresh_page_data(reset_input=True)

    # edited by glg
    def _get_async_stale_result_service(self):
        service = getattr(self, "async_stale_result_service", None)
        if service is not None:
            return service
        # Kompatibilitas lifecycle test: controller bisa dibuat via __new__ tanpa __init__.
        service = AsyncStaleSafeResultService(
            log_info=getattr(self, "log_info", lambda *_args, **_kwargs: None),
            log_warning=getattr(self, "log_warning", lambda *_args, **_kwargs: None),
        )
        self.async_stale_result_service = service
        return service

    # edited by glg
    def _refresh_cache_key(self):
        return f"settlement:refresh:{id(self)}"

    # edited by glg
    def _history_filter_cache_key(self):
        return f"settlement:history_filter:{id(self)}"

    # edited by glg
    def _preview_detail_cache_key(self, transaksi_id):
        return f"settlement:preview_detail:{id(self)}:{int(transaksi_id or 0)}"

    # edited by glg
    def _history_detail_cache_key(self, history_data):
        history_row = history_data if isinstance(history_data, dict) else {}
        history_id = self._to_positive_int(history_row.get("id"), default=0)
        if history_id > 0:
            return f"settlement:history_detail:{id(self)}:{history_id}"
        counter = str(history_row.get("counter") or history_row.get("kode_settlement") or "").strip()
        if counter:
            return f"settlement:history_detail:{id(self)}:{counter}"
        return f"settlement:history_detail:{id(self)}:fallback"

    # edited by glg
    def _schedule_stale_retry(self, schedule_key, callback, delay_ms=240):
        stale_service = self._get_async_stale_result_service()
        if not stale_service.mark_retry_scheduled(schedule_key):
            return False
        retry_delay = max(140, int(delay_ms or 0))

        def _run():
            stale_service.clear_retry_scheduled(schedule_key)
            if self.is_disposed:
                return
            try:
                callback()
            except (RuntimeError, TypeError, ValueError, AttributeError) as exc:
                self.log_warning(
                    f"[SETTLEMENT_STALE_RETRY_ERROR] retry key={schedule_key} gagal: {exc}"
                )

        QTimer.singleShot(retry_delay, _run)
        return True

    def on_settlement_selesai(self):
        self.log_info("Settlement selesai - refresh POS atau reset form di sini")

    # edited by glg
    def _notify_settlement_completed(self, result):
        callback = getattr(self, "on_settlement_completed", None)
        if not callable(callback):
            return
        try:
            callback(result)
        except TypeError:
            callback()
        except Exception as exc:
            self.log_warning(f"Gagal jalankan callback settlement completed: {exc}")

    def show(self):
        if getattr(self.view, "is_page", False):
            self.view.show()
            return None
        self.view.exec()

    def load_transaksi(self):
        # edited by glg
        # Baseline performa: pisahkan durasi query, render list, dan ringkasan non-tunai.
        started_at = time.perf_counter()
        query_started_at = time.perf_counter()
        self.transaksi_data = self.safe_call(
            self.model.get_transaksi_belum_settle,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat transaksi belum settle: {exc}"),
        )
        query_elapsed_ms = (time.perf_counter() - query_started_at) * 1000.0
        self.log_debug(f"load_transaksi: jumlah_data={len(self.transaksi_data)}")
        # edited by glg
        # Hindari query rekap jika view tidak memiliki sink UI untuk menampilkan hasilnya.
        if self._view_supports_rekap():
            self.tampilkan_rekap()
        self.log_debug(f"Jumlah transaksi belum settle: {len(self.transaksi_data)}")
        render_started_at = time.perf_counter()
        self.view.render_transaksi_list(self.transaksi_data)
        render_elapsed_ms = (time.perf_counter() - render_started_at) * 1000.0
        non_tunai_started_at = time.perf_counter()
        self.tampilkan_info_non_tunai()
        non_tunai_elapsed_ms = (time.perf_counter() - non_tunai_started_at) * 1000.0
        total_elapsed_ms = (time.perf_counter() - started_at) * 1000.0
        if total_elapsed_ms >= 220.0:
            self.log_info(
                f"[PERF] settlement_load rows={len(self.transaksi_data or [])} "
                f"query_ms={query_elapsed_ms:.1f} render_ms={render_elapsed_ms:.1f} "
                f"non_tunai_ms={non_tunai_elapsed_ms:.1f} total_ms={total_elapsed_ms:.1f}"
            )

    # edited by glg
    def _view_supports_rekap(self):
        if not hasattr(self, "view") or self.view is None:
            return False
        return bool(hasattr(self.view, "rekap_text") and self.view.rekap_text is not None)

    # edited by glg
    def _next_refresh_request_id(self):
        with self._refresh_request_lock:
            self._refresh_request_seq += 1
            return int(self._refresh_request_seq)

    # edited by glg
    def _is_latest_refresh_request(self, request_id):
        with self._refresh_request_lock:
            return int(request_id or 0) == int(self._refresh_request_seq or 0)

    # edited by glg
    def _fetch_refresh_payload(self, include_rekap=False):
        transaksi_query_started_at = time.perf_counter()
        transaksi_data = self.safe_call(
            self.model.get_transaksi_belum_settle,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat transaksi belum settle: {exc}"),
        )
        transaksi_query_elapsed_ms = (time.perf_counter() - transaksi_query_started_at) * 1000.0

        rekap = {}
        rekap_query_elapsed_ms = 0.0
        if bool(include_rekap):
            rekap_query_started_at = time.perf_counter()
            rekap = self.safe_call(
                self.model.get_rekap_settlement,
                default={},
                on_error=lambda exc: self.log_warning(f"Gagal memuat rekap settlement: {exc}"),
            )
            rekap_query_elapsed_ms = (time.perf_counter() - rekap_query_started_at) * 1000.0

        non_tunai_query_started_at = time.perf_counter()
        non_tunai_summary = self.safe_call(
            self.model.get_ringkasan_non_tunai,
            default={},
            on_error=lambda exc: self.log_warning(f"Gagal memuat ringkasan non tunai: {exc}"),
        )
        non_tunai_query_elapsed_ms = (time.perf_counter() - non_tunai_query_started_at) * 1000.0

        history_query_started_at = time.perf_counter()
        history = self.safe_call(
            self.model.get_last_settlements,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat history settlement: {exc}"),
        )
        history_query_elapsed_ms = (time.perf_counter() - history_query_started_at) * 1000.0

        return {
            "transaksi_data": transaksi_data or [],
            "rekap": rekap or {},
            "non_tunai_summary": non_tunai_summary or {},
            "history": history or [],
            "include_rekap": bool(include_rekap),
            "transaksi_query_elapsed_ms": transaksi_query_elapsed_ms,
            "rekap_query_elapsed_ms": rekap_query_elapsed_ms,
            "non_tunai_query_elapsed_ms": non_tunai_query_elapsed_ms,
            "history_query_elapsed_ms": history_query_elapsed_ms,
        }

    # edited by glg
    def _apply_refresh_payload(self, payload):
        started_at = time.perf_counter()
        data = payload if isinstance(payload, dict) else {}
        cache_payload = dict(data)
        cache_payload["reset_input"] = False
        self._get_async_stale_result_service().remember_payload(self._refresh_cache_key(), cache_payload)
        transaksi_data = data.get("transaksi_data") or []
        self.transaksi_data = transaksi_data

        rekap_render_elapsed_ms = 0.0
        if bool(data.get("include_rekap")) and self._view_supports_rekap():
            rekap_render_started_at = time.perf_counter()
            self.view.tampilkan_rekap(
                self.view_data_service.build_rekap_text(data.get("rekap") or {})
            )
            rekap_render_elapsed_ms = (time.perf_counter() - rekap_render_started_at) * 1000.0

        transaksi_render_started_at = time.perf_counter()
        self.view.render_transaksi_list(transaksi_data)
        transaksi_render_elapsed_ms = (time.perf_counter() - transaksi_render_started_at) * 1000.0

        non_tunai_render_started_at = time.perf_counter()
        summary = data.get("non_tunai_summary") or {}
        total_non_tunai = self.view_data_service.calculate_total_non_tunai(summary)
        self.state_service.set_non_tunai_summary(summary, total_non_tunai)
        self.view.tampilkan_info_non_tunai(summary)
        self._update_settle_button_state()
        non_tunai_render_elapsed_ms = (time.perf_counter() - non_tunai_render_started_at) * 1000.0

        history_render_started_at = time.perf_counter()
        history = data.get("history") or []
        self.view.tampilkan_history(history)
        history_render_elapsed_ms = (time.perf_counter() - history_render_started_at) * 1000.0

        reset_input = bool(data.get("reset_input"))
        if reset_input and hasattr(self.view, "uang_input") and self.view.uang_input is not None:
            self.view.uang_input.clear()
            self.view.tampilkan_info_transaksi(None)
            self._update_settle_button_state(0)

        apply_elapsed_ms = (time.perf_counter() - started_at) * 1000.0
        total_elapsed_ms = (
            float(data.get("transaksi_query_elapsed_ms") or 0.0)
            + float(data.get("rekap_query_elapsed_ms") or 0.0)
            + float(data.get("non_tunai_query_elapsed_ms") or 0.0)
            + float(data.get("history_query_elapsed_ms") or 0.0)
            + apply_elapsed_ms
        )
        if total_elapsed_ms >= 260.0:
            self.log_info(
                f"[PERF] settlement_refresh history_rows={len(history)} "
                f"transaksi_query_ms={float(data.get('transaksi_query_elapsed_ms') or 0.0):.1f} "
                f"rekap_query_ms={float(data.get('rekap_query_elapsed_ms') or 0.0):.1f} "
                f"non_tunai_query_ms={float(data.get('non_tunai_query_elapsed_ms') or 0.0):.1f} "
                f"history_query_ms={float(data.get('history_query_elapsed_ms') or 0.0):.1f} "
                f"transaksi_render_ms={transaksi_render_elapsed_ms:.1f} "
                f"rekap_render_ms={rekap_render_elapsed_ms:.1f} "
                f"non_tunai_render_ms={non_tunai_render_elapsed_ms:.1f} "
                f"history_render_ms={history_render_elapsed_ms:.1f} "
                f"apply_ms={apply_elapsed_ms:.1f} total_ms={total_elapsed_ms:.1f}"
            )

    # edited by glg
    def _run_async_refresh_fetch(self, request_id, reset_input, include_rekap, retry_attempt=0):
        if not self._is_latest_refresh_request(request_id):
            return

        def _worker():
            payload = self._fetch_refresh_payload(include_rekap=include_rekap)
            payload["reset_input"] = bool(reset_input)
            if hasattr(self.view, "emit_refresh_payload"):
                self.view.emit_refresh_payload(request_id, payload)

        key = f"settlement-refresh:{id(self)}"
        future = submit_ui_query_task_keyed(key, _worker)
        # edited by glg
        # Fail-safe antrean penuh: pakai stale payload + retry async (tanpa query sync berat).
        if is_worker_queue_dropped(future):
            stale_used = self._get_async_stale_result_service().apply_stale_payload(
                cache_key=self._refresh_cache_key(),
                apply_callback=self._apply_refresh_payload,
                request_id=request_id,
                is_latest_request=self._is_latest_refresh_request,
                mutate_payload=lambda stale_payload: self._mutate_stale_refresh_payload(
                    stale_payload,
                    include_rekap=include_rekap,
                ),
                reason_code="SETTLEMENT_REFRESH_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    "[SETTLEMENT_REFRESH_QUEUE_FULL_STALE_MISS] antrean penuh dan cache refresh belum tersedia."
                )
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[SETTLEMENT_REFRESH_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"settlement:refresh_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_refresh_fetch(
                    request_id,
                    reset_input=reset_input,
                    include_rekap=include_rekap,
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=260,
            )

    # edited by glg
    def _mutate_stale_refresh_payload(self, payload, include_rekap=False):
        data = payload if isinstance(payload, dict) else {}
        data["reset_input"] = False
        if not bool(include_rekap):
            data["include_rekap"] = False
        return data

    # edited by glg
    def _on_refresh_payload_ready(self, request_id, payload):
        if not self._is_latest_refresh_request(request_id):
            return
        data = payload if isinstance(payload, dict) else {}
        if str(data.get("mode") or "").strip().lower() == "history_only":
            self._apply_history_only_payload(data)
            return
        self._apply_refresh_payload(data)

    # edited by glg
    def _apply_history_only_payload(self, payload):
        data = payload if isinstance(payload, dict) else {}
        self._get_async_stale_result_service().remember_payload(self._history_filter_cache_key(), data)
        history = data.get("history") or []
        render_started_at = time.perf_counter()
        self.view.tampilkan_history(history)
        render_elapsed_ms = (time.perf_counter() - render_started_at) * 1000.0
        query_elapsed_ms = float(data.get("history_query_elapsed_ms") or 0.0)
        total_elapsed_ms = query_elapsed_ms + render_elapsed_ms
        if total_elapsed_ms >= 180.0:
            self.log_info(
                f"[PERF] settlement_history_filter rows={len(history)} "
                f"query_ms={query_elapsed_ms:.1f} render_ms={render_elapsed_ms:.1f} "
                f"total_ms={total_elapsed_ms:.1f}"
            )

    # edited by glg
    def refresh_page_data(self, reset_input=False, force_sync=False):
        include_rekap = self._view_supports_rekap()
        if bool(force_sync):
            payload = self._fetch_refresh_payload(include_rekap=include_rekap)
            payload["reset_input"] = bool(reset_input)
            self._apply_refresh_payload(payload)
            return
        request_id = self._next_refresh_request_id()
        self._run_async_refresh_fetch(request_id, reset_input=reset_input, include_rekap=include_rekap)

    def tampilkan_info_non_tunai(self):
        summary = self.safe_call(
            self.model.get_ringkasan_non_tunai,
            default={},
            on_error=lambda exc: self.log_warning(f"Gagal memuat ringkasan non tunai: {exc}"),
        )
        total_non_tunai = self.view_data_service.calculate_total_non_tunai(summary)
        self.state_service.set_non_tunai_summary(summary, total_non_tunai)
        self.view.tampilkan_info_non_tunai(summary)
        self._update_settle_button_state()

    def tampilkan_detail(self, row, col):
        if row < 0 or row >= len(self.transaksi_data):
            return
        tanggal = self.transaksi_data[row]['tanggal']
        kasir = self.transaksi_data[row]['kasir']
        detail_list = self.safe_call(
            self.model.get_detail_transaksi_per_hari_kasir,
            tanggal,
            kasir,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat detail transaksi {tanggal}/{kasir}: {exc}"),
        )
        self.view.render_detail_transaksi(detail_list)

    def tampilkan_detail_transaksi(self, tanggal, kasir):
        detail_list = self.safe_call(
            self.model.get_detail_transaksi_per_hari_kasir,
            tanggal,
            kasir,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat detail transaksi harian {tanggal}/{kasir}: {exc}"),
        )
        self.detail_view.show_transaksi_detail(
            tanggal,
            kasir,
            detail_list,
            on_select_transaksi=self.tampilkan_preview_detail_transaksi,
        )

    def tampilkan_preview_detail_transaksi(self, transaksi_id):
        normalized_id = self._to_positive_int(transaksi_id, default=0)
        if normalized_id <= 0:
            self.view.tampilkan_error("Detail transaksi tidak ditemukan.")
            return
        request_id = self._next_preview_detail_request_id()
        self._run_async_preview_detail_fetch(request_id, normalized_id)

    # edited by glg
    def _to_positive_int(self, value, default=0):
        try:
            parsed = int(str(value).strip())
        except (TypeError, ValueError):
            parsed = int(default or 0)
        return parsed if parsed > 0 else int(default or 0)

    # edited by glg
    def _next_preview_detail_request_id(self):
        with self._preview_detail_request_lock:
            self._preview_detail_request_seq += 1
            return int(self._preview_detail_request_seq)

    # edited by glg
    def _is_latest_preview_detail_request(self, request_id):
        with self._preview_detail_request_lock:
            return int(request_id or 0) == int(self._preview_detail_request_seq or 0)

    # edited by glg
    def _fetch_preview_detail_payload(self, transaksi_id):
        detail_list = self.safe_call(
            self.model.get_detail_transaksi,
            transaksi_id,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat detail item transaksi {transaksi_id}: {exc}"),
        )
        return {
            "transaksi_id": int(transaksi_id),
            "detail_list": detail_list,
        }

    # edited by glg
    def _run_async_preview_detail_fetch(self, request_id, transaksi_id, retry_attempt=0):
        if not self._is_latest_preview_detail_request(request_id):
            return

        def _worker():
            payload = self._fetch_preview_detail_payload(transaksi_id)
            if hasattr(self.view, "emit_preview_detail_payload"):
                self.view.emit_preview_detail_payload(request_id, payload)

        future = submit_ui_preview_task(_worker)
        # edited by glg
        # Fail-safe antrean preview penuh: stale-safe + retry async.
        if is_worker_queue_dropped(future):
            stale_used = self._get_async_stale_result_service().apply_stale_payload(
                cache_key=self._preview_detail_cache_key(transaksi_id),
                apply_callback=lambda stale_payload: self._on_preview_detail_payload_ready(
                    request_id,
                    stale_payload,
                ),
                request_id=request_id,
                is_latest_request=self._is_latest_preview_detail_request,
                reason_code="SETTLEMENT_PREVIEW_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    f"[SETTLEMENT_PREVIEW_QUEUE_FULL_STALE_MISS] transaksi_id={int(transaksi_id or 0)}"
                )
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[SETTLEMENT_PREVIEW_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"settlement:preview_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_preview_detail_fetch(
                    request_id,
                    transaksi_id=int(transaksi_id or 0),
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=240,
            )

    # edited by glg
    def _on_preview_detail_payload_ready(self, request_id, payload):
        if not self._is_latest_preview_detail_request(request_id):
            return
        data = payload if isinstance(payload, dict) else {}
        transaksi_id = self._to_positive_int(data.get("transaksi_id"), default=0)
        detail_list = data.get("detail_list") or []
        if not detail_list:
            self.view.tampilkan_error("Detail transaksi tidak ditemukan.")
            return
        self._get_async_stale_result_service().remember_payload(
            self._preview_detail_cache_key(transaksi_id),
            data,
        )
        self.detail_view.show_transaksi_item_detail(transaksi_id, detail_list)

    # edited by glg
    def _next_history_detail_request_id(self):
        with self._history_detail_request_lock:
            self._history_detail_request_seq += 1
            return int(self._history_detail_request_seq)

    # edited by glg
    def _is_latest_history_detail_request(self, request_id):
        with self._history_detail_request_lock:
            return int(request_id or 0) == int(self._history_detail_request_seq or 0)

    # edited by glg
    def _fetch_history_detail_payload(self, history_data, transaksi_ids):
        detail_list = self.safe_call(
            self.model.get_transaksi_by_ids,
            transaksi_ids,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat detail history settlement: {exc}"),
        )
        return {
            "history_data": dict(history_data or {}),
            "detail_list": detail_list or [],
        }

    # edited by glg
    def _run_async_history_detail_fetch(
        self,
        request_id,
        history_data,
        transaksi_ids,
        retry_attempt=0,
    ):
        if not self._is_latest_history_detail_request(request_id):
            return

        def _worker():
            payload = self._fetch_history_detail_payload(history_data, transaksi_ids)
            if hasattr(self.view, "emit_history_detail_payload"):
                self.view.emit_history_detail_payload(request_id, payload)

        future = submit_ui_preview_task(_worker)
        # edited by glg
        # Fail-safe antrean preview penuh: stale-safe + retry async.
        if is_worker_queue_dropped(future):
            stale_used = self._get_async_stale_result_service().apply_stale_payload(
                cache_key=self._history_detail_cache_key(history_data),
                apply_callback=lambda stale_payload: self._on_history_detail_payload_ready(
                    request_id,
                    stale_payload,
                ),
                request_id=request_id,
                is_latest_request=self._is_latest_history_detail_request,
                reason_code="SETTLEMENT_HISTORY_DETAIL_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    "[SETTLEMENT_HISTORY_DETAIL_QUEUE_FULL_STALE_MISS] cache detail history belum tersedia."
                )
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[SETTLEMENT_HISTORY_DETAIL_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"settlement:history_detail_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_history_detail_fetch(
                    request_id,
                    history_data=history_data,
                    transaksi_ids=transaksi_ids,
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=260,
            )

    # edited by glg
    def _on_history_detail_payload_ready(self, request_id, payload):
        if not self._is_latest_history_detail_request(request_id):
            return
        data = payload if isinstance(payload, dict) else {}
        history_data = data.get("history_data") or {}
        detail_list = data.get("detail_list") or []
        if not detail_list:
            self.view.tampilkan_error("Detail transaksi settlement tidak ditemukan.")
            return
        self._get_async_stale_result_service().remember_payload(
            self._history_detail_cache_key(history_data),
            data,
        )
        self.detail_view.show_history_detail(history_data, detail_list)

    def tampilkan_detail_history_settlement(self, history_data):
        data_ids = (history_data or {}).get("data_transaksi_id")
        transaksi_ids = self.view_data_service.extract_transaksi_ids(data_ids)
        if not transaksi_ids:
            self.view.tampilkan_error("Data transaksi settlement tidak tersedia.")
            return
        request_id = self._next_history_detail_request_id()
        self._run_async_history_detail_fetch(request_id, history_data, transaksi_ids)

    # edited by glg
    def _build_history_filter_payload(self, start_date, end_date):
        query_started_at = time.perf_counter()
        history = self.safe_call(
            self.model.get_history_settlement_by_range,
            start_date,
            end_date,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memfilter history settlement: {exc}"),
        )
        return {
            "mode": "history_only",
            "history": history or [],
            "history_query_elapsed_ms": (time.perf_counter() - query_started_at) * 1000.0,
        }

    # edited by glg
    def _run_async_history_filter_fetch(self, request_id, start_date, end_date, retry_attempt=0):
        if not self._is_latest_refresh_request(request_id):
            return

        def _worker():
            payload = self._build_history_filter_payload(start_date, end_date)
            if hasattr(self.view, "emit_refresh_payload"):
                self.view.emit_refresh_payload(request_id, payload)

        key = f"settlement-history-filter:{id(self)}"
        future = submit_ui_query_task_keyed(key, _worker)
        if not is_worker_queue_dropped(future):
            return

        stale_used = self._get_async_stale_result_service().apply_stale_payload(
            cache_key=self._history_filter_cache_key(),
            apply_callback=self._apply_history_only_payload,
            request_id=request_id,
            is_latest_request=self._is_latest_refresh_request,
            reason_code="SETTLEMENT_HISTORY_FILTER_QUEUE_FULL_STALE",
        )
        if not stale_used:
            self.log_info(
                "[SETTLEMENT_HISTORY_FILTER_QUEUE_FULL_STALE_MISS] cache filter history belum tersedia."
            )
        if int(retry_attempt or 0) >= 3:
            self.log_warning(
                "[SETTLEMENT_HISTORY_FILTER_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
            )
            return
        self._schedule_stale_retry(
            schedule_key=f"settlement:history_filter_retry:{id(self)}:{int(request_id or 0)}",
            callback=lambda: self._run_async_history_filter_fetch(
                request_id,
                start_date=start_date,
                end_date=end_date,
                retry_attempt=int(retry_attempt or 0) + 1,
            ),
            delay_ms=260,
        )

    def filter_history_by_range(self):
        start_date, end_date = self.view.get_history_filter_range()
        if not start_date or not end_date:
            self.view.tampilkan_error("Rentang tanggal tidak valid.")
            return
        request_id = self._next_refresh_request_id()
        self._run_async_history_filter_fetch(request_id, start_date, end_date)

    def aktifkan_admin_section(self):
        state, nilai = self.input_service.parse_admin_input(self.view.uang_input.text())
        if state == "empty":
            self.view.tampilkan_info_transaksi(None)
            self._update_settle_button_state(0)
            return
        if state == "invalid":
            self.view.settle_button.setEnabled(False)
            self.view.tampilkan_info_transaksi(None)
            return
        self.view.tampilkan_info_transaksi(nilai)
        self._update_settle_button_state(nilai)

    def _get_total_non_tunai(self):
        return self.state_service.get_total_non_tunai()

    def _update_settle_button_state(self, nilai_tunai=None):
        if not hasattr(self.view, "settle_button") or self.view.settle_button is None:
            return
        text = ""
        if hasattr(self.view, "uang_input") and self.view.uang_input:
            text = self.view.uang_input.text().strip()
        total_tunai = self.input_service.resolve_total_tunai(nilai_tunai, text)
        total_non_tunai = self._get_total_non_tunai()
        enabled = self.input_service.can_enable_settle(
            self.state_service.has_transaksi_data(),
            total_tunai,
            total_non_tunai,
        )
        self.view.settle_button.setEnabled(enabled)

    def _show_preview_settlement(self, total_disetor, total_non_tunai):
        return self.detail_view.ask_settlement_preview(total_disetor, total_non_tunai)

    def _show_admin_verification(self):
        return self.detail_view.ask_admin_verification(self.orchestrator_service.validate_admin)

    # edited by glg
    def print_ulang_settlement_history(self, history_data):
        result = self.safe_call(
            self.history_reprint_service.execute,
            history_data,
            default={"ok": False, "message": "Terjadi kesalahan saat proses print ulang settlement."},
            on_error=lambda exc: self.log_warning(f"Gagal proses print ulang history settlement: {exc}"),
        ) or {}
        if not bool(result.get("ok")):
            self.view.tampilkan_error(result.get("message", "Print ulang nota settlement gagal."))
            return False
        self.view.tampilkan_pesan(result.get("message", "Print ulang nota settlement berhasil."))
        return True

    def proses_settlement(self):
        if not self.transaksi_data:
            self.view.tampilkan_error("Tidak ada transaksi yang dapat di settle.")
            return
        uang_str = ""
        if hasattr(self.view, "uang_input") and self.view.uang_input is not None:
            uang_str = self.view.uang_input.text().strip()
        total_non_tunai = self._get_total_non_tunai()
        ok_parse, total_disetor, parse_message = self.orchestrator_service.parse_total_disetor(
            uang_str,
            total_non_tunai,
        )
        if not ok_parse:
            self.view.tampilkan_error(parse_message)
            return
        if not self._show_preview_settlement(total_disetor, total_non_tunai):
            return
        admin_name = self._show_admin_verification()
        if not admin_name:
            return

        self._execute_settlement(total_disetor, admin_name)

    def _execute_settlement(self, total_disetor, admin_name):
        result = self.orchestrator_service.execute_settlement(
            transaksi_data=self.transaksi_data,
            total_disetor=total_disetor,
            admin_name=admin_name,
            user_info=self.user_info,
        )
        if not result.get("ok"):
            self.view.tampilkan_error(result.get("message", "Gagal settlement."))
            return
        if hasattr(self.view, "settle_button") and self.view.settle_button is not None:
            self.view.settle_button.setEnabled(False)
        # edited by glg
        # Refresh dibuat async agar aksi settle tidak memblokir UI thread.
        self.refresh_page_data(reset_input=True, force_sync=False)
        self._notify_settlement_completed(result)
        message = self.view_data_service.build_success_message(
            counter=result.get("counter"),
            print_ok=result.get("print_ok", True),
        )
        self.view.tampilkan_pesan(message)
        if hasattr(self.view, "_handle_close_request"):
            self.view._handle_close_request()
        else:
            self.view.close()

    def tampilkan_rekap(self):
        rekap = self.safe_call(
            self.model.get_rekap_settlement,
            default={},
            on_error=lambda exc: self.log_warning(f"Gagal memuat rekap settlement: {exc}"),
        )
        self.view.tampilkan_rekap(self.view_data_service.build_rekap_text(rekap))

    @property
    def transaksi_data(self):
        return self.state_service.get_transaksi_data()

    @transaksi_data.setter
    def transaksi_data(self, value):
        self.state_service.set_transaksi_data(value)

    def get_total_settlement(self):
        return self.state_service.get_total_settlement()
