# -*- coding: utf-8 -*-
"""LVT Map Layout - Engine: template loading, layout creation.

Design principle: templates are SELF-CONTAINED.  They handle ALL
positioning, grids, and frames via data-defined expressions.
This engine only:
  1. Loads the template
  2. Sets page size
  3. Sets map extent & scale
  4. Refreshes until expressions converge
  5. Updates user-provided labels
  6. Toggles element visibility per user checkboxes
"""

import contextlib
import os

from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (
    QgsProject,
    QgsPrintLayout,
    QgsReadWriteContext,
    QgsLayoutItemMap,
    QgsLayoutItemLabel,
    QgsLayoutItemLegend,
    QgsLayoutItemPicture,
    QgsLayoutObject,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsProperty,
    QgsUnitTypes,
)

from ..shared.qt_compat import QtCompat, MsgButton


# Template file mapping: (mode, lang) → filename
TEMPLATES = {
    ("slide", "EN"): "Khung_Slide_EN.qpt",
    ("slide", "VN"): "Khung_Slide.qpt",
    ("print", "EN"): "Khung LVT2601_print_27Apr2026_EN.qpt",
    ("print", "VN"): "Khung LVT2601_print_27Apr2026_VN.qpt",
}

# Element IDs that the engine may update (labels, scalebars, coords).
# These are the ONLY template items the plugin touches — everything
# else (grids, frames, positions) is left to the template.
ITEM_IDS = {
    ("slide", "EN"): {
        "map": "Map",
        "map_data": "Map data",
        "scalebar_m": "Scale meter",
    },
    ("slide", "VN"): {
        "map": "Map",
        "map_data": "Dữ liệu bản đồ",
        "scalebar_m": "Scale mét",
    },
    ("print", "EN"): {
        "map": "Map",
        "title": "Map title",
        "org_name": "Map org name",
        "study_area": "Study area",
        "map_data": "Map data",
        "references": "References",
        "page_size": "Print page size",
        "scalebar_m": "Scale meter",
        "scalebar_deg": "Scalebar deg min",
        "main_frame": "Main frame",
    },
    ("print", "VN"): {
        "map": "Map",
        "title": "Tên bản đồ",
        "org_name": "Tên đv xd bản đồ",
        "study_area": "Khu vực bản đồ",
        "map_data": "Dữ liệu bản đồ",
        "references": "Viện dẫn",
        "page_size": "Kích thước trang in",
        "scalebar_m": "Scale mét",
        "scalebar_deg": "Scalebar độ phút",
        "main_frame": "Khung tổng",
    },
}


class LvtEngine:
    """Core engine: loads .qpt templates, builds layouts."""

    def __init__(self, iface, plugin_dir):
        self.iface = iface
        self.plugin_dir = plugin_dir
        self._layout_counter = 0
        self._tpl_cache = {}  # (mode, lang) → XML string

    # ── Template Resolution ──────────────────────────────────────

    def _resolve_template(self, mode, lang):
        """Find the .qpt template file."""
        fname = TEMPLATES[(mode, lang)]
        p = os.path.join(self.plugin_dir, "templates", fname)
        if os.path.exists(p):
            return p
        p = os.path.join(os.path.dirname(self.plugin_dir), fname)
        if os.path.exists(p):
            return p
        raise FileNotFoundError(
            f"Template not found: {fname}\n"
            f"Place it in: {os.path.join(self.plugin_dir, 'templates')}"
        )

    @staticmethod
    def _patch_template_xml(xml_str):
        """Patch golden template XML in-memory (never modifies files).

        Fixes known issues in the golden templates that cannot be edited:
        - Main grid (intervalX=10000): rightAnnotationDisplay 3→1 (HideAll→ShowLatitudeOnly)
          so Y (Northing) annotations appear on the right border.
        - rightAnnotationDirection 0→4 (Horizontal→VerticalDescending) to match left side.
        """
        import re

        def _fix_main_grid_line(m):
            line = m.group(0)
            # Only patch the main grid (identified by intervalX="10000" + showAnnotation="1")
            if 'intervalX="10000"' in line and 'showAnnotation="1"' in line:
                line = line.replace('rightAnnotationDisplay="3"',
                                    'rightAnnotationDisplay="1"')
                line = line.replace('rightAnnotationDirection="0"',
                                    'rightAnnotationDirection="4"')
            return line

        # Match each <ComposerMapGrid ...> element (single line in .qpt files)
        return re.sub(r'<ComposerMapGrid[^>]+>', _fix_main_grid_line, xml_str)

    def _load_template_doc(self, mode, lang):
        """Load and cache template XML as QDomDocument."""
        key = (mode, lang)
        if key not in self._tpl_cache:
            tpl_path = self._resolve_template(mode, lang)
            with open(tpl_path, "r", encoding="utf-8") as f:
                raw = f.read()
            # Apply in-memory patches (golden files are never modified)
            self._tpl_cache[key] = self._patch_template_xml(raw)

        doc = QDomDocument()
        ok, err_msg, err_line, _col = doc.setContent(self._tpl_cache[key])
        if not ok:
            raise RuntimeError(
                f"Template XML error at line {err_line}: {err_msg}"
            )
        return doc

    # ── Layout Creation ──────────────────────────────────────────

    def create_layout(self, params, progress_cb=None):
        """Create a QgsPrintLayout from template + user params.

        Args:
            params: Dict of layout parameters from dialog.
            progress_cb: Optional callback(step, message) for progress updates.

        Pipeline (optimized):
          1. Load template (cached XML)
          2. Set page size + extent + scale (batch, no refresh yet)
          3. Minimal refresh for DD convergence (1 for slide, 2 for print)
          4. Re-enforce scale (no extra refresh)
          5. Single-pass item scan: labels + visibility + signature
        """
        def _progress(step, msg):
            if progress_cb:
                progress_cb(step, msg)

        project = QgsProject.instance()
        mode = params["mode"]   # "slide" or "print"
        lang = params["lang"]   # "EN" or "VN"
        tpl_key = (mode, lang)
        ids = ITEM_IDS[tpl_key]

        # Load cached template XML
        _progress(1, "Loading template...")
        doc = self._load_template_doc(mode, lang)

        # Create layout
        self._layout_counter += 1
        layout_name = f"LVT_{mode}_{lang}_{self._layout_counter}"
        layout = QgsPrintLayout(project)

        _progress(2, "Applying template design...")
        _items, ok2 = layout.loadFromTemplate(doc, QgsReadWriteContext())
        if not ok2:
            raise RuntimeError("Failed to load template into layout.")

        layout.setName(layout_name)
        project.layoutManager().addLayout(layout)
        layout = project.layoutManager().layoutByName(layout_name)

        # ── Step 1: Batch setup (page + extent + scale) ───────────
        _progress(3, "Setting page size & map extent...")
        pw = params["page_width"]
        ph = params["page_height"]
        page = layout.pageCollection().page(0)
        page.setPageSize(
            QgsLayoutSize(pw, ph, QgsUnitTypes.LayoutUnit.LayoutMillimeters)
        )

        map_item = self._find_item(layout, ids["map"], QgsLayoutItemMap)
        if map_item:
            extent = self._resolve_extent(params)
            map_item.zoomToExtent(extent)
            map_item.setScale(params["scale"])

            # Override template DD scale lock (was hardcoded 10000)
            # Lock to user's chosen scale instead
            user_scale = params["scale"]
            dd = QgsProperty.fromExpression(str(user_scale))
            map_item.dataDefinedProperties().setProperty(
                QgsLayoutObject.DataDefinedProperty.MapScale, dd
            )



        # ── Step 2: DD convergence refresh ───────────────────────
        # slide: 1 pass — DD expressions converge in a single cycle
        # print: 3 passes — needed so margin/thickness DD expressions
        #   (10.3mm, 12.8mm, 14.8+0.35mm) all converge correctly.
        cycles = 3 if mode == "print" else 1
        for i in range(cycles):
            _progress(4 + i, f"Rendering layout (pass {i + 1}/{cycles})...")
            layout.refresh()
            QCoreApplication.processEvents()

        # ── Step 3: Re-enforce scale + settle ─────────────────────
        _progress(4 + cycles, "Finalizing scale...")
        if map_item:
            map_item.setScale(params["scale"])
            layout.refresh()
            QCoreApplication.processEvents()

        # ── Step 4: Update labels ─────────────────────────────────
        _progress(5 + cycles, "Writing labels...")
        self._update_labels(layout, ids, params)

        # ── Step 5: Single-pass item scan ─────────────────────────
        # Scan layout.items() ONCE and categorize all items together.
        # Previously scanned 6 separate times (one per feature).
        _progress(6 + cycles, "Configuring elements...")
        all_items = list(layout.items())

        legend_native_enabled = params.get("legend", {}).get("enabled", False)
        show_scalebar = params["show_scalebar"]
        show_legend = params["show_legend"]
        show_north = params["show_north"]
        svg_path = params.get("north_arrow_svg", "")
        na_left = params.get("north_arrow_left")

        # Categorize all items in one pass
        legend_items = []
        picture_items = []
        frame_north = None
        sig_items = []

        for item in all_items:
            if isinstance(item, QgsLayoutItemLegend):
                legend_items.append(item)
            elif isinstance(item, QgsLayoutItemPicture):
                picture_items.append(item)
            else:
                # North arrow HTML frame detection
                if na_left is not None and frame_north is None:
                    with contextlib.suppress(ImportError, AttributeError):
                        from qgis.core import QgsLayoutFrame
                        if isinstance(item, QgsLayoutFrame):
                            mf = item.multiFrame()
                            if mf and hasattr(mf, 'html'):
                                html_content = mf.html()
                                if html_content and 'north' in html_content.lower():
                                    frame_north = item
                # Signature item detection
                item_id = getattr(item, 'id', lambda: '')()
                if item_id and item_id.startswith("Sig "):
                    sig_items.append(item)

        # Scalebar visibility
        if not show_scalebar:
            self._hide_item(layout, ids["scalebar_m"])
            if "scalebar_deg" in ids:
                self._hide_item(layout, ids["scalebar_deg"])

        # Legend handling
        if legend_native_enabled:
            # Remove template legends — native legend will replace them
            for item in legend_items:
                layout.removeLayoutItem(item)
        elif not show_legend:
            for item in legend_items:
                item.setVisibility(False)

        # North arrow visibility / style
        if not show_north:
            for item in picture_items:
                item.setVisibility(False)
        else:
            if svg_path and os.path.isfile(svg_path):
                for item in picture_items:
                    item.setPicturePath(svg_path)

        # Reposition north arrow HTML frame
        if na_left is not None and frame_north is not None:
            na_top = params.get("north_arrow_top")
            na_w = params.get("north_arrow_w")
            na_h = params.get("north_arrow_h")
            frame_north.attemptMove(
                QgsLayoutPoint(
                    na_left, na_top,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            frame_north.attemptResize(
                QgsLayoutSize(
                    na_w, na_h,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters))

        # ── Step 6: Signature block ────────────────────────
        # Clean up stale signature items (already found in single-pass scan)
        for item in sig_items:
            layout.removeLayoutItem(item)

        if params.get("signature_block"):
            _progress(7 + cycles, "Adding signature block...")
            self._add_signature_block(layout, params)

        # ── Step 7: Legend block ───────────────────────────────────
        if params.get("legend", {}).get("enabled"):
            _progress(8 + cycles, "Adding legend block...")
            try:
                from .legend import add_legend_block
                plugin_dir = os.path.dirname(os.path.dirname(__file__))
                add_legend_block(layout, params, plugin_dir)
            except Exception as e:
                from qgis.core import QgsMessageLog, Qgis
                QgsMessageLog.logMessage(
                    f"Legend block error: {e}",
                    "LVT4U", Qgis.MessageLevel.Warning)

        # ── Final: re-enforce scale + one last DD pass ───────────
        if map_item:
            map_item.setScale(params["scale"])



        _progress(9 + cycles, "Done!")
        return layout



    # ── Label Updates ─────────────────────────────────────────────

    def _update_labels(self, layout, ids, params):
        """Write user-provided text into template label items."""
        # Title
        if "title" in ids and params.get("title"):
            self._set_label(layout, ids["title"], params["title"])

        # Organization name
        if "org_name" in ids and params.get("org_name"):
            self._set_label(layout, ids["org_name"], params["org_name"])

        # Study area
        if "study_area" in ids and params.get("study_area"):
            self._set_label(layout, ids["study_area"], params["study_area"])

        # Data sources
        if "map_data" in ids and params.get("data_sources"):
            self._set_label(layout, ids["map_data"], params["data_sources"])

        # References / Viện dẫn: ALWAYS restore fixed title text
        if "references" in ids:
            fixed_ref = ("Dữ liệu xây dựng bản đồ:"
                         if params.get("lang") == "VN"
                         else "Map data references:")
            self._set_label(layout, ids["references"], fixed_ref)

    # ── Extent Resolution ────────────────────────────────────────

    def _resolve_extent(self, params):
        """Get the map extent based on user selection."""
        mode = params["extent_mode"]
        if mode == "drawn" and params.get("drawn_extent"):
            return params["drawn_extent"]
        elif mode == "layer" and params.get("extent_layer"):
            return params["extent_layer"].extent()
        else:
            return self.iface.mapCanvas().extent()

    # ── Helpers ───────────────────────────────────────────────────

    @staticmethod
    def _find_item(layout, item_id, item_type=None):
        item = layout.itemById(item_id)
        if item and item_type and not isinstance(item, item_type):
            return None
        return item

    @staticmethod
    def _set_label(layout, item_id, text):
        item = layout.itemById(item_id)
        if item and isinstance(item, QgsLayoutItemLabel):
            item.setText(text)

    @staticmethod
    def _hide_item(layout, item_id):
        item = layout.itemById(item_id)
        if item:
            item.setVisibility(False)

    def _add_signature_block(self, layout, params):
        """Add N signature blocks at bottom-left of layout.

        Each block uses 3 items:
          - QgsLayoutItemShape (rectangle) for border
          - QgsLayoutItemLabel for header (title + date + separator)
          - QgsLayoutItemLabel for name (native VAlign bottom)
        """
        from qgis.PyQt.QtGui import QFont
        from qgis.core import QgsLayoutItemShape
        from qgis.core import QgsFillSymbol

        # Remove any existing signature items (prevent duplicates)
        to_remove = []
        for item in layout.items():
            item_id = getattr(item, 'id', lambda: '')()
            if item_id and item_id.startswith("Sig "):
                to_remove.append(item)
        for item in to_remove:
            layout.removeLayoutItem(item)

        sig_h = params.get("sig_height", 50)
        sig_w = params.get("sig_width", 50)
        blocks = params.get("sig_blocks", [])

        # Font tuples: (family, size, bold, italic)
        ft = params.get("sig_font_title",
                        ("Times New Roman", 12, True, False))
        fd = params.get("sig_font_date",
                        ("Times New Roman", 10, False, True))
        fn = params.get("sig_font_name",
                        ("Times New Roman", 11, True, False))

        # Position: read map frame (= blue frame) geometry directly
        # This is always accurate regardless of paper/orientation
        map_item = None
        for item in layout.items():
            if isinstance(item, QgsLayoutItemMap):
                map_item = item
                break

        if not map_item:
            return  # No map frame, can't position

        map_x = map_item.positionWithUnits().x()
        map_y = map_item.positionWithUnits().y()
        map_h = map_item.sizeWithUnits().height()

        # Block offset from map frame (user-configurable, default 3mm)
        ml = params.get("sig_margin_left", 3)
        mb = params.get("sig_margin_bottom", 3)
        base_x = map_x + ml
        frame_bottom = map_y + map_h
        base_y = frame_bottom - mb - sig_h

        # Header label height (title + date + separator)
        hdr_h = 16  # mm
        # Name label height
        name_h = 8  # mm

        def _css(fam, sz, bold, italic):
            """Build CSS style string from font tuple."""
            w = "bold" if bold else "normal"
            s = "italic" if italic else "normal"
            return (f"font-family:'{fam}';font-size:{sz}pt;"
                    f"font-weight:{w};font-style:{s};")

        def _make_font(fam, sz, bold, italic):
            """Create QFont from tuple."""
            f = QFont(fam, sz)
            f.setBold(bold)
            f.setItalic(italic)
            return f

        for i, blk in enumerate(blocks):
            x = base_x + i * sig_w
            title = blk.get("title", "")
            sig_date = blk.get("date", "")
            name = blk.get("name", "")

            # ── 1. Rectangle (border frame) ──
            rect = QgsLayoutItemShape(layout)
            rect.setShapeType(QgsLayoutItemShape.Shape.Rectangle)
            rect.attemptMove(
                QgsLayoutPoint(x, base_y,
                               QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            rect.attemptResize(
                QgsLayoutSize(sig_w, sig_h,
                              QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            # White fill + 0.2mm black border
            sym = QgsFillSymbol.createSimple({
                'color': '255,255,255,255',
                'outline_color': '0,0,0,255',
                'outline_width': '0.2',
                'outline_width_unit': 'MM',
            })
            rect.setSymbol(sym)
            rect.setBackgroundEnabled(False)
            rect.setFrameEnabled(False)
            rect.setLocked(True)
            rect.setId(f"Sig Frame {i + 1}")
            layout.addLayoutItem(rect)
            layout.raiseItem(rect)

            # ── 2. Header label (title + date + hr) at top ──
            hdr_html = (
                '<div style="text-align:center;">'
                f'<div style="text-align:center;{_css(*ft)}">'
                f'{title}</div>'
                f'<div style="text-align:center;{_css(*fd)}">'
                f'{sig_date}</div>'
                '<hr style="border:none;border-top:0.2mm solid #000;'
                'width:90%;margin:2mm auto 0 auto;"/>'
                '</div>'
            )
            hdr = QgsLayoutItemLabel(layout)
            hdr.setMode(QgsLayoutItemLabel.Mode.ModeHtml)
            hdr.setText(hdr_html)
            hdr.attemptMove(
                QgsLayoutPoint(x, base_y + 3,
                               QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            hdr.attemptResize(
                QgsLayoutSize(sig_w, hdr_h,
                              QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            hdr.setBackgroundEnabled(False)
            hdr.setFrameEnabled(False)
            hdr.setLocked(True)
            hdr.setId(f"Sig Header {i + 1}")
            layout.addLayoutItem(hdr)
            layout.raiseItem(hdr)

            # ── 3. Name label at bottom (native VAlign) ──
            name_lbl = QgsLayoutItemLabel(layout)
            name_lbl.setMode(QgsLayoutItemLabel.Mode.ModeFont)
            name_lbl.setText(name)
            name_lbl.setFont(_make_font(*fn))
            name_lbl.setHAlign(QtCompat.AlignHCenter)
            name_lbl.setVAlign(QtCompat.AlignBottom)
            name_y = base_y + sig_h - name_h - 3  # 3mm from bottom
            name_lbl.attemptMove(
                QgsLayoutPoint(x, name_y,
                               QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            name_lbl.attemptResize(
                QgsLayoutSize(sig_w, name_h,
                              QgsUnitTypes.LayoutUnit.LayoutMillimeters))
            name_lbl.setBackgroundEnabled(False)
            name_lbl.setFrameEnabled(False)
            name_lbl.setLocked(True)
            name_lbl.setId(f"Sig Name {i + 1}")
            layout.addLayoutItem(name_lbl)
            layout.raiseItem(name_lbl)
