from pypos.core.base_controller import BaseController
from datetime import datetime
import uuid

from pypos.core.utils.path_utils import get_db_path
from pypos.modules.penjualan.errors import ReturnProcessError
from pypos.modules.penjualan.models.return_model import ReturnItem, ReturnModel
from pypos.modules.penjualan.models.settlement_model import SettlementModel
from pypos.modules.penjualan.services.admin_authorization_service import AdminAuthorizationService
from pypos.modules.penjualan.services.penjualan_config_service import PenjualanConfigService
from pypos.modules.penjualan.services.return_policy_service import ReturnPolicyService
from pypos.modules.penjualan.services.settlement_mutation_lock_service import (
    SettlementMutationLockService,
)
from pypos.modules.printer.controllers.printer_settings_controller import PrinterSettingsController
from pypos.modules.printer.services.return_voucher_print_service import ReturnVoucherPrinter


class ReturnController(BaseController):
    def __init__(
        self,
        model: ReturnModel = None,
        settlement_model: SettlementModel = None,
        printer_settings_controller: PrinterSettingsController = None,
    ):
        super().__init__()
        resolved_db_path = (
            getattr(model, "db_path", None)
            or getattr(settlement_model, "db_path", None)
            or get_db_path()
        )
        self.model = model or ReturnModel(db_path=resolved_db_path)
        self.settlement_model = settlement_model or SettlementModel(db_path=resolved_db_path)
        self.printer_settings_controller = printer_settings_controller or PrinterSettingsController()
        self.penjualan_config_service = PenjualanConfigService(db_path=resolved_db_path)
        self.admin_authorization_service = AdminAuthorizationService(self.settlement_model)
        self.return_policy_service = ReturnPolicyService()
        # edited by glg
        # Shared lock validator agar rule settlement return/pembatalan konsisten.
        self.settlement_lock_service = SettlementMutationLockService(
            db_path=resolved_db_path,
            log_warning=self.log_warning,
        )

    # edited by glg
    @staticmethod
    def _generate_trace_id(scope: str = "return") -> str:
        prefix = str(scope or "return").strip().lower().replace(" ", "_")
        stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")[-12:]
        suffix = uuid.uuid4().hex[:8]
        return f"{prefix}-{stamp}-{suffix}"

    def get_allowed_date_range(self):
        allowed_days = self.penjualan_config_service.get_return_allowed_days(default=0)
        return self.return_policy_service.get_allowed_date_range(allowed_days)

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

    # edited by glg
    def _to_row_dict(self, row):
        if isinstance(row, dict):
            return dict(row)
        try:
            return dict(row)
        except (TypeError, ValueError):
            return {}

    # edited by glg
    def _get_settlement_lock_map(self, transaksi_ids):
        normalized_ids = []
        for raw_id in list(transaksi_ids or []):
            parsed = self._to_positive_int(raw_id, 0)
            if parsed > 0:
                normalized_ids.append(int(parsed))
        try:
            return self.settlement_lock_service.get_lock_map(normalized_ids)
        except (RuntimeError, TypeError, ValueError) as exc:
            self.log_warning(f"Gagal memuat lock settlement return: {exc}")
            # edited by glg
            # Fail-safe: jika lock map gagal, blokir mutation seluruh transaksi target.
            return {int(trx_id): True for trx_id in normalized_ids}

    # edited by glg
    def _is_transaksi_settled_locked(self, transaksi_id):
        normalized_id = self._to_positive_int(transaksi_id, 0)
        if normalized_id <= 0:
            return False
        lock_map = self._get_settlement_lock_map([normalized_id])
        return bool(lock_map.get(normalized_id, False))

    def search_transaksi_master(self, keyword=""):
        limit = self.penjualan_config_service.get_return_search_limit(default=200)
        start_date, end_date = self.get_allowed_date_range()
        rows = self.safe_call(
            self.model.search_transaksi_master,
            keyword=keyword,
            start_date=start_date.strftime("%Y-%m-%d"),
            end_date=end_date.strftime("%Y-%m-%d"),
            limit=limit,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat daftar transaksi return: {exc}"),
        )
        row_dicts = [self._to_row_dict(row) for row in (rows or [])]
        transaksi_ids = []
        for row in row_dicts:
            transaksi_id = self._to_positive_int(row.get("id"), 0)
            if transaksi_id > 0:
                transaksi_ids.append(transaksi_id)
        lock_map = self._get_settlement_lock_map(transaksi_ids)
        return [
            row
            for row in row_dicts
            if not lock_map.get(self._to_positive_int(row.get("id"), 0), False)
        ]

    def load_transaksi_detail(self, transaksi_id):
        return self.safe_call(
            self.model.load_transaksi_detail,
            transaksi_id,
            default=[],
            on_error=lambda exc: self.log_warning(f"Gagal memuat detail transaksi return: {exc}"),
        )

    def verify_admin_authorization(self, admin_name, password):
        return self.admin_authorization_service.validate_settlement_admin(admin_name, password)

    def _build_return_items(self, selected_items):
        items = []
        for row in selected_items or []:
            produk_id = str(row.get("produk_id", "")).strip()
            produk_nama = str(row.get("produk_nama", "") or "").strip()
            qty_return = int(row.get("qty_return") or 0)
            harga = float(row.get("harga") or 0)
            jenis_return = str(row.get("jenis_return") or "partial")
            if not produk_id or qty_return <= 0:
                continue
            items.append(
                ReturnItem(
                    produk_id=produk_id,
                    produk_nama=produk_nama,
                    jumlah=qty_return,
                    harga=harga,
                    jenis_return=jenis_return,
                )
            )
        return items

    def proses_return(self, transaksi_id, selected_items, refund_method="cash", jenis_return="partial"):
        trace_id = self._generate_trace_id("return")
        transaksi_id = str(transaksi_id or "").strip()
        if not transaksi_id:
            return {
                "ok": False,
                "message": "Pilih transaksi terlebih dahulu.",
                "error_code": "RETURN_INVALID_TRANSAKSI_ID",
                "reason": "invalid_transaksi_id",
                "trace_id": trace_id,
            }
        if self._is_transaksi_settled_locked(transaksi_id):
            return {
                "ok": False,
                "message": "Transaksi sudah disettle dan tidak dapat diproses return.",
                "error_code": "RETURN_SETTLEMENT_LOCKED",
                "reason": "settlement_locked",
                "trace_id": trace_id,
            }

        items = self._build_return_items(selected_items)
        if not items:
            return {
                "ok": False,
                "message": "Belum ada qty return yang valid.",
                "error_code": "RETURN_INVALID_QTY",
                "reason": "invalid_qty_return",
                "trace_id": trace_id,
            }

        total_refund = float(sum((item.harga or 0) * (item.jumlah or 0) for item in items))
        refund_method = self.return_policy_service.normalize_refund_method(refund_method)

        try:
            kode_return = self.model.insert_return(
                transaksi_id=transaksi_id,
                items=items,
                jenis_return=jenis_return,
                refund_method=refund_method,
                trace_id=trace_id,
            )
        except ReturnProcessError as exc:
            error_code = str(getattr(exc, "code", "RETURN_PROCESS_ERROR") or "RETURN_PROCESS_ERROR")
            error_message = str(getattr(exc, "message", "") or "").strip()
            self.log_error(f"Gagal proses return [{error_code}] trace_id={trace_id}: {exc}")
            if error_message:
                return {
                    "ok": False,
                    "message": f"{error_message} (Kode: {error_code})",
                    "error_code": error_code,
                    "reason": "return_process_error",
                    "trace_id": trace_id,
                }
            return {
                "ok": False,
                "message": f"Gagal memproses return. (Kode: {error_code})",
                "error_code": error_code,
                "reason": "return_process_error",
                "trace_id": trace_id,
            }
        except (TypeError, ValueError) as exc:
            error_code = "RETURN_CONTROLLER_DATA_ERROR"
            self.log_error(f"Gagal proses return [{error_code}] trace_id={trace_id}: {exc}")
            return {
                "ok": False,
                "message": f"Data return tidak valid. (Kode: {error_code})",
                "error_code": error_code,
                "reason": "invalid_return_data",
                "trace_id": trace_id,
            }
        except RuntimeError as exc:
            error_code = "RETURN_CONTROLLER_RUNTIME_ERROR"
            self.log_error(f"Gagal proses return [{error_code}] trace_id={trace_id}: {exc}")
            return {
                "ok": False,
                "message": f"Gagal memproses return. (Kode: {error_code})",
                "error_code": error_code,
                "reason": "runtime_error",
                "trace_id": trace_id,
            }
        except AttributeError as exc:
            error_code = "RETURN_CONTROLLER_UNEXPECTED_ERROR"
            self.log_error(f"Gagal proses return [{error_code}] trace_id={trace_id}: {exc}")
            return {
                "ok": False,
                "message": f"Gagal memproses return. (Kode: {error_code})",
                "error_code": error_code,
                "reason": "unexpected_error",
                "trace_id": trace_id,
            }

        return {
            "ok": True,
            "kode_return": kode_return,
            "total_refund": total_refund,
            "refund_method": refund_method,
            "trace_id": trace_id,
        }

    def cetak_voucher_return(self, kode_return, refund_method, parent=None):
        if str(refund_method or "").lower().strip() != "voucher":
            return True
        try:
            printer = ReturnVoucherPrinter(self.model.conn, kode_return)
            return bool(printer.print_by_settings(self.printer_settings_controller, parent=parent))
        except (RuntimeError, TypeError, AttributeError) as exc:
            self.log_warning(f"Gagal cetak voucher return: {exc}")
            return False

    # edited by glg
    def dispose(self):
        try:
            if self.model and hasattr(self.model, "close"):
                self.model.close()
        except (RuntimeError, TypeError, AttributeError) as exc:
            self.log_warning(f"Gagal dispose ReturnController: {exc}")
        return super().dispose()
