# -*- coding: utf-8 -*-
"""LVT4U Package Map — Portable project packager.

Inspired by the Project Packager plugin (Tarot Osuji, GPL-2.0).
Simplified approach: copies all data files via GDAL, remaps layer
sources using QgsProviderRegistry, and writes a portable .qgz project.
"""
import os
import shutil

from osgeo import gdal
from qgis.PyQt.QtCore import Qt, QDir, QUrl
from qgis.PyQt.QtGui import QFont, QIcon
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
    QLineEdit, QFileDialog, QMessageBox, QFormLayout,
    QProgressDialog, QToolButton, QRadioButton,
    QCheckBox, QGroupBox, QGridLayout,
)
from qgis.core import (
    QgsApplication, QgsProject, QgsProviderRegistry,
    QgsDataProvider, QgsVectorDataProvider,
    QgsRenderContext, QgsLayoutItemPicture,
    QgsSymbolLayerUtils,
)


# =====================================================================
# Symbol / image path utilities (handles SVG, raster fills, layout pics)
# =====================================================================

# Stock SVG paths shipped with QGIS — we skip these
_stock_svgs = list(
    map(lambda x: QDir(x).canonicalPath(), QgsSymbolLayerUtils.listSvgFiles())
)


def _symbol_layers_from_symbol(symbol):
    """Recursively collect all symbol layers from a symbol."""
    slyrs = []
    for slyr in symbol.symbolLayers():
        sub = slyr.subSymbol()
        if sub:
            slyrs.extend(_symbol_layers_from_symbol(sub))
        slyrs.append(slyr)
    return slyrs


def _get_path(slyr):
    """Get the external file path from a symbol layer."""
    for getter in ('path', 'svgFilePath', 'imageFilePath'):
        try:
            return getattr(slyr, getter)()
        except AttributeError:
            pass
    return None


def _set_path(slyr, new_path):
    """Set the external file path on a symbol layer."""
    for setter in ('setPath', 'setSvgFilePath', 'setImageFilePath'):
        try:
            getattr(slyr, setter)(new_path)
            return
        except AttributeError:
            pass


def _collect_symbol_paths(project, context):
    """Map: symbol_layer_or_layout_item → absolute file path."""
    result = {}

    # Symbol layers in map layers
    for lyr in project.mapLayers().values():
        try:
            syms = lyr.renderer().symbols(context)
        except AttributeError:
            continue
        for sym in syms:
            for slyr in _symbol_layers_from_symbol(sym):
                raw = _get_path(slyr)
                path = QDir(raw).canonicalPath() if raw else ''
                if path and os.path.exists(path) and path not in _stock_svgs:
                    result[slyr] = path

    # Layout items (pictures + shape symbols)
    for layout in project.layoutManager().printLayouts():
        model = layout.itemsModel()
        for row in range(model.rowCount()):
            item = model.itemFromIndex(model.index(row, 0))
            if isinstance(item, QgsLayoutItemPicture):
                raw = item.picturePath()
                path = QDir(raw).canonicalPath() if raw else ''
                if path and os.path.exists(path) and path not in _stock_svgs:
                    result[item] = path
            else:
                try:
                    sym = item.symbol()
                except AttributeError:
                    continue
                for slyr in _symbol_layers_from_symbol(sym):
                    raw = _get_path(slyr)
                    path = QDir(raw).canonicalPath() if raw else ''
                    if path and os.path.exists(
                            path) and path not in _stock_svgs:
                        result[slyr] = path

    return result


# =====================================================================
# Helpers
# =====================================================================

def _is_in_dir(parent, child):
    """Check if child path is inside parent directory."""
    parent = os.path.abspath(parent)
    child = os.path.abspath(child)
    try:
        return os.path.commonpath([parent, child]) == parent
    except ValueError:
        return False


def _get_source_info(layer):
    """Decode a layer's data provider URI into (path, layerName, options).

    Returns None if the layer is not file-based.
    """
    dp = layer.dataProvider()
    if dp is None:
        return None
    reg = QgsProviderRegistry.instance()
    parts = reg.decodeUri(dp.name(), layer.source())
    path = parts.get('path')
    if path:
        path = QDir(path).canonicalPath()
    if not path:
        return None
    layer_name = parts.get('layerName')
    return path, layer_name, None


# =====================================================================
# Dialog
# =====================================================================

class PackageMapDialog(QDialog):
    """Simple, effective project packager dialog."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.setWindowTitle("LVT4U — Package Map")
        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        self._build_ui()

    def _build_ui(self):
        layout = QVBoxLayout(self)

        # Header
        hdr = QLabel("📦  Package Map")
        hdr.setFont(QFont("Arial", 13, QFont.Bold))
        layout.addWidget(hdr)

        desc = QLabel(
            "Đóng gói project QGIS hiện tại thành thư mục di động.\n"
            "Toàn bộ dữ liệu, style, nhãn, layout được copy — mở trên máy khác hiển thị đúng.")
        desc.setWordWrap(True)
        desc.setStyleSheet("color: #555; margin-bottom: 8px;")
        layout.addWidget(desc)

        # Output folder
        form = QFormLayout()
        row = QHBoxLayout()
        self.dir_edit = QLineEdit()
        self.dir_edit.setReadOnly(True)
        self.dir_edit.setMinimumWidth(400)
        row.addWidget(self.dir_edit)
        btn_browse = QToolButton()
        btn_browse.setIcon(
            QIcon(':/images/themes/default/mActionFileOpen.svg'))
        btn_browse.clicked.connect(self._browse)
        row.addWidget(btn_browse)
        form.addRow("Thư mục đích:", row)
        layout.addLayout(form)

        # Options
        grp = QGroupBox("Tùy chọn")
        grid = QGridLayout(grp)
        self.radio_copy = QRadioButton(
            "Copy dữ liệu gốc (giữ nguyên định dạng)")
        self.radio_copy.setChecked(True)
        grid.addWidget(self.radio_copy, 0, 0, 1, 2)

        self.radio_gpkg = QRadioButton(
            "Đóng gói vào một file GeoPackage (.gpkg)")
        grid.addWidget(self.radio_gpkg, 1, 0, 1, 2)

        self.chk_vacuum = QCheckBox("Vacuum file SQLite sau khi copy")
        grid.addWidget(self.chk_vacuum, 2, 1)

        layout.addWidget(grp)

        # Buttons
        btn_row = QHBoxLayout()
        btn_help = QPushButton("❓ Help")
        btn_help.setMinimumHeight(36)
        btn_help.setStyleSheet(
            "QPushButton{background:#ff8f00;color:#fff;font-weight:bold;"
            "padding:6px 14px;border-radius:4px}"
            "QPushButton:hover{background:#ffa000}"
        )
        btn_help.clicked.connect(self._show_help)
        btn_row.addWidget(btn_help)
        btn_row.addStretch()

        self.btn_run = QPushButton("📦  Đóng gói")
        self.btn_run.setFont(QFont("Arial", 11, QFont.Bold))
        self.btn_run.setMinimumHeight(36)
        self.btn_run.setMinimumWidth(150)
        self.btn_run.setStyleSheet(
            "QPushButton { background: #2e7d32; color: white; border-radius: 6px; padding: 6px 20px; }"
            "QPushButton:hover { background: #388e3c; }")
        self.btn_run.clicked.connect(self._run)
        btn_row.addWidget(self.btn_run)

        btn_close = QPushButton("Đóng")
        btn_close.setMinimumHeight(36)
        btn_close.clicked.connect(self.close)
        btn_row.addWidget(btn_close)
        layout.addLayout(btn_row)

        self.setMaximumHeight(0)  # compact

    def _browse(self):
        path = QFileDialog.getExistingDirectory(
            self, "Chọn thư mục đích", self.dir_edit.text()
        )
        if path:
            self.dir_edit.setText(path)

    def refresh_layers(self):
        """No-op for interface compatibility."""

    # -----------------------------------------------------------------
    # Packaging engine
    # -----------------------------------------------------------------
    def _run(self):
        project = QgsProject.instance()

        # Must have a saved project
        if not project.fileName():
            QMessageBox.warning(
                self, "LVT4U",
                "Project chưa được lưu.\nHãy lưu project (Ctrl+S) trước khi đóng gói.")
            return

        if project.isDirty():
            QMessageBox.warning(
                self, "LVT4U",
                "Project có thay đổi chưa lưu.\nHãy lưu project (Ctrl+S) trước khi đóng gói.")
            return

        if not self.dir_edit.text():
            self._browse()
        if not self.dir_edit.text():
            return

        # Build source map: layer → (path, layerName, options)
        src_map = {}
        for lyr in project.mapLayers().values():
            info = _get_source_info(lyr)
            if info:
                src_map[lyr] = info

        if not src_map:
            QMessageBox.information(
                self, "LVT4U",
                "Không có layer file nào trong project hiện tại."
            )
            return

        # Determine output directory
        outdir = os.path.join(self.dir_edit.text(), project.baseName())
        home = project.homePath()
        if home and _is_in_dir(home, outdir):
            QMessageBox.warning(
                self, "LVT4U",
                "Thư mục đích không được nằm trong thư mục project!"
            )
            return

        # Handle existing folder
        if os.path.exists(outdir):
            res = QMessageBox.question(
                self, "LVT4U",
                f"Thư mục '{outdir}' đã tồn tại.\n"
                f"Dữ liệu cũ sẽ bị xóa. Tiếp tục?",
                QMessageBox.Yes | QMessageBox.No,
            )
            if res != QMessageBox.Yes:
                return
            try:
                shutil.rmtree(outdir)
            except Exception as e:
                QMessageBox.critical(self, "LVT4U", f"Lỗi xóa thư mục: {e}")
                return

        # Collect symbol/image paths
        context = QgsRenderContext.fromMapSettings(
            self.iface.mapCanvas().mapSettings()
        )
        sym_map = _collect_symbol_paths(project, context)

        if self.radio_gpkg.isChecked():
            # GPKG Packaging mode
            self._run_gpkg_package(project, outdir)
            return

        # Build unique path set (data + symbols)
        all_paths = [path for path, _, _ in src_map.values()]
        all_paths.extend(sym_map.values())
        paths_set = list(set(all_paths))

        # Create directory mapping: avoid name collisions
        dirs_set = list(set(os.path.dirname(p) for p in paths_set))
        names = [project.baseName() + '.qgz']  # reserve project name
        for d in dirs_set:
            name = os.path.basename(d)
            if name in names:
                suffix = 1
                while f"{name}_{suffix}" in names:
                    suffix += 1
                name = f"{name}_{suffix}"
            names.append(name)
        del names[0]  # remove reserved project name
        dir_map = dict(zip(dirs_set, names))
        path_map = {p: dir_map[os.path.dirname(p)] for p in paths_set}

        # Progress dialog
        pd = QProgressDialog(self.iface.mainWindow())
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        pd.setWindowTitle(
            "LVT4U — Đóng gói" if _vi else "LVT4U — Packaging")
        pd.setAutoReset(False)
        pd.setMinimumDuration(0)
        pd.setWindowModality(Qt.WindowModal)
        pd.setMinimumWidth(400)
        pd.setMaximum(len(path_map) + 1)
        pd.setValue(0)

        orig_project = project.fileName()

        try:
            # ---- Copy files ----
            for path in path_map:
                if pd.wasCanceled():
                    return

                dstdir = os.path.join(outdir, path_map[path])
                os.makedirs(dstdir, exist_ok=True)

                # Use GDAL to get full file list (handles .shp siblings etc.)
                if os.path.isdir(path):
                    file_list = [path]
                else:
                    try:
                        ds = gdal.OpenEx(path)
                        file_list = ds.GetFileList() if ds else [path]
                    except (AttributeError, RuntimeError):
                        file_list = [path]
                    finally:
                        ds = None

                for fpath in file_list:
                    pd.setLabelText(f"Copying: {os.path.basename(fpath)}")
                    QgsApplication.processEvents()
                    try:
                        if os.path.isdir(fpath):
                            shutil.copytree(
                                fpath,
                                os.path.join(dstdir, os.path.basename(fpath))
                            )
                        else:
                            shutil.copy2(fpath, dstdir)
                    except FileNotFoundError:
                        pass
                    except (PermissionError, IsADirectoryError):
                        try:
                            shutil.copytree(
                                fpath,
                                os.path.join(dstdir, os.path.basename(fpath))
                            )
                        except Exception:
                            pass

                    # Vacuum SQLite if requested
                    if self.chk_vacuum.isChecked():
                        self._try_vacuum(
                            os.path.join(dstdir, os.path.basename(fpath)), pd
                        )

                pd.setValue(pd.value() + 1)

            if pd.wasCanceled():
                return

            # ---- Remap layer data sources ----
            reg = QgsProviderRegistry.instance()
            for lyr, src in src_map.items():
                path, _, _ = src
                dp = lyr.dataProvider()
                encoding = (dp.encoding()
                            if isinstance(dp, QgsVectorDataProvider) else None)
                parts = reg.decodeUri(dp.name(), lyr.source())
                parts['path'] = QDir(
                    os.path.join(
                        outdir, path_map[path], os.path.basename(path))
                ).absolutePath()
                new_source = reg.encodeUri(dp.name(), parts)
                lyr.setDataSource(
                    new_source, lyr.name(), lyr.providerType(),
                    QgsDataProvider.ProviderOptions()
                )
                if encoding is not None:
                    lyr.dataProvider().setEncoding(encoding)

            # ---- Remap symbol/image paths ----
            for slyr, path in sym_map.items():
                new_path = QDir(
                    os.path.join(
                        outdir, path_map[path], os.path.basename(path))
                ).absolutePath()
                if isinstance(slyr, QgsLayoutItemPicture):
                    fmt = (QgsLayoutItemPicture.FormatSVG
                           if new_path.endswith(('.svg', '.svgz'))
                           else QgsLayoutItemPicture.FormatRaster)
                    slyr.setPicturePath(new_path, fmt)
                else:
                    _set_path(slyr, new_path)

            # ---- Write project ----
            pd.setValue(pd.maximum())
            pd.setLabelText("Lưu project...")
            QgsApplication.processEvents()

            project.setPresetHomePath('')
            project.writeEntryBool('Paths', '/Absolute', False)
            qgz_path = QDir(
                os.path.join(outdir, project.baseName())
            ).absolutePath() + '.qgz'
            result = project.write(qgz_path)

        except Exception as e:
            QMessageBox.critical(self, "LVT4U", f"Lỗi đóng gói: {e}")
            return
        finally:
            pd.hide()
            pd.deleteLater()
            # Reload original project to restore live state
            mb = self.iface.messageBar()
            mb.widgetAdded.connect(mb.popWidget)
            project.read(orig_project)
            mb.widgetAdded.disconnect(mb.popWidget)

        if result:
            self.iface.messageBar().pushSuccess(
                "LVT4U",
                f'Đóng gói thành công! → <a href="{QUrl.fromLocalFile(outdir).toString()}">{outdir}</a>'
            )
            self.close()
        else:
            QMessageBox.warning(
                self, "LVT4U", "Lưu project thất bại!"
            )

    def _run_gpkg_package(self, project, outdir):
        """Package project: vectors→GPKG, rasters→copy, WMS/XYZ→keep URL."""
        import processing
        from qgis.core import (
            QgsVectorLayer, QgsRasterLayer,
            QgsMessageLog, Qgis, QgsProviderRegistry,
            QgsDataProvider)

        os.makedirs(outdir, exist_ok=True)
        gpkg_path = os.path.join(outdir, f"{project.baseName()}.gpkg")
        qgz_path = os.path.join(outdir, f"{project.baseName()}.qgz")
        raster_dir = os.path.join(outdir, "raster")
        orig_project = project.fileName()

        # ── Classify layers ──
        all_layers = list(project.mapLayers().values())
        vectors = []
        rasters = []       # file-based raster
        online = []         # WMS, XYZ, WFS, ArcGIS...
        _ONLINE_PROVIDERS = {'wms', 'wfs', 'arcgismapserver',
                             'arcgisfeatureserver', 'vectortile',
                             'xyztiles', 'wcs'}

        for lyr in all_layers:
            if not lyr.isValid():
                continue
            prov = lyr.providerType().lower() if lyr.providerType() else ''
            if prov in _ONLINE_PROVIDERS:
                online.append(lyr)
            elif isinstance(lyr, QgsVectorLayer):
                vectors.append(lyr)
            elif isinstance(lyr, QgsRasterLayer):
                # Check if file-based (has a local path)
                info = _get_source_info(lyr)
                if info and os.path.isfile(info[0]):
                    rasters.append(lyr)
                else:
                    online.append(lyr)  # e.g. virtual raster, cloud
            # else: mesh, point cloud, etc → skip

        QgsMessageLog.logMessage(
            f"[Package] {len(vectors)} vector, {len(rasters)} raster, "
            f"{len(online)} online layers", "LVT4U", Qgis.Info)

        if not vectors and not rasters:
            QMessageBox.warning(
                self, "LVT4U",
                "Không có layer file nào để đóng gói.\n"
                f"({len(online)} online layers sẽ giữ nguyên URL)")
            return

        # ── Progress ──
        total_steps = len(rasters) + 3  # raster copies + gpkg + project + done
        pd = QProgressDialog(self.iface.mainWindow())
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        pd.setWindowTitle(
            "LVT4U — Đóng gói GPKG" if _vi else "LVT4U — Package GPKG")
        pd.setAutoReset(False)
        pd.setMinimumDuration(0)
        pd.setWindowModality(Qt.WindowModal)
        pd.setMinimumWidth(420)
        pd.setMaximum(total_steps)
        pd.setValue(0)
        QgsApplication.processEvents()

        try:
            step = 0

            # ── 1. Package vectors into GPKG ──
            if vectors:
                pd.setLabelText(
                    f"Đóng gói {len(vectors)} vector layers → GPKG...")
                QgsApplication.processEvents()
                params = {
                    'LAYERS': vectors,
                    'OUTPUT': gpkg_path,
                    'OVERWRITE': True,
                    'SAVE_STYLES': True,
                    'SAVE_METADATA': True,
                    'SELECTED_FEATURES_ONLY': False,
                }
                processing.run("native:package", params)
            step += 1
            pd.setValue(step)

            # ── 2. Copy raster files ──
            if rasters:
                os.makedirs(raster_dir, exist_ok=True)
            raster_path_map = {}  # layer_id → new relative path
            for rlyr in rasters:
                if pd.wasCanceled():
                    return
                info = _get_source_info(rlyr)
                if not info:
                    continue
                src_path = info[0]
                pd.setLabelText(
                    f"Copy raster: {os.path.basename(src_path)}")
                QgsApplication.processEvents()

                # Use GDAL to get all sidecar files (.tfw, .aux.xml, etc.)
                try:
                    ds = gdal.OpenEx(src_path)
                    file_list = ds.GetFileList() if ds else [src_path]
                except Exception:
                    file_list = [src_path]
                finally:
                    ds = None

                for fpath in (file_list or [src_path]):
                    if os.path.isfile(fpath):
                        dst = os.path.join(raster_dir,
                                           os.path.basename(fpath))
                        try:
                            shutil.copy2(fpath, dst)
                        except Exception as e:
                            QgsMessageLog.logMessage(
                                f"[Package] Copy failed: {fpath}: {e}",
                                "LVT4U", Qgis.Warning)

                # Map: layer_id → new basename in raster/
                raster_path_map[rlyr.id()] = os.path.join(
                    "raster", os.path.basename(src_path))

                step += 1
                pd.setValue(step)

            # ── 3. Remap layer sources & save project ──
            pd.setLabelText("Lưu project...")
            QgsApplication.processEvents()

            reg = QgsProviderRegistry.instance()

            # Remap vectors → GPKG
            for vlyr in vectors:
                dp = vlyr.dataProvider()
                if not dp:
                    continue
                parts = reg.decodeUri(dp.name(), vlyr.source())
                # native:package uses layer name as table name
                parts['path'] = QDir(gpkg_path).absolutePath()
                # layerName stays the same (native:package preserves it)
                new_source = reg.encodeUri(dp.name(), parts)
                encoding = (dp.encoding()
                            if isinstance(dp, QgsVectorDataProvider) else None)
                vlyr.setDataSource(
                    new_source, vlyr.name(), vlyr.providerType(),
                    QgsDataProvider.ProviderOptions())
                if encoding:
                    vlyr.dataProvider().setEncoding(encoding)

            # Remap rasters → raster/ subfolder
            for rlyr in rasters:
                rel_path = raster_path_map.get(rlyr.id())
                if not rel_path:
                    continue
                abs_path = QDir(
                    os.path.join(outdir, rel_path)).absolutePath()
                dp = rlyr.dataProvider()
                if not dp:
                    continue
                rlyr.setDataSource(
                    abs_path, rlyr.name(), rlyr.providerType(),
                    QgsDataProvider.ProviderOptions())

            # Online layers: no change needed (URLs stay as-is)

            project.setPresetHomePath('')
            project.writeEntryBool('Paths', '/Absolute', False)
            project.write(qgz_path)

            step += 1
            pd.setValue(pd.maximum())

            # ── 4. Summary ──
            parts_msg = []
            if vectors:
                parts_msg.append(f"{len(vectors)} vector → GPKG")
            if rasters:
                parts_msg.append(f"{len(rasters)} raster → copy")
            if online:
                parts_msg.append(f"{len(online)} online → giữ URL")

            msg = (f'Đóng gói thành công! '
                   f'{" | ".join(parts_msg)} → '
                   f'<a href="{QUrl.fromLocalFile(outdir).toString()}">'
                   f'{outdir}</a>')
            self.iface.messageBar().pushSuccess("LVT4U", msg)
            self.close()

        except Exception as e:
            QMessageBox.critical(self, "LVT4U",
                                 f"Lỗi đóng gói GPKG:\n{e}")
        finally:
            pd.hide()
            pd.deleteLater()
            # Reload original project to restore live state
            mb = self.iface.messageBar()
            mb.widgetAdded.connect(mb.popWidget)
            project.read(orig_project)
            mb.widgetAdded.disconnect(mb.popWidget)

    @staticmethod
    def _try_vacuum(filepath, pd):
        """Vacuum a SQLite file if detected."""
        import sqlite3
        from contextlib import closing
        try:
            with open(filepath, 'rb') as f:
                header = f.read(16)
            if header == b'SQLite format 3\x00':
                pd.setLabelText(f"Vacuum: {os.path.basename(filepath)}")
                QgsApplication.processEvents()
                with closing(sqlite3.connect(filepath)) as conn:
                    conn.execute('VACUUM')
        except Exception:
            pass

    def _show_help(self):
        """Open help dialog for Map Packager module."""
        from ..shared.help_dialog import HelpDialog
        dlg = HelpDialog("map_packager", parent=self)
        dlg.exec_()
