# -*- coding: utf-8 -*-
"""
/***************************************************************************
 s2CloudMask
                                 A QGIS plugin
 This plugin helps to mask cloud and shadow in Sentinel-2 imagery, provide cloudless scene or mosaic 
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2025-06-23
        git sha              : $Format:%H$
        copyright            : (C) 2025 by Thanh@JAFTA
        email                : thanh@vnforest.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import qgis
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, QDate, QTimer, QT_VERSION_STR
from qgis.PyQt.QtGui import QIcon, QTextCharFormat, QColor, QFont
from qgis.PyQt.QtWidgets import QAction, QMessageBox, QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView, QLabel, QProgressBar, QApplication
from qgis.core import (QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsProcessingFeedback, QgsMessageLog, Qgis)
from .resources import *
from .s2cloudmask_dockwidget import s2CloudMaskDockWidget
from . import install_dependencies

import os.path, datetime, requests, json, processing
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict
from osgeo import gdal 

from .downloadBands import downloadL1CBands
from .mosaic import SimpleSentinel2Mosaic

# Safe imports for optional dependencies
try:
    import pystac_client
    PYSTAC_AVAILABLE = True
except ImportError:
    PYSTAC_AVAILABLE = False
    QgsMessageLog.logMessage("pystac_client not available", "s2CloudMask", Qgis.Warning)

try:
    from shapely.geometry import Polygon, shape
    SHAPELY_AVAILABLE = True
except ImportError:
    SHAPELY_AVAILABLE = False
    QgsMessageLog.logMessage("shapely not available", "s2CloudMask", Qgis.Warning)

try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False
    QgsMessageLog.logMessage("numpy not available", "s2CloudMask", Qgis.Warning)

try:
    import cv2
    OPENCV_AVAILABLE = True
except ImportError:
    OPENCV_AVAILABLE = False
    QgsMessageLog.logMessage("opencv-python-headless not available", "s2CloudMask", Qgis.Warning)

try:
    import s2cloudless
    S2CLOUDLESS_AVAILABLE = True
except ImportError:
    S2CLOUDLESS_AVAILABLE = False
    QgsMessageLog.logMessage("s2cloudless not available", "s2CloudMask", Qgis.Warning)

documents_folder = os.path.join(os.environ['USERPROFILE'], 'Documents')

class DownloadProgressDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Processing Sentinel-2 Data")
        self.setModal(True)
        self.resize(400, 120)
        
        layout = QVBoxLayout()
        
        self.status_label = QLabel("Initializing...")
        layout.addWidget(self.status_label)
        
        self.progress_bar = QProgressBar()
        layout.addWidget(self.progress_bar)
        
        self.detail_label = QLabel("")
        layout.addWidget(self.detail_label)
        
        # Add cancel button
        self.cancel_button = QPushButton("Cancel")
        self.cancel_button.clicked.connect(self.reject)
        layout.addWidget(self.cancel_button)
        
        self.setLayout(layout)
        self.cancelled = False
    
    def set_range(self, min_val, max_val):
        self.progress_bar.setRange(min_val, max_val)
    
    def set_value(self, value):
        self.progress_bar.setValue(value)
        QApplication.processEvents()
    
    def set_status(self, status):
        self.status_label.setText(status)
        QApplication.processEvents()
    
    def set_detail(self, detail):
        self.detail_label.setText(detail)
        QApplication.processEvents()
    
    def is_cancelled(self):
        return self.cancelled
    
    def reject(self):
        self.cancelled = True
        super().reject()

class s2CloudMask:
    def __init__(self, iface):
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.cdseId = None 
        self.cdseSecret = None 
        self.qtVersion = int(QT_VERSION_STR.split('.')[0])
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(self.plugin_dir, 'i18n', 's2CloudMask_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Cloud Mask Sentinel-2')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u's2CloudMask')
        self.toolbar.setObjectName(u's2CloudMask')
        self.STAC_ESA = "https://stac.dataspace.copernicus.eu/v1"
        #self.eeBox = None
        self.pluginIsActive = True
        self.qToday = QDate.currentDate()
        self.selectedDate = QDate.currentDate()
        self.qLastMonth = QDate.currentDate().addDays(-31)
        self.is_selecting_start = True 
        self.start_date = None 
        self.end_date = None 
        # Set up formatting for different date states
        self.start_format = QTextCharFormat()
        self.start_format.setBackground(QColor(100, 255, 100))  # Light green for start date
        
        self.end_format = QTextCharFormat()
        self.end_format.setBackground(QColor(255, 100, 100))    # Light red for end date
        
        self.range_format = QTextCharFormat()
        self.range_format.setBackground(QColor(100, 150, 255, 77))  # Light blue for range        

        # Add caching variables
        self.cached_date_minmax = None
        self.cached_eeBbox = None
        self.cached_image_dates = []
        self.cached_valid_dates = []
        self.image_info = []
        self.products_list = [] 
        self.access_token = None
        self.dependencies_checked = False
        self.qgisVersion = qgis.core.Qgis.QGIS_VERSION_INT

    def check_dependencies(self):
        """Check and install dependencies if needed"""
        if self.dependencies_checked:
            return True
        
        # Configure import paths first
        install_dependencies.configure_import_paths()
        
        # Check if dependencies are missing
        missing_deps = install_dependencies.get_missing_dependencies()
        if missing_deps:
            # Show message about missing dependencies
            package_list = "\n".join([f"- {pkg[0]}" for pkg in missing_deps])
            if self.qtVersion == 5: 
                reply = QMessageBox.question(
                    self.iface.mainWindow(),
                    "Missing Dependencies",
                    f"s2CloudMask requires the following Python packages:\n\n{package_list}\n\n"
                    "Would you like to install them now?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.Yes
                )
                
                if reply == QMessageBox.Yes:
                    success = install_dependencies.check_and_install_dependencies(self.iface.mainWindow())
                    if not success:
                        QMessageBox.critical(
                            self.iface.mainWindow(),
                            "Installation Failed",
                            "Failed to install required dependencies. The plugin may not work correctly."
                        )
                        return False
                    else:
                        # Re-import modules after installation
                        self._reimport_dependencies()
                else:
                    QMessageBox.warning(
                        self.iface.mainWindow(),
                        "Dependencies Required",
                        "The plugin requires these dependencies to function properly. "
                        "Some features may not work without them."
                    )
                    return False
            elif self.qtVersion == 6: 
                reply = QMessageBox.question(
                    self.iface.mainWindow(),
                    "Missing Dependencies",
                    f"s2CloudMask requires the following Python packages:\n\n{package_list}\n\n"
                    "Would you like to install them now?",
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                    QMessageBox.StandardButton.Yes
                )

                if reply == QMessageBox.StandardButton.Yes:
                    success = install_dependencies.check_and_install_dependencies(self.iface.mainWindow())
                    if not success:
                        QMessageBox.critical(
                            self.iface.mainWindow(),
                            "Installation Failed",
                            "Failed to install required dependencies. The plugin may not work correctly."
                        )
                        return False
                    else:
                        # Re-import modules after installation
                        self._reimport_dependencies()
                else:
                    QMessageBox.warning(
                        self.iface.mainWindow(),
                        "Dependencies Required",
                        "The plugin requires these dependencies to function properly. "
                        "Some features may not work without them."
                    )
                    return False
        
        self.dependencies_checked = True
        return True
    
    def _reimport_dependencies(self):
        """Re-import dependencies after installation"""
        global PYSTAC_AVAILABLE, SHAPELY_AVAILABLE, NUMPY_AVAILABLE, OPENCV_AVAILABLE, S2CLOUDLESS_AVAILABLE
        global pystac_client, Polygon, shape, np, cv2, s2cloudless
        
        try:
            import pystac_client
            PYSTAC_AVAILABLE = True
            QgsMessageLog.logMessage("pystac_client imported successfully", "s2CloudMask", Qgis.Info)
        except ImportError:
            PYSTAC_AVAILABLE = False
        
        try:
            from shapely.geometry import Polygon, shape
            SHAPELY_AVAILABLE = True
            QgsMessageLog.logMessage("shapely imported successfully", "s2CloudMask", Qgis.Info)
        except ImportError:
            SHAPELY_AVAILABLE = False
        
        try:
            import numpy as np
            NUMPY_AVAILABLE = True
            QgsMessageLog.logMessage(f"numpy {np.__version__} imported successfully", "s2CloudMask", Qgis.Info)
        except ImportError:
            NUMPY_AVAILABLE = False
        
        try:
            import cv2
            OPENCV_AVAILABLE = True
            QgsMessageLog.logMessage("opencv-python-headless imported successfully", "s2CloudMask", Qgis.Info)
        except ImportError:
            OPENCV_AVAILABLE = False
        
        try:
            import s2cloudless
            S2CLOUDLESS_AVAILABLE = True
            QgsMessageLog.logMessage("s2cloudless imported successfully", "s2CloudMask", Qgis.Info)
        except ImportError:
            S2CLOUDLESS_AVAILABLE = False
    
    def _check_required_imports(self):
        """Check if required imports are available for specific operations"""
        missing = []
        
        if not PYSTAC_AVAILABLE:
            missing.append("pystac_client")
        if not SHAPELY_AVAILABLE:
            missing.append("shapely")
        if not NUMPY_AVAILABLE:
            missing.append("numpy")
        if not OPENCV_AVAILABLE:
            missing.append("opencv-python-headless")
        if not S2CLOUDLESS_AVAILABLE:
            missing.append("s2cloudless")
        
        if missing:
            QMessageBox.warning(
                self.iface.mainWindow(),
                "Missing Dependencies",
                f"The following required packages are not available:\n\n" +
                "\n".join([f"- {pkg}" for pkg in missing]) +
                "\n\nPlease install them using the dependency installer."
            )
            return False
        
        return True

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/plugins/s2cloudmask/icon.png'
        self.add_action(
            icon_path,
            text = self.tr(u'Cloud Mask Sentinel-2'),
            callback = self.run,
            parent = self.iface.mainWindow())

    def showWaitDialog(self, parent=None):
        dialog = QDialog(parent)
        dialog.setWindowTitle("Processing")
        layout = QVBoxLayout()
        label = QLabel()
        label.setText("Please wait...")
        label.setWordWrap(True)
        layout.addWidget(label)
        
        progress = QProgressBar()
        progress.setRange(0, 0)  # Busy indicator
        layout.addWidget(progress)
        
        dialog.setLayout(layout)
        dialog.setModal(True)
        dialog.setFixedSize(300, 100)
        return dialog

    def show_creds(self): 
        self.dockwidget.labelId.setVisible(True)
        self.dockwidget.labelSec.setVisible(True)
        self.dockwidget.idLineEdit.setVisible(True)
        self.dockwidget.secLineEdit.setVisible(True)
        self.dockwidget.labelGuide.setVisible(True)
        self.dockwidget.labelGuide.setText("""Don't have a CDSE account? Register a free account at <b> <a href = "https://dataspace.copernicus.eu/">HERE</a></b>""")
        if self.qtVersion == 6: 
            self.dockwidget.labelGuide.setTextFormat(Qt.TextFormat.RichText)
            self.dockwidget.labelGuide.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
        elif self.qtVersion == 5: 
            self.dockwidget.labelGuide.setTextFormat(Qt.RichText)
            self.dockwidget.labelGuide.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.dockwidget.labelGuide.setOpenExternalLinks(True)

    def hide_creds(self):
        self.dockwidget.labelId.setVisible(False)
        self.dockwidget.labelSec.setVisible(False)
        self.dockwidget.idLineEdit.setVisible(False)
        self.dockwidget.secLineEdit.setVisible(False)
        self.dockwidget.labelGuide.setVisible(False)

    def check_creds(self): 
        plugin_dir = os.path.dirname(__file__)
        creds_path = os.path.join(plugin_dir, '_creds.json')
        if not os.path.exists(creds_path):
            cdseId = ''
            cdseSecret = ''
        else: 
            with open(creds_path, 'r') as ff: 
                data = json.load(ff)
                credentials = data.get('credentials')
                if credentials and 'cdseId' in credentials and 'cdseSecrets' in credentials:
                    cdseId = data.get('credentials').get('cdseId')
                    cdseSecret = data.get('credentials').get('cdseSecrets')
                else:
                    cdseId = ''
                    cdseSecret = ''
        return [cdseId, cdseSecret]

    def _connect_buttons_for_scene_mode(self):
        """Helper method to connect buttons for single scene mode"""
        try:
            self.dockwidget.previewButton.clicked.disconnect()
            self.dockwidget.acceptButton.clicked.disconnect()
            self.dockwidget.cancelButton.clicked.disconnect()
        except TypeError:
            pass
        
        self.dockwidget.previewButton.clicked.connect(self.previewImage)
        self.dockwidget.acceptButton.clicked.connect(self.cloudMasking)
        self.dockwidget.cancelButton.clicked.connect(self.onCancel)

    def _connect_buttons_for_mosaic_mode(self):
        """Helper method to connect buttons for mosaic mode"""
        try:
            self.dockwidget.previewButton.clicked.disconnect()
            self.dockwidget.acceptButton.clicked.disconnect()
        except TypeError:
            pass
        
        self.dockwidget.previewButton.clicked.connect(self.previewImageList)
        self.dockwidget.acceptButton.clicked.connect(self.cloudMosaic)

    def run(self):
        """Run method that loads and starts the plugin"""
       # Check dependencies first
        if not self.check_dependencies():
            return
        
        # Check if all required imports are available
        if not self._check_required_imports():
            return

        self.dockwidget = s2CloudMaskDockWidget()
        cdse_id, cdse_secret = self.check_creds()
        if (not cdse_id or not cdse_secret) or (cdse_id == '' or cdse_secret == ''): 
            self.show_creds()
        else: 
            self.hide_creds()
            self.cdseId = cdse_id
            self.cdseSecret = cdse_secret        

        self.dockwidget.labelDateFrom.setVisible(False)
        self.dockwidget.dateFromEdit.setVisible(False)
        self.dockwidget.labelDateTo.setVisible(False)
        self.dockwidget.dateToEdit.setVisible(False)
        self.dockwidget.calendarWidget.setVisible(False)
        self.dockwidget.previewButton.setEnabled(False)
        self.dockwidget.acceptButton.setEnabled(False)
        self.dockwidget.sceneComboBox.setCurrentIndex(0)
        self.dockwidget.sceneComboBox.currentIndexChanged.connect(self.onSceneSelection)
        try:
            self.dockwidget.cancelButton.clicked.disconnect()
        except TypeError:
            pass
        self.dockwidget.cancelButton.clicked.connect(self.onCancel)
        self.dockwidget.closingPlugin.connect(self.onClosePlugin)
        self.dockwidget.calendarWidget.currentPageChanged.connect(self.onCalendarChanged)
        self.dockwidget.calendarWidget.clicked.connect(self.onDateSelected)
        self.dockwidget.show()

    def getMonthlyRange(self, year, month):
        first_day_of_month = QDate(year, month, 1)
        first_day_weekday = first_day_of_month.dayOfWeek()
        min_date = first_day_of_month.addDays(-first_day_weekday)
        max_date = min_date.addDays(41)
        return [min_date, max_date]

    def onCalendarChanged(self): 
        year = self.dockwidget.calendarWidget.yearShown()
        month = self.dockwidget.calendarWidget.monthShown()
        day = self.selectedDate.day()
        date_range = self.getMonthlyRange(year, month)
        selectedMethod = self.dockwidget.sceneComboBox.currentIndex()
        if selectedMethod == 2: 
            self.dockwidget.dateFromEdit.setDate(date_range[0])
            self.dockwidget.dateToEdit.setDate(date_range[1])
            self.setupCalendarMosaic(date_range)
        if selectedMethod == 1: 
            self.dockwidget.dateFromEdit.setDate(QDate(year, month, day))
            self.setupCalendarScene(date_range)
        self.start_date = date_range[0]
        self.end_date = date_range[1]

    def onDateSelected(self, date):
        selectedMethod = self.dockwidget.sceneComboBox.currentIndex()
        if selectedMethod == 1: # Single Scene
            if self.qtVersion == 5: 
                self.dockwidget.calendarWidget.setContextMenuPolicy(Qt.NoContextMenu)
            elif self.qtVersion == 6: 
                self.dockwidget.calendarWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
            if date in self.valid_dates:
                self.selectedDate = date
                self.dockwidget.previewButton.setEnabled(True)
                self.dockwidget.acceptButton.setEnabled(True)
                self.dockwidget.dateFromEdit.setDate(QDate(date))
                try:
                    self.dockwidget.previewButton.clicked.disconnect()
                    self.dockwidget.acceptButton.clicked.disconnect()
                    self.dockwidget.cancelButton.clicked.disconnect()
                except TypeError:
                    pass
                self.dockwidget.previewButton.clicked.connect(self.previewImage)
                self.dockwidget.acceptButton.clicked.connect(self.cloudMasking)
                self.dockwidget.cancelButton.clicked.connect(self.onCancel)
            else:
                QMessageBox.warning(None, u'Date without imagery data', u'You have clicked on %s but the Sentinel-2 (L1C) image is not available over the current area of interest. Feel free to select a date with light green color background!' %(date.toString('yyyy-MM-dd')))
                self.dockwidget.cancelButton.clicked.connect(self.onCancel)
        if selectedMethod == 2: # Mosaic
            try:
                self.dockwidget.calendarWidget.customContextMenuRequested.disconnect()
            except TypeError:
                pass   
            if self.qtVersion == 5: 
                self.dockwidget.calendarWidget.setContextMenuPolicy(Qt.CustomContextMenu)
            elif self.qtVersion == 6: 
                self.dockwidget.calendarWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
            self.dockwidget.calendarWidget.customContextMenuRequested.connect(self.on_right_click)
            try:
                self.dockwidget.acceptButton.clicked.disconnect()
            except TypeError:
                pass
            self.dockwidget.acceptButton.clicked.connect(self.cloudMosaic)
            """Handle date clicks for interactive range selection"""
            if self.is_selecting_start:
                # First click - select start date
                self.start_date = date
                self.end_date = None #self.qToday
                self.is_selecting_start = False
                self.highlight_dates()
                self.update_range_label()
            else:
                # Second click - select end date
                self.end_date = date
                if self.end_date < self.start_date:
                    self.start_date, self.end_date = self.end_date, self.start_date
                self.is_selecting_start = False
                self.highlight_dates()
                self.update_range_label()
                date_minmax = [self.start_date, self.end_date]
                #self.cached_date_minmax = date_minmax.copy() if hasattr(date_minmax, 'copy') else list(date_minmax)
                print ('Date Min Max', date_minmax)
                self.setupCalendarMosaic(date_minmax)

    def searchS2ByDateExtent(self, start_date, end_date, bbox):
        date_range = f"{start_date}/{end_date}"
        contained_images = []
        currentBbox = bbox 
        CATALOG = pystac_client.Client.open(self.STAC_ESA)
        # Do a search
        SEARCH = CATALOG.search(
            collections = ["sentinel-2-l1c"], 
            datetime = date_range,  
            bbox = currentBbox
        )
        polygon = Polygon([(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3]), (bbox[0], bbox[1])])
        containing_items = []
        orbit_groups = defaultdict(list)
        searchs = SEARCH.get_all_items()
        for rs in searchs: 
            if shape(rs.geometry).contains(polygon): 
                containing_items.append(rs)
        
        for ci in containing_items: 
            item_parts = ci.id.split('_')
            satellite = item_parts[0] 
            relative_orbit = item_parts[4]
            img_date = item_parts[6]
            orbit_key = f"{satellite}_{relative_orbit}_{img_date}"
            orbit_groups[orbit_key].append(ci)

        # Select one item per orbit group
        deduplicated_items = []
        for orbit_key, items_group in orbit_groups.items():
            if len(items_group) == 1:
                deduplicated_items.append(items_group[0])
            else:
                selected_item = self._select_best_item(items_group, bbox)
                deduplicated_items.append(selected_item)
        deduplicated_items.sort(key=lambda x: x.datetime, reverse=True)
        return deduplicated_items

    def getQgisExtent(self, action = None):
        canvas = self.iface.mapCanvas()
        ext = canvas.extent()
        # Check scale limit
        if canvas.scale() > 25100:
            if not action:
                QMessageBox.warning(None, "Too big extent", "When searching Sentinel-2 data to be processed offline, it could be ideally to limit the Scale to <= 25000 although you can preview the whole scene after click 'Preview' button.")
                self.dockwidget.sceneComboBox.setCurrentIndex(0)
                return None
        source_crs = canvas.mapSettings().destinationCrs()
        target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        if source_crs != target_crs:
            transform = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
            extent = transform.transformBoundingBox(ext)
        else:
            extent = ext
        
        bbox = [extent.xMinimum(), extent.yMinimum(), extent.xMaximum(), extent.yMaximum()]
        return bbox

    def getProductIdByName(self, product_name):
        headers = {}
        search_url = f"https://catalogue.dataspace.copernicus.eu/odata/v1/Products?$filter=Name eq '{product_name}.SAFE'"
        response = requests.get(search_url, headers=headers)
        if response.status_code != 200:
            return None
        response_data = response.json()
        products = response_data.get('value', [])
        
        if not products:
            return None
        return products[0].get('Id')
        
    def loadPreview(self, tci_filepath, product_name):
        try:
            layer_name = f"TCI_Preview_{product_name.replace('.SAFE', '')}"
            raster_layer = QgsRasterLayer(tci_filepath, layer_name)
            if raster_layer.isValid():
                QgsProject.instance().addMapLayer(raster_layer)
                return True
            else:
                return False
        except Exception as e:
            return False

    def previewImage(self): 
        qdate = self.selectedDate
        product_name = None 
        process_dir = os.path.join(documents_folder, 'Sentinel-2')
        waitDialog = self.showWaitDialog(self.iface.mainWindow())
        waitDialog.show()
        QApplication.processEvents()
        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()

        for img in self.image_info: 
            pdate = img[1]
            if pdate == qdate.toString("yyyy-MM-dd"):
                product_name = img[3]
                break 
        granule_path = os.path.join(process_dir, f"{product_name}.SAFE", 'GRANULE')
        tci_filepath = None 
        for dirpath, dirnames, filenames in os.walk(granule_path): 
            for filename in filenames:
                if filename.endswith('TCI.jp2'):
                    tci_filepath = os.path.join(dirpath, filename)
        if not tci_filepath:
            downloaded_tci = downloadL1CBands(self.cdseId, self.cdseSecret, product_name, process_dir, "TCI")
            if not downloaded_tci: 
                return 
            tci_filepath = downloaded_tci[0]['local_path']

        self.loadPreview(tci_filepath, product_name)
        self.writeAndClose(self.cdseId, self.cdseSecret)
        waitDialog.close()

    def previewImageList(self):
        waitDialog = self.showWaitDialog(self.iface.mainWindow())
        waitDialog.show()
        QApplication.processEvents()  
        lowest_cloud = min(self.image_info, key=lambda x: x[2])
        product_name_lowest = lowest_cloud[3]
        process_dir = os.path.join(documents_folder, 'Sentinel-2')
        lowest_path = os.path.join(process_dir, f"{product_name_lowest}.SAFE", 'GRANULE')

        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()

        tci_filepath = None 
        for dirpath, dirnames, filenames in os.walk(lowest_path): 
            for filename in filenames:
                if filename.endswith('TCI.jp2'):
                    tci_filepath = os.path.join(dirpath, filename)
        if not tci_filepath:
            downloaded_tci = downloadL1CBands(self.cdseId, self.cdseSecret, product_name_lowest, process_dir, "TCI")
            if not downloaded_tci: 
                return 
            tci_filepath = downloaded_tci[0]['local_path']
        self.loadPreview(tci_filepath, product_name_lowest)
        
        newDialog = QDialog()
        newDialog.setWindowTitle("Sentinel-2 Images Available")
        newDialog.setMinimumSize(400, 300)
        layout = QVBoxLayout()
        table = QTableWidget()
        table.setRowCount(len(self.image_info))
        table.setColumnCount(3)
        table.setHorizontalHeaderLabels(["Tile ID", "Date", "Cloud Cover (%)"])
        for row, (tile_id, date, cloud_cover, p_name) in enumerate(self.image_info):
            if self.qtVersion == 5: 
                tile_item = QTableWidgetItem(tile_id)
                tile_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  
                table.setItem(row, 0, tile_item)
                date_item = QTableWidgetItem(date)
                date_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  
                table.setItem(row, 1, date_item)

                cloud_item = QTableWidgetItem(str(cloud_cover))
                cloud_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 
                cloud_item.setTextAlignment(Qt.AlignCenter)
                table.setItem(row, 2, cloud_item)

            elif self.qtVersion == 6: 
                tile_item = QTableWidgetItem(tile_id)
                tile_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)  
                table.setItem(row, 0, tile_item)
                date_item = QTableWidgetItem(date)
                date_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)  
                table.setItem(row, 1, date_item)

                cloud_item = QTableWidgetItem(str(cloud_cover))
                cloud_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled) 
                cloud_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                table.setItem(row, 2, cloud_item)                
        if self.qtVersion == 5: 
            table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
            table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
        elif self.qtVersion == 6: 
            table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
            table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
            
        table.setSortingEnabled(True)
        close_button = QPushButton("Close")
        close_button.clicked.connect(newDialog.accept)
        layout.addWidget(table)
        layout.addWidget(close_button)
        newDialog.setLayout(layout)
        waitDialog.close()
        if self.qtVersion == 5:
            newDialog.exec_() 
        elif self.qtVersion == 6:
            newDialog.exec()
        self.writeAndClose(self.cdseId, self.cdseSecret)   

    def highlight_dates(self):
        self.dockwidget.calendarWidget.setDateTextFormat(QDate(), QTextCharFormat())
        if self.start_date and self.end_date:
            current_date = self.start_date
            while current_date <= self.end_date:
                if current_date == self.start_date:
                    self.dockwidget.calendarWidget.setDateTextFormat(current_date, self.start_format)
                elif current_date == self.end_date:
                    self.dockwidget.calendarWidget.setDateTextFormat(current_date, self.end_format)
                else:
                    self.dockwidget.calendarWidget.setDateTextFormat(current_date, self.range_format)
                current_date = current_date.addDays(1)
        elif self.start_date:
            self.dockwidget.calendarWidget.setDateTextFormat(self.start_date, self.start_format)

    def update_range_label(self):
        if self.start_date and self.end_date:
            start_str = self.start_date.toString("MMM dd, yyyy")
            end_str = self.end_date.toString("MMM dd, yyyy")
            days_count = self.start_date.daysTo(self.end_date) + 1
            self.dockwidget.dateFromEdit.setDate(QDate(self.start_date))
            self.dockwidget.dateToEdit.setDate(QDate(self.end_date))
        elif self.start_date:
            start_str = self.start_date.toString("MMM dd, yyyy")
            self.dockwidget.dateFromEdit.setDate(QDate(self.start_date))
        else:
            self.dockwidget.dateFromEdit.setDate(QDate(self.qLastMonth))
            self.dockwidget.dateToEdit.setDate(QDate(self.qToday))

    def on_right_click(self, position):
        self.clear_selection()

    def clear_selection(self):
        self.is_selecting_start = True
        self.dockwidget.calendarWidget.setDateTextFormat(QDate(), QTextCharFormat())
        year = self.dockwidget.calendarWidget.yearShown()
        month = self.dockwidget.calendarWidget.monthShown()
        date_range = self.getMonthlyRange(year, month)
        self.dockwidget.dateFromEdit.setDate(date_range[0])
        if date_range[1] > QDate.currentDate(): 
            self.dockwidget.dateToEdit.setDate(self.qToday)
        else: 
            self.dockwidget.dateToEdit.setDate(date_range[1])
        self.setupCalendarMosaic(date_range)

    def onSceneSelection(self, idx): 
        if idx == 0: 
            self.onCancel()
        if idx == 1: 
            self.dockwidget.labelDateFrom.setText('Image date: ')
            self.dockwidget.labelDateFrom.setVisible(True)
            self.dockwidget.labelDateTo.setVisible(False)
            self.dockwidget.dateToEdit.setVisible(False)
            self.dockwidget.dateFromEdit.setVisible(True)
            self.dockwidget.dateFromEdit.setEnabled(False)
            self.dockwidget.dateFromEdit.setDisplayFormat("dd-MM-yyyy")
            self.dockwidget.dateFromEdit.setDate(self.qToday)
            self.dockwidget.calendarWidget.setVisible(True)
            self.dockwidget.calendarWidget.setMinimumDate(QDate(2017, 1, 1))
            self.dockwidget.calendarWidget.setMaximumDate(self.qToday)
            date_range = self.getMonthlyRange(self.qToday.year(), self.qToday.month())
            self.setupCalendarScene(date_range)
            
        if idx == 2: 
            self.dockwidget.labelDateFrom.setText('From date: ')
            self.dockwidget.labelDateFrom.setVisible(True)
            self.dockwidget.dateFromEdit.setVisible(True)
            self.dockwidget.dateFromEdit.setEnabled(False)
            self.dockwidget.dateFromEdit.setDisplayFormat("dd-MM-yyyy")
            self.dockwidget.labelDateTo.setText('To date: ')
            self.dockwidget.labelDateTo.setVisible(True)
            self.dockwidget.dateToEdit.setVisible(True)
            self.dockwidget.dateToEdit.setEnabled(False)
            self.dockwidget.dateToEdit.setDisplayFormat("dd-MM-yyyy")
            
            self.dockwidget.calendarWidget.setVisible(True)
            self.dockwidget.calendarWidget.setMinimumDate(QDate(2017, 1, 1))
            self.dockwidget.calendarWidget.setMaximumDate(self.qToday)
            year = self.dockwidget.calendarWidget.yearShown()
            month = self.dockwidget.calendarWidget.monthShown()
            date_range = self.getMonthlyRange(year, month)
            self.dockwidget.dateFromEdit.setDate(date_range[0])
            if date_range[1] > QDate.currentDate(): 
                self.dockwidget.dateToEdit.setDate(self.qToday)
            else: 
                self.dockwidget.dateToEdit.setDate(date_range[1])
            self.setupCalendarMosaic(date_range)

    def _select_best_item(self, items_group, bbox):
        best_item = None
        largest_area = 0
        
        for item in items_group:
            try:
                item_geom = shape(item.geometry)
                area = item_geom.area
                if area > largest_area:
                    largest_area = area
                    best_item = item
            except:
                continue
        return best_item if best_item else items_group[0]
    
    def setupCalendarMosaic(self, date_minmax):
        self.image_info = []
        min_date = date_minmax[0].toString("yyyy-MM-dd")
        max_date = date_minmax[1].toString("yyyy-MM-dd")
        date_range = f"{min_date}/{max_date}"
        print ('DATE RANGE', date_range)
        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()

        bbox = self.getQgisExtent()
        if not bbox: 
            return 
        date_minmax_changed = self.cached_date_minmax != date_minmax
        bbox_changed = self.cached_eeBbox != bbox
        if date_minmax_changed or bbox_changed or not self.cached_image_dates:
            image_dates = []
            CATALOG = pystac_client.Client.open(self.STAC_ESA)
            SEARCH = CATALOG.search(collections = ["sentinel-2-l1c"], datetime = date_range, bbox = bbox)
            polygon = Polygon([(bbox[0], bbox[1]),(bbox[2], bbox[1]),(bbox[2], bbox[3]),(bbox[0], bbox[3]),(bbox[0], bbox[1])])
            containing_items = []
            orbit_groups = defaultdict(list)
            searchs = SEARCH.get_all_items()
            for rs in searchs: 
                if shape(rs.geometry).contains(polygon): 
                    containing_items.append(rs)
            
            for ci in containing_items: 
                item_parts = ci.id.split('_')
                satellite = item_parts[0] 
                relative_orbit = item_parts[4]
                img_date = item_parts[6]
                orbit_key = f"{satellite}_{relative_orbit}_{img_date}"
                orbit_groups[orbit_key].append(ci)

            # Select one item per orbit group
            deduplicated_items = []
            for orbit_key, items_group in orbit_groups.items():
                if len(items_group) == 1:
                    deduplicated_items.append(items_group[0])
                else:
                    selected_item = self._select_best_item(items_group, bbox)
                    deduplicated_items.append(selected_item)
            deduplicated_items.sort(key=lambda x: x.datetime, reverse=True)
            for item in deduplicated_items: 
                tile_id = item.id.split('_')[5]
                date_str = item.datetime.strftime('%Y-%m-%d')
                cloud_cover = int(item.properties['eo:cloud_cover'])
                self.image_info.append((tile_id, date_str, cloud_cover, item.id))

            image_dates = sorted(set(item.datetime.strftime("%Y-%m-%d") for item in deduplicated_items))
            self.dockwidget.calendarWidget.setDateEditEnabled(False)
            
            valid_dates = [QDate.fromString(date, "yyyy-MM-dd") for date in image_dates]
            self.cached_date_minmax = date_minmax.copy() if hasattr(date_minmax, 'copy') else list(date_minmax)
            self.cached_eeBbox = bbox.copy()
            self.cached_image_dates = image_dates.copy()
            self.cached_valid_dates = valid_dates.copy()
        else:
            image_dates = self.cached_image_dates
            valid_dates = self.cached_valid_dates
        self.eeBbox = bbox
        self.valid_dates = valid_dates
        self.formatted_dates = image_dates
        self.dockwidget.calendarWidget.setDateEditEnabled(False)
        self.dockwidget.calendarWidget.setDateTextFormat(QDate(), QTextCharFormat())
        
        for qdate in self.valid_dates:
            fmt = self.dockwidget.calendarWidget.dateTextFormat(qdate)
            fmt.setForeground(QColor(0, 0, 0)) 
            fmt.setBackground(QColor(200, 255, 200))
            if self.qtVersion == 6: 
                fmt.setFontWeight(QFont.Weight.Bold)
            elif self.qtVersion == 5: 
                fmt.setFontWeight(QFont.Bold)
            self.dockwidget.calendarWidget.setDateTextFormat(qdate, fmt)

        try:
            self.dockwidget.acceptButton.clicked.disconnect()
            self.dockwidget.previewButton.clicked.disconnect()
        except TypeError:
            # No connections exist yet, which is fine
            pass

        self.dockwidget.previewButton.setEnabled(True)
        self.dockwidget.acceptButton.setEnabled(True)
        self.dockwidget.acceptButton.clicked.connect(self.cloudMosaic)
        self.dockwidget.previewButton.clicked.connect(self.previewImageList)

    def setupCalendarScene(self, date_minmax):
        min_date = date_minmax[0].toString("yyyy-MM-dd")
        max_date = date_minmax[1].toString("yyyy-MM-dd")
        self.image_info = []
        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()
        currentBbox = self.getQgisExtent()
        if not currentBbox: 
            return 
        image_dates = []
        date_range = f"{min_date}/{max_date}"
        bbox = self.getQgisExtent()
        if not bbox: 
            return 
        date_minmax_changed = self.cached_date_minmax != date_minmax
        bbox_changed = self.cached_eeBbox != bbox
        if date_minmax_changed or bbox_changed or not self.cached_image_dates:
            image_dates = []
            CATALOG = pystac_client.Client.open(self.STAC_ESA)
            SEARCH = CATALOG.search(collections = ["sentinel-2-l1c"], datetime = date_range, bbox = bbox)
            polygon = Polygon([(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3]), (bbox[0], bbox[1])])
            containing_items = []
            orbit_groups = defaultdict(list)
            searchs = SEARCH.get_all_items()
            for rs in searchs: 
                if shape(rs.geometry).contains(polygon): 
                    containing_items.append(rs)
            
            for ci in containing_items: 
                item_parts = ci.id.split('_')
                satellite = item_parts[0] 
                relative_orbit = item_parts[4]
                img_date = item_parts[6]
                orbit_key = f"{satellite}_{relative_orbit}_{img_date}"
                orbit_groups[orbit_key].append(ci)

            # Select one item per orbit group
            deduplicated_items = []
            for orbit_key, items_group in orbit_groups.items():
                if len(items_group) == 1:
                    deduplicated_items.append(items_group[0])
                else:
                    selected_item = self._select_best_item(items_group, bbox)
                    deduplicated_items.append(selected_item)
            deduplicated_items.sort(key=lambda x: x.datetime, reverse=True)

            for item in deduplicated_items: 
                tile_id = item.id.split('_')[5]
                date_str = item.datetime.strftime('%Y-%m-%d')
                image_dates.append(date_str)
                cloud_cover = int(item.properties['eo:cloud_cover'])
                self.image_info.append((tile_id, date_str, cloud_cover, item.id))

        # for product in products: 
        self.dockwidget.calendarWidget.setDateEditEnabled(False)
        valid_dates = [QDate.fromString(date[:10], "yyyy-MM-dd") for date in image_dates]
        self.valid_dates = valid_dates
        self.formatted_dates = image_dates
        self.dockwidget.calendarWidget.setDateEditEnabled(False)
        self.dockwidget.calendarWidget.setDateTextFormat(QDate(), QTextCharFormat())
        for qdate in self.valid_dates:
            fmt = self.dockwidget.calendarWidget.dateTextFormat(qdate)
            fmt.setForeground(QColor(0, 0, 0))  # Black text
            fmt.setBackground(QColor(200, 255, 200))  # Light green background
            if self.qtVersion == 6: 
                fmt.setFontWeight(QFont.Weight.Bold)
            elif self.qtVersion == 5: 
                fmt.setFontWeight(QFont.Bold)
            self.dockwidget.calendarWidget.setDateTextFormat(qdate, fmt)

    def cropImage(self, input_path, bbox, output_path):
        try:
            dataset = gdal.Open(input_path)
            options = gdal.WarpOptions(outputBounds = bbox, format = 'GTiff')
            gdal.Warp(output_path, dataset, options=options)
            return True
        except Exception as e:
            return False
    
    def writeAndClose(self, aws_id, aws_secret):
        creds_path = os.path.join(self.plugin_dir, '_creds.json')
        if not os.path.exists(creds_path):
            creds_content = {
                "credentials": {
                    "cdseId": f"{aws_id}",
                    "cdseSecrets": f"{aws_secret}"
                }
            }

            with open(creds_path, 'w') as f:
                json.dump(creds_content, f, indent=4)

        self.dockwidget.labelId.setVisible(False)
        self.dockwidget.idLineEdit.clear()
        self.dockwidget.idLineEdit.setVisible(False)
        self.dockwidget.labelSec.setVisible(False)
        self.dockwidget.secLineEdit.clear()
        self.dockwidget.secLineEdit.setVisible(False)
        self.dockwidget.labelGuide.setVisible(False)

    def cloudMosaic(self):
        from .maskingCloudL1C import applyCloudMasking

        progress_dialog = DownloadProgressDialog(self.iface.mainWindow())
        progress_dialog.show()
        
        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()

        try:
            progress_dialog.set_status("Initializing...")
            progress_dialog.set_detail("Setting up date range and bbox")
            min_date = datetime(self.cached_date_minmax[0].year(), self.cached_date_minmax[0].month(), self.cached_date_minmax[0].day())
            max_date = datetime(self.cached_date_minmax[1].year(), self.cached_date_minmax[1].month(), self.cached_date_minmax[1].day())
            currentBbox = self.getQgisExtent(action='mosaic')
            process_dir = os.path.join(documents_folder, "Sentinel-2")
            progress_dialog.set_status("Searching for Sentinel-2 products...")
            progress_dialog.set_detail("Querying satellite data")
            products = self.searchS2ByDateExtent(min_date.strftime("%Y-%m-%d"), max_date.strftime("%Y-%m-%d"), currentBbox)
            
            if progress_dialog.is_cancelled():
                return
            progress_dialog.set_detail("Determining coordinate system")
            for de in products:
                utm_zone = int(de.id.split('_')[5][1:3])
                geom = shape(de.geometry)
                if geom.centroid.y > 0:
                    epsg_code = f"EPSG:{32600 + utm_zone}"
                else:
                    epsg_code = f"EPSG:{32700 + utm_zone}"
                break
            
            # Setup directories
            greenness_dir = os.path.join(process_dir, "MOSAIC_MASKED")
            greenness_name = ''
            os.makedirs(greenness_dir, exist_ok=True)
            scene_dirs = []
            
            start = min_date.strftime("%Y%m%d")
            today = datetime.now()
            if max_date > today:
                end = today.strftime("%Y%m%d")
            else:
                end = max_date.strftime("%Y%m%d")
            
            total_products = len(products)
            total_steps = total_products * 2 + 5
            progress_dialog.set_range(0, total_steps)
            current_step = 0

            for i, product in enumerate(products):
                if progress_dialog.is_cancelled():
                    return
                product_name = product.id
                progress_dialog.set_status(f"Processing image {i+1}/{total_products}")
                progress_dialog.set_detail(f"Downloading bands for: {product_name.split('_')[5]}")
                progress_dialog.set_value(current_step)
                downloaded = downloadL1CBands(self.cdseId, self.cdseSecret, product_name, process_dir)
                if not downloaded: 
                    return 
                current_step += 1
                if progress_dialog.is_cancelled():
                    return
                # Cloud masking step
                progress_dialog.set_detail(f"Applying cloud masking tile ID: {product_name.split('_')[5]}")
                progress_dialog.set_value(current_step)
                result = applyCloudMasking(product_name=f"{product_name}.SAFE", process_dir=process_dir, epsg_code=epsg_code)
                scene_dir = os.path.join(process_dir, f"{product_name}.SAFE", "TEMP")
                scene_dirs.append(scene_dir)
                current_step += 1
            
            if progress_dialog.is_cancelled():
                return
            progress_dialog.set_status("Creating mosaic...")
            progress_dialog.set_detail("Processing all bands")
            progress_dialog.set_value(current_step)
            mosaic = SimpleSentinel2Mosaic(greenness_dir)
            result_files = mosaic.process_all_bands(scene_dirs, f"{start}_{end}")
            current_step += 1
            
            if progress_dialog.is_cancelled():
                return
            progress_dialog.set_detail("Renaming bands")
            progress_dialog.set_value(current_step)
            final_file = os.path.join(greenness_dir, f"{start}_{end}_composite.tif")
            ds = gdal.Open(final_file, gdal.GA_Update)
            rename_bands = ["B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B11", "B12"]
            for i, new_name in enumerate(rename_bands):
                band = ds.GetRasterBand(i + 1)
                band.SetDescription(new_name)
            
            ds.FlushCache()
            ds = None
            current_step += 1
            
            if progress_dialog.is_cancelled():
                return
            
            # TCI creation
            progress_dialog.set_detail("Creating True Color Image (TCI)")
            progress_dialog.set_value(current_step)
            
            temp_tci = os.path.join(greenness_dir, f"{start}_{end}_TCI.tif")
            band_names = ["B04", "B03", "B02"]
            masked_temp_files = []
            
            for bb in band_names:
                output_path = os.path.join(greenness_dir, f"{start}_{end}_{bb}_median.tif")
                masked_temp_files.append(output_path)
            current_step += 1
            
            if progress_dialog.is_cancelled():
                return
            
            # Build virtual raster
            progress_dialog.set_detail("Building virtual raster")
            progress_dialog.set_value(current_step)
            
            processing.run("gdal:buildvirtualraster", {
                'INPUT': masked_temp_files,
                'RESOLUTION': 0,
                'SEPARATE': True,
                'PROJ_DIFFERENCE': False,
                'ADD_ALPHA': False,
                'ASSIGN_CRS': None,
                'RESAMPLING': 0,
                'OUTPUT': temp_tci.replace('.tif', '_temp.tif')
            }, feedback=QgsProcessingFeedback())
            current_step += 1
            
            if progress_dialog.is_cancelled():
                return
            
            # Translate and scale
            progress_dialog.set_detail("Finalizing TCI image")
            progress_dialog.set_value(current_step)
            if self.qgisVersion < 31400: 
                dtype_tci = 0
            else: 
                dtype_tci = 1

            processing.run("gdal:translate", {
                'INPUT': temp_tci.replace('.tif', '_temp.tif'),
                'TARGET_CRS': None,
                'NODATA': 0,
                'COPY_SUBDATASETS': False,
                'OPTIONS': 'COMPRESS=LZW',
                'EXTRA': '-scale 0 10000 0 255',
                'DATA_TYPE': dtype_tci,
                'OUTPUT': temp_tci
            }, feedback=QgsProcessingFeedback())

            tci = QgsRasterLayer(temp_tci, f'{start}_{end}_TCI')
            QgsProject.instance().addMapLayer(tci)

            # for file in masked_temp_files:
            #     if os.path.exists(file):
            #         os.remove(file)

            progress_dialog.set_value(total_steps)
            progress_dialog.set_status("Processing complete!")
            progress_dialog.set_detail("All operations finished successfully")
            progress_dialog.accept()
            QMessageBox.information(None, u'Done', 
                                f'The cloud-masked Sentinel-2 L1C imagery (10 bands) was successfully processed and stored at {greenness_dir}. '
                                f'Temporary files are still remained in TEMP directory of each image, you can keep or remove them when needed.')
            self.iface.mapCanvas().refresh()
        except Exception as e:
            progress_dialog.reject()
            QMessageBox.critical(None, "Error", f"An error occurred during processing: {str(e)}")
            raise
        
        finally:
            if progress_dialog.isVisible():
                progress_dialog.reject()
        self.writeAndClose(self.cdseId, self.cdseSecret)

    def cloudMasking(self): 
        from .maskingCloudL1C import applyCloudMasking
        
        qdate = self.selectedDate
        min_date = datetime(qdate.year(), qdate.month(),qdate.day())
        max_date = min_date + timedelta(days = 1)
        currentBbox = self.getQgisExtent(action = 'mask')
        process_dir = os.path.join(documents_folder, "Sentinel-2")
        greenness_dir = os.path.join(process_dir, "SCENE_MASKED")
        greenness_name = ''
        os.makedirs(greenness_dir, exist_ok = True)
        progress_dialog = DownloadProgressDialog(self.iface.mainWindow())
        progress_dialog.show()

        if not self.cdseId or self.cdseId == '': 
            self.cdseId = self.dockwidget.idLineEdit.text()
        if not self.cdseSecret or self.cdseSecret == '': 
            self.cdseSecret = self.dockwidget.secLineEdit.text()

        try:
            progress_dialog.set_status("Downloading Sentinel-2 L1C Bands...")
            progress_dialog.set_range(0, 100)
            progress_dialog.set_value(0)

            products = self.searchS2ByDateExtent(min_date.strftime("%Y-%m-%d"), max_date.strftime("%Y-%m-%d"), currentBbox)
            product = products[0]
            product_name = product.id
            greenness_name = product_name[38:]
            utm_zone = int(product.id.split('_')[5][1:3])
            geom = shape(product.geometry)
            if geom.centroid.y > 0: 
                epsg_code = f"EPSG:{32600 + utm_zone}"
            else: 
                epsg_code = f"EPSG:{32700 + utm_zone}"
            downloaded = downloadL1CBands(self.cdseId, self.cdseSecret,
                product_name=product_name, 
                download_dir=process_dir, 
                band_name=None,  
                progress_dialog=progress_dialog,
                progress_start=0,
                progress_end=40
            )
            if not downloaded: 
                return 
            
            if progress_dialog.is_cancelled():
                return None

            progress_dialog.set_status("Applying cloud masking...")
            progress_dialog.set_detail("Processing cloud mask algorithms...")
            result = applyCloudMasking(
                product_name=f"{product_name}.SAFE", 
                process_dir=process_dir, 
                epsg_code=epsg_code,
                progress_dialog=progress_dialog,
                progress_start=40,
                progress_end=80
            )
            
            if progress_dialog.is_cancelled():
                return None

            # Step 3: Other long-running operations
            progress_dialog.set_status("Finalizing output...")
            progress_dialog.set_detail("Generating final output files...")

            #final_output_path = os.path.join(greenness_dir, greenness_name)
            temp_dir = os.path.join(process_dir, f"{product_name}.SAFE", 'TEMP')
            scene_path = Path(temp_dir)
            output_bands = ['B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B8A', 'B11', 'B12']
            masked_band_files = []
            for band in output_bands:
                pattern = f"*{band}*masked*.tif"
                matches = list(scene_path.glob(pattern))
                if matches:
                    masked_band_files.append(str(matches[0]))

            output_vrt = os.path.join(greenness_dir, f"{greenness_name}.vrt")
            final_output_tif = os.path.join(greenness_dir, f"{greenness_name}.tif")
            processing.run("gdal:buildvirtualraster", {
                'INPUT': masked_band_files,
                'RESOLUTION': 0,
                'SEPARATE': True,
                'PROJ_DIFFERENCE': False,
                'ADD_ALPHA': False,
                'ASSIGN_CRS': None,
                'RESAMPLING': 0,
                'OUTPUT': output_vrt
            }, feedback=QgsProcessingFeedback())

            # Convert VRT to TIFF
            processing.run("gdal:translate", {
                'INPUT': output_vrt,
                'TARGET_CRS': None,
                'NODATA': 0,
                'COPY_SUBDATASETS': False,
                'OPTIONS': 'COMPRESS=LZW',
                'EXTRA': '',
                'DATA_TYPE': 3,
                'OUTPUT': final_output_tif
            }, feedback=QgsProcessingFeedback())

            ds = gdal.Open(final_output_tif, gdal.GA_Update)
            # Loop through each band and rename
            for i, new_name in enumerate(output_bands):
                band = ds.GetRasterBand(i + 1)
                band.SetDescription(new_name)

            ds.FlushCache()
            ds = None

            temp_tci = os.path.join(greenness_dir, f"{greenness_name}_TCI.vrt")
            masked_temp_files = []
            for band in ["B04", "B03", "B02"]: 
                pattern = f"*{band}*masked*.tif"
                matches = list(scene_path.glob(pattern))
                if matches:
                    masked_temp_files.append(str(matches[0]))

            processing.run("gdal:buildvirtualraster", {
                'INPUT': masked_temp_files,
                'RESOLUTION': 0,
                'SEPARATE': True,
                'PROJ_DIFFERENCE': False,
                'ADD_ALPHA': False,
                'ASSIGN_CRS': None,
                'RESAMPLING': 0,
                'OUTPUT': temp_tci.replace('.vrt', '_temp.vrt')
            }, feedback=QgsProcessingFeedback())
            if self.qgisVersion < 31400: 
                dtype_tci = 0
            else: 
                dtype_tci = 1

            processing.run("gdal:translate", {
                'INPUT': temp_tci.replace('.vrt', '_temp.vrt'),
                'TARGET_CRS': None,
                'NODATA': 0,
                'COPY_SUBDATASETS': False,
                'OPTIONS': 'COMPRESS=LZW',
                'EXTRA': '-scale 0 10000 0 255',
                'DATA_TYPE': dtype_tci,
                'OUTPUT': temp_tci
            }, feedback=QgsProcessingFeedback())

            if progress_dialog.is_cancelled():
                return None
            progress_dialog.set_status("Processing complete!")
            progress_dialog.set_value(100)
            progress_dialog.set_detail("All operations completed successfully.")

            tci = QgsRasterLayer(temp_tci, f'{greenness_name}_TCI')
            QgsProject.instance().addMapLayer(tci)
            QMessageBox.information(None, u'Done', f'The cloud-masked Sentinel-2 L1C imagery (10 bands) was successfully processed and stored at {final_output_tif}. Temporary files are still remained in {temp_dir}, you can keep or remove them when needed.')            
            self.iface.mapCanvas().refresh()
        except Exception as e:
            progress_dialog.set_status(f"Error: {str(e)}")
            progress_dialog.set_detail("Processing failed. Please check the logs.")
            raise
        finally:
            QTimer.singleShot(2000, progress_dialog.accept)
        
        self.writeAndClose(self.cdseId, self.cdseSecret)

    def tr(self, message):
        return QCoreApplication.translate('s2CloudMask', message)

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToRasterMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def onCancel(self): 
        self.dockwidget.close()

    def onClosePlugin(self):
        self.dockwidget.closingPlugin.disconnect(self.onClosePlugin)
        self.pluginIsActive = False

    def unload(self):
        for action in self.actions:
            self.iface.removePluginRasterMenu(self.tr(u'&Cloud Mask Sentinel-2'), action)
            self.iface.removeToolBarIcon(action)
        del self.toolbar
