# -*- coding: utf-8 -*-
"""
/***************************************************************************
 easydemDialog
                                 A QGIS plugin
 Get Digital Elevation Model (DEM) data from Google Earth Engine and plot as raster layer it contour lines, make elevation maps.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-11-13
        git sha              : $Format:%H$
        copyright            : (C) 2024 by Caio Arantes
        email                : caiosimplicioarantes@gmail.com
        ICON SOURCE: <a href="https://www.flaticon.com/free-icons/topography" title="topography icons">Topography icons created by Freepik - Flaticon</a>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""
# Standard library imports
import os
import sys
import importlib
import platform
import subprocess
import zipfile
import json
import webbrowser
import io
import array
import shutil
import urllib.request
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import tempfile

# Third-party imports
import geopandas as gpd
import requests
import pandas as pd
import numpy as np
import ee
from scipy.signal import savgol_filter
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# qgis.PyQt imports
from qgis.PyQt import uic, QtWidgets
from qgis.PyQt.QtCore import QSettings, Qt, QDate, QVariant
from qgis.PyQt.QtWidgets import (
    QDialog,
    QMessageBox,
    QFileDialog,
    QApplication,
    QGridLayout,
    QWidget,
    QVBoxLayout,
    QCheckBox,
    QDialogButtonBox,
    QPushButton,
    QLineEdit,
)
from qgis.PyQt.QtGui import QColor

# QGIS imports
import qgis
from qgis.core import (
    QgsProject,
    QgsRasterLayer,
    QgsRasterShader,
    QgsColorRampShader,
    QgsSingleBandPseudoColorRenderer,
    QgsStyle,
    QgsRasterBandStats,
    QgsMapLayer,
    QgsVectorLayer,
    QgsColorRamp,
    QgsLayerTreeLayer,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsMultiBandColorRenderer,
    QgsContrastEnhancement,
    QgsProcessingFeedback,
    QgsApplication,
    QgsRectangle,
    QgsFeature,
    QgsGeometry,
    QgsField,
    QgsVectorFileWriter,
    QgsWkbTypes,
)

from qgis.utils import iface
from qgis import processing
from qgis.analysis import QgsNativeAlgorithms

from .modules import datasets_info


language = QSettings().value("locale/userLocale", "en")[0:2]

if language == "pt":
    ui_file = os.path.join("ui", "easy_dialog_base_pt.ui")
else:
    ui_file = os.path.join("ui", "easy_dialog_base.ui")

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), ui_file))

class easydemDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        super(easydemDialog, self).__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMinimizeButtonHint)

        self.language = QSettings().value("locale/userLocale", "en")[0:2]

        if self.language == "pt":
            self.dem_datasets = datasets_info.dem_datasets_pt
        else:
            self.dem_datasets = datasets_info.dem_datasets_en

        # Load the content of the intro.html file into a variable
        if language == "pt":
            intro_file_path = os.path.join(os.path.dirname(__file__), 'modules', 'intro_pt.html')
        else:
            intro_file_path = os.path.join(os.path.dirname(__file__), 'modules', 'intro.html')

        with open(intro_file_path, 'r', encoding='utf-8') as file:
            intro_content = file.read()

        self.textEdit.setHtml(intro_content)  # Set the content to the textEdit widget
        self.textEdit.setOpenExternalLinks(True)       
        self.textEdit.setReadOnly(True)  # Prevent editing
        self.textEdit.anchorClicked.connect(self.open_link)
        self.dem_info_textbox.setReadOnly(True)  # Prevent editing# Make it interactive
        self.dem_info_textbox.setOpenExternalLinks(True)
        self.dem_info_textbox.anchorClicked.connect(self.open_link)

        self.tabWidget.setCurrentIndex(0)

        self.folder_set = False
        self.aoi_set = True
        self.autentication = False
        self.resizeEvent("small")

        # Call update_dem_datasets after initialization to avoid accessing dem_datasets before it's defined.
        self.update_dem_datasets()
        #self.load_vector_layers()
        self.dem_dataset_combobox.currentIndexChanged.connect(self.update_dem_info)
        self.load_vector_layers_button.clicked.connect(self.update_combo_box)
        self.vector_layer_combobox.currentIndexChanged.connect(self.get_selected_layer_path)
        self.autenticacao.clicked.connect(self.auth)
        self.desautenticacao.clicked.connect(self.auth_clear)
        self.elevacao.clicked.connect(self.elevacao_clicked)
        self.mQgsFileWidget.fileChanged.connect(self.on_file_changed)
        self.tabWidget.currentChanged.connect(self.on_tab_changed)
        self.browser.clicked.connect(self.open_learn_dialog)
        self.horizontalSlider_buffer.valueChanged.connect(self.update_labels)

        self.project_QgsPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal)

        # Ensure this sets up self.project_QgsPasswordLineEdit (or rename to something like self.projectIdLineEdit)
        self.loadProjectId()
        # Connect the textChanged signal to automatically save changes.
        self.project_QgsPasswordLineEdit.textChanged.connect(self.autoSaveProjectId)

    def open_learn_dialog(self):
        """Open the learn dialog."""
        webbrowser.open("https://caioarantes.github.io/EasyDEM/")

    def update_combo_box(self):

        try:
            self.load_vector_layers()
            self.get_selected_layer_path()
            self.load_vector_function()
        except:
            pass    

    def update_labels(self):
        """Updates the text of several labels based on the values of horizontal
        sliders."""
        self.label_buffer.setText(f"{self.horizontalSlider_buffer.value()}m")

    def apply_buffer(self, aoi):
        """Applies a buffer to the AOI geometry."""
        buffer_distance = self.horizontalSlider_buffer.value()
        print(f"Applying buffer of {buffer_distance} meters to AOI.")
        if buffer_distance != 0:
            print(f"Buffer distance: {buffer_distance} meters")
            aoi = aoi.map(lambda feature: feature.buffer(buffer_distance))
            #self.aoi = aoi
            return aoi
        else:
            print("No buffer applied")
            return aoi

    def resizeEvent(self, event):
        self.setMinimumSize(0, 0)  # Remove minimum size constraint
        self.setMaximumSize(16777215, 16777215)  # Remove maximum size constraint

        if event == "small":
            self.resize(594, 396)
            self.setFixedSize(self.width(), self.height())  # Lock to small size
        elif event == "big":
            self.resize(594, 482)
            self.setFixedSize(self.width(), self.height())  # Lock to big size

    def open_link(self, url):
        """Open the clicked link in the default web browser."""
        print(f"Opening URL: {url.toString()}")
        webbrowser.open(url.toString())

    def next_clicked(self):
        self.tabWidget.setCurrentIndex((self.tabWidget.currentIndex() + 1) % self.tabWidget.count())

    def back_clicked(self):
        self.tabWidget.setCurrentIndex((self.tabWidget.currentIndex() - 1) % self.tabWidget.count())

    def load_path_sugestion(self):
        """
        Load the path suggestion based on the user's operating system.
        """
        system = platform.system()
        if system == 'Windows':
            self.output_folder = os.path.join(os.environ['USERPROFILE'], 'Downloads')
        elif system == 'Linux':
            self.output_folder = os.path.join(os.environ['HOME'], 'Downloads')
        elif system == 'Darwin':  # MacOS
            self.output_folder = os.path.join(os.environ['HOME'], 'Downloads')

        # Pre-configure with a suggested directory
        self.mQgsFileWidget.setFilePath(self.output_folder)

    def loadProjectId(self):
        """
        Loads the saved project ID from QSettings and sets it in the widget.
        This will run every time the plugin is opened.
        """
        settings = QSettings()
        # Retrieve the project ID from QSettings. The key "MyPlugin/projectID" is arbitrary.
        saved_project_id = settings.value("MyPlugin/projectID", "", type=str)
        self.project_QgsPasswordLineEdit.setText(saved_project_id)
        print("Loaded project ID:", saved_project_id)
        self.autenticacao.setEnabled(bool(self.project_QgsPasswordLineEdit.text()))

    def autoSaveProjectId(self, new_text):
        """
        Automatically saves the project ID to QSettings whenever the text changes.
        This ensures that the project ID remains available even after QGIS is closed and reopened.
        """
        settings = QSettings()
        settings.setValue("MyPlugin/projectID", new_text)
        print("Project ID auto-saved:", new_text)
        self.autenticacao.setEnabled(bool(self.project_QgsPasswordLineEdit.text()))

    def pop_aviso_auth(self, aviso):
        """
        Displays a warning message box with the given message and Ok button.
        Args:
            aviso (str): The warning message to display in the message box.
        Returns:
            None
        Note:
            This method restores the override cursor before displaying the message box.
        """
        QApplication.restoreOverrideCursor()
        msg = QMessageBox(self)
        msg.setWindowTitle("Warning!")
        msg.setIcon(QMessageBox.Icon.Warning)
        msg.setText(aviso)

        # Acessar os botões para definir texto personalizado
        ok_button = msg.addButton(QMessageBox.StandardButton.Ok)
        ok_button.setText("Ok")

        msg.exec()

    def on_tab_changed(self, index):
        print(f"Tab changed to index: {index}")
        
        if index == 1 and (self.autentication == False):
            self.tabWidget.setCurrentIndex(0)
            self.resizeEvent("small")
            return

        if index == 1:

            try:
                self.load_vector_layers()
                self.get_selected_layer_path()
                self.load_vector_function()
            except:
                pass

            self.resizeEvent("big")

        if index == 0:
            self.resizeEvent("small")
           
    def next_button_clicked(self):
        self.tabWidget.setCurrentIndex(self.tabWidget.currentIndex() + 1)

    def on_file_changed(self, file_path):
        """Slot called when the selected file changes."""
        print(f"File selected: {file_path}")
        self.output_folder = file_path
        self.folder_set = True

    def update_dem_datasets(self):
        print(list(self.dem_datasets.keys()))
        self.dem_dataset_combobox.addItems(list(self.dem_datasets.keys()))
        self.update_dem_info()

    def get_unique_filename(self, base_file_name):
        """
        Generates a unique filename by checking if the file already exists
        and adding a numerical suffix to it if needed.

        Parameters:
        base_file_name (str): The base filename to use.

        Returns:
        str: The unique filename.
        """
        output_file = self.output_folder+f'/{base_file_name}.tif'
        counter = 1

        while os.path.exists(output_file):
            output_file = self.output_folder +f'/{base_file_name}_{counter}.tif'
            counter += 1

        print(f"Unique filename: {output_file}")
        return output_file

    def load_vector_layers(self):
        # Get all layers in the current QGIS project / Obtém todas as camadas
        # no projeto QGIS atual
        layers = list(QgsProject.instance().mapLayers().values())

        # Filter polygon and multipolygon vector layers / Filtra camadas
        # vetoriais de polígono e multipolígono
        vector_layers = [
            layer
            for layer in layers
            if layer.type() == QgsMapLayer.VectorLayer
            and layer.geometryType() == QgsWkbTypes.PolygonGeometry
        ]

        # Get current layer names / Obtém os nomes das camadas atuais
        current_layer_names = set(
            self.vector_layer_combobox.itemText(i)
            for i in range(self.vector_layer_combobox.count())
        )

        # Clear the combobox and the dictionary / Limpa a combobox e o
        # dicionário
        self.vector_layer_combobox.clear()
        self.vector_layer_ids = {}

        # Find the new layer while populating the combobox / Encontra a nova
        # camada enquanto popula a combobox
        new_layer_name = None
        for layer in vector_layers:
            layer_name = layer.name()
            self.vector_layer_combobox.addItem(layer_name)
            self.vector_layer_ids[layer_name] = layer.id()

            # If this layer wasn't in the previous list, it's new / Se esta
            # camada não estava na lista anterior, é nova
            if layer_name not in current_layer_names:
                new_layer_name = layer_name

        # If we found a new layer, select it / Se encontramos uma nova camada,
        # selecione-a
        if new_layer_name:
            index = self.vector_layer_combobox.findText(new_layer_name)
            self.vector_layer_combobox.setCurrentIndex(index)

        if self.vector_layer_combobox.count() == 0:
            self.aoi = None
            
            if language == 'pt':
                self.pop_warning("Nenhuma camada vetorial encontrada no projeto.")
            else:
                self.pop_warning("No vector layers found in the project.")
    


    def get_selected_layer_path(self):
        """
        Retrieves the path of the currently selected layer in the combobox and
        triggers further processing.
        """
        """
        Recupera o caminho da camada atualmente selecionada na combobox e
        aciona o processamento adicional.
        """
        # Get the currently selected layer name from the combobox / Obtém o nome
        # da camada atualmente selecionada da combobox
        layer_name = (
            self.vector_layer_combobox.currentText().strip()
        )  # Remove whitespace
        if layer_name == "":
            print("No layer selected.")
            return None
        print(f"Layer name from combobox: '{layer_name}'")  # Debug
        self.zoom_to_layer(layer_name)

        # Get the corresponding layer ID / Obtém o ID da camada
        # correspondente
        layer_id = self.vector_layer_ids.get(layer_name)
        print(f"Layer ID from vector_layer_ids: {layer_id}")  # Debug

        if layer_id is None:
            print(
                f"Error: Layer ID is None for layer name '{layer_name}'.  Check vector_layer_ids."
            )
            print(
                f"Contents of vector_layer_ids: {self.vector_layer_ids}"
            )  # Debug
            return None

        # Get the layer using its ID / Obtém a camada usando seu ID
        layer = QgsProject.instance().mapLayer(layer_id)
        if layer:
            print(
                f"Layer found: {layer.name()}, ID: {layer_id}"
            )  # Debug: Confirm layer is found
            self.selected_aoi_layer_path = (
                layer.dataProvider().dataSourceUri().split("|")[0]
            )
            print(
                f"Selected layer path: {self.selected_aoi_layer_path}"
            )  # Debug: Show selected layer path

            # Trigger the processing function / Aciona a função de
            # processamento

            self.aoi = self.load_vector_function()

            return None
        else:
            print(
                f"Layer '{layer_name}' with ID '{layer_id}' not found in the project."
            )
            return None

    def update_dem_info(self):
        dem_name = self.dem_dataset_combobox.currentText()
        dem_info = self.dem_datasets[dem_name]["Info"]
        self.dem_info_textbox.setHtml(dem_info)
        self.dem_resolution_combobox.clear()
        self.dem_resolution_combobox.addItems([str(res) for res in self.dem_datasets[dem_name]["Resolution"]])

    def zoom_to_layer(self, layer_name, margin_ratio=0.1):
        """
        Zoom to the specified layer with an optional margin.

        :param layer_name: Name of the layer to zoom to.
        :param margin_ratio: Fraction of the extent to add as margin (default is 0.1, or 10%).
        """
        project = QgsProject.instance()
        layers = project.mapLayersByName(layer_name)  # Get layers matching the name
        
        if not layers:
            print(f"Layer '{layer_name}' not found.")
            return
        
        layer = layers[0]  # Use the first matching layer
        iface = qgis.utils.iface  # Access the QGIS interface
        canvas = iface.mapCanvas()  # Get the active map canvas
        
        # Ensure the canvas CRS matches the layer CRS
        canvas.setDestinationCrs(layer.crs())
        
        # Get the layer's extent and add a margin
        layer_extent = layer.extent()
        x_margin = layer_extent.width() * margin_ratio
        y_margin = layer_extent.height() * margin_ratio
        
        expanded_extent = QgsRectangle(
            layer_extent.xMinimum() - x_margin,
            layer_extent.yMinimum() - y_margin,
            layer_extent.xMaximum() + x_margin,
            layer_extent.yMaximum() + y_margin
        )
        
        # Set the expanded extent to the canvas
        canvas.setExtent(expanded_extent)
        canvas.refresh()
        
        print(f"Zoomed to layer extent with margin: {expanded_extent.toString()}")

    def auth(self):
        """
        Authenticates Earth Engine and validates the default project.
        Warnings are displayed only if the default project is invalid.
        """
        try:
            # Step 1: Authenticate and initialize Earth Engine
            print("Authenticating Earth Engine...")
            ee.Authenticate()
            ee.Initialize(project=self.project_QgsPasswordLineEdit.text())
            print("Authentication successful!")

            # Step 2: Test default project
            print("Testing default project...")
            default_project_path = f"projects/{self.project_QgsPasswordLineEdit.text()}/assets/"  # Replace with your default project's path if known

            # Attempt to list assets in the default project
            try:
                assets = ee.data.listAssets({'parent': default_project_path})
                print(f"Assets in default project: {assets}")

                if assets.get('assets') is not None:  # Valid project detected
                    print("Default project is valid.")
                    self.pop_aviso_auth("Authentication successful!")
                    self.autentication = True
                    self.load_vector_layers()
                    self.load_path_sugestion()
                    self.next_clicked()
                else:
                    print("Default project is valid but contains no assets.")  # No warning needed for this case
            except ee.EEException as e:
                # Invalid project or access issue
                print(f"Default project validation failed: {e}")
                self.pop_aviso_auth(f"Default project validation failed: {e}\nFollow the instructions to have a valid Google Cloud project.")
                self.auth_clear(True)


        except ee.EEException as e:
            # Handle Earth Engine-specific errors
            print(f"Earth Engine error: {e}")
            if "Earth Engine client library not initialized" in str(e):
                message = "Authentication failed. Please authenticate again."
                print(message)
                self.pop_aviso_auth(message)
            else:
                message = f"An error occurred during authentication or initialization: {e}"
                print(message)
                self.pop_aviso_auth(message)
                self.auth_clear(True)

        except Exception as e:
            # Handle unexpected errors
            message = f"An unexpected error occurred: {e}"
            print(message)
            self.pop_aviso_auth(message)

    def auth_clear(self, silent=False):
        """
        Completely clears Earth Engine authentication by deleting the entire
        Earth Engine configuration directory, including credentials and cached data.
        """
        self.project_QgsPasswordLineEdit.clear()
        self.autenticacao.setEnabled(False)
        self.autentication = False


        system = platform.system()
        
        # Determine the Earth Engine configuration directory based on OS.
        if system == 'Windows':
            config_dir = os.path.join(os.environ['USERPROFILE'], '.config', 'earthengine')
        elif system in ['Linux', 'Darwin']:  # Linux or MacOS (Darwin)
            config_dir = os.path.join(os.environ['HOME'], '.config', 'earthengine')
        else:
            raise Exception(f"Unsupported operating system: {system}")
        
        # Check if the configuration directory exists and delete it.
        if os.path.exists(config_dir):
            try:
                shutil.rmtree(config_dir)
                if not silent:
                    message = "Earth Engine configuration cleared successfully (all files deleted)."
                    print(message)
                    self.pop_aviso_auth(message)
            except Exception as e:
                message = f"Error clearing Earth Engine configuration: {e}"
                print(message)
                self.pop_aviso_auth(message)
        else:
            message = "No Earth Engine configuration found to clear."
            print(message)
            self.pop_aviso_auth(message)

    def pop_aviso(self, aviso):
        QApplication.restoreOverrideCursor()
        msg = QMessageBox(parent=self)
        msg.setWindowTitle("Alerta!")
        msg.setIcon(QMessageBox.Icon.Warning)
        msg.setText(aviso)
        msg.setStandardButtons(QMessageBox.Icon.Warning | QMessageBox.Cancel)  # Add Ok and Cancel buttons

        ret = msg.exec()  # Get the result of the dialog

        if ret == QMessageBox.StandardButton.Ok:
            
            # Handle Ok button click
            print("Ok button clicked")
            # Add your code here for what to do when Ok is clicked
            return True
        elif ret == QMessageBox.Cancel:
            
            # Handle Cancel button click
            print("Cancel button clicked")
            # Add your code here for what to do when Cancel is clicked
            return False

    def load_vector_function(self):
        """
        Loads the vector layer from the selected file path, reprojects it to EPSG:4326,
        dissolves multiple features if necessary, and converts it into an Earth Engine
        FeatureCollection representing the AOI.
        """
        shapefile_path = self.selected_aoi_layer_path
        self.aoi = None  # Ensure the attribute exists to avoid AttributeError

        try:
            # Load the shapefile, handling both .zip archives and regular files.
            if shapefile_path.endswith('.zip'):
                with zipfile.ZipFile(shapefile_path, 'r') as zip_ref:
                    shapefile_found = False
                    for file in zip_ref.namelist():
                        if file.endswith('.shp'):
                            shapefile_found = True
                            shapefile_within_zip = file
                            break
                    if not shapefile_found:
                        print("No .shp file found inside the zip archive.")
                        return

                    # Read shapefile directly from the zip archive.
                    self.aoi = gpd.read_file(f"zip://{shapefile_path}/{shapefile_within_zip}")
            else:
                self.aoi = gpd.read_file(shapefile_path)

            # Reproject the GeoDataFrame to EPSG:4326 to ensure correct coordinates for Earth Engine.
            self.aoi = self.aoi.to_crs(epsg=4326)

            if self.aoi.empty:
                print("The shapefile does not contain any geometries.")
                return

            # Dissolve multiple features into a single geometry if necessary.
            if len(self.aoi) > 1:
                self.aoi = self.aoi.dissolve()

            # Extract the first geometry.
            geometry = self.aoi.geometry.iloc[0]

            # Validate the geometry type.
            if geometry.geom_type not in ['Polygon', 'MultiPolygon']:
                print("The geometry is not a valid type (Polygon or MultiPolygon).")
                return

            # Convert the geometry to GeoJSON format.
            geojson = geometry.__geo_interface__

            # Remove any third dimension from the coordinates.
            if geojson['type'] == 'Polygon':
                geojson['coordinates'] = [list(map(lambda coord: coord[:2], ring)) for ring in geojson['coordinates']]
            elif geojson['type'] == 'MultiPolygon':
                geojson['coordinates'] = [
                    [list(map(lambda coord: coord[:2], ring)) for ring in polygon]
                    for polygon in geojson['coordinates']
                ]

            # Create an Earth Engine geometry object.
            ee_geometry = ee.Geometry(geojson)
            feature = ee.Feature(ee_geometry)
            self.aoi = ee.FeatureCollection([feature])

            print("AOI defined successfully.")
            self.aoi_set = True

        except Exception as e:
            print(f"Error in load_vector_function: {e}")
            return

    def pop_warning(self, aviso):
        QApplication.restoreOverrideCursor()
        msg = QMessageBox(self)
        if self.language == "pt":
            msg.setWindowTitle("Aviso!")
        else:
            msg.setWindowTitle("Warning!")

        msg.setIcon(QMessageBox.Icon.Warning)
        msg.setText(aviso)
        msg.addButton(QMessageBox.StandardButton.Ok)
        msg.setStyleSheet("font-size: 10pt;")
        msg.exec()

    def elevacao_clicked(self):

        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)  

        try: 
            self.elevacao_workflow()
            QApplication.restoreOverrideCursor()
        except Exception as e:
            print(f"Error in elevacao_workflow: {e}")
            QApplication.restoreOverrideCursor()
            self.pop_aviso(f"Error in elevation data processing: {e}")
            return

    def elevacao_workflow(self):

        # Assuming 'self.aoi' holds the Earth Engine FeatureCollection
        # --- KEY CHANGE: Use updateMask for more reliable clipping ---
        # Apply buffer if needed. Ensure self.aoi is an ee.Geometry object.
        aoi = self.apply_buffer(self.aoi)

        DEM_source_key = self.dem_dataset_combobox.currentText()
        DEM_source_id = self.dem_datasets[DEM_source_key]["ID"]
        DEM_resolution = int(self.dem_resolution_combobox.currentText())
        print(f"Selected DEM source: {DEM_source_key} ({DEM_source_id})", DEM_resolution)

        # Replace invalid characters in DEM source ID for filenames
        safe_dem_source_id = DEM_source_id.replace("/", "_").replace("\\", "_")

        # Fetch DEM image based on selected source
        if DEM_source_id == 'COPERNICUS/DEM/GLO30':
            dem = ee.ImageCollection(DEM_source_id).select('DEM').mosaic().clip(aoi)
        elif DEM_source_id == 'JAXA/ALOS/AW3D30/V3_2':
            dem = ee.ImageCollection(DEM_source_id).select('DSM').mosaic().clip(aoi)
        elif DEM_source_id == 'NASA/NASADEM_HGT/001':
            dem = ee.Image(DEM_source_id).select('elevation').clip(aoi)
        elif DEM_source_id == 'USGS/GMTED2010_FULL':
            dem = ee.Image(DEM_source_id).select('min').clip(aoi)
        elif DEM_source_id == 'ASTER/ASTGTM':
            dem = ee.Image(DEM_source_id).select('elevation').clip(aoi)
        else:
            dem = ee.Image(DEM_source_id).clip(aoi).select('elevation')

        # Create a temporary file to store the downloaded DEM
        with tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as tmp_file:
            temp_output_file = tmp_file.name

        final_image = dem.toFloat()


        # 1. Create an explicit mask from the AOI geometry.
        # ee.Image(1).clip(aoi) creates an image where the AOI is 1 and outside is 0.
        # .mask() converts this to a mask where 1 is valid data and 0 is NoData.
        mask = ee.Image(1).clip(aoi).mask()

        # 2. Apply this mask to the final composite image.
        # This operation sets pixels outside the mask to NoData (transparent).
        final_image_masked = final_image.updateMask(mask)

        # 3. Define the download region using the BOUNDING BOX of the AOI.
        # This ensures the downloaded GeoTIFF is a rectangle that fully covers the AOI.
        # The actual clipping to the irregular shape is handled by updateMask.
        download_region = aoi.geometry().bounds().getInfo()

        try:
            url = final_image_masked.getDownloadUrl({
                'scale': DEM_resolution,
                'region': download_region,
                'format': 'GeoTIFF'
            })

            response = requests.get(url)
            if response.status_code == 200:
                with open(temp_output_file, 'wb') as file:
                    file.write(response.content)
                print(f"DEM image downloaded to temporary file: {temp_output_file}")
            else:
                print(f"Failed to download DEM image: {response.status_code}")
                return

            # Load the vector layer for clipping
            vector_layer = QgsVectorLayer(self.selected_aoi_layer_path, "Vector Layer", "ogr")
            if not vector_layer.isValid():
                print(f"Error: Vector layer '{self.selected_aoi_layer_path}' is invalid.")
                # Clean up the temporary file
                os.remove(temp_output_file)
                return

            # Generate a unique name for the output, including DEM source ID
            output_path = self.get_unique_filename(safe_dem_source_id)
            layer_name = self.vector_layer_combobox.currentText() + f' - {safe_dem_source_id}'

            # Move the downloaded file to the output path and load it directly in QGIS
            try:
                shutil.move(temp_output_file, output_path)
                print(f"Downloaded DEM moved to: {output_path}")

                # Load and add the raster to the map canvas (no local clipping)
                self._load_raster_to_canvas(output_path, layer_name)

            except Exception as e:
                print(f"Error moving or loading raster: {str(e)}")
                # If something failed, try to remove temp file if it still exists
                if os.path.exists(temp_output_file):
                    try:
                        os.remove(temp_output_file)
                    except Exception:
                        pass

        except Exception as e:
            print(f"Error during download: {e}")
            self.pop_aviso(f"Error during download: {e}")
            return
        
    def _load_raster_to_canvas(self, raster_path, layer_name):
        """Loads a raster with single band pseudocolor rendering (Magma style) to the QGIS canvas,
        dynamically determining the data range. This version does not perform any clipping."""
        raster_layer = QgsRasterLayer(raster_path, layer_name)
        if not raster_layer.isValid():
            print(f"Failed to load raster layer from '{raster_path}'.")
            return

        # Get min and max values from the raster
        provider = raster_layer.dataProvider()
        stats = provider.bandStatistics(1)
        min_val = stats.minimumValue
        max_val = stats.maximumValue

        print(f"Using data range {min_val} to {max_val} for rendering.")

        QgsProject.instance().addMapLayer(raster_layer, False)
        root = QgsProject.instance().layerTreeRoot()
        root.insertChildNode(0, QgsLayerTreeLayer(raster_layer))
        print("Raster layer loaded successfully!")

        # Create a color ramp shader
        color_ramp_shader = QgsColorRampShader()
        color_ramp_shader.setColorRampType(QgsColorRampShader.Interpolated)

        # Load the predefined "Magma" color ramp from the QGIS style manager
        style = QgsStyle().defaultStyle()
        color_ramp = style.colorRamp('Magma')

        # Check if the color ramp is successfully loaded
        if color_ramp:
            # Define the number of color stops (adjust as needed)
            num_stops = 5
            step = (max_val - min_val) / (num_stops - 1)

            # Create color ramp items using the actual data range
            color_ramp_items = []
            for i in range(num_stops):
                value = min_val + i * step
                color = color_ramp.color(i / (num_stops - 1))  # Interpolates color along the ramp
                color_ramp_items.append(QgsColorRampShader.ColorRampItem(value, color))

            # Set the color ramp items to the color ramp shader
            color_ramp_shader.setColorRampItemList(color_ramp_items)
        else:
            print("Color ramp 'Magma' not found in the QGIS style library.")
            return  # Exit if the color ramp is not found

        # Create a raster shader and set it to use the color ramp shader
        raster_shader = QgsRasterShader()
        raster_shader.setRasterShaderFunction(color_ramp_shader)

        # Apply the raster shader to the raster layer renderer
        renderer = QgsSingleBandPseudoColorRenderer(raster_layer.dataProvider(), 1, raster_shader)

        # Set the classification range to match the data range
        renderer.setClassificationMin(min_val)
        renderer.setClassificationMax(max_val)

        raster_layer.setRenderer(renderer)

        # Refresh the layer to update the visualization
        raster_layer.triggerRepaint()
