"""
PDF Generation utility for invoices and quotes
Uses ReportLab to generate professional PDF documents

Note: This module has been migrated from WeasyPrint to ReportLab for better reliability
and fewer system dependencies. Legacy WeasyPrint imports remain for backward compatibility
but are not actively used in the new implementation.
"""

import html as html_lib
import os
from datetime import datetime

try:
    # Try importing WeasyPrint. This may fail on systems without native deps.
    from weasyprint import CSS, HTML  # type: ignore
    from weasyprint.text.fonts import FontConfiguration  # type: ignore

    _WEASYPRINT_AVAILABLE = True
except Exception:
    # Defer to fallback implementation at runtime
    HTML = None  # type: ignore
    CSS = None  # type: ignore
    FontConfiguration = None  # type: ignore
    _WEASYPRINT_AVAILABLE = False
from flask import current_app
from flask_babel import gettext as _

from app import db
from app.models import InvoicePDFTemplate, QuotePDFTemplate, Settings

try:
    from babel.dates import format_date as babel_format_date
except Exception:
    babel_format_date = None
from pathlib import Path

from flask import render_template


def update_page_size_in_css(css_text, page_size):
    """
    Update @page size property to match the specified page size.

    This function handles:
    - Replacing existing @page size property
    - Adding @page size property if missing
    - Handling nested @page rules (e.g., @bottom-center)
    - Multiple @page rules (updates all of them)

    Args:
        css_text: CSS string that may contain @page rules
        page_size: Target page size (e.g., "A4", "Letter")

    Returns:
        Updated CSS string with correct @page size
    """
    import re

    if not css_text or not page_size:
        return css_text

    # Find all @page rules (may have multiple)
    page_pattern = r"@page\s*\{"
    matches = list(re.finditer(page_pattern, css_text, re.IGNORECASE | re.MULTILINE))

    if not matches:
        # No @page rule exists - add one at the beginning
        new_page_rule = f"@page {{\n            size: {page_size};\n            margin: 2cm;\n        }}\n\n"
        return new_page_rule + css_text

    # Process matches in reverse order to maintain positions
    for match in reversed(matches):
        start_pos = match.start()
        # Find matching closing brace, accounting for nested braces
        brace_count = 0
        end_pos = len(css_text)

        for i in range(match.end() - 1, len(css_text)):
            if css_text[i] == "{":
                brace_count += 1
            elif css_text[i] == "}":
                brace_count -= 1
                if brace_count == 0:
                    end_pos = i + 1
                    break

        page_block = css_text[start_pos:end_pos]

        # Replace or add size property
        if re.search(r"size\s*:", page_block, re.IGNORECASE):
            # Replace existing size property - handle any whitespace, quotes, and values
            # Match: size: "A5" or size: A5 or size:A5 etc.
            updated_block = re.sub(
                r"size\s*:\s*['\"]?[^;}\n]+['\"]?",
                f"size: {page_size}",
                page_block,
                flags=re.IGNORECASE | re.MULTILINE,
            )
            css_text = css_text[:start_pos] + updated_block + css_text[end_pos:]
        else:
            # Add size property after @page {
            updated_block = re.sub(
                r"(@page\s*\{)",
                r"\1\n            size: " + page_size + r";",
                page_block,
                count=1,
                flags=re.IGNORECASE,
            )
            css_text = css_text[:start_pos] + updated_block + css_text[end_pos:]

    return css_text


def update_wrapper_dimensions_in_css(css_text, page_size):
    """
    Update wrapper dimensions (width, height, max-width, max-height) in CSS to match page size.

    This function updates the .invoice-wrapper and .quote-wrapper dimensions to match
    the selected page size. Dimensions are calculated at 72 DPI for PDF.

    Args:
        css_text: CSS string that may contain wrapper dimension definitions
        page_size: Target page size (e.g., "A4", "A5", "Letter")

    Returns:
        Updated CSS string with correct wrapper dimensions
    """
    if not css_text or not page_size:
        return css_text

    # Standard page sizes (shared by both InvoicePDFTemplate and QuotePDFTemplate)
    PAGE_SIZES = {
        "A4": {"width": 210, "height": 297},
        "Letter": {"width": 216, "height": 279},
        "Legal": {"width": 216, "height": 356},
        "A3": {"width": 297, "height": 420},
        "A5": {"width": 148, "height": 210},
        "Tabloid": {"width": 279, "height": 432},
    }

    # Get page dimensions
    page_dimensions = PAGE_SIZES.get(page_size)
    if not page_dimensions:
        return css_text

    # Calculate dimensions in pixels at 72 DPI (PDF standard)
    width_mm = page_dimensions["width"]
    height_mm = page_dimensions["height"]
    width_px = int((width_mm / 25.4) * 72)
    height_px = int((height_mm / 25.4) * 72)

    import re

    # Pattern to match wrapper dimension properties
    # Match: width: 420px, width:420px, width: 420px !important, etc.
    dimension_patterns = [
        (r"\.invoice-wrapper\s*\{[^}]*?)(width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.invoice-wrapper\s*\{[^}]*?)(height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
        (r"\.invoice-wrapper\s*\{[^}]*?)(max-width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.invoice-wrapper\s*\{[^}]*?)(max-height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
        (r"\.invoice-wrapper\s*\{[^}]*?)(min-width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.invoice-wrapper\s*\{[^}]*?)(min-height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(max-width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(max-height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(min-width\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{width_px}px\\3"),
        (r"\.quote-wrapper\s*\{[^}]*?)(min-height\s*:\s*)\d+px(\s*!important)?", f"\\1\\2{height_px}px\\3"),
    ]

    updated_css = css_text
    for pattern, replacement in dimension_patterns:
        updated_css = re.sub(pattern, replacement, updated_css, flags=re.IGNORECASE | re.DOTALL)

    # Also update html, body dimensions if they exist
    updated_css = re.sub(
        r"(html,\s*body\s*\{[^}]*?)(width\s*:\s*)\d+px(\s*!important)?",
        f"\\1\\2{width_px}px\\3",
        updated_css,
        flags=re.IGNORECASE | re.DOTALL,
    )
    updated_css = re.sub(
        r"(html,\s*body\s*\{[^}]*?)(height\s*:\s*)\d+px(\s*!important)?",
        f"\\1\\2{height_px}px\\3",
        updated_css,
        flags=re.IGNORECASE | re.DOTALL,
    )

    return updated_css


def validate_page_size_in_css(css_text, expected_page_size):
    """
    Validate that CSS contains the correct @page size.

    Args:
        css_text: CSS string to validate
        expected_page_size: Expected page size (e.g., "A4", "Letter")

    Returns:
        tuple: (is_valid: bool, found_sizes: list) - True if all @page rules have correct size
    """
    import re

    if not css_text or not expected_page_size:
        return False, []

    # Find all @page rules and check their size
    page_rules = re.findall(r"@page\s*\{[^}]*\}", css_text, re.IGNORECASE | re.DOTALL)
    found_sizes = []

    for rule in page_rules:
        size_match = re.search(r"size\s*:\s*['\"]?([^;}\n'\"]+)['\"]?", rule, re.IGNORECASE)
        if size_match:
            found_size = size_match.group(1).strip()
            found_sizes.append(found_size)
            # Remove quotes if present (double-check)
            found_size = found_size.strip("\"'")
            if found_size != expected_page_size:
                return False, found_sizes

    # If we found @page rules, all should have the correct size
    if page_rules and not found_sizes:
        return False, []  # @page rules exist but no size specified

    # If no @page rules, that's also a problem
    if not page_rules:
        return False, []

    return True, found_sizes


class InvoicePDFGenerator:
    """Generate PDF invoices with company branding"""

    def __init__(self, invoice, settings=None, page_size="A4"):
        self.invoice = invoice
        self.settings = settings or Settings.get_settings()
        self.page_size = page_size or "A4"

    def generate_pdf(self):
        """Generate PDF content and return as bytes using ReportLab"""
        import json
        import sys

        from flask import current_app

        def debug_print(msg):
            """Print debug message to stdout with immediate flush for Docker visibility"""
            print(msg, file=sys.stdout, flush=True)
            print(msg, file=sys.stderr, flush=True)
            # Also log using Flask logger if available
            try:
                current_app.logger.info(msg)
            except Exception:
                pass

        invoice_id = getattr(self.invoice, "id", "N/A")
        invoice_number = getattr(self.invoice, "invoice_number", "N/A")

        debug_print(
            f"\n[PDF_EXPORT] PDF GENERATOR - InvoiceID: {invoice_id}, InvoiceNumber: {invoice_number}, PageSize: {self.page_size}"
        )
        debug_print(f"{'='*80}\n")
        current_app.logger.info(
            f"[PDF_EXPORT] Starting PDF generation - InvoiceID: {invoice_id}, InvoiceNumber: {invoice_number}, PageSize: '{self.page_size}'"
        )

        # Get template for the specified page size
        from app.models import InvoicePDFTemplate

        # CRITICAL: Expire all cached objects to ensure we get the latest saved template
        db.session.expire_all()

        current_app.logger.info(
            f"[PDF_EXPORT] Querying database for template - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
        )

        # CRITICAL: Do a completely fresh query using raw SQL to bypass any ORM caching
        # This ensures we get the absolute latest data from the database
        from sqlalchemy import text

        result = db.session.execute(
            text(
                "SELECT id, page_size, template_json, updated_at FROM invoice_pdf_templates WHERE page_size = :page_size"
            ),
            {"page_size": self.page_size},
        ).first()

        template_json_raw_from_db = None
        template = None

        if result:
            template_id, page_size_db, template_json_raw_from_db, updated_at = result
            current_app.logger.info(
                f"[PDF_EXPORT] Template found via raw query - PageSize: '{page_size_db}', TemplateID: {template_id}, UpdatedAt: {updated_at}, TemplateJSONLength: {len(template_json_raw_from_db) if template_json_raw_from_db else 0}, InvoiceID: {invoice_id}"
            )
            # Now get the full template object for use (for other attributes if needed)
            template = InvoicePDFTemplate.query.get(template_id)
            # CRITICAL: Use template_json directly from raw query, not from ORM object (which might be cached)
            if template_json_raw_from_db:
                template.template_json = template_json_raw_from_db
            # Force refresh all other attributes
            db.session.refresh(template)
        else:
            current_app.logger.warning(
                f"[PDF_EXPORT] Template not found for PageSize: '{self.page_size}', creating default - InvoiceID: {invoice_id}"
            )
            template = InvoicePDFTemplate.get_template(self.page_size)
            template_json_raw_from_db = template.template_json

        # Store template as instance variable for use in format_date
        self.template = template

        debug_print(f"[DEBUG] Retrieved template: page_size={template.page_size}, id={template.id}")
        template_json_to_use = template_json_raw_from_db if template_json_raw_from_db else template.template_json
        template_json_length = len(template_json_to_use) if template_json_to_use else 0
        template_json_preview = (
            (template_json_to_use[:100] + "...")
            if template_json_to_use and len(template_json_to_use) > 100
            else (template_json_to_use or "(empty)")
        )
        # Also get a hash/fingerprint of the JSON to verify it's actually the saved one
        import hashlib

        template_json_hash = (
            hashlib.md5(template_json_to_use.encode("utf-8")).hexdigest()[:16] if template_json_to_use else "none"
        )
        current_app.logger.info(
            f"[PDF_EXPORT] Template retrieved - PageSize: '{template.page_size}', TemplateID: {template.id}, HasJSON: {bool(template_json_to_use)}, JSONLength: {template_json_length}, JSONHash: {template_json_hash}, JSONPreview: {template_json_preview}, UpdatedAt: {template.updated_at}, InvoiceID: {invoice_id}"
        )

        # Get or generate ReportLab template JSON
        template_json_dict = None
        # CRITICAL: Use template_json_raw_from_db (from raw query) - this is the absolute latest from database
        # template_json_to_use is already set above
        # Check if template_json exists and is not empty/whitespace
        if template_json_to_use and template_json_to_use.strip():
            try:
                current_app.logger.info(
                    f"[PDF_EXPORT] Parsing template JSON - PageSize: '{self.page_size}', JSON length: {len(template_json_to_use)}, InvoiceID: {invoice_id}"
                )
                template_json_dict = json.loads(template_json_to_use)
                element_count = len(template_json_dict.get("elements", []))
                json_page_size = template_json_dict.get("page", {}).get("size", "unknown")
                # Get first few element types for debugging
                element_types = [elem.get("type", "unknown") for elem in template_json_dict.get("elements", [])[:5]]
                debug_print(f"[DEBUG] Found ReportLab template JSON (length: {len(template_json_to_use)})")
                current_app.logger.info(
                    f"[PDF_EXPORT] Template JSON parsed successfully - PageSize: '{self.page_size}', JSON PageSize: '{json_page_size}', Elements: {element_count}, FirstElementTypes: {element_types}, InvoiceID: {invoice_id}"
                )
            except Exception as e:
                debug_print(f"[WARNING] Failed to parse template_json: {e}")
                template_json_preview_use = (
                    (template_json_to_use[:100] + "...")
                    if template_json_to_use and len(template_json_to_use) > 100
                    else (template_json_to_use or "(empty)")
                )
                current_app.logger.error(
                    f"[PDF_EXPORT] Failed to parse template JSON - PageSize: '{self.page_size}', Error: {str(e)}, JSONPreview: {template_json_preview_use}, InvoiceID: {invoice_id}",
                    exc_info=True,
                )
                template_json_dict = None
        else:
            current_app.logger.warning(
                f"[PDF_EXPORT] Template JSON is empty or whitespace - PageSize: '{self.page_size}', TemplateID: {template.id}, TemplateJSONIsNone: {template_json_to_use is None}, TemplateJSONIsEmpty: {not template_json_to_use or not template_json_to_use.strip()}, RawQueryResult: {template_json_raw_from_db is not None if 'template_json_raw_from_db' in locals() else 'N/A'}, InvoiceID: {invoice_id}"
            )

        # If no JSON template exists, ensure it's populated with default (will save to database if empty)
        if not template_json_dict:
            debug_print(
                f"[DEBUG] No template JSON found, ensuring default template JSON for page size {self.page_size}"
            )
            current_app.logger.info(
                f"[PDF_EXPORT] Template JSON is empty, ensuring default template - PageSize: '{self.page_size}', "
                f"TemplateID: {template.id}, InvoiceID: {invoice_id}"
            )

            # Call ensure_template_json() which will populate with default if empty/invalid
            # This saves the default to the database, so it's available for future exports
            # It only saves if template_json is truly empty/invalid, not if it's a valid custom template
            template.ensure_template_json()

            # Re-query template_json from database to get the updated value (avoid ORM caching)
            db.session.expire(template)
            result_updated = db.session.execute(
                text("SELECT template_json FROM invoice_pdf_templates WHERE id = :template_id"),
                {"template_id": template.id},
            ).first()

            if result_updated and result_updated[0]:
                template_json_to_use = result_updated[0]
                try:
                    template_json_dict = json.loads(template_json_to_use)
                    element_count = len(template_json_dict.get("elements", []))
                    debug_print(f"[DEBUG] Retrieved default template JSON with {element_count} elements (saved to DB)")
                    current_app.logger.info(
                        f"[PDF_EXPORT] Default template JSON retrieved from database - PageSize: '{self.page_size}', "
                        f"Elements: {element_count}, InvoiceID: {invoice_id}"
                    )
                except Exception as e:
                    current_app.logger.error(
                        f"[PDF_EXPORT] Failed to parse template JSON after ensure_template_json() - PageSize: '{self.page_size}', Error: {str(e)}, InvoiceID: {invoice_id}",
                        exc_info=True,
                    )
                    # Fall back to generating default in memory if parsing fails
                    from app.utils.pdf_template_schema import get_default_template

                    template_json_dict = get_default_template(self.page_size)
            else:
                # Fallback: generate default in memory if ensure_template_json() didn't work
                current_app.logger.warning(
                    f"[PDF_EXPORT] ensure_template_json() didn't populate template_json, using in-memory default - PageSize: '{self.page_size}', TemplateID: {template.id}, InvoiceID: {invoice_id}"
                )
                from app.utils.pdf_template_schema import get_default_template

                template_json_dict = get_default_template(self.page_size)
        else:
            # CRITICAL: Ensure template page size and dimensions match the requested page size
            # This fixes layout issues when templates were customized but dimensions don't match
            template_page_config = template_json_dict.get("page", {})
            template_page_size = template_page_config.get("size", self.page_size)

            if template_page_size != self.page_size:
                current_app.logger.warning(
                    f"[PDF_EXPORT] Template page size mismatch - TemplatePageSize: '{template_page_size}', "
                    f"RequestedPageSize: '{self.page_size}', InvoiceID: {invoice_id}. "
                    f"Updating template to match requested page size."
                )
                # Update template page size to match requested size
                template_page_config["size"] = self.page_size
                template_json_dict["page"] = template_page_config

            # Ensure page dimensions are correct for the requested page size
            from app.utils.pdf_template_schema import PAGE_SIZE_DIMENSIONS_MM

            if self.page_size in PAGE_SIZE_DIMENSIONS_MM:
                expected_dims = PAGE_SIZE_DIMENSIONS_MM[self.page_size]
                current_width = template_page_config.get("width")
                current_height = template_page_config.get("height")

                if current_width != expected_dims["width"] or current_height != expected_dims["height"]:
                    current_app.logger.info(
                        f"[PDF_EXPORT] Correcting template page dimensions - PageSize: '{self.page_size}', "
                        f"Old: {current_width}x{current_height}mm, New: {expected_dims['width']}x{expected_dims['height']}mm, InvoiceID: {invoice_id}"
                    )
                    template_page_config["width"] = expected_dims["width"]
                    template_page_config["height"] = expected_dims["height"]
                    template_json_dict["page"] = template_page_config

            # Update element positions if they exceed page bounds (due to page size change)
            # This helps fix layout issues when switching between page sizes
            if template_page_size != self.page_size:
                page_dims = PAGE_SIZE_DIMENSIONS_MM.get(self.page_size, {"width": 210, "height": 297})
                page_width_pt = (page_dims["width"] / 25.4) * 72  # Convert mm to points
                page_height_pt = (page_dims["height"] / 25.4) * 72

                elements = template_json_dict.get("elements", [])
                adjusted_count = 0
                for element in elements:
                    x = element.get("x", 0)
                    y = element.get("y", 0)
                    width = element.get("width", 0)
                    height = element.get("height", 0)

                    # Check if element is outside page bounds
                    if x + width > page_width_pt or y + height > page_height_pt:
                        # Scale element to fit within page (proportional scaling)
                        if x + width > page_width_pt:
                            scale_x = (page_width_pt - 20) / (x + width)  # Leave 20pt margin
                            element["x"] = x * scale_x
                            element["width"] = width * scale_x
                            adjusted_count += 1
                        if y + height > page_height_pt:
                            scale_y = (page_height_pt - 20) / (y + height)  # Leave 20pt margin
                            element["y"] = y * scale_y
                            element["height"] = height * scale_y
                            adjusted_count += 1

                if adjusted_count > 0:
                    current_app.logger.info(
                        f"[PDF_EXPORT] Adjusted {adjusted_count} elements to fit page size '{self.page_size}' - InvoiceID: {invoice_id}"
                    )

        # Always use ReportLab template renderer with JSON
        debug_print(f"[DEBUG] Using ReportLab template renderer for page size {self.page_size}")
        from app.utils.pdf_generator_reportlab import ReportLabTemplateRenderer
        from app.utils.pdf_template_schema import validate_template_json

        # Validate template JSON
        current_app.logger.info(
            f"[PDF_EXPORT] Validating template JSON - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
        )
        is_valid, error = validate_template_json(template_json_dict)
        if not is_valid:
            debug_print(f"[ERROR] Template JSON validation failed: {error}")
            current_app.logger.error(
                f"[PDF_EXPORT] Template JSON validation failed - PageSize: '{self.page_size}', Error: {error}, InvoiceID: {invoice_id}"
            )
            # Even if validation fails, try to render with default fallback
            return self._generate_pdf_with_default()
        else:
            current_app.logger.info(
                f"[PDF_EXPORT] Template JSON validation passed - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
            )

        # Prepare data context for template rendering
        current_app.logger.info(
            f"[PDF_EXPORT] Preparing template context - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
        )
        data_context = self._prepare_template_context()

        # Render PDF using ReportLab
        current_app.logger.info(
            f"[PDF_EXPORT] Creating ReportLab renderer - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
        )
        renderer = ReportLabTemplateRenderer(template_json_dict, data_context, self.page_size)
        try:
            current_app.logger.info(
                f"[PDF_EXPORT] Starting ReportLab render - PageSize: '{self.page_size}', InvoiceID: {invoice_id}"
            )
            pdf_bytes = renderer.render_to_bytes()
            pdf_size_bytes = len(pdf_bytes)
            debug_print(f"[DEBUG] ReportLab PDF generated successfully - size: {pdf_size_bytes} bytes")
            current_app.logger.info(
                f"[PDF_EXPORT] ReportLab PDF generated successfully - PageSize: '{self.page_size}', PDFSize: {pdf_size_bytes} bytes, InvoiceID: {invoice_id}"
            )
            return pdf_bytes
        except Exception as e:
            debug_print(f"[ERROR] ReportLab rendering failed: {e}")
            import traceback

            debug_print(traceback.format_exc())
            current_app.logger.error(
                f"[PDF_EXPORT] ReportLab rendering failed - PageSize: '{self.page_size}', Error: {str(e)}, InvoiceID: {invoice_id}",
                exc_info=True,
            )
            # Fall back to default generation
            return self._generate_pdf_with_default()

    def _prepare_template_context(self):
        """Prepare data context for template rendering"""
        # Convert SQLAlchemy objects to simple structures for template
        from types import SimpleNamespace

        # Create invoice wrapper
        invoice_wrapper = SimpleNamespace()
        for attr in [
            "id",
            "invoice_number",
            "issue_date",
            "due_date",
            "status",
            "client_name",
            "client_email",
            "client_address",
            "client_id",
            "subtotal",
            "tax_rate",
            "tax_amount",
            "total_amount",
            "notes",
            "terms",
        ]:
            try:
                setattr(invoice_wrapper, attr, getattr(self.invoice, attr))
            except AttributeError:
                pass

        # Convert relationships to lists
        try:
            if hasattr(self.invoice.items, "all"):
                invoice_wrapper.items = self.invoice.items.all()
            else:
                invoice_wrapper.items = list(self.invoice.items) if self.invoice.items else []
        except Exception:
            invoice_wrapper.items = []

        try:
            if hasattr(self.invoice.extra_goods, "all"):
                invoice_wrapper.extra_goods = self.invoice.extra_goods.all()
            else:
                invoice_wrapper.extra_goods = list(self.invoice.extra_goods) if self.invoice.extra_goods else []
        except Exception:
            invoice_wrapper.extra_goods = []

        try:
            if hasattr(self.invoice, "expenses") and hasattr(self.invoice.expenses, "all"):
                invoice_wrapper.expenses = self.invoice.expenses.all()
            else:
                invoice_wrapper.expenses = (
                    list(self.invoice.expenses) if hasattr(self.invoice, "expenses") and self.invoice.expenses else []
                )
        except Exception:
            invoice_wrapper.expenses = []

        # Build combined all_line_items for PDF table (items + extra_goods + expenses)
        # Each entry has: description, quantity, unit_price, total_amount
        all_line_items = []
        for item in invoice_wrapper.items:
            all_line_items.append(
                SimpleNamespace(
                    description=getattr(item, "description", str(item)) or "",
                    quantity=getattr(item, "quantity", 1),
                    unit_price=getattr(item, "unit_price", 0),
                    total_amount=getattr(item, "total_amount", 0),
                )
            )
        for good in invoice_wrapper.extra_goods:
            desc_parts = [getattr(good, "name", str(good)) or ""]
            if getattr(good, "description", None):
                desc_parts.append(str(good.description))
            if getattr(good, "sku", None):
                desc_parts.append(f"SKU: {good.sku}")
            if getattr(good, "category", None):
                desc_parts.append(f"Category: {good.category.title()}")
            all_line_items.append(
                SimpleNamespace(
                    description="\n".join(desc_parts),
                    quantity=getattr(good, "quantity", 1),
                    unit_price=getattr(good, "unit_price", 0),
                    total_amount=getattr(good, "total_amount", 0),
                )
            )
        for expense in invoice_wrapper.expenses:
            desc_parts = [getattr(expense, "title", str(expense)) or ""]
            if getattr(expense, "description", None):
                desc_parts.append(str(expense.description))
            amt = getattr(expense, "total_amount", None) or getattr(expense, "amount", 0)
            all_line_items.append(
                SimpleNamespace(
                    description="\n".join(desc_parts),
                    quantity=1,
                    unit_price=amt,
                    total_amount=amt,
                )
            )
        invoice_wrapper.all_line_items = all_line_items

        # Project
        invoice_wrapper.project = self.invoice.project

        # Client (for PEPPOL compliance when setting is on)
        invoice_wrapper.client = getattr(self.invoice, "client", None)

        # Settings
        settings_wrapper = SimpleNamespace()
        for attr in [
            "company_name",
            "company_address",
            "company_email",
            "company_phone",
            "company_website",
            "company_tax_id",
            "currency",
            "invoice_terms",
            "company_bank_info",
        ]:
            try:
                setattr(settings_wrapper, attr, getattr(self.settings, attr))
            except AttributeError:
                pass

        # Add helper methods
        def has_logo():
            return self.settings.has_logo()

        def get_logo_path():
            return self.settings.get_logo_path()

        settings_wrapper.has_logo = has_logo
        settings_wrapper.get_logo_path = get_logo_path

        # Helper functions for templates
        from babel.dates import format_date as babel_format_date

        from app.utils.template_filters import get_image_base64, get_logo_base64

        def format_date(value, format="medium"):
            try:
                # Use DD.MM.YYYY format for invoices and quotes
                return value.strftime("%d.%m.%Y") if value else ""
            except Exception:
                return str(value) if value else ""

        def format_money(value):
            try:
                return f"{float(value):,.2f} {self.settings.currency}"
            except Exception:
                return f"{value} {self.settings.currency}"

        # PEPPOL compliance: include when invoices_peppol_compliant is on
        result = {
            "invoice": invoice_wrapper,
            "settings": settings_wrapper,
            "get_logo_base64": get_logo_base64,
            "format_date": format_date,
            "format_money": format_money,
        }
        if getattr(self.settings, "invoices_peppol_compliant", False):
            client = getattr(self.invoice, "client", None)
            result["peppol_compliance"] = {
                "enabled": True,
                "seller_endpoint_id": (getattr(self.settings, "peppol_sender_endpoint_id", None) or "").strip(),
                "seller_scheme_id": (getattr(self.settings, "peppol_sender_scheme_id", None) or "").strip(),
                "seller_vat": (getattr(self.settings, "company_tax_id", None) or "").strip(),
                "buyer_endpoint_id": (
                    (client.get_custom_field("peppol_endpoint_id", "") or "").strip() if client else ""
                ),
                "buyer_scheme_id": (client.get_custom_field("peppol_scheme_id", "") or "").strip() if client else "",
                "buyer_vat": (
                    (client.get_custom_field("vat_id", "") or client.get_custom_field("tax_id", "") or "").strip()
                    if client
                    else ""
                ),
            }
        return result

    def _generate_pdf_with_default(self):
        """Generate PDF using default fallback ReportLab generator"""
        from app.utils.pdf_generator_fallback import InvoicePDFGeneratorFallback

        fallback = InvoicePDFGeneratorFallback(self.invoice, settings=self.settings)
        return fallback.generate_pdf()

    def _render_from_custom_template(self, template=None):
        """Render HTML and CSS from custom templates stored in database, with fallback to default template."""
        # Define debug_print for this method scope
        import sys

        def debug_print(msg):
            """Print debug message to stdout with immediate flush for Docker visibility"""
            print(msg, file=sys.stdout, flush=True)
            print(msg, file=sys.stderr, flush=True)

        if template:
            # Ensure template matches the selected page size
            if hasattr(template, "page_size") and template.page_size != self.page_size:
                # Template doesn't match - this shouldn't happen, but handle it
                # Get the correct template
                from app.models import InvoicePDFTemplate

                correct_template = InvoicePDFTemplate.query.filter_by(page_size=self.page_size).first()
                if correct_template:
                    template = correct_template
                else:
                    # Couldn't find correct template - use default generation instead
                    raise ValueError(f"Template for page size {self.page_size} not found")

            # Don't strip - preserve exact content as saved (whitespace might be important)
            html_template = template.template_html or ""
            css_template = template.template_css or ""
        else:
            # No template provided - this should not happen in normal flow
            # If it does, we can't proceed without a template
            raise ValueError(f"No template provided for page size {self.page_size}. This is a bug.")
        html = ""

        def update_page_size_in_html(html_text):
            """Update @page size property in HTML's inline <style> tags"""
            import re

            # Find and update @page rules in <style> tags
            def update_style_tag(match):
                style_content = match.group(2)  # Content inside <style> tag
                updated_content = update_page_size_in_css(style_content, self.page_size)
                return f"{match.group(1)}{updated_content}{match.group(3)}"

            # Match <style> tags (with or without attributes)
            style_pattern = r"(<style[^>]*>)(.*?)(</style>)"
            if re.search(style_pattern, html_text, re.IGNORECASE | re.DOTALL):
                html_text = re.sub(style_pattern, update_style_tag, html_text, flags=re.IGNORECASE | re.DOTALL)

            return html_text

        def remove_page_rule_from_html(html_text):
            """Remove @page rules from HTML inline styles to avoid conflicts with separate CSS"""
            import re

            def remove_from_style_tag(match):
                style_content = match.group(2)
                # Remove @page rule from style content
                # Need to handle nested @bottom-center rules properly
                # Match @page { ... } including any nested rules
                brace_count = 0
                page_pattern = r"@page\s*\{"
                page_match = re.search(page_pattern, style_content, re.IGNORECASE)

                if page_match:
                    start = page_match.start()
                    # Find matching closing brace
                    pos = page_match.end() - 1
                    end = len(style_content)
                    for i in range(page_match.end() - 1, len(style_content)):
                        if style_content[i] == "{":
                            brace_count += 1
                        elif style_content[i] == "}":
                            brace_count -= 1
                            if brace_count == 0:
                                end = i + 1
                                break
                    # Remove the @page rule
                    style_content = style_content[:start] + style_content[end:]
                    # Clean up any double newlines or extra whitespace
                    style_content = re.sub(r"\n\s*\n", "\n", style_content)

                return f"{match.group(1)}{style_content}{match.group(3)}"

            # Match <style> tags and remove @page rules from them
            style_pattern = r"(<style[^>]*>)(.*?)(</style>)"
            if re.search(style_pattern, html_text, re.IGNORECASE | re.DOTALL):
                html_text = re.sub(style_pattern, remove_from_style_tag, html_text, flags=re.IGNORECASE | re.DOTALL)

            return html_text

        # Handle CSS: When both HTML (with inline styles) and separate CSS exist,
        # extract inline styles, merge with separate CSS, and remove from HTML to avoid conflicts
        import re

        css_to_use = ""
        html_inline_styles_extracted = False

        # Extract inline styles from HTML if present
        extracted_inline_css = ""
        if html_template and "<style>" in html_template:
            style_match = re.search(r"<style[^>]*>(.*?)</style>", html_template, re.IGNORECASE | re.DOTALL)
            if style_match:
                extracted_inline_css = style_match.group(1)
                html_inline_styles_extracted = True

        if css_template and css_template.strip():
            # Use separate CSS template - this is the authoritative source
            # Don't merge with inline styles - the CSS template should contain everything needed
            # (Editor saves both HTML with styles AND CSS, but CSS is the clean source)
            debug_print(f"[DEBUG] Using separate CSS template (length: {len(css_template)})")

            # Check @page size before update
            import re

            before_match = re.search(r"@page\s*\{[^}]*?size\s*:\s*([^;}\n]+)", css_template, re.IGNORECASE | re.DOTALL)
            if before_match:
                before_size = before_match.group(1).strip()
                debug_print(f"[DEBUG] CSS template @page size BEFORE update: '{before_size}'")

            css_to_use = update_page_size_in_css(css_template, self.page_size)

            # Update wrapper dimensions to match page size (fixes hardcoded dimension issues)
            css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
            debug_print(f"[DEBUG] Updated wrapper dimensions in template CSS for page size: {self.page_size}")

            # Validate @page size after update
            is_valid, found_sizes = validate_page_size_in_css(css_to_use, self.page_size)
            if not is_valid:
                debug_print(f"[ERROR] @page size validation failed! Expected '{self.page_size}', found: {found_sizes}")
                current_app.logger.warning(
                    f"PDF template CSS @page size mismatch. Expected '{self.page_size}', found: {found_sizes}"
                )
            else:
                debug_print(f"[DEBUG] ✓ CSS template @page size correctly updated and validated: '{self.page_size}'")
        elif extracted_inline_css:
            # Only inline styles exist - extract and use them
            css_to_use = update_page_size_in_css(extracted_inline_css, self.page_size)
            css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
        else:
            # No CSS provided, use default
            try:
                from flask import render_template as _render_tpl

                css_to_use = _render_tpl("invoices/pdf_styles_default.css")
                css_to_use = update_page_size_in_css(css_to_use, self.page_size)
                css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
            except Exception:
                css_to_use = self._generate_css()

        # Ensure @page rule has correct size - this is critical for PDF generation
        css = css_to_use

        # Add comprehensive overflow prevention CSS
        overflow_css = get_overflow_prevention_css()
        css = css + "\n" + overflow_css

        # Import helper functions for template
        from babel.dates import format_date as babel_format_date

        from app.utils.template_filters import get_image_base64, get_logo_base64

        # Get date format from template, default to %d.%m.%Y
        date_format_str = (
            getattr(self.template, "date_format", "%d.%m.%Y")
            if hasattr(self, "template") and self.template
            else "%d.%m.%Y"
        )

        def format_date(value, format="medium"):
            """Format date for template"""
            # Use date format from template settings
            return value.strftime(date_format_str) if value else ""

        def format_money(value):
            """Format money for template"""
            try:
                return f"{float(value):,.2f}"
            except Exception:
                return str(value)

        # Convert lazy='dynamic' relationships to lists for template rendering
        # This ensures {% for item in invoice.items %} works correctly
        try:
            if hasattr(self.invoice.items, "all"):
                # It's a SQLAlchemy Query object - need to call .all()
                invoice_items = self.invoice.items.all()
            else:
                # Already a list or other iterable
                invoice_items = list(self.invoice.items) if self.invoice.items else []
        except Exception:
            invoice_items = []

        try:
            if hasattr(self.invoice.extra_goods, "all"):
                # It's a SQLAlchemy Query object - need to call .all()
                invoice_extra_goods = self.invoice.extra_goods.all()
            else:
                # Already a list or other iterable
                invoice_extra_goods = list(self.invoice.extra_goods) if self.invoice.extra_goods else []
        except Exception:
            invoice_extra_goods = []

        # Create a wrapper object that has the converted lists
        from types import SimpleNamespace

        invoice_data = SimpleNamespace()
        # Copy all attributes from original invoice
        for attr in dir(self.invoice):
            if not attr.startswith("_"):
                try:
                    setattr(invoice_data, attr, getattr(self.invoice, attr))
                except Exception:
                    pass
        # Override with converted lists
        invoice_data.items = invoice_items
        invoice_data.extra_goods = invoice_extra_goods

        # Convert expenses from Query to list
        try:
            if hasattr(self.invoice, "expenses") and hasattr(self.invoice.expenses, "all"):
                invoice_expenses = self.invoice.expenses.all()
            else:
                invoice_expenses = list(self.invoice.expenses) if self.invoice.expenses else []
        except Exception:
            invoice_expenses = []
        invoice_data.expenses = invoice_expenses

        # Load decorative images
        try:
            from app.models import InvoiceImage

            decorative_images = InvoiceImage.get_invoice_images(self.invoice.id)
        except Exception:
            decorative_images = []
        invoice_data.decorative_images = decorative_images

        try:
            # Render using Flask's Jinja environment to include app filters and _()
            if html_template:
                from app.utils.safe_template_render import render_sandboxed_string

                # When we have separate CSS, remove @page rules from HTML inline styles
                # to ensure the separate CSS @page rule is used (WeasyPrint uses first @page it finds)
                # Keep all other inline styles (like positioning) to preserve layout
                if html_inline_styles_extracted and css_template:
                    # Check if HTML has @page rules
                    import re

                    html_page_rules = re.findall(r"@page\s*\{[^}]*\}", html_template, re.IGNORECASE | re.DOTALL)
                    if html_page_rules:
                        debug_print(
                            f"[DEBUG] Found {len(html_page_rules)} @page rule(s) in HTML inline styles - removing them"
                        )
                        for i, rule in enumerate(html_page_rules):
                            debug_print(f"[DEBUG]   HTML @page rule {i+1}: {rule[:80]}")

                    # Remove @page rules from HTML inline styles (keep everything else)
                    html_template_updated = remove_page_rule_from_html(html_template)
                    debug_print("[DEBUG] Removed @page rules from HTML inline styles")
                else:
                    # No separate CSS or no inline styles - use template as-is or update inline @page
                    if html_template and "<style>" in html_template:
                        # Update @page size in HTML inline styles
                        html_template_updated = update_page_size_in_html(html_template)
                    else:
                        html_template_updated = html_template
                html = render_sandboxed_string(
                    html_template_updated,
                    autoescape=True,
                    invoice=invoice_data,  # Use wrapped object with lists
                    settings=self.settings,
                    Path=Path,
                    get_logo_base64=get_logo_base64,
                    get_image_base64=get_image_base64,
                    format_date=format_date,
                    format_money=format_money,
                    now=datetime.now(),
                )
        except Exception as e:
            # Log the exception for debugging
            import traceback

            print(f"Error rendering custom PDF template: {e}")
            print(traceback.format_exc())
            html = ""

        if not html:
            try:
                html = render_template(
                    "invoices/pdf_default.html",
                    invoice=invoice_data,  # Use wrapped object with lists
                    settings=self.settings,
                    Path=Path,
                    get_logo_base64=get_logo_base64,
                    get_image_base64=get_image_base64,
                    format_date=format_date,
                    format_money=format_money,
                    now=datetime.now(),
                )
            except Exception as e:
                # Log the exception for debugging
                import traceback

                print(f"Error rendering default PDF template: {e}")
                print(traceback.format_exc())
                html = f"<html><body><h1>{_('Invoice')} {self.invoice.invoice_number}</h1></body></html>"
        return html, css

    def _generate_html(self):
        """Generate HTML content for the invoice"""
        html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>{_('Invoice')} {self.invoice.invoice_number}</title>
            <style>
            :root {{
                --primary: #2563eb;
                --primary-600: #1d4ed8;
                --text: #0f172a;
                --muted: #475569;
                --border: #e2e8f0;
                --bg: #ffffff;
                --bg-alt: #f8fafc;
            }}
            * {{ box-sizing: border-box; }}
            body {{
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                color: var(--text);
                margin: 0;
                padding: 0;
                background: var(--bg);
                font-size: 12pt;
            }}
            .wrapper {{
                padding: 24px 28px;
            }}
            .invoice-header {{
                display: flex;
                align-items: flex-start;
                justify-content: space-between;
                border-bottom: 2px solid var(--border);
                padding-bottom: 16px;
                margin-bottom: 18px;
            }}
            .brand {{ display: flex; gap: 16px; align-items: center; }}
            .company-logo {{ max-width: 140px; max-height: 70px; display: block; }}
            .company-name {{ font-size: 22pt; font-weight: 700; margin: 0; color: var(--primary); }}
            .company-meta span {{ display: block; color: var(--muted); font-size: 10pt; }}
            .invoice-meta {{ text-align: right; }}
            .invoice-title {{ font-size: 26pt; font-weight: 800; color: var(--primary); margin: 0 0 8px 0; }}
            .meta-grid {{ display: grid; grid-template-columns: auto auto; gap: 4px 16px; font-size: 10.5pt; }}
            .label {{ color: var(--muted); font-weight: 600; }}
            .value {{ color: var(--text); font-weight: 600; }}

            .two-col {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 18px; }}
            .card {{ background: var(--bg-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }}
            .section-title {{ font-size: 12pt; font-weight: 700; color: var(--primary-600); margin: 0 0 8px 0; }}
            .small {{ color: var(--muted); font-size: 10pt; }}

            table {{ width: 100%; border-collapse: collapse; margin-top: 4px; }}
            thead {{ display: table-header-group; }}
            tfoot {{ display: table-footer-group; }}
            thead th {{ background: var(--bg-alt); color: var(--muted); font-weight: 700; border: 1px solid var(--border); padding: 10px; font-size: 10.5pt; text-align: left; }}
            tbody td {{ border: 1px solid var(--border); padding: 10px; font-size: 10.5pt; }}
            tfoot td {{ border: 1px solid var(--border); padding: 10px; font-weight: 700; }}
            .num {{ text-align: right; }}
            .desc {{ width: 50%; }}

            /* Pagination controls */
            tr, td, th {{ break-inside: avoid; page-break-inside: avoid; }}
            .card, .invoice-header, .two-col {{ break-inside: avoid; page-break-inside: avoid; }}
            h4 {{ break-after: avoid; }}

            .totals {{ margin-top: 6px; }}
            .note {{ margin-top: 10px; }}
            .footer {{ border-top: 1px solid var(--border); margin-top: 18px; padding-top: 10px; color: var(--muted); font-size: 10pt; }}
            </style>
        </head>
        <body>
            <div class="wrapper">
                <!-- Header -->
                <div class="invoice-header">
                    <div class="brand">
                        {self._get_company_logo_html()}
                        <div>
                            <h1 class="company-name">{self._escape(self.settings.company_name)}</h1>
                            <div class="company-meta small">
                                <span>{self._nl2br(self.settings.company_address)}</span>
                                <span>{_('Email')}: {self._escape(self.settings.company_email)}  ·  {_('Phone')}: {self._escape(self.settings.company_phone)}</span>
                                <span>{_('Website')}: {self._escape(self.settings.company_website)}</span>
                                {self._get_company_tax_info()}
                            </div>
                        </div>
                    </div>
                    <div class="invoice-meta">
                        <div class="invoice-title">{_('INVOICE')}</div>
                        <div class="meta-grid">
                            <div class="label">{_('Invoice #')}</div><div class="value">{self.invoice.invoice_number}</div>
                            <div class="label">{_('Issue Date')}</div><div class="value">{self.invoice.issue_date.strftime('%Y-%m-%d') if self.invoice.issue_date else ''}</div>
                            <div class="label">{_('Due Date')}</div><div class="value">{self.invoice.due_date.strftime('%Y-%m-%d') if self.invoice.due_date else ''}</div>
                            <div class="label">{_('Status')}</div><div class="value">{_(self.invoice.status.title())}</div>
                        </div>
                    </div>
                </div>
                
                <!-- Client Information -->
                <div class="two-col">
                    <div class="card">
                        <div class="section-title">{_('Bill To')}</div>
                        <div><strong>{self._escape(self.invoice.client_name)}</strong></div>
                        {self._get_client_email_html()}
                        {self._get_client_address_html()}
                    </div>
                    <div class="card">
                        <div class="section-title">{_('Project')}</div>
                        <div><strong>{self._escape(self.invoice.project.name)}</strong></div>
                        {self._get_project_description_html()}
                    </div>
                </div>
                
                <!-- Invoice Items -->
                <div>
                    <table>
                        <thead>
                            <tr>
                                <th class="desc">{_('Description')}</th>
                                <th class="num">{_('Quantity (Hours)')}</th>
                                <th class="num">{_('Unit Price')}</th>
                                <th class="num">{_('Total Amount')}</th>
                            </tr>
                        </thead>
                        <tbody>
                            {self._generate_items_rows()}
                        </tbody>
                        <tfoot>
                            {self._generate_totals_rows()}
                        </tfoot>
                    </table>
                </div>
                
                <!-- Additional Information -->
                {self._get_additional_info_html()}
                
                <!-- Footer -->
                <div class="footer">
                    {self._get_payment_info_html()}
                    <div><strong>{_('Terms & Conditions:')}</strong> {self._escape(self.settings.invoice_terms)}</div>
                </div>
            </div>
        </body>
        </html>
        """
        return html

    def _escape(self, value):
        return html_lib.escape(value) if value else ""

    def _nl2br(self, value):
        if not value:
            return ""
        return self._escape(value).replace("\n", "<br>")

    def _get_company_logo_html(self):
        """Generate HTML for company logo if available"""
        if self.settings.has_logo():
            logo_path = self.settings.get_logo_path()
            if logo_path and os.path.exists(logo_path):
                # Use base64 data URI for reliable PDF embedding (works better with WeasyPrint)
                try:
                    import base64
                    import mimetypes

                    with open(logo_path, "rb") as logo_file:
                        logo_data = base64.b64encode(logo_file.read()).decode("utf-8")

                    # Detect MIME type
                    mime_type, _ = mimetypes.guess_type(logo_path)
                    if not mime_type:
                        # Default to PNG if can't detect
                        mime_type = "image/png"

                    data_uri = f"data:{mime_type};base64,{logo_data}"
                    return f'<img src="{data_uri}" alt="Company Logo" class="company-logo">'
                except Exception as e:
                    # Fallback to file URI if base64 fails
                    try:
                        file_url = Path(logo_path).resolve().as_uri()
                    except Exception:
                        file_url = f"file://{logo_path}"
                    return f'<img src="{file_url}" alt="Company Logo" class="company-logo">'
        return ""

    def _get_company_tax_info(self):
        """Generate HTML for company tax information"""
        if self.settings.company_tax_id:
            return f'<div class="company-tax">Tax ID: {self.settings.company_tax_id}</div>'
        return ""

    def _get_client_email_html(self):
        """Generate HTML for client email if available"""
        if self.invoice.client_email:
            return f'<div class="client-email">{self.invoice.client_email}</div>'
        return ""

    def _get_client_address_html(self):
        """Generate HTML for client address if available"""
        if self.invoice.client_address:
            return f'<div class="client-address">{self.invoice.client_address}</div>'
        return ""

    def _get_project_description_html(self):
        """Generate HTML for project description if available"""
        if self.invoice.project.description:
            return f'<div class="project-description">{self.invoice.project.description}</div>'
        return ""

    def _generate_items_rows(self):
        """Generate HTML rows for invoice items and extra goods"""
        rows = []

        # Add regular invoice items
        for item in self.invoice.items:
            row = f"""
            <tr>
                <td>
                    {self._escape(item.description)}
                    {self._get_time_entry_info_html(item)}
                </td>
                <td class="num">{item.quantity:.2f}</td>
                <td class="num">{self._format_currency(item.unit_price)}</td>
                <td class="num">{self._format_currency(item.total_amount)}</td>
            </tr>
            """
            rows.append(row)

        # Add extra goods
        for good in self.invoice.extra_goods:
            # Build description with category and SKU if available
            description_parts = [self._escape(good.name)]
            if good.description:
                description_parts.append(
                    f"<br><small class='good-description'>{self._escape(good.description)}</small>"
                )
            if good.sku:
                description_parts.append(f"<br><small class='good-sku'>{_('SKU')}: {self._escape(good.sku)}</small>")
            if good.category:
                description_parts.append(
                    f"<br><small class='good-category'>{_('Category')}: {self._escape(good.category.title())}</small>"
                )

            description_html = "".join(description_parts)

            row = f"""
            <tr>
                <td>
                    {description_html}
                </td>
                <td class="num">{good.quantity:.2f}</td>
                <td class="num">{self._format_currency(good.unit_price)}</td>
                <td class="num">{self._format_currency(good.total_amount)}</td>
            </tr>
            """
            rows.append(row)

        return "".join(rows)

    def _get_time_entry_info_html(self, item):
        """Generate HTML for time entry information if available"""
        if item.time_entry_ids:
            count = len(item.time_entry_ids.split(","))
            return f'<br><small class="time-entry-info">Generated from {count} time entries</small>'
        return ""

    def _generate_totals_rows(self):
        """Generate HTML rows for invoice totals"""
        rows = []

        # Subtotal
        rows.append(
            f"""
        <tr>
            <td colspan="3" class="num">Subtotal:</td>
            <td class="num">{self._format_currency(self.invoice.subtotal)}</td>
        </tr>
        """
        )

        # Tax if applicable
        if self.invoice.tax_rate > 0:
            rows.append(
                f"""
            <tr>
                <td colspan="3" class="num">Tax ({self.invoice.tax_rate:.2f}%):</td>
                <td class="num">{self._format_currency(self.invoice.tax_amount)}</td>
            </tr>
            """
            )

        # Total
        rows.append(
            f"""
        <tr>
            <td colspan="3" class="num">Total Amount:</td>
            <td class="num">{self._format_currency(self.invoice.total_amount)}</td>
        </tr>
        """
        )

        return "".join(rows)

    def _get_additional_info_html(self):
        """Generate HTML for additional invoice information"""
        html_parts = []

        if self.invoice.notes:
            html_parts.append(
                f"""
            <div class="notes-section">
                <h4>{_('Notes:')}</h4>
                <p>{self.invoice.notes}</p>
            </div>
            """
            )

        if self.invoice.terms:
            html_parts.append(
                f"""
            <div class="terms-section">
                <h4>{_('Terms:')}</h4>
                <p>{self.invoice.terms}</p>
            </div>
            """
            )

        if html_parts:
            return f'<div class="additional-info">{"".join(html_parts)}</div>'
        return ""

    def _format_currency(self, value):
        """Format numeric currency with thousands separators and 2 decimals."""
        try:
            return f"{float(value):,.2f} {self.settings.currency}"
        except Exception:
            return f"{value} {self.settings.currency}"

    def _get_payment_info_html(self):
        """Generate HTML for payment information"""
        if self.settings.company_bank_info:
            return f"""
            <h4>{_('Payment Information:')}</h4>
            <div class="bank-info">{self.settings.company_bank_info}</div>
            """
        return ""

    def _generate_css(self):
        """Generate CSS styles for the invoice"""
        # Get page size, defaulting to A4
        page_size = self.page_size or "A4"
        # Use .format() instead of f-string to avoid escaping all CSS braces
        return """
        @page {{
            size: {page_size};
            margin: 2cm;
            @bottom-center {{
                content: "Page " counter(page) " of " counter(pages);
                font-size: 10pt;
                color: #666;
            }}
        }}
        
        body {{
            font-family: 'Helvetica Neue', Arial, sans-serif;
            font-size: 12pt;
            line-height: 1.4;
            color: #333;
            margin: 0;
            padding: 0;
        }}
        
        .invoice-container {{
            max-width: 100%;
        }}
        
        .header {{
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 2em;
            border-bottom: 2px solid #007bff;
            padding-bottom: 1em;
        }}
        
        .company-info {{
            flex: 1;
        }}
        
        .company-logo {{
            max-width: 150px;
            max-height: 80px;
            display: block;
            margin-left: auto;
            margin-right: 0;
            margin-bottom: 1em;
        }}
        
        .company-name {{
            font-size: 24pt;
            font-weight: bold;
            color: #007bff;
            margin: 0 0 0.5em 0;
        }}
        
        .company-address {{
            margin-bottom: 0.5em;
            line-height: 1.3;
        }}
        
        .company-contact {{
            margin-bottom: 0.5em;
        }}
        
        .company-contact span {{
            display: block;
            margin-bottom: 0.2em;
            font-size: 10pt;
        }}
        
        .company-tax {{
            font-size: 10pt;
            color: #666;
        }}
        
        .invoice-info {{
            text-align: right;
            min-width: 200px;
        }}
        
        .logo-container {{
            text-align: right;
            margin-bottom: 1em;
        }}
        
        .invoice-title {{
            font-size: 28pt;
            font-weight: bold;
            color: #007bff;
            margin: 0 0 1em 0;
        }}
        
        .invoice-details .detail-row {{
            margin-bottom: 0.5em;
        }}
        
        .detail-row .label {{
            font-weight: bold;
            margin-right: 0.5em;
        }}
        
        .status-draft {{ color: #6c757d; }}
        .status-sent {{ color: #17a2b8; }}
        .status-paid {{ color: #28a745; }}
        .status-overdue {{ color: #dc3545; }}
        .status-cancelled {{ color: #343a40; }}
        
        .client-section, .project-section {{
            margin-bottom: 2em;
        }}
        
        .client-section h3, .project-section h3 {{
            font-size: 14pt;
            font-weight: bold;
            color: #007bff;
            margin: 0 0 0.5em 0;
            border-bottom: 1px solid #dee2e6;
            padding-bottom: 0.3em;
        }}
        
        .client-name {{
            font-weight: bold;
            font-size: 14pt;
            margin-bottom: 0.5em;
        }}
        
        .client-email, .client-address, .project-description {{
            margin-bottom: 0.3em;
            color: #666;
        }}
        
        .items-section {{
            margin-bottom: 2em;
        }}
        
        .invoice-table {{
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 1em;
        }}
        
        .invoice-table th,
        .invoice-table td {{
            border: 1px solid #dee2e6;
            padding: 0.75em;
            text-align: left;
        }}
        
        .invoice-table th {{
            background-color: #f8f9fa;
            font-weight: bold;
            color: #495057;
        }}
        
        .description {{ width: 40%; }}
        .quantity {{ width: 15%; text-align: center; }}
        .unit-price {{ width: 20%; text-align: right; }}
        .total {{ width: 25%; text-align: right; }}
        
        .text-center {{ text-align: center; }}
        .text-right {{ text-align: right; }}
        
        .time-entry-info {{
            color: #6c757d;
            font-style: italic;
        }}
        
        .subtotal {{ background-color: #f8f9fa; }}
        .tax {{ background-color: #fff3cd; }}
        .total {{ background-color: #d1ecf1; font-weight: bold; }}
        
        .additional-info {{
            margin-bottom: 2em;
        }}
        
        .notes-section, .terms-section {{
            margin-bottom: 1em;
        }}
        
        .notes-section h4, .terms-section h4 {{
            font-size: 12pt;
            font-weight: bold;
            color: #495057;
            margin: 0 0 0.5em 0;
        }}
        
        .footer {{
            margin-top: 2em;
            padding-top: 1em;
            border-top: 1px solid #dee2e6;
        }}
        
        .payment-info {{
            margin-bottom: 1em;
        }}
        
        .payment-info h4 {{
            font-size: 12pt;
            font-weight: bold;
            color: #495057;
            margin: 0 0 0.5em 0;
        }}
        
        .bank-info {{
            color: #666;
            line-height: 1.3;
        }}
        
        .terms h4 {{
            font-size: 12pt;
            font-weight: bold;
            color: #495057;
            margin: 0 0 0.5em 0;
        }}
        
        .terms p {{
            color: #666;
            line-height: 1.3;
        }}
        
        /* Utility classes */
        .nl2br {{
            white-space: pre-line;
        }}
        """.format(
            page_size=page_size
        )


def get_overflow_prevention_css():
    """
    Get comprehensive CSS rules to prevent content overflow beyond page boundaries.
    This should be applied to all PDF exports and previews.

    Returns:
        CSS string with overflow prevention rules
    """
    return """
    /* Comprehensive overflow prevention for PDF exports */
    html, body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        box-sizing: border-box;
    }
    
    /* Ensure all wrapper containers respect page boundaries and clip overflow */
    .invoice-wrapper,
    .quote-wrapper,
    .wrapper,
    div[class*="wrapper"],
    div[class*="container"] {
        overflow: hidden !important;
        box-sizing: border-box !important;
        position: relative;
        /* Clip content that extends beyond wrapper boundaries - use strict clipping */
        clip-path: inset(0) !important;
        /* Additional clipping for absolutely positioned children */
        contain: layout style paint;
    }
    
    /* Clip absolutely positioned elements that might overflow page boundaries */
    [style*="position:absolute"],
    [style*="position: fixed"],
    .element, .text-element, .rectangle-element, .circle-element, .line-element {
        box-sizing: border-box;
        /* Ensure positioned elements are clipped by parent wrapper */
        /* Elements must not exceed wrapper boundaries */
        contain: layout style paint;
    }
    
    /* Ensure wrapper strictly clips all children - prevent any overflow */
    .invoice-wrapper,
    .quote-wrapper,
    .wrapper {
        /* Make wrapper a containing block for absolutely positioned children */
        position: relative !important;
        /* Strict clipping - ensure nothing extends beyond wrapper */
        overflow: hidden !important;
        clip-path: inset(0) !important;
    }
    
    /* Constrain absolutely positioned elements to wrapper boundaries */
    /* Elements positioned outside wrapper boundaries will be clipped */
    .invoice-wrapper [style*="position:absolute"],
    .invoice-wrapper [style*="position: fixed"],
    .quote-wrapper [style*="position:absolute"],
    .quote-wrapper [style*="position: fixed"],
    .wrapper [style*="position:absolute"],
    .wrapper [style*="position: fixed"] {
        /* Elements must stay within wrapper - will be clipped by parent overflow */
        box-sizing: border-box;
        contain: layout style paint;
        /* Ensure elements don't extend beyond parent boundaries */
        max-width: 100%;
        max-height: 100%;
    }
    
    /* Specifically constrain elements that might overflow */
    .invoice-wrapper .element,
    .invoice-wrapper .text-element,
    .invoice-wrapper .rectangle-element,
    .invoice-wrapper .circle-element,
    .invoice-wrapper .line-element,
    .quote-wrapper .element,
    .quote-wrapper .text-element,
    .quote-wrapper .rectangle-element,
    .quote-wrapper .circle-element,
    .quote-wrapper .line-element {
        box-sizing: border-box;
        contain: layout style paint;
        /* Prevent overflow beyond wrapper */
        overflow: hidden;
    }
    
    /* Prevent tables from overflowing */
    table {
        max-width: 100%;
        table-layout: auto;
        word-wrap: break-word;
        overflow-wrap: break-word;
    }
    
    /* Prevent images from overflowing */
    img {
        max-width: 100%;
        height: auto;
        object-fit: contain;
    }
    
    /* Prevent text from overflowing containers */
    * {
        word-wrap: break-word;
        overflow-wrap: break-word;
    }
    """


class QuotePDFGenerator:
    """Generate PDF quotes with company branding"""

    def __init__(self, quote, settings=None, page_size="A4"):
        self.quote = quote
        self.settings = settings or Settings.get_settings()
        self.page_size = page_size or "A4"

    def generate_pdf(self):
        """Generate PDF content and return as bytes using ReportLab"""
        import json
        import sys

        from flask import current_app

        def debug_print(msg):
            """Print debug message to stdout with immediate flush for Docker visibility"""
            print(msg, file=sys.stdout, flush=True)
            print(msg, file=sys.stderr, flush=True)
            # Also log using Flask logger if available
            try:
                current_app.logger.info(msg)
            except Exception:
                pass

        quote_id = getattr(self.quote, "id", "N/A")
        quote_number = getattr(self.quote, "quote_number", "N/A")

        debug_print(
            f"\n[PDF_EXPORT] QUOTE PDF GENERATOR - QuoteID: {quote_id}, QuoteNumber: {quote_number}, PageSize: {self.page_size}"
        )
        debug_print(f"{'='*80}\n")
        current_app.logger.info(
            f"[PDF_EXPORT] Starting quote PDF generation - QuoteID: {quote_id}, QuoteNumber: {quote_number}, PageSize: '{self.page_size}'"
        )

        # Get template for the specified page size
        # CRITICAL: Expire all cached objects to ensure we get the latest saved template
        db.session.expire_all()

        current_app.logger.info(
            f"[PDF_EXPORT] Querying database for quote template - PageSize: '{self.page_size}', QuoteID: {quote_id}"
        )

        # CRITICAL: Do a completely fresh query using raw SQL to bypass any ORM caching
        # This ensures we get the absolute latest data from the database
        from sqlalchemy import text

        result = db.session.execute(
            text(
                "SELECT id, page_size, template_json, updated_at FROM quote_pdf_templates WHERE page_size = :page_size"
            ),
            {"page_size": self.page_size},
        ).first()

        template_json_raw_from_db = None
        template = None

        if result:
            template_id, page_size_db, template_json_raw_from_db, updated_at = result
            current_app.logger.info(
                f"[PDF_EXPORT] Quote template found via raw query - PageSize: '{page_size_db}', TemplateID: {template_id}, UpdatedAt: {updated_at}, TemplateJSONLength: {len(template_json_raw_from_db) if template_json_raw_from_db else 0}, QuoteID: {quote_id}"
            )
            # Now get the full template object for use (for other attributes if needed)
            template = QuotePDFTemplate.query.get(template_id)
            # CRITICAL: Use template_json directly from raw query, not from ORM object (which might be cached)
            if template_json_raw_from_db:
                template.template_json = template_json_raw_from_db
            # Force refresh all other attributes
            db.session.refresh(template)
        else:
            current_app.logger.warning(
                f"[PDF_EXPORT] Quote template not found for PageSize: '{self.page_size}', creating default - QuoteID: {quote_id}"
            )
            template = QuotePDFTemplate.get_template(self.page_size)
            template_json_raw_from_db = template.template_json

        # Store template as instance variable for use in format_date
        self.template = template

        debug_print(f"[DEBUG] Retrieved quote template: page_size={template.page_size}, id={template.id}")
        template_json_to_use = template_json_raw_from_db if template_json_raw_from_db else template.template_json
        template_json_length = len(template_json_to_use) if template_json_to_use else 0
        template_json_preview = (
            (template_json_to_use[:100] + "...")
            if template_json_to_use and len(template_json_to_use) > 100
            else (template_json_to_use or "(empty)")
        )
        # Also get a hash/fingerprint of the JSON to verify it's actually the saved one
        import hashlib

        template_json_hash = (
            hashlib.md5(template_json_to_use.encode("utf-8")).hexdigest()[:16] if template_json_to_use else "none"
        )
        current_app.logger.info(
            f"[PDF_EXPORT] Quote template retrieved - PageSize: '{template.page_size}', TemplateID: {template.id}, HasJSON: {bool(template_json_to_use)}, JSONLength: {template_json_length}, JSONHash: {template_json_hash}, JSONPreview: {template_json_preview}, UpdatedAt: {template.updated_at}, QuoteID: {quote_id}"
        )

        # Get or generate ReportLab template JSON
        template_json_dict = None
        # CRITICAL: Use template_json_raw_from_db (from raw query) - this is the absolute latest from database
        # template_json_to_use is already set above
        # Check if template_json exists and is not empty/whitespace
        if template_json_to_use and template_json_to_use.strip():
            try:
                current_app.logger.info(
                    f"[PDF_EXPORT] Parsing quote template JSON - PageSize: '{self.page_size}', JSON length: {len(template_json_to_use)}, QuoteID: {quote_id}"
                )
                template_json_dict = json.loads(template_json_to_use)
                element_count = len(template_json_dict.get("elements", []))
                json_page_size = template_json_dict.get("page", {}).get("size", "unknown")
                # Get first few element types for debugging
                element_types = [elem.get("type", "unknown") for elem in template_json_dict.get("elements", [])[:5]]
                debug_print(f"[DEBUG] Found ReportLab template JSON (length: {len(template_json_to_use)})")
                current_app.logger.info(
                    f"[PDF_EXPORT] Quote template JSON parsed successfully - PageSize: '{self.page_size}', JSON PageSize: '{json_page_size}', Elements: {element_count}, FirstElementTypes: {element_types}, QuoteID: {quote_id}"
                )
            except Exception as e:
                debug_print(f"[WARNING] Failed to parse template_json: {e}")
                template_json_preview_use = (
                    (template_json_to_use[:100] + "...")
                    if template_json_to_use and len(template_json_to_use) > 100
                    else (template_json_to_use or "(empty)")
                )
                current_app.logger.error(
                    f"[PDF_EXPORT] Failed to parse quote template JSON - PageSize: '{self.page_size}', Error: {str(e)}, JSONPreview: {template_json_preview_use}, QuoteID: {quote_id}",
                    exc_info=True,
                )
                template_json_dict = None
        else:
            # Log why template_json is not being used
            reason = "template_json is None" if template_json_to_use is None else "template_json is empty or whitespace"
            current_app.logger.warning(
                f"[PDF_EXPORT] Quote template JSON is empty/whitespace - PageSize: '{self.page_size}', TemplateID: {template.id}, Reason: {reason}, TemplateJSONLength: {len(template_json_to_use) if template_json_to_use else 0}, QuoteID: {quote_id}"
            )

        # If no JSON template exists, ensure it's populated with default (will save to database if empty)
        if not template_json_dict:
            debug_print(
                f"[DEBUG] No quote template JSON found, ensuring default template JSON for page size {self.page_size}"
            )
            current_app.logger.info(
                f"[PDF_EXPORT] Quote template JSON is empty, ensuring default template - PageSize: '{self.page_size}', "
                f"TemplateID: {template.id}, QuoteID: {quote_id}"
            )

            # Call ensure_template_json() which will populate with default if empty/invalid
            # This saves the default to the database, so it's available for future exports
            # It only saves if template_json is truly empty/invalid, not if it's a valid custom template
            template.ensure_template_json()

            # Re-query template_json from database to get the updated value (avoid ORM caching)
            db.session.expire(template)
            result_updated = db.session.execute(
                text("SELECT template_json FROM quote_pdf_templates WHERE id = :template_id"),
                {"template_id": template.id},
            ).first()

            if result_updated and result_updated[0]:
                template_json_to_use = result_updated[0]
                try:
                    template_json_dict = json.loads(template_json_to_use)
                    element_count = len(template_json_dict.get("elements", []))
                    debug_print(
                        f"[DEBUG] Retrieved default quote template JSON with {element_count} elements (saved to DB)"
                    )
                    current_app.logger.info(
                        f"[PDF_EXPORT] Default quote template JSON retrieved from database - PageSize: '{self.page_size}', "
                        f"Elements: {element_count}, QuoteID: {quote_id}"
                    )
                except Exception as e:
                    current_app.logger.error(
                        f"[PDF_EXPORT] Failed to parse quote template JSON after ensure_template_json() - PageSize: '{self.page_size}', Error: {str(e)}, QuoteID: {quote_id}",
                        exc_info=True,
                    )
                    # Fall back to generating default in memory if parsing fails
                    from app.utils.pdf_template_schema import get_default_template

                    template_json_dict = get_default_template(self.page_size)
            else:
                # Fallback: generate default in memory if ensure_template_json() didn't work
                current_app.logger.warning(
                    f"[PDF_EXPORT] ensure_template_json() didn't populate quote template_json, using in-memory default - PageSize: '{self.page_size}', TemplateID: {template.id}, QuoteID: {quote_id}"
                )
                from app.utils.pdf_template_schema import get_default_template

                template_json_dict = get_default_template(self.page_size)

        # Always use ReportLab template renderer with JSON
        debug_print(f"[DEBUG] Using ReportLab template renderer for page size {self.page_size}")
        from app.utils.pdf_generator_reportlab import ReportLabTemplateRenderer
        from app.utils.pdf_template_schema import validate_template_json

        # Validate template JSON
        current_app.logger.info(
            f"[PDF_EXPORT] Validating quote template JSON - PageSize: '{self.page_size}', QuoteID: {quote_id}"
        )
        is_valid, error = validate_template_json(template_json_dict)
        if not is_valid:
            debug_print(f"[ERROR] Template JSON validation failed: {error}")
            current_app.logger.error(
                f"[PDF_EXPORT] Quote template JSON validation failed - PageSize: '{self.page_size}', Error: {error}, QuoteID: {quote_id}"
            )
            # Even if validation fails, try to render with default fallback
            return self._generate_pdf_with_default()
        else:
            current_app.logger.info(
                f"[PDF_EXPORT] Quote template JSON validation passed - PageSize: '{self.page_size}', QuoteID: {quote_id}"
            )

        # Prepare data context for template rendering
        current_app.logger.info(
            f"[PDF_EXPORT] Preparing quote template context - PageSize: '{self.page_size}', QuoteID: {quote_id}"
        )
        data_context = self._prepare_quote_template_context()

        # Render PDF using ReportLab
        current_app.logger.info(
            f"[PDF_EXPORT] Creating ReportLab renderer for quote - PageSize: '{self.page_size}', QuoteID: {quote_id}"
        )
        renderer = ReportLabTemplateRenderer(template_json_dict, data_context, self.page_size)
        try:
            current_app.logger.info(
                f"[PDF_EXPORT] Starting ReportLab render for quote - PageSize: '{self.page_size}', QuoteID: {quote_id}"
            )
            pdf_bytes = renderer.render_to_bytes()
            pdf_size_bytes = len(pdf_bytes)
            debug_print(f"[DEBUG] ReportLab PDF generated successfully - size: {pdf_size_bytes} bytes")
            current_app.logger.info(
                f"[PDF_EXPORT] ReportLab quote PDF generated successfully - PageSize: '{self.page_size}', PDFSize: {pdf_size_bytes} bytes, QuoteID: {quote_id}"
            )
            return pdf_bytes
        except Exception as e:
            debug_print(f"[ERROR] ReportLab rendering failed: {e}")
            import traceback

            debug_print(traceback.format_exc())
            current_app.logger.error(
                f"[PDF_EXPORT] ReportLab quote rendering failed - PageSize: '{self.page_size}', Error: {str(e)}, QuoteID: {quote_id}",
                exc_info=True,
            )
            # Fall back to default generation
            return self._generate_pdf_with_default()

    def _prepare_quote_template_context(self):
        """Prepare data context for quote template rendering"""
        # Convert SQLAlchemy objects to simple structures for template
        from types import SimpleNamespace

        # Create quote wrapper
        quote_wrapper = SimpleNamespace()
        for attr in [
            "id",
            "quote_number",
            "title",
            "description",
            "status",
            "subtotal",
            "tax_rate",
            "tax_amount",
            "total_amount",
            "discount_type",
            "discount_amount",
            "discount_reason",
            "coupon_code",
            "currency_code",
            "notes",
            "terms",
            "valid_until",
            "created_at",
            "updated_at",
        ]:
            try:
                setattr(quote_wrapper, attr, getattr(self.quote, attr))
            except AttributeError:
                pass

        # Convert relationships to lists
        try:
            if hasattr(self.quote.items, "all"):
                quote_wrapper.items = self.quote.items.all()
            else:
                quote_wrapper.items = list(self.quote.items) if self.quote.items else []
        except Exception:
            quote_wrapper.items = []

        # Client
        if hasattr(self.quote, "client") and self.quote.client:
            quote_wrapper.client = self.quote.client
        else:
            quote_wrapper.client = None

        # Project
        quote_wrapper.project = self.quote.project if hasattr(self.quote, "project") else None

        # Settings
        settings_wrapper = SimpleNamespace()
        for attr in [
            "company_name",
            "company_address",
            "company_email",
            "company_phone",
            "company_website",
            "company_tax_id",
            "currency",
        ]:
            try:
                setattr(settings_wrapper, attr, getattr(self.settings, attr))
            except AttributeError:
                pass

        def has_logo():
            return self.settings.has_logo()

        def get_logo_path():
            return self.settings.get_logo_path()

        settings_wrapper.has_logo = has_logo
        settings_wrapper.get_logo_path = get_logo_path

        # Helper functions for templates
        from babel.dates import format_date as babel_format_date

        from app.utils.template_filters import get_image_base64, get_logo_base64

        # Get date format from template, default to %d.%m.%Y
        date_format_str = (
            getattr(self.template, "date_format", "%d.%m.%Y")
            if hasattr(self, "template") and self.template
            else "%d.%m.%Y"
        )

        def format_date(value, format="medium"):
            try:
                # Use date format from template settings
                return value.strftime(date_format_str) if value else ""
            except Exception:
                return str(value) if value else ""

        def format_money(value):
            try:
                currency = getattr(quote_wrapper, "currency_code", None) or self.settings.currency
                return f"{float(value):,.2f} {currency}"
            except Exception:
                currency = getattr(quote_wrapper, "currency_code", None) or self.settings.currency
                return f"{value} {currency}"

        return {
            "quote": quote_wrapper,
            "invoice": quote_wrapper,  # Some templates use 'invoice' instead of 'quote'
            "settings": settings_wrapper,
            "get_logo_base64": get_logo_base64,
            "format_date": format_date,
            "format_money": format_money,
        }

    def _generate_pdf_with_default(self):
        """Generate PDF using default fallback ReportLab generator"""
        from app.utils.pdf_generator_fallback import QuotePDFGeneratorFallback

        fallback = QuotePDFGeneratorFallback(self.quote, settings=self.settings)
        return fallback.generate_pdf()

    def _render_from_custom_template(self, template=None):
        """Render HTML and CSS from custom templates stored in database, with fallback to default template."""
        import sys

        def debug_print(msg):
            """Print debug message to stdout with immediate flush for Docker visibility"""
            print(msg, file=sys.stdout, flush=True)
            print(msg, file=sys.stderr, flush=True)

        if template:
            # Ensure template matches the selected page size
            if hasattr(template, "page_size") and template.page_size != self.page_size:
                correct_template = QuotePDFTemplate.query.filter_by(page_size=self.page_size).first()
                if correct_template:
                    template = correct_template
                else:
                    raise ValueError(f"Template for page size {self.page_size} not found")

            html_template = template.template_html or ""
            css_template = template.template_css or ""
        else:
            raise ValueError(f"No template provided for page size {self.page_size}. This is a bug.")

        html = ""

        def remove_page_rule_from_html(html_text):
            """Remove @page rules from HTML inline styles to avoid conflicts with separate CSS"""
            import re

            def remove_from_style_tag(match):
                style_content = match.group(2)
                brace_count = 0
                page_pattern = r"@page\s*\{"
                page_match = re.search(page_pattern, style_content, re.IGNORECASE)

                if page_match:
                    start = page_match.start()
                    end = len(style_content)
                    for i in range(page_match.end() - 1, len(style_content)):
                        if style_content[i] == "{":
                            brace_count += 1
                        elif style_content[i] == "}":
                            brace_count -= 1
                            if brace_count == 0:
                                end = i + 1
                                break
                    style_content = style_content[:start] + style_content[end:]
                    style_content = re.sub(r"\n\s*\n", "\n", style_content)

                return f"{match.group(1)}{style_content}{match.group(3)}"

            style_pattern = r"(<style[^>]*>)(.*?)(</style>)"
            if re.search(style_pattern, html_text, re.IGNORECASE | re.DOTALL):
                html_text = re.sub(style_pattern, remove_from_style_tag, html_text, flags=re.IGNORECASE | re.DOTALL)

            return html_text

        import re

        css_to_use = ""
        html_inline_styles_extracted = False

        # Extract inline styles from HTML if present
        extracted_inline_css = ""
        if html_template and "<style>" in html_template:
            style_match = re.search(r"<style[^>]*>(.*?)</style>", html_template, re.IGNORECASE | re.DOTALL)
            if style_match:
                extracted_inline_css = style_match.group(1)
                html_inline_styles_extracted = True

        if css_template and css_template.strip():
            debug_print(f"[DEBUG] Using separate CSS template (length: {len(css_template)})")

            before_match = re.search(r"@page\s*\{[^}]*?size\s*:\s*([^;}\n]+)", css_template, re.IGNORECASE | re.DOTALL)
            if before_match:
                before_size = before_match.group(1).strip()
                debug_print(f"[DEBUG] CSS template @page size BEFORE update: '{before_size}'")

            css_to_use = update_page_size_in_css(css_template, self.page_size)

            # Update wrapper dimensions to match page size (fixes hardcoded dimension issues)
            css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
            debug_print(f"[DEBUG] Updated wrapper dimensions in template CSS for page size: {self.page_size}")

            # Validate @page size after update
            is_valid, found_sizes = validate_page_size_in_css(css_to_use, self.page_size)
            if not is_valid:
                debug_print(f"[ERROR] @page size validation failed! Expected '{self.page_size}', found: {found_sizes}")
                current_app.logger.warning(
                    f"Quote PDF template CSS @page size mismatch. Expected '{self.page_size}', found: {found_sizes}"
                )
            else:
                debug_print(f"[DEBUG] ✓ CSS template @page size correctly updated and validated: '{self.page_size}'")
        elif extracted_inline_css:
            css_to_use = update_page_size_in_css(extracted_inline_css, self.page_size)
            css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
        else:
            try:
                from flask import render_template as _render_tpl

                css_to_use = _render_tpl("quotes/pdf_styles_default.css")
                css_to_use = update_page_size_in_css(css_to_use, self.page_size)
                css_to_use = update_wrapper_dimensions_in_css(css_to_use, self.page_size)
            except Exception:
                css_to_use = self._generate_css()

        # Ensure @page rule has correct size
        css = css_to_use

        # Add comprehensive overflow prevention CSS
        overflow_css = get_overflow_prevention_css()
        css = css + "\n" + overflow_css

        # Import helper functions for template
        from babel.dates import format_date as babel_format_date

        from app.utils.template_filters import get_image_base64, get_logo_base64

        # Get date format from template, default to %d.%m.%Y
        date_format_str = (
            getattr(self.template, "date_format", "%d.%m.%Y")
            if hasattr(self, "template") and self.template
            else "%d.%m.%Y"
        )

        def format_date(value, format="medium"):
            """Format date for template"""
            # Use date format from template settings
            return value.strftime(date_format_str) if value else ""

        def format_money(value):
            """Format money for template"""
            try:
                return f"{float(value):,.2f}"
            except Exception:
                return str(value)

        # Convert lazy='dynamic' relationships to lists for template rendering
        try:
            if hasattr(self.quote.items, "all"):
                quote_items = self.quote.items.all()
            else:
                quote_items = list(self.quote.items) if self.quote.items else []
        except Exception:
            quote_items = []

        # Create a wrapper object that has the converted lists
        from types import SimpleNamespace

        quote_data = SimpleNamespace()
        # Copy all attributes from original quote
        for attr in dir(self.quote):
            if not attr.startswith("_"):
                try:
                    setattr(quote_data, attr, getattr(self.quote, attr))
                except Exception:
                    pass
        # Override with converted lists
        quote_data.items = quote_items

        # Load decorative images
        try:
            from app.models import QuoteImage

            decorative_images = QuoteImage.get_quote_images(self.quote.id)
        except Exception:
            decorative_images = []
        quote_data.decorative_images = decorative_images

        try:
            # Render using Flask's Jinja environment
            if html_template:
                from app.utils.safe_template_render import render_sandboxed_string

                # When we have separate CSS, remove @page rules from HTML inline styles
                if html_inline_styles_extracted and css_template:
                    html_page_rules = re.findall(r"@page\s*\{[^}]*\}", html_template, re.IGNORECASE | re.DOTALL)
                    if html_page_rules:
                        debug_print(
                            f"[DEBUG] Found {len(html_page_rules)} @page rule(s) in HTML inline styles - removing them"
                        )
                    html_template_updated = remove_page_rule_from_html(html_template)
                    debug_print("[DEBUG] Removed @page rules from HTML inline styles")
                else:
                    html_template_updated = html_template

                html = render_sandboxed_string(
                    html_template_updated,
                    autoescape=True,
                    quote=quote_data,
                    settings=self.settings,
                    Path=Path,
                    get_logo_base64=get_logo_base64,
                    get_image_base64=get_image_base64,
                    format_date=format_date,
                    format_money=format_money,
                    now=datetime.now(),
                )
        except Exception as e:
            import traceback

            print(f"Error rendering custom quote PDF template: {e}")
            print(traceback.format_exc())
            html = ""

        if not html:
            try:
                html = render_template(
                    "quotes/pdf_default.html",
                    quote=quote_data,
                    settings=self.settings,
                    Path=Path,
                    get_logo_base64=get_logo_base64,
                    get_image_base64=get_image_base64,
                    format_date=format_date,
                    format_money=format_money,
                    now=datetime.now(),
                )
            except Exception as e:
                import traceback

                print(f"Error rendering default quote PDF template: {e}")
                print(traceback.format_exc())
                html = f"<html><body><h1>{_('Quote')} {self.quote.quote_number}</h1></body></html>"

        return html, css

    def _generate_html(self):
        """Generate HTML content for the quote"""
        return render_template("quotes/pdf_default.html", quote=self.quote, settings=self.settings)

    def _generate_css(self):
        """Generate CSS styles for the quote"""
        page_size = self.page_size or "A4"
        return """
        @page {{
            size: {page_size};
            margin: 2cm;
            @bottom-center {{
                content: "Page " counter(page) " of " counter(pages);
                font-size: 10pt;
                color: #666;
            }}
        }}
        
        body {{
            font-family: 'Helvetica Neue', Arial, sans-serif;
            font-size: 12pt;
            line-height: 1.4;
            color: #333;
            margin: 0;
            padding: 0;
        }}
        """.format(
            page_size=page_size
        )
