# -*- coding: utf-8 -*-
"""
LVT Sync - Main dialog with Login, Projects, and Sync tabs.
All UI text is in Vietnamese.
Author: Lộc Vũ Trung
"""

import json
import traceback
from datetime import datetime
import os
import zipfile
import tempfile

from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
    QTabWidget, QWidget, QLabel, QLineEdit,
    QPushButton, QTableWidget, QTableWidgetItem, QTextEdit,
    QProgressBar, QHeaderView, QMessageBox, QAbstractItemView,
    QGroupBox, QTreeWidget, QTreeWidgetItem
)
from qgis.PyQt.QtCore import QVariant, Qt
from qgis.PyQt.QtGui import QFont

from qgis.core import (
    QgsWkbTypes,
    QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
    QgsPointXY, QgsField, QgsVectorFileWriter,
    QgsCoordinateTransformContext, QgsCoordinateReferenceSystem,
    QgsCoordinateTransform
)

from .gdrive_client import GoogleDriveClient
from ..shared.file_utils import gpkg_wal_checkpoint, is_onedrive_cloud_file


# ---------------------------------------------------------------------------
# Geometry helpers
# ---------------------------------------------------------------------------

def _qgis_geom_type_to_server(wkb_type):
    """Map QGIS WKB geometry type to server string."""
    flat = QgsWkbTypes.flatType(wkb_type)
    mapping = {
        QgsWkbTypes.Point: 'point',
        QgsWkbTypes.MultiPoint: 'point',
        QgsWkbTypes.LineString: 'linestring',
        QgsWkbTypes.MultiLineString: 'linestring',
        QgsWkbTypes.Polygon: 'polygon',
        QgsWkbTypes.MultiPolygon: 'polygon',
    }
    return mapping.get(flat, 'point')


def _server_geom_type_to_qgis_uri(geom_type):
    """Map server geometry type string to QGIS memory layer URI type."""
    mapping = {
        'point': 'Point',
        'linestring': 'LineString',
        'polygon': 'Polygon',
    }
    return mapping.get(geom_type, 'Point')


def _coords_json_to_qgs_geometry(coords_json_str, geom_type):
    """Convert server coordinates_json to QgsGeometry.

    Server format: [[lat, lng], [lat, lng], ...]
    QGIS uses QgsPointXY(lng, lat).
    """
    try:
        coords = json.loads(coords_json_str) if isinstance(coords_json_str, str) else coords_json_str
    except (json.JSONDecodeError, TypeError):
        return None

    if not coords:
        return None

    if geom_type == 'point':
        # Single point: [lat, lng] or [[lat, lng]]
        if isinstance(coords[0], (int, float)):
            lat, lng = coords[0], coords[1]
        else:
            lat, lng = coords[0][0], coords[0][1]
        return QgsGeometry.fromPointXY(QgsPointXY(lng, lat))

    elif geom_type == 'linestring':
        points = [QgsPointXY(c[1], c[0]) for c in coords]
        return QgsGeometry.fromPolylineXY(points)

    elif geom_type == 'polygon':
        points = [QgsPointXY(c[1], c[0]) for c in coords]
        # Close ring if needed
        if points and points[0] != points[-1]:
            points.append(points[0])
        return QgsGeometry.fromPolygonXY([points])

    return None


def _qgs_geometry_to_coords_json(geometry, geom_type):
    """Convert QgsGeometry to server coordinates_json string.

    Returns JSON string of [[lat, lng], ...].
    """
    if geometry is None or geometry.isEmpty():
        return '[]'

    if geom_type == 'point':
        pt = geometry.asPoint()
        return json.dumps([[pt.y(), pt.x()]])

    elif geom_type == 'linestring':
        # Handle both single and multi linestring
        if geometry.isMultipart():
            lines = geometry.asMultiPolyline()
            points = lines[0] if lines else []
        else:
            points = geometry.asPolyline()
        return json.dumps([[p.y(), p.x()] for p in points])

    elif geom_type == 'polygon':
        # Handle both single and multi polygon
        if geometry.isMultipart():
            polygons = geometry.asMultiPolygon()
            ring = polygons[0][0] if polygons and polygons[0] else []
        else:
            rings = geometry.asPolygon()
            ring = rings[0] if rings else []
        return json.dumps([[p.y(), p.x()] for p in ring])

    return '[]'


# ---------------------------------------------------------------------------
# Main Dialog
# ---------------------------------------------------------------------------

class LVTSyncDialog(QDialog):
    """Main dialog for LVT Sync plugin with three tabs."""

    def __init__(self, iface, parent=None):
        super().__init__(parent)
        self.iface = iface
        self.client = GoogleDriveClient()
        self._projects_cache = []

        self._setup_ui()
        self._connect_signals()
        self._update_auth_ui()

    # ------------------------------------------------------------------
    # UI Setup
    # ------------------------------------------------------------------

    def _setup_ui(self):
        """Build the complete dialog UI."""
        self.setWindowTitle('LVT Sync — Đồng bộ LVTField')
        self.resize(600, 500)
        self.setMinimumSize(500, 400)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)

        # Tab widget
        self.tabs = QTabWidget()
        layout.addWidget(self.tabs)

        # Create tabs
        self.tabs.addTab(self._create_login_tab(), '🔑 Đăng nhập')
        self.tabs.addTab(self._create_projects_tab(), '📁 Dự án')
        self.tabs.addTab(self._create_sync_tab(), '🔄 Đồng bộ')
        self.tabs.addTab(self._create_export_tab(), '📤 Xuất file')

        # Bottom status bar
        self.lbl_status_bar = QLabel('')
        self.lbl_status_bar.setStyleSheet('color: #666; font-size: 11px; padding: 2px;')
        layout.addWidget(self.lbl_status_bar)

    # -- Tab 1: Login -------------------------------------------------------

    def _create_login_tab(self):
        """Create the Google Sign-In tab."""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.setSpacing(12)

        # Google Drive info
        grp_info = QGroupBox('Google Drive Sync')
        info_layout = QVBoxLayout(grp_info)
        info_label = QLabel(
            'Dữ liệu được lưu trên Google Drive cá nhân của bạn.\n'
            'Thư mục: Google Drive / LVTField / projects\n'
            'Miễn phí — 15GB/tài khoản Google.'
        )
        info_label.setWordWrap(True)
        info_label.setStyleSheet('color: #555; font-size: 11px;')
        info_layout.addWidget(info_label)
        layout.addWidget(grp_info)

        # Login group
        grp_login = QGroupBox('Tài khoản Google')
        login_layout = QVBoxLayout(grp_login)

        btn_row = QHBoxLayout()
        self.btn_login = QPushButton('🔑 Đăng nhập bằng Google')
        self.btn_login.setStyleSheet(
            'QPushButton { background-color: #4285F4; color: white; padding: 10px 24px; '
            'border-radius: 6px; font-weight: bold; font-size: 13px; } '
            'QPushButton:hover { background-color: #3367D6; }'
        )
        self.btn_logout = QPushButton('Đăng xuất')
        self.btn_logout.setEnabled(False)
        btn_row.addWidget(self.btn_login)
        btn_row.addWidget(self.btn_logout)
        btn_row.addStretch()
        login_layout.addLayout(btn_row)
        layout.addWidget(grp_login)

        # Status group
        grp_status = QGroupBox('Trạng thái')
        status_layout = QFormLayout(grp_status)
        self.lbl_auth_status = QLabel('⚪ Chưa đăng nhập')
        self.lbl_auth_status.setFont(QFont('Segoe UI', 10))
        status_layout.addRow(self.lbl_auth_status)
        self.lbl_user_info = QLabel('')
        status_layout.addRow(self.lbl_user_info)
        layout.addWidget(grp_status)

        layout.addStretch()
        return widget

    def _create_projects_tab(self):
        """Create the projects listing tab."""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.setSpacing(8)

        # Toolbar
        toolbar = QHBoxLayout()
        self.btn_refresh_projects = QPushButton('🔄 Làm mới')
        self.btn_download_project = QPushButton('⬇️ Tải về QGIS')
        self.btn_download_project.setStyleSheet(
            'QPushButton { background-color: #4CAF50; color: white; padding: 6px 16px; '
            'border-radius: 4px; font-weight: bold; } '
            'QPushButton:hover { background-color: #388E3C; }'
        )
        toolbar.addWidget(self.btn_refresh_projects)
        toolbar.addStretch()
        toolbar.addWidget(self.btn_download_project)
        layout.addLayout(toolbar)

        # Projects table
        self.tbl_projects = QTableWidget(0, 4)
        self.tbl_projects.setHorizontalHeaderLabels([
            'Tên dự án', 'Mô tả', 'Số layer', 'ID'
        ])
        header = self.tbl_projects.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.Stretch)
        header.setSectionResizeMode(1, QHeaderView.Stretch)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
        self.tbl_projects.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.tbl_projects.setSelectionMode(QAbstractItemView.SingleSelection)
        self.tbl_projects.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tbl_projects.setAlternatingRowColors(True)
        layout.addWidget(self.tbl_projects)

        # Info label
        self.lbl_projects_info = QLabel('Nhấn "Làm mới" để tải danh sách dự án.')
        self.lbl_projects_info.setStyleSheet('color: #888;')
        layout.addWidget(self.lbl_projects_info)

        return widget

    # -- Tab 3: Sync ---------------------------------------------------------

    def _create_sync_tab(self):
        """Create the synchronization tab."""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.setSpacing(8)

        # Action buttons
        btn_layout = QHBoxLayout()
        self.btn_push = QPushButton('⬆️ Đẩy lên Google Drive')
        self.btn_push.setStyleSheet(
            'QPushButton { background-color: #FF9800; color: white; padding: 8px 20px; '
            'border-radius: 4px; font-weight: bold; font-size: 12px; } '
            'QPushButton:hover { background-color: #F57C00; }'
        )
        self.btn_pull = QPushButton('⬇️ Tải về từ Google Drive')
        self.btn_pull.setStyleSheet(
            'QPushButton { background-color: #2196F3; color: white; padding: 8px 20px; '
            'border-radius: 4px; font-weight: bold; font-size: 12px; } '
            'QPushButton:hover { background-color: #1976D2; }'
        )
        btn_layout.addWidget(self.btn_push)
        btn_layout.addWidget(self.btn_pull)
        layout.addLayout(btn_layout)

        # Progress
        self.progress = QProgressBar()
        self.progress.setRange(0, 100)
        self.progress.setValue(0)
        self.progress.setTextVisible(True)
        layout.addWidget(self.progress)

        # Last sync info
        info_layout = QHBoxLayout()
        self.lbl_last_sync = QLabel('Lần đồng bộ cuối: —')
        self.lbl_sync_stats = QLabel('')
        info_layout.addWidget(self.lbl_last_sync)
        info_layout.addStretch()
        info_layout.addWidget(self.lbl_sync_stats)
        layout.addLayout(info_layout)

        # Log area
        lbl_log = QLabel('Nhật ký:')
        lbl_log.setFont(QFont('Segoe UI', 9, QFont.Bold))
        layout.addWidget(lbl_log)

        self.txt_log = QTextEdit()
        self.txt_log.setReadOnly(True)
        self.txt_log.setFont(QFont('Consolas', 9))
        self.txt_log.setStyleSheet('background-color: #1e1e1e; color: #d4d4d4; padding: 4px;')
        layout.addWidget(self.txt_log)

        return widget

    # -- Tab 4: Export -------------------------------------------------------

    def _create_export_tab(self):
        """Create the file export tab."""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.setSpacing(10)

        # Output directory
        grp_output = QGroupBox('Thư mục xuất')
        out_layout = QHBoxLayout(grp_output)
        self.txt_export_dir = QLineEdit()
        self.txt_export_dir.setPlaceholderText('Chọn thư mục lưu file...')
        # Default to user's Desktop
        default_dir = os.path.join(os.path.expanduser('~'), 'Desktop')
        if os.path.exists(default_dir):
            self.txt_export_dir.setText(default_dir)
        self.btn_browse_export = QPushButton('Chọn...')
        self.btn_browse_export.setFixedWidth(80)
        out_layout.addWidget(self.txt_export_dir)
        out_layout.addWidget(self.btn_browse_export)
        layout.addWidget(grp_output)

        # Layer selection group
        grp_layers = QGroupBox('Chọn lớp xuất')
        layers_layout = QVBoxLayout(grp_layers)

        # Quick action buttons
        btn_row = QHBoxLayout()
        self.btn_select_all = QPushButton('Chọn tất cả')
        self.btn_select_all.setFixedHeight(28)
        self.btn_select_all.setStyleSheet(
            'QPushButton { background-color: #4CAF50; color: white; padding: 4px 12px; '
            'border-radius: 3px; font-size: 11px; } '
            'QPushButton:hover { background-color: #388E3C; }'
        )
        self.btn_deselect_all = QPushButton('Bỏ tất cả')
        self.btn_deselect_all.setFixedHeight(28)
        self.btn_deselect_all.setStyleSheet(
            'QPushButton { background-color: #FF9800; color: white; padding: 4px 12px; '
            'border-radius: 3px; font-size: 11px; } '
            'QPushButton:hover { background-color: #F57C00; }'
        )
        self.btn_visible_only = QPushButton('Chỉ layer hiện')
        self.btn_visible_only.setFixedHeight(28)
        self.btn_visible_only.setStyleSheet(
            'QPushButton { background-color: #2196F3; color: white; padding: 4px 12px; '
            'border-radius: 3px; font-size: 11px; } '
            'QPushButton:hover { background-color: #1976D2; }'
        )
        self.btn_refresh_layers = QPushButton('🔄')
        self.btn_refresh_layers.setFixedSize(28, 28)
        self.btn_refresh_layers.setToolTip('Làm mới danh sách layer')
        btn_row.addWidget(self.btn_select_all)
        btn_row.addWidget(self.btn_deselect_all)
        btn_row.addWidget(self.btn_visible_only)
        btn_row.addStretch()
        btn_row.addWidget(self.btn_refresh_layers)
        layers_layout.addLayout(btn_row)

        # Layer tree widget
        self.tree_layers = QTreeWidget()
        self.tree_layers.setHeaderLabels(['Lớp dữ liệu', 'Loại', 'Số đối tượng'])
        self.tree_layers.setRootIsDecorated(False)
        self.tree_layers.setAlternatingRowColors(True)
        self.tree_layers.setMaximumHeight(180)
        tree_header = self.tree_layers.header()
        tree_header.setSectionResizeMode(0, QHeaderView.Stretch)
        tree_header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        tree_header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
        layers_layout.addWidget(self.tree_layers)

        # Selection summary
        self.lbl_layer_summary = QLabel('')
        self.lbl_layer_summary.setStyleSheet('color: #888; font-size: 11px; padding: 2px;')
        layers_layout.addWidget(self.lbl_layer_summary)

        layout.addWidget(grp_layers)

        # GPKG export group
        grp_gpkg = QGroupBox('Xuất GPKG — Một file chứa dữ liệu + style')
        gpkg_layout = QVBoxLayout(grp_gpkg)
        self.chk_save_styles = None  # Will use checkbox
        from qgis.PyQt.QtWidgets import QCheckBox
        self.chk_save_styles = QCheckBox('Lưu style (nhãn, màu sắc, nét vẽ)')
        self.chk_save_styles.setChecked(True)
        gpkg_layout.addWidget(self.chk_save_styles)
        self.btn_export_gpkg = QPushButton('📦 Xuất GPKG')
        self.btn_export_gpkg.setStyleSheet(
            'QPushButton { background-color: #4CAF50; color: white; padding: 8px 20px; '
            'border-radius: 4px; font-weight: bold; font-size: 12px; } '
            'QPushButton:hover { background-color: #388E3C; }'
        )
        gpkg_layout.addWidget(self.btn_export_gpkg)
        layout.addWidget(grp_gpkg)

        # ZIP export group
        grp_zip = QGroupBox('Xuất dự án ZIP — File .qgs + tất cả .gpkg')
        zip_layout = QVBoxLayout(grp_zip)
        zip_info = QLabel(
            'Đóng gói toàn bộ dự án QGIS thành 1 file ZIP.\n'
            'Bao gồm: project.qgs + các layer.gpkg + styles.\n'
            'Dùng để import vào LVTField trên điện thoại.'
        )
        zip_info.setStyleSheet('color: #666; font-size: 11px;')
        zip_info.setWordWrap(True)
        zip_layout.addWidget(zip_info)
        self.btn_export_zip = QPushButton('📦 Xuất dự án ZIP')
        self.btn_export_zip.setStyleSheet(
            'QPushButton { background-color: #2196F3; color: white; padding: 8px 20px; '
            'border-radius: 4px; font-weight: bold; font-size: 12px; } '
            'QPushButton:hover { background-color: #1976D2; }'
        )
        zip_layout.addWidget(self.btn_export_zip)
        layout.addWidget(grp_zip)

        # Export progress
        self.export_progress = QProgressBar()
        self.export_progress.setRange(0, 100)
        self.export_progress.setValue(0)
        self.export_progress.setTextVisible(True)
        layout.addWidget(self.export_progress)

        # Export log
        lbl_export_log = QLabel('Nhật ký xuất:')
        lbl_export_log.setFont(QFont('Segoe UI', 9, QFont.Bold))
        layout.addWidget(lbl_export_log)

        self.txt_export_log = QTextEdit()
        self.txt_export_log.setReadOnly(True)
        self.txt_export_log.setFont(QFont('Consolas', 9))
        self.txt_export_log.setStyleSheet(
            'background-color: #1e1e1e; color: #d4d4d4; padding: 4px;'
        )
        self.txt_export_log.setMaximumHeight(150)
        layout.addWidget(self.txt_export_log)

        layout.addStretch()
        return widget

    # ------------------------------------------------------------------
    # Signal connections
    # ------------------------------------------------------------------

    def _connect_signals(self):
        """Connect all button signals to handlers."""
        self.btn_login.clicked.connect(self._on_login)
        self.btn_logout.clicked.connect(self._on_logout)
        self.btn_refresh_projects.clicked.connect(self._on_refresh_projects)
        self.btn_download_project.clicked.connect(self._on_download_project)
        self.btn_push.clicked.connect(self._on_push)
        self.btn_pull.clicked.connect(self._on_pull)
        self.btn_browse_export.clicked.connect(self._on_browse_export_dir)
        self.btn_export_gpkg.clicked.connect(self._on_export_gpkg)
        self.btn_export_zip.clicked.connect(self._on_export_zip)
        self.btn_select_all.clicked.connect(self._on_select_all_layers)
        self.btn_deselect_all.clicked.connect(self._on_deselect_all_layers)
        self.btn_visible_only.clicked.connect(self._on_visible_only_layers)
        self.btn_refresh_layers.clicked.connect(self._refresh_layer_list)
        self.tabs.currentChanged.connect(self._on_tab_changed)
        self.tree_layers.itemChanged.connect(lambda _: self._update_layer_summary())

    def _log(self, message, level='info'):
        """Append a timestamped message to the sync log.

        Args:
            message: Log message text.
            level: One of 'info', 'ok', 'warn', 'error'.
        """
        timestamp = datetime.now().strftime('%H:%M:%S')
        colors = {
            'info': '#d4d4d4',
            'ok': '#4EC9B0',
            'warn': '#DCDCAA',
            'error': '#F44747',
        }
        color = colors.get(level, colors['info'])
        self.txt_log.append(
            f'<span style="color:#888">[{timestamp}]</span> '
            f'<span style="color:{color}">{message}</span>'
        )

    # ------------------------------------------------------------------
    # Auth UI
    # ------------------------------------------------------------------

    def _update_auth_ui(self):
        """Update login UI elements based on authentication state."""
        if self.client.is_authenticated():
            self.lbl_auth_status.setText('🟢 Đã đăng nhập')
            self.lbl_auth_status.setStyleSheet('color: #2e7d32; font-weight: bold;')
            self.lbl_user_info.setText(
                f'👤 {self.client.user_name or "Google User"}\n'
                f'📧 {self.client.user_email or ""}'
            )
            self.btn_login.setEnabled(False)
            self.btn_logout.setEnabled(True)
            self.lbl_status_bar.setText(f'Đã đăng nhập: {self.client.user_email}')
        else:
            self.lbl_auth_status.setText('⚪ Chưa đăng nhập')
            self.lbl_auth_status.setStyleSheet('color: #666;')
            self.lbl_user_info.setText('')
            self.btn_login.setEnabled(True)
            self.btn_logout.setEnabled(False)
            self.lbl_status_bar.setText('')

    def _on_login(self):
        """Handle Google Sign-In button click."""
        self.btn_login.setEnabled(False)
        self.btn_login.setText('Đang mở trình duyệt...')
        from qgis.PyQt.QtWidgets import QApplication
        QApplication.processEvents()

        try:
            self.client.login()
            self._update_auth_ui()
            self._log(f'Đăng nhập Google thành công: {self.client.user_email}', 'ok')
            QMessageBox.information(self, 'Thành công',
                                    f'Đã đăng nhập Google!\nXin chào {self.client.user_name or ""}')
        except Exception as exc:
            QMessageBox.critical(self, 'Đăng nhập thất bại', f'Lỗi:\n{exc}')
            self._log(f'Đăng nhập Google thất bại: {exc}', 'error')
        finally:
            self.btn_login.setEnabled(True)
            self.btn_login.setText('🔑 Đăng nhập bằng Google')

    def _on_logout(self):
        """Handle logout button click."""
        self.client.logout()
        self._update_auth_ui()
        self._log('Đã đăng xuất.', 'info')

    # -- Projects -----------------------------------------------------------

    def _on_refresh_projects(self):
        """Fetch and display projects from Google Drive."""
        if not self.client.is_authenticated():
            QMessageBox.warning(self, 'Chưa đăng nhập', 'Vui lòng đăng nhập trước.')
            self.tabs.setCurrentIndex(0)
            return

        self.btn_refresh_projects.setEnabled(False)
        self.lbl_projects_info.setText('Đang tải...')
        from qgis.PyQt.QtWidgets import QApplication
        QApplication.processEvents()

        try:
            projects = self.client.list_projects()
            self._projects_cache = projects
            self._populate_projects_table(projects)
            self.lbl_projects_info.setText(f'Tìm thấy {len(projects)} dự án.')
            self._log(f'Đã tải {len(projects)} dự án từ Google Drive.', 'ok')
        except Exception as exc:
            self.lbl_projects_info.setText(f'Lỗi: {exc}')
            self._log(f'Lỗi tải dự án: {exc}', 'error')
        finally:
            self.btn_refresh_projects.setEnabled(True)

    def _populate_projects_table(self, projects):
        """Fill the projects table with data."""
        self.tbl_projects.setRowCount(0)
        for proj in projects:
            row = self.tbl_projects.rowCount()
            self.tbl_projects.insertRow(row)
            self.tbl_projects.setItem(row, 0, QTableWidgetItem(proj.get('name', '')))
            self.tbl_projects.setItem(row, 1, QTableWidgetItem(proj.get('modified', '')))
            self.tbl_projects.setItem(row, 2, QTableWidgetItem('—'))
            self.tbl_projects.setItem(row, 3, QTableWidgetItem(proj.get('id', '')))

    def _on_download_project(self):
        """Download selected project from Google Drive."""
        if not self.client.is_authenticated():
            QMessageBox.warning(self, 'Chưa đăng nhập', 'Vui lòng đăng nhập trước.')
            return
        selected = self.tbl_projects.selectedItems()
        if not selected:
            QMessageBox.warning(self, 'Chưa chọn', 'Vui lòng chọn một dự án.')
            return
        row = selected[0].row()
        project_name = self.tbl_projects.item(row, 0).text()
        project_id = self.tbl_projects.item(row, 3).text()
        reply = QMessageBox.question(
            self, 'Xác nhận tải về',
            f'Tải dự án "{project_name}" về QGIS?',
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No
        )
        if reply != QMessageBox.Yes:
            return
        self._download_project(project_id, project_name)

    def _download_project(self, project_id, project_name):
        """Download GPKG files from Google Drive and add to QGIS."""
        from qgis.PyQt.QtWidgets import QApplication
        import tempfile

        self._log(f'Bắt đầu tải: {project_name}', 'info')
        self.progress.setValue(0)

        try:
            files = self.client.list_files_in_project(project_id)
            dl_files = [f for f in files if f['name'].endswith('.gpkg.zip') or f['name'].endswith('.gpkg')]
            self._log(f'Tìm thấy {len(dl_files)} files.', 'info')

            if not dl_files:
                self._log('Dự án không có dữ liệu nào.', 'warn')
                return

            tmp_dir = tempfile.mkdtemp(prefix='lvt_pull_')

            for idx, f in enumerate(dl_files):
                self._log(f'  → Tải {f["name"]}...', 'info')
                QApplication.processEvents()

                data = self.client._api_download(f['id'])
                local_path = os.path.join(tmp_dir, f['name'])
                with open(local_path, 'wb') as fp:
                    fp.write(data)

                # If ZIP, extract GPKG
                import zipfile as zf
                if f['name'].endswith('.gpkg.zip'):
                    with zf.ZipFile(local_path, 'r') as z:
                        z.extractall(tmp_dir)
                    gpkg_name = f['name'].replace('.zip', '')
                    local_path = os.path.join(tmp_dir, gpkg_name)

                # Add to QGIS
                layer_name = f['name'].replace('.gpkg.zip', '').replace('.gpkg', '')
                display_name = f'{project_name} — {layer_name}'
                layer = QgsVectorLayer(local_path, display_name, 'ogr')
                if layer.isValid():
                    QgsProject.instance().addMapLayer(layer)
                    self._log(f'    ✅ Đã thêm {layer_name}', 'ok')
                else:
                    self._log(f'    ❌ Lỗi mở {layer_name}', 'error')

                self.progress.setValue(int(((idx + 1) / len(dl_files)) * 100))
                QApplication.processEvents()

            self._log(f'Hoàn tất! Đã tải {len(dl_files)} layers.', 'ok')
            self.lbl_last_sync.setText(f'Lần đồng bộ cuối: {datetime.now().strftime("%H:%M:%S %d/%m/%Y")}')

        except Exception as exc:
            self._log(f'Lỗi tải dự án: {exc}', 'error')
            self._log(traceback.format_exc(), 'error')

    def _create_qgis_layer(self, name, geom_type, features, group_name=''):
        """Create a QgsVectorLayer from server feature data.

        Args:
            name: Layer name.
            geom_type: Server geometry type string.
            features: List of feature dicts from server.
            group_name: Optional group / project name prefix.

        Returns:
            QgsVectorLayer or None on failure.
        """
        qgis_geom = _server_geom_type_to_qgis_uri(geom_type)
        display_name = f'{group_name} — {name}' if group_name else name
        uri = f'{qgis_geom}?crs=EPSG:4326&index=yes'

        layer = QgsVectorLayer(uri, display_name, 'memory')
        if not layer.isValid():
            return None

        provider = layer.dataProvider()

        # Collect all attribute keys from features
        all_attr_keys = set()
        for feat_data in features:
            attrs_str = feat_data.get('attributes', '{}')
            try:
                attrs = json.loads(attrs_str) if isinstance(attrs_str, str) else (attrs_str or {})
            except (json.JSONDecodeError, TypeError):
                attrs = {}
            all_attr_keys.update(attrs.keys())

        # Always add remote_id field for tracking
        fields = [QgsField('remote_id', QVariant.String)]
        fields.append(QgsField('remote_layer_id', QVariant.String))
        fields.append(QgsField('remote_version', QVariant.Int))

        for key in sorted(all_attr_keys):
            fields.append(QgsField(key, QVariant.String))

        provider.addAttributes(fields)
        layer.updateFields()

        # Add features
        qgs_features = []
        for feat_data in features:
            coords_json = feat_data.get('coordinates_json', '[]')
            geom = _coords_json_to_qgs_geometry(coords_json, geom_type)

            attrs_str = feat_data.get('attributes', '{}')
            try:
                attrs = json.loads(attrs_str) if isinstance(attrs_str, str) else (attrs_str or {})
            except (json.JSONDecodeError, TypeError):
                attrs = {}

            qfeat = QgsFeature(layer.fields())
            if geom:
                qfeat.setGeometry(geom)

            # Set tracking attributes
            qfeat.setAttribute('remote_id', feat_data.get('id', ''))
            qfeat.setAttribute('remote_layer_id', feat_data.get('layer_id', ''))
            qfeat.setAttribute('remote_version', feat_data.get('version', 1))

            # Set user attributes
            for key in sorted(all_attr_keys):
                val = attrs.get(key, '')
                qfeat.setAttribute(key, str(val) if val is not None else '')

            qgs_features.append(qfeat)

        if qgs_features:
            provider.addFeatures(qgs_features)
            layer.updateExtents()

        return layer

    # -- Sync: Push ----------------------------------------------------------

    def _on_push(self):
        """Push current QGIS project to Google Drive as JSON."""
        if not self.client.is_authenticated():
            QMessageBox.warning(self, 'Chưa đăng nhập', 'Vui lòng đăng nhập Google Drive trước.')
            self.tabs.setCurrentIndex(0)
            return

        project = QgsProject.instance()
        if not project.fileName():
            QMessageBox.warning(self, 'Chưa có dự án', 'Vui lòng mở một dự án QGIS trước.')
            return

        # Collect vector layers (allow empty ones too)
        vector_layers = [
            lyr for lyr in project.mapLayers().values()
            if isinstance(lyr, QgsVectorLayer)
        ]
        if not vector_layers:
            QMessageBox.information(self, 'Không có dữ liệu', 'Không có vector layer nào.')
            return

        project_name = os.path.splitext(os.path.basename(project.fileName()))[0]
        layer_info = '\n'.join(f'  • {lyr.name()} ({lyr.featureCount()} features)' for lyr in vector_layers)
        reply = QMessageBox.question(
            self, 'Xác nhận',
            f'Đẩy dự án "{project_name}" lên Google Drive?\n\n{layer_info}',
            QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes
        )
        if reply != QMessageBox.Yes:
            return

        self.btn_push.setEnabled(False)
        self.progress.setValue(0)
        from qgis.PyQt.QtWidgets import QApplication

        try:
            import uuid as uuid_mod

            self._log('Chuẩn bị dữ liệu...', 'info')
            QApplication.processEvents()

            json_files = {}

            # project.json
            json_files['project.json'] = json.dumps({
                'version': 1, 'app_version': '2.6.8',
                'exported_at': datetime.now().isoformat(),
                'project': {
                    'id': str(uuid_mod.uuid4()), 'name': project_name,
                    'description': '', 'crs': project.crs().authid() or 'EPSG:4326',
                    'created_at': datetime.now().isoformat(),
                    'updated_at': datetime.now().isoformat(),
                },
            }, ensure_ascii=False).encode('utf-8')

            # layers + features
            layers_list, features_list = [], []
            for layer in vector_layers:
                layer_id = str(uuid_mod.uuid4())
                geom_type = _qgis_geom_type_to_server(layer.wkbType())
                style_config = {}
                renderer = layer.renderer()
                if renderer and renderer.symbol():
                    sym = renderer.symbol()
                    style_config = {
                        'color': sym.color().name() if sym.color() else '#3388ff',
                        'opacity': sym.opacity() if hasattr(sym, 'opacity') else 1.0,
                    }
                tree_layer = project.layerTreeRoot().findLayer(layer.id())
                is_vis = 1 if (tree_layer and tree_layer.isVisible()) else 1
                layers_list.append({
                    'id': layer_id, 'name': layer.name(),
                    'geometry_type': geom_type, 'style_config': style_config,
                    'z_order': 0, 'is_visible': is_vis,
                })
                for feat in layer.getFeatures():
                    coords = _qgs_geometry_to_coords_json(feat.geometry(), geom_type)
                    attrs = {}
                    for field in feat.fields():
                        val = feat.attribute(field.name())
                        if val is not None:
                            attrs[field.name()] = str(val)
                    features_list.append({
                        'id': str(uuid_mod.uuid4()), 'layer_id': layer_id,
                        'geometry': coords,
                        'properties': json.dumps(attrs, ensure_ascii=False),
                        'created_at': datetime.now().isoformat(),
                        'updated_at': datetime.now().isoformat(), 'version': 1,
                    })

            json_files['layers.json'] = json.dumps({
                'version': 1, 'exported_at': datetime.now().isoformat(), 'layers': layers_list
            }, ensure_ascii=False).encode('utf-8')
            json_files['features.json'] = json.dumps({
                'version': 1, 'exported_at': datetime.now().isoformat(), 'features': features_list
            }, ensure_ascii=False).encode('utf-8')

            try:
                layouts_data = self._extract_layouts()
                if layouts_data:
                    json_files['layouts.json'] = json.dumps({
                        'version': 1, 'exported_at': datetime.now().isoformat(), 'layouts': layouts_data
                    }, ensure_ascii=False).encode('utf-8')
            except Exception:
                pass

            self._log(f'Dữ liệu: {len(layers_list)} layers, {len(features_list)} features', 'ok')
            self.progress.setValue(15)
            QApplication.processEvents()

            # Find/create folders
            self._log('① Tìm thư mục LVTField...', 'info')
            QApplication.processEvents()
            root_id = self.client._find_or_create_folder('LVTField')
            self._log('  ✅ OK', 'ok')
            self.progress.setValue(25)
            QApplication.processEvents()

            self._log('② Tìm thư mục projects...', 'info')
            QApplication.processEvents()
            projects_id = self.client._find_or_create_folder('projects', root_id)
            self._log('  ✅ OK', 'ok')
            self.progress.setValue(30)
            QApplication.processEvents()

            self._log(f'③ Tìm thư mục {project_name}...', 'info')
            QApplication.processEvents()
            folder_id = self.client._find_or_create_folder(project_name, projects_id)
            self._log('  ✅ OK', 'ok')
            self.progress.setValue(35)
            QApplication.processEvents()

            # Upload each JSON file
            file_names = list(json_files.keys())
            for i, fname in enumerate(file_names):
                size_kb = len(json_files[fname]) / 1024
                self._log(f'④ Upload {fname} ({size_kb:.0f} KB)...', 'info')
                QApplication.processEvents()
                self.client.upload_file(folder_id, fname, json_files[fname], 'application/json')
                pct = 40 + int((i + 1) / len(file_names) * 55)
                self.progress.setValue(pct)
                self._log(f'  ✅ {fname}', 'ok')
                QApplication.processEvents()

            # Done
            self.progress.setValue(100)
            self._log(f'✅ Hoàn tất! Đã đẩy "{project_name}" lên Google Drive!', 'ok')
            QMessageBox.information(
                self, 'Thành công',
                f'Đã đẩy dự án "{project_name}" lên Google Drive!')

        except Exception as exc:
            self._log(f'Lỗi: {exc}', 'error')
            self._log(traceback.format_exc(), 'error')
            QMessageBox.critical(self, 'Lỗi', f'Upload thất bại:\n{exc}')
        finally:
            self.btn_push.setEnabled(True)

    def _push_layers(self, vector_layers):
        """Push vector layers to Google Drive as JSON with styles."""
        from qgis.PyQt.QtWidgets import QApplication
        from qgis.core import (
            QgsSimpleFillSymbolLayer, QgsSimpleLineSymbolLayer,
            QgsSimpleMarkerSymbolLayer,
            QgsRenderContext
        )

        def qcolor_to_int(c):
            return (c.alpha() << 24) | (c.red() << 16) | (c.green() << 8) | c.blue()

        def extract_style(layer):
            """Extract QGIS style → Flutter style_json dict."""
            style = {}
            renderer = layer.renderer()
            if renderer:
                symbol = None
                if renderer.type() == 'singleSymbol':
                    symbol = renderer.symbol()
                elif renderer.type() in ('categorizedSymbol', 'graduatedSymbol'):
                    try:
                        syms = renderer.symbols(QgsRenderContext())
                        if syms:
                            symbol = syms[0]
                    except Exception:
                        pass
                elif renderer.type() == 'RuleRenderer':
                    try:
                        for rule in renderer.rootRule().children():
                            if rule.symbol():
                                symbol = rule.symbol()
                                break
                    except Exception:
                        pass

                if symbol and symbol.symbolLayerCount() > 0:
                    sl = symbol.symbolLayer(0)
                    if isinstance(sl, QgsSimpleFillSymbolLayer):
                        fc = sl.fillColor()
                        style["fillColor"] = qcolor_to_int(fc) | 0xFF000000
                        style["fillOpacity"] = round(fc.alpha() / 255.0, 2)
                        style["strokeColor"] = qcolor_to_int(sl.strokeColor())
                        style["strokeWidth"] = round(sl.strokeWidth() * 3, 1)
                    elif isinstance(sl, QgsSimpleLineSymbolLayer):
                        style["color"] = qcolor_to_int(sl.color())
                        style["strokeColor"] = qcolor_to_int(sl.color())
                        w = round(sl.width() * 3, 1)
                        style["strokeWidth"] = w
                        style["width"] = w
                    elif isinstance(sl, QgsSimpleMarkerSymbolLayer):
                        style["fillColor"] = qcolor_to_int(sl.fillColor()) | 0xFF000000
                        style["fillOpacity"] = round(sl.fillColor().alpha() / 255.0, 2)
                        style["color"] = qcolor_to_int(sl.fillColor())
                        style["strokeColor"] = qcolor_to_int(sl.strokeColor())
                        style["strokeWidth"] = round(sl.strokeWidth() * 3, 1) if hasattr(sl, "strokeWidth") else 1.5
                        style["size"] = round(sl.size() * 3, 1)
                    else:
                        # Fallback: try generic color
                        try:
                            style["color"] = qcolor_to_int(symbol.color())
                            style["strokeColor"] = qcolor_to_int(symbol.color())
                        except Exception:
                            pass

            # Labels — FULL extraction of all QGIS label properties
            try:
                if layer.labelsEnabled() and layer.labeling():
                    settings = layer.labeling().settings()
                    tf = settings.format()
                    font = tf.font()
                    buf = tf.buffer()

                    # ══════════════════════════════════════════
                    # 1. LABEL SOURCE (field or expression)
                    # ══════════════════════════════════════════
                    if settings.isExpression:
                        style["labelExpression"] = settings.fieldName
                    else:
                        style["labelField"] = settings.fieldName

                    # ══════════════════════════════════════════
                    # 2. FONT — complete extraction
                    # ══════════════════════════════════════════
                    style["labelFontSize"] = tf.size()
                    style["labelColor"] = qcolor_to_int(tf.color())
                    style["labelFontBold"] = font.bold()
                    style["labelFontItalic"] = font.italic()
                    style["labelFontFamily"] = font.family()

                    # Font extras
                    try:
                        style["labelFontUnderline"] = font.underline()
                        style["labelFontStrikeout"] = font.strikeOut()
                    except Exception:
                        pass

                    # Letter & word spacing
                    try:
                        ls = font.letterSpacing()
                        style["labelLetterSpacing"] = round(ls, 2)
                    except Exception:
                        pass
                    try:
                        ws = font.wordSpacing()
                        style["labelWordSpacing"] = round(ws, 2)
                    except Exception:
                        pass

                    # Capitalization (0=Mixed, 1=AllUpper, 2=AllLower, 3=Title, 4=SmallCaps)
                    try:
                        cap = int(font.capitalization())
                        if cap != 0:
                            style["labelCapitalization"] = cap
                    except Exception:
                        pass

                    # Text opacity
                    try:
                        top = tf.opacity()
                        if top < 1.0:
                            style["labelOpacity"] = round(top, 2)
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 3. LINE SPACING — always store
                    # ══════════════════════════════════════════
                    try:
                        lh = tf.lineHeight()
                        style["labelLineHeight"] = round(lh, 2) if lh > 0 else 1.0
                    except Exception:
                        style["labelLineHeight"] = 1.0

                    # ══════════════════════════════════════════
                    # 4. BUFFER / HALO — complete
                    # ══════════════════════════════════════════
                    style["labelBufferDraw"] = buf.enabled()
                    if buf.enabled():
                        style["labelBufferSize"] = round(buf.size() * 3, 1)
                        style["labelBufferColor"] = qcolor_to_int(buf.color())
                        try:
                            style["labelBufferOpacity"] = round(buf.opacity(), 2)
                        except Exception:
                            pass
                        try:
                            style["labelBufferJoinStyle"] = int(buf.joinStyle())
                        except Exception:
                            pass

                    # ══════════════════════════════════════════
                    # 5. BACKGROUND SHAPE (shield/rectangle)
                    # ══════════════════════════════════════════
                    try:
                        bg = tf.background()
                        if bg.enabled():
                            style["labelBackgroundEnabled"] = True
                            try:
                                style["labelBackgroundColor"] = qcolor_to_int(bg.fillColor())
                            except Exception:
                                pass
                            try:
                                style["labelBackgroundStrokeColor"] = qcolor_to_int(bg.strokeColor())
                            except Exception:
                                pass
                            try:
                                style["labelBackgroundOpacity"] = round(bg.opacity(), 2)
                            except Exception:
                                pass
                            try:
                                style["labelBackgroundType"] = int(bg.type())
                            except Exception:
                                pass
                            try:
                                sz = bg.size()
                                style["labelBackgroundSizeX"] = round(sz.width(), 1)
                                style["labelBackgroundSizeY"] = round(sz.height(), 1)
                            except Exception:
                                pass
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 6. SHADOW
                    # ══════════════════════════════════════════
                    try:
                        shd = tf.shadow()
                        if shd.enabled():
                            style["labelShadowEnabled"] = True
                            style["labelShadowColor"] = qcolor_to_int(shd.color())
                            try:
                                style["labelShadowOpacity"] = round(shd.opacity(), 2)
                            except Exception:
                                pass
                            try:
                                style["labelShadowOffsetDist"] = round(shd.offsetDistance() * 3, 1)
                            except Exception:
                                pass
                            try:
                                style["labelShadowOffsetAngle"] = shd.offsetAngle()
                            except Exception:
                                pass
                            try:
                                style["labelShadowBlurRadius"] = round(shd.blurRadius() * 3, 1)
                            except Exception:
                                pass
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 7. PLACEMENT & POSITION
                    # ══════════════════════════════════════════
                    try:
                        p = int(settings.placement)
                        style["labelPlacement"] = min(p, 4)
                    except Exception:
                        style["labelPlacement"] = 1

                    # Quadrant offset (for point features: 0-8)
                    try:
                        qo = int(settings.quadOffset)
                        style["labelQuadOffset"] = qo
                    except Exception:
                        pass

                    # Offset X/Y (mm → pixels)
                    try:
                        ox = settings.xOffset
                        oy = settings.yOffset
                        if abs(ox) > 0.01 or abs(oy) > 0.01:
                            style["labelOffsetX"] = round(ox * 3, 1)
                            style["labelOffsetY"] = round(oy * 3, 1)
                    except Exception:
                        pass

                    # Label rotation (fixed angle)
                    try:
                        rot = settings.angleOffset
                        if abs(rot) > 0.01:
                            style["labelRotation"] = round(rot, 1)
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 8. MULTILINE & WRAPPING
                    # ══════════════════════════════════════════
                    try:
                        ma = int(settings.multilineAlignment)
                        style["labelMultilineAlign"] = min(ma, 3)
                    except Exception:
                        pass

                    # Wrap character
                    try:
                        wc = settings.wrapChar
                        if wc:
                            style["labelWrapChar"] = wc
                    except Exception:
                        pass

                    # Auto wrap length
                    try:
                        awl = settings.autoWrapLength
                        if awl > 0:
                            style["labelAutoWrapLength"] = awl
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 9. SCALE VISIBILITY
                    # ══════════════════════════════════════════
                    import math
                    try:
                        if hasattr(settings, 'scaleVisibility'):
                            sv = settings.scaleVisibility
                            style["labelScaleVisibility"] = bool(sv)
                            if sv:
                                style["labelScaleMin"] = settings.minimumScale
                                style["labelScaleMax"] = settings.maximumScale
                    except Exception:
                        pass

                    # Convert QGIS scale → zoom level
                    if style.get("labelScaleVisibility"):
                        s_min = style.get("labelScaleMin", 0)
                        s_max = style.get("labelScaleMax", 0)
                        if s_min > 0:
                            z_out = max(1, round(
                                18 - math.log2(s_min / 1000)
                            ))
                            style["labelMinZoom"] = float(z_out)
                        if s_max > 0:
                            z_in = min(22, round(
                                18 - math.log2(s_max / 1000)
                            ))
                            style["labelMaxZoom"] = float(z_in)
                    else:
                        fc_count = layer.featureCount()
                        if fc_count > 5000:
                            style["labelMinZoom"] = 15.0
                        elif fc_count > 1000:
                            style["labelMinZoom"] = 14.0
                        elif fc_count > 200:
                            style["labelMinZoom"] = 13.0
                        else:
                            style["labelMinZoom"] = 12.0

                    # ══════════════════════════════════════════
                    # 10. PRIORITY & RENDERING ORDER
                    # ══════════════════════════════════════════
                    try:
                        pri = settings.priority
                        if pri != 5:
                            style["labelPriority"] = pri
                    except Exception:
                        pass

                    try:
                        zi = settings.zIndex
                        if abs(zi) > 0.01:
                            style["labelZIndex"] = round(zi, 1)
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 11. NUMBER FORMATTING
                    # ══════════════════════════════════════════
                    try:
                        if settings.formatNumbers:
                            style["labelFormatNumbers"] = True
                            style["labelDecimals"] = settings.decimals
                    except Exception:
                        pass

                    # ══════════════════════════════════════════
                    # 12. PIXEL SIZE LIMITS
                    # ══════════════════════════════════════════
                    try:
                        if settings.fontLimitPixelSize:
                            style["labelMinPixelSize"] = settings.fontMinPixelSize
                            style["labelMaxPixelSize"] = settings.fontMaxPixelSize
                    except Exception:
                        pass

            except Exception as le:
                self._log(f'    \u26a0\ufe0f Label: {le}', 'warn')

            return style

        project_name = QgsProject.instance().baseName() or "QGIS_Project"
        self._log(f'Bắt đầu đẩy lên: {project_name}', 'info')
        self.progress.setValue(0)
        QApplication.processEvents()

        try:
            self._log(f'Tạo dự án: {project_name}', 'info')
            QApplication.processEvents()
            remote_project = self.client.create_project(project_name)
            project_folder_id = remote_project['id']
            self._log('  ✅ Dự án OK', 'ok')
            self.progress.setValue(10)
            QApplication.processEvents()

            import json as _json
            import uuid
            total = len(vector_layers)

            # Get layer order from layer tree
            root = QgsProject.instance().layerTreeRoot()
            tree_order = root.layerOrder()
            tree_ids = [lyr.id() for lyr in tree_order]

            # 1. Upload project.json
            project_id = str(uuid.uuid4())
            project_meta = {
                'version': 1, 'app_version': '2.6.4',
                'exported_at': datetime.now().isoformat(),
                'project': {
                    'id': project_id, 'name': project_name,
                    'description': 'Exported from QGIS',
                    'crs': 'EPSG:4326',
                    'created_at': datetime.now().isoformat(),
                    'updated_at': datetime.now().isoformat(),
                },
            }
            self.client.upload_file(project_folder_id, 'project.json', _json.dumps(project_meta, ensure_ascii=False).encode('utf-8'), 'application/json')
            self._log('  ✅ project.json', 'ok')
            self.progress.setValue(20)
            QApplication.processEvents()

            # 2. Extract layers with styles
            layers_list = []
            all_features = []
            feat_count = 0

            for idx, layer in enumerate(vector_layers):
                layer_id = str(uuid.uuid4())
                geom_map = {0: 'Point', 1: 'LineString', 2: 'Polygon', 3: 'Point', 4: 'LineString', 5: 'Polygon'}
                geom_type = geom_map.get(layer.geometryType(), 'Polygon')

                # Extract style from QGIS renderer + labels
                style = extract_style(layer)
                self._log(f'  🎨 {layer.name()} — {geom_type}, style: {len(style)} props', 'info')
                QApplication.processEvents()

                # Z-order from layer tree
                z_order = 0
                is_visible = 1
                try:
                    tree_idx = tree_ids.index(layer.id())
                    z_order = len(tree_ids) - 1 - tree_idx
                    node = root.findLayer(layer.id())
                    is_visible = 1 if node and node.isVisible() else 0
                except Exception:
                    pass

                layers_list.append({
                    'id': layer_id,
                    'name': layer.name(),
                    'geometry_type': geom_type,
                    'style_config': style if style else None,
                    'z_order': z_order,
                    'is_visible': is_visible,
                })

                # Extract features
                layer_total = layer.featureCount()
                self._log(f'    📊 {layer_total} features...', 'info')
                # CRS transform: layer CRS → WGS84 (EPSG:4326)
                wgs84 = QgsCoordinateReferenceSystem('EPSG:4326')
                layer_crs = layer.crs()
                need_transform = layer_crs.isValid() and layer_crs != wgs84
                if need_transform:
                    xform = QgsCoordinateTransform(layer_crs, wgs84, QgsProject.instance())
                    self._log(f'    🌐 CRS: {layer_crs.authid()} → WGS84', 'info')
                for feat in layer.getFeatures():
                    geom = feat.geometry()
                    if geom and not geom.isEmpty() and need_transform:
                        geom_copy = QgsGeometry(geom)
                        geom_copy.transform(xform)
                        geom_str = geom_copy.asJson()
                    elif geom and not geom.isEmpty():
                        geom_str = geom.asJson()
                    else:
                        geom_str = '[]'
                    props = {}
                    for field in layer.fields():
                        val = feat[field.name()]
                        if val is not None and str(val) != 'NULL':
                            props[field.name()] = str(val)
                    all_features.append({
                        'id': str(uuid.uuid4()),
                        'layer_id': layer_id,
                        'geometry': geom_str,
                        'properties': _json.dumps(props, ensure_ascii=False),
                        'collected_at': datetime.now().isoformat(),
                        'created_at': datetime.now().isoformat(),
                        'updated_at': datetime.now().isoformat(),
                        'version': 1,
                    })
                    feat_count += 1
                    if feat_count % 200 == 0:
                        self._log(f'      ⏳ {feat_count}...', 'info')
                        QApplication.processEvents()

                pct = 20 + int(((idx + 1) / total) * 50)
                self.progress.setValue(pct)
                QApplication.processEvents()

            # 3. Upload layers.json
            self._log(f'  📦 Tổng: {feat_count} features, {len(layers_list)} layers', 'info')
            QApplication.processEvents()
            layers_bytes = _json.dumps({'version': 1, 'exported_at': datetime.now().isoformat(), 'layers': layers_list}, ensure_ascii=False).encode('utf-8')
            self.client.upload_file(project_folder_id, 'layers.json', layers_bytes, 'application/json')
            self._log(f'  ✅ layers.json ({len(layers_list)} layers)', 'ok')
            self.progress.setValue(80)
            QApplication.processEvents()

            # 4. Upload features.json
            features_bytes = _json.dumps({'version': 1, 'exported_at': datetime.now().isoformat(), 'features': all_features}, ensure_ascii=False).encode('utf-8')
            feat_kb = len(features_bytes) / 1024
            self._log(f'  ⬆️ features.json ({feat_kb:.0f} KB)', 'info')
            QApplication.processEvents()
            self.client.upload_file(project_folder_id, 'features.json', features_bytes, 'application/json')
            self._log(f'  ✅ features.json ({feat_count} features)', 'ok')
            self.progress.setValue(100)

            self._log(f'🎉 Hoàn tất! {total} layers, {feat_count} features + styles', 'ok')
            self.lbl_last_sync.setText(f'Lần đồng bộ cuối: {datetime.now().strftime("%H:%M:%S %d/%m/%Y")}')
            QMessageBox.information(self, 'Thành công', f'Đã đẩy {total} layers ({feat_count} features) với style lên Google Drive!')

        except Exception as exc:
            self._log(f'Lỗi đẩy dữ liệu: {exc}', 'error')
            self._log(traceback.format_exc(), 'error')
            QMessageBox.critical(self, 'Lỗi', f'Đẩy lên thất bại:\n{exc}')

    def _on_pull(self):
        """Pull project from Google Drive."""
        if not self.client.is_authenticated():
            QMessageBox.warning(self, 'Chưa đăng nhập', 'Vui lòng đăng nhập trước.')
            self.tabs.setCurrentIndex(0)
            return

        try:
            projects = self.client.list_projects()
            if not projects:
                QMessageBox.information(self, 'Trống', 'Không có dự án nào trên Google Drive.')
                return

            names = [p['name'] for p in projects]
            from qgis.PyQt.QtWidgets import QInputDialog
            choice, ok = QInputDialog.getItem(self, 'Chọn dự án', 'Dự án trên Google Drive:', names, 0, False)
            if not ok:
                return

            selected = next(p for p in projects if p['name'] == choice)
            self._download_project(selected['id'], selected['name'])

        except Exception as exc:
            self._log(f'Lỗi: {exc}', 'error')
            QMessageBox.critical(self, 'Lỗi', f'Lỗi tải về:\n{exc}')

    def _export_log(self, message, level='info'):
        """Append message to export log."""
        timestamp = datetime.now().strftime('%H:%M:%S')
        colors = {
            'info': '#d4d4d4', 'ok': '#4EC9B0',
            'warn': '#DCDCAA', 'error': '#F44747',
        }
        color = colors.get(level, colors['info'])
        self.txt_export_log.append(
            f'<span style="color:#888">[{timestamp}]</span> '
            f'<span style="color:{color}">{message}</span>'
        )

    def _on_browse_export_dir(self):
        """Open folder browser for export directory."""
        from qgis.PyQt.QtWidgets import QFileDialog
        folder = QFileDialog.getExistingDirectory(
            self, 'Chọn thư mục xuất',
            self.txt_export_dir.text() or os.path.expanduser('~')
        )
        if folder:
            self.txt_export_dir.setText(folder)

    def _validate_project(self) -> list:
        """Pre-flight validation before export. Returns list of (level, message) tuples."""
        issues = []
        project = QgsProject.instance()

        if not project.fileName():
            issues.append(('error', 'Dự án chưa được lưu. Hãy lưu dự án trước khi xuất.'))
            return issues

        layers = project.mapLayers().values()
        if not layers:
            issues.append(('error', 'Dự án không có lớp dữ liệu nào.'))
            return issues

        for layer in layers:
            if not isinstance(layer, QgsVectorLayer):
                continue
            if not layer.isValid():
                issues.append(('warning', f'Lớp "{layer.name()}" không hợp lệ — sẽ bị bỏ qua.'))
            elif layer.featureCount() == 0:
                issues.append(('info', f'Lớp "{layer.name()}" không có đối tượng.'))
            # Check for unsaved edits
            if layer.isEditable() and layer.isModified():
                issues.append(('warning', f'Lớp "{layer.name()}" có thay đổi chưa lưu!'))
            # Check OneDrive dehydration
            source = layer.source().split('|')[0]
            if os.path.exists(source) and is_onedrive_cloud_file(source):
                issues.append(('warning', f'Lớp "{layer.name()}" là file OneDrive chưa tải về.'))

        return issues

    def _get_vector_layers(self):
        """Get all vector layers with features from current QGIS project."""
        return [
            lyr for lyr in QgsProject.instance().mapLayers().values()
            if isinstance(lyr, QgsVectorLayer) and lyr.featureCount() > 0
        ]

    def _refresh_layer_list(self):
        """Refresh the layer selection tree widget with current project layers."""
        self.tree_layers.blockSignals(True)
        self.tree_layers.clear()
        geom_names = {
            QgsWkbTypes.PointGeometry: 'Point',
            QgsWkbTypes.LineGeometry: 'Line',
            QgsWkbTypes.PolygonGeometry: 'Polygon',
        }
        project = QgsProject.instance()
        _ = project.layerTreeRoot()

        for layer in project.mapLayers().values():
            if not isinstance(layer, QgsVectorLayer):
                continue
            geom_type = geom_names.get(layer.geometryType(), '?')
            feat_count = layer.featureCount()
            item = QTreeWidgetItem()
            item.setText(0, layer.name())
            item.setText(1, geom_type)
            item.setText(2, str(feat_count))
            item.setCheckState(0, Qt.Checked if feat_count > 0 else Qt.Unchecked)
            item.setData(0, Qt.UserRole, layer.id())
            # Gray out empty layers
            if feat_count == 0:
                from qgis.PyQt.QtGui import QBrush, QColor
                gray_brush = QBrush(QColor(Qt.gray))
                for col in range(3):
                    item.setForeground(col, gray_brush)
            self.tree_layers.addTopLevelItem(item)

        self.tree_layers.blockSignals(False)
        self._update_layer_summary()

    def _update_layer_summary(self):
        """Update the selection summary label."""
        total = self.tree_layers.topLevelItemCount()
        checked = sum(
            1 for i in range(total)
            if self.tree_layers.topLevelItem(i).checkState(0) == Qt.Checked
        )
        total_features = 0
        for i in range(total):
            item = self.tree_layers.topLevelItem(i)
            if item.checkState(0) == Qt.Checked:
                try:
                    total_features += int(item.text(2))
                except ValueError:
                    pass
        self.lbl_layer_summary.setText(
            f'Đã chọn {checked}/{total} lớp — {total_features} đối tượng'
        )

    def _on_select_all_layers(self):
        """Check all layers."""
        self.tree_layers.blockSignals(True)
        for i in range(self.tree_layers.topLevelItemCount()):
            self.tree_layers.topLevelItem(i).setCheckState(0, Qt.Checked)
        self.tree_layers.blockSignals(False)
        self._update_layer_summary()

    def _on_deselect_all_layers(self):
        """Uncheck all layers."""
        self.tree_layers.blockSignals(True)
        for i in range(self.tree_layers.topLevelItemCount()):
            self.tree_layers.topLevelItem(i).setCheckState(0, Qt.Unchecked)
        self.tree_layers.blockSignals(False)
        self._update_layer_summary()

    def _on_visible_only_layers(self):
        """Check only currently visible layers."""
        self.tree_layers.blockSignals(True)
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        for i in range(self.tree_layers.topLevelItemCount()):
            item = self.tree_layers.topLevelItem(i)
            layer_id = item.data(0, Qt.UserRole)
            node = root.findLayer(layer_id)
            is_visible = node is not None and node.isVisible()
            item.setCheckState(0, Qt.Checked if is_visible else Qt.Unchecked)
        self.tree_layers.blockSignals(False)
        self._update_layer_summary()

    def _on_tab_changed(self, index):
        """Refresh layer list when export tab is selected."""
        # Export tab is index 3
        if index == 3:
            self._refresh_layer_list()

    def _get_selected_layers(self):
        """Get only the checked layers from the tree widget.

        Returns:
            List of QgsVectorLayer that are checked in the export tree.
        """
        project = QgsProject.instance()
        selected = []
        for i in range(self.tree_layers.topLevelItemCount()):
            item = self.tree_layers.topLevelItem(i)
            if item.checkState(0) == Qt.Checked:
                layer_id = item.data(0, Qt.UserRole)
                layer = project.mapLayer(layer_id)
                if layer and isinstance(layer, QgsVectorLayer):
                    selected.append(layer)
        return selected

    def _on_export_gpkg(self):
        """Export all vector layers to a single GPKG file."""
        from qgis.PyQt.QtWidgets import QApplication

        export_dir = self.txt_export_dir.text().strip()
        if not export_dir or not os.path.isdir(export_dir):
            QMessageBox.warning(self, 'Lỗi', 'Vui lòng chọn thư mục xuất hợp lệ.')
            return

        layers = self._get_selected_layers()
        if not layers:
            QMessageBox.warning(self, 'Chưa chọn lớp',
                                'Vui lòng chọn ít nhất một lớp để xuất.\n\n'
                                'Tích chọn các lớp trong danh sách phía trên.')
            return

        project_name = QgsProject.instance().baseName() or 'LVTField_Export'
        # Sanitize filename
        safe_name = ''.join(c if c.isalnum() or c in ('_', '-') else '_' for c in project_name)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M')
        output_path = os.path.join(export_dir, f'{safe_name}_{timestamp}.gpkg')

        save_styles = self.chk_save_styles.isChecked()

        # Pre-flight validation
        issues = self._validate_project()
        errors = [msg for lvl, msg in issues if lvl == 'error']
        warnings = [msg for lvl, msg in issues if lvl == 'warning']
        if errors:
            QMessageBox.critical(self, 'Không thể xuất',
                                 'Lỗi:\n• ' + '\n• '.join(errors))
            return
        if warnings:
            reply = QMessageBox.warning(
                self, 'Cảnh báo',
                'Phát hiện vấn đề:\n• ' + '\n• '.join(warnings) + '\n\nBạn có muốn tiếp tục?',
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
            )
            if reply != QMessageBox.Yes:
                return

        self._export_log(f'Bắt đầu xuất GPKG: {len(layers)} layers', 'info')
        self.export_progress.setValue(0)
        self.export_progress.setFormat('Đang xuất GPKG...')
        QApplication.processEvents()

        try:
            total_features = 0
            for idx, layer in enumerate(layers):
                layer_name = layer.name()
                self._export_log(f'  → Xuất layer: {layer_name} ({layer.featureCount()} features)', 'info')
                QApplication.processEvents()

                options = QgsVectorFileWriter.SaveVectorOptions()
                options.driverName = 'GPKG'
                options.layerName = layer_name
                if idx > 0:
                    options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer

                error_code, error_msg, new_file, new_layer = QgsVectorFileWriter.writeAsVectorFormatV3(
                    layer, output_path, QgsCoordinateTransformContext(), options
                )

                if error_code != QgsVectorFileWriter.NoError:
                    self._export_log(f'    ❌ Lỗi: {error_msg}', 'error')
                    continue

                total_features += layer.featureCount()
                # WAL checkpoint before opening for style save
                gpkg_wal_checkpoint(output_path)
                self._export_log('    ✅ OK', 'ok')

                # Save style to GPKG if requested
                if save_styles:
                    try:
                        # Reload layer from GPKG to save style
                        temp_layer = QgsVectorLayer(
                            f'{output_path}|layername={layer_name}', layer_name, 'ogr'
                        )
                        if temp_layer.isValid():
                            # Copy renderer and labeling from original
                            temp_layer.setRenderer(layer.renderer().clone())
                            if layer.labeling():
                                temp_layer.setLabeling(layer.labeling().clone())
                                temp_layer.setLabelsEnabled(layer.labelsEnabled())
                            # Save style to GPKG database
                            temp_layer.saveStyleToDatabase(
                                layer_name, 'LVTSync export', True, ''
                            )
                            self._export_log('    ✅ Style saved', 'ok')
                            # PROPER CLEANUP: disconnect data provider before deleting
                            temp_layer.setDataSource('', '', '')
                        del temp_layer
                        import gc
                        gc.collect()
                    except Exception as style_err:
                        self._export_log(f'    ⚠ Style lỗi: {style_err}', 'warn')

                progress = int(((idx + 1) / len(layers)) * 100)
                self.export_progress.setValue(progress)
                self.export_progress.setFormat(f'Layer {idx + 1}/{len(layers)}: {layer_name} — %p%')
                QApplication.processEvents()

            file_size = os.path.getsize(output_path) / (1024 * 1024)
            self._export_log(
                f'Hoàn tất! {len(layers)} layers, {total_features} features.', 'ok'
            )
            self._export_log(f'File: {output_path} ({file_size:.1f} MB)', 'ok')

            QMessageBox.information(
                self, 'Xuất GPKG thành công',
                f'✅ Đã xuất thành công!\n\n'
                f'• {len(layers)} layers, {total_features} features\n'
                f'• File: {os.path.basename(output_path)}\n'
                f'• Kích thước: {file_size:.1f} MB\n\n'
                f'📂 {output_path}'
            )

        except Exception as exc:
            self._export_log(f'Lỗi xuất GPKG: {exc}', 'error')
            QMessageBox.critical(self, 'Lỗi', f'Xuất GPKG thất bại:\n{exc}')

    def _on_export_zip(self):
        """Export entire QGIS project as ZIP (.qgs + .gpkg files)."""
        from qgis.PyQt.QtWidgets import QApplication

        export_dir = self.txt_export_dir.text().strip()
        if not export_dir or not os.path.isdir(export_dir):
            QMessageBox.warning(self, 'Lỗi', 'Vui lòng chọn thư mục xuất hợp lệ.')
            return

        layers = self._get_selected_layers()
        if not layers:
            QMessageBox.warning(self, 'Chưa chọn lớp',
                                'Vui lòng chọn ít nhất một lớp để xuất.\n\n'
                                'Tích chọn các lớp trong danh sách phía trên.')
            return

        project_name = QgsProject.instance().baseName() or 'LVTField_Project'
        safe_name = ''.join(c if c.isalnum() or c in ('_', '-') else '_' for c in project_name)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M')
        zip_path = os.path.join(export_dir, f'{safe_name}_{timestamp}.zip')

        # Pre-flight validation
        issues = self._validate_project()
        errors = [msg for lvl, msg in issues if lvl == 'error']
        warnings = [msg for lvl, msg in issues if lvl == 'warning']
        if errors:
            QMessageBox.critical(self, 'Không thể xuất',
                                 'Lỗi:\n• ' + '\n• '.join(errors))
            return
        if warnings:
            reply = QMessageBox.warning(
                self, 'Cảnh báo',
                'Phát hiện vấn đề:\n• ' + '\n• '.join(warnings) + '\n\nBạn có muốn tiếp tục?',
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
            )
            if reply != QMessageBox.Yes:
                return

        self._export_log(f'Bắt đầu xuất dự án ZIP: {len(layers)} layers', 'info')
        self.export_progress.setValue(0)
        self.export_progress.setFormat('Đang xuất ZIP...')
        QApplication.processEvents()

        try:
            with tempfile.TemporaryDirectory(prefix='lvtsync_') as tmpdir:
                total_features = 0
                gpkg_files = []

                # Step 1: Export each layer as separate GPKG with styles
                for idx, layer in enumerate(layers):
                    layer_name = layer.name()
                    # Sanitize layer name for filename
                    safe_layer = ''.join(
                        c if c.isalnum() or c in ('_', '-') else '_'
                        for c in layer_name
                    )
                    gpkg_path = os.path.join(tmpdir, f'{safe_layer}.gpkg')

                    self._export_log(f'  → Xuất layer: {layer_name}', 'info')
                    QApplication.processEvents()

                    options = QgsVectorFileWriter.SaveVectorOptions()
                    options.driverName = 'GPKG'
                    options.layerName = layer_name

                    error_code, error_msg, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(
                        layer, gpkg_path, QgsCoordinateTransformContext(), options
                    )

                    if error_code != QgsVectorFileWriter.NoError:
                        self._export_log(f'    ❌ Lỗi: {error_msg}', 'error')
                        continue

                    # WAL checkpoint before style save
                    gpkg_wal_checkpoint(gpkg_path)

                    # Save style
                    try:
                        temp_layer = QgsVectorLayer(
                            f'{gpkg_path}|layername={layer_name}', layer_name, 'ogr'
                        )
                        if temp_layer.isValid():
                            temp_layer.setRenderer(layer.renderer().clone())
                            if layer.labeling():
                                temp_layer.setLabeling(layer.labeling().clone())
                                temp_layer.setLabelsEnabled(layer.labelsEnabled())
                            temp_layer.saveStyleToDatabase(
                                layer_name, 'LVTSync export', True, ''
                            )
                            # PROPER CLEANUP: disconnect data provider before deleting
                            temp_layer.setDataSource('', '', '')
                        del temp_layer
                        import gc
                        gc.collect()
                    except Exception:
                        pass

                    gpkg_files.append((gpkg_path, f'{safe_layer}.gpkg'))
                    total_features += layer.featureCount()

                    progress = int(((idx + 1) / len(layers)) * 50)  # 0-50%
                    self.export_progress.setValue(progress)
                    self.export_progress.setFormat(f'Layer {idx + 1}/{len(layers)}: {layer_name} — %p%')
                    QApplication.processEvents()

                if not gpkg_files:
                    self._export_log('Không xuất được layer nào.', 'error')
                    return

                # Step 2: Save .qgs project file with relative datasources
                self._export_log('Tạo file project.qgs...', 'info')
                QApplication.processEvents()

                qgs_path = os.path.join(tmpdir, 'project.qgs')
                # Create a temporary project with GPKG references
                self._create_portable_qgs(qgs_path, layers, gpkg_files)
                self.export_progress.setValue(70)
                self.export_progress.setFormat('Tạo project.qgs... — %p%')

                # Step 3: Pack everything into ZIP
                self._export_log('Đóng gói ZIP...', 'info')
                QApplication.processEvents()

                # WAL checkpoint all GPKGs before zipping
                for gpkg_path_chk, _ in gpkg_files:
                    gpkg_wal_checkpoint(gpkg_path_chk)

                with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
                    zf.write(qgs_path, 'project.qgs')
                    for gpkg_path, gpkg_name in gpkg_files:
                        zf.write(gpkg_path, gpkg_name)

                self.export_progress.setValue(100)
                self.export_progress.setFormat('Hoàn tất! — %p%')

            file_size = os.path.getsize(zip_path) / (1024 * 1024)
            self._export_log(
                f'Hoàn tất! {len(gpkg_files)} layers, {total_features} features.', 'ok'
            )
            self._export_log(f'File: {zip_path} ({file_size:.1f} MB)', 'ok')

            QMessageBox.information(
                self, 'Xuất ZIP thành công',
                f'✅ Đã xuất dự án thành công!\n\n'
                f'• {len(gpkg_files)} layers, {total_features} features\n'
                f'• File: {os.path.basename(zip_path)}\n'
                f'• Kích thước: {file_size:.1f} MB\n\n'
                f'📱 Copy file này sang điện thoại → LVTField → Import ZIP\n\n'
                f'📂 {zip_path}'
            )

        except Exception as exc:
            self._export_log(f'Lỗi xuất ZIP: {exc}', 'error')
            import traceback as tb
            self._export_log(tb.format_exc(), 'error')
            QMessageBox.critical(self, 'Lỗi', f'Xuất ZIP thất bại:\n{exc}')

    def _create_portable_qgs(self, qgs_path, layers, gpkg_files):
        """Create a .qgs project file with relative paths to GPKG files.

        Args:
            qgs_path: Output .qgs file path.
            layers: Original QGIS layers.
            gpkg_files: List of (abs_path, relative_name) tuples.
        """
        # Save current project state
        original_project = QgsProject.instance()

        # Create a new temporary project
        temp_project = QgsProject()
        temp_project.setTitle(original_project.title() or original_project.baseName())

        for layer, (gpkg_abs, gpkg_rel) in zip(layers, gpkg_files):
            layer_name = layer.name()
            # Create a layer pointing to the GPKG with relative path
            uri = f'./{gpkg_rel}|layername={layer_name}'
            temp_layer = QgsVectorLayer(uri, layer_name, 'ogr')

            if not temp_layer.isValid():
                # Fallback: use absolute path (will be fixed after)
                temp_layer = QgsVectorLayer(
                    f'{gpkg_abs}|layername={layer_name}', layer_name, 'ogr'
                )

            if temp_layer.isValid():
                # Copy renderer
                if layer.renderer():
                    temp_layer.setRenderer(layer.renderer().clone())
                # Copy labeling
                if layer.labeling():
                    temp_layer.setLabeling(layer.labeling().clone())
                    temp_layer.setLabelsEnabled(layer.labelsEnabled())
                temp_project.addMapLayer(temp_layer)

        # Write project
        temp_project.write(qgs_path)

        # Post-process: ensure relative paths in .qgs XML
        try:
            with open(qgs_path, 'r', encoding='utf-8') as f:
                content = f.read()
            # Replace absolute temp paths with relative
            tmpdir = os.path.dirname(qgs_path)
            content = content.replace(tmpdir.replace('\\', '/') + '/', './')
            content = content.replace(tmpdir + '\\', './')
            content = content.replace(tmpdir + '/', './')
            with open(qgs_path, 'w', encoding='utf-8') as f:
                f.write(content)
        except Exception:
            pass  # relative path fixup is best-effort

        temp_project.removeAllMapLayers()
        del temp_project
        import gc
        gc.collect()
