# edited by glg
import sqlite3
import time
import threading

from PySide6.QtCore import QTimer

from pypos.core.base_controller import BaseController
from pypos.core.utils.worker_pool_utils import (
    is_worker_queue_dropped,
    submit_ui_preview_task,
    submit_ui_query_task_keyed,
)
from pypos.modules.penjualan.models.load_transaksi_model import LoadTransaksiModel
from pypos.modules.penjualan.views.load_transaksi_view import LoadTransaksiView
from pypos.modules.penjualan.services.load_transaksi_service import LoadTransaksiService
from pypos.modules.penjualan.services.load_transaksi_apply_service import (
    LoadTransaksiApplyService,
)
from pypos.modules.penjualan.services.preorder_api_service import PreorderApiService
from pypos.modules.penjualan.services.async_stale_safe_result_service import (
    AsyncStaleSafeResultService,
)


class LoadTransaksiController(BaseController):
    def __init__(self, parent, transaksi_controller, db_path):
        super().__init__()
        self.model = LoadTransaksiModel(db_path)
        self.service = LoadTransaksiService()
        self.load_apply_service = LoadTransaksiApplyService()
        self.preorder_api_service = PreorderApiService()
        self.async_stale_result_service = AsyncStaleSafeResultService(
            log_info=self.log_info,
            log_warning=self.log_warning,
        )
        self.transaksi_controller = transaksi_controller
        self.bind_view(LoadTransaksiView(self), bind_back=False)
        self.parent = parent
        self.transaksi_terpilih_id = None
        self._row_contexts = []
        self._selected_context = None
        # edited by glg
        # Sequence preview async untuk mencegah detail lama menimpa pilihan terbaru.
        self._preview_request_seq = 0
        self._preview_request_lock = threading.Lock()
        # edited by glg
        # Sequence load list async untuk mencegah hasil preload lama menimpa request terbaru.
        self._list_request_seq = 0
        self._list_request_lock = threading.Lock()
        # edited by glg
        # Circuit-breaker ringan untuk endpoint preorder agar dialog F10 tidak freeze berulang.
        self._remote_disabled_until = 0.0
        self._remote_last_reason = ""
        if hasattr(self.view, "set_list_payload_handler"):
            self.view.set_list_payload_handler(self._on_list_payload_ready)
        if hasattr(self.view, "set_preview_payload_handler"):
            self.view.set_preview_payload_handler(self._on_preview_payload_ready)
        self.load_list_transaksi()

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

    # edited by glg
    def _preview_cache_key(self, parsed_id):
        return f"load_transaksi:preview:{id(self)}:{int(parsed_id or 0)}"

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

        QTimer.singleShot(retry_delay, _run)
        return True

    def set_filter(self, user_id=None, tanggal=None):
        rows = self.safe_call(
            self.model.get_filtered_transaksi_list,
            user_id=user_id,
            tanggal=tanggal,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal filter transaksi tersimpan: {exc}"),
        )
        display_rows, row_contexts = self.service.build_local_display_rows(rows)
        self._row_contexts = row_contexts
        self._selected_context = None
        self.view.render_transaksi_list(display_rows)

    def show(self):
        return self.view.exec()

    # edited by glg
    def _get_user_info(self):
        info = getattr(self.transaksi_controller, "user_info", None)
        return info if isinstance(info, dict) else {}

    # edited by glg
    def _get_context_by_row(self, row_index):
        try:
            idx = int(row_index)
        except (TypeError, ValueError):
            return None
        if idx < 0 or idx >= len(self._row_contexts):
            return None
        context = self._row_contexts[idx]
        return context if isinstance(context, dict) else None

    # edited by glg
    def _load_remote_preorder_items(self):
        now_ts = time.time()
        if now_ts < float(getattr(self, "_remote_disabled_until", 0.0) or 0.0):
            return []

        result = self.safe_call(
            self.preorder_api_service.get_preorder_list,
            user_info=self._get_user_info(),
            default={},
            on_error=lambda exc: self.log_warning(f"Gagal ambil daftar preorder API: {exc}"),
        )
        items, reason = self.service.parse_remote_preorder_list(result)
        if items:
            self._remote_disabled_until = 0.0
            self._remote_last_reason = ""
            return items

        reason_text = str(reason or "").strip()
        if not reason_text:
            return []

        if self.service.is_remote_unavailable_reason(reason_text):
            cooldown = self.service.get_preorder_disable_cooldown_sec(default=120)
            self._remote_disabled_until = time.time() + float(cooldown)
            if reason_text != self._remote_last_reason:
                self.log_warning(
                    f"Preorder API unavailable ({reason_text}). "
                    f"Fallback lokal aktif selama {cooldown} detik."
                )
                self._remote_last_reason = reason_text
        elif reason_text != "data preorder kosong":
            if reason_text != self._remote_last_reason:
                self.log_warning(f"Preorder API tidak aktif/invalid: {reason_text}")
                self._remote_last_reason = reason_text
        return []

    def _load_remote_selected_preorder(self, preorder_item):
        preorder = preorder_item if isinstance(preorder_item, dict) else {}
        preorder_id = str(preorder.get("id") or "").strip()
        otp = str(preorder.get("otp") or "").strip()
        if not preorder_id:
            self.show_warning("PreOrder", "ID pre-order tidak valid.")
            return False
        if not otp:
            self.show_warning("PreOrder", "OTP pre-order tidak tersedia.")
            return False

        result = self.safe_call(
            self.preorder_api_service.use_preorder,
            preorder_id=preorder_id,
            otp=otp,
            user_info=self._get_user_info(),
            default={"status": 0, "reason": "request_failed"},
            on_error=lambda exc: self.log_warning(f"Gagal use_preorder id={preorder_id}: {exc}"),
        )
        parsed, reason = self.service.parse_use_preorder_result(result)
        if not parsed:
            self.show_warning("PreOrder", f"Gagal memuat pre-order: {reason or 'unknown_error'}")
            return False

        self.transaksi_controller.load_transaksi_ke_view(
            parsed["detail_rows"],
            parsed["customers_id"],
            parsed["customers_nama"],
            parsed["diskon"],
            parsed["ppn"],
            parsed["total_harga"],
            preorder_payload=parsed.get("preorder_payload"),
        )
        return True

    # edited by glg
    def _next_list_request_id(self):
        with self._list_request_lock:
            self._list_request_seq += 1
            return int(self._list_request_seq)

    # edited by glg
    def _is_latest_list_request(self, request_id):
        with self._list_request_lock:
            return int(request_id or 0) == int(self._list_request_seq or 0)

    # edited by glg
    def _build_list_payload(self):
        local_rows = self.safe_call(
            self.model.get_tersimpan_transaksi_list,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat list transaksi tersimpan: {exc}"),
        )
        remote_items = self._load_remote_preorder_items()
        display_rows, row_contexts = self.service.combine_display_rows(
            remote_items=remote_items,
            local_rows=local_rows,
        )
        return {
            "display_rows": display_rows or [],
            "row_contexts": row_contexts or [],
        }

    # edited by glg
    def _apply_list_payload(self, payload):
        data = payload if isinstance(payload, dict) else {}
        display_rows = data.get("display_rows") or []
        row_contexts = data.get("row_contexts") or []
        self._get_async_stale_result_service().remember_payload(self._list_cache_key(), data)
        self._row_contexts = row_contexts
        self._selected_context = None
        self.view.render_transaksi_list(display_rows)
        self.transaksi_terpilih_id = None
        if hasattr(self.view, "reset_preview"):
            self.view.reset_preview()
        if hasattr(self.view, "table"):
            self.view.table.clearSelection()

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

        def _worker():
            payload = self._build_list_payload()
            if hasattr(self.view, "emit_list_payload"):
                self.view.emit_list_payload(request_id, payload)

        key = f"load-transaksi-list:{id(self)}"
        future = submit_ui_query_task_keyed(key, _worker)
        # edited by glg
        # Fail-safe antrean penuh: pakai payload stale + jadwalkan 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._list_cache_key(),
                apply_callback=self._apply_list_payload,
                request_id=request_id,
                is_latest_request=self._is_latest_list_request,
                reason_code="LOAD_TRX_LIST_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    "[LOAD_TRX_LIST_QUEUE_FULL_STALE_MISS] antrean penuh dan cache belum tersedia."
                )
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[LOAD_TRX_LIST_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"load_transaksi:list_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_list_fetch(
                    request_id,
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=260,
            )

    # edited by glg
    def _on_list_payload_ready(self, request_id, payload):
        if not self._is_latest_list_request(request_id):
            return
        self._apply_list_payload(payload)

    def load_list_transaksi(self, force_sync=False):
        if bool(force_sync):
            payload = self._build_list_payload()
            self._apply_list_payload(payload)
            return
        request_id = self._next_list_request_id()
        self._run_async_list_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 (AttributeError, RuntimeError, TypeError):
            return ""

    def transaksi_diklik(self, row, column):
        _ = column
        context = self._get_context_by_row(row)
        self._selected_context = context
        if context and str(context.get("source") or "") == "api":
            self._invalidate_preview_requests()
            self.transaksi_terpilih_id = None
            if hasattr(self.view, "tampilkan_preview_preorder_summary"):
                self.view.tampilkan_preview_preorder_summary(context.get("preorder"))
            elif hasattr(self.view, "reset_preview"):
                self.view.reset_preview()
            return

        parsed_id = None
        if context and str(context.get("source") or "") == "local":
            parsed_id = context.get("transaksi_id")
        if not parsed_id:
            transaksi_id_text = self._safe_cell_text(row, 0)
            parsed_id = self.service.parse_transaksi_id(transaksi_id_text)
        else:
            transaksi_id_text = str(parsed_id)
        if parsed_id is None:
            self._invalidate_preview_requests()
            self.transaksi_terpilih_id = None
            if transaksi_id_text:
                self.log_warning(f"ID transaksi tidak valid pada row={row}: {transaksi_id_text}")
            if hasattr(self.view, "reset_preview"):
                self.view.reset_preview()
            return

        self.transaksi_terpilih_id = parsed_id
        request_id = self._next_preview_request_id()
        self._run_async_preview_fetch(request_id, int(parsed_id))

    # edited by glg
    def _invalidate_preview_requests(self):
        with self._preview_request_lock:
            self._preview_request_seq += 1

    # edited by glg
    def _next_preview_request_id(self):
        with self._preview_request_lock:
            self._preview_request_seq += 1
            return int(self._preview_request_seq)

    # edited by glg
    def _is_latest_preview_request(self, request_id):
        with self._preview_request_lock:
            return int(request_id or 0) == int(self._preview_request_seq or 0)

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

    # edited by glg
    def _run_async_preview_fetch(self, request_id, parsed_id, retry_attempt=0):
        if not self._is_latest_preview_request(request_id):
            return

        def _worker():
            payload = self._fetch_preview_payload(parsed_id)
            if hasattr(self.view, "emit_preview_payload"):
                self.view.emit_preview_payload(request_id, payload)

        future = submit_ui_preview_task(_worker)
        # edited by glg
        # Fail-safe antrean preview penuh: gunakan stale cache + retry async.
        if is_worker_queue_dropped(future):
            stale_used = self._get_async_stale_result_service().apply_stale_payload(
                cache_key=self._preview_cache_key(parsed_id),
                apply_callback=lambda stale_payload: self._on_preview_payload_ready(request_id, stale_payload),
                request_id=request_id,
                is_latest_request=self._is_latest_preview_request,
                reason_code="LOAD_TRX_PREVIEW_QUEUE_FULL_STALE",
            )
            if not stale_used:
                self.log_info(
                    f"[LOAD_TRX_PREVIEW_QUEUE_FULL_STALE_MISS] parsed_id={int(parsed_id or 0)}"
                )
                # edited by glg
                # Fallback sinkron sekali saat cache stale belum tersedia.
                payload = self._fetch_preview_payload(parsed_id)
                self._on_preview_payload_ready(request_id, payload)
                return
            if int(retry_attempt or 0) >= 3:
                self.log_warning(
                    "[LOAD_TRX_PREVIEW_QUEUE_FULL_RETRY_EXHAUSTED] retry dihentikan untuk mencegah loop."
                )
                return
            self._schedule_stale_retry(
                schedule_key=f"load_transaksi:preview_retry:{id(self)}:{int(request_id or 0)}",
                callback=lambda: self._run_async_preview_fetch(
                    request_id,
                    int(parsed_id or 0),
                    retry_attempt=int(retry_attempt or 0) + 1,
                ),
                delay_ms=240,
            )

    # edited by glg
    def _on_preview_payload_ready(self, request_id, payload):
        if not self._is_latest_preview_request(request_id):
            return
        data = payload if isinstance(payload, dict) else {}
        parsed_id = int(data.get("parsed_id") or 0)
        if parsed_id <= 0:
            return
        if int(self.transaksi_terpilih_id or 0) != parsed_id:
            return
        self._get_async_stale_result_service().remember_payload(self._preview_cache_key(parsed_id), data)
        self.view.tampilkan_preview_detail(data.get("detail_rows") or [])

    # edited by glg
    def _load_selected_api_context(self, context):
        if not context or str(context.get("source") or "") != "api":
            return False
        if self._load_remote_selected_preorder(context.get("preorder")):
            self.view.accept()
        return True

    # edited by glg
    def _fetch_local_load_payload(self, transaksi_id):
        normalized_id = int(transaksi_id or 0)
        header = self.safe_call(
            self.model.get_transaksi_header,
            normalized_id,
            default=None,
            on_error=lambda exc: self.log_warning(
                f"Gagal mengambil header transaksi {normalized_id}: {exc}"
            ),
        )
        detail_rows = self.safe_call(
            self.model.get_detail_transaksi,
            normalized_id,
            default=[],
            on_error=lambda exc: self.log_warning(
                f"Gagal mengambil detail transaksi {normalized_id}: {exc}"
            ),
        )
        raw_transaksi = self.safe_call(
            self.model.get_transaksi_row,
            normalized_id,
            default=None,
            on_error=lambda exc: self.log_warning(
                f"Gagal mengambil raw transaksi {normalized_id}: {exc}"
            ),
        )
        return self.load_apply_service.build_local_load_payload(
            transaksi_id=normalized_id,
            header=header,
            detail_rows=detail_rows,
            raw_transaksi=raw_transaksi,
            parse_header=self.service.parse_header,
            build_preorder_payload=self.service.build_preorder_payload,
        )

    # edited by glg
    def _apply_local_load_payload_to_view(self, payload):
        data = payload if isinstance(payload, dict) else {}
        transaksi_id = self.service.parse_transaksi_id(data.get("transaksi_id")) or 0
        try:
            self.transaksi_controller.load_transaksi_ke_view(
                data.get("detail_rows") or [],
                data.get("customers_id"),
                data.get("customers_nama"),
                data.get("diskon"),
                data.get("ppn"),
                data.get("total_harga"),
                preorder_payload=data.get("preorder_payload"),
            )
            return True
        except (AttributeError, RuntimeError, TypeError, ValueError, KeyError) as exc:
            self.log_error(f"Gagal memuat transaksi {transaksi_id} ke view: {exc}")
            self.show_warning("Load Gagal", "Transaksi gagal dimuat ke form penjualan.")
            return False

    # edited by glg
    def _cleanup_loaded_transaksi(self, transaksi_id):
        normalized_id = self.service.parse_transaksi_id(transaksi_id) or 0
        if normalized_id <= 0:
            self.log_warning("Cleanup transaksi dilewati karena transaksi_id tidak valid.")
            self.show_warning(
                "Load Parsial",
                "Transaksi berhasil dimuat, tetapi data tersimpan gagal dihapus.\n"
                "Silakan hapus manual dari daftar tersimpan.",
            )
            self.load_list_transaksi(force_sync=True)
            return False
        try:
            self.model.delete_transaksi_by_id(normalized_id)
            return True
        except ValueError as exc:
            self.log_warning(f"Cleanup transaksi ditolak id={normalized_id}: {exc}")
        except (sqlite3.Error, RuntimeError, TypeError) as exc:
            self.log_error(f"Gagal cleanup transaksi tersimpan {normalized_id}: {exc}")
        self.show_warning(
            "Load Parsial",
            "Transaksi berhasil dimuat, tetapi data tersimpan gagal dihapus.\n"
            "Silakan hapus manual dari daftar tersimpan.",
        )
        self.load_list_transaksi(force_sync=True)
        return False

    def load_dipilih(self):
        context = self._selected_context
        if self._load_selected_api_context(context):
            return

        selected_id = self.service.parse_transaksi_id(self.transaksi_terpilih_id)
        if selected_id is None:
            self.show_warning("Pilih Transaksi", "Pilih transaksi untuk diload.")
            return

        payload = self._fetch_local_load_payload(selected_id)
        if not bool(payload.get("ok")):
            self.show_warning("Load Gagal", "Data transaksi tidak ditemukan atau header tidak lengkap.")
            return

        if not self._apply_local_load_payload_to_view(payload):
            return

        if not self._cleanup_loaded_transaksi(payload.get("transaksi_id")):
            return

        self.view.accept()

    def hapus_dipilih(self):
        context = self._selected_context
        if context and str(context.get("source") or "") == "api":
            self.show_warning(
                "Aksi Tidak Tersedia",
                "Pre-order dari API tidak bisa dihapus dari POS.\nGunakan proses di web admin bila diperlukan.",
            )
            return

        if not self.transaksi_terpilih_id:
            self.show_warning("Pilih Transaksi", "Pilih transaksi untuk dihapus.")
            return

        nomer = None
        try:
            row = self.view.table.currentRow()
            if row >= 0:
                nomer_item = self.view.table.item(row, 1)
                if nomer_item:
                    nomer = nomer_item.text()
        except (AttributeError, RuntimeError, TypeError) as exc:
            self.log_warning(f"Gagal membaca nomor transaksi di tabel: {exc}")

        label = nomer or str(self.transaksi_terpilih_id)
        if not self.ask_confirm("Konfirmasi Hapus", f"Yakin ingin menghapus transaksi tersimpan:\n{label}?"):
            return

        try:
            self.model.delete_transaksi_by_id(self.transaksi_terpilih_id)
        except ValueError as exc:
            self.show_warning("Hapus Ditolak", str(exc))
            self.log_warning(f"Hapus transaksi ditolak id={self.transaksi_terpilih_id}: {exc}")
            return
        except (sqlite3.Error, RuntimeError, TypeError) as exc:
            self.show_warning("Hapus Gagal", "Gagal menghapus transaksi. Silakan coba lagi.")
            self.log_error(f"Gagal menghapus transaksi {self.transaksi_terpilih_id}: {exc}")
            return

        self.load_list_transaksi()
        self.show_info("Berhasil", "Transaksi berhasil dihapus.")
