﻿# edited by glg
from pypos.core.base_controller import BaseController
from pypos.modules.penjualan.models.load_transaksi_model import LoadTransaksiModel
from pypos.modules.penjualan.views.history_transaksi_view import HistoryTransaksiView
from pypos.modules.penjualan.services.history_reprint_service import HistoryReprintService
from pypos.modules.penjualan.services.async_stale_safe_result_service import (
    AsyncStaleSafeResultService,
)
from datetime import date, timedelta
import time
import threading

from PySide6.QtCore import QTimer

from pypos.core.utils.worker_pool_utils import (
    is_worker_queue_dropped,
    submit_ui_preview_task,
    submit_ui_query_task_keyed,
)

class HistoryTransaksiController(BaseController):
    def __init__(self, parent, user_id, db_path, tanggal=None, as_page=False, on_close_requested=None):
        super().__init__()
        self.model = LoadTransaksiModel(db_path)
        self.parent = parent
        self.user_id = user_id
        self.tanggal = tanggal
        default_date = tanggal or date.today().isoformat()
        self.start_date = default_date
        self.end_date = default_date
        self.transaksi_terpilih_id = None
        # edited by glg
        # Guard event ganda (cellClicked + currentCellChanged) agar query detail tidak dobel.
        self._last_selected_transaksi_id = None
        self._last_selected_at_ms = 0
        # edited by glg
        # Sequence request async untuk mencegah hasil lama menimpa preview transaksi terbaru.
        self._detail_request_seq = 0
        self._detail_request_lock = threading.Lock()
        # edited by glg
        # Sequence request async untuk refresh daftar history transaksi.
        self._history_request_seq = 0
        self._history_request_lock = threading.Lock()
        self.async_stale_result_service = AsyncStaleSafeResultService(
            log_info=self.log_info,
            log_warning=self.log_warning,
        )
        self.bind_view(
            HistoryTransaksiView(self, parent=parent, as_page=as_page, on_close_requested=on_close_requested),
            bind_back=False
        )
        if hasattr(self.view, "set_detail_payload_handler"):
            self.view.set_detail_payload_handler(self._on_detail_payload_ready)
        if hasattr(self.view, "set_history_payload_handler"):
            self.view.set_history_payload_handler(self._on_history_payload_ready)
        self.reprint_service = HistoryReprintService(db_path=db_path)
        self.view.set_date_inputs(self.start_date, self.end_date)
        self.load_history_transaksi()
        from PySide6.QtWidgets import QHeaderView
        self.view.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

    # 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 _history_cache_key(self):
        return f"history_transaksi:list:{id(self)}"

    # edited by glg
    def _detail_cache_key(self, transaksi_id_text):
        return f"history_transaksi:detail:{id(self)}:{str(transaksi_id_text or '').strip()}"

    # 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"[HISTORY_STALE_RETRY_ERROR] retry key={schedule_key} gagal: {exc}")

        QTimer.singleShot(retry_delay, _run)
        return True

    def show(self):
        if getattr(self.view, "is_page", False):
            self.view.show()
            return None
        self.view.setWindowTitle("History Penjualan Hari Ini")
        return self.view.exec()

    # edited by glg
    def _next_history_request_id(self):
        with self._history_request_lock:
            self._history_request_seq += 1
            return int(self._history_request_seq)

    # edited by glg
    def _is_latest_history_request(self, request_id):
        with self._history_request_lock:
            return int(request_id or 0) == int(self._history_request_seq or 0)

    # edited by glg
    def _build_history_payload(self):
        query_started_at = time.perf_counter()
        rows = self.safe_call(
            self.model.get_filtered_transaksi_list,
            user_id=self.user_id,
            tanggal=self.tanggal,
            start_date=self.start_date,
            end_date=self.end_date,
            include_trashed=True,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat history transaksi: {exc}"),
        )
        query_elapsed_ms = (time.perf_counter() - query_started_at) * 1000.0
        return {
            "rows": rows or [],
            "query_elapsed_ms": query_elapsed_ms,
        }

    # edited by glg
    def _apply_history_payload(self, payload):
        data = payload if isinstance(payload, dict) else {}
        self._get_async_stale_result_service().remember_payload(self._history_cache_key(), data)
        rows = data.get("rows") or []
        render_started_at = time.perf_counter()
        self.view.tampilkan_history_rows(rows)
        render_elapsed_ms = (time.perf_counter() - render_started_at) * 1000.0
        query_elapsed_ms = float(data.get("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] history_load rows={len(rows)} "
                f"query_ms={query_elapsed_ms:.1f} render_ms={render_elapsed_ms:.1f} "
                f"total_ms={total_elapsed_ms:.1f}"
            )

    # edited by glg
    def _run_async_history_fetch(self, request_id, retry_attempt=0):
        if not self._is_latest_history_request(request_id):
            return

        def _worker():
            payload = self._build_history_payload()
            if hasattr(self.view, "emit_history_payload"):
                self.view.emit_history_payload(request_id, payload)

        key = f"history-transaksi-refresh:{id(self)}"
        future = submit_ui_query_task_keyed(key, _worker)
        # edited by glg
        # Fail-safe antrean penuh: stale-safe + retry async tanpa query sinkron berat.
        if is_worker_queue_dropped(future):
            stale_used = self._get_async_stale_result_service().apply_stale_payload(
                cache_key=self._history_cache_key(),
                apply_callback=self._apply_history_payload,
                request_id=request_id,
                is_latest_request=self._is_latest_history_request,
                reason_code="HISTORY_LIST_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info("[HISTORY_LIST_QUEUE_FULL_STALE_MISS] cache history list belum tersedia.")
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[HISTORY_LIST_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"history:list_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_history_fetch(
                    request_id,
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=260,
            )

    # edited by glg
    def _on_history_payload_ready(self, request_id, payload):
        if not self._is_latest_history_request(request_id):
            return
        self._apply_history_payload(payload)

    def load_history_transaksi(self, force_sync=False):
        if bool(force_sync):
            payload = self._build_history_payload()
            self._apply_history_payload(payload)
            return
        request_id = self._next_history_request_id()
        self._run_async_history_fetch(request_id)

    def _safe_cell_text(self, row, col):
        try:
            item = self.view.table.item(row, col)
            return item.text() if item else ""
        except Exception:
            return ""

    def transaksi_diklik(self, row, column):
        transaksi_id_text = self._safe_cell_text(row, 0)
        invoice_number = self._safe_cell_text(row, 1)
        if not transaksi_id_text:
            return
        try:
            self.transaksi_terpilih_id = int(transaksi_id_text)
        except Exception:
            self.log_warning(f"ID transaksi tidak valid pada history row={row}: {transaksi_id_text}")
            return
        # edited by glg
        # Di beberapa interaksi Qt, 1 aksi user bisa memicu 2 event beruntun.
        # Skip hanya untuk transaksi yang sama dalam jeda sangat pendek.
        now_ms = int(time.monotonic() * 1000)
        if (
            self._last_selected_transaksi_id == self.transaksi_terpilih_id
            and (now_ms - int(self._last_selected_at_ms or 0)) <= 120
        ):
            return
        self._last_selected_transaksi_id = self.transaksi_terpilih_id
        self._last_selected_at_ms = now_ms
        self.view.current_invoice_number = invoice_number
        request_id = self._next_detail_request_id()
        self._run_async_detail_fetch(request_id, str(self.transaksi_terpilih_id))

    # edited by glg
    def _next_detail_request_id(self):
        with self._detail_request_lock:
            self._detail_request_seq += 1
            return int(self._detail_request_seq)

    # edited by glg
    def _is_latest_detail_request(self, request_id):
        with self._detail_request_lock:
            return int(request_id or 0) == int(self._detail_request_seq or 0)

    # edited by glg
    def _fetch_detail_payload(self, transaksi_id_text):
        header_data = self.safe_call(
            self.model.get_transaksi_header,
            transaksi_id_text,
            default=None,
            on_error=lambda exc: self.log_warning(f"gagal memuat header history transaksi: {exc}"),
        )
        detail_rows = self.safe_call(
            self.model.get_detail_transaksi,
            transaksi_id_text,
            default=[],
            on_error=lambda exc: self.log_warning(f"gagal memuat detail history transaksi: {exc}"),
        )
        voucher_info = self.safe_call(
            self.model.get_voucher_usage_info,
            transaksi_id_text,
            default={},
            on_error=lambda exc: self.log_warning(f"gagal memuat voucher usage info: {exc}")
        )
        return_items = self.safe_call(
            self.model.get_return_items_info,
            transaksi_id_text,
            default=[],
            on_error=lambda exc: self.log_warning(f"gagal memuat return items info: {exc}")
        )
        return {
            "header_data": header_data,
            "detail_rows": detail_rows,
            "voucher_info": voucher_info,
            "return_items": return_items,
        }

    # edited by glg
    def _run_async_detail_fetch(self, request_id, transaksi_id_text, retry_attempt=0):
        if not transaksi_id_text:
            return
        if not self._is_latest_detail_request(request_id):
            return

        def _worker():
            payload = self._fetch_detail_payload(transaksi_id_text)
            if hasattr(self.view, "emit_detail_payload"):
                self.view.emit_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._detail_cache_key(transaksi_id_text),
                apply_callback=lambda stale_payload: self._on_detail_payload_ready(request_id, stale_payload),
                request_id=request_id,
                is_latest_request=self._is_latest_detail_request,
                reason_code="HISTORY_DETAIL_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    f"[HISTORY_DETAIL_QUEUE_FULL_STALE_MISS] transaksi_id={str(transaksi_id_text or '').strip()}"
                )
                # edited by glg
                # Fallback sinkron sekali saat cache stale belum tersedia.
                payload = self._fetch_detail_payload(transaksi_id_text)
                self._on_detail_payload_ready(request_id, payload)
                return
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[HISTORY_DETAIL_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"history:detail_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_detail_fetch(
                    request_id,
                    transaksi_id_text=transaksi_id_text,
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=240,
            )

    # edited by glg
    def _on_detail_payload_ready(self, request_id, payload):
        if not self._is_latest_detail_request(request_id):
            return
        data = payload if isinstance(payload, dict) else {}
        selected_id = getattr(self, "transaksi_terpilih_id", None)
        if selected_id in (None, ""):
            header_data = data.get("header_data") if isinstance(data, dict) else {}
            if isinstance(header_data, dict):
                selected_id = header_data.get("id")
        self._get_async_stale_result_service().remember_payload(
            self._detail_cache_key(selected_id),
            data,
        )
        self.view.tampilkan_detail_transaksi(
            data.get("header_data"),
            data.get("detail_rows") or [],
            data.get("voucher_info") or {},
            data.get("return_items") or [],
        )

    def filter_by_preset(self, mode):
        """
        mode: today|yesterday|last7|last30
        """
        today = date.today()
        if mode == "today":
            start = end = today
        elif mode == "yesterday":
            start = end = today - timedelta(days=1)
        elif mode == "last7":
            end = today
            start = today - timedelta(days=6)
        elif mode == "last30":
            end = today
            start = today - timedelta(days=29)
        else:
            start = end = today
        self.start_date = start.isoformat()
        self.end_date = end.isoformat()
        self.view.set_date_inputs(self.start_date, self.end_date)
        self.load_history_transaksi()

    def filter_by_date_range(self, start_date, end_date):
        """
        start_date, end_date: string ISO yyyy-MM-dd
        """
        self.start_date = start_date
        self.end_date = end_date
        self.view.set_date_inputs(start_date, end_date)
        self.load_history_transaksi()

    def load_dipilih(self):
        # edited by glg
        # Controller history hanya untuk preview/reprint; aksi load diproses di controller transaksi.
        self.log_debug("load_dipilih dipanggil pada HistoryTransaksiController (no-op).")
        return None

    def print_ulang_transaksi(self, header_data, detail_rows):
        try:
            valid, message = self.reprint_service.validate_payload(
                transaksi_id=self.transaksi_terpilih_id,
                invoice_number=self.view.current_invoice_number,
                header_data=header_data,
                detail_rows=detail_rows,
            )
            if not valid:
                self.show_warning("Print Ulang", message)
                return
            success, warning_message = self.reprint_service.reprint(
                transaksi_id=self.transaksi_terpilih_id,
                invoice_number=self.view.current_invoice_number,
                header_data=header_data,
                detail_rows=detail_rows,
            )
            if success:
                self.show_info("Berhasil", "Struk berhasil di-print ulang!")
                return
            self.show_warning("Peringatan", warning_message)
        except Exception as e:
            self.log_error(f"Gagal print ulang struk: {e}")
            self.show_error("Error Print", "Gagal print ulang struk. Silakan coba lagi atau hubungi teknisi.")
