# -*- coding: utf-8 -*-
"""
Satellite Image Downloader — QGIS dialog (v3).

Layout: Left = parameters + table | Right = preview panel.
Thumbnails load on-click only (fast), download = full quality.

Author: Loc Vu Trung / Slow Forest
License: GPL-3.0
"""

import os

from qgis.PyQt.QtCore import (
    Qt, QDate, QThread, pyqtSignal,
    QUrl, QObject
)
from qgis.PyQt.QtGui import QPixmap, QFont, QColor
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QComboBox, QDateEdit, QPushButton, QSlider,
    QRadioButton, QCheckBox, QFileDialog, QProgressBar,
    QMessageBox, QLineEdit, QButtonGroup, QSizePolicy,
    QWidget, QTableWidget, QTableWidgetItem, QHeaderView,
    QAbstractItemView, QSplitter
)
from qgis.PyQt.QtNetwork import QNetworkAccessManager, QNetworkRequest
from qgis.core import (
    QgsWkbTypes,
    QgsProject, QgsRasterLayer, QgsVectorLayer,
    QgsCoordinateReferenceSystem, QgsCoordinateTransform)

from ..shared.i18n import tr
from .satellites import SATELLITES


# ======================================================================
# Preview Signal Bridge (thread-safe)
# ======================================================================

class _PreviewBridge(QObject):
    """Thread-safe signal bridge for preview image delivery."""
    preview_ready = pyqtSignal(object, str)   # img_bytes, cache_key
    preview_error = pyqtSignal(str)           # error message


# ======================================================================
# Worker Thread
# ======================================================================

class _GEEWorker(QThread):
    """Background worker for GEE operations."""
    finished = pyqtSignal(object)
    error = pyqtSignal(str)
    progress = pyqtSignal(str)
    progress_pct = pyqtSignal(int)   # 0-100 percentage

    def __init__(self, task, kwargs):
        super().__init__()
        self.task = task
        self.kwargs = kwargs

    def run(self):
        try:
            # ── Copernicus Tasks (NO GEE needed) ────────────
            if self.task == "cdse_search":
                self.progress.emit(
                    tr("Searching satellite catalog..."))
                self.progress_pct.emit(10)
                from .copernicus import CopernicusDownloader
                cdse = CopernicusDownloader(
                    self.kwargs.pop("username"),
                    self.kwargs.pop("password"))
                self.progress_pct.emit(30)
                result = cdse.search_images(
                    **self.kwargs,
                    progress_cb=lambda pct: self.progress_pct.emit(
                        30 + int(pct * 0.6)))
                self.progress_pct.emit(95)
                thumb_cache = {}
                for item in result:
                    if item.get("quicklook"):
                        thumb_cache[item["id"]] = item["quicklook"]
                self.progress_pct.emit(100)
                self.finished.emit({
                    "task": "search",
                    "data": result,
                    "thumb_cache": thumb_cache,
                })
                return

            elif self.task == "stac_search":
                self.progress.emit(
                    tr("Searching satellite catalog..."))
                self.progress_pct.emit(20)
                import requests as _req
                stac_col = self.kwargs.get(
                    "stac_collection", "sentinel-2-l2a")
                aoi = self.kwargs["aoi_geojson"]
                start = self.kwargs["start_date"]
                end = self.kwargs["end_date"]
                max_cloud = self.kwargs.get("max_cloud", 20)

                body = {
                    "collections": [stac_col],
                    "datetime": (f"{start}T00:00:00Z/"
                                 f"{end}T23:59:59Z"),
                    "limit": 50,
                    "intersects": aoi,
                }
                if "landsat" in stac_col or "sentinel" in stac_col:
                    body["query"] = {
                        "eo:cloud_cover": {"lte": max_cloud}}

                self.progress_pct.emit(40)
                resp = _req.post(
                    "https://earth-search.aws.element84.com"
                    "/v1/search",
                    json=body, timeout=60)
                resp.raise_for_status()
                features = resp.json().get("features", [])
                self.progress_pct.emit(80)

                results = []
                thumb_cache = {}
                for feat in features:
                    props = feat.get("properties", {})
                    dt = props.get("datetime", "")[:10]
                    cloud = props.get("eo:cloud_cover")
                    tile = props.get("grid:code", "")
                    if tile.startswith("MGRS-"):
                        tile = tile[5:]
                    assets = feat.get("assets", {})

                    # Thumbnail: prefer links[rel=thumbnail] (HTTP)
                    # over assets.thumbnail (may be s3://)
                    ql_url = ""
                    for lnk in feat.get("links", []):
                        if lnk.get("rel") == "thumbnail":
                            ql_url = lnk.get("href", "")
                            break
                    if not ql_url and "thumbnail" in assets:
                        ql_url = assets["thumbnail"].get("href", "")

                    # Convert s3:// → HTTPS for all asset URLs
                    stac_assets = {}
                    for k, v in assets.items():
                        href = v.get("href", "")
                        if href.startswith("s3://usgs-landsat/"):
                            href = href.replace(
                                "s3://usgs-landsat/",
                                "https://landsatlook.usgs.gov/data/")
                        stac_assets[k] = href

                    item = {
                        "id": feat.get("id", ""),
                        "name": feat.get("id", ""),
                        "date": dt,
                        "cloud": (round(cloud, 1)
                                  if cloud is not None else None),
                        "tile_id": tile,
                        "quicklook": ql_url,
                        "source": "stac",
                        "stac_assets": stac_assets,
                    }
                    results.append(item)
                    if ql_url:
                        thumb_cache[item["id"]] = ql_url

                self.progress_pct.emit(100)
                self.finished.emit({
                    "task": "search",
                    "data": results,
                    "thumb_cache": thumb_cache,
                })
                return

            elif self.task == "cdse_download_batch":
                from .copernicus import CopernicusDownloader
                cdse = CopernicusDownloader(
                    self.kwargs.pop("username"),
                    self.kwargs.pop("password"))
                items = self.kwargs.pop("items")
                output_dir = self.kwargs.pop("output_dir")
                aoi_geojson = self.kwargs.pop("aoi_geojson", None)
                bands_key = self.kwargs.pop("bands_key", "")
                sat_key = self.kwargs.pop("sat_key", "S2_SR")
                total = len(items)
                paths = []
                for i, item in enumerate(items):
                    pct = int((i / total) * 100)
                    self.progress_pct.emit(pct)
                    self.progress.emit(
                        tr("Downloading {n}/{total}: {date}...").format(
                            n=i + 1, total=total,
                            date=item["date"]))
                    try:
                        path = cdse.download_product(
                            product_id=item["id"],
                            output_dir=output_dir,
                            product_name=item.get("name", ""),
                            aoi_geojson=aoi_geojson,
                            bands_key=bands_key,
                            sat_key=sat_key)
                        paths.append(path)
                    except Exception as exc:
                        self.progress.emit(
                            tr("Skipped {date}: {err}").format(
                                date=item["date"], err=str(exc)))
                self.progress_pct.emit(100)
                self.finished.emit({
                    "task": "download_batch", "data": paths})
                return

            elif self.task == "stac_download_batch":
                items = self.kwargs.pop("items")
                output_dir = self.kwargs.pop("output_dir")
                aoi_geojson = self.kwargs.pop("aoi_geojson", None)
                bands_key = self.kwargs.pop("bands_key", "")
                sat_key = self.kwargs.pop("sat_key", "S2_SR")
                total = len(items)
                paths = []

                from osgeo import gdal
                gdal.UseExceptions()
                from .copernicus import CopernicusDownloader

                # Resolve bands for this satellite
                band_info = CopernicusDownloader._resolve_bands(
                    sat_key, bands_key)
                wanted_assets = band_info["assets"]
                is_index = band_info.get("index")

                # Build bbox from AOI
                bbox = None
                if aoi_geojson:
                    bbox = CopernicusDownloader._geojson_to_bbox(
                        aoi_geojson)

                for i, item in enumerate(items):
                    pct = int((i / total) * 100)
                    self.progress_pct.emit(pct)
                    self.progress.emit(
                        tr("Downloading {n}/{total}: {date}...").format(
                            n=i + 1, total=total,
                            date=item["date"]))

                    stac_assets = item.get("stac_assets", {})
                    # Convert to {key: {"href": url}} format
                    assets_dict = {}
                    for k, v in stac_assets.items():
                        if isinstance(v, str) and v:
                            assets_dict[k] = {"href": v}
                        elif isinstance(v, dict):
                            assets_dict[k] = v

                    safe_name = item.get(
                        "name", item["id"]).replace("/", "_")[:80]
                    out_path = os.path.join(
                        output_dir, f"{safe_name}.tif")

                    try:
                        if is_index:
                            CopernicusDownloader._download_and_compute_index(
                                None, assets_dict, wanted_assets,
                                is_index, bbox, out_path)
                        elif len(wanted_assets) == 1:
                            key = CopernicusDownloader._find_best_asset(
                                assets_dict, wanted_assets)
                            if not key:
                                continue
                            src_url = assets_dict[key]["href"]
                            vsicurl = f"/vsicurl/{src_url}"
                            opts = {}
                            if bbox:
                                opts["projWin"] = [
                                    bbox[0], bbox[3],
                                    bbox[2], bbox[1]]
                                opts["projWinSRS"] = "EPSG:4326"
                            gdal.Translate(
                                out_path, vsicurl, **opts)
                        else:
                            # Multi-band stack
                            tmp_files = []
                            for j, key in enumerate(wanted_assets):
                                akey = CopernicusDownloader._find_best_asset(
                                    assets_dict, [key])
                                if not akey:
                                    continue
                                src_url = assets_dict[akey]["href"]
                                vsicurl = f"/vsicurl/{src_url}"
                                tmp_path = os.path.join(
                                    output_dir,
                                    f"_tmp_stac_{i}_{j}.tif")
                                opts = {}
                                if bbox:
                                    opts["projWin"] = [
                                        bbox[0], bbox[3],
                                        bbox[2], bbox[1]]
                                    opts["projWinSRS"] = "EPSG:4326"
                                gdal.Translate(
                                    tmp_path, vsicurl, **opts)
                                tmp_files.append(tmp_path)
                            if tmp_files:
                                vrt = gdal.BuildVRT(
                                    "", tmp_files, separate=True)
                                gdal.Translate(out_path, vrt)
                                vrt = None
                                for f in tmp_files:
                                    try:
                                        os.remove(f)
                                    except OSError:
                                        pass
                    except Exception as exc:
                        self.progress.emit(
                            f"Error: {item['date']}: {exc}")
                        continue

                    if os.path.exists(out_path):
                        paths.append(out_path)

                self.progress_pct.emit(100)
                self.finished.emit({
                    "task": "download_batch", "data": paths})
                return

            elif self.task == "cdse_preview":
                import requests as _req
                url = self.kwargs.get("url", "")
                cache_key = self.kwargs.get("cache_key", "")
                product_id = self.kwargs.get("product_id", "")

                # If no quicklook URL, fetch it from API
                if not url and product_id:
                    api_url = (
                        "https://catalogue.dataspace.copernicus.eu"
                        f"/odata/v1/Products({product_id})"
                        "?$expand=Assets"
                    )
                    ar = _req.get(api_url, timeout=15)
                    if ar.status_code == 200:
                        pdata = ar.json()
                        for asset in pdata.get("Assets", []):
                            if "QUICKLOOK" in asset.get("Type", "").upper():
                                aid = asset.get("Id", "")
                                if aid:
                                    url = (
                                        "https://catalogue.dataspace.copernicus.eu"
                                        f"/odata/v1/Assets({aid})/$value"
                                    )
                                break

                if not url:
                    self.error.emit("No preview available for this image")
                    return

                resp = _req.get(url, timeout=20, allow_redirects=True)
                resp.raise_for_status()
                self.finished.emit({
                    "task": "cdse_preview",
                    "data": {
                        "img_bytes": resp.content,
                        "cache_key": cache_key,
                    },
                })
                return

            # ── GEE Tasks (only if ee is installed) ─────────
            from .downloader import GEEDownloader
            engine = GEEDownloader()

            if self.task == "search":
                self.progress.emit(tr("Searching satellite catalog..."))
                result = engine.search_images(**self.kwargs)
                self.finished.emit({"task": "search", "data": result})

            elif self.task == "get_thumb_url":
                self.progress.emit(tr("Loading preview..."))
                url = engine.get_thumb_url(**self.kwargs)
                self.finished.emit({
                    "task": "get_thumb_url",
                    "data": {"url": url,
                             "image_id": self.kwargs.get("image_id"),
                             "bands_key": self.kwargs.get("bands_key", "")},
                })

            elif self.task == "download_batch":
                items = self.kwargs.pop("items")
                base_kwargs = self.kwargs
                paths = []
                for i, item in enumerate(items):
                    self.progress.emit(
                        tr("Downloading {n}/{total}: {date}...").format(
                            n=i + 1, total=len(items),
                            date=item["date"]))
                    kw = dict(base_kwargs)
                    kw["image_id"] = item["id"]
                    path = engine.download_single(**kw)
                    paths.append(path)
                self.finished.emit({"task": "download_batch", "data": paths})

            elif self.task == "download_composite":
                self.progress.emit(tr("Creating composite & downloading..."))
                path = engine.download_composite(**self.kwargs)
                self.finished.emit({"task": "download", "data": path})

        except Exception as e:
            err_str = str(e)
            if "authenticate" in err_str.lower() or "authorize" in err_str.lower():
                self.error.emit(
                    "Authentication required.\n\n"
                    "A browser window will open for Google login.\n"
                    "After signing in, click 'Search' again.")
            else:
                self.error.emit(f"{type(e).__name__}: {e}")


# ======================================================================
# Main Dialog
# ======================================================================

class SatelliteDlDialog(QDialog):
    """Satellite Image Downloader — v3 with click-to-preview."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self._worker = None
        self._search_results = []
        self._thumb_cache = {}      # {image_id: thumb_url}
        self._nam = QNetworkAccessManager(self)
        self._current_preview_id = None
        self._preview_bridge = _PreviewBridge(self)
        self._preview_bridge.preview_ready.connect(
            self._show_cdse_preview)
        self._preview_bridge.preview_error.connect(
            lambda msg: self.lbl_preview_loading.setText(msg))

        self.setWindowTitle(tr("LVT4U — Satellite Image Downloader"))
        self.setMinimumSize(900, 600)

        # Limit height to 95% of screen to avoid bottom overflow
        from qgis.PyQt.QtWidgets import QApplication
        screen = QApplication.primaryScreen()
        if screen:
            screen_h = screen.availableGeometry().height()
            max_h = int(screen_h * 0.95)
            self.setMaximumHeight(max_h)
            self.resize(960, min(800, max_h))
        else:
            self.resize(960, 800)

        self._build_ui()
        self._connect_signals()
        self.refresh_layers()

    # ------------------------------------------------------------------
    # UI
    # ------------------------------------------------------------------

    def _build_ui(self):
        main_layout = QVBoxLayout(self)
        main_layout.setSpacing(6)
        main_layout.setContentsMargins(8, 6, 8, 8)

        # Title
        title = QLabel(tr("Satellite Image Downloader"))
        title.setFont(QFont("Segoe UI", 13, QFont.Bold))
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet("color: #1565C0; padding: 2px;")
        main_layout.addWidget(title)

        # ── TOP PARAMS ──
        params_w = QWidget()
        pl = QHBoxLayout(params_w)
        pl.setContentsMargins(0, 0, 0, 0)
        pl.setSpacing(6)

        # Left column: Satellite + Time + AOI
        left_col = QVBoxLayout()
        left_col.setSpacing(4)

        # Satellite
        r_sat = QHBoxLayout()
        r_sat.addWidget(QLabel(tr("Satellite:")))
        self.cmb_satellite = QComboBox()
        for key, cfg in SATELLITES.items():
            self.cmb_satellite.addItem(cfg["name"], key)
        self.cmb_satellite.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed)
        r_sat.addWidget(self.cmb_satellite)
        left_col.addLayout(r_sat)

        # Time
        r_time = QHBoxLayout()
        r_time.addWidget(QLabel(tr("From:")))
        self.date_start = QDateEdit()
        self.date_start.setCalendarPopup(True)
        self.date_start.setDisplayFormat("yyyy-MM-dd")
        self.date_start.setDate(QDate.currentDate().addMonths(-3))
        r_time.addWidget(self.date_start)
        r_time.addWidget(QLabel(tr("To:")))
        self.date_end = QDateEdit()
        self.date_end.setCalendarPopup(True)
        self.date_end.setDisplayFormat("yyyy-MM-dd")
        self.date_end.setDate(QDate.currentDate())
        r_time.addWidget(self.date_end)
        for label, days in [("30d", 30), ("90d", 90),
                            ("6m", 180), ("1y", 365)]:
            btn = QPushButton(label)
            btn.setFixedWidth(38)
            btn.setFixedHeight(24)
            btn.clicked.connect(
                lambda _, d=days: self._set_quick_date(d))
            r_time.addWidget(btn)
        left_col.addLayout(r_time)

        # AOI
        r_aoi = QHBoxLayout()
        self.aoi_group = QButtonGroup(self)
        r1 = QRadioButton(tr("Layer"))
        r1.setChecked(True)
        r3 = QRadioButton(tr("Map View"))
        self.aoi_group.addButton(r1, 0)
        self.aoi_group.addButton(r3, 2)
        r_aoi.addWidget(r1)
        self.cmb_layer = QComboBox()
        self.cmb_layer.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed)
        r_aoi.addWidget(self.cmb_layer)
        r_aoi.addWidget(r3)
        left_col.addLayout(r_aoi)

        pl.addLayout(left_col, 3)

        # Right column: Bands + Cloud + Resolution
        right_col = QVBoxLayout()
        right_col.setSpacing(4)

        r_bands = QHBoxLayout()
        r_bands.addWidget(QLabel(tr("Bands:")))
        self.cmb_bands = QComboBox()
        self.cmb_bands.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed)
        r_bands.addWidget(self.cmb_bands)
        right_col.addLayout(r_bands)

        r_cloud = QHBoxLayout()
        r_cloud.addWidget(QLabel(tr("Cloud max:")))
        self.sld_cloud = QSlider(Qt.Horizontal)
        self.sld_cloud.setRange(0, 100)
        self.sld_cloud.setValue(20)
        r_cloud.addWidget(self.sld_cloud)
        self.lbl_cloud = QLabel("20%")
        self.lbl_cloud.setMinimumWidth(35)
        r_cloud.addWidget(self.lbl_cloud)
        right_col.addLayout(r_cloud)

        r_res = QHBoxLayout()
        r_res.addWidget(QLabel(tr("Resolution:")))
        self.cmb_resolution = QComboBox()
        r_res.addWidget(self.cmb_resolution)
        r_res.addStretch()
        r_res.addStretch()
        right_col.addLayout(r_res)

        pl.addLayout(right_col, 2)

        main_layout.addWidget(params_w)

        # Search button
        search_row = QHBoxLayout()
        self.btn_search = QPushButton(tr("  Search Images"))
        self.btn_search.setIcon(
            self.style().standardIcon(
                self.style().SP_FileDialogContentsView))
        self.btn_search.setMinimumHeight(32)
        self.btn_search.setStyleSheet(
            "QPushButton { background: #0D47A1; color: white; "
            "font-weight: bold; border-radius: 4px; padding: 5px 18px; }"
            "QPushButton:hover { background: #1565C0; }")
        search_row.addWidget(self.btn_search)

        self.btn_reset = QPushButton(tr("  Reset"))
        self.btn_reset.setIcon(
            self.style().standardIcon(
                self.style().SP_DialogResetButton))
        self.btn_reset.setMinimumHeight(32)
        self.btn_reset.setStyleSheet(
            "QPushButton { border-radius: 4px; padding: 5px 12px; }")
        self.btn_reset.clicked.connect(self._on_reset)
        search_row.addWidget(self.btn_reset)

        search_row.addStretch()
        self.lbl_status = QLabel(tr("Ready"))
        self.lbl_status.setStyleSheet("color: #555;")
        search_row.addWidget(self.lbl_status)
        main_layout.addLayout(search_row)

        # ── MIDDLE: Splitter (Table | Preview) ──
        self.splitter = QSplitter(Qt.Horizontal)

        # LEFT: Table
        tbl_widget = QWidget()
        tbl_layout = QVBoxLayout(tbl_widget)
        tbl_layout.setContentsMargins(0, 0, 0, 0)
        tbl_layout.setSpacing(4)

        self.tbl_results = QTableWidget()
        self.tbl_results.setColumnCount(4)
        self.tbl_results.setHorizontalHeaderLabels([
            "", tr("Date"), tr("Cloud %"), tr("Tile ID")
        ])
        self.tbl_results.setSelectionBehavior(
            QAbstractItemView.SelectRows)
        self.tbl_results.setSelectionMode(
            QAbstractItemView.SingleSelection)
        self.tbl_results.setAlternatingRowColors(True)
        self.tbl_results.verticalHeader().setVisible(False)

        header = self.tbl_results.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.Fixed)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QHeaderView.Stretch)
        self.tbl_results.setColumnWidth(0, 30)
        tbl_layout.addWidget(self.tbl_results)

        # Select buttons
        sel_row = QHBoxLayout()
        btn_all = QPushButton(tr("Select All"))
        btn_all.setFixedHeight(24)
        btn_all.clicked.connect(self._select_all)
        sel_row.addWidget(btn_all)
        btn_none = QPushButton(tr("Deselect All"))
        btn_none.setFixedHeight(24)
        btn_none.clicked.connect(self._select_none)
        sel_row.addWidget(btn_none)
        sel_row.addStretch()
        self.lbl_selected = QLabel("")
        self.lbl_selected.setStyleSheet(
            "color: #1B5E20; font-weight: bold;")
        sel_row.addWidget(self.lbl_selected)
        tbl_layout.addLayout(sel_row)

        self.splitter.addWidget(tbl_widget)

        # RIGHT: Preview panel
        preview_widget = QWidget()
        preview_widget.setMinimumWidth(350)
        pv_layout = QVBoxLayout(preview_widget)
        pv_layout.setContentsMargins(4, 0, 0, 0)
        pv_layout.setSpacing(4)

        pv_title = QLabel(tr("Preview"))
        pv_title.setFont(QFont("Segoe UI", 11, QFont.Bold))
        pv_title.setAlignment(Qt.AlignCenter)
        pv_title.setStyleSheet("color: #333;")
        pv_layout.addWidget(pv_title)

        # Preview image
        self.lbl_preview = QLabel()
        self.lbl_preview.setAlignment(Qt.AlignCenter)
        self.lbl_preview.setMinimumSize(260, 260)
        self.lbl_preview.setStyleSheet(
            "QLabel { background: #f0f0f0; border: 1px solid #ccc; "
            "border-radius: 4px; }")
        self.lbl_preview.setText(
            tr("Click an image in the table\nto preview cloud coverage"))
        self.lbl_preview.setWordWrap(True)
        pv_layout.addWidget(self.lbl_preview, 1)

        # Preview info
        self.lbl_preview_info = QLabel("")
        self.lbl_preview_info.setAlignment(Qt.AlignCenter)
        self.lbl_preview_info.setWordWrap(True)
        self.lbl_preview_info.setStyleSheet(
            "color: #444; font-size: 11px; padding: 4px;")
        pv_layout.addWidget(self.lbl_preview_info)

        # Preview loading indicator
        self.lbl_preview_loading = QLabel("")
        self.lbl_preview_loading.setAlignment(Qt.AlignCenter)
        self.lbl_preview_loading.setStyleSheet("color: #999;")
        pv_layout.addWidget(self.lbl_preview_loading)

        self.splitter.addWidget(preview_widget)
        self.splitter.setSizes([480, 480])
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 1)

        main_layout.addWidget(self.splitter, 1)

        # ── BOTTOM: Output + Buttons ──
        bottom_w = QWidget()
        bl = QHBoxLayout(bottom_w)
        bl.setContentsMargins(0, 4, 0, 0)
        bl.setSpacing(8)

        # Output options
        bl.addWidget(QLabel(tr("Format:")))
        self.rb_tiff = QRadioButton("GeoTIFF")
        self.rb_tiff.setChecked(True)
        self.rb_png = QRadioButton("PNG")
        fmt_group = QButtonGroup(self)
        fmt_group.addButton(self.rb_tiff)
        fmt_group.addButton(self.rb_png)
        bl.addWidget(self.rb_tiff)
        bl.addWidget(self.rb_png)

        bl.addWidget(QLabel(tr("Folder:")))
        self.txt_folder = QLineEdit()
        # Restore saved folder or default to Downloads
        from qgis.PyQt.QtCore import QSettings
        saved_folder = QSettings().value(
            "LVT4U / satellite_dl_folder", "")
        if saved_folder and os.path.isdir(saved_folder):
            self.txt_folder.setText(saved_folder)
        else:
            self.txt_folder.setText(
                os.path.join(os.path.expanduser("~"), "Downloads"))
        self.txt_folder.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed)
        bl.addWidget(self.txt_folder)
        btn_browse = QPushButton("...")
        btn_browse.setFixedWidth(30)
        btn_browse.clicked.connect(self._browse_folder)
        bl.addWidget(btn_browse)

        self.chk_add_layer = QCheckBox(tr("Add to QGIS"))
        self.chk_add_layer.setChecked(True)
        bl.addWidget(self.chk_add_layer)

        main_layout.addWidget(bottom_w)

        # Progress + action buttons
        self.progress = QProgressBar()
        self.progress.setRange(0, 0)
        self.progress.setVisible(False)
        self.progress.setMaximumHeight(14)
        main_layout.addWidget(self.progress)

        btn_row = QHBoxLayout()
        self.btn_download = QPushButton(tr("  Download Selected"))
        self.btn_download.setIcon(
            self.style().standardIcon(
                self.style().SP_DialogSaveButton))
        self.btn_download.setMinimumHeight(34)
        self.btn_download.setEnabled(False)
        self.btn_download.setStyleSheet(
            "QPushButton { background: #2E7D32; color: white; "
            "font-weight: bold; border-radius: 4px; padding: 6px 18px; }"
            "QPushButton:hover { background: #388E3C; }"
            "QPushButton:disabled { background: #A5D6A7; color: #eee; }")
        btn_row.addWidget(self.btn_download)

        btn_row.addStretch()

        btn_help = QPushButton(tr("Help"))
        btn_help.setIcon(
            self.style().standardIcon(
                self.style().SP_DialogHelpButton))
        btn_help.setMinimumHeight(32)
        btn_help.clicked.connect(self._on_help)
        btn_row.addWidget(btn_help)

        btn_close = QPushButton(tr("Close"))
        btn_close.setMinimumHeight(32)
        btn_close.clicked.connect(self.close)
        btn_row.addWidget(btn_close)

        main_layout.addLayout(btn_row)

        # Initial population
        self._on_satellite_changed()

    # ------------------------------------------------------------------
    # Signals
    # ------------------------------------------------------------------

    def _connect_signals(self):
        self.cmb_satellite.currentIndexChanged.connect(
            self._on_satellite_changed)
        self.sld_cloud.valueChanged.connect(
            lambda v: self.lbl_cloud.setText(f"{v}%"))
        self.aoi_group.buttonClicked.connect(self._on_aoi_source_changed)
        self.btn_search.clicked.connect(self._on_search)
        self.btn_download.clicked.connect(self._on_download)
        self.cmb_layer.currentIndexChanged.connect(self._update_aoi_info)
        self.tbl_results.itemChanged.connect(self._on_check_changed)
        # Click row → preview
        self.tbl_results.clicked.connect(self._on_row_clicked)

    # ------------------------------------------------------------------
    # Slots
    # ------------------------------------------------------------------

    def _on_satellite_changed(self):
        sat_key = self.cmb_satellite.currentData()
        if not sat_key:
            return
        cfg = SATELLITES[sat_key]

        # Update bands dropdown with description
        self.cmb_bands.clear()
        for bname, bcfg in cfg["bands"].items():
            desc = bcfg.get("description", "")
            if desc:
                label = f"{bname}  —  {desc}"
            else:
                label = bname
            self.cmb_bands.addItem(label, bname)

        # Update resolution dropdown
        self.cmb_resolution.clear()
        for r in cfg.get("resolutions", [10]):
            self.cmb_resolution.addItem(f"{r} m", r)

        has_cloud = cfg.get("cloud_field") is not None
        self.sld_cloud.setEnabled(has_cloud)
        if not has_cloud:
            self.lbl_cloud.setText("N/A")
        else:
            self.lbl_cloud.setText(f"{self.sld_cloud.value()}%")

        self._clear_results()

    def _on_reset(self):
        """Reset all search parameters and results."""
        self._clear_results()
        self.date_start.setDate(QDate.currentDate().addMonths(-1))
        self.date_end.setDate(QDate.currentDate())
        self.sld_cloud.setValue(20)
        self.cmb_satellite.setCurrentIndex(0)
        self.lbl_status.setText(tr("Ready"))
        self.refresh_layers()

    def _on_aoi_source_changed(self):
        idx = self.aoi_group.checkedId()
        self.cmb_layer.setEnabled(idx == 0)

    def _set_quick_date(self, days):
        self.date_end.setDate(QDate.currentDate())
        self.date_start.setDate(QDate.currentDate().addDays(-days))

    def _browse_folder(self):
        folder = QFileDialog.getExistingDirectory(
            self, tr("Select output folder"), self.txt_folder.text())
        if folder:
            self.txt_folder.setText(folder)
            # Save for next session
            from qgis.PyQt.QtCore import QSettings
            QSettings().setValue("LVT4U / satellite_dl_folder", folder)

    def refresh_layers(self):
        self.cmb_layer.clear()
        for layer in QgsProject.instance().mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                if layer.geometryType() == QgsWkbTypes.PolygonGeometry:
                    self.cmb_layer.addItem(
                        f"{layer.name()} ({layer.featureCount()})",
                        layer.id())
        self._update_aoi_info()

    def _update_aoi_info(self):
        pass  # simplified — info shown in layer dropdown

    # ------------------------------------------------------------------
    # AOI (robust)
    # ------------------------------------------------------------------

    def _get_aoi_geojson(self):
        idx = self.aoi_group.checkedId()
        if idx == 0:
            return self._aoi_from_layer()
        else:
            return self._aoi_from_map_view()

    def _aoi_from_layer(self):
        """Extract AOI as simple bounding box from layer extent.

        Uses layer.extent() which is instant — no geometry iteration.
        Complex polygon merging was removed because it caused extreme
        slowness on layers with many / complex features.
        """
        layer_id = self.cmb_layer.currentData()
        if not layer_id:
            raise ValueError(tr("No polygon layer selected."))

        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.featureCount() == 0:
            raise ValueError(tr("Layer is empty or not found."))

        src_crs = layer.crs()
        dst_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        xform = QgsCoordinateTransform(
            src_crs, dst_crs, QgsProject.instance())

        ext = layer.extent()
        ext = xform.transformBoundingBox(ext)
        return {
            "type": "Polygon",
            "coordinates": [[
                [ext.xMinimum(), ext.yMinimum()],
                [ext.xMaximum(), ext.yMinimum()],
                [ext.xMaximum(), ext.yMaximum()],
                [ext.xMinimum(), ext.yMaximum()],
                [ext.xMinimum(), ext.yMinimum()],
            ]],
        }

    def _aoi_from_map_view(self):
        extent = self.iface.mapCanvas().extent()
        src_crs = self.iface.mapCanvas().mapSettings().destinationCrs()
        dst_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        xform = QgsCoordinateTransform(
            src_crs, dst_crs, QgsProject.instance())
        extent = xform.transformBoundingBox(extent)
        return {
            "type": "Polygon",
            "coordinates": [[
                [extent.xMinimum(), extent.yMinimum()],
                [extent.xMaximum(), extent.yMinimum()],
                [extent.xMaximum(), extent.yMaximum()],
                [extent.xMinimum(), extent.yMaximum()],
                [extent.xMinimum(), extent.yMinimum()],
            ]],
        }

    # ------------------------------------------------------------------
    # Search
    # ------------------------------------------------------------------

    # Shared Copernicus credentials (free, open access)
    _CDSE_USER = "slow.lao.tech1@gmail.com"
    _CDSE_PASS = "U?96&Pr~aL~3K4q"

    # Map satellite keys to Copernicus collection + product type
    _CDSE_SAT_MAP = {
        "S2_SR": {"collection": "SENTINEL-2",
                  "product_type": "S2MSI2A",
                  "stac_collection": "sentinel-2-l2a"},
        "L8_SR": {"collection": None,  # Not on CDSE → use STAC
                  "stac_collection": "landsat-c2-l2"},
        "L9_SR": {"collection": None,  # Not on CDSE → use STAC
                  "stac_collection": "landsat-c2-l2"},
        "S1_GRD": {"collection": "SENTINEL-1",
                   "product_type": "S1_GRD",
                   "stac_collection": "sentinel-1-grd"},
        "MODIS_NDVI": {"collection": None,  # STAC only
                       "stac_collection": "modis-13Q1-061"},
    }

    def _on_search(self):
        try:
            sat_key = self.cmb_satellite.currentData()
            sat_cfg = self._CDSE_SAT_MAP.get(sat_key, {})

            aoi = self._get_aoi_geojson()
            start = self.date_start.date().toString("yyyy-MM-dd")
            end = self.date_end.date().toString("yyyy-MM-dd")
            cloud = self.sld_cloud.value()

            # Use STAC search for ALL satellites (no auth needed,
            # works for historical data without 403 errors)
            stac_col = sat_cfg.get(
                "stac_collection", "sentinel-2-l2a")
            params = {
                "aoi_geojson": aoi,
                "start_date": start,
                "end_date": end,
                "max_cloud": cloud,
                "stac_collection": stac_col,
                "sat_key": sat_key,
            }
            self._clear_results()
            self._start_worker("stac_search", params)
        except ValueError as e:
            QMessageBox.warning(self, "LVT4U", str(e))
        except Exception as e:
            QMessageBox.critical(self, "LVT4U",
                                 f"{type(e).__name__}: {e}")

    # ------------------------------------------------------------------
    # Results Table
    # ------------------------------------------------------------------

    def _clear_results(self):
        self._search_results = []
        self._thumb_cache.clear()
        self.tbl_results.setRowCount(0)
        self.btn_download.setEnabled(False)
        self.lbl_selected.setText("")
        self.lbl_preview.setPixmap(QPixmap())
        self.lbl_preview.setText(
            tr("Click an image in the table\nto preview cloud coverage"))
        self.lbl_preview_info.setText("")
        self._current_preview_id = None

    def _populate_results(self, images):
        self._search_results = images
        self.tbl_results.blockSignals(True)
        self.tbl_results.setRowCount(len(images))

        for row, img in enumerate(images):
            # Checkbox
            chk = QTableWidgetItem()
            chk.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            chk.setCheckState(Qt.Unchecked)
            self.tbl_results.setItem(row, 0, chk)

            # Date
            d_item = QTableWidgetItem(img["date"])
            d_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            d_item.setTextAlignment(Qt.AlignCenter)
            self.tbl_results.setItem(row, 1, d_item)

            # Cloud %
            cloud = img.get("cloud")
            c_str = f"{cloud}%" if cloud is not None else "N/A"
            c_item = QTableWidgetItem(c_str)
            c_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            c_item.setTextAlignment(Qt.AlignCenter)
            if cloud is not None:
                if cloud < 10:
                    c_item.setBackground(QColor("#C8E6C9"))
                elif cloud < 30:
                    c_item.setBackground(QColor("#FFF9C4"))
                elif cloud < 60:
                    c_item.setBackground(QColor("#FFE0B2"))
                else:
                    c_item.setBackground(QColor("#FFCDD2"))
            self.tbl_results.setItem(row, 2, c_item)

            # Image ID / Tile ID
            if img.get("tile_id"):
                short = img["tile_id"]
            elif "/" in img["id"]:
                short = img["id"].split("/")[-1]
            else:
                short = img["id"][:16] + "..."
            id_item = QTableWidgetItem(short)
            id_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            id_item.setToolTip(img.get("name", img["id"]))
            self.tbl_results.setItem(row, 3, id_item)

            self.tbl_results.setRowHeight(row, 28)

        self.tbl_results.blockSignals(False)

    # ------------------------------------------------------------------
    # Click row → preview
    # ------------------------------------------------------------------

    def _on_row_clicked(self, index):
        """Load preview for clicked row — uses cache for speed."""
        row = index.row()
        if row < 0 or row >= len(self._search_results):
            return

        img = self._search_results[row]

        img_id = img["id"]
        bands_key = self.cmb_bands.currentData() or ""
        cache_key = f"{img_id}|{bands_key}"

        # Skip if same image+bands already previewed
        if cache_key == self._current_preview_id:
            return

        self._current_preview_id = cache_key
        self.lbl_preview.setText("")
        self.lbl_preview_info.setText(
            f"{img['date']}  |  "
            f"Cloud: {img.get('cloud', 'N/A')}%")

        # Copernicus/STAC: use requests (Qt NAM fails on special URLs)
        if img.get("source") in ("copernicus", "stac"):
            ql = img.get("quicklook", "")
            if not ql and img_id in self._thumb_cache:
                ql = self._thumb_cache[img_id]
            self.lbl_preview_loading.setText(tr("Loading preview..."))
            self._load_cdse_preview(ql, img_id, cache_key)
            return

        # GEE: Fast path via cached thumb URL
        if cache_key in self._thumb_cache:
            self.lbl_preview_loading.setText(tr("Loading preview..."))
            self._download_preview_image(
                self._thumb_cache[cache_key], cache_key)
            return
        if img_id in self._thumb_cache:
            self.lbl_preview_loading.setText(tr("Loading preview..."))
            self._download_preview_image(
                self._thumb_cache[img_id], cache_key)
            return

        # GEE path: get thumb URL via worker
        self.lbl_preview_loading.setText(tr("Loading preview..."))
        try:
            aoi = self._get_aoi_geojson()
            params = {
                "image_id": img_id,
                "sat_key": self.cmb_satellite.currentData(),
                "aoi_geojson": aoi,
                "bands_key": bands_key,
            }
            self._start_preview_worker(params)
        except Exception as e:
            self.lbl_preview_loading.setText(f"Error: {e}")

    def _load_cdse_preview(self, ql_url, product_id, cache_key):
        """Download CDSE quicklook in background thread, display via signal."""
        import threading
        bridge = self._preview_bridge

        def _worker():
            try:
                import requests as _req

                url = ql_url
                # If no URL, fetch asset for this product
                if not url:
                    api = (
                        "https://catalogue.dataspace.copernicus.eu"
                        f"/odata/v1/Products({product_id})"
                        "?$expand=Assets"
                    )
                    ar = _req.get(api, timeout=15)
                    if ar.status_code == 200:
                        for asset in ar.json().get("Assets", []):
                            if "QUICKLOOK" in asset.get("Type", "").upper():
                                aid = asset.get("Id", "")
                                if aid:
                                    url = (
                                        "https://catalogue.dataspace.copernicus.eu"
                                        f"/odata/v1/Assets({aid})/$value"
                                    )
                                break

                if not url:
                    bridge.preview_error.emit("Preview not available")
                    return

                resp = _req.get(url, timeout=20, allow_redirects=True)
                resp.raise_for_status()
                bridge.preview_ready.emit(resp.content, cache_key)

            except Exception as e:
                bridge.preview_error.emit(f"Preview error: {e}")

        t = threading.Thread(target=_worker, daemon=True)
        t.start()

    def _show_cdse_preview(self, img_bytes, cache_key):
        """Display CDSE preview image bytes as QPixmap (main thread)."""
        self.lbl_preview_loading.setText("")

        if cache_key != self._current_preview_id:
            return  # user clicked another image

        from qgis.PyQt.QtCore import QByteArray
        pixmap = QPixmap()
        qba = QByteArray(img_bytes)

        if not pixmap.loadFromData(qba):
            pixmap = QPixmap()
            pixmap.loadFromData(qba, "JPEG")
        if pixmap.isNull():
            pixmap = QPixmap()
            pixmap.loadFromData(qba, "PNG")

        if not pixmap.isNull():
            panel_size = self.lbl_preview.size()
            scaled = pixmap.scaled(
                panel_size.width() - 10,
                panel_size.height() - 10,
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation)
            self.lbl_preview.setPixmap(scaled)
        else:
            self.lbl_preview.setText(tr("Preview not available"))

    def _start_preview_worker(self, params, task_name="get_thumb_url"):
        """Start a worker to get preview image.

        IMPORTANT: Only ONE worker can run at a time to avoid
        SSL access violations on Windows (concurrent SSL in QThread).
        """
        # Cancel any running worker first — prevents SSL crash
        if self._worker and self._worker.isRunning():
            try:
                self._worker.finished.disconnect()
                self._worker.error.disconnect()
            except (TypeError, RuntimeError):
                pass
            self._worker.quit()
            self._worker.wait(1000)
            if self._worker.isRunning():
                self._worker.terminate()
                self._worker.wait(500)

        self._worker = _GEEWorker(task_name, params)
        self._worker.finished.connect(self._on_worker_done)
        self._worker.error.connect(self._on_preview_error)
        self._worker.start()

    def _on_preview_error(self, msg):
        self.lbl_preview_loading.setText(f"Error: {msg}")

    def _download_preview_image(self, url, image_id):
        """Download preview image via Qt network (non-blocking)."""
        request = QNetworkRequest(QUrl(url))
        reply = self._nam.get(request)
        reply.finished.connect(
            lambda: self._on_preview_downloaded(reply, image_id))

    def _on_preview_downloaded(self, reply, image_id):
        """Handle downloaded preview image."""
        self.lbl_preview_loading.setText("")

        if image_id != self._current_preview_id:
            reply.deleteLater()
            return  # user clicked another image

        if reply.error():
            err_code = reply.error()
            self.lbl_preview_loading.setText(
                tr("Preview failed (error {c})").format(c=err_code))
            reply.deleteLater()
            return

        data = reply.readAll()
        reply.deleteLater()

        pixmap = QPixmap()
        # Try auto-detect first
        loaded = pixmap.loadFromData(data)
        # If failed, try explicit JPEG format
        if not loaded or pixmap.isNull():
            pixmap = QPixmap()
            loaded = pixmap.loadFromData(data, "JPEG")
        # Try PNG
        if not loaded or pixmap.isNull():
            pixmap = QPixmap()
            loaded = pixmap.loadFromData(data, "PNG")

        if not pixmap.isNull():
            # Scale to fit preview panel
            panel_size = self.lbl_preview.size()
            scaled = pixmap.scaled(
                panel_size.width() - 10,
                panel_size.height() - 10,
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation)
            self.lbl_preview.setPixmap(scaled)
        else:
            self.lbl_preview.setText(tr("Preview not available"))

    # ------------------------------------------------------------------
    # Select / Deselect
    # ------------------------------------------------------------------

    def _on_check_changed(self, item):
        if item.column() != 0:
            return
        n = len(self._get_selected())
        self.lbl_selected.setText(
            tr("{n} selected").format(n=n) if n else "")
        self.btn_download.setEnabled(n > 0)

    def _get_selected(self):
        selected = []
        for row in range(self.tbl_results.rowCount()):
            item = self.tbl_results.item(row, 0)
            if item and item.checkState() == Qt.Checked:
                if row < len(self._search_results):
                    selected.append(self._search_results[row])
        return selected

    def _select_all(self):
        self.tbl_results.blockSignals(True)
        for row in range(self.tbl_results.rowCount()):
            item = self.tbl_results.item(row, 0)
            if item:
                item.setCheckState(Qt.Checked)
        self.tbl_results.blockSignals(False)
        n = self.tbl_results.rowCount()
        self.lbl_selected.setText(tr("{n} selected").format(n=n))
        self.btn_download.setEnabled(n > 0)

    def _select_none(self):
        self.tbl_results.blockSignals(True)
        for row in range(self.tbl_results.rowCount()):
            item = self.tbl_results.item(row, 0)
            if item:
                item.setCheckState(Qt.Unchecked)
        self.tbl_results.blockSignals(False)
        self.lbl_selected.setText("")
        self.btn_download.setEnabled(False)

    # ------------------------------------------------------------------
    # Download
    # ------------------------------------------------------------------

    def _on_download(self):
        selected = self._get_selected()
        if not selected:
            QMessageBox.warning(self, "LVT4U",
                                tr("Select at least one image."))
            return

        try:
            output_dir = self.txt_folder.text().strip()
            if not output_dir:
                raise ValueError(tr("Select an output folder."))
            os.makedirs(output_dir, exist_ok=True)

            # Detect source from selected images
            sources = set(
                r.get("source", "gee")
                for r in self._search_results
                if r["id"] in [s["id"] for s in selected])

            aoi = self._get_aoi_geojson()

            if "copernicus" in sources:
                params = {
                    "username": self._CDSE_USER,
                    "password": self._CDSE_PASS,
                    "items": selected,
                    "output_dir": output_dir,
                    "aoi_geojson": aoi,
                    "bands_key": self.cmb_bands.currentData() or "",
                    "sat_key": self.cmb_satellite.currentData(),
                }
                self._start_worker("cdse_download_batch", params)
            elif "stac" in sources:
                params = {
                    "items": selected,
                    "output_dir": output_dir,
                    "aoi_geojson": aoi,
                    "bands_key": self.cmb_bands.currentData() or "",
                    "sat_key": self.cmb_satellite.currentData(),
                }
                self._start_worker("stac_download_batch", params)
            else:
                aoi = self._get_aoi_geojson()
                fmt = "GeoTIFF" if self.rb_tiff.isChecked() else "PNG"
                params = {
                    "items": selected,
                    "sat_key": self.cmb_satellite.currentData(),
                    "aoi_geojson": aoi,
                    "bands_key": self.cmb_bands.currentData(),
                    "scale": self.cmb_resolution.currentData(),
                    "output_dir": output_dir,
                    "fmt": fmt,
                    "crop_to_polygon": False,
                }
                self._start_worker("download_batch", params)
        except ValueError as e:
            QMessageBox.warning(self, "LVT4U", str(e))
        except Exception as e:
            QMessageBox.critical(self, "LVT4U",
                                 f"{type(e).__name__}: {e}")

    # ------------------------------------------------------------------
    # Worker
    # ------------------------------------------------------------------

    def _start_worker(self, task, kwargs):
        if self._worker and self._worker.isRunning():
            QMessageBox.warning(self, "LVT4U",
                                tr("A task is running. Please wait."))
            return

        # GEE tasks need earthengine-api installed
        if task in ("search", "get_thumb_url", "download_batch",
                    "download_composite"):
            if not self._ensure_deps():
                return

        self._set_busy(True)
        self._worker = _GEEWorker(task, kwargs)
        self._worker.finished.connect(self._on_worker_done)
        self._worker.error.connect(self._on_worker_error)
        self._worker.progress.connect(
            lambda msg: self.lbl_status.setText(msg))
        self._worker.progress_pct.connect(self._update_progress)
        self._worker.start()

    def _ensure_deps(self):
        """Check if satellite engine is available.

        Returns True if ready, False if not yet installed.
        Install is handled silently at plugin startup.
        """
        try:
            import ee  # noqa: F401
            return True
        except ImportError:
            QMessageBox.warning(
                self, "LVT4U",
                tr("Satellite image engine is being installed.\n"
                   "Please wait a moment and try again.\n\n"
                   "If this persists, restart QGIS."))
            return False
        return True

    def _on_worker_done(self, result):
        self._set_busy(False)
        task = result["task"]
        data = result["data"]

        if task == "search":
            # Handle thumb_cache from CDSE results
            if "thumb_cache" in result:
                self._thumb_cache.update(result["thumb_cache"])
            n = len(data)
            self.lbl_status.setText(
                tr("Found {n} images").format(n=n))
            if n > 0:
                self._populate_results(data)

        elif task == "get_thumb_url":
            url = data["url"]
            img_id = data["image_id"]
            bands_key = data.get("bands_key", "")
            cache_key = f"{img_id}|{bands_key}"
            # Cache for future clicks
            self._thumb_cache[cache_key] = url
            # Download the image via Qt NAM (fast, non-blocking)
            self._download_preview_image(url, cache_key)

        elif task == "cdse_preview":
            img_bytes = data.get("img_bytes", b"")
            cache_key = data.get("cache_key", "")
            self.lbl_preview_loading.setText("")

            if cache_key != self._current_preview_id:
                return  # user clicked another image

            pixmap = QPixmap()
            from qgis.PyQt.QtCore import QByteArray
            qba = QByteArray(img_bytes)
            loaded = pixmap.loadFromData(qba)
            if not loaded or pixmap.isNull():
                pixmap = QPixmap()
                loaded = pixmap.loadFromData(qba, "JPEG")
            if not loaded or pixmap.isNull():
                pixmap = QPixmap()
                loaded = pixmap.loadFromData(qba, "PNG")

            if not pixmap.isNull():
                panel_size = self.lbl_preview.size()
                scaled = pixmap.scaled(
                    panel_size.width() - 10,
                    panel_size.height() - 10,
                    Qt.KeepAspectRatio,
                    Qt.SmoothTransformation)
                self.lbl_preview.setPixmap(scaled)
            else:
                self.lbl_preview.setText(tr("Preview not available"))

        elif task == "download_batch":
            paths = data
            total_mb = sum(
                os.path.getsize(p) / (1024 * 1024) for p in paths)
            self.lbl_status.setText(
                tr("Downloaded {n} images ({s:.1f} MB)").format(
                    n=len(paths), s=total_mb))
            for path in paths:
                self._add_to_canvas(path)
            QMessageBox.information(
                self, "LVT4U",
                tr("Downloaded {n} images to:\n{d}").format(
                    n=len(paths), d=self.txt_folder.text()))

        elif task == "download":
            path = data
            size_mb = os.path.getsize(path) / (1024 * 1024)
            self.lbl_status.setText(
                tr("Downloaded: {f} ({s:.1f} MB)").format(
                    f=os.path.basename(path), s=size_mb))
            self._add_to_canvas(path)

    # _start_batch_thumbs removed — caused SSL crash on Windows

    def _on_worker_error(self, msg):
        self._set_busy(False)
        self.lbl_status.setText(f"Error: {msg}")
        QMessageBox.critical(self, "LVT4U", msg)

    def _update_progress(self, pct):
        """Update progress bar with actual percentage."""
        if pct <= 0:
            self.progress.setRange(0, 0)   # indeterminate
        else:
            self.progress.setRange(0, 100)
            self.progress.setValue(pct)

    def _set_busy(self, busy):
        self.progress.setVisible(busy)
        if busy:
            self.progress.setRange(0, 0)   # start indeterminate
            self.progress.setValue(0)
        self.btn_search.setEnabled(not busy)

    def _add_to_canvas(self, path):
        if not self.chk_add_layer.isChecked():
            return
        if not os.path.exists(path):
            return
        name = os.path.splitext(os.path.basename(path))[0]
        rlayer = QgsRasterLayer(path, name)
        if rlayer.isValid():
            QgsProject.instance().addMapLayer(rlayer)

    # ------------------------------------------------------------------
    # Help
    # ------------------------------------------------------------------

    def _on_help(self):
        from ..shared.i18n import current_language

        if current_language() == "vi":
            title = "Huong dan su dung - Tai anh ve tinh"
            html = """
<h2 style='color:#1565C0'>Tai anh ve tinh - Huong dan su dung chi tiet</h2>

<div style='background:#E8F5E9;border-left:4px solid #2E7D32;padding:10px 14px;margin:10px 0;border-radius:4px'>
<b>Muc dich:</b> Cong cu nay phuc vu <b>hoc tap va nghien cuu khoa hoc</b>.<br>
Phi loi nhuan — Khong su dung cho muc dich thuong mai.
</div>

<h3>1. Nguon anh ve tinh (Data Sources)</h3>
<p>Tat ca nguon anh deu la <b>du lieu mo, mien phi</b>, khong can tai khoan hoac dang nhap:</p>
<table border=1 cellpadding=5 style='border-collapse:collapse;width:100%'>
<tr style='background:#E3F2FD'><td><b>Ve tinh</b></td><td><b>Nguon cung cap</b></td><td><b>Do phan giai</b></td><td><b>Chu ky</b></td></tr>
<tr><td>Sentinel-2 L2A</td><td>Copernicus CDSE + AWS COG</td><td>10m</td><td>5 ngay</td></tr>
<tr><td>Landsat 8 / 9</td><td>AWS COG (Element84 STAC)</td><td>30m</td><td>16 ngay</td></tr>
<tr><td>Sentinel-1 SAR</td><td>AWS COG (Element84 STAC)</td><td>10m</td><td>6-12 ngay</td></tr>
<tr><td>MODIS NDVI</td><td>AWS COG (Element84 STAC)</td><td>250m</td><td>16 ngay</td></tr>
</table>
<p style='color:#666;font-size:11px'>
<b>CDSE</b> = Copernicus Data Space Ecosystem (EU) &nbsp;|&nbsp; <b>AWS COG</b> = Amazon Web Services Cloud Optimized GeoTIFF &nbsp;|&nbsp;
<b>STAC</b> = SpatioTemporal Asset Catalog
</p>

<h3>2. Chon ve tinh</h3>
<ul>
<li><b>Sentinel-2 L2A</b> — Anh quang hoc da pho (RGB, NIR, SWIR). Phu hop phan tich thuc vat, nuoc, dat trong.</li>
<li><b>Landsat 8 / 9</b> — Anh quang hoc, du lieu tu 1972. Phu hop phan tich bien dong dai han.</li>
<li><b>Sentinel-1 SAR</b> — Radar song ngan, <b>xuyen may</b>, chup ca ngay va dem. Phu hop giam sat rung mua mua.</li>
<li><b>MODIS NDVI</b> — Chi so thuc vat tong hop 16 ngay. Phu hop giam sat theo mua vu, vung rong.</li>
</ul>

<h3>3. Thiet lap thoi gian & vung quan tam</h3>
<ul>
<li><b>Khoang thoi gian:</b> Nut nhanh 30d, 90d, 6m, 1y hoac tu chon ngay bat dau / ket thuc</li>
<li><b>Vung (AOI):</b></li>
<ul>
<li><b>Layer</b>: Chon 1 lop polygon trong QGIS — he thong tu dong chuyen doi CRS sang WGS84</li>
<li><b>Map View</b>: Su dung khung nhin hien tai tren ban do</li>
</ul>
<li><b>Cloud max %:</b> Keo thanh truot de gioi han do may toi da (chi ap dung cho anh quang hoc)</li>
</ul>

<h3>4. Tim kiem & xem truoc</h3>
<ul>
<li>Bam nut <b>Tim kiem</b> — he thong truy van kho du lieu ve tinh qua STAC API</li>
<li><b>Click vao 1 hang</b> trong bang ket qua de xem truoc anh thu nho (thumbnail)</li>
<li>Mau chi so may:</li>
<ul>
<li style='color:#2E7D32'><b>Xanh</b> &lt;10% — Rat tot</li>
<li style='color:#F9A825'><b>Vang</b> &lt;30% — Chap nhan duoc</li>
<li style='color:#E65100'><b>Cam</b> &lt;60% — Nhieu may</li>
<li style='color:#C62828'><b>Do</b> &gt;60% — Qua nhieu may</li>
</ul>
</ul>

<h3>5. Tai anh ve</h3>
<ul>
<li><b>Tick checkbox</b> o cot dau tien cho nhung anh muon tai</li>
<li>Chon dinh dang:</li>
<ul>
<li><b>GeoTIFF</b> — Du lieu goc co toa do (mo duoc trong QGIS, ArcGIS)</li>
<li><b>PNG</b> — Anh hinh (de xem, trinh bay)</li>
</ul>
<li>Bam nut <b>Tai anh da chon</b></li>
<li>Tick <b>Them vao ban do</b> de tu dong load anh vao QGIS sau khi tai</li>
</ul>

<h3>6. To hop kenh anh (Band Composites)</h3>
<table border=1 cellpadding=5 style='border-collapse:collapse;width:100%'>
<tr style='background:#E3F2FD'><td><b>Ten</b></td><td><b>Mo ta</b></td><td><b>Kenh</b></td><td><b>Ung dung</b></td></tr>
<tr><td><b>True Color</b></td><td>Mau tu nhien</td><td>B4-B3-B2</td><td>Quan sat tong quat</td></tr>
<tr><td><b>False Color</b></td><td>Thuc vat = do</td><td>B8-B4-B3</td><td>Phan biet rung / dat trong</td></tr>
<tr><td><b>NDVI</b></td><td>Chi so thuc vat</td><td>(B8-B4) / (B8+B4)</td><td>Suc khoe thuc vat (-1 den 1)</td></tr>
<tr><td><b>NDWI</b></td><td>Chi so nuoc</td><td>(B3-B8) / (B3+B8)</td><td>Phat hien mat nuoc</td></tr>
<tr><td><b>SWIR</b></td><td>Hong ngoai song ngan</td><td>B12-B8A-B4</td><td>Do am dat, chay rung</td></tr>
</table>

<hr>
<h3 style='color:#2E7D32'>Loc Vu Trung</h3>
<p style='margin:2px 0'><b>FSC, Silviculture &amp; Digital Transformation Expert</b></p>
<table cellpadding=3 style='margin-top:8px'>
<tr><td>📱</td><td><b>Zalo:</b> 0913 191 178</td></tr>
<tr><td>🌐</td><td><b>Website:</b> <a href='https://locvutrung.lvtcenter.it.com'>locvutrung.lvtcenter.it.com</a></td></tr>
<tr><td>🎬</td><td><b>YouTube:</b> <a href='https://youtube.com/@locvutrung'>youtube.com/@locvutrung</a></td></tr>
</table>
<p style='margin-top:10px'><b>Expertise:</b></p>
<ul style='margin-top:2px'>
<li>FSC / CoC Forest Certification</li>
<li>EU Deforestation Regulation (EUDR)</li>
<li>Webapp / Appsheet Applications</li>
<li>QGIS GIS / Data Management</li>
<li>Silviculture</li>
</ul>
<p style='color:#888; margin-top:12px'>LVT4U Satellite Module v2.6.1</p>
"""
        else:
            title = "Help — Satellite Image Downloader"
            html = """
<h2 style='color:#1565C0'>Satellite Image Downloader — Detailed User Guide</h2>

<div style='background:#E8F5E9;border-left:4px solid #2E7D32;padding:10px 14px;margin:10px 0;border-radius:4px'>
<b>Purpose:</b> This tool is for <b>educational and scientific research</b> purposes only.<br>
Non-profit — Not for commercial use.
</div>

<h3>1. Data Sources</h3>
<p>All imagery is from <b>open, free</b> catalogs — no account or login required:</p>
<table border=1 cellpadding=5 style='border-collapse:collapse;width:100%'>
<tr style='background:#E3F2FD'><td><b>Satellite</b></td><td><b>Provider</b></td><td><b>Resolution</b></td><td><b>Revisit</b></td></tr>
<tr><td>Sentinel-2 L2A</td><td>Copernicus CDSE + AWS COG</td><td>10m</td><td>5 days</td></tr>
<tr><td>Landsat 8 / 9</td><td>AWS COG (Element84 STAC)</td><td>30m</td><td>16 days</td></tr>
<tr><td>Sentinel-1 SAR</td><td>AWS COG (Element84 STAC)</td><td>10m</td><td>6-12 days</td></tr>
<tr><td>MODIS NDVI</td><td>AWS COG (Element84 STAC)</td><td>250m</td><td>16 days</td></tr>
</table>
<p style='color:#666;font-size:11px'>
<b>CDSE</b> = Copernicus Data Space Ecosystem (EU) &nbsp;|&nbsp; <b>AWS COG</b> = Amazon Web Services Cloud Optimized GeoTIFF &nbsp;|&nbsp;
<b>STAC</b> = SpatioTemporal Asset Catalog
</p>

<h3>2. Select Satellite</h3>
<ul>
<li><b>Sentinel-2 L2A</b> — Multispectral optical (RGB, NIR, SWIR). Best for vegetation, water, and land-use analysis.</li>
<li><b>Landsat 8 / 9</b> — Optical imagery, archive since 1972. Best for long-term change detection.</li>
<li><b>Sentinel-1 SAR</b> — Microwave radar, <b>cloud-penetrating</b>, day & night. Best for monsoon-season forest monitoring.</li>
<li><b>MODIS NDVI</b> — 16-day vegetation composites. Best for seasonal / regional-scale monitoring.</li>
</ul>

<h3>3. Set Time Range & Area of Interest</h3>
<ul>
<li><b>Time range:</b> Quick buttons (30d, 90d, 6m, 1y) or custom start / end dates</li>
<li><b>Area (AOI):</b></li>
<ul>
<li><b>Layer</b>: Select a polygon layer in QGIS — CRS is automatically transformed to WGS84</li>
<li><b>Map View</b>: Use the current map canvas extent</li>
</ul>
<li><b>Cloud max %:</b> Drag slider to limit maximum cloud cover (optical imagery only)</li>
</ul>

<h3>4. Search & Preview</h3>
<ul>
<li>Click <b>Search</b> — queries the satellite catalog via STAC API</li>
<li><b>Click a row</b> in the results table to preview the thumbnail</li>
<li>Cloud coverage colors:</li>
<ul>
<li style='color:#2E7D32'><b>Green</b> &lt;10% — Excellent</li>
<li style='color:#F9A825'><b>Yellow</b> &lt;30% — Acceptable</li>
<li style='color:#E65100'><b>Orange</b> &lt;60% — Cloudy</li>
<li style='color:#C62828'><b>Red</b> &gt;60% — Too cloudy</li>
</ul>
</ul>

<h3>5. Download</h3>
<ul>
<li><b>Tick checkboxes</b> in the first column for images you want</li>
<li>Choose output format:</li>
<ul>
<li><b>GeoTIFF</b> — Georeferenced data (opens in QGIS, ArcGIS)</li>
<li><b>PNG</b> — Visual image (for viewing, presentations)</li>
</ul>
<li>Click <b>Download Selected</b></li>
<li>Tick <b>Add to map</b> to auto-load downloaded images into QGIS</li>
</ul>

<h3>6. Band Composites</h3>
<table border=1 cellpadding=5 style='border-collapse:collapse;width:100%'>
<tr style='background:#E3F2FD'><td><b>Name</b></td><td><b>Description</b></td><td><b>Bands</b></td><td><b>Use case</b></td></tr>
<tr><td><b>True Color</b></td><td>Natural color</td><td>B4-B3-B2</td><td>General observation</td></tr>
<tr><td><b>False Color</b></td><td>Vegetation = red</td><td>B8-B4-B3</td><td>Forest vs bare land</td></tr>
<tr><td><b>NDVI</b></td><td>Vegetation index</td><td>(B8-B4) / (B8+B4)</td><td>Plant health (-1 to 1)</td></tr>
<tr><td><b>NDWI</b></td><td>Water index</td><td>(B3-B8) / (B3+B8)</td><td>Water body detection</td></tr>
<tr><td><b>SWIR</b></td><td>Short-wave infrared</td><td>B12-B8A-B4</td><td>Soil moisture, fire scars</td></tr>
</table>

<hr>
<h3 style='color:#2E7D32'>Loc Vu Trung</h3>
<p style='margin:2px 0'><b>FSC, Silviculture &amp; Digital Transformation Expert</b></p>
<table cellpadding=3 style='margin-top:8px'>
<tr><td>📱</td><td><b>Zalo:</b> 0913 191 178</td></tr>
<tr><td>🌐</td><td><b>Website:</b> <a href='https://locvutrung.lvtcenter.it.com'>locvutrung.lvtcenter.it.com</a></td></tr>
<tr><td>🎬</td><td><b>YouTube:</b> <a href='https://youtube.com/@locvutrung'>youtube.com/@locvutrung</a></td></tr>
</table>
<p style='margin-top:10px'><b>Expertise:</b></p>
<ul style='margin-top:2px'>
<li>FSC / CoC Forest Certification</li>
<li>EU Deforestation Regulation (EUDR)</li>
<li>Webapp / Appsheet Applications</li>
<li>QGIS GIS / Data Management</li>
<li>Silviculture</li>
</ul>
<p style='color:#888; margin-top:12px'>LVT4U Satellite Module v2.6.1</p>
"""
        from qgis.PyQt.QtWidgets import QTextBrowser
        dlg = QDialog(self)
        dlg.setWindowTitle(title)
        dlg.resize(520, 550)
        lay = QVBoxLayout(dlg)
        browser = QTextBrowser()
        browser.setHtml(html)
        browser.setOpenExternalLinks(True)
        lay.addWidget(browser)
        btn = QPushButton("OK")
        btn.clicked.connect(dlg.close)
        lay.addWidget(btn)
        dlg.exec_()
