# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Clickpointtoedit — QGIS Plugin
 UI v2: Header + Tabs (แก้ไข / ดูข้อมูล / ประวัติ) + Stats + Progress + Street View
 Create Plugin By: https://github.com/Genroy
 Copyright: (C) 2025 Thamoon Kedkaew (CeJ)
***************************************************************************/
"""

from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, pyqtSignal, QObject, QEvent
from qgis.PyQt.QtGui import QIcon, QFont, QColor
from qgis.PyQt.QtWidgets import (
    QAction, QDockWidget, QPushButton, QVBoxLayout, QHBoxLayout,
    QFormLayout, QLineEdit, QMessageBox, QWidget, QScrollArea,
    QFileDialog, QLabel, QComboBox, QDialog, QListWidget,
    QListWidgetItem, QSizePolicy, QFrame, QTabWidget, QProgressBar,
    QShortcut, QTreeWidget, QTreeWidgetItem
)
from qgis.PyQt.QtGui import QKeySequence
from qgis.gui import QgsMapToolIdentifyFeature, QgsHighlight
from qgis.core import (
    QgsProject, QgsVectorLayer, QgsFeature, QgsFeatureRequest,
    QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsPointXY
)
import os.path, csv, webbrowser
from datetime import datetime
import codecs


# ── Custom Identify Tool ──────────────────────────────────────────
class CustomIdentifyTool(QgsMapToolIdentifyFeature):
    feature_clicked = pyqtSignal(object)

    def __init__(self, canvas, layer):
        super().__init__(canvas)
        self.canvas = canvas
        self._layer = layer
        self.setLayer(layer)

    def canvasReleaseEvent(self, event):
        from qgis.core import (QgsCoordinateTransform, QgsRectangle,
                                QgsFeatureRequest, QgsProject, QgsWkbTypes,
                                QgsGeometry, QgsPointXY)
        # เช็คว่า layer ยังอยู่ไหม
        try:
            _ = self._layer.id()
        except RuntimeError:
            return
        # แปลง pixel → map point → layer CRS
        map_pt     = self.toMapCoordinates(event.pos())
        canvas_crs = self.canvas.mapSettings().destinationCrs()
        layer_crs  = self._layer.crs()
        tr = QgsCoordinateTransform(canvas_crs, layer_crs, QgsProject.instance())
        layer_pt = tr.transform(map_pt)

        # tolerance ขึ้นกับ geometry type
        geom_type = self._layer.geometryType()  # 0=Point, 1=Line, 2=Polygon
        if geom_type == QgsWkbTypes.PointGeometry:
            tol = self.canvas.mapUnitsPerPixel() * 12  # point ต้องคลิกใกล้กว่า
        else:
            tol = self.canvas.mapUnitsPerPixel() * 6

        rect = QgsRectangle(
            layer_pt.x() - tol, layer_pt.y() - tol,
            layer_pt.x() + tol, layer_pt.y() + tol
        )

        req = QgsFeatureRequest().setFilterRect(rect)
        click_geom = QgsGeometry.fromPointXY(QgsPointXY(layer_pt))

        # หา feature ที่ใกล้ click point ที่สุด
        best_feat = None
        best_dist = float("inf")
        for feat in self._layer.getFeatures(req):
            if feat.hasGeometry():
                dist = feat.geometry().distance(click_geom)
                if dist < best_dist:
                    best_dist = dist
                    best_feat = feat

        if best_feat is not None:
            self.feature_clicked.emit(best_feat)


# ── ComboBox ป้องกัน scroll wheel ────────────────────────────────
class NoScrollComboBox(QComboBox):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._popup_open = False
        self.setFocusPolicy(Qt.StrongFocus)

    def showPopup(self):
        self._popup_open = True
        super().showPopup()

    def hidePopup(self):
        super().hidePopup()
        self._popup_open = False

    def wheelEvent(self, event):
        if self._popup_open:
            super().wheelEvent(event)
        else:
            event.ignore()


# ── CSS Stylesheet ────────────────────────────────────────────────
STYLE = """
QWidget#dock_root {
    background: #f8f9fa;
}
QWidget#header_bar {
    background: #1a73e8;
    border-radius: 0px;
}
QLabel#header_title {
    color: white;
    font-size: 13px;
    font-weight: bold;
}
QLabel#header_sub {
    color: rgba(255,255,255,0.8);
    font-size: 11px;
}
QTabWidget::pane {
    border: none;
    background: white;
}
QTabBar::tab {
    padding: 6px 14px;
    font-size: 12px;
    color: #666;
    border-bottom: 2px solid transparent;
    background: transparent;
}
QTabBar::tab:selected {
    color: #1a73e8;
    border-bottom: 2px solid #1a73e8;
    font-weight: bold;
}
QTabBar::tab:hover {
    color: #1a73e8;
    background: #f0f6ff;
}
QWidget#stat_bar {
    background: white;
    border-bottom: 1px solid #e8eaed;
}
QLabel#stat_num {
    font-size: 16px;
    font-weight: bold;
    color: #1a73e8;
}
QLabel#stat_lbl {
    font-size: 10px;
    color: #888;
}
QProgressBar {
    border: none;
    border-radius: 3px;
    background: #e8eaed;
    height: 6px;
    text-align: center;
}
QProgressBar::chunk {
    background: #1a73e8;
    border-radius: 3px;
}
QPushButton#btn_save {
    background: #1a73e8;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 7px 20px;
    font-size: 13px;
    font-weight: bold;
}
QPushButton#btn_save:hover { background: #1557b0; }
QPushButton#btn_save:disabled {
    background: #c5c5c5;
    color: #888;
    border: none;
}
QPushButton#btn_cancel {
    background: white;
    color: #444;
    border: 1px solid #dadce0;
    border-radius: 6px;
    padding: 7px 16px;
    font-size: 13px;
}
QPushButton#btn_cancel:hover { background: #f8f9fa; }
QPushButton#btn_export {
    background: #e8f0fe;
    color: #1a73e8;
    border: 1px solid #c5d9fa;
    border-radius: 6px;
    padding: 7px 16px;
    font-size: 13px;
}
QPushButton#btn_export:hover { background: #d2e3fc; }
QPushButton#btn_sv {
    background: #e6f4ea;
    color: #137333;
    border: 1px solid #b7dfbf;
    border-radius: 6px;
    padding: 5px 12px;
    font-size: 12px;
}
QPushButton#btn_sv:hover { background: #ceead6; }
QPushButton#btn_edit {
    background: white;
    color: #1a73e8;
    border: 1px solid #c5d9fa;
    border-radius: 4px;
    padding: 2px 8px;
    font-size: 11px;
}
QPushButton#btn_edit:hover { background: #e8f0fe; }
QLineEdit {
    border: 1px solid #dadce0;
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 12px;
    background: white;
}
QLineEdit:focus { border: 1.5px solid #1a73e8; }
QLineEdit:disabled { background: #f8f9fa; color: #aaa; }
QComboBox {
    border: 1px solid #dadce0;
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 12px;
    background: white;
}
QLabel#field_lbl {
    font-size: 12px;
    color: #444;
}
QLabel#type_badge {
    font-size: 10px;
    color: #888;
    background: #f1f3f4;
    border-radius: 3px;
    padding: 1px 5px;
}
QLabel#history_empty {
    color: #aaa;
    font-size: 12px;
    padding: 20px;
}
"""


class SVCloseWatcher(QObject):
    """QObject สำหรับดัก closeEvent ของ Street View window"""
    def __init__(self, callback, parent=None):
        super().__init__(parent)
        self._callback = callback

    def eventFilter(self, obj, event):
        from qgis.PyQt.QtCore import QEvent
        if event.type() == QEvent.Close:
            try:
                self._callback()
            except Exception:
                pass
        return False


class Clickpointtoedit:
    def __init__(self, iface):
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self.plugin_dir = os.path.dirname(__file__)
        self.actions = []
        self.menu = self.tr(u'&Clickpointtoedit')
        self.tool = None
        self.dock = None
        self.fields = []
        self.highlight = None
        self.saved_highlights = []
        self.action = None
        self.help_dock = None

        # stats
        self._edit_count_today = 0
        self._pending_save = 0
        # ประวัติการแก้ไข: {feature_id: [{"time":..,"layer":..,"fields":{name:(old,new)}}]}
        self._edit_history = {}
        # layer ที่กำลังแก้ไขอยู่ (ล็อคตอนคลิก feature)
        self._current_layer = None
        self._save_btn = None
        self._has_changes = False
        self._restore_blink_timer = None
        self._restore_blink_hl = None
        self._sv_watcher = None
        self._sv_widget_ref = None
        self._hist_blink_timer = None
        self._hist_blink_hl = None
        # log restore: [{"time":..,"layer":..,"feature_id":..,"fields":{name:orig_val}}]
        self._restore_log = []

        # UI constants
        self.UI_MIN_WIDTH   = 220
        self.UI_CTRL_HEIGHT = 28
        self.UI_DOCK_MIN_W  = 400
        self.UI_DOCK_MAX_W  = 560
        self.UI_DOCK_PREF_W = 460
        self.UI_DOCK_PREF_H = 580

        # Mappings
        self.bl_owner_map = {"1 = รัฐ": 1, "2 = เอกชน": 2, "3 = กรุงเทพมหานคร": 3}
        self.bl_owner_reverse_map = {v: k for k, v in self.bl_owner_map.items()}

        self.bl_area_flag_map = {"C = การคำนวณ": "C", "F = การวัดในพื้นที่": "F", "P = ผังบริเวณ": "P"}
        self.bl_area_flag_reverse_map = {v: k for k, v in self.bl_area_flag_map.items()}

        self.bl_type_map = {
            "1 = บ้านเดี่ยว / อาคารเดี่ยว": 1, "2 = บ้านแฝด": 2, "3 = ทาวเฮ้าส์": 3,
            "4 = ห้องแถว (ไม้ สังกะสี กระเบื้อง)": 4, "5 = ตึกแถว / อาคารครึ่งตึกครึ่งไม้": 5,
            "6 = อาคารพักอาศัยถาวรและชั่วคราว (โรงแรม หอพัก อาคารชุด)": 6,
            "7 = เรือนแพ": 7, "8 = เพิงกึ่งถาวร": 8, "98 = อื่น ๆ": 98, "99 = ไม่ทราบ": 99
        }
        self.bl_type_reverse_map = {v: k for k, v in self.bl_type_map.items()}

        self.bl_use_map = {
            "1000 = ที่อยู่อาศัย": 1000, "1100 = ที่พักอาศัย": 1100,
            "1102 = บ้านพักพนักงานบริษัทพานิชยกรรม": 1102,
            "1103 = บ้านพักพนักงานในอุตสาหกรรม": 1103,
            "1105 = บ้านพักพนักงานรัฐวิสาหกิจ": 1105,
            "1106 = หอพักนักเรียน / นักศึกษา": 1106,
            "1200 = วัง ตำหนัก และที่พระราชฐาน": 1200,
            "1300 = บ้านพักข้าราชการ": 1300,
            "1600 = อนุรักษ์เพื่อการอยู่อาศัย": 1600,
            "1800 = ที่อยู่อาศัยอื่น ๆ": 1800,
            "2000 = พาณิชยกรรม": 2000, "2100 = สำนักงานและบริษัท": 2100,
            "2200 = ธุรกิจบริการ": 2200, "2210 = ตลาด": 2210,
            "2220 = โรงแรม บังกะโล รีสอร์ท": 2220, "2230 = ห้างสรรพสินค้า": 2230,
            "2231 = ศูนย์รวมวัสดุก่อสร้าง": 2231, "2232 = COMMUNITY MALL": 2232,
            "2233 = มินิมาร์ท (MINIMART)": 2233,
            "2240 = สถานีบริการเชื้อเพลิง": 2240,
            "2260 = สถาบันสอนคอมพิวเตอร์/กวดวิชา": 2260,
            "2280 = ธุรกิจบริการอื่น ๆ": 2280,
            "2300 = ธนาคารและสถาบันการเงิน": 2300,
            "2400 = ธุรกิจนันทนาการ": 2400,
            "2800 = พาณิชยกรรมอื่น ๆ": 2800,
            "3000 = อุตสาหกรรม": 3000, "3110 = โรงงาน": 3110,
            "3120 = โรงฆ่าสัตว์": 3120, "3300 = คลังสินค้า": 3300,
            "3800 = อุตสาหกรรมอื่น": 3800,
            "4000 = การใช้ประโยชน์แบบผสม": 4000,
            "4100 = ที่พักอาศัยกึ่งพาณิชยกรรม": 4100,
            "5000 = สาธารณูปโภค": 5000, "5130 = ท่าอากาศยาน": 5130,
            "5140 = ท่าเรือ": 5140, "5150 = สถานีขนส่ง": 5150,
            "5160 = สถานีรถไฟ": 5160,
            "6000 = สาธารณูปการ": 6000, "6100 = สถาบันการศึกษา": 6100,
            "6110 = โรงเรียนอนุบาล": 6110, "6121 = โรงเรียนประถมศึกษา": 6121,
            "6130 = โรงเรียนมัธยมศึกษา": 6130, "6160 = ระดับอุดมศึกษา": 6160,
            "6200 = สถาบันศาสนา": 6200, "6210 = วัด": 6210,
            "6300 = สถาบันราชการ": 6300, "6320 = สำนักงานเขต": 6320,
            "6340 = สถานีตำรวจ": 6340, "6350 = สถานีดับเพลิง": 6350,
            "6400 = รัฐวิสาหกิจ": 6400,
            "6500 = การสาธารณสุข": 6500, "6530 = โรงพยาบาลรัฐ": 6530,
            "6600 = ศิลปวัฒนธรรม": 6600,
            "7000 = นันทนาการ": 7000, "7310 = สวนสาธารณะ": 7310,
            "7320 = การกีฬา": 7320,
            "8000 = เกษตรกรรม": 8000,
            "9000 = อาคารประเภทอื่น ๆ": 9900,
            "9991 = อาคารร้าง": 9991, "9992 = โรงจอดรถ": 9992,
            "9996 = ไม่ใช่อาคาร": 9996, "9998 = อาคารกำลังก่อสร้าง": 9998,
            "9999 = ไม่สำรวจข้อมูล": 9999
        }
        self.bl_use_reverse_map = {v: k for k, v in self.bl_use_map.items()}

        self.bl_matl_map = {
            "1 = คอนกรีต": 1, "2 = ไม้ / ไม้เทียม": 2,
            "3 = คอนกรีตและไม้": 3, "4 = เซลโลกรีต / ยิบซั่ม": 4,
            "5 = สังกะสี / เมทัลชีท": 5, "8 = อื่น ๆ": 8, "99 = ไม่ทราบ": 99
        }
        self.bl_matl_reverse_map = {v: k for k, v in self.bl_matl_map.items()}

        self.initGui()

    def tr(self, message):
        return QCoreApplication.translate('Clickpointtoedit', message)

    def add_action(self, icon_path, text, callback, enabled_flag=True,
                   add_to_menu=True, add_to_toolbar=True,
                   status_tip=None, whats_this=None, parent=None):
        icon   = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        if status_tip:  action.setStatusTip(status_tip)
        if whats_this:  action.setWhatsThis(whats_this)
        if add_to_toolbar: self.iface.addToolBarIcon(action)
        if add_to_menu:    self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action

    def initGui(self):
        icon_path = os.path.join(self.plugin_dir, 'icon.png')
        if self.action:
            self.iface.removeToolBarIcon(self.action)
            self.iface.removePluginMenu(self.menu, self.action)
        self.action = self.add_action(
            icon_path,
            text=self.tr(u'Turn on Edit เปิดโหมดแก้ไขข้อมูล'),
            callback=self.run,
            parent=self.iface.mainWindow()
        )

    def unload(self):
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.iface.removeToolBarIcon(action)
        self.actions = []
        if self.dock:
            self.iface.removeDockWidget(self.dock)
            self.dock = None
        if self.help_dock:
            self.iface.removeDockWidget(self.help_dock)
            self.help_dock = None

    # ── Sync Street View กับ feature ที่คลิก ─────────────────────
    def _sync_street_view_to_feature(self, feature, layer):
        """ถ้า Street View เปิดอยู่ ให้โหลดตำแหน่งของ feature ใหม่"""
        try:
            from qgis.utils import plugins
            sv = plugins.get("streetview_qgis_cej")
            if sv and sv.widget and sv.widget.isVisible():
                lat, lon = self._get_feature_latlon(feature, layer)
                if lat is not None:
                    sv.widget.load_street_view_html(lat, lon, 0)
                    # อัปเดตสถานะปุ่ม
                    if hasattr(self, "_sv_btn") and self._sv_btn:
                        self._sv_btn.setChecked(True)
                        self._sv_btn.setText("✖ ปิด Street View")
        except Exception:
            pass

    # ── Re-activate map tool ─────────────────────────────────────
    def _reactivate_tool(self):
        """Re-activate tool หลังจาก widget อื่นแย่ง focus ไป"""
        try:
            if self.tool and self._current_layer:
                _ = self._current_layer.id()  # เช็คว่า layer ยังอยู่
                self.canvas.setMapTool(self.tool)
        except Exception:
            pass

    # ── Clear highlights ──────────────────────────────────────────
    def _clear_all_highlights(self):
        try:
            items = ([self.highlight] if self.highlight else []) + self.saved_highlights
            self.highlight = None
            self.saved_highlights = []
            for h in items:
                try:
                    h.hide()
                    h.setVisible(False)
                    h.deleteLater()
                except Exception:
                    pass
            self.canvas.refresh()
        except Exception:
            pass

    # ── Run ───────────────────────────────────────────────────────
    def run(self):
        layer = self.iface.activeLayer()
        if not isinstance(layer, QgsVectorLayer):
            self.iface.messageBar().pushWarning(
                "Clickpointtoedit", "กรุณาเลือก Vector Layer ก่อน")
            return
        # ล็อค layer ตอนกดปุ่ม plugin — ไม่สนใจ layer อื่นที่ซ้อนอยู่
        self._current_layer = layer
        # สร้าง tool ใหม่ทุกครั้งเพื่อ reference layer ล่าสุด
        if self.tool:
            try:
                self.canvas.unsetMapTool(self.tool)
            except Exception:
                pass
        self.tool = CustomIdentifyTool(self.canvas, layer)
        self.tool.feature_clicked.connect(self.onFeatureIdentified)
        self.canvas.setMapTool(self.tool)
        self.iface.messageBar().pushInfo(
            "Clickpointtoedit",
            f"คลิก Feature บน layer '{layer.name()}' เพื่อแก้ไขข้อมูล")

    # ── Build Dock ────────────────────────────────────────────────
    def onFeatureIdentified(self, feature):
        try:
            self._do_identify(feature)
        except Exception as e:
            import traceback
            from qgis.core import QgsMessageLog, Qgis
            tb = traceback.format_exc()
            QgsMessageLog.logMessage(
                f"Clickpointtoedit error: {e}\n{tb}",
                "Clickpointtoedit", Qgis.Critical)
            # แสดง traceback ใน message bar ด้วย
            self.iface.messageBar().pushCritical(
                "Clickpointtoedit", f"Error: {e} (ดูรายละเอียดใน Log Messages > Clickpointtoedit)")

    def _do_identify(self, feature):
        # หยุดกระพริบ restore ถ้ายังค้างอยู่
        self._stop_restore_blink()
        # ใช้ layer ที่ล็อคไว้ตอนกดปุ่ม run เท่านั้น
        layer = self._current_layer
        if not isinstance(layer, QgsVectorLayer):
            self.iface.messageBar().pushWarning(
                "Clickpointtoedit", "ไม่พบ layer ที่เลือก กรุณากดปุ่ม plugin ใหม่")
            return

        # Red highlight
        if self.highlight:
            self.highlight.hide()
        self.highlight = QgsHighlight(self.canvas, feature.geometry(), layer)
        self.highlight.setColor(Qt.red)
        self.highlight.setWidth(3)
        self.highlight.show()

        if not layer.isEditable():
            layer.startEditing()

        if self.dock:
            self.iface.removeDockWidget(self.dock)

        try:
            field_names = [f.name() for f in layer.fields()]
            # ลอง ID fields หลายชื่อ
            id_candidates = ["BLDG_ID", "BL_HID", "id", "ID", "fid", "FID", "osm_id", "gid"]
            bldg_id = f"FID:{feature.id()}"
            for cand in id_candidates:
                if cand in field_names:
                    val = feature[cand]
                    if val not in [None, "", "NULL"]:
                        bldg_id = str(val)
                        break
        except Exception:
            bldg_id = f"FID:{feature.id()}"

        self.dock = QDockWidget(f"Edit — {layer.name()}", self.iface.mainWindow())
        self.dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        self.dock.setFeatures(
            QDockWidget.DockWidgetMovable |
            QDockWidget.DockWidgetClosable |
            QDockWidget.DockWidgetFloatable
        )
        self.dock.setMinimumWidth(self.UI_DOCK_MIN_W)
        self.dock.setMaximumWidth(self.UI_DOCK_MAX_W)

        root = QWidget()
        root.setObjectName("dock_root")
        root.setStyleSheet(STYLE)
        root_lay = QVBoxLayout(root)
        root_lay.setContentsMargins(0, 0, 0, 0)
        root_lay.setSpacing(0)

        # ── Header ──
        header = QWidget()
        header.setObjectName("header_bar")
        header.setFixedHeight(56)
        h_lay = QHBoxLayout(header)
        h_lay.setContentsMargins(14, 6, 14, 6)

        v_text = QVBoxLayout()
        v_text.setSpacing(1)
        title_lbl = QLabel(f"🏢  {bldg_id}")
        title_lbl.setObjectName("header_title")
        sub_lbl   = QLabel(f"{layer.name()}  •  {datetime.now().strftime('%d/%m/%Y %H:%M')}")
        sub_lbl.setObjectName("header_sub")
        v_text.addWidget(title_lbl)
        v_text.addWidget(sub_lbl)
        h_lay.addLayout(v_text)
        h_lay.addStretch(1)

        # ปุ่ม Street View — toggle เปิด/ปิด
        self._sv_btn = QPushButton("🗺 Street View")
        self._sv_btn.setObjectName("btn_sv")
        self._sv_btn.setFixedHeight(30)
        self._sv_btn.setCheckable(True)
        self._sv_btn.setCursor(Qt.PointingHandCursor)
        self._sv_btn.clicked.connect(lambda checked: self._toggle_street_view(feature, layer, checked))
        h_lay.addWidget(self._sv_btn)

        # ปุ่ม Restore ใน header — แสดงเฉพาะถ้ามี history ของ feature นี้
        fid = feature.id()
        if fid in self._edit_history:
            restore_header_btn = QPushButton("↩ Restore")
            restore_header_btn.setFixedHeight(30)
            restore_header_btn.setFixedWidth(80)
            restore_header_btn.setCursor(Qt.PointingHandCursor)
            restore_header_btn.setStyleSheet(
                "QPushButton{background:#fff3e0;color:#e65100;"
                "border:1px solid #ffb74d;border-radius:6px;font-size:12px;}"
                "QPushButton:hover{background:#ffe0b2;}")
            restore_header_btn.clicked.connect(
                lambda: self._restore_entry(fid))
            h_lay.addWidget(restore_header_btn)

        root_lay.addWidget(header)

        # ── Stats bar ──
        stat_bar = QWidget()
        stat_bar.setObjectName("stat_bar")
        stat_bar.setFixedHeight(52)
        s_lay = QHBoxLayout(stat_bar)
        s_lay.setContentsMargins(14, 4, 14, 4)
        s_lay.setSpacing(0)

        for num, lbl_text, color in [
            (str(self._edit_count_today),     "แก้ไขวันนี้", "#1a73e8"),
            (str(len(self.saved_highlights)), "บันทึกแล้ว", "#137333"),
        ]:
            chip = QWidget()
            c_lay = QVBoxLayout(chip)
            c_lay.setContentsMargins(0, 0, 0, 0)
            c_lay.setSpacing(0)
            n = QLabel(num)
            n.setObjectName("stat_num")
            n.setStyleSheet(f"font-size:16px;font-weight:bold;color:{color};")
            n.setAlignment(Qt.AlignCenter)
            t = QLabel(lbl_text)
            t.setObjectName("stat_lbl")
            t.setAlignment(Qt.AlignCenter)
            c_lay.addWidget(n)
            c_lay.addWidget(t)
            s_lay.addWidget(chip, 1)

            sep = QFrame()
            sep.setFrameShape(QFrame.VLine)
            sep.setStyleSheet("color:#e8eaed;")
            s_lay.addWidget(sep)

        root_lay.addWidget(stat_bar)

        # ── Progress bar ──
        prog_wrap = QWidget()
        prog_wrap.setFixedHeight(28)
        prog_lay  = QHBoxLayout(prog_wrap)
        prog_lay.setContentsMargins(14, 4, 14, 4)
        prog_lay.setSpacing(8)
        self._prog_bar = QProgressBar()
        self._prog_bar.setFixedHeight(6)
        self._prog_bar.setTextVisible(False)
        prog_lbl = QLabel("กรอกข้อมูล 0%")
        prog_lbl.setObjectName("stat_lbl")
        prog_lbl.setFixedWidth(90)
        self._prog_lbl = prog_lbl
        prog_lay.addWidget(self._prog_bar, 1)
        prog_lay.addWidget(prog_lbl)
        root_lay.addWidget(prog_wrap)

        # ── Tabs ──
        tabs = QTabWidget()
        tabs.setDocumentMode(True)
        tabs.setStyleSheet("QTabBar::tab { font-size: 11px; padding: 5px 8px; }")

        # Tab 1: แก้ไขข้อมูล
        edit_tab = self._build_edit_tab(feature, layer)
        tabs.addTab(edit_tab, "✏️ แก้ไข")

        # Tab 2: ดูข้อมูล (read-only)
        view_tab = self._build_view_tab(feature, layer)
        tabs.addTab(view_tab, "👁 ดูข้อมูล")

        # Tab 3: ประวัติ (แสดงทุก feature ที่เคยแก้ไขใน session นี้)
        hist_tab = self._build_history_tab(feature.id())
        tabs.addTab(hist_tab, "🕐 ประวัติ")

        root_lay.addWidget(tabs, 1)

        # ── Footer buttons ──
        footer = QWidget()
        footer.setFixedHeight(52)
        f_lay = QHBoxLayout(footer)
        f_lay.setContentsMargins(12, 8, 12, 8)
        f_lay.setSpacing(8)

        cancel_btn = QPushButton("ยกเลิก")
        cancel_btn.setObjectName("btn_cancel")
        cancel_btn.setCursor(Qt.PointingHandCursor)
        cancel_btn.clicked.connect(self.cancel_action)

        export_btn = QPushButton("📥 Export")
        export_btn.setObjectName("btn_export")
        export_btn.setCursor(Qt.PointingHandCursor)
        export_btn.clicked.connect(self.export_layer_and_log)

        save_btn = QPushButton("💾  บันทึก")
        save_btn.setObjectName("btn_save")
        save_btn.setCursor(Qt.PointingHandCursor)
        save_btn.setEnabled(False)
        save_btn.setToolTip("แก้ไขข้อมูลก่อนจึงจะบันทึกได้")
        save_btn.clicked.connect(lambda: self.confirm_save(layer, feature.id(), feature))
        self._save_btn = save_btn

        f_lay.addWidget(cancel_btn)
        f_lay.addStretch(1)
        f_lay.addWidget(export_btn)
        f_lay.addWidget(save_btn)
        root_lay.addWidget(footer)

        # Keyboard shortcuts — ใช้ Ctrl+S และ Ctrl+Z เพื่อไม่ขัดแย้งกับ plugin อื่น
        QShortcut(QKeySequence("Ctrl+S"), root,
                  lambda: self.confirm_save(layer, feature.id(), feature))
        QShortcut(QKeySequence("Ctrl+Z"), root, self.cancel_action)

        self.dock.setWidget(root)
        self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock)
        self.dock.resize(self.UI_DOCK_PREF_W, self.UI_DOCK_PREF_H)
        self.dock.show()

        # update progress หลัง build
        self._update_progress()

        # ถ้า Street View เปิดอยู่ → โหลดตำแหน่งใหม่ตาม feature ที่คลิก
        self._sync_street_view_to_feature(feature, layer)

    # ── Safe feature value getter ────────────────────────────────
    @staticmethod
    def _get_field_value(feature, field_name):
        """ดึงค่า field อย่างปลอดภัย รองรับทุกชนิด layer"""
        try:
            idx = feature.fields().indexFromName(field_name)
            if idx >= 0:
                return feature.attributes()[idx]
            return feature[field_name]
        except Exception:
            return None

    # ── Tab 1: แก้ไขข้อมูล ───────────────────────────────────────
    def _build_edit_tab(self, feature, layer):
        container = QWidget()
        outer = QVBoxLayout(container)
        outer.setContentsMargins(0, 0, 0, 0)
        outer.setSpacing(0)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.NoFrame)

        form_widget = QWidget()
        form = QFormLayout(form_widget)
        form.setContentsMargins(14, 12, 14, 12)
        form.setLabelAlignment(Qt.AlignRight)
        form.setHorizontalSpacing(10)
        form.setVerticalSpacing(8)

        self.fields = []
        total_fields = 0
        filled_fields = 0

        manual_max_length = {
            "BLDG_ID": 15, "BL_TYPE": 2, "BL_NSTOREY": 2, "BL_NUNIT": 4,
            "BL_UNIT_F": 10, "BL_AREA_FLAG": 1, "BL_NAME_T": 100,
            "BL_NAME_E": 100, "BL_HOUSENUM": 10, "BL_VILLNUM": 10,
            "BL_VILLAGE": 35, "BL_ROAD": 150, "BL_SUBDISTRICT": 30,
            "BL_DISTRICT": 30, "BL_CHANGWAT": 30, "BL_POSTCODE": 5,
            "BL_ADDRESS": 180, "BL_ACT_MAJOR": 45, "BL_ACT_MINOR": 45,
            "BL_ACT_OTHER": 45, "REMARK": 255, "MATCHING": 1, "STATUS": 2,
            "SOURCE": 2, "S_DATE": 6, "CREATED_USER": 255,
            "LAST_EDITED_USER": 255, "BL_HID": 11, "BL_PAGE_NO": 50
        }

        for field in layer.fields():
            ft = field.typeName().lower()
            if ft in ["geometry", "blob", "clob"]:
                continue

            total_fields += 1
            try:
                value = self._get_field_value(feature, field.name())
            except Exception:
                value = ""
            if value is None:
                value = ""
            if value != "":
                filled_fields += 1

            lbl = QLabel(field.name())
            lbl.setObjectName("field_lbl")

            # ComboBox fields
            if field.name() == "BL_OWNER":
                combo = NoScrollComboBox()
                combo.addItems(list(self.bl_owner_map.keys()))
                combo.setEditable(False)
                if value:
                    try:
                        sv = self.bl_owner_reverse_map.get(int(value), "")
                        if sv: combo.setCurrentText(sv)
                    except Exception: pass
                combo.currentIndexChanged.connect(self._update_progress)
                combo.currentIndexChanged.connect(lambda _: self._mark_changed())
                combo.currentIndexChanged.connect(lambda _: self._mark_changed())
                form.addRow(lbl, self._make_row(combo, ft))
                self.fields.append((field.name(), combo))
                continue

            if field.name() == "BL_AREA_FLAG":
                combo = NoScrollComboBox()
                combo.addItems(list(self.bl_area_flag_map.keys()))
                combo.setEditable(False)
                if value:
                    sv = self.bl_area_flag_reverse_map.get(str(value), "")
                    if sv: combo.setCurrentText(sv)
                combo.currentIndexChanged.connect(self._update_progress)
                combo.currentIndexChanged.connect(lambda _: self._mark_changed())
                form.addRow(lbl, self._make_row(combo, ft))
                self.fields.append((field.name(), combo))
                continue

            if field.name() == "BL_TYPE":
                combo = NoScrollComboBox()
                combo.addItems(list(self.bl_type_map.keys()))
                combo.setEditable(False)
                if value:
                    try:
                        sv = self.bl_type_reverse_map.get(int(value), "")
                        if sv: combo.setCurrentText(sv)
                    except Exception: pass
                combo.currentIndexChanged.connect(self._update_progress)
                combo.currentIndexChanged.connect(lambda _: self._mark_changed())
                form.addRow(lbl, self._make_row(combo, ft))
                self.fields.append((field.name(), combo))
                continue

            if field.name().strip().upper() == "BL_MATL":
                combo = NoScrollComboBox()
                combo.addItems(list(self.bl_matl_map.keys()))
                combo.setEditable(False)
                combo.setMaxVisibleItems(12)
                if value not in [None, "", "NULL"]:
                    try:
                        sv = self.bl_matl_reverse_map.get(int(value), "")
                    except Exception:
                        sv = value if isinstance(value, str) and value in self.bl_matl_map else ""
                    if sv: combo.setCurrentText(sv)
                combo.currentIndexChanged.connect(self._update_progress)
                combo.currentIndexChanged.connect(lambda _: self._mark_changed())
                form.addRow(lbl, self._make_row(combo, ft))
                self.fields.append((field.name(), combo))
                continue

            if field.name().strip().upper() == "BL_USE":
                bl_use_edit = QLineEdit()
                current_key = ""
                if value not in [None, "", "NULL"]:
                    try:
                        current_key = self.bl_use_reverse_map.get(int(value), "")
                    except Exception:
                        current_key = ""
                if current_key:
                    bl_use_edit.setText(current_key)
                bl_use_edit.textChanged.connect(self._update_progress)
                bl_use_edit.textChanged.connect(lambda _: self._mark_changed())
                form.addRow(lbl, self._make_text_row(bl_use_edit, ft))
                self.fields.append((field.name(), bl_use_edit))

                # Search row
                sr_wrap = QWidget()
                sr = QHBoxLayout(sr_wrap)
                sr.setContentsMargins(0, 0, 0, 0)
                search_edit = QLineEdit()
                search_edit.setPlaceholderText("พิมพ์ code หรือชื่อ แล้วกด Enter")
                search_btn  = QPushButton("ค้นหา / เปลี่ยน")
                search_btn.setObjectName("btn_edit")
                search_edit.setFixedHeight(self.UI_CTRL_HEIGHT)
                search_btn.setFixedHeight(self.UI_CTRL_HEIGHT)
                sr.addWidget(search_edit, 1)
                sr.addWidget(search_btn, 0)

                def show_picker(prefill, _edit=bl_use_edit):
                    cur = _edit.text().strip()
                    dlg = QDialog(self.iface.mainWindow())
                    dlg.setWindowTitle("เลือก BL_USE")
                    dlg.resize(380, 400)
                    v = QVBoxLayout(dlg)
                    fe = QLineEdit(dlg)
                    fe.setPlaceholderText("พิมพ์ code หรือชื่อ เช่น 2280")
                    v.addWidget(fe)
                    lw = QListWidget(dlg)
                    for lab, code in sorted(self.bl_use_map.items(), key=lambda x: x[1]):
                        it = QListWidgetItem(lab, lw)
                        it.setData(Qt.UserRole, lab)
                    v.addWidget(lw)

                    def filt(txt):
                        t = txt.strip()
                        for i in range(lw.count()):
                            it = lw.item(i)
                            it.setHidden(bool(t) and t not in it.text())
                        if cur:
                            items = lw.findItems(cur, Qt.MatchExactly)
                            if items and not items[0].isHidden():
                                lw.setCurrentItem(items[0])
                                lw.scrollToItem(items[0])
                                return
                        for i in range(lw.count()):
                            it = lw.item(i)
                            if not it.isHidden():
                                lw.setCurrentRow(i)
                                lw.scrollToItem(it)
                                break

                    def pick(it):
                        kl = it.data(Qt.UserRole)
                        if QMessageBox.question(
                            self.iface.mainWindow(), "ยืนยัน",
                            f"ตั้งค่า BL_USE เป็น:\n{kl}",
                            QMessageBox.Yes | QMessageBox.No
                        ) == QMessageBox.Yes:
                            _edit.setText(kl)
                            dlg.accept()

                    lw.itemDoubleClicked.connect(pick)
                    fe.textChanged.connect(filt)
                    if prefill: fe.setText(prefill)
                    filt(fe.text())
                    dlg.exec_()

                search_btn.clicked.connect(lambda: show_picker(search_edit.text()))
                search_edit.returnPressed.connect(lambda: show_picker(search_edit.text()))
                form.addRow("  ค้นหา BL_USE", sr_wrap)
                continue

            # Generic QLineEdit
            le = QLineEdit(str(value))
            if field.name() in manual_max_length:
                le.setMaxLength(manual_max_length[field.name()])
            if ft in ["int", "integer", "integer32", "integer64"]:
                le.setToolTip("กรอกตัวเลขจำนวนเต็ม")
            elif ft in ["double", "real", "float"]:
                le.setToolTip("กรอกตัวเลขทศนิยม")
            elif ft in ["date", "datetime"]:
                le.setToolTip("รูปแบบ YYYY-MM-DD")
            le.textChanged.connect(self._update_progress)
            le.textChanged.connect(lambda _: self._mark_changed())
            form.addRow(lbl, self._make_text_row(le, ft))
            self.fields.append((field.name(), le))

        scroll.setWidget(form_widget)
        outer.addWidget(scroll, 1)

        # เก็บ total/filled สำหรับ progress
        self._total_fields  = total_fields
        self._filled_fields = filled_fields
        return container

    # ── Tab 2: ดูข้อมูล (read-only) ──────────────────────────────
    def _build_view_tab(self, feature, layer):
        container = QWidget()
        outer = QVBoxLayout(container)
        outer.setContentsMargins(0, 0, 0, 0)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.NoFrame)

        form_widget = QWidget()
        form = QFormLayout(form_widget)
        form.setContentsMargins(14, 12, 14, 12)
        form.setLabelAlignment(Qt.AlignRight)
        form.setHorizontalSpacing(10)
        form.setVerticalSpacing(6)

        for field in layer.fields():
            ft = field.typeName().lower()
            if ft in ["geometry", "blob", "clob"]:
                continue
            try:
                value = self._get_field_value(feature, field.name())
            except Exception:
                value = None
            display = str(value) if value is not None else "—"

            lbl = QLabel(field.name())
            lbl.setObjectName("field_lbl")
            val_lbl = QLabel(display)
            val_lbl.setWordWrap(True)
            val_lbl.setStyleSheet("font-size:12px; color:#333; padding:3px 0;")
            form.addRow(lbl, val_lbl)

        scroll.setWidget(form_widget)
        outer.addWidget(scroll, 1)
        return container

    # ── Tab 3: ประวัติ ────────────────────────────────────────────
    def _build_history_tab(self, current_feature_id):
        self._history_feature_id = current_feature_id
        container = QWidget()
        outer = QVBoxLayout(container)
        outer.setContentsMargins(10, 10, 10, 10)
        outer.setSpacing(8)

        # เรียง feature ล่าสุดก่อน
        all_items = sorted(
            self._edit_history.items(),
            key=lambda x: x[1]["time_last"], reverse=True
        )

        if not all_items:
            lbl = QLabel("ยังไม่มีประวัติการแก้ไขใน session นี้")
            lbl.setObjectName("history_empty")
            lbl.setAlignment(Qt.AlignCenter)
            outer.addWidget(lbl)
        else:
            scroll = QScrollArea()
            scroll.setWidgetResizable(True)
            scroll.setFrameShape(QFrame.NoFrame)
            inner = QWidget()
            v = QVBoxLayout(inner)
            v.setContentsMargins(0, 0, 0, 0)
            v.setSpacing(8)

            for fid, entry in all_items:
                is_current = (fid == current_feature_id)
                card = QFrame()
                bc = "#1a73e8" if is_current else "#e8eaed"
                card.setStyleSheet(
                    f"QFrame{{background:white;border:1.5px solid {bc};"
                    "border-radius:8px;padding:6px;cursor:pointer;}}"
                    "QFrame:hover{background:#f0f6ff;}")
                card.setCursor(Qt.PointingHandCursor)
                card.setToolTip("คลิกเพื่อ zoom ไปที่ feature นี้")
                # ดัก mousePressEvent ให้ zoom ไปที่ feature
                card.mousePressEvent = lambda e, f=fid: self._zoom_to_feature(f)
                c_lay = QVBoxLayout(card)
                c_lay.setSpacing(4)

                # Header
                h_row = QHBoxLayout()
                badge = "📍 feature นี้" if is_current else f"FID:{fid}"
                hdr = QLabel(f"🕐 {entry['time_last']}  {badge} — คลิกเพื่อ zoom")
                hdr.setStyleSheet(
                    f"font-size:11px;font-weight:bold;"
                    f"color:{'#1a73e8' if is_current else '#555'};")
                restore_btn = QPushButton("↩ Restore")
                restore_btn.setObjectName("btn_edit")
                restore_btn.setFixedHeight(22)
                restore_btn.setFixedWidth(70)
                restore_btn.setCursor(Qt.PointingHandCursor)
                restore_btn.clicked.connect(
                    lambda _, f=fid: self._restore_entry(f))
                h_row.addWidget(hdr, 1)
                h_row.addWidget(restore_btn)
                c_lay.addLayout(h_row)

                # แสดง field: original → current
                for fname in entry["current"]:
                    orig = entry["original"].get(fname, "—")
                    curr = entry["current"].get(fname, "—")
                    rl = QLabel(
                        f"<b>{fname}</b>: "
                        f"<span style='color:#c0392b'>{orig}</span>"
                        f" → <span style='color:#27ae60'>{curr}</span>")
                    rl.setStyleSheet("font-size:11px; padding-left:8px;")
                    rl.setTextFormat(Qt.RichText)
                    rl.setWordWrap(True)
                    c_lay.addWidget(rl)

                v.addWidget(card)

            v.addStretch(1)
            inner.setLayout(v)
            scroll.setWidget(inner)
            outer.addWidget(scroll, 1)

        return container

    def _stop_restore_blink(self):
        """หยุดกระพริบและเคลียร์ highlight restore"""
        try:
            if hasattr(self, "_restore_blink_timer") and self._restore_blink_timer:
                self._restore_blink_timer.stop()
                self._restore_blink_timer = None
        except Exception:
            pass
        try:
            if hasattr(self, "_restore_blink_hl") and self._restore_blink_hl:
                self._restore_blink_hl.hide()
                self._restore_blink_hl.deleteLater()
                self._restore_blink_hl = None
                self.canvas.refresh()
        except Exception:
            pass

    def _zoom_to_feature(self, feature_id):
        """Zoom, highlight และกระพริบสีน้ำเงินไปที่ feature ที่เลือก"""
        try:
            from qgis.core import QgsFeatureRequest
            from qgis.gui import QgsHighlight
            from qgis.PyQt.QtCore import QTimer
            from qgis.PyQt.QtGui import QColor
            layer = self._current_layer
            if not layer:
                return
            req = QgsFeatureRequest().setFilterFid(feature_id)
            feat = next(layer.getFeatures(req), None)
            if not feat or not feat.hasGeometry():
                return

            # zoom to feature
            bbox = feat.geometry().boundingBox()
            bbox.scale(2.0)
            self.canvas.setExtent(bbox)
            self.canvas.refresh()

            # หยุด blink เดิมถ้ามี
            if hasattr(self, "_hist_blink_timer") and self._hist_blink_timer:
                try:
                    self._hist_blink_timer.stop()
                except Exception:
                    pass
            if hasattr(self, "_hist_blink_hl") and self._hist_blink_hl:
                try:
                    self._hist_blink_hl.hide()
                    self._hist_blink_hl.deleteLater()
                except Exception:
                    pass

            # สร้าง highlight สีน้ำเงิน
            hl = QgsHighlight(self.canvas, feat.geometry(), layer)
            hl.setColor(QColor(26, 115, 232))   # สีน้ำเงิน #1a73e8
            hl.setFillColor(QColor(26, 115, 232, 60))
            hl.setWidth(4)
            hl.show()
            self.canvas.refresh()
            self._hist_blink_hl = hl

            # กระพริบเร็วๆ 6 ครั้ง (on/off ทุก 200ms)
            blink_state = [0]
            timer = QTimer()
            self._hist_blink_timer = timer

            def _blink():
                blink_state[0] += 1
                try:
                    if blink_state[0] % 2 == 0:
                        hl.show()
                    else:
                        hl.hide()
                    self.canvas.refresh()
                    if blink_state[0] >= 8:
                        timer.stop()
                        hl.show()  # จบที่ show ค้างไว้
                        self.canvas.refresh()
                        self.highlight = hl  # เก็บไว้เป็น current highlight
                except Exception:
                    timer.stop()

            timer.timeout.connect(_blink)
            timer.start(150)  # กระพริบทุก 150ms

        except Exception as e:
            self.iface.messageBar().pushWarning("Clickpointtoedit", f"Zoom error: {e}")

    def _restore_entry(self, feature_id):
        """Restore ค่ากลับไปเป็น original ก่อนแก้ไขทั้งหมด"""
        entry = self._edit_history.get(feature_id)
        if not entry:
            return

        fields_str = ', '.join(entry["original"].keys())
        ts = entry["time_first"]
        msg = f"Restore ข้อมูลกลับไปค่าเดิมก่อนแก้ไขครั้งแรก ({ts})?\n\nField: {fields_str}"
        if QMessageBox.question(
            self.iface.mainWindow(), "ยืนยัน Restore", msg,
            QMessageBox.Yes | QMessageBox.No
        ) != QMessageBox.Yes:
            return

        layer = self._current_layer
        if not isinstance(layer, QgsVectorLayer):
            QMessageBox.warning(self.iface.mainWindow(), "Error",
                                "ไม่พบ layer ที่แก้ไข")
            return

        if not layer.isEditable():
            layer.startEditing()

        lfields = layer.fields()
        for fname, orig_val in entry["original"].items():
            idx = lfields.indexFromName(fname)
            if idx >= 0:
                layer.changeAttributeValue(feature_id, idx, orig_val)
        layer.commitChanges()

        # บันทึก restore log ก่อนลบ history
        self._restore_log.append({
            "time":       datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "layer":      layer.name(),
            "feature_id": feature_id,
            "fields":     dict(entry["original"])
        })
        # ลบออกจาก history
        del self._edit_history[feature_id]

        # กระพริบสีเหลืองตลอด จนกว่า user จะคลิกอะไรสักอย่าง
        try:
            from qgis.PyQt.QtCore import QTimer as _QTimer
            feat_iter = layer.getFeatures(
                QgsFeatureRequest().setFilterFid(feature_id))
            feat_r = next(feat_iter, None)
            if feat_r:
                blink_hl = QgsHighlight(self.canvas, feat_r.geometry(), layer)
                blink_hl.setColor(Qt.yellow)
                blink_hl.setWidth(6)
                blink_hl.show()
                self.canvas.refresh()

                self._restore_blink_hl = blink_hl
                self._restore_blink_timer = _QTimer()
                self._restore_blink_state = [True]

                def _blink():
                    try:
                        self._restore_blink_state[0] = not self._restore_blink_state[0]
                        if self._restore_blink_state[0]:
                            blink_hl.show()
                        else:
                            blink_hl.hide()
                        self.canvas.refresh()
                    except Exception:
                        self._stop_restore_blink()

                self._restore_blink_timer.timeout.connect(_blink)
                self._restore_blink_timer.start(400)

                # หยุดกระพริบเมื่อ user คลิกบน canvas
                self.canvas.clicked.connect(self._stop_restore_blink)                     if hasattr(self.canvas, "clicked") else None
        except Exception:
            pass

        QMessageBox.information(
            self.iface.mainWindow(), "Restore สำเร็จ",
            "✅ Restore ข้อมูลเรียบร้อยแล้ว")

        if self.dock:
            self.iface.removeDockWidget(self.dock)
            self.dock = None

    # ── Progress update ───────────────────────────────────────────
    def _update_progress(self):
        try:
            filled = 0
            total  = max(len(self.fields), 1)
            for _, widget in self.fields:
                if isinstance(widget, QComboBox):
                    if widget.currentText():
                        filled += 1
                elif isinstance(widget, QLineEdit):
                    if widget.text().strip():
                        filled += 1
            pct = int(filled / total * 100)
            self._prog_bar.setValue(pct)
            self._prog_lbl.setText(f"กรอกแล้ว {pct}%")
        except Exception:
            pass

    def _mark_changed(self):
        """เปิดใช้ปุ่มบันทึกเมื่อมีการแก้ไข"""
        self._has_changes = True
        try:
            if self._save_btn:
                self._save_btn.setEnabled(True)
                self._save_btn.setToolTip("")
        except Exception:
            pass

    # ── Street View ───────────────────────────────────────────────
    def _get_feature_latlon(self, feature, layer):
        """แปลง geometry → lat, lon (WGS84)"""
        geom = feature.geometry()
        if geom.isEmpty():
            return None, None
        centroid = geom.centroid().asPoint()
        tr = QgsCoordinateTransform(
            layer.crs(),
            QgsCoordinateReferenceSystem("EPSG:4326"),
            QgsProject.instance())
        pt = tr.transform(QgsPointXY(centroid.x(), centroid.y()))
        return pt.y(), pt.x()

    def _toggle_street_view(self, feature, layer, checked):
        """เปิด/ปิด Street View"""
        from qgis.utils import plugins
        sv_plugin = plugins.get("streetview_qgis_cej")

        if not checked:
            # ปิด Street View
            self._close_street_view(sv_plugin)
            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setText("🗺 Street View")
            return

        # เปิด Street View
        lat, lon = self._get_feature_latlon(feature, layer)
        if lat is None:
            self.iface.messageBar().pushWarning(
                "Clickpointtoedit", "ไม่สามารถดึงพิกัด Feature ได้")
            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setChecked(False)
            return

        # ถ้าไม่มี StreetViewQGISCej → เปิด browser แทน
        if not sv_plugin:
            url = (f"https://www.google.com/maps/@?api=1"
                   f"&map_action=pano&viewpoint={lat},{lon}")
            webbrowser.open(url)
            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setChecked(False)
                self._sv_btn.setText("🗺 Street View")
            self.iface.messageBar().pushInfo(
                "Street View",
                "เปิด Google Maps ใน browser (ติดตั้ง StreetViewQGISCej เพื่อดูใน QGIS)")
            return

        try:
            # login ถ้ายังไม่ได้ login
            login_user = getattr(sv_plugin, "login_user", "")
            api_key    = getattr(sv_plugin, "_api_key", "")
            if not login_user or not api_key:
                if not sv_plugin.ensure_login():
                    if hasattr(self, "_sv_btn") and self._sv_btn:
                        self._sv_btn.setChecked(False)
                    return

            # สร้าง widget ถ้ายังไม่มี
            if not sv_plugin.widget:
                sv_plugin.open_street_view()

            if sv_plugin.widget:
                sv_plugin.widget.load_street_view_html(lat, lon, 0)
                sv_plugin.widget.show()
                sv_plugin.widget.raise_()
                # อัปเดต marker ตำแหน่งใหม่
                try:
                    from qgis.core import (QgsCoordinateTransform,
                        QgsCoordinateReferenceSystem, QgsPointXY,
                        QgsGeometry, QgsProject)
                    canvas  = sv_plugin.widget.iface.mapCanvas()
                    crs_dst = canvas.mapSettings().destinationCrs()
                    tr = QgsCoordinateTransform(
                        QgsCoordinateReferenceSystem("EPSG:4326"),
                        crs_dst, QgsProject.instance())
                    pt = tr.transform(QgsPointXY(lon, lat))
                    sv_plugin.widget.set_marker_canvas_point(pt)
                except Exception:
                    pass
                # ดัก closeEvent ด้วย SVCloseWatcher
                if hasattr(self, "_sv_watcher") and self._sv_watcher:
                    try:
                        sv_plugin.widget.removeEventFilter(self._sv_watcher)
                    except Exception:
                        pass
                self._sv_watcher = SVCloseWatcher(self._on_sv_closed)
                sv_plugin.widget.installEventFilter(self._sv_watcher)
                self._sv_widget_ref = sv_plugin.widget

            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setText("✖ ปิด Street View")
            from qgis.PyQt.QtCore import QTimer
            QTimer.singleShot(300, self._reactivate_tool)

        except Exception as e:
            self.iface.messageBar().pushWarning("Street View", f"Error: {e}")
            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setChecked(False)

    def _close_street_view(self, sv_plugin=None):
        """ซ่อน Street View และล้าง marker geometry (ไม่ลบ layer)"""
        try:
            from qgis.utils import plugins
            from qgis.core import QgsGeometry
            sv = sv_plugin or plugins.get("streetview_qgis_cej")
            if sv and sv.widget:
                sv.widget.hide()
                # ล้าง marker geometry แต่ยัง keep layer ไว้
                try:
                    w = sv.widget
                    if w.point_layer and w.point_fid is not None:
                        w.point_layer.dataProvider().changeGeometryValues(
                            {w.point_fid: QgsGeometry()})
                        w.point_layer.triggerRepaint()
                except Exception:
                    pass
        except Exception:
            pass

    def _on_sv_visibility_changed(self, visible):
        """ดัก event เมื่อ user ปิด Street View dock เอง"""
        if not visible:
            self._close_street_view()
            try:
                if hasattr(self, "_sv_btn") and self._sv_btn:
                    self._sv_btn.setChecked(False)
                    self._sv_btn.setText("🗺 Street View")
            except Exception:
                pass

    def _on_sv_closed(self):
        """ดัก event เมื่อ user กดปิด X บน Street View QDialog"""
        self._close_street_view()
        try:
            if hasattr(self, "_sv_btn") and self._sv_btn:
                self._sv_btn.setChecked(False)
                self._sv_btn.setText("🗺 Street View")
        except Exception:
            pass

    # ── Row helpers ───────────────────────────────────────────────
    def _make_text_row(self, line: QLineEdit, ft: str) -> QWidget:
        line.setMinimumWidth(self.UI_MIN_WIDTH)
        line.setFixedHeight(self.UI_CTRL_HEIGHT)
        line.setEnabled(False)
        line.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        edit_btn = QPushButton("แก้ไข")
        edit_btn.setObjectName("btn_edit")
        edit_btn.setFixedHeight(self.UI_CTRL_HEIGHT)
        edit_btn.setCursor(Qt.PointingHandCursor)

        def toggle():
            if line.isEnabled():
                line.setEnabled(False)
                edit_btn.setText("แก้ไข")
            else:
                line.setEnabled(True)
                line.setFocus()
                line.selectAll()
                edit_btn.setText("ล็อก")
        edit_btn.clicked.connect(toggle)

        badge = QLabel(f"({ft})")
        badge.setObjectName("type_badge")

        row = QHBoxLayout()
        row.setContentsMargins(0, 0, 0, 0)
        row.setSpacing(6)
        row.addWidget(line, 1)
        row.addWidget(edit_btn)
        row.addWidget(badge)

        w = QWidget()
        w.setLayout(row)
        return w

    def _make_row(self, ctrl: QWidget, ft: str) -> QWidget:
        ctrl.setMinimumWidth(self.UI_MIN_WIDTH)
        ctrl.setFixedHeight(self.UI_CTRL_HEIGHT)
        if isinstance(ctrl, QComboBox):
            ctrl.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow)
            ctrl.setMinimumContentsLength(24)
        ctrl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        badge = QLabel(f"({ft})")
        badge.setObjectName("type_badge")

        row = QHBoxLayout()
        row.setContentsMargins(0, 0, 0, 0)
        row.setSpacing(6)
        row.addWidget(ctrl, 1)
        row.addWidget(badge)

        w = QWidget()
        w.setLayout(row)
        return w

    # ── Save / Cancel / Export ────────────────────────────────────
    def save_data(self, layer, feature_id, feature):
        if not layer.isEditable():
            return
        fields        = layer.fields()
        updated       = False
        changed_fields = {}  # {field_name: (old_val, new_val)}

        manual_max_length = {
            "BLDG_ID": 15, "BL_TYPE": 2, "BL_NSTOREY": 2, "BL_NUNIT": 4,
            "BL_UNIT_F": 10, "BL_AREA_FLAG": 1, "BL_NAME_T": 100,
            "BL_NAME_E": 100, "BL_HOUSENUM": 10, "BL_VILLNUM": 10,
            "BL_VILLAGE": 35, "BL_ROAD": 150, "BL_SUBDISTRICT": 30,
            "BL_DISTRICT": 30, "BL_CHANGWAT": 30, "BL_POSTCODE": 5,
            "BL_ADDRESS": 180, "BL_ACT_MAJOR": 45, "BL_ACT_MINOR": 45,
            "BL_ACT_OTHER": 45, "REMARK": 255, "MATCHING": 1, "STATUS": 2,
            "SOURCE": 2, "S_DATE": 6, "CREATED_USER": 255,
            "LAST_EDITED_USER": 255, "BL_HID": 11, "BL_PAGE_NO": 50
        }

        for name, widget in self.fields:
            idx   = fields.indexFromName(name)
            field = fields[idx]
            ft    = field.typeName().lower()
            if ft in ["geometry", "blob", "clob"]:
                continue

            def _safe_old(n): return self._get_field_value(feature, n)

            if name == "BL_OWNER" and isinstance(widget, QComboBox):
                value     = self.bl_owner_map.get(widget.currentText())
                old_value = _safe_old(name)
            elif name == "BL_AREA_FLAG" and isinstance(widget, QComboBox):
                value     = self.bl_area_flag_map.get(widget.currentText())
                old_value = _safe_old(name)
            elif name == "BL_TYPE" and isinstance(widget, QComboBox):
                value     = self.bl_type_map.get(widget.currentText())
                old_value = _safe_old(name)
            elif name.strip().upper() == "BL_MATL" and isinstance(widget, QComboBox):
                value     = self.bl_matl_map.get(widget.currentText())
                old_value = _safe_old(name)
            elif name.strip().upper() == "BL_USE" and isinstance(widget, QLineEdit):
                t = widget.text().strip()
                if t in self.bl_use_map:
                    value = self.bl_use_map[t]
                elif t.isdigit():
                    code_val = int(t)
                    value = code_val if code_val in self.bl_use_reverse_map else None
                else:
                    value = None
                old_value = _safe_old(name)
            else:
                vs        = widget.text().strip()
                old_value = _safe_old(name)
                if vs.upper() in ["NULL", "NONE", ""]:
                    value = None
                elif ft in ['integer', 'int', 'integer32', 'integer64']:
                    try: value = int(vs)
                    except Exception: value = None
                elif ft in ['double', 'real', 'float']:
                    try: value = float(vs)
                    except Exception: value = None
                elif name in manual_max_length:
                    ml    = manual_max_length[name]
                    value = vs[:ml] if len(vs) > ml else vs
                else:
                    value = str(vs)

            if (value != old_value) and not (
                    value is None and old_value in [None, '', 'NULL', 'None']):
                layer.changeAttributeValue(feature_id, idx, value)
                changed_fields[name] = (old_value, value)
                updated = True

        if updated:
            layer.commitChanges()
            self._edit_count_today += 1

            # เก็บ history ใน memory — merge เข้ารายการเดียวต่อ feature
            now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            if feature_id not in self._edit_history:
                # ครั้งแรก: เก็บค่า original ไว้
                self._edit_history[feature_id] = {
                    "time_first": now_str,
                    "time_last":  now_str,
                    "layer":      layer.name(),
                    "original":   {k: v[0] for k, v in changed_fields.items()},
                    "current":    {k: v[1] for k, v in changed_fields.items()},
                }
            else:
                # ครั้งต่อไป: อัปเดต current + เพิ่ม field ใหม่ที่ยังไม่มี original
                entry = self._edit_history[feature_id]
                entry["time_last"] = now_str
                for k, (old_v, new_v) in changed_fields.items():
                    if k not in entry["original"]:
                        entry["original"][k] = old_v
                    entry["current"][k] = new_v

            green = QgsHighlight(self.canvas, feature.geometry(), layer)
            green.setColor(Qt.green)
            green.setWidth(3)
            green.show()
            self.saved_highlights.append(green)

            log_path = os.path.join(os.path.expanduser("~"), "edit_log.csv")
            with open(log_path, "a", newline="", encoding="utf-8") as lf:
                csv.writer(lf).writerow([
                    datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    layer.name(), feature_id
                ])

            QMessageBox.information(
                self.iface.mainWindow(), "บันทึกสำเร็จ",
                "✅ บันทึกข้อมูลเรียบร้อยแล้ว")

            # ปิด Street View dock + ลบ marker ถ้าเปิดอยู่
            try:
                from qgis.utils import plugins
                if "streetview_qgis_cej" in plugins:
                    sv = plugins["streetview_qgis_cej"]
                    if sv.widget:
                        sv.widget.reset_state()
                        sv.iface.removeDockWidget(sv.widget)
                        sv.widget = None
            except Exception:
                pass

            try:
                if self.dock:
                    self.iface.removeDockWidget(self.dock)
                    self.dock.close()
                    self.dock = None
            except Exception:
                pass
        else:
            QMessageBox.information(
                self.iface.mainWindow(), "ไม่มีการเปลี่ยนแปลง",
                "ไม่มีข้อมูลที่ถูกเปลี่ยนแปลง")

    def confirm_save(self, layer, feature_id, feature):
        if QMessageBox.question(
            self.iface.mainWindow(), "ยืนยันการบันทึก",
            "คุณแน่ใจหรือไม่ว่าต้องการบันทึกการแก้ไขนี้?",
            QMessageBox.Yes | QMessageBox.No
        ) == QMessageBox.Yes:
            self.save_data(layer, feature_id, feature)

    def cancel_action(self):
        if QMessageBox.question(
            self.iface.mainWindow(), "ยกเลิกการแก้ไข",
            "คุณแน่ใจหรือไม่ว่าต้องการยกเลิก?",
            QMessageBox.Yes | QMessageBox.No
        ) == QMessageBox.Yes:
            layer = self._current_layer or self.iface.activeLayer()
            if layer:
                try:
                    layer.rollBack()
                except Exception:
                    pass
            # เคลียร์ highlight ทั้งหมด
            self._clear_all_highlights()
            # ปิด Street View dock (แต่ไม่ logout)
            self._close_street_view()
            try:
                if hasattr(self, "_sv_btn") and self._sv_btn:
                    self._sv_btn.setChecked(False)
                    self._sv_btn.setText("🗺 Street View")
            except Exception:
                pass
            QMessageBox.information(
                self.iface.mainWindow(), "ยกเลิกแล้ว", "ยกเลิกการแก้ไขเรียบร้อยแล้ว")
            if self.dock:
                self.iface.removeDockWidget(self.dock)
                self.dock.close()
                self.dock = None

    def export_layer_and_log(self):
        if QMessageBox.question(
            self.iface.mainWindow(), "Export Log",
            "คุณต้องการ export รายงานการแก้ไขหรือไม่?",
            QMessageBox.Yes | QMessageBox.No
        ) != QMessageBox.Yes:
            return

        log_path    = os.path.join(os.path.expanduser("~"), "edit_log.csv")
        edited_data = {}

        if os.path.exists(log_path):
            try:
                with open(log_path, "r", encoding="utf-8") as lf:
                    for line in lf:
                        parts = line.strip().split(",")
                        if len(parts) >= 3:
                            try:
                                edited_data[int(parts[2])] = parts[0]
                            except ValueError:
                                pass
            except Exception as e:
                print("Error reading log:", e)

        layer = self._current_layer or self.iface.activeLayer()
        if not layer:
            QMessageBox.warning(
                self.iface.mainWindow(), "ไม่มีชั้นข้อมูล",
                "กรุณาเลือกชั้นข้อมูลก่อน export")
            return

        has_restore = bool(self._restore_log)
        fmt_hint    = "Excel Files (*.xlsx)" if has_restore else "CSV Files (*.csv)"
        default_ext = ".xlsx"                if has_restore else ".csv"
        save_path, _ = QFileDialog.getSaveFileName(
            None,
            "บันทึกรายงาน",
            f"edit_report{default_ext}",
            fmt_hint + ";;CSV Files (*.csv);;Excel Files (*.xlsx)"
        )
        if not save_path:
            return

        use_xlsx = save_path.lower().endswith(".xlsx")

        try:
            if use_xlsx:
                self._export_xlsx(save_path, layer, edited_data)
            else:
                self._export_csv(save_path, layer, edited_data)

            msg = f"✅ Export เรียบร้อยแล้ว\n{save_path}"
            if has_restore and use_xlsx:
                msg += "\n\n📋 มี sheet 'Restore Log' แสดงรายการ restore ด้วย"
            QMessageBox.information(
                self.iface.mainWindow(), "Export สำเร็จ", msg)
            self._clear_all_highlights()
            if os.path.exists(log_path):
                os.remove(log_path)
        except Exception as e:
            QMessageBox.warning(
                self.iface.mainWindow(), "Error",
                f"ไม่สามารถ export ได้:\n{str(e)}")

    def _export_csv(self, path, layer, edited_data):
        """Export แบบ CSV (ไม่มี restore sheet)"""
        with codecs.open(path, "w", "utf-8-sig") as f:
            writer  = csv.writer(f)
            headers = [fi.name() for fi in layer.fields()] + ["edited_at"]
            writer.writerow(headers)
            for feat in layer.getFeatures():
                if feat.id() in edited_data:
                    try:
                        values = [self._get_field_value(feat, fi.name())
                                  for fi in layer.fields()]
                        values.append(edited_data[feat.id()])
                        writer.writerow(values)
                    except Exception:
                        pass

    def _export_xlsx(self, path, layer, edited_data):
        """Export แบบ XLSX — sheet 1: Edit Log, sheet 2: Restore Log"""
        try:
            import openpyxl
            from openpyxl.styles import PatternFill, Font, Alignment
        except ImportError:
            # fallback เป็น CSV ถ้าไม่มี openpyxl
            csv_path = path.replace(".xlsx", ".csv")
            self._export_csv(csv_path, layer, edited_data)
            raise Exception(
                f"ไม่พบ openpyxl — บันทึกเป็น CSV แทนที่ {csv_path}\n"
                "ติดตั้ง openpyxl ด้วย: pip install openpyxl")

        wb = openpyxl.Workbook()

        # ── Sheet 1: Edit Log ──
        ws1 = wb.active
        ws1.title = "Edit Log"
        hdr_fill = PatternFill("solid", fgColor="1a73e8")
        hdr_font = Font(color="FFFFFF", bold=True)

        field_names = [fi.name() for fi in layer.fields()]
        headers     = field_names + ["edited_at"]
        for ci, h in enumerate(headers, 1):
            cell = ws1.cell(row=1, column=ci, value=h)
            cell.fill = hdr_fill
            cell.font = hdr_font
            cell.alignment = Alignment(horizontal="center")

        row_idx = 2
        for feat in layer.getFeatures():
            if feat.id() in edited_data:
                try:
                    values = [self._get_field_value(feat, n) for n in field_names]
                    values.append(edited_data[feat.id()])
                    for ci, v in enumerate(values, 1):
                        ws1.cell(row=row_idx, column=ci, value=v)
                    row_idx += 1
                except Exception:
                    pass

        # auto width
        for col in ws1.columns:
            max_len = max((len(str(c.value or "")) for c in col), default=10)
            ws1.column_dimensions[col[0].column_letter].width = min(max_len + 4, 40)

        # ── Sheet 2: Restore Log (ถ้ามี) ──
        if self._restore_log:
            ws2 = wb.create_sheet("Restore Log")
            rst_fill = PatternFill("solid", fgColor="e65100")
            rst_font = Font(color="FFFFFF", bold=True)

            # หา field names ทั้งหมดที่ถูก restore
            all_fields = []
            for entry in self._restore_log:
                for fn in entry["fields"]:
                    if fn not in all_fields:
                        all_fields.append(fn)

            rst_headers = ["restored_at", "layer", "feature_id"] + all_fields
            for ci, h in enumerate(rst_headers, 1):
                cell = ws2.cell(row=1, column=ci, value=h)
                cell.fill = rst_fill
                cell.font = rst_font
                cell.alignment = Alignment(horizontal="center")

            for ri, entry in enumerate(self._restore_log, 2):
                ws2.cell(row=ri, column=1, value=entry["time"])
                ws2.cell(row=ri, column=2, value=entry["layer"])
                ws2.cell(row=ri, column=3, value=entry["feature_id"])
                for ci, fn in enumerate(all_fields, 4):
                    ws2.cell(row=ri, column=ci,
                             value=str(entry["fields"].get(fn, "")))

            for col in ws2.columns:
                max_len = max((len(str(c.value or "")) for c in col), default=10)
                ws2.column_dimensions[col[0].column_letter].width = min(max_len + 4, 40)

        wb.save(path)
