# -*- coding: utf-8 -*-
"""LVT4U — Đánh số lô tự động (Auto Lot Numbering).

Tự động đánh mã lô cho các polygon trong một lớp vector theo thứ tự
không gian (trái → phải, trên → xuống) trong phạm vi một khoảnh.

Logic:
    1. Người dùng chọn layer lô (polygon).
    2. Người dùng chọn field Tiểu khu và field Khoảnh để lọc phạm vi.
    3. Người dùng chọn các giá trị cụ thể của TK và Khoảnh.
    4. Người dùng nhập quy tắc mã: prefix (a, Lô,...) + số bắt đầu + padding.
    5. Hệ thống sắp xếp các polygon theo centroid (W→E, N→S) rồi ghi mã.

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

import re

from qgis.PyQt.QtCore import Qt, QRegExp
from ..shared.qt_compat import MsgButton
from qgis.PyQt.QtGui import QColor, QFont, QRegExpValidator
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
    QComboBox, QGroupBox, QMessageBox, QLineEdit,
    QCheckBox, QSpinBox, QFormLayout, QFrame,
    QScrollArea, QWidget, QListWidget, QListWidgetItem,
    QAbstractItemView, QSizePolicy, QRadioButton, QButtonGroup,
)
from qgis.core import (
    QgsProject, QgsWkbTypes, QgsPointXY, QgsVectorLayer,
)

from ..shared.i18n import tr


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _polygon_layers():
    """Return list of polygon QgsVectorLayer currently loaded."""
    layers = []
    for lyr in QgsProject.instance().mapLayers().values():
        if (isinstance(lyr, QgsVectorLayer) and
                QgsWkbTypes.geometryType(lyr.wkbType())
                == QgsWkbTypes.GeometryType.PolygonGeometry):
            layers.append(lyr)
    return sorted(layers, key=lambda lyr: lyr.name().lower())


def _field_names(layer):
    """Return list of field names of a vector layer."""
    if layer is None:
        return []
    return [f.name() for f in layer.fields()]


def _distinct_values(layer, field_name):
    """Return sorted distinct values for a field (as strings)."""
    if layer is None or not field_name:
        return []
    idx = layer.fields().indexOf(field_name)
    if idx < 0:
        return []
    values = set()
    for feat in layer.getFeatures():
        v = feat[idx]
        if v is not None and str(v).strip():
            values.add(str(v).strip())
    return sorted(values, key=lambda x: (
        [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', x)]
    ))


def _centroid_xy(feature):
    """Return (cx, cy) centroid of a feature geometry."""
    geom = feature.geometry()
    if geom is None or geom.isEmpty():
        return (0.0, 0.0)
    c = geom.centroid().asPoint()
    return (c.x(), c.y())


def _sort_features(features, sort_x_first=True, ascending_x=True,
                   ascending_y=False, row_tolerance=None):
    """Sort features by centroid spatially.

    Strategy:
        1.  Bucket features into "rows" using Y-tolerance (default = 10% of
            Y-extent).  Features whose Y values are within tolerance belong to
            the same row.
        2.  Sort rows top→bottom (descending Y) or bottom→top (ascending Y).
        3.  Within each row sort left→right (ascending X) or right→left.

    Args:
        features: list of QgsFeature
        sort_x_first: primary sort axis = X (True) or Y (False)
        ascending_x:  X direction → True = W→E, False = E→W
        ascending_y:  Y direction → True = S→N, False = N→S (default)
        row_tolerance: float, Y-bucket size (None = auto 10 % of Y range)
    """
    if not features:
        return features

    centroids = {f.id(): _centroid_xy(f) for f in features}

    if row_tolerance is None:
        ys = [cy for (cx, cy) in centroids.values()]
        y_range = max(ys) - min(ys) if len(ys) > 1 else 1.0
        row_tolerance = y_range * 0.10   # 10 % of height

    # Assign row bucket: quantise Y into bands
    def _row_key(cy):
        return round(cy / row_tolerance) if row_tolerance else cy

    # Sort: primary = Y-row (N→S = descending Y), secondary = X
    x_sign = 1 if ascending_x else -1
    y_sign = 1 if ascending_y else -1   # ascending_y=False → N→S = highest Y first

    def _sort_key(f):
        cx, cy = centroids[f.id()]
        row = -cy * y_sign             # invert so highest Y comes first when ascending_y=False
        return (round(row / row_tolerance) if row_tolerance else row,
                cx * x_sign)

    return sorted(features, key=_sort_key)


# ---------------------------------------------------------------------------
# Dialog
# ---------------------------------------------------------------------------

class LotNumberingDialog(QDialog):
    """Dialog: Đánh số lô tự động."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.setWindowTitle(tr("Đánh số lô tự động"))
        self.setMinimumWidth(520)
        self.setMinimumHeight(640)
        self._build_ui()
        self.refresh_layers()

    # ------------------------------------------------------------------
    # UI Construction
    # ------------------------------------------------------------------

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setSpacing(10)
        root.setContentsMargins(14, 14, 14, 14)

        # ── Title ──────────────────────────────────────────────────────
        title = QLabel(tr("🔢 Đánh số lô tự động"))
        font = QFont()
        font.setPointSize(13)
        font.setBold(True)
        title.setFont(font)
        root.addWidget(title)

        sep = QFrame()
        sep.setFrameShape(QFrame.Shape.HLine)
        sep.setFrameShadow(QFrame.Shadow.Sunken)
        root.addWidget(sep)

        # ── 1. Layer ───────────────────────────────────────────────────
        grp_layer = QGroupBox(tr("1. Layer lô (Polygon)"))
        fl1 = QFormLayout(grp_layer)
        self.cmb_layer = QComboBox()
        self.cmb_layer.currentIndexChanged.connect(self._on_layer_changed)
        fl1.addRow(tr("Layer:"), self.cmb_layer)
        root.addWidget(grp_layer)

        # ── 2. Bộ lọc Tiểu khu / Khoảnh ───────────────────────────────
        grp_filter = QGroupBox(tr("2. Lọc phạm vi (Tiểu khu / Khoảnh)"))
        fl2 = QFormLayout(grp_filter)

        self.cmb_tk_field = QComboBox()
        self.cmb_tk_field.currentIndexChanged.connect(self._on_tk_field_changed)
        fl2.addRow(tr("Field Tiểu khu:"), self.cmb_tk_field)

        self.cmb_tk_value = QComboBox()
        self.cmb_tk_value.addItem(tr("(Tất cả)"), None)
        self.cmb_tk_value.currentIndexChanged.connect(self._on_tk_value_changed)
        fl2.addRow(tr("Giá trị TK:"), self.cmb_tk_value)

        self.cmb_kh_field = QComboBox()
        self.cmb_kh_field.currentIndexChanged.connect(self._on_kh_field_changed)
        fl2.addRow(tr("Field Khoảnh:"), self.cmb_kh_field)

        self.cmb_kh_value = QComboBox()
        self.cmb_kh_value.addItem(tr("(Tất cả)"), None)
        fl2.addRow(tr("Giá trị Khoảnh:"), self.cmb_kh_value)

        root.addWidget(grp_filter)

        # ── 3. Field ghi mã ────────────────────────────────────────────
        grp_out = QGroupBox(tr("3. Field ghi mã lô"))
        fl3 = QFormLayout(grp_out)

        self.cmb_out_field = QComboBox()
        fl3.addRow(tr("Field đích:"), self.cmb_out_field)

        root.addWidget(grp_out)

        # ── 4. Quy tắc đánh mã ─────────────────────────────────────────
        grp_rule = QGroupBox(tr("4. Quy tắc đánh mã"))
        fl4 = QFormLayout(grp_rule)

        # Prefix
        self.txt_prefix = QLineEdit()
        self.txt_prefix.setPlaceholderText(tr("Ví dụ: a  hoặc  Lô  (để trống = chỉ số)"))
        self.txt_prefix.textChanged.connect(self._update_preview)
        fl4.addRow(tr("Prefix:"), self.txt_prefix)

        # Số bắt đầu
        self.spn_start = QSpinBox()
        self.spn_start.setRange(0, 99999)
        self.spn_start.setValue(1)
        self.spn_start.valueChanged.connect(self._update_preview)
        fl4.addRow(tr("Số bắt đầu:"), self.spn_start)

        # Độ rộng số (padding)
        self.spn_pad = QSpinBox()
        self.spn_pad.setRange(0, 6)
        self.spn_pad.setValue(0)
        self.spn_pad.setToolTip(tr("0 = không padding.  2 = 01, 02...  3 = 001, 002..."))
        self.spn_pad.valueChanged.connect(self._update_preview)
        fl4.addRow(tr("Padding số (0 = tắt):"), self.spn_pad)

        # Phân cách prefix-số
        self.txt_sep = QLineEdit()
        self.txt_sep.setMaxLength(5)
        self.txt_sep.setPlaceholderText(tr("Ví dụ: khoảng trắng ' ' hoặc '-' hoặc để trống"))
        self.txt_sep.textChanged.connect(self._update_preview)
        fl4.addRow(tr("Phân cách prefix-số:"), self.txt_sep)

        root.addWidget(grp_rule)

        # ── 5. Thứ tự sắp xếp ──────────────────────────────────────────
        grp_sort = QGroupBox(tr("5. Thứ tự sắp xếp"))
        vl5 = QVBoxLayout(grp_sort)

        # X direction
        hl_x = QHBoxLayout()
        hl_x.addWidget(QLabel(tr("Chiều ngang:")))
        self.rb_west_east = QRadioButton(tr("Trái → Phải (Tây → Đông)"))
        self.rb_east_west = QRadioButton(tr("Phải → Trái (Đông → Tây)"))
        self.rb_west_east.setChecked(True)
        bg_x = QButtonGroup(self)
        bg_x.addButton(self.rb_west_east)
        bg_x.addButton(self.rb_east_west)
        hl_x.addWidget(self.rb_west_east)
        hl_x.addWidget(self.rb_east_west)
        vl5.addLayout(hl_x)

        # Y direction
        hl_y = QHBoxLayout()
        hl_y.addWidget(QLabel(tr("Chiều dọc:")))
        self.rb_north_south = QRadioButton(tr("Trên → Dưới (Bắc → Nam)"))
        self.rb_south_north = QRadioButton(tr("Dưới → Trên (Nam → Bắc)"))
        self.rb_north_south.setChecked(True)
        bg_y = QButtonGroup(self)
        bg_y.addButton(self.rb_north_south)
        bg_y.addButton(self.rb_south_north)
        hl_y.addWidget(self.rb_north_south)
        hl_y.addWidget(self.rb_south_north)
        vl5.addLayout(hl_y)

        root.addWidget(grp_sort)

        # ── Preview ─────────────────────────────────────────────────────
        self.lbl_preview = QLabel()
        self.lbl_preview.setStyleSheet(
            "background:#f0f4f8; border:1px solid #ccc; border-radius:4px;"
            "padding:6px; color:#2c3e50; font-family:monospace;"
        )
        self.lbl_preview.setWordWrap(True)
        root.addWidget(self.lbl_preview)

        # ── Buttons ─────────────────────────────────────────────────────
        sep2 = QFrame()
        sep2.setFrameShape(QFrame.Shape.HLine)
        sep2.setFrameShadow(QFrame.Shadow.Sunken)
        root.addWidget(sep2)

        hl_btn = QHBoxLayout()
        self.btn_count = QPushButton(tr("🔍 Đếm lô"))
        self.btn_count.clicked.connect(self._count_features)
        self.btn_run = QPushButton(tr("✅ Đánh mã"))
        self.btn_run.setDefault(True)
        self.btn_run.clicked.connect(self._run)
        btn_close = QPushButton(tr("Đóng"))
        btn_close.clicked.connect(self.close)

        hl_btn.addWidget(self.btn_count)
        hl_btn.addStretch()
        hl_btn.addWidget(btn_close)
        hl_btn.addWidget(self.btn_run)
        root.addLayout(hl_btn)

        self._update_preview()

    # ------------------------------------------------------------------
    # Refresh
    # ------------------------------------------------------------------

    def refresh_layers(self):
        """Re-populate the layer combo from current QGIS project."""
        current_id = None
        if self.cmb_layer.currentIndex() >= 0:
            current_id = self.cmb_layer.currentData()

        self.cmb_layer.blockSignals(True)
        self.cmb_layer.clear()
        restore_idx = 0
        for i, lyr in enumerate(_polygon_layers()):
            self.cmb_layer.addItem(lyr.name(), lyr.id())
            if lyr.id() == current_id:
                restore_idx = i
        self.cmb_layer.blockSignals(False)
        self.cmb_layer.setCurrentIndex(restore_idx)
        self._on_layer_changed()

    # ------------------------------------------------------------------
    # Event Handlers
    # ------------------------------------------------------------------

    def _current_layer(self):
        lid = self.cmb_layer.currentData()
        if not lid:
            return None
        return QgsProject.instance().mapLayer(lid)

    def _on_layer_changed(self):
        lyr = self._current_layer()
        fields = _field_names(lyr)

        for cmb in (self.cmb_tk_field, self.cmb_kh_field, self.cmb_out_field):
            cmb.blockSignals(True)
            cmb.clear()

        self.cmb_tk_field.addItem(tr("(Không lọc)"), "")
        self.cmb_kh_field.addItem(tr("(Không lọc)"), "")

        for f in fields:
            self.cmb_tk_field.addItem(f, f)
            self.cmb_kh_field.addItem(f, f)
            self.cmb_out_field.addItem(f, f)

        # Auto-guess fields
        lower = [f.lower() for f in fields]
        for guess in ("tieu_khu", "tk", "tiểu khu", "tieukhu", "compartment"):
            for i, l in enumerate(lower):
                if guess in l:
                    self.cmb_tk_field.setCurrentIndex(i + 1)
                    break
        for guess in ("khoanh", "khoảnh", "subcompartment", "kh"):
            for i, l in enumerate(lower):
                if guess in l:
                    self.cmb_kh_field.setCurrentIndex(i + 1)
                    break
        for guess in ("ma_lo", "malo", "mã lô", "lot_code", "lo_id", "tt", "stt"):
            for i, l in enumerate(lower):
                if guess in l:
                    self.cmb_out_field.setCurrentIndex(i)
                    break

        for cmb in (self.cmb_tk_field, self.cmb_kh_field, self.cmb_out_field):
            cmb.blockSignals(False)

        self._on_tk_field_changed()

    def _on_tk_field_changed(self):
        field = self.cmb_tk_field.currentData()
        lyr = self._current_layer()
        self.cmb_tk_value.blockSignals(True)
        self.cmb_tk_value.clear()
        self.cmb_tk_value.addItem(tr("(Tất cả)"), None)
        if field:
            for v in _distinct_values(lyr, field):
                self.cmb_tk_value.addItem(v, v)
        self.cmb_tk_value.blockSignals(False)
        self._on_tk_value_changed()

    def _on_tk_value_changed(self):
        self._on_kh_field_changed()

    def _on_kh_field_changed(self):
        field = self.cmb_kh_field.currentData()
        lyr = self._current_layer()
        self.cmb_kh_value.blockSignals(True)
        self.cmb_kh_value.clear()
        self.cmb_kh_value.addItem(tr("(Tất cả)"), None)
        if field:
            for v in _distinct_values(lyr, field):
                self.cmb_kh_value.addItem(v, v)
        self.cmb_kh_value.blockSignals(False)
        self._update_preview()

    # ------------------------------------------------------------------
    # Mã preview
    # ------------------------------------------------------------------

    def _make_code(self, n):
        prefix = self.txt_prefix.text()
        sep = self.txt_sep.text()
        pad = self.spn_pad.value()
        num = str(n).zfill(pad) if pad > 0 else str(n)
        if prefix:
            return f"{prefix}{sep}{num}"
        return num

    def _update_preview(self):
        start = self.spn_start.value()
        codes = [self._make_code(i) for i in range(start, start + 5)]
        self.lbl_preview.setText(
            tr("Preview: ") + "  →  ".join(codes) + "  →  ..."
        )

    # ------------------------------------------------------------------
    # Count
    # ------------------------------------------------------------------

    def _get_filtered_features(self):
        """Return list of features matching current TK/Khoảnh filter."""
        lyr = self._current_layer()
        if lyr is None:
            return []

        tk_field = self.cmb_tk_field.currentData() or ""
        tk_val = self.cmb_tk_value.currentData()
        kh_field = self.cmb_kh_field.currentData() or ""
        kh_val = self.cmb_kh_value.currentData()

        features = []
        for feat in lyr.getFeatures():
            if tk_field and tk_val is not None:
                v = str(feat[tk_field]).strip() if feat[tk_field] is not None else ""
                if v != str(tk_val):
                    continue
            if kh_field and kh_val is not None:
                v = str(feat[kh_field]).strip() if feat[kh_field] is not None else ""
                if v != str(kh_val):
                    continue
            features.append(feat)
        return features

    def _count_features(self):
        feats = self._get_filtered_features()
        lyr = self._current_layer()
        name = lyr.name() if lyr else "?"
        QMessageBox.information(
            self,
            tr("Kết quả đếm"),
            tr(f"Layer '{name}': tìm thấy {len(feats)} lô phù hợp bộ lọc.")
        )

    # ------------------------------------------------------------------
    # Run
    # ------------------------------------------------------------------

    def _run(self):
        lyr = self._current_layer()
        if lyr is None:
            QMessageBox.warning(self, tr("Lỗi"), tr("Vui lòng chọn layer."))
            return

        out_field = self.cmb_out_field.currentData()
        if not out_field:
            QMessageBox.warning(self, tr("Lỗi"), tr("Vui lòng chọn field đích."))
            return

        if lyr.fields().indexOf(out_field) < 0:
            QMessageBox.warning(
                self, tr("Lỗi"),
                tr(f"Field '{out_field}' không tồn tại trong layer.")
            )
            return

        features = self._get_filtered_features()
        if not features:
            QMessageBox.warning(
                self, tr("Không có dữ liệu"),
                tr("Không tìm thấy lô nào với bộ lọc hiện tại.")
            )
            return

        # Sort
        asc_x = self.rb_west_east.isChecked()
        asc_y = self.rb_south_north.isChecked()   # N→S = ascending_y=False
        sorted_feats = _sort_features(
            features,
            ascending_x=asc_x,
            ascending_y=asc_y,
        )

        # Confirm
        tk_field = self.cmb_tk_field.currentData() or ""
        tk_val = self.cmb_tk_value.currentData()
        kh_field = self.cmb_kh_field.currentData() or ""
        kh_val = self.cmb_kh_value.currentData()

        filter_desc = []
        if tk_field and tk_val is not None:
            filter_desc.append(f"{tk_field} = '{tk_val}'")
        if kh_field and kh_val is not None:
            filter_desc.append(f"{kh_field} = '{kh_val}'")
        filter_str = " và ".join(filter_desc) if filter_desc else tr("(Tất cả)")

        start = self.spn_start.value()
        preview_codes = [self._make_code(start + i) for i in range(min(3, len(sorted_feats)))]

        ans = QMessageBox.question(
            self,
            tr("Xác nhận đánh mã"),
            tr(
                f"Sẽ ghi mã vào field '{out_field}' cho {len(sorted_feats)} lô.\n"
                f"Bộ lọc: {filter_str}\n"
                f"Mã ví dụ: {', '.join(preview_codes)}...\n\n"
                f"⚠️ Thao tác này sẽ GHI ĐÈ giá trị cũ!\n"
                f"Tiếp tục?"
            ),
            MsgButton.Yes | MsgButton.No,
            MsgButton.No,
        )
        if ans != MsgButton.Yes:
            return

        # Write
        if not lyr.isEditable():
            lyr.startEditing()

        field_idx = lyr.fields().indexOf(out_field)
        errors = 0
        for i, feat in enumerate(sorted_feats):
            code = self._make_code(start + i)
            ok = lyr.changeAttributeValue(feat.id(), field_idx, code)
            if not ok:
                errors += 1

        lyr.commitChanges()

        if errors:
            QMessageBox.warning(
                self, tr("Hoàn tất (có lỗi)"),
                tr(f"Đánh mã xong. {len(sorted_feats) - errors}/{len(sorted_feats)} lô thành công.")
            )
        else:
            QMessageBox.information(
                self, tr("Hoàn tất"),
                tr(f"✅ Đã đánh mã {len(sorted_feats)} lô thành công vào field '{out_field}'.")
            )

        # Refresh map
        lyr.triggerRepaint()
        self.iface.mapCanvas().refresh()
