# -*- coding: utf-8 -*-
"""FSC Harvest Map — PDF Exporter (Dynamic Fields).

Batch-exports individual A4 portrait PDFs for FSC harvest plots.
Reuses layout / engine.py (LvtEngine) for all layout creation,
signature blocks, legend, north arrow, and label updates.

For each plot:
  1. Create a temp memory layer with ONLY the target feature
  2. Apply highlight renderer + fraction-style label
  3. Lock map CRS to the layer CRS
  4. Set map layers explicitly (temp_plot + aux layers)
  5. Add info block (owner, area, CRS, commune...)
  6. Create layout via LvtEngine → Export PDF at 300 dpi
  7. Remove temp layer

Author: Lộc Vũ Trung (LVT) / Slow Forest
License: GPL-3.0
"""

import os
import re
import unicodedata

from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QColor, QFont
from qgis.core import (
    QgsWkbTypes,
    QgsProject, QgsLayoutExporter, QgsLayoutItemMap, QgsLayoutItemLabel,
    QgsLayoutItemLegend, QgsLegendStyle, QgsLayoutPoint, QgsLayoutSize,
    QgsLayoutMeasurement, QgsRectangle, QgsCoordinateTransform, QgsMessageLog,
    QgsPalLayerSettings, QgsVectorLayerSimpleLabeling, QgsTextFormat, QgsTextBufferSettings,
    QgsFillSymbol, QgsSingleSymbolRenderer, QgsVectorLayer, QgsFeature,
    QgsUnitTypes, Qgis
)


# Nice scale steps for auto-fit
_NICE_SCALES = [1000, 1500, 2000, 2500, 3000, 4000, 5000, 7500,
                10000, 12500, 15000, 20000, 25000]
_NONE = "— (none) —"
_DEG_TO_M = 111320.0  # approximate meters per degree at equator


def _is_geographic(crs):
    """Check if CRS uses geographic (degree) units."""
    if not crs or not crs.isValid():
        return False
    return crs.isGeographic()


def _get_padding(bbox, crs):
    """Get appropriate padding in CRS units.

    For projected CRS: min 10m padding.
    For geographic CRS: min ~20m equivalent in degrees.
    """
    if _is_geographic(crs):
        min_pad = 0.0002  # ~22 meters
    else:
        min_pad = 10  # 10 meters
    pad_x = max(bbox.width() * 0.3, min_pad)
    pad_y = max(bbox.height() * 0.3, min_pad)
    return pad_x, pad_y


def _auto_scale(bbox, crs=None, map_w_mm=180, map_h_mm=220, padding=1.3):
    """Calculate auto-fit scale from bounding box.

    Handles both projected (meters) and geographic (degrees) CRS.
    """
    from qgis.core import QgsMessageLog
    bw = bbox.width() * padding
    bh = bbox.height() * padding
    if bw <= 0 or bh <= 0:
        return 2000

    is_geo = crs and _is_geographic(crs)
    # Also detect degree-scale bbox even if CRS claims projected
    # (happens when transform was skipped or failed)
    if not is_geo and bw < 1.0 and bh < 1.0:
        # bbox values < 1 in a projected CRS = likely still in degrees
        is_geo = True

    if is_geo:
        bw *= _DEG_TO_M
        bh *= _DEG_TO_M

    scale_x = bw / (map_w_mm / 1000.0)
    scale_y = bh / (map_h_mm / 1000.0)
    raw = max(scale_x, scale_y)

    QgsMessageLog.logMessage(
        f"[FSC] _auto_scale: bbox_w={bbox.width():.6f} "
        f"bbox_h={bbox.height():.6f} is_geo={is_geo} "
        f"raw={raw:.1f} crs={crs.authid() if crs else 'None'}",
        "LVT4U")

    for s in _NICE_SCALES:
        if s >= raw:
            return s
    return _NICE_SCALES[-1]


def _safe_filename(text):
    """Convert Vietnamese text to safe ASCII filename."""
    if not text:
        return "unknown"
    nfkd = unicodedata.normalize("NFKD", str(text))
    ascii_str = nfkd.encode("ASCII", "ignore").decode("ASCII")
    safe = re.sub(r"[^A-Za-z0-9_\-]", "_", ascii_str)
    safe = re.sub(r"_+", "_", safe).strip("_")
    return safe or "unknown"


def _fget(feat, field_name, none_marker=_NONE):
    """Safely get feature attribute value as string."""
    if not field_name or field_name == none_marker:
        return ""
    names = feat.fields().names()
    if field_name not in names:
        return ""
    v = feat[field_name]
    if v is None or str(v) == "NULL":
        return ""
    return str(v)


def _parse_month_year(month_str, year_str):
    """Parse month / year, handling Excel serial dates.

    Excel serial: 44432 = 2021-09-01, 45535 = 2024-09-01
    Returns (month_display, year_display) as strings.
    """
    from datetime import datetime, timedelta

    month = month_str
    year = year_str

    # Detect Excel serial date in month field (5-digit numbers)
    if month and month.isdigit() and len(month) >= 5:
        try:
            serial = int(month)
            # Excel epoch: Jan 1, 1900 = serial 1
            dt = datetime(1899, 12, 30) + timedelta(days=serial)
            month = str(dt.month)
            if not year:
                year = str(dt.year)
        except (ValueError, OverflowError):
            pass

    # Also check year field for serial dates
    if year and year.isdigit() and len(year) >= 5:
        try:
            serial = int(year)
            dt = datetime(1899, 12, 30) + timedelta(days=serial)
            year = str(dt.year)
            if not month or month == month_str:  # month not yet parsed
                month = str(dt.month)
        except (ValueError, OverflowError):
            pass

    return month, year


def _create_single_feature_layer(feat, crs, layer_name="__fsc_plot__"):
    """Create a temporary memory layer containing exactly ONE feature."""
    geom_type = QgsWkbTypes.displayString(feat.geometry().wkbType())
    uri = f"{geom_type}?crs={crs.authid()}"
    mem = QgsVectorLayer(uri, layer_name, "memory")
    dp = mem.dataProvider()

    # Copy all fields
    dp.addAttributes(feat.fields().toList())
    mem.updateFields()

    # Copy the feature
    new_feat = QgsFeature(mem.fields())
    new_feat.setGeometry(feat.geometry())
    for field in feat.fields():
        if field.name() in [f.name() for f in mem.fields()]:
            new_feat[field.name()] = feat[field.name()]
    dp.addFeatures([new_feat])
    mem.updateExtents()

    return mem


def _style_plot_layer(layer):
    """Apply highlight renderer: green fill + red stroke."""
    sym = QgsFillSymbol.createSimple({
        'color': '180,255,180,60',
        'outline_color': '255,0,0,255',
        'outline_width': '0.8',
    })
    layer.setRenderer(QgsSingleSymbolRenderer(sym))


def _apply_label(layer, expr):
    """Apply fraction-style label expression.

    Styled to match the existing QML label standard:
    - Arial 7pt, regular weight
    - Center-aligned multiline
    - Placed at polygon centroid (OverPoint)
    - White buffer for readability
    """
    settings = QgsPalLayerSettings()
    settings.fieldName = expr
    settings.isExpression = True
    try:
        settings.multilineAlign = Qgis.LabelMultiLineAlignment.Center
    except AttributeError:
        try:
            settings.multilineAlign = QgsPalLayerSettings.MultiCenter
        except AttributeError:
            pass

    fmt = QgsTextFormat()
    font = QFont("Arial", 7)
    fmt.setFont(font)
    fmt.setSize(7)
    fmt.setColor(QColor(0, 0, 0))

    buf = QgsTextBufferSettings()
    buf.setEnabled(True)
    buf.setSize(1.2)
    buf.setColor(QColor(255, 255, 255, 200))
    fmt.setBuffer(buf)

    settings.setFormat(fmt)
    layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
    layer.setLabelsEnabled(True)


def _add_fsc_legend(layout, map_layers, map_item=None,
                    x_mm=5, y_mm=235, w_mm=60, h_mm=30):
    """Add a legend showing ONLY the layers used in the map layout.

    - Frame: 0.03mm solid line
    - Font: Arial 7pt
    - Position: bottom-left of page
    - Filters legend to only show map_layers (not all project layers)
    """
    legend = QgsLayoutItemLegend(layout)
    legend.setTitle("Chú giải")

    # Link to map item
    if map_item:
        legend.setLinkedMap(map_item)

    # Add to layout first (stable object lifetime)
    legend.setId("fsc_legend")
    layout.addLayoutItem(legend)

    # Disable auto-update so we can manually control which layers appear
    legend.setAutoUpdateModel(False)

    # Clear all existing layers from legend, then add only map_layers
    root = legend.model().rootGroup()
    root.removeAllChildren()
    for lyr in map_layers:
        if lyr and lyr.isValid():
            root.addLayer(lyr)

    # Font settings — Title: Arial 12pt bold, rest: Arial 7pt
    for style_type in [QgsLegendStyle.Title,
                       QgsLegendStyle.Group,
                       QgsLegendStyle.Subgroup,
                       QgsLegendStyle.SymbolLabel]:
        style = legend.style(style_type)
        if style_type == QgsLegendStyle.Title:
            f = QFont("Arial", 12)
            f.setBold(True)
        else:
            f = QFont("Arial", 7)
        style.setFont(f)
        legend.setStyle(style_type, style)

    # Symbol size
    legend.setSymbolWidth(5)
    legend.setSymbolHeight(3.5)

    # Frame: 0.03mm
    legend.setFrameEnabled(True)
    legend.setFrameStrokeWidth(
        QgsLayoutMeasurement(0.03, QgsUnitTypes.LayoutMillimeters))
    legend.setFrameStrokeColor(QColor(0, 0, 0))

    # Background
    legend.setBackgroundEnabled(True)
    legend.setBackgroundColor(QColor(255, 255, 255, 230))

    # Position + size
    legend.attemptMove(
        QgsLayoutPoint(x_mm, y_mm, QgsUnitTypes.LayoutMillimeters))
    legend.attemptResize(
        QgsLayoutSize(w_mm, h_mm, QgsUnitTypes.LayoutMillimeters))
    legend.setResizeToContents(True)

    return legend


def preview_plot(iface, plugin_dir, feat, params):
    """Create a layout for ONE feature and return it for Designer preview.

    Unlike export_plots, this does NOT export to PDF. The layout stays
    in the project's layout manager so the user can inspect it in the
    QGIS Layout Designer. Temp layers are also kept alive.

    Returns:
        QgsPrintLayout: The created layout, or None on failure.
    """
    from ..layout.engine import LvtEngine

    engine = LvtEngine(iface, os.path.join(plugin_dir, "layout"))
    none_marker = params.get("none_marker", _NONE)

    main_layer = params.get("main_layer")
    if not main_layer:
        raise ValueError("No main layer provided")

    project = QgsProject.instance()
    layer_crs = main_layer.crs()

    # Map CRS: use user-selected CRS from dialog, fallback to layer CRS
    map_crs = params.get("map_crs") or main_layer.crs()
    crs_display = map_crs.description() if map_crs.isValid() else "N / A"

    # Dynamic field extraction
    owner = _fget(feat, params.get("owner_field"), none_marker)
    plot_id = _fget(feat, params.get("plot_id_field"), none_marker)
    area = _fget(feat, params.get("area_field"), none_marker)
    month_raw = _fget(feat, params.get("month_field"), none_marker)
    year_raw = _fget(feat, params.get("year_field"), none_marker)
    month, year = _parse_month_year(month_raw, year_raw)
    commune = _fget(feat, params.get("commune_field"), none_marker)
    _ = _fget(feat, "Thon", none_marker)

    # 1. Temp layer with single feature
    temp_layer = _create_single_feature_layer(
        feat, layer_crs, "Phạm vi lô rừng")
    _style_plot_layer(temp_layer)

    label_expr = params.get("label_expr", "")
    if params.get("show_label", True) and label_expr:
        _apply_label(temp_layer, label_expr)

    project.addMapLayer(temp_layer, False)

    # 3. Calculate extent in map CRS
    bbox = feat.geometry().boundingBox()
    if (layer_crs.authid() != map_crs.authid()
            and layer_crs.isValid() and map_crs.isValid()):  # noqa: W503
        transform = QgsCoordinateTransform(
            layer_crs, map_crs, project)
        bbox = transform.transformBoundingBox(bbox)

    if params.get("custom_scale"):
        scale = params["custom_scale"]
    else:
        scale = _auto_scale(bbox, crs=map_crs)

    padded = QgsRectangle(bbox)
    pad_x, pad_y = _get_padding(bbox, map_crs)
    # Extra bottom padding to center polygon above signature blocks
    bottom_extra = pad_y * 1.0
    padded.setXMinimum(bbox.xMinimum() - pad_x)
    padded.setXMaximum(bbox.xMaximum() + pad_x)
    padded.setYMinimum(bbox.yMinimum() - pad_y - bottom_extra)
    padded.setYMaximum(bbox.yMaximum() + pad_y)

    # 4. Signature blocks
    sig_blocks = []
    if params.get("sig_owner", True):
        sig_blocks.append({
            "title": "CHỦ RỪNG", "date": "", "name": owner,
        })
    if params.get("sig_group", True):
        # Resolve group rep name: from field → manual text → empty
        grp_name = ""
        grp_field = params.get("group_rep_field", "")
        none_marker = params.get("none_marker", "")
        if grp_field and grp_field != none_marker:
            grp_name = _fget(feat, grp_field, none_marker)
        if not grp_name or grp_name == none_marker:
            grp_name = params.get("group_rep_name", "")
        sig_blocks.append({
            "title": "ĐẠI DIỆN NHÓM",
            "date": "", "name": grp_name,
        })

    # Build study_area: "Chủ rừng: X | mã_lô | DT ha | xã | năm | CRS"
    area_display = ""
    if area:
        try:
            area_display = f"{float(area):.2f} ha"
        except ValueError:
            area_display = str(area)

    info_parts = []
    if owner:
        info_parts.append(owner)
    if plot_id:
        info_parts.append(plot_id)
    if area_display:
        info_parts.append(area_display)
    if commune:
        info_parts.append(commune)
    if month and year:
        info_parts.append(f"T{month}/{year}")
    elif year:
        info_parts.append(str(year))
    info_parts.append(crs_display)
    study_area = " | ".join(info_parts)

    # 5. Build layout via engine
    layout_params = {
        "mode": "print",
        "lang": "VN",
        "page_width": 210,
        "page_height": 297,
        "scale": scale,
        "extent_mode": "drawn",
        "drawn_extent": padded,
        "title": params.get("title",
                            "BẢN ĐỒ TRÍCH LỤC LÔ RỪNG TRỒNG"),
        "org_name": params.get("org_name", ""),
        "study_area": study_area,
        "data_sources": params.get("subtitle",
                                   "Quản lý rừng bền vững và "
                                   "Chứng chỉ rừng FSC"),
        "show_scalebar": True,
        "show_legend": params.get("show_legend", True),
        "show_north": True,
        "north_arrow_left": 160,
        "north_arrow_top": 55,
        "north_arrow_w": 20,
        "north_arrow_h": 25,
        "signature_block": len(sig_blocks) > 0,
        "sig_blocks": sig_blocks,
        "sig_height": 40,
        "sig_width": 45,
    }

    layout = engine.create_layout(layout_params)

    # Remove any legacy fsc_info_block items
    _remove = [it for it in layout.items()
               if hasattr(it, 'id') and it.id() and  # noqa: W504
               'fsc_info' in it.id().lower()]
    for it in _remove:
        layout.removeLayoutItem(it)

    # Hide page_size label from layout
    _page_ids = {'kích thước trang in', 'print page size'}
    for it in layout.items():
        if isinstance(it, QgsLayoutItemLabel):
            if it.id() and it.id().lower() in _page_ids:
                it.setVisibility(False)

    # 6. Set map CRS + layers (only the single highlighted plot + aux)
    aux_layers = params.get("aux_layers", [])
    map_layers = [temp_layer] + list(aux_layers)

    map_item_ref = None
    for item in layout.items():
        if isinstance(item, QgsLayoutItemMap):
            # Disable template data-defined scale (hardcoded 10000)
            from qgis.core import QgsLayoutObject, QgsProperty
            item.dataDefinedProperties().setProperty(
                QgsLayoutObject.MapScale,
                QgsProperty())
            item.setCrs(map_crs)
            item.setKeepLayerSet(True)
            item.setLayers(map_layers)
            item.zoomToExtent(padded)
            item.setScale(scale)
            map_item_ref = item
            break

    # Refresh + re-enforce scale (QGIS may revert after refresh)
    from qgis.core import QgsMessageLog
    layout.refresh()
    QCoreApplication.processEvents()
    if map_item_ref:
        map_item_ref.setScale(scale)
        layout.refresh()
        QCoreApplication.processEvents()
        QgsMessageLog.logMessage(
            f"[FSC-preview] Final scale applied: "
            f"1:{map_item_ref.scale():.0f} "
            f"(requested 1:{scale})", "LVT4U")

    # 7. Add legend (positioned right of signature blocks, same height)
    if params.get("show_legend", True) and map_item_ref:
        # Calculate legend position from map frame + sig layout
        map_x = map_item_ref.positionWithUnits().x()
        map_y = map_item_ref.positionWithUnits().y()
        map_w = map_item_ref.sizeWithUnits().width()
        map_h = map_item_ref.sizeWithUnits().height()
        sig_h = layout_params.get("sig_height", 40)
        sig_w = layout_params.get("sig_width", 45)
        sig_ml = layout_params.get("sig_margin_left", 3)
        sig_mb = layout_params.get("sig_margin_bottom", 3)
        n_sig = len(sig_blocks)

        # Same Y as signature blocks
        legend_y = (map_y + map_h) - sig_mb - sig_h
        # X = right after last sig block + small gap
        legend_x = map_x + sig_ml + (n_sig * sig_w) + 2
        # Width = remaining space to right edge of map frame - margin
        legend_w = (map_x + map_w) - legend_x - sig_ml
        legend_h = sig_h  # same height as sig blocks

        if legend_w > 15:  # only add if enough space
            _add_fsc_legend(layout, map_layers, map_item=map_item_ref,
                            x_mm=legend_x, y_mm=legend_y,
                            w_mm=legend_w, h_mm=legend_h)

    layout.refresh()
    QCoreApplication.processEvents()

    # NOTE: Layout + temp layers are intentionally NOT cleaned up
    # They stay in the project so the Designer can display them.
    # User closes Designer → layout remains until project close or
    # next preview replaces it.

    return layout


def export_plots(iface, plugin_dir, features, params, progress_cb=None):
    """Export PDF maps for selected features."""
    from ..layout.engine import LvtEngine

    engine = LvtEngine(iface, os.path.join(plugin_dir, "layout"))
    output_dir = params["output_dir"]
    os.makedirs(output_dir, exist_ok=True)
    none_marker = params.get("none_marker", _NONE)

    main_layer = params.get("main_layer")
    if not main_layer:
        raise ValueError("No main layer provided")

    total = len(features)
    pdf_paths = []

    for idx, feat in enumerate(features):
        owner = _fget(feat, params.get("owner_field"), none_marker)
        plot_id = _fget(feat, params.get("plot_id_field"), none_marker)

        if progress_cb:
            progress_cb(idx + 1, total,
                        f"[{idx+1}/{total}] {owner} — {plot_id}")  # noqa: E226

        try:
            pdf_path = _export_single(
                engine, iface, feat, params, output_dir,
                none_marker, main_layer)
            if pdf_path:
                pdf_paths.append(pdf_path)
        except Exception as e:
            QgsMessageLog.logMessage(
                f"Export error for feature {feat.id()}: {e}",
                "LVT4U", Qgis.Warning
            )

        QCoreApplication.processEvents()

    # Merge PDFs if requested
    if params.get("merge_pdf") and len(pdf_paths) > 1:
        merged = _merge_pdfs(pdf_paths, output_dir, params)
        if merged:
            return [merged]

    return pdf_paths


def _style_main_layer_copy(layer):
    pass  # Unused — kept as no-op for backward compat


def _export_single(engine, iface, feat, params, output_dir,
                   none_marker, main_layer):
    """Export a single feature to PDF using LvtEngine.

    Workflow:
      1. Create temp memory layer with ONLY the target feature
      2. Style + label the temp layer
      3. Add main_layer copy (all plots) for spatial context
      4. Add to project temporarily
      5. Build layout via engine
      6. Lock map CRS (WGS 84 default) + set layers
      7. Set map layers: highlight on top, main below, then aux
      8. Add info block with plot details + CRS
      9. Export PDF
     10. Remove temp layers from project

    Returns:
        str: Path to created PDF, or None on failure.
    """
    project = QgsProject.instance()
    layer_crs = main_layer.crs()

    # Determine map display CRS from user selection
    map_crs = params.get("map_crs") or layer_crs
    crs_display = map_crs.description() if map_crs.isValid() else "N / A"

    # Dynamic field extraction
    owner = _fget(feat, params.get("owner_field"), none_marker)
    plot_id = _fget(feat, params.get("plot_id_field"), none_marker)
    area = _fget(feat, params.get("area_field"), none_marker)
    month_raw = _fget(feat, params.get("month_field"), none_marker)
    year_raw = _fget(feat, params.get("year_field"), none_marker)
    month, year = _parse_month_year(month_raw, year_raw)
    commune = _fget(feat, params.get("commune_field"), none_marker)
    _ = _fget(feat, "Thon", none_marker)

    # ── 1. Create temp memory layer with this ONE feature ──
    temp_layer = _create_single_feature_layer(
        feat, layer_crs, "Phạm vi lô rừng")

    # ── 2. Style + label ──
    _style_plot_layer(temp_layer)

    label_expr = params.get("label_expr", "")
    if params.get("show_label", True) and label_expr:
        _apply_label(temp_layer, label_expr)

    # ── 3. Add temp layer to project (required for layout rendering) ──
    project.addMapLayer(temp_layer, False)

    # ── 4. Calculate extent ──
    # Transform feature bbox to map CRS if needed
    bbox = feat.geometry().boundingBox()
    from qgis.core import QgsMessageLog
    QgsMessageLog.logMessage(
        f"[FSC-export] layer_crs={layer_crs.authid()} "
        f"map_crs={map_crs.authid()} "
        f"raw_bbox=({bbox.width():.6f} x {bbox.height():.6f})",
        "LVT4U")
    if (layer_crs.authid() != map_crs.authid()
            and layer_crs.isValid() and map_crs.isValid()):  # noqa: W503
        transform = QgsCoordinateTransform(
            layer_crs, map_crs, project)
        bbox = transform.transformBoundingBox(bbox)
        QgsMessageLog.logMessage(
            f"[FSC-export] transformed bbox=({bbox.width():.2f} "
            f"x {bbox.height():.2f}) meters",
            "LVT4U")

    if params.get("custom_scale"):
        scale = params["custom_scale"]
        QgsMessageLog.logMessage(
            f"[FSC-export] Using CUSTOM scale: 1:{scale}", "LVT4U")
    else:
        scale = _auto_scale(bbox, crs=map_crs)
        QgsMessageLog.logMessage(
            f"[FSC-export] Using AUTO scale: 1:{scale}", "LVT4U")

    padded = QgsRectangle(bbox)
    pad_x, pad_y = _get_padding(bbox, map_crs)
    # Extra bottom padding to center polygon above signature blocks
    bottom_extra = pad_y * 1.0
    padded.setXMinimum(bbox.xMinimum() - pad_x)
    padded.setXMaximum(bbox.xMaximum() + pad_x)
    padded.setYMinimum(bbox.yMinimum() - pad_y - bottom_extra)
    padded.setYMaximum(bbox.yMaximum() + pad_y)

    # ── 5. Build signature blocks ──
    sig_blocks = []
    if params.get("sig_owner", True):
        sig_blocks.append({
            "title": "CHỦ RỪNG",
            "date": "",
            "name": owner,
        })
    if params.get("sig_group", True):
        # Resolve group rep name: from field → manual text → empty
        grp_name = ""
        grp_field = params.get("group_rep_field", "")
        none_marker = params.get("none_marker", "")
        if grp_field and grp_field != none_marker:
            grp_name = _fget(feat, grp_field, none_marker)
        if not grp_name or grp_name == none_marker:
            grp_name = params.get("group_rep_name", "")
        sig_blocks.append({
            "title": "ĐẠI DIỆN NHÓM",
            "date": "", "name": grp_name,
        })

    # Build study_area: "Chủ rừng: X | mã_lô | DT ha | xã | năm | CRS"
    area_display = ""
    if area:
        try:
            area_display = f"{float(area):.2f} ha"
        except ValueError:
            area_display = str(area)

    info_parts = []
    if owner:
        info_parts.append(owner)
    if plot_id:
        info_parts.append(plot_id)
    if area_display:
        info_parts.append(area_display)
    if commune:
        info_parts.append(commune)
    if month and year:
        info_parts.append(f"T{month}/{year}")
    elif year:
        info_parts.append(str(year))
    info_parts.append(crs_display)
    study_area = " | ".join(info_parts)

    # ── 6. Build layout via engine ──
    layout_params = {
        "mode": "print",
        "lang": "VN",
        "page_width": 210,
        "page_height": 297,
        "scale": scale,
        "extent_mode": "drawn",
        "drawn_extent": padded,
        "title": params.get("title",
                            "BẢN ĐỒ TRÍCH LỤC LÔ RỪNG TRỒNG"),
        "org_name": params.get("org_name", ""),
        "study_area": study_area,
        "data_sources": params.get("subtitle",
                                   "Quản lý rừng bền vững và "
                                   "Chứng chỉ rừng FSC"),
        "show_scalebar": True,
        "show_legend": params.get("show_legend", True),
        "show_north": True,
        "north_arrow_left": 160,
        "north_arrow_top": 55,
        "north_arrow_w": 20,
        "north_arrow_h": 25,
        "signature_block": len(sig_blocks) > 0,
        "sig_blocks": sig_blocks,
        "sig_height": 40,
        "sig_width": 45,
    }

    layout = engine.create_layout(layout_params)

    # Remove any legacy fsc_info_block items
    _remove = [it for it in layout.items()
               if hasattr(it, 'id') and it.id() and  # noqa: W504
               'fsc_info' in it.id().lower()]
    for it in _remove:
        layout.removeLayoutItem(it)

    # Hide page_size label from layout
    _page_ids = {'kích thước trang in', 'print page size'}
    for it in layout.items():
        if isinstance(it, QgsLayoutItemLabel):
            if it.id() and it.id().lower() in _page_ids:
                it.setVisibility(False)

    # ── 7. Set map CRS + layers (only highlighted plot + aux) ──
    aux_layers = params.get("aux_layers", [])
    map_layers = [temp_layer] + list(aux_layers)

    map_item_ref = None
    for item in layout.items():
        if isinstance(item, QgsLayoutItemMap):
            # Disable template data-defined scale (hardcoded 10000)
            from qgis.core import QgsLayoutObject, QgsProperty
            item.dataDefinedProperties().setProperty(
                QgsLayoutObject.MapScale,
                QgsProperty())
            item.setCrs(map_crs)
            item.setKeepLayerSet(True)
            item.setLayers(map_layers)
            item.zoomToExtent(padded)
            item.setScale(scale)
            map_item_ref = item
            break

    # Refresh + re-enforce scale (QGIS may revert after refresh)
    layout.refresh()
    QCoreApplication.processEvents()
    if map_item_ref:
        map_item_ref.setScale(scale)
        layout.refresh()
        QCoreApplication.processEvents()
        QgsMessageLog.logMessage(
            f"[FSC-export] Final scale applied: "
            f"1:{map_item_ref.scale():.0f} "
            f"(requested 1:{scale})", "LVT4U")

    # 8. Add legend (positioned right of signature blocks, same height)
    if params.get("show_legend", True) and map_item_ref:
        map_x = map_item_ref.positionWithUnits().x()
        map_y = map_item_ref.positionWithUnits().y()
        map_w = map_item_ref.sizeWithUnits().width()
        map_h = map_item_ref.sizeWithUnits().height()
        sig_h = layout_params.get("sig_height", 40)
        sig_w = layout_params.get("sig_width", 45)
        sig_ml = layout_params.get("sig_margin_left", 3)
        sig_mb = layout_params.get("sig_margin_bottom", 3)
        n_sig = len(sig_blocks)

        legend_y = (map_y + map_h) - sig_mb - sig_h
        legend_x = map_x + sig_ml + (n_sig * sig_w) + 2
        legend_w = (map_x + map_w) - legend_x - sig_ml
        legend_h = sig_h

        if legend_w > 15:
            _add_fsc_legend(layout, map_layers, map_item=map_item_ref,
                            x_mm=legend_x, y_mm=legend_y,
                            w_mm=legend_w, h_mm=legend_h)

    layout.refresh()
    QCoreApplication.processEvents()

    # ── 9. Export PDF ──
    month_str = f"T{month}" if month else ""
    filename = f"{_safe_filename(owner)}_{_safe_filename(plot_id)}"
    if month_str and year:
        filename += f"_{month_str}_{year}"
    elif year:
        filename += f"_{year}"
    from datetime import datetime as _dt
    filename += f"_{_dt.now().strftime('%d%m%Y_%H%M%S')}.pdf"

    pdf_path = os.path.join(output_dir, filename)

    exporter = QgsLayoutExporter(layout)
    settings = QgsLayoutExporter.PdfExportSettings()
    settings.dpi = 300

    result = exporter.exportToPdf(pdf_path, settings)

    # ── 10. Cleanup ──
    project.layoutManager().removeLayout(layout)
    project.removeMapLayer(temp_layer.id())

    if result == QgsLayoutExporter.Success:
        return pdf_path
    else:
        QgsMessageLog.logMessage(
            f"PDF export failed for {plot_id}: error code {result}",
            "LVT4U", Qgis.Warning
        )
        return None


def _merge_pdfs(pdf_paths, output_dir, params):
    """Merge multiple PDFs into one file."""
    try:
        from PyPDF2 import PdfMerger
    except ImportError:
        QgsMessageLog.logMessage(
            "PyPDF2 not installed — cannot merge PDFs. "
            "Install with: pip install PyPDF2",
            "LVT4U", Qgis.Warning
        )
        return None

    from datetime import datetime as _dt
    n_plots = len(pdf_paths)
    ts = _dt.now().strftime('%d%m%Y_%H%M%S')
    merged_name = f"Xuat_{n_plots}_lo_rung_gop_{ts}.pdf"
    merged_path = os.path.join(output_dir, merged_name)

    merger = PdfMerger()
    for p in pdf_paths:
        merger.append(p)
    merger.write(merged_path)
    merger.close()

    for p in pdf_paths:
        try:
            os.remove(p)
        except OSError:
            pass

    return merged_path
