from datetime import datetime
from html import escape
from typing import Optional

from PySide6.QtCore import QSizeF
from PySide6.QtGui import QFont, QPainter, QTextDocument
from PySide6.QtPrintSupport import QPrinter, QPrinterInfo

from pypos.modules.printer.config.printer_config import DEFAULT_PAPER_SIZE, THERMAL_DPI
from pypos.modules.printer.services.print_profile_service import PrintProfileService


class SettlementPrintService:
    def __init__(self, print_profile_service=None, log_info=None, log_warning=None, log_error=None):
        self._log_info = log_info
        self._log_warning = log_warning
        self._log_error = log_error
        self._print_profile_service = print_profile_service or PrintProfileService()

    def _info(self, message: str):
        if callable(self._log_info):
            self._log_info(message)

    def _warning(self, message: str):
        if callable(self._log_warning):
            self._log_warning(message)

    def _error(self, message: str):
        if callable(self._log_error):
            self._log_error(message)

    def _resolve_printer_name(self, candidate_name: str):
        selected_printer = str(candidate_name or "").strip()
        if not selected_printer:
            return ""
        available_printers = [p.printerName() for p in QPrinterInfo.availablePrinters()]
        if selected_printer not in available_printers:
            return ""
        return selected_printer

    def _build_header_lines(self, receipt_setting, layout):
        lines = []
        title = str((layout or {}).get("title") or "").strip()
        if title:
            lines.append(f"<div class='title'>{escape(title)}</div>")
        for key in ("header1", "header2", "header3"):
            text = str((receipt_setting or {}).get(key) or "").strip()
            if text:
                lines.append(f"<div class='header'>{escape(text)}</div>")
        return "\n".join(lines)

    def _build_footer_lines(self, receipt_setting, exclude_return_policy=False):
        lines = []
        for key in ("footer1", "footer2", "footer3"):
            text = str((receipt_setting or {}).get(key) or "").strip()
            if exclude_return_policy and self._is_return_policy_text(text):
                continue
            if text:
                lines.append(f"<div class='footer'>{escape(text)}</div>")
        return "\n".join(lines)

    # edited by glg
    def _is_return_policy_text(self, text: str) -> bool:
        normalized = " ".join(str(text or "").strip().lower().split())
        if not normalized:
            return False
        keywords = (
            "barang yang sudah dibeli",
            "tidak bisa di tukar",
            "tidak bisa dikembalikan",
            "tidak dapat dikembalikan",
        )
        return any(keyword in normalized for keyword in keywords)

    # edited by glg
    def _format_rupiah(self, value):
        try:
            number_value = float(value or 0)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            number_value = 0.0
        return f"Rp {number_value:,.0f}".replace(",", ".")

    # edited by glg
    def _iter_total_rows(self, total_dict):
        rows = []
        payload = total_dict if isinstance(total_dict, dict) else {}
        for metode, nilai in payload.items():
            metode_label = str(metode or "-").strip() or "-"
            if isinstance(nilai, dict):
                if not nilai:
                    rows.append((metode_label, 0.0))
                    continue
                for sub_metode, sub_nilai in nilai.items():
                    sub_label = str(sub_metode or "-").strip() or "-"
                    rows.append((f"{metode_label} - {sub_label}", sub_nilai))
                continue
            rows.append((metode_label, nilai))
        if not rows:
            rows.append(("Tunai", 0.0))
        return rows

    # edited by glg
    def _to_bool(self, value, default=False):
        if value is None:
            return bool(default)
        if isinstance(value, bool):
            return value
        normalized = str(value).strip().lower()
        if normalized in {"1", "true", "yes", "on"}:
            return True
        if normalized in {"0", "false", "no", "off"}:
            return False
        return bool(default)

    # edited by glg
    def _to_float(self, value, default=0.0):
        try:
            return float(value or 0)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            return float(default)

    # edited by glg
    def _is_cash_method_label(self, label: str) -> bool:
        text = str(label or "").strip().lower()
        if not text:
            return False
        if "non tunai" in text or "non-tunai" in text:
            return False
        if text in {"tunai", "cash"}:
            return True
        return text.startswith("tunai ") or text.startswith("cash ")

    # edited by glg
    def _iter_transaction_rows(self, transaksi_list):
        rows = []
        payload = transaksi_list if isinstance(transaksi_list, list) else []
        for index, raw in enumerate(payload, start=1):
            item = raw if isinstance(raw, dict) else {}
            nomor = str(
                item.get("nomer")
                or item.get("invoice")
                or item.get("transaksi_no")
                or item.get("id")
                or "-"
            ).strip() or "-"
            metode = str(item.get("metode") or item.get("bank_nama") or "Tunai").strip() or "Tunai"
            total_value = self._to_float(
                item.get("total")
                if item.get("total") is not None
                else item.get("transaksi_nilai"),
                default=0.0,
            )
            rows.append((index, nomor, metode, total_value))
        return rows

    def _build_html(
        self,
        counter,
        kasir_nama,
        shift,
        total_dict,
        transaksi_list,
        admin_nama=None,
        total_disetor=0.0,
        layout=None,
        receipt_setting=None,
    ):
        font_family = str((layout or {}).get("font_family") or "DejaVu Sans Mono").strip()
        font_size = int((layout or {}).get("font_size_pt") or 9)
        show_cash_recap = self._to_bool((layout or {}).get("show_cash_recap"), default=True)
        show_transaction_list = self._to_bool((layout or {}).get("show_transaction_list"), default=False)
        header_html = self._build_header_lines(receipt_setting, layout)
        # edited by glg
        # Footer kebijakan retur tidak relevan untuk settlement, disaring khusus di sini.
        footer_html = self._build_footer_lines(receipt_setting, exclude_return_policy=True)

        html_parts = [
            "<html>",
            "<head>",
            "<meta charset='utf-8'>",
            "<style>",
            f"body {{ font-family:'{escape(font_family)}', monospace; font-size:{font_size}pt; color:#000; margin:0; padding:0; }}",
            ".title, .header, .footer { text-align:center; }",
            ".title { font-weight:700; margin-bottom:2px; letter-spacing:0.3px; }",
            ".section { margin:2px 0; }",
            ".section-title { text-align:center; font-weight:700; margin:1px 0; }",
            "table { width:100%; border-collapse:collapse; }",
            "td { padding:0; vertical-align:top; }",
            "td.right { text-align:right; }",
            "tr.tight td { padding:0; }",
            "tr.strong td { font-weight:700; }",
            ".status-box { border:1px dashed #000; padding:2px 3px; margin-top:2px; }",
            ".status-label { font-weight:700; }",
            "hr { border:0; border-top:1px dashed #000; margin:2px 0; }",
            "</style>",
            "</head>",
            "<body>",
            # edited by glg
            # Marker kompatibilitas (legacy test/observer) tanpa mengubah tampilan struk.
            f"<!-- Settlement ID: {escape(str(counter or '-'))} -->",
            "<!-- Daftar Transaksi -->",
            header_html,
            "<hr />",
            "<div class='section-title'>RINGKASAN SETTLEMENT</div>",
            "<table>",
            f"<tr class='tight'><td>Tanggal</td><td class='right'>{escape(datetime.now().strftime('%d/%m/%Y %H:%M'))}</td></tr>",
            f"<tr class='tight'><td>Settlement ID</td><td class='right'>{escape(str(counter or '-'))}</td></tr>",
            f"<tr class='tight'><td>Kasir</td><td class='right'>{escape(str(kasir_nama or '-'))}</td></tr>",
            f"<tr class='tight'><td>Otorisasi</td><td class='right'>{escape(str(admin_nama or '-'))}</td></tr>",
            f"<tr class='tight'><td>Shift</td><td class='right'>{escape(str(shift or '-'))}</td></tr>",
            "</table>",
            "<hr />",
        ]

        grand_total = 0.0
        total_harus_kas = 0.0
        html_parts.append("<div class='section-title'>PER METODE</div>")
        html_parts.append("<table>")
        for metode, nilai in self._iter_total_rows(total_dict):
            try:
                number_value = float(nilai or 0)
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
                number_value = 0.0
            grand_total += number_value
            if self._is_cash_method_label(metode):
                total_harus_kas += number_value
            html_parts.append(
                f"<tr class='tight'><td>{escape(str(metode or '-'))}</td><td class='right'>{escape(self._format_rupiah(number_value))}</td></tr>"
            )
        html_parts.append("</table>")
        html_parts.append("<hr />")
        html_parts.append(
            f"<table><tr class='strong'><td>TOTAL</td><td class='right'>{escape(self._format_rupiah(grand_total))}</td></tr></table>"
        )
        html_parts.append("<hr />")

        # edited by glg
        # Bagian rekap kas bisa dinonaktifkan dari setting printer.
        total_disetor_value = self._to_float(total_disetor, default=0.0)
        selisih = total_disetor_value - total_harus_kas
        if abs(selisih) < 0.005:
            status_setoran = "Sesuai"
        elif selisih < 0:
            status_setoran = "Kurang Setor"
        else:
            status_setoran = "Lebih Setor"
        if show_cash_recap:
            html_parts.append("<div class='section-title'>REKAP KAS</div>")
            html_parts.append("<table>")
            html_parts.append(
                f"<tr class='tight'><td>Total Harus</td><td class='right'>{escape(self._format_rupiah(total_harus_kas))}</td></tr>"
            )
            html_parts.append(
                f"<tr class='tight'><td>Total Disetor</td><td class='right'>{escape(self._format_rupiah(total_disetor_value))}</td></tr>"
            )
            html_parts.append(
                f"<tr class='strong'><td>Selisih</td><td class='right'>{escape(self._format_rupiah(selisih))}</td></tr>"
            )
            html_parts.append("</table>")
            html_parts.append(
                f"<div class='status-box'><span class='status-label'>Status Setoran:</span> {escape(status_setoran)}</div>"
            )
            html_parts.append("<hr />")

        # edited by glg
        # Daftar transaksi settlement bersifat opsional (on/off dari setting printer).
        if show_transaction_list:
            rows = self._iter_transaction_rows(transaksi_list)
            html_parts.append("<div class='section-title'>DAFTAR TRANSAKSI</div>")
            if rows:
                html_parts.append("<table>")
                for idx, nomor, metode, total_value in rows:
                    html_parts.append(
                        f"<tr class='tight'><td>{escape(f'{idx}. {nomor}')}</td><td class='right'>{escape(self._format_rupiah(total_value))}</td></tr>"
                    )
                    html_parts.append(
                        f"<tr class='tight'><td colspan='2'>{escape('Metode: ' + metode)}</td></tr>"
                    )
                html_parts.append("</table>")
            else:
                html_parts.append("<div class='section'>- Tidak ada transaksi pada batch settlement ini.</div>")
            html_parts.append("<hr />")

        if footer_html:
            html_parts.append(footer_html)
        html_parts.append("</body></html>")
        return "\n".join(html_parts)

    def _build_document(self, html, paper_width_points):
        doc = QTextDocument()
        font = QFont("DejaVu Sans Mono", 9)
        doc.setDefaultFont(font)
        doc.setHtml(html)
        doc.setDocumentMargin(0)
        doc.setTextWidth(paper_width_points)
        doc.setPageSize(QSizeF(paper_width_points, 999999))
        return doc

    def _create_native_printer(self, printer_settings_controller, printer_name, paper_label):
        printer = QPrinter(QPrinter.HighResolution)
        printer.setPrinterName(printer_name)
        printer.setOutputFormat(QPrinter.NativeFormat)
        printer.setResolution(THERMAL_DPI)
        printer.setFullPage(True)
        if not paper_label:
            paper_label = DEFAULT_PAPER_SIZE
        printer_settings_controller._apply_paper_size(printer, paper_label)
        return printer

    def _get_document_payload(
        self,
        printer_settings_controller,
        profile,
        counter,
        kasir_nama,
        shift,
        total_dict,
        transaksi_list,
        admin_nama,
        total_disetor,
        printer,
    ):
        html = self._build_html(
            counter=counter,
            kasir_nama=kasir_nama,
            shift=shift,
            total_dict=total_dict,
            transaksi_list=transaksi_list,
            admin_nama=admin_nama,
            total_disetor=total_disetor,
            layout=profile.get("layout"),
            receipt_setting=profile.get("receipt_setting"),
        )
        paper_width = printer.paperRect(QPrinter.Point).width()
        doc = self._build_document(html, paper_width)
        return doc

    # edited by glg
    def _draw_document_scaled(self, painter, printer, doc):
        """
        Render QTextDocument ke area printable thermal secara konsisten.
        Fokus pada scale horizontal agar konten tidak menyusut di kiri-atas.
        """
        try:
            page_rect = printer.pageRect(QPrinter.DevicePixel)
            target_width = float(page_rect.width() or 0.0)
            offset_x = float(page_rect.left() or 0.0)
            offset_y = float(page_rect.top() or 0.0)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError):
            target_width = 0.0
            offset_x = 0.0
            offset_y = 0.0

        source_width = float(doc.textWidth() or doc.idealWidth() or 1.0)
        if source_width <= 0:
            source_width = 1.0

        scale_x = (target_width / source_width) if target_width > 0 else 1.0
        if scale_x <= 0:
            scale_x = 1.0

        painter.save()
        painter.translate(offset_x, offset_y)
        painter.scale(scale_x, scale_x)
        doc.drawContents(painter)
        painter.restore()

    def _print_direct(
        self,
        printer_settings_controller,
        profile,
        counter,
        kasir_nama,
        shift,
        total_dict,
        transaksi_list,
        admin_nama,
        total_disetor,
    ):
        selected_printer = self._resolve_printer_name(profile.get("printer_name"))
        if not selected_printer:
            self._warning(f"Printer settlement tidak tersedia: '{profile.get('printer_name') or ''}'")
            return False

        paper_label = str(profile.get("paper_size") or DEFAULT_PAPER_SIZE).strip() or DEFAULT_PAPER_SIZE
        printer = self._create_native_printer(printer_settings_controller, selected_printer, paper_label)
        doc = self._get_document_payload(
            printer_settings_controller,
            profile,
            counter,
            kasir_nama,
            shift,
            total_dict,
            transaksi_list,
            admin_nama,
            total_disetor,
            printer,
        )

        painter = QPainter()
        if painter.begin(printer):
            self._draw_document_scaled(painter, printer, doc)
            painter.end()
            self._info(f"Settlement {counter} dicetak ke '{selected_printer}'")
            return True
        self._error(f"Gagal membuka painter untuk printer '{selected_printer}'")
        return False

    def _preview(
        self,
        printer_settings_controller,
        print_view,
        profile,
        counter,
        kasir_nama,
        shift,
        total_dict,
        transaksi_list,
        admin_nama,
        total_disetor,
    ):
        paper_label = str(profile.get("paper_size") or DEFAULT_PAPER_SIZE).strip() or DEFAULT_PAPER_SIZE
        width_mm = printer_settings_controller.paper_service.get_paper_mm(paper_label)
        preview_printer = printer_settings_controller._make_preview_printer(width_mm)
        doc = self._get_document_payload(
            printer_settings_controller,
            profile,
            counter,
            kasir_nama,
            shift,
            total_dict,
            transaksi_list,
            admin_nama,
            total_disetor,
            preview_printer,
        )
        # edited by glg
        # Samakan engine preview settlement dengan preview struk pembayaran (raw painter).
        def render_func(painter):
            self._draw_document_scaled(painter, preview_printer, doc)

        width_pt = printer_settings_controller._mm_to_points(width_mm)
        print_view.show_preview_raw(render_func, preview_printer, width_pt)
        self._info(f"Preview settlement {counter} ditampilkan")
        return True

    def print_settlement_by_mode(
        self,
        printer_settings_controller,
        print_view,
        counter,
        kasir_nama,
        shift,
        total_dict,
        transaksi_list,
        admin_nama=None,
        total_disetor=0.0,
        printer_name: Optional[str] = None,
    ):
        self._info(f"Print Settlement ID: {counter}")
        profile = self._print_profile_service.resolve_settlement_profile(
            printer_settings_controller=printer_settings_controller,
            printer_name=printer_name,
        )
        layout = profile.get("layout") if isinstance(profile, dict) else {}
        self._info(
            "[SettlementPrint] layout options -> "
            f"show_cash_recap={1 if self._to_bool((layout or {}).get('show_cash_recap'), True) else 0}, "
            f"show_transaction_list={1 if self._to_bool((layout or {}).get('show_transaction_list'), False) else 0}"
        )
        mode = str(profile.get("mode") or "preview").strip().lower()
        if mode == "preview":
            return self._preview(
                printer_settings_controller=printer_settings_controller,
                print_view=print_view,
                profile=profile,
                counter=counter,
                kasir_nama=kasir_nama,
                shift=shift,
                total_dict=total_dict,
                transaksi_list=transaksi_list,
                admin_nama=admin_nama,
                total_disetor=total_disetor,
            )
        return self._print_direct(
            printer_settings_controller=printer_settings_controller,
            profile=profile,
            counter=counter,
            kasir_nama=kasir_nama,
            shift=shift,
            total_dict=total_dict,
            transaksi_list=transaksi_list,
            admin_nama=admin_nama,
            total_disetor=total_disetor,
        )
