# -*- coding: utf-8 -*-
"""
LVT4U — Quick Load Layers Dialog.

Select folder/files → auto-analyze geometry → suggest styles from QML library
→ configure label/category fields → load all → apply → group → save QML.

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

import os

from qgis.PyQt.QtCore import Qt, QSize
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QPushButton, QFileDialog, QMessageBox, QProgressBar,
    QApplication, QCheckBox, QLineEdit, QComboBox,
    QHeaderView, QTextEdit, QTableWidget,
    QTableWidgetItem, QAbstractItemView,
)
from qgis.PyQt.QtGui import QColor
from qgis.core import (
    QgsProject, QgsVectorLayer, QgsWkbTypes,
    QgsCoordinateReferenceSystem,
    QgsStyle, QgsSymbolLayerUtils,
    QgsCategorizedSymbolRenderer, QgsRendererCategory,
    QgsSingleSymbolRenderer, QgsSymbol,
    QgsPalLayerSettings, QgsVectorLayerSimpleLabeling,
    QgsTextFormat, QgsTextBufferSettings,
)
from osgeo import ogr

from .._crs_list import CRS_LIST
from ..shared.qt_compat import QtCompat, MsgButton


# ═══════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════

_VECTOR_EXTENSIONS = {'.shp', '.geojson', '.json', '.gpkg', '.tab', '.kml'}


def _find_vector_files(directory, recurse=True):
    """Recursively find all supported vector files in a directory."""
    results = []
    if recurse:
        for root, dirs, files in os.walk(directory):
            for f in files:
                ext = os.path.splitext(f)[1].lower()
                if ext in _VECTOR_EXTENSIONS:
                    results.append(os.path.join(root, f))
    else:
        for f in os.listdir(directory):
            ext = os.path.splitext(f)[1].lower()
            if ext in _VECTOR_EXTENSIONS:
                results.append(os.path.join(directory, f))
    results.sort()
    return results


def _detect_crs(fpath):
    """Detect CRS of a vector file. Returns EPSG code string or empty."""
    try:
        ds = ogr.Open(fpath, 0)
        if ds:
            lyr = ds.GetLayer(0)
            if lyr:
                srs = lyr.GetSpatialRef()
                if srs:
                    srs.AutoIdentifyEPSG()
                    code = srs.GetAuthorityCode(None)
                    if code:
                        ds = None
                        return f"EPSG:{code}"
                    # Try from proj4
                    proj4 = srs.ExportToProj4()
                    if proj4:
                        ds = None
                        return proj4[:30]
            ds = None
    except Exception:  # nosec B110
        pass  # nosec B110
    return ""


def _smart_match_style(filename, qml_catalog, geom_type):
    """Try to match a filename to the best QML style from the catalog."""
    qml_list = qml_catalog.get(geom_type, [])
    if not qml_list:
        return -1
    name_lower = os.path.splitext(os.path.basename(filename))[0].lower()
    name_lower = name_lower.replace('_', ' ').replace('-', ' ')
    best_score = 0
    best_idx = -1
    for idx, (qml_name, qml_path) in enumerate(qml_list):
        qml_lower = qml_name.lower().replace('_', ' ').replace('-', ' ')
        score = 0
        name_words = set(name_lower.split())
        qml_words = set(qml_lower.split())
        common = name_words & qml_words
        common -= {'shp', 'polygon', 'line', 'point', 'text', 'region',
                   'polyline', 'the', 'and', 'for', 'in', 'of'}
        score = len(common) * 10
        for word in name_words:
            if len(word) >= 3 and word in qml_lower:
                score += 5
        if score > best_score:
            best_score = score
            best_idx = idx
    return best_idx if best_score >= 5 else -1


def _analyze_file(fpath):
    """Analyze a vector file: geometry type, feature count, fields, CRS."""
    info = {
        'geom_type': '', 'geom_label': '',
        'feat_count': 0, 'fields': [], 'crs': '',
    }
    try:
        ds = ogr.Open(fpath, 0)
        if not ds:
            return info
        lyr = ds.GetLayer(0)
        if not lyr:
            ds = None
            return info
        info['feat_count'] = lyr.GetFeatureCount()
        defn = lyr.GetLayerDefn()
        info['fields'] = [
            defn.GetFieldDefn(i).GetName()
            for i in range(defn.GetFieldCount())
        ]
        # CRS
        srs = lyr.GetSpatialRef()
        if srs:
            srs.AutoIdentifyEPSG()
            code = srs.GetAuthorityCode(None)
            if code:
                info['crs'] = f"EPSG:{code}"
            else:
                name = srs.GetName() or srs.ExportToProj4()[:25]
                info['crs'] = name
        # Geometry type
        gtype = ogr.GT_Flatten(lyr.GetGeomType()) if lyr.GetGeomType() else -1
        if gtype in (1, 4):
            info['geom_type'] = 'point'
            info['geom_label'] = '📍 Point'
        elif gtype in (2, 5):
            info['geom_type'] = 'line'
            info['geom_label'] = '📏 Line'
        elif gtype in (3, 6):
            info['geom_type'] = 'polygon'
            info['geom_label'] = '🔷 Polygon'
        ds = None
    except Exception:  # nosec B110
        pass  # nosec B110
    return info


# ═══════════════════════════════════════════════════════════════
# Quick Load Dialog
# ═══════════════════════════════════════════════════════════════

# Column indices
COL_CHK = 0
COL_FILE = 1
COL_NAME = 2
COL_CRS_SRC = 3
COL_CRS_DST = 4
COL_STYLE = 5
COL_LABEL = 6
COL_CAT = 7
NUM_COLS = 8


class QuickLoadDialog(QDialog):
    """Quick Load Layers — load, style, label, group in one step."""

    def __init__(self, iface, qml_catalog, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.setWindowTitle("⚡ Quick Load Layers")
        self.setWindowFlags(
            self.windowFlags() & ~QtCompat.WindowContextHelpButtonHint)
        self._qml_catalog = qml_catalog
        self._file_entries = []

        # Maximize width
        try:
            screen = QApplication.primaryScreen().availableGeometry()
            w = int(screen.width() * 0.92)
            h = int(screen.height() * 0.80)
            self.resize(w, h)
            self.move(
                screen.x() + (screen.width() - w) // 2,
                screen.y() + (screen.height() - h) // 2)
        except Exception:
            self.resize(1200, 700)

        self._build_ui()

    # ───────────────────────────────────────────────────────────
    # UI
    # ───────────────────────────────────────────────────────────

    def _build_ui(self):
        ly = QVBoxLayout(self)
        ly.setSpacing(6)

        # ── Row 1: Browse + Group + Options ──
        hl_top = QHBoxLayout()
        self.btn_folder = QPushButton("📂 Thư mục...")
        self.btn_folder.clicked.connect(self._browse_folder)
        self.btn_files = QPushButton("📄 Chọn file...")
        self.btn_files.clicked.connect(self._browse_files)
        self.btn_clear = QPushButton("✕ Xóa tất cả")
        self.btn_clear.clicked.connect(self._clear_all)
        hl_top.addWidget(self.btn_folder)
        hl_top.addWidget(self.btn_files)
        hl_top.addWidget(self.btn_clear)

        hl_top.addWidget(QLabel("  Nhóm:"))
        self.txt_group = QLineEdit()
        self.txt_group.setPlaceholderText("Tên nhóm layer")
        self.txt_group.setMaximumWidth(200)
        hl_top.addWidget(self.txt_group)
        self.chk_group_geom = QCheckBox("Nhóm theo geometry")
        self.chk_group_geom.setChecked(True)
        hl_top.addWidget(self.chk_group_geom)

        hl_top.addStretch()
        self.lbl_count = QLabel("0 file(s)")
        self.lbl_count.setStyleSheet("font-weight:bold; color:#1565c0;")
        hl_top.addWidget(self.lbl_count)
        ly.addLayout(hl_top)

        # ── Main config table ──
        self.tbl = QTableWidget(0, NUM_COLS)
        self.tbl.setHorizontalHeaderLabels([
            "☑", "File", "Tên hiển thị",
            "CRS gốc", "CRS đích", "Style",
            "Label field", "Category field",
        ])
        h = self.tbl.horizontalHeader()
        h.setStretchLastSection(False)
        h.setSectionResizeMode(COL_CHK, QHeaderView.ResizeMode.ResizeToContents)
        h.setSectionResizeMode(COL_FILE, QHeaderView.ResizeMode.Interactive)
        h.setSectionResizeMode(COL_NAME, QHeaderView.ResizeMode.Interactive)
        h.setSectionResizeMode(COL_CRS_SRC, QHeaderView.ResizeMode.ResizeToContents)
        h.setSectionResizeMode(COL_CRS_DST, QHeaderView.ResizeMode.Interactive)
        h.setSectionResizeMode(COL_STYLE, QHeaderView.ResizeMode.Stretch)
        h.setSectionResizeMode(COL_LABEL, QHeaderView.ResizeMode.Interactive)
        h.setSectionResizeMode(COL_CAT, QHeaderView.ResizeMode.Interactive)
        self.tbl.setColumnWidth(COL_FILE, 180)
        self.tbl.setColumnWidth(COL_NAME, 160)
        self.tbl.setColumnWidth(COL_CRS_DST, 130)
        self.tbl.setColumnWidth(COL_LABEL, 120)
        self.tbl.setColumnWidth(COL_CAT, 120)
        self.tbl.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.tbl.setAlternatingRowColors(True)
        self.tbl.verticalHeader().setVisible(False)
        ly.addWidget(self.tbl, 1)  # stretch factor = 1

        # ── Bottom options ──
        hl_opts = QHBoxLayout()
        self.chk_save_qml = QCheckBox("💾 Lưu QML cùng thư mục file gốc")
        self.chk_save_qml.setChecked(True)
        self.chk_add_project = QCheckBox("Thêm vào project")
        self.chk_add_project.setChecked(True)

        self.btn_check_all = QPushButton("☑ Chọn tất cả")
        self.btn_check_all.clicked.connect(
            lambda: self._set_all_checks(True))
        self.btn_uncheck_all = QPushButton("☐ Bỏ chọn")
        self.btn_uncheck_all.clicked.connect(
            lambda: self._set_all_checks(False))

        hl_opts.addWidget(self.chk_save_qml)
        hl_opts.addWidget(self.chk_add_project)
        hl_opts.addStretch()
        hl_opts.addWidget(self.btn_check_all)
        hl_opts.addWidget(self.btn_uncheck_all)
        ly.addLayout(hl_opts)

        # ── Load button ──
        self.btn_load = QPushButton("⚡ Load + Apply All")
        self.btn_load.setMinimumHeight(42)
        self.btn_load.setStyleSheet(
            "QPushButton { font-size: 14px; font-weight: bold; "
            "background-color: #1565c0; color: white; border-radius: 4px; }"
            "QPushButton:hover { background-color: #1976d2; }"
            "QPushButton:disabled { background-color: #9e9e9e; }"
        )
        self.btn_load.clicked.connect(self._execute_load)
        ly.addWidget(self.btn_load)

        # ── Progress + log ──
        self.progress = QProgressBar()
        self.progress.setVisible(False)
        ly.addWidget(self.progress)

        self.log = QTextEdit()
        self.log.setVisible(False)
        self.log.setMaximumHeight(100)
        self.log.setReadOnly(True)
        self.log.setStyleSheet(
            "QTextEdit { font-family: Consolas, monospace; font-size: 11px; "
            "background-color: #1e1e1e; color: #d4d4d4; }"
        )
        ly.addWidget(self.log)

    # ───────────────────────────────────────────────────────────
    # Browse
    # ───────────────────────────────────────────────────────────

    def _browse_folder(self):
        folder = QFileDialog.getExistingDirectory(
            self, "Chọn thư mục chứa file GIS")
        if not folder:
            return
        files = _find_vector_files(folder, recurse=True)
        if not files:
            QMessageBox.information(
                self, 'LVT',
                'Không tìm thấy file GIS trong thư mục này.')
            return
        if not self.txt_group.text().strip():
            self.txt_group.setText(os.path.basename(folder))
        self._add_files(files)

    def _browse_files(self):
        filter_str = (
            "GIS Files (*.shp *.geojson *.json *.gpkg *.tab *.kml);;"
            "Shapefile (*.shp);;GeoJSON (*.geojson *.json);;"
            "GeoPackage (*.gpkg);;MapInfo TAB (*.tab);;All (*)"
        )
        files, _ = QFileDialog.getOpenFileNames(
            self, "Chọn file GIS", "", filter_str)
        if files:
            self._add_files(files)

    def _add_files(self, file_paths):
        """Add files to the config table, analyzing each."""
        existing = {e['path'] for e in self._file_entries}
        new_files = [f for f in file_paths if f not in existing]
        if not new_files:
            return

        start_row = self.tbl.rowCount()
        self.tbl.setRowCount(start_row + len(new_files))

        for i, fpath in enumerate(new_files):
            row = start_row + i
            fname = os.path.splitext(os.path.basename(fpath))[0]

            # Analyze
            info = _analyze_file(fpath)
            entry = {
                'path': fpath,
                'name': fname,
                'geom_type': info['geom_type'],
                'fields': info['fields'],
                'crs': info['crs'],
                'feat_count': info['feat_count'],
            }
            self._file_entries.append(entry)

            # Col 0: Checkbox
            chk = QCheckBox()
            chk.setChecked(True)
            self.tbl.setCellWidget(row, COL_CHK, chk)

            # Col 1: File (read-only, with geometry icon)
            geom_icon = {'point': '📍', 'line': '📏',
                         'polygon': '🔷'}.get(info['geom_type'], '?')
            item = QTableWidgetItem(
                f"{geom_icon} {fname}")
            item.setFlags(item.flags() & ~QtCompat.ItemIsEditable)
            item.setToolTip(
                f"{fpath}\n"
                f"Geometry: {info['geom_label']}\n"
                f"Features: {info['feat_count']}\n"
                f"Fields: {len(info['fields'])}")
            self.tbl.setItem(row, COL_FILE, item)

            # Col 2: Display name (editable)
            edt = QLineEdit(fname.replace('_', ' ').title())
            self.tbl.setCellWidget(row, COL_NAME, edt)

            # Col 3: CRS gốc (read-only)
            crs_item = QTableWidgetItem(info['crs'] or '—')
            crs_item.setFlags(crs_item.flags() & ~QtCompat.ItemIsEditable)
            if not info['crs']:
                crs_item.setForeground(QColor('#c62828'))
            self.tbl.setItem(row, COL_CRS_SRC, crs_item)

            # Col 4: CRS đích (combo)
            cmb_crs = QComboBox()
            cmb_crs.addItem("— Giữ nguyên —", None)
            for label, code in CRS_LIST:
                if code:
                    cmb_crs.addItem(label, code)
                else:
                    cmb_crs.insertSeparator(cmb_crs.count())
            self.tbl.setCellWidget(row, COL_CRS_DST, cmb_crs)

            # Col 5: Style combo — QML first, then QGIS Style Manager
            cmb_style = self._build_style_combo(info['geom_type'], fname)
            self.tbl.setCellWidget(row, COL_STYLE, cmb_style)

            # Col 6: Label field
            cmb_label = QComboBox()
            cmb_label.addItem("(không)", None)
            for fn in info['fields']:
                cmb_label.addItem(fn, fn)
            self._auto_select(cmb_label, [
                'ten', 'name', 'label', 'ten_lo', 'ten_xa',
                'ten_huyen', 'NAME', 'Ten', 'TEN'])
            self.tbl.setCellWidget(row, COL_LABEL, cmb_label)

            # Col 7: Category field
            cmb_cat = QComboBox()
            cmb_cat.addItem("(không)", None)
            for fn in info['fields']:
                cmb_cat.addItem(fn, fn)
            self._auto_select(cmb_cat, [
                'ldlr', 'maldlr', 'loai_rung', 'loai', 'type',
                'loai_dat', 'ma_ldlr', 'LDLR', 'MALDLR', 'ma_mau'])
            self.tbl.setCellWidget(row, COL_CAT, cmb_cat)

            self.tbl.setRowHeight(row, 28)

        self.lbl_count.setText(f"{len(self._file_entries)} file(s)")

    def _build_style_combo(self, geom_type, fname):
        """Build style combo: QML presets first, then QGIS Style Manager."""
        cmb = QComboBox()
        cmb.addItem("— Mặc định —", None)

        # Section 1: QML presets (top)
        qml_list = self._qml_catalog.get(geom_type, [])
        if qml_list:
            cmb.insertSeparator(cmb.count())
            sep = cmb.count()
            cmb.addItem("──── QML Preset (LVT) ────")
            cmb.model().item(sep).setEnabled(False)
            for qml_name, qml_path in qml_list:
                cmb.addItem(f"📄 {qml_name}", "qml::" + qml_path)

        # Section 2: QGIS Style Manager
        try:
            from qgis.core import QgsSymbol
            style_mgr = QgsStyle.defaultStyle()
            type_map = {
                'polygon': QgsSymbol.SymbolType.Fill,
                'line': QgsSymbol.SymbolType.Line,
                'point': QgsSymbol.SymbolType.Marker,
            }
            target_type = type_map.get(geom_type)
            if target_type is not None:
                sym_names = sorted(style_mgr.symbolNames())
                matched = [
                    n for n in sym_names
                    if style_mgr.symbol(n)
                    and style_mgr.symbol(n).type() == target_type  # noqa: W503
                ]
                if matched:
                    cmb.insertSeparator(cmb.count())
                    sep2 = cmb.count()
                    cmb.addItem("──── QGIS Styles ────")
                    cmb.model().item(sep2).setEnabled(False)
                    preview = QSize(20, 20)
                    for sname in matched:
                        sym = style_mgr.symbol(sname)
                        icon = QgsSymbolLayerUtils.symbolPreviewIcon(
                            sym, preview)
                        cmb.addItem(icon, sname, "sym::" + sname)
        except Exception:
            pass  # nosec B110

        # Smart match QML
        match_idx = _smart_match_style(fname, self._qml_catalog, geom_type)
        if match_idx >= 0 and qml_list:
            qml_path = qml_list[match_idx][1]
            ci = cmb.findData("qml::" + qml_path)
            if ci >= 0:
                cmb.setCurrentIndex(ci)

        return cmb

    @staticmethod
    def _auto_select(combo, preferred_names):
        """Auto-select first matching field name in combo."""
        for pref in preferred_names:
            idx = combo.findData(pref)
            if idx < 0:
                idx = combo.findText(pref, QtCompat.MatchContains)
            if idx > 0:
                combo.setCurrentIndex(idx)
                return

    def _set_all_checks(self, checked):
        QtCompat.Checked if checked else QtCompat.Unchecked
        for row in range(self.tbl.rowCount()):
            chk = self.tbl.cellWidget(row, COL_CHK)
            if chk:
                chk.setChecked(checked)

    def _clear_all(self):
        self.tbl.setRowCount(0)
        self._file_entries.clear()
        self.lbl_count.setText("0 file(s)")

    # ───────────────────────────────────────────────────────────
    # Execute: Load + Apply All
    # ───────────────────────────────────────────────────────────

    def _execute_load(self):
        if not self._file_entries:
            QMessageBox.warning(self, 'LVT', 'Chọn file trước.')
            return

        self.log.setVisible(True)
        self.log.clear()
        self.progress.setVisible(True)
        self.btn_load.setEnabled(False)
        QApplication.processEvents()

        project = QgsProject.instance()
        root = project.layerTreeRoot()
        do_save_qml = self.chk_save_qml.isChecked()
        do_add = self.chk_add_project.isChecked()
        group_name = self.txt_group.text().strip()
        group_by_geom = self.chk_group_geom.isChecked()

        # Create group structure
        main_group = None
        geom_groups = {}
        if group_name:
            main_group = root.findGroup(group_name)
            if not main_group:
                main_group = root.insertGroup(0, group_name)

        if group_by_geom and main_group:
            for gtype, gname in [('polygon', '🔷 Polygon'),
                                 ('line', '📏 Line'),
                                 ('point', '📍 Point')]:
                existing = main_group.findGroup(gname)
                geom_groups[gtype] = existing or main_group.addGroup(gname)

        total = len(self._file_entries)
        loaded = 0

        for fi, entry in enumerate(self._file_entries):
            self.progress.setValue(int((fi / total) * 100))

            # Check if row is checked
            chk = self.tbl.cellWidget(fi, COL_CHK)
            if chk and not chk.isChecked():
                continue

            fpath = entry['path']
            geom_type = entry['geom_type']

            self.log.append(f"\n📄 {os.path.basename(fpath)}")
            QApplication.processEvents()

            # Read config from table
            edt_name = self.tbl.cellWidget(fi, COL_NAME)
            cmb_crs = self.tbl.cellWidget(fi, COL_CRS_DST)
            cmb_style = self.tbl.cellWidget(fi, COL_STYLE)
            cmb_label = self.tbl.cellWidget(fi, COL_LABEL)
            cmb_cat = self.tbl.cellWidget(fi, COL_CAT)

            display_name = (edt_name.text().strip()
                            if edt_name else entry['name'])
            crs_code = cmb_crs.currentData() if cmb_crs else None
            style_val = cmb_style.currentData() if cmb_style else None
            label_field = cmb_label.currentData() if cmb_label else None
            cat_field = cmb_cat.currentData() if cmb_cat else None

            # ── Step 1: Load layer ──
            vlayer = QgsVectorLayer(fpath, display_name, 'ogr')
            if not vlayer.isValid():
                self.log.append("  ❌ Cannot load")
                continue

            vlayer.setProviderEncoding('UTF-8')

            # ── Step 2: Apply CRS đích ──
            if crs_code:
                crs = QgsCoordinateReferenceSystem(crs_code)
                if crs.isValid():
                    vlayer.setCrs(crs)
                    self.log.append(f"  🌐 CRS → {crs_code}")

            # ── Step 3: Apply style ──
            style_applied = False
            if style_val:
                if isinstance(style_val, str) and style_val.startswith(
                        "qml::"):
                    qml_path = style_val[5:]
                    if os.path.isfile(qml_path):
                        msg, ok = vlayer.loadNamedStyle(qml_path)
                        if ok:
                            self.log.append(
                                f"  🎨 QML: "
                                f"{os.path.basename(qml_path)}")
                            style_applied = True
                        else:
                            self.log.append(f"  ⚠️ QML: {msg}")
                elif isinstance(style_val, str) and style_val.startswith(
                        "sym::"):
                    sym_name = style_val[5:]
                    style_mgr = QgsStyle.defaultStyle()
                    sym = style_mgr.symbol(sym_name)
                    if sym:
                        renderer = QgsSingleSymbolRenderer(sym.clone())
                        vlayer.setRenderer(renderer)
                        vlayer.triggerRepaint()
                        self.log.append(f"  🎨 Symbol: {sym_name}")
                        style_applied = True

            # ── Step 4: Category ──
            if cat_field and not style_applied:
                self._apply_categorized(vlayer, cat_field)
                self.log.append(f"  📊 Category: {cat_field}")

            # ── Step 5: Labels ──
            if label_field:
                self._apply_labels(vlayer, label_field)
                self.log.append(f"  🏷️ Label: {label_field}")

            # ── Step 6: Add to project ──
            if do_add:
                project.addMapLayer(vlayer, False)
                target = None
                if group_by_geom and geom_type in geom_groups:
                    target = geom_groups[geom_type]
                elif main_group:
                    target = main_group
                else:
                    target = root
                target.insertLayer(0, vlayer)

            # ── Step 7: Save QML ──
            if do_save_qml:
                qml_out = os.path.splitext(fpath)[0] + '.qml'
                msg, ok = vlayer.saveNamedStyle(qml_out)
                if ok:
                    self.log.append(
                        f"  💾 {os.path.basename(qml_out)}")

            loaded += 1
            self.log.append(f"  ✅ {display_name}")

        # Remove empty geom groups
        if group_by_geom and main_group:
            for gtype, grp in geom_groups.items():
                if len(grp.children()) == 0:
                    main_group.removeChildNode(grp)

        self.progress.setValue(100)
        self.btn_load.setEnabled(True)
        self.iface.mapCanvas().refresh()
        self.log.append(f"\n{'=' * 50}")
        self.log.append(f"✅ HOÀN THÀNH! {loaded}/{total} layers")

        QMessageBox.information(
            self, '✅ Done',
            f'Đã tải {loaded} layer vào project!\n'
            f'Group: {group_name or "(không nhóm)"}')

    # ───────────────────────────────────────────────────────────
    # Style helpers
    # ───────────────────────────────────────────────────────────

    def _apply_categorized(self, layer, field_name):
        try:
            field_idx = layer.fields().indexFromName(field_name)
            if field_idx < 0:
                return
            unique_vals = sorted(layer.uniqueValues(field_idx))
            categories = []
            palette = [
                '#2e7d32', '#1565c0', '#c62828', '#f57f17',
                '#6a1b9a', '#00838f', '#4e342e', '#37474f',
                '#ad1457', '#1b5e20', '#283593', '#bf360c',
                '#827717', '#4527a0', '#006064', '#3e2723',
                '#ff6f00', '#0d47a1', '#b71c1c', '#33691e',
            ]
            geom_type = layer.geometryType()
            for i, val in enumerate(unique_vals):
                if val is None or str(val).strip() == '':
                    continue
                color = QColor(palette[i % len(palette)])
                if geom_type == QgsWkbTypes.GeometryType.PolygonGeometry:
                    sym = QgsSymbol.defaultSymbol(
                        QgsWkbTypes.GeometryType.PolygonGeometry)
                elif geom_type == QgsWkbTypes.GeometryType.LineGeometry:
                    sym = QgsSymbol.defaultSymbol(
                        QgsWkbTypes.GeometryType.LineGeometry)
                else:
                    sym = QgsSymbol.defaultSymbol(
                        QgsWkbTypes.GeometryType.PointGeometry)
                sym.setColor(color)
                if geom_type == QgsWkbTypes.GeometryType.PolygonGeometry:
                    sym.symbolLayer(0).setStrokeColor(
                        QColor(0, 0, 0, 120))
                    sym.symbolLayer(0).setStrokeWidth(0.3)
                cat = QgsRendererCategory(str(val), sym, str(val))
                categories.append(cat)
            if categories:
                renderer = QgsCategorizedSymbolRenderer(
                    field_name, categories)
                layer.setRenderer(renderer)
                layer.triggerRepaint()
        except Exception:  # nosec B110
            pass  # nosec B110

    def _apply_labels(self, layer, field_name):
        try:
            settings = QgsPalLayerSettings()
            settings.fieldName = field_name
            settings.enabled = True
            fmt = QgsTextFormat()
            fmt.setSize(9)
            fmt.setColor(QColor(30, 30, 30))
            buf = QgsTextBufferSettings()
            buf.setEnabled(True)
            buf.setSize(1.0)
            buf.setColor(QColor(255, 255, 255, 200))
            fmt.setBuffer(buf)
            settings.setFormat(fmt)
            labeling = QgsVectorLayerSimpleLabeling(settings)
            layer.setLabeling(labeling)
            layer.setLabelsEnabled(True)
            layer.triggerRepaint()
        except Exception:  # nosec B110
            pass  # nosec B110
