# -*- coding: utf-8 -*-
import os

from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import (
    QApplication,
    QDialog,
    QDialogButtonBox,
    QFileDialog,
    QHBoxLayout,
    QLabel,
    QPlainTextEdit,
    QPushButton,
    QSplitter,
    QTableWidgetItem,
    QMessageBox,
    QHeaderView,
    QVBoxLayout,
    QWidget,
)
from qgis.PyQt.QtCore import Qt, QEvent, QItemSelection, QItemSelectionModel, QTimer
from qgis.PyQt.QtGui import QColor, QBrush
from qgis.core import (
    QgsProject,
    QgsCoordinateTransform,
    QgsFeatureRequest,
    QgsGeometry,
    QgsRectangle,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.gui import QgsRubberBand

from .gpkg_data_manager import GpkgDataManager
from .status_expression import evaluate_row_expr
from .column_config_dialog import (
    ColumnConfigDialog,
    COLUMN_HIDDEN,
    COLUMN_DISPLAY,
    COLUMN_EDITABLE,
    COLUMN_INFO,
)

FORM_CLASS, _ = uic.loadUiType(
    os.path.join(os.path.dirname(__file__), 'gpkg_editor_dockwidget_base.ui')
)

# テキスト色
COLOR_EDITABLE = QBrush(QColor(0, 0, 255))   # 青: 編集可能（未編集）
COLOR_EDITED = QBrush(QColor(255, 0, 0))      # 赤: 編集済み


class GpkgEditorWindow(QWidget, FORM_CLASS):
    """GPKG編集用ウィンドウ。"""

    def __init__(self, iface, parent=None):
        super().__init__(parent)
        self.setupUi(self)

        # 左パネル/右パネルをQSplitterに収める
        self._splitter = QSplitter(Qt.Horizontal, self)
        self.mainLayout.removeWidget(self.leftPanel)
        self.mainLayout.removeWidget(self.rightPanel)
        self._splitter.addWidget(self.leftPanel)
        self._splitter.addWidget(self.rightPanel)
        self._splitter.setSizes([380, 820])
        self._splitter.setChildrenCollapsible(True)
        self._splitter.setHandleWidth(0)
        self._splitter.splitterMoved.connect(self._on_splitter_moved)
        self.mainLayout.addWidget(self._splitter)

        # 左パネル開閉チェックボックス
        self.chkPanelClose.toggled.connect(self._on_panel_close_toggled)

        # ショートカットアコーディオン
        self.btnShortcutsToggle.toggled.connect(self._toggle_shortcuts)
        # 描画完了後に初期状態（閉じ）の高さ制約を適用
        QTimer.singleShot(0, self._apply_shortcuts_closed_height)

        self.iface = iface
        self.data_manager = GpkgDataManager()
        self.column_config = {}
        self._current_fids = []
        self._editing = False
        self._locked = False
        self._rubber_bands = []
        self._rubber_bands_base = []   # 全体表示モード用（計画全フィーチャー）
        self._plan_active = False
        self._active_plan_name = None
        self._status_expr1 = ''
        self._status_expr2 = ''
        self._current_merged_data = []
        self._feature_add_mode = False  # フィーチャー追加フロー中かどうか
        # GPKGレイヤー一覧を初期化
        self._refresh_layer_combo()

        # シグナル接続
        self.cmbGpkgLayer.currentIndexChanged.connect(self._on_layer_selected)
        self.btnColumnConfig.clicked.connect(self._on_column_config)
        self.btnExportGpkg.clicked.connect(self._on_export_gpkg)
        self.btnExportCsv.clicked.connect(self._on_export_csv)
        self.btnLock.toggled.connect(self._on_lock_toggled)
        self.chkOverwrite.toggled.connect(self._on_overwrite_toggled)
        self.chkShowAll.toggled.connect(self._on_show_all_toggled)
        self.chkFullscreen.toggled.connect(self._on_fullscreen_toggled)
        self.btnPlanSave.clicked.connect(self._on_plan_save)
        self.btnPlanDelete.clicked.connect(self._on_plan_delete)
        self.btnPlanAddFeature.clicked.connect(self._on_plan_add_feature)
        self.btnPlanDeleteFeature.clicked.connect(self._on_plan_delete_feature)
        self.cmbPlan.currentIndexChanged.connect(self._on_plan_selected)
        self.btnStatusRow1.clicked.connect(self._on_status_row1_config)
        self.btnStatusRow2.clicked.connect(self._on_status_row2_config)
        self.tableFeatures.cellChanged.connect(self._on_cell_changed)

        # 複数選択を有効化（セル編集を維持するため SelectItems のまま）
        self.tableFeatures.setSelectionMode(
            self.tableFeatures.ExtendedSelection
        )

        # テーブル行選択→マップ中心移動（ロック中のみ有効）＋ステータス更新
        self.tableFeatures.selectionModel().currentRowChanged.connect(
            self._on_table_row_changed
        )
        self.tableFeatures.selectionModel().selectionChanged.connect(
            self._on_table_selection_changed
        )
        self.tableFeatures.selectionModel().currentRowChanged.connect(
            lambda *_: self._update_status_display()
        )

        # プロジェクトのレイヤー追加/削除を監視してコンボを自動更新
        QgsProject.instance().layersAdded.connect(self._refresh_layer_combo)
        QgsProject.instance().layersRemoved.connect(self._refresh_layer_combo)

        # マップ選択変更の監視
        self.iface.mapCanvas().selectionChanged.connect(self._on_selection_changed)
        self.iface.mapCanvas().viewport().installEventFilter(self)
        # Shift+スクロール→横スクロール（eventFilter）
        self.tableFeatures.viewport().installEventFilter(self)

        # Enter→編集開始/確定トグル
        self.tableFeatures.installEventFilter(self)

    def eventFilter(self, obj, event):
        # Shift+ホイール → 横スクロール
        if obj == self.tableFeatures.viewport() and event.type() == QEvent.Wheel:
            if event.modifiers() == Qt.ShiftModifier:
                sb = self.tableFeatures.horizontalScrollBar()
                sb.setValue(sb.value() - event.angleDelta().y())
                return True

        # Ctrl+C → 選択セルをタブ区切りでコピー
        if obj == self.tableFeatures and event.type() == QEvent.KeyPress:
            if event.key() == Qt.Key_C and event.modifiers() == Qt.ControlModifier:
                self._copy_selected_cells()
                return True

        # Ctrl+V → クリップボードの内容を選択セルからペースト
        if obj == self.tableFeatures and event.type() == QEvent.KeyPress:
            if event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
                if self.tableFeatures.state() != self.tableFeatures.EditingState:
                    self._paste_to_selected_cells()
                    return True

        # Ctrl+(Shift+)矢印キー → 端セルへ移動または範囲選択
        if obj == self.tableFeatures and event.type() == QEvent.KeyPress:
            mods = event.modifiers()
            key = event.key()
            if (mods in (Qt.ControlModifier, Qt.ControlModifier | Qt.ShiftModifier)
                    and key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right)
                    and self.tableFeatures.state() != self.tableFeatures.EditingState):
                current = self.tableFeatures.currentIndex()
                if current.isValid():
                    row, col = current.row(), current.column()
                    if key == Qt.Key_Up:
                        target_row, target_col = 0, col
                    elif key == Qt.Key_Down:
                        target_row, target_col = self.tableFeatures.rowCount() - 1, col
                    elif key == Qt.Key_Left:
                        target_row, target_col = row, 0
                    else:  # Key_Right
                        target_row, target_col = row, self.tableFeatures.columnCount() - 1
                    if mods == (Qt.ControlModifier | Qt.ShiftModifier):
                        target = self.tableFeatures.model().index(target_row, target_col)
                        sel = QItemSelection(current, target)
                        self.tableFeatures.selectionModel().select(
                            sel, QItemSelectionModel.ClearAndSelect
                        )
                    else:
                        self.tableFeatures.setCurrentCell(target_row, target_col)
                    return True

        # Enter キー → 編集開始/確定トグル
        if obj == self.tableFeatures and event.type() == QEvent.KeyPress:
            if event.key() in (Qt.Key_Return, Qt.Key_Enter):
                state = self.tableFeatures.state()
                if state == self.tableFeatures.EditingState:
                    # 編集中 → 確定（デフォルト動作に委譲）
                    return False
                else:
                    # 非編集中 → 編集開始
                    current = self.tableFeatures.currentIndex()
                    if current.isValid():
                        item = self.tableFeatures.item(
                            current.row(), current.column()
                        )
                        if item and (item.flags() & Qt.ItemIsEditable):
                            self.tableFeatures.editItem(item)
                            return True
                return False

        return super().eventFilter(obj, event)

    def cleanup(self):
        """プラグイン終了時のリソース解放。unload から呼ばれる。"""
        self._clear_rubber_bands()
        self._clear_rubber_bands_base()
        try:
            self.iface.mapCanvas().selectionChanged.disconnect(
                self._on_selection_changed
            )
        except TypeError:
            pass
        try:
            QgsProject.instance().layersAdded.disconnect(self._refresh_layer_combo)
            QgsProject.instance().layersRemoved.disconnect(self._refresh_layer_combo)
        except TypeError:
            pass
        self.data_manager.close()

    # ──────────────────────────────────────────────
    # 左パネル開閉
    # ──────────────────────────────────────────────

    def _on_splitter_moved(self, pos, index):
        """スプリッターのドラッグ操作を禁止して固定位置に戻す。"""
        sizes = self._splitter.sizes()
        total = sum(sizes)
        if sizes[0] > 0:
            # 左パネルが開いている場合は幅を 380 に固定
            self._splitter.setSizes([380, max(0, total - 380)])
        else:
            # 左パネルが閉じている場合はそのまま維持
            self._splitter.setSizes([0, total])

    def _on_panel_close_toggled(self, checked):
        sizes = self._splitter.sizes()
        if checked:
            self._splitter.setSizes([0, sum(sizes)])
        else:
            self._splitter.setSizes([380, max(0, sizes[1] - 380)])

    def _apply_shortcuts_closed_height(self):
        """初期・閉じ時: shortcutsSection をボタン1行分に制限し dock を縮小。
        dock 縮小はフロート時には行わない（表示崩れの原因になるため）。
        """
        h = self.btnShortcutsToggle.height()
        self.shortcutsSection.setMaximumHeight(h if h > 0 else 28)
        dock = self.parentWidget()
        # isFloating() を持つ（QDockWidget）かつ格納中のときのみ高さを強制縮小
        if dock and not getattr(dock, 'isFloating', lambda: True)():
            target = self.rightPanel.sizeHint().height()
            dock.setMaximumHeight(target)
            QTimer.singleShot(100, lambda: dock.setMaximumHeight(16777215))

    def _toggle_shortcuts(self, checked):
        self.shortcutsContent.setVisible(checked)
        self.btnShortcutsToggle.setText(
            '▼ ショートカット' if checked else '▶ ショートカット'
        )
        if checked:
            self.shortcutsSection.setMaximumHeight(16777215)
        else:
            QTimer.singleShot(0, self._apply_shortcuts_closed_height)

    # ──────────────────────────────────────────────
    # GPKGレイヤー コンボボックス
    # ──────────────────────────────────────────────

    def _refresh_layer_combo(self, *_args):
        """プロジェクト内のGPKGレイヤーでコンボボックスを更新する。"""
        prev_id = self.cmbGpkgLayer.currentData()

        self.cmbGpkgLayer.blockSignals(True)
        self.cmbGpkgLayer.clear()
        self.cmbGpkgLayer.addItem('-- 選択してください --', None)

        layer_tree = QgsProject.instance().layerTreeRoot()
        restore_idx = 0
        for layer in QgsProject.instance().mapLayers().values():
            if not isinstance(layer, QgsVectorLayer):
                continue
            # レイヤーツリー（パネル）に存在しないものは除外
            # （他プラグインがレジストリ未削除のままツリーからだけ消した場合の対策）
            if layer_tree.findLayer(layer.id()) is None:
                continue
            src = layer.source().split('|')[0]
            if not src.lower().endswith('.gpkg'):
                continue
            self.cmbGpkgLayer.addItem(layer.name(), layer.id())
            if layer.id() == prev_id:
                restore_idx = self.cmbGpkgLayer.count() - 1

        self.cmbGpkgLayer.setCurrentIndex(restore_idx)
        self.cmbGpkgLayer.blockSignals(False)

    def _on_layer_selected(self, index):
        """コンボボックスでレイヤーが選択された時。"""
        # ロック・計画を解除
        if self._locked:
            self.btnLock.setChecked(False)
        self._deactivate_plan()

        layer_id = self.cmbGpkgLayer.currentData()
        if not layer_id:
            self.data_manager.close()
            self.column_config = {}
            self._clear_table()
            self.btnColumnConfig.setEnabled(False)
            self.btnExportGpkg.setEnabled(False)
            self.btnExportCsv.setEnabled(False)
            self.btnLock.setEnabled(False)
            self.btnStatusRow1.setEnabled(False)
            self.btnStatusRow2.setEnabled(False)
            self._set_plan_ui_enabled(False)
            self.lblStatus.setText('GPKGレイヤーを選択してください')
            return

        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return

        gpkg_path = layer.source().split('|')[0]

        try:
            self.data_manager.load_gpkg(gpkg_path)
        except ValueError as e:
            QMessageBox.critical(self, 'エラー', str(e))
            return

        self.lblStatus.setText(f'読込完了: {layer.name()}')

        columns = self.data_manager.get_original_fields()
        self.column_config = {col: COLUMN_HIDDEN for col in columns}

        self.btnColumnConfig.setEnabled(True)
        self.btnExportGpkg.setEnabled(True)
        self.btnExportCsv.setEnabled(True)
        self.btnLock.setEnabled(True)
        self.btnStatusRow1.setEnabled(True)
        self.btnStatusRow2.setEnabled(True)
        self._set_plan_ui_enabled(True)
        self._refresh_plan_combo()

        self._clear_table()

    # ──────────────────────────────────────────────
    # カラム設定
    # ──────────────────────────────────────────────

    def _on_column_config(self):
        columns = self.data_manager.get_original_fields()
        dlg = ColumnConfigDialog(columns, self.column_config, self)
        if dlg.exec_() == ColumnConfigDialog.Accepted:
            self.column_config = dlg.get_config()
            if self._current_fids:
                self._update_table(self._current_fids)
            else:
                visible_cols = self._get_visible_cols()
                self._editing = True
                self.tableFeatures.setRowCount(0)
                self.tableFeatures.setColumnCount(len(visible_cols))
                self.tableFeatures.setHorizontalHeaderLabels(visible_cols)
                self._editing = False

    # ──────────────────────────────────────────────
    # ロックモード
    # ──────────────────────────────────────────────

    def _on_lock_toggled(self, checked):
        self._locked = checked
        if checked:
            self.btnLock.setText('ロック中')
        else:
            self.btnLock.setText('ロック')
            if not self._plan_active:
                self._clear_rubber_bands()
                self._process_selection()

    def _highlight_feature(self, fid):
        """指定フィーチャーのみをラバーバンドで強調表示する。"""
        self._highlight_features([fid] if fid is not None else [])

    def _highlight_features(self, fids):
        """複数フィーチャーをラバーバンドで強調表示する。
        全体表示モード時は90%透過の黄色で選択フィーチャーを表示する。
        """
        self._clear_rubber_bands()
        if not self.data_manager.original_layer or not fids:
            return

        if self.chkShowAll.isChecked():
            stroke = QColor(255, 200, 0, 200)  # アンバー（不透明寄り）
            fill = QColor(255, 255, 0, 26)      # 黄色 90%透過 (alpha≈26)
            width = 2.0
        else:
            stroke = QColor(255, 0, 0, 180)
            fill = QColor(0, 0, 0, 0)
            width = 1.3

        # 最適化: レイヤーのジオメトリタイプに合わせて1つのラバーバンドを作成
        layer = self.data_manager.original_layer
        rb = QgsRubberBand(self.iface.mapCanvas(), layer.geometryType())
        rb.setStrokeColor(stroke)
        rb.setFillColor(fill)
        rb.setWidth(width)

        request = QgsFeatureRequest().setFilterFids(fids)
        for feat in layer.getFeatures(request):
            if feat.geometry().isNull():
                continue
            rb.addGeometry(feat.geometry(), layer)
        self._rubber_bands.append(rb)

    def _clear_rubber_bands(self):
        for rb in self._rubber_bands:
            self.iface.mapCanvas().scene().removeItem(rb)
        self._rubber_bands.clear()

    def _clear_rubber_bands_base(self):
        """全体表示用ラバーバンド（計画全フィーチャー）をクリアする。"""
        for rb in self._rubber_bands_base:
            self.iface.mapCanvas().scene().removeItem(rb)
        self._rubber_bands_base.clear()

    def _update_show_all_display(self):
        """全体表示モードで計画全フィーチャーをラバーバンド表示する。"""
        self._clear_rubber_bands_base()
        if not self.chkShowAll.isChecked():
            return
        if not self._current_fids or not self.data_manager.original_layer:
            return

        # 最適化: 全体表示用も1つのラバーバンドにまとめる
        layer = self.data_manager.original_layer
        rb = QgsRubberBand(self.iface.mapCanvas(), layer.geometryType())
        rb.setStrokeColor(QColor(255, 0, 0, 120))
        rb.setFillColor(QColor(0, 0, 0, 0))
        rb.setWidth(1.0)

        request = QgsFeatureRequest().setFilterFids(self._current_fids)
        for feat in layer.getFeatures(request):
            if feat.geometry().isNull():
                continue
            rb.addGeometry(feat.geometry(), layer)
        self._rubber_bands_base.append(rb)

    def _on_show_all_toggled(self, checked):
        """全体表示チェックの切り替え処理。"""
        if checked:
            self._update_show_all_display()
        else:
            self._clear_rubber_bands_base()
            fids = self._get_selected_fids()
            if fids and (self._locked or self._plan_active):
                self._highlight_features(fids)
            else:
                self._clear_rubber_bands()

    def _on_fullscreen_toggled(self, checked):
        """全画面表示チェックの切り替え処理。フロート中のみ有効。"""
        dock = self.parentWidget()
        if not dock or not getattr(dock, 'isFloating', lambda: False)():
            self.chkFullscreen.blockSignals(True)
            self.chkFullscreen.setChecked(False)
            self.chkFullscreen.blockSignals(False)
            return
        win = dock.window()
        if checked:
            win.showMaximized()
        else:
            win.showNormal()

    def _on_table_row_changed(self, current, _previous):
        """ロック中または計画アクティブ時：テーブル行選択→マップ中心移動。
        複数選択時は全選択フィーチャーの合算バウンディングボックス中心に移動する。
        """
        if not self._locked and not self._plan_active:
            return
        if not current.isValid():
            return
        if not self.data_manager.original_layer:
            return

        selected_fids = self._get_selected_fids()
        if not selected_fids:
            item = self.tableFeatures.item(current.row(), 0)
            if item:
                fid = item.data(Qt.UserRole)
                if fid is not None:
                    selected_fids = [fid]
        if not selected_fids:
            return

        layer = self.data_manager.original_layer
        canvas = self.iface.mapCanvas()
        canvas_crs = canvas.mapSettings().destinationCrs()
        layer_crs = layer.crs()
        need_transform = (
            layer_crs.isValid() and canvas_crs.isValid() and layer_crs != canvas_crs
        )

        def to_canvas_crs(point):
            if need_transform:
                t = QgsCoordinateTransform(layer_crs, canvas_crs, QgsProject.instance())
                return t.transform(point)
            return point

        if len(selected_fids) == 1:
            request = QgsFeatureRequest().setFilterFids(selected_fids)
            feat = next(layer.getFeatures(request), None)
            if feat and not feat.geometry().isNull():
                center = to_canvas_crs(feat.geometry().boundingBox().center())
                canvas.setCenter(center)
                canvas.refresh()
        else:
            # 複数選択: 全フィーチャーの合算バウンディングボックス中心へ移動
            combined_extent = None
            request = QgsFeatureRequest().setFilterFids(selected_fids)
            for feat in layer.getFeatures(request):
                if feat.geometry().isNull():
                    continue
                bbox = feat.geometry().boundingBox()
                if combined_extent is None:
                    combined_extent = QgsRectangle(bbox)
                else:
                    combined_extent.combineExtentWith(bbox)
            if combined_extent and not combined_extent.isEmpty():
                center = to_canvas_crs(combined_extent.center())
                canvas.setCenter(center)
                canvas.refresh()

    def _on_table_selection_changed(self, selected, deselected):
        """テーブルの選択変更時：選択された全フィーチャーを強調表示。"""
        if not self._locked and not self._plan_active:
            return

        # 選択中の全行からfidを収集
        fids = self._get_selected_fids()
        self._highlight_features(fids)

    def _get_selected_fids(self):
        """テーブルで選択中の全行のfidリストを返す。"""
        selected_rows = set()
        for idx in self.tableFeatures.selectionModel().selectedIndexes():
            selected_rows.add(idx.row())

        fids = []
        for row in selected_rows:
            item = self.tableFeatures.item(row, 0)
            if item:
                fid = item.data(Qt.UserRole)
                if fid is not None:
                    fids.append(fid)
        return fids

    def _copy_selected_cells(self):
        """選択セルをタブ区切りテキストとしてクリップボードにコピーする。"""
        indexes = self.tableFeatures.selectionModel().selectedIndexes()
        if not indexes:
            return

        # 行・列でソート
        rows = sorted(set(idx.row() for idx in indexes))
        cols = sorted(set(idx.column() for idx in indexes))
        selected_set = {(idx.row(), idx.column()) for idx in indexes}

        lines = []
        for row in rows:
            cells = []
            for col in cols:
                if (row, col) in selected_set:
                    item = self.tableFeatures.item(row, col)
                    cells.append(item.text() if item else '')
                else:
                    cells.append('')
            lines.append('\t'.join(cells))

        QApplication.clipboard().setText('\n'.join(lines))

    def _paste_to_selected_cells(self):
        """クリップボードのテキストを選択セルからペーストする。
        コピー元が1セルの場合は選択中の全編集可能セルに同じ値を展開する。
        """
        text = QApplication.clipboard().text()
        if not text:
            return

        # タブ・改行でパース（末尾の空行は除去）
        paste_rows = [row.split('\t') for row in text.splitlines()]
        if not paste_rows:
            return

        indexes = self.tableFeatures.selectionModel().selectedIndexes()

        # ── 1セルコピー: 選択中の全編集可能セルに同じ値を展開 ──
        if len(paste_rows) == 1 and len(paste_rows[0]) == 1:
            value = paste_rows[0][0]
            targets = indexes if indexes else []
            if not targets:
                current = self.tableFeatures.currentIndex()
                if current.isValid():
                    targets = [current]
            for idx in targets:
                item = self.tableFeatures.item(idx.row(), idx.column())
                if item and (item.flags() & Qt.ItemIsEditable):
                    item.setText(value)
            return

        # ── 複数セルコピー: 左上を起点にグリッド展開 ──
        if indexes:
            start_row = min(idx.row() for idx in indexes)
            start_col = min(idx.column() for idx in indexes)
        else:
            current = self.tableFeatures.currentIndex()
            if not current.isValid():
                return
            start_row = current.row()
            start_col = current.column()

        row_count = self.tableFeatures.rowCount()
        col_count = self.tableFeatures.columnCount()

        for pr, paste_row in enumerate(paste_rows):
            target_row = start_row + pr
            if target_row >= row_count:
                break
            for pc, value in enumerate(paste_row):
                target_col = start_col + pc
                if target_col >= col_count:
                    break
                item = self.tableFeatures.item(target_row, target_col)
                if item and (item.flags() & Qt.ItemIsEditable):
                    item.setText(value)

    # ──────────────────────────────────────────────
    # 地物選択連動
    # ──────────────────────────────────────────────

    def _on_selection_changed(self, layer):
        if self._locked:
            return
        if not self.data_manager.original_layer:
            return
        if self._feature_add_mode:
            self._update_add_mode_button()
            return
        if layer and not self._is_same_source(layer):
            return
        if self._plan_active:
            return
        self._process_selection(layer)

    def _is_same_source(self, layer):
        """レイヤーのソースがオリジナルGPKGと同じか判定する。"""
        if not layer or not self.data_manager.original_path:
            return False
        src = layer.source().split('|')[0]
        return os.path.normpath(src) == os.path.normpath(
            self.data_manager.original_path
        )

    def _process_selection(self, layer=None):
        if not self.data_manager.original_layer:
            return

        active_layer = layer or self.iface.activeLayer()
        if not active_layer:
            return
        if not self._is_same_source(active_layer):
            return

        selected = active_layer.selectedFeatures()
        if not selected:
            self._clear_table()
            self.lblStatus.setText('地物が選択されていません')
            return

        # 同じGPKGレイヤーなら選択フィーチャーIDをそのまま使う
        if self._is_same_source(active_layer):
            fids = [f.id() for f in selected]
        else:
            # 異なるレイヤーの場合は空間交差で検索
            combined_geom = QgsGeometry()
            for feat in selected:
                if combined_geom.isNull():
                    combined_geom = QgsGeometry(feat.geometry())
                else:
                    combined_geom = combined_geom.combine(feat.geometry())

            if combined_geom.isNull():
                self._clear_table()
                return

            fids = self.data_manager.get_intersecting_fids(combined_geom)

        if not fids:
            self._clear_table()
            self.lblStatus.setText('交差するフィーチャーがありません')
            return

        self._current_fids = fids
        self._update_table(fids)
        self.lblStatus.setText(f'{len(fids)} 件のフィーチャーが見つかりました')

    # ──────────────────────────────────────────────
    # テーブル表示
    # ──────────────────────────────────────────────

    def _get_display_cols(self):
        return [c for c, v in self.column_config.items() if v == COLUMN_DISPLAY]

    def _get_edit_cols(self):
        return [c for c, v in self.column_config.items() if v == COLUMN_EDITABLE]

    def _get_info_cols(self):
        return [c for c, v in self.column_config.items() if v == COLUMN_INFO]

    def _get_visible_cols(self):
        return self._get_display_cols() + self._get_edit_cols() + self._get_info_cols()

    def _clear_table(self):
        self._editing = True
        self.tableFeatures.setRowCount(0)
        self.tableFeatures.setColumnCount(0)
        self._current_fids = []
        self._current_merged_data = []
        self._editing = False
        self._clear_rubber_bands_base()
        self._update_status_display()

    def _update_table(self, fids):
        self._editing = True

        display_cols = self._get_display_cols()
        edit_cols = self._get_edit_cols()
        info_cols = self._get_info_cols()
        visible_cols = self._get_visible_cols()

        if not visible_cols:
            self._clear_table()
            self.lblStatus.setText(
                '表示カラムが設定されていません。カラム設定を行ってください。'
            )
            self._editing = False
            return

        merged = self.data_manager.get_merged_features(
            fids, display_cols + info_cols, edit_cols
        )

        self.tableFeatures.setColumnCount(len(visible_cols))
        self.tableFeatures.setHorizontalHeaderLabels(visible_cols)
        self.tableFeatures.setRowCount(len(merged))

        for row_idx, row_data in enumerate(merged):
            fid = row_data['fid']
            edited_cols = row_data.get('_edited_cols', set())

            for col_idx, col_name in enumerate(visible_cols):
                value = row_data.get(col_name, '')
                item = QTableWidgetItem(str(value) if value is not None else '')
                item.setData(Qt.UserRole, fid)
                item.setData(Qt.UserRole + 1, col_name)

                if col_name in edit_cols:
                    item.setFlags(item.flags() | Qt.ItemIsEditable)
                    # 色分け: 編集済み=赤、未編集=青
                    if col_name in edited_cols:
                        item.setForeground(COLOR_EDITED)
                    else:
                        item.setForeground(COLOR_EDITABLE)
                else:
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                    # 表示のみ = 黒（デフォルト）

                self.tableFeatures.setItem(row_idx, col_idx, item)

        self.tableFeatures.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents
        )
        self.tableFeatures.horizontalHeader().setMinimumSectionSize(100)
        self._editing = False
        self._current_merged_data = merged
        self._update_feature_count()
        self._update_status_display()
        self._update_show_all_display()

    # ──────────────────────────────────────────────
    # セル編集
    # ──────────────────────────────────────────────

    def _on_cell_changed(self, row, col):
        if self._editing:
            return

        item = self.tableFeatures.item(row, col)
        if not item:
            return

        fid = item.data(Qt.UserRole)
        col_name = item.data(Qt.UserRole + 1)
        new_value = item.text()

        edit_cols = self._get_edit_cols()
        if col_name not in edit_cols:
            return

        try:
            self.data_manager.save_edit(fid, col_name, new_value, edit_cols)
            # 編集済み → 赤字に変更
            item.setForeground(COLOR_EDITED)
            # merged data を更新してステータス表示に反映
            for row_data in self._current_merged_data:
                if row_data['fid'] == fid:
                    row_data[col_name] = new_value
                    break
            self._update_status_display()
        except Exception as e:
            QMessageBox.warning(self, '保存エラー', f'編集の保存に失敗しました: {e}')

    # ──────────────────────────────────────────────
    # 計画管理
    # ──────────────────────────────────────────────

    def _set_plan_ui_enabled(self, enabled):
        self.cmbPlan.setEnabled(enabled)
        self.lineEditPlanName.setEnabled(enabled)
        self.btnPlanSave.setEnabled(enabled)
        self.btnPlanDelete.setEnabled(enabled)
        self.btnPlanAddFeature.setEnabled(False)
        self.btnPlanDeleteFeature.setEnabled(False)
        if not enabled:
            self._deactivate_plan()
            self.cmbPlan.clear()
            self.lineEditPlanName.clear()
            self.lblFeatureCount.setText('フィーチャー数: -')

    def _refresh_plan_combo(self):
        self.cmbPlan.blockSignals(True)
        self.cmbPlan.clear()
        self.cmbPlan.addItem('-- 計画を選択 --')
        for name in self.data_manager.list_plans():
            self.cmbPlan.addItem(name)
        self.cmbPlan.setCurrentIndex(0)
        self.cmbPlan.blockSignals(False)

    def _on_plan_selected(self, index):
        if index <= 0:
            self._deactivate_plan()
            self._clear_table()
            self.lineEditPlanName.clear()
            self.lblStatus.setText('GPKGレイヤーを選択してください')
            return
        plan_name = self.cmbPlan.currentText()
        plan = self.data_manager.load_plan(plan_name)
        if not plan:
            return

        raw_config = plan['column_config']
        raw_config.pop('__col_order__', None)  # 旧バージョンの残存キーを除去
        self.column_config = raw_config
        self._current_fids = plan['fids']
        self.lineEditPlanName.setText(plan_name)

        # ステータス式を復元
        status_exprs = plan.get('status_exprs', {})
        self._status_expr1 = status_exprs.get('expr1', '')
        self._status_expr2 = status_exprs.get('expr2', '')

        self._activate_plan(plan_name)
        self._update_table(self._current_fids)
        self.lblStatus.setText(
            f'計画「{plan_name}」を読み込みました '
            f'({len(self._current_fids)} 件)'
        )

    def _on_plan_save(self):
        name = self.lineEditPlanName.text().strip()
        if not name:
            QMessageBox.warning(self, '保存エラー', '計画名を入力してください。')
            return
        if not self._current_fids:
            QMessageBox.warning(
                self, '保存エラー',
                'テーブルにデータがありません。\n'
                '地物を選択してから保存してください。',
            )
            return

        self.data_manager.save_plan(
            name, self._current_fids, self.column_config,
            self._get_status_exprs(),
        )
        self._refresh_plan_combo()

        # 保存した計画を選択状態にする
        idx = self.cmbPlan.findText(name)
        if idx >= 0:
            self.cmbPlan.blockSignals(True)
            self.cmbPlan.setCurrentIndex(idx)
            self.cmbPlan.blockSignals(False)

        self._activate_plan(name)
        self.lblStatus.setText(f'計画「{name}」を保存しました')

    def _on_plan_delete(self):
        idx = self.cmbPlan.currentIndex()
        if idx <= 0:
            QMessageBox.warning(self, '削除エラー', '削除する計画を選択してください。')
            return
        name = self.cmbPlan.currentText()
        ret = QMessageBox.question(
            self, '確認',
            f'計画「{name}」を削除しますか？',
            QMessageBox.Yes | QMessageBox.No,
        )
        if ret != QMessageBox.Yes:
            return

        self.data_manager.delete_plan(name)
        self._deactivate_plan()
        self.lineEditPlanName.clear()
        self._clear_table()
        self._refresh_plan_combo()
        self.lblStatus.setText(f'計画「{name}」を削除しました')

    def _update_feature_count(self):
        self.lblFeatureCount.setText(f'フィーチャー数: {len(self._current_fids)}')

    def _activate_plan(self, name):
        """計画をアクティブ（ロック）状態にする。"""
        self._plan_active = True
        self._active_plan_name = name
        self.btnPlanAddFeature.setEnabled(True)
        self.btnPlanDeleteFeature.setEnabled(True)

    def _deactivate_plan(self):
        """計画のアクティブ状態を解除する。"""
        self._plan_active = False
        self._active_plan_name = None
        self._feature_add_mode = False
        self.btnPlanAddFeature.setText('フィーチャーの追加')
        self.btnPlanAddFeature.setEnabled(False)
        self.btnPlanDeleteFeature.setEnabled(False)
        self._clear_rubber_bands_base()
        if not self._locked:
            self._clear_rubber_bands()

    def _on_plan_add_feature(self):
        """フィーチャーの追加: 2段階フロー。"""
        if not self._plan_active or not self._active_plan_name:
            return

        if not self._feature_add_mode:
            # Step 1: 選択モードに入る
            self._feature_add_mode = True
            self._update_add_mode_button()
            self.lblStatus.setText(
                'メインウィンドウでフィーチャーを選択してください（複数選択可）'
            )
            return

        active_layer = self._get_add_mode_layer()
        if not active_layer:
            self._feature_add_mode = False
            self.btnPlanAddFeature.setText('フィーチャーの追加')
            self.lblStatus.setText('フィーチャーの追加をキャンセルしました')
            return

        selected = active_layer.selectedFeatures()
        if not selected:
            self._feature_add_mode = False
            self.btnPlanAddFeature.setText('フィーチャーの追加')
            self.lblStatus.setText('フィーチャーの追加をキャンセルしました')
            return

        # 追加対象のfidを取得（_get_add_mode_layer は同一ソースのみ返す）
        new_fids = [f.id() for f in selected]

        if not new_fids:
            QMessageBox.information(self, '追加', '追加対象のフィーチャーがありません。')
            return

        # 既存fidsと重複しないものだけ追加
        existing = set(self._current_fids)
        added = [fid for fid in new_fids if fid not in existing]

        if not added:
            self._feature_add_mode = False
            self.btnPlanAddFeature.setText('フィーチャーの追加')
            self.lblStatus.setText('選択されたフィーチャーはすべて計画に含まれています')
            return

        # 確認ダイアログ
        ret = QMessageBox.question(
            self, '確認',
            f'{len(added)} 件のフィーチャーを追加します。よろしいですか？',
            QMessageBox.Ok | QMessageBox.Cancel,
        )
        if ret != QMessageBox.Ok:
            self._feature_add_mode = False
            self.btnPlanAddFeature.setText('フィーチャーの追加')
            self.lblStatus.setText('フィーチャーの追加をキャンセルしました')
            return

        self._current_fids = self._current_fids + added

        # 計画を自動保存
        self.data_manager.save_plan(
            self._active_plan_name, self._current_fids, self.column_config,
            self._get_status_exprs(),
        )

        self._update_table(self._current_fids)
        self._clear_rubber_bands()

        # モードをリセット
        self._feature_add_mode = False
        self.btnPlanAddFeature.setText('フィーチャーの追加')
        self.lblStatus.setText(
            f'{len(added)} 件のフィーチャーを追加しました '
            f'(計 {len(self._current_fids)} 件)'
        )

    def _get_add_mode_layer(self):
        """追加モードで有効なレイヤーを取得する。"""
        active_layer = self.iface.activeLayer()
        if not active_layer:
            return None
        if not self._is_same_source(active_layer):
            return None
        return active_layer

    def _update_add_mode_button(self):
        """追加モード中のボタン表示を選択状態に合わせて更新する。"""
        active_layer = self._get_add_mode_layer()
        selected = active_layer.selectedFeatures() if active_layer else []
        if selected:
            self.btnPlanAddFeature.setText('選択を確定する')
        else:
            self.btnPlanAddFeature.setText('キャンセル')

    def _on_plan_delete_feature(self):
        """テーブルで選択中のフィーチャーを計画から削除する。"""
        if not self._plan_active or not self._active_plan_name:
            return

        # テーブルで選択されている行からfidを取得
        selected_indexes = self.tableFeatures.selectionModel().selectedIndexes()
        if not selected_indexes:
            QMessageBox.warning(
                self, '削除エラー',
                'テーブルから削除するフィーチャーを選択してください。',
            )
            return

        remove_fids = set()
        for idx in selected_indexes:
            item = self.tableFeatures.item(idx.row(), 0)
            if item:
                fid = item.data(Qt.UserRole)
                if fid is not None:
                    remove_fids.add(fid)

        if not remove_fids:
            return

        # 確認ダイアログ
        ret = QMessageBox.question(
            self, '確認',
            f'選択された {len(remove_fids)} 件のフィーチャーを削除します。よろしいですか？',
            QMessageBox.Ok | QMessageBox.Cancel,
        )
        if ret != QMessageBox.Ok:
            return

        self._current_fids = [
            fid for fid in self._current_fids if fid not in remove_fids
        ]

        # 計画を自動保存
        self.data_manager.save_plan(
            self._active_plan_name, self._current_fids, self.column_config,
            self._get_status_exprs(),
        )

        self._update_table(self._current_fids)
        self._clear_rubber_bands()
        self.lblStatus.setText(
            f'{len(remove_fids)} 件のフィーチャーを削除しました '
            f'(計 {len(self._current_fids)} 件)'
        )

    # ──────────────────────────────────────────────
    # ステータス表示
    # ──────────────────────────────────────────────

    _STATUS_HELP = (
        'QGIS式風の書式（選択行の値を表示）\n\n'
        '"カラム名"  選択行のカラム値\n'
        "'テキスト'  文字列リテラル\n"
        '||  文字列結合    =, !=, >, <  比較\n'
        'if(条件, 真, 偽)  条件分岐\n'
        'round(数値[, 桁])  四捨五入（桁は省略可）\n\n'
        '集計関数（全行対象）:\n'
        '  count() / sum("COL") / unique("COL")\n\n'
        '例: "名称" || \' - \' || "種別" || \'  (\' || count() || \'件)\''
    )

    def _get_status_exprs(self):
        """現在のステータス式を辞書として返す。"""
        return {'expr1': self._status_expr1, 'expr2': self._status_expr2}

    def _auto_save_plan_status(self):
        """計画アクティブ時にステータス式の変更を自動保存する。"""
        if self._plan_active and self._active_plan_name:
            self.data_manager.save_plan(
                self._active_plan_name, self._current_fids, self.column_config,
                self._get_status_exprs(),
            )

    def _on_status_row1_config(self):
        text, ok = self._edit_status_expr('ステータス1行目', self._status_expr1)
        if ok:
            self._status_expr1 = text
            self._update_status_display()
            self._auto_save_plan_status()

    def _on_status_row2_config(self):
        text, ok = self._edit_status_expr('ステータス2行目', self._status_expr2)
        if ok:
            self._status_expr2 = text
            self._update_status_display()
            self._auto_save_plan_status()

    # 関数挿入ボタンの定義: (ボタンラベル, 挿入テキスト)
    _INSERT_SNIPPETS = [
        ('||',        ' || '),
        ('if()',      'if(, , )'),
        ('round()',   'round(, )'),
        ('count()',   'count()'),
        ('sum()',     'sum("")'),
        ('unique()',  'unique("")'),
    ]

    def _edit_status_expr(self, title, current_text):
        dlg = QDialog(self)
        dlg.setWindowTitle(title)
        dlg.setModal(True)

        layout = QVBoxLayout(dlg)

        help_label = QLabel(self._STATUS_HELP, dlg)
        help_label.setWordWrap(True)
        layout.addWidget(help_label)

        edit = QPlainTextEdit(dlg)
        edit.setPlainText(current_text or '')
        edit.setTabChangesFocus(True)
        font = edit.font()
        ps = font.pointSize()
        if ps > 0:
            font.setPointSize(ps + 1)
        edit.setFont(font)
        fm = edit.fontMetrics()
        edit.setFixedHeight(fm.lineSpacing() * 6 + 12)
        layout.addWidget(edit)

        # 関数挿入ボタン
        btn_layout = QHBoxLayout()
        for label, snippet in self._INSERT_SNIPPETS:
            btn = QPushButton(label, dlg)
            btn.setFocusPolicy(Qt.NoFocus)
            btn.clicked.connect(
                lambda _, s=snippet: edit.insertPlainText(s)
            )
            btn_layout.addWidget(btn)
        layout.addLayout(btn_layout)

        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dlg
        )
        buttons.accepted.connect(dlg.accept)
        buttons.rejected.connect(dlg.reject)
        layout.addWidget(buttons)

        screen = QApplication.primaryScreen()
        if screen:
            max_width = int(screen.availableGeometry().width() * 0.9)
        else:
            max_width = 1200
        target_width = 560
        dlg.resize(min(target_width, max_width), dlg.sizeHint().height())

        if dlg.exec_() == QDialog.Accepted:
            return edit.toPlainText(), True
        return current_text, False

    def _update_status_display(self):
        """ステータス表示ラベルを選択行の式評価結果で更新する。"""
        row_data = self._get_selected_row_data()
        data = self._current_merged_data
        self.lblStatusRow1.setText(
            evaluate_row_expr(self._status_expr1, row_data, data)
        )
        self.lblStatusRow2.setText(
            evaluate_row_expr(self._status_expr2, row_data, data)
        )

    def _get_selected_row_data(self):
        """テーブルで選択中の行のデータを返す。"""
        row_idx = self.tableFeatures.currentRow()
        if row_idx < 0 or row_idx >= len(self._current_merged_data):
            return {}
        return self._current_merged_data[row_idx]

    # ──────────────────────────────────────────────
    # エクスポート
    # ──────────────────────────────────────────────

    def _get_export_fids(self):
        """「計画範囲のみ出力」チェック時は現在のfidsを、未チェック時はNoneを返す。"""
        if self.chkPlanOnly.isChecked():
            if not self._current_fids:
                QMessageBox.warning(
                    self, '出力エラー',
                    'テーブルにデータがありません。\n'
                    '地物を選択または計画を読み込んでから出力してください。',
                )
                return False, None
            return True, self._current_fids
        return True, None

    def _on_overwrite_toggled(self, checked):
        """上書き保存チェック時は「計画範囲のみ出力」を無効化する。"""
        self.chkPlanOnly.setEnabled(not checked)

    def _on_export_gpkg(self):
        if self.chkOverwrite.isChecked():
            self._overwrite_gpkg()
            return

        ok, fids = self._get_export_fids()
        if not ok:
            return

        path, _ = QFileDialog.getSaveFileName(
            self, 'GPKG出力先を選択', '',
            'GeoPackage Files (*.gpkg);;All Files (*)',
        )
        if not path:
            return

        try:
            self.data_manager.export_gpkg(path, fids=fids)
            QMessageBox.information(self, '完了', f'GPKGファイルを出力しました:\n{path}')
        except Exception as e:
            QMessageBox.critical(self, 'エラー', f'GPKG出力に失敗しました: {e}')

    def _overwrite_gpkg(self):
        """編集内容をQGIS標準編集APIでGPKGに直接書き込む（FID・未編集属性を保持）。"""
        path = self.data_manager.original_path
        if not path:
            return

        layer_id = self.cmbGpkgLayer.currentData()
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return

        all_edits = self.data_manager.get_all_edits()
        if not all_edits:
            QMessageBox.information(self, '情報', '保存する編集がありません。')
            return

        ret = QMessageBox.warning(
            self, '上書き確認',
            f'元のGPKGファイルに編集を書き込みます:\n{path}\n\nこの操作は取り消せません。よろしいですか？',
            QMessageBox.Yes | QMessageBox.No,
        )
        if ret != QMessageBox.Yes:
            return

        try:
            layer.startEditing()
            fields = layer.fields()

            for fid, col_edits in all_edits.items():
                for col_name, value in col_edits.items():
                    field_idx = fields.indexOf(col_name)
                    if field_idx < 0:
                        continue
                    layer.changeAttributeValue(fid, field_idx, value)

            if not layer.commitChanges():
                errors = layer.commitErrors()
                raise Exception('\n'.join(errors))

            # 編集データをクリア（GPKGに反映済み）
            self.data_manager.clear_edits()

            # data_manager の original_layer を再読み込み（キャッシュリセット）
            self.data_manager.load_gpkg(path)

            # テーブルを更新（編集フラグをリセット）
            if self._current_fids:
                self._update_table(self._current_fids)

            self.lblStatus.setText('上書き保存が完了しました')
            QMessageBox.information(self, '完了', f'GPKGファイルに編集を書き込みました:\n{path}')
        except Exception as e:
            layer.rollBack()
            QMessageBox.critical(self, 'エラー', f'上書き保存に失敗しました: {e}')

    def _on_export_csv(self):
        ok, fids = self._get_export_fids()
        if not ok:
            return

        path, _ = QFileDialog.getSaveFileName(
            self, 'CSV出力先を選択', '',
            'CSV Files (*.csv);;All Files (*)',
        )
        if not path:
            return

        try:
            self.data_manager.export_csv(path, fids=fids)
            QMessageBox.information(self, '完了', f'CSVファイルを出力しました:\n{path}')
        except Exception as e:
            QMessageBox.critical(self, 'エラー', f'CSV出力に失敗しました: {e}')
