from qgis.core import (QgsProject, QgsRasterLayer, QgsMapLayer, QgsMapSettings, QgsMapRendererCustomPainterJob, QgsMessageLog, Qgis,     QgsProcessing, QgsProcessingFeedback, QgsApplication, QgsVectorLayer, QgsTask, QgsProcessingUtils, QgsProcessingAlgRunnerTask, QgsProcessingContext, QgsField, QgsFields, QgsVectorFileWriter, QgsFeature, QgsDistanceArea, QgsCoordinateReferenceSystem)
from qgis.PyQt.QtWidgets import (QDockWidget, QGraphicsScene, QGraphicsPixmapItem, QPushButton, QVBoxLayout, QPushButton, QLabel, QDoubleSpinBox, QVBoxLayout, QFormLayout, QGroupBox, QProgressBar, QApplication, QMessageBox, QRadioButton, QButtonGroup, QWidget, QHBoxLayout)
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry, QgsZonalStatistics
from qgis.PyQt.QtGui import QImage, QPainter, QPixmap, QColor
from qgis.PyQt.QtCore import Qt, QSize, QVariant
from functools import partial
from qgis.utils import iface
from qgis import processing
from qgis.PyQt import uic
import unicodedata
import subprocess
import traceback
import tempfile
import shutil
import math
import re
import os

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'CriarRedesDrenagem.ui'))

class RedesDrenagem(QDockWidget, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(RedesDrenagem, self).__init__(parent)

        # Configura a interface do usuário a partir do Designer.
        self.setupUi(self)

        # Altera o título da janela
        self.setWindowTitle("Redes de Drenagem")

        # Armazena a referência da interface QGIS
        self.iface = iface

        # Conjunto de camadas raster com nameChanged conectado
        self._raster_layers_connected = set()

        # Cria uma cena gráfica para o QGraphicsView
        self.scene = QGraphicsScene(self)
        self.graphicsViewRaster.setScene(self.scene)

        # Flag usada pelo mostrar_mensagem
        self._lock_messagebar = False

        # Adiciona o dock widget à interface QGIS na parte inferior
        self.iface.addDockWidget(Qt.BottomDockWidgetArea, self)

        # Inicializa o ComboBox de Raster
        self.init_combo_box_raster()

        # textEdit com resumo do raster (somente leitura)
        self.textEditMDT.setReadOnly(True)
        # opcional: deixa mais "tabelado"
        self.textEditMDT.setFontFamily("Consolas")
        self.textEditMDT.setFontPointSize(9)

        # Cria controles específicos de SAGA dentro do scrollAreaAtividades
        self._setup_saga_controls()

        # Cria controles específicos de TauDEM dentro do scrollAreaAtividades
        self._setup_taudem_controls()

        # Cria controles específicos de WhiteboxTools dentro do scrollAreaAtividades
        self._setup_whitebox_controls()

        # Conecta seleção de motor (SAGA / TauDEM / WhiteboxTools)
        self.radioButtonSAGA.toggled.connect(self._update_motor_ui)
        self.radioButtonTauDem.toggled.connect(self._update_motor_ui)
        self.radioButtonTools.toggled.connect(self._update_motor_ui)

        # Atualiza a interface inicial conforme o radio selecionado
        self._update_motor_ui()

        self._safe_dem_cache = {} #Salva o Raster Usado

        # Botão Executar começa desabilitado
        self.pushButtonExecutar.setEnabled(False)

        # Conecta os sinais aos slots
        self.connect_signals()

    def connect_signals(self):
        """Conecta os sinais do combo e do projeto QGIS."""
        # Quando mudar o raster selecionado, atualiza a visualização
        self.comboBoxRaster.currentIndexChanged.connect(self.display_raster)

        # botão executar
        self.pushButtonExecutar.clicked.connect(self.on_executar_clicked)

        # botão profundidade mínima (poças / alagamento local)
        self.pushButtonPronfundidade.clicked.connect(self.on_pushButtonPronfundidade_clicked)

        self.comboBoxRaster.currentIndexChanged.connect(self._update_executar_button_state)
        self.comboBoxRaster.currentIndexChanged.connect(self._update_action_buttons_state)

        # Motores também atualizam o estado do botão
        self.radioButtonSAGA.toggled.connect(self._update_executar_button_state)
        self.radioButtonTauDem.toggled.connect(self._update_executar_button_state)
        self.radioButtonTools.toggled.connect(self._update_executar_button_state)
        self.radioButtonSAGA.toggled.connect(self._update_action_buttons_state)
        self.radioButtonTauDem.toggled.connect(self._update_action_buttons_state)
        self.radioButtonTools.toggled.connect(self._update_action_buttons_state)

        # Sinais do projeto para atualizar o combo quando camadas mudarem
        project = QgsProject.instance()
        project.layersAdded.connect(self.on_layers_added)
        project.layersRemoved.connect(self.on_layers_removed)
        project.cleared.connect(self.init_combo_box_raster)

    def on_layers_added(self, layers):
        """Chamado quando camadas são adicionadas ao projeto."""
        self.update_combo_box()

    def on_layers_removed(self, layer_ids):
        """Chamado quando camadas são removidas do projeto."""
        self.update_combo_box()

    def showEvent(self, event):
        """
        Sobrescreve o evento de exibição do diálogo para resetar os Widgets.
        """
        self.display_raster()
        self.init_combo_box_raster()
        self._update_action_buttons_state()
        self._update_executar_button_state()
        
        super(RedesDrenagem, self).showEvent(event)

    def closeEvent(self, event):
        """
        Quando o diálogo/dock é fechado, removemos sincronizações relevantes.
        """
        project = QgsProject.instance()
        # Desconecta sinais do projeto (evita chamadas depois do dock destruído)
        try:
            project.layersAdded.disconnect(self.on_layers_added)
        except TypeError:
            pass
        try:
            project.layersRemoved.disconnect(self.on_layers_removed)
        except TypeError:
            pass
        try:
            project.cleared.disconnect(self.init_combo_box_raster)
        except TypeError:
            pass

        # Desconecta nameChanged das camadas raster
        for layer_id in list(self._raster_layers_connected):
            layer = QgsProject.instance().mapLayer(layer_id)
            if layer is not None:
                try:
                    layer.nameChanged.disconnect(self.on_raster_name_changed)
                except TypeError:
                    pass
        self._raster_layers_connected.clear()

        super(RedesDrenagem, self).closeEvent(event)

    def init_combo_box_raster(self):
        """
        Inicializa o comboBox de raster, populando-o com as camadas raster do projeto
        e ajusta a seleção e visualização conforme necessário.
        """
        # Armazenar o ID atualmente selecionado
        current_raster_id = self.comboBoxRaster.currentData()

        layers = QgsProject.instance().mapLayers().values()
        raster_layers = []
        for layer in layers:
            if not isinstance(layer, QgsRasterLayer):
                continue

            # pula rasters que estão nos grupos de drenagem gerados pelo plugin
            if self._layer_in_excluded_group(layer.id()):
                continue

            raster_layers.append(layer)

        # Evita disparar currentIndexChanged enquanto repovoa
        self.comboBoxRaster.blockSignals(True)
        self.comboBoxRaster.clear()

        for raster_layer in raster_layers:
            self.comboBoxRaster.addItem(raster_layer.name(), raster_layer.id())

        # Restaura seleção anterior, se possível
        if current_raster_id:
            index = self.comboBoxRaster.findData(current_raster_id)
            if index != -1:
                self.comboBoxRaster.setCurrentIndex(index)
            elif self.comboBoxRaster.count() > 0:
                self.comboBoxRaster.setCurrentIndex(0)
        else:
            if self.comboBoxRaster.count() > 0:
                self.comboBoxRaster.setCurrentIndex(0)

        self.comboBoxRaster.blockSignals(False)

        # Atualiza conexões de nameChanged para as camadas raster atuais
        self._refresh_raster_name_connections(raster_layers)

        # Atualiza a visualização do raster selecionado (se houver)
        self.display_raster()

        # Garante que o estado do botão Executar reflita o novo combo
        self._update_executar_button_state()
        self._update_action_buttons_state()

    def _refresh_raster_name_connections(self, raster_layers):
        """
        (Re)conecta o sinal nameChanged das camadas raster atuais
        para que o comboBoxRaster seja atualizado imediatamente
        quando o nome da camada for alterado.
        """
        project = QgsProject.instance()

        # Garante que o atributo exista
        if not hasattr(self, "_raster_layers_connected"):
            self._raster_layers_connected = set()

        # Desconecta sinais antigos
        for layer_id in list(self._raster_layers_connected):
            layer = project.mapLayer(layer_id)
            if layer is not None:
                try:
                    layer.nameChanged.disconnect(self.on_raster_name_changed)
                except TypeError:
                    pass
        self._raster_layers_connected.clear()

        # Conecta sinais para as camadas raster atuais
        for layer in raster_layers:
            try:
                layer.nameChanged.connect(self.on_raster_name_changed)
                self._raster_layers_connected.add(layer.id())
            except TypeError:
                # Se já estiver conectado, ignora
                pass

    def on_raster_name_changed(self):
        """
        Atualiza o texto do comboBoxRaster quando o nome de uma camada raster muda.
        """
        layer = self.sender()
        if not isinstance(layer, QgsRasterLayer):
            return

        layer_id = layer.id()
        new_name = layer.name()

        # Atualiza o item correspondente no comboBoxRaster
        for i in range(self.comboBoxRaster.count()):
            if self.comboBoxRaster.itemData(i) == layer_id:
                self.comboBoxRaster.setItemText(i, new_name)
                break

    def update_combo_box(self):
        """
        Atualiza o comboBox de raster ao adicionar ou remover camadas no projeto,
        tentando manter a seleção anterior quando possível.
        """
        # Armazena a seleção atual para restaurar após a atualização
        current_index = self.comboBoxRaster.currentIndex()
        current_layer_id = self.comboBoxRaster.itemData(current_index)

        # Atualiza o combo box
        self.init_combo_box_raster()

        # Tenta restaurar a seleção anterior
        if current_layer_id:
            index = self.comboBoxRaster.findData(current_layer_id)
            if index != -1:
                self.comboBoxRaster.setCurrentIndex(index)
            else:
                if self.comboBoxRaster.count() > 0:
                    self.comboBoxRaster.setCurrentIndex(0)

    def _layer_in_excluded_group(self, layer_id: str) -> bool:
        """
        Retorna True se a camada (layer_id) estiver dentro de algum
        dos grupos de drenagem gerados pelo plugin:
        - 'SAGA - Drenagem'
        - 'TauDEM - Drenagem'
        - 'WhiteboxTools - Drenagem'
        """
        excluded_groups = {"SAGA - Drenagem", "TauDEM - Drenagem", "WhiteboxTools - Drenagem",}

        project = QgsProject.instance()
        root = project.layerTreeRoot()
        if root is None:
            return False

        node = root.findLayer(layer_id)
        if node is None:
            # camada ainda não está em nenhuma árvore (ou foi removida)
            return False

        parent = node.parent()
        from qgis.core import QgsLayerTreeGroup  # se já estiver importado no topo, pode remover esta linha

        while parent is not None:
            if isinstance(parent, QgsLayerTreeGroup):
                if parent.name() in excluded_groups:
                    return True
            parent = parent.parent()

        return False

    def display_raster(self):
        """
        Renderiza a camada raster selecionada no QGraphicsView, ajustando a visualização
        e atualiza o resumo no textEditMDT.
        """
        # Garante que a cena exista
        if not hasattr(self, "scene") or self.scene is None:
            self.scene = QGraphicsScene(self)
            self.graphicsViewRaster.setScene(self.scene)

        # Limpa a cena antes de adicionar um novo item
        self.scene.clear()

        # Obtém o ID da camada raster selecionada
        selected_raster_id = self.comboBoxRaster.currentData()
        if not selected_raster_id:
            # nada selecionado → limpa o textEdit também
            self.textEditMDT.clear()
            self.textEditMDT.setPlainText("Nenhum raster selecionado.")
            return

        # Busca a camada raster pelo ID
        selected_layer = QgsProject.instance().mapLayer(selected_raster_id)

        # Atualiza o textEditMDT com as informações do raster
        if isinstance(selected_layer, QgsRasterLayer):
            self._update_mdt_info()
        else:
            self.textEditMDT.clear()
            self.textEditMDT.setPlainText("Nenhum raster selecionado.")
            return

        if isinstance(selected_layer, QgsRasterLayer):
            # Configurações do mapa
            map_settings = QgsMapSettings()
            map_settings.setLayers([selected_layer])
            map_settings.setBackgroundColor(QColor(255, 255, 255))

            width = self.graphicsViewRaster.viewport().width()
            height = self.graphicsViewRaster.viewport().height()
            if width <= 0 or height <= 0:
                return

            map_settings.setOutputSize(QSize(width, height))
            map_settings.setExtent(selected_layer.extent())

            image = QImage(width, height, QImage.Format_ARGB32)
            image.fill(Qt.transparent)

            painter = QPainter(image)
            render_job = QgsMapRendererCustomPainterJob(map_settings, painter)

            render_job.start()
            render_job.waitForFinished()
            painter.end()

            pixmap = QPixmap.fromImage(image)
            pixmap_item = QGraphicsPixmapItem(pixmap)

            self.scene.addItem(pixmap_item)
            self.graphicsViewRaster.setSceneRect(pixmap_item.boundingRect())
            self.graphicsViewRaster.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)

    def _update_mdt_info(self):
        """
        Atualiza o textEditMDT com informações do raster
        atualmente selecionado no comboBoxRaster.
        """
        if not hasattr(self, "textEditMDT"):
            return

        layer_id = self.comboBoxRaster.currentData()
        layer = QgsProject.instance().mapLayer(layer_id)

        if not isinstance(layer, QgsRasterLayer):
            self.textEditMDT.setHtml("<b>Nenhum MDT selecionado.</b>")
            return

        pr = layer.dataProvider()
        extent = layer.extent()
        crs = layer.crs()

        cols = layer.width()
        rows = layer.height()

        # Resolução (tamanho de pixel)
        res_x = extent.width() / cols if cols else 0
        res_y = extent.height() / rows if rows else 0

        # Área total aproximada (em unidades² do CRS)
        area = extent.width() * extent.height()

        # Se o CRS for em metros, mostra também em hectares
        area_info = f"{area:,.2f} (unidades² do CRS)"
        try:
            from qgis.core import QgsUnitTypes
            if crs.mapUnits() == QgsUnitTypes.DistanceMeters:
                area_ha = area / 10000.0
                area_info = f"{area:,.2f} m² ({area_ha:,.2f} ha)"
        except Exception:
            # Se der qualquer problema com QgsUnitTypes, fica só com unidades²
            pass

        # Fonte (tira parâmetros muito grandes se quiser)
        source = pr.dataSourceUri()
        # Opcional: só o caminho até o " |" (caso seja um raster no disco)
        if " |" in source:
            source = source.split(" |", 1)[0]

        html = []

        html.append(f"<b>Nome:</b> {layer.name()}")
        html.append(f"<b>Fonte:</b> {source}")
        html.append(f"<b>CRS:</b> {crs.authid()} - {crs.description()}")

        html.append("<b>Extensão:</b>")
        html.append(f"  Xmin: {extent.xMinimum():.3f}")
        html.append(f"  Xmax: {extent.xMaximum():.3f}")
        html.append(f"  Ymin: {extent.yMinimum():.3f}")
        html.append(f"  Ymax: {extent.yMaximum():.3f}")

        html.append(f"<b>Dimensões (col x lin):</b> {cols} x {rows}")
        html.append(f"<b>Resolução:</b> {res_x:.3f} x {res_y:.3f} (unidades do CRS)")
        html.append(f"<b>Área (aprox.):</b> {area_info}")

        # Se quiser, pode manter mais campos aqui (tipo de pixel, NoData, etc.)

        self.textEditMDT.setReadOnly(True)
        self.textEditMDT.setHtml("<br>".join(html))

    def mostrar_mensagem(self, texto, tipo, duracao=3, caminho_pasta=None, caminho_arquivo=None, forcar=False):
        """
        Exibe uma mensagem na barra de mensagens do QGIS, proporcionando feedback ao usuário
        baseado nas ações realizadas.
        Agora ela respeita um "cadeado" (self._lock_messagebar) para não sobrepor barras
        de progresso que estejam sendo exibidas.

        :param texto: Texto da mensagem a ser exibida.
        :param tipo: Tipo da mensagem ("Erro" ou "Sucesso") que determina a cor e o ícone da mensagem.
        :param duracao: Duração em segundos durante a qual a mensagem será exibida (padrão é 3 segundos).
        :param caminho_pasta: Caminho da pasta a ser aberta ao clicar no botão (padrão é None).
        :param caminho_arquivo: Caminho do arquivo a ser executado ao clicar no botão (padrão é None).
        :param forcar: Se True, mostra na barra mesmo que ela esteja "travada" por uma barra de progresso.
        """
        # se o messageBar estiver "reservado" por uma barra de progresso, não vamos empurrar nada
        # a menos que o chamador peça explicitamente (forcar=True)
        if getattr(self, "_lock_messagebar", False) and not forcar:
            # manda pro log do QGIS para não perder a mensagem
            # # (Menu → Exibir → Painéis → Registro de mensagens → "Tempo Salvo Tools")
            # QgsMessageLog.logMessage(f"{tipo}: {texto}", "Tempo Salvo Tools", level=Qgis.Info)
            return

        # barra de mensagens do QGIS
        bar = self.iface.messageBar()

        if tipo == "Erro":
            bar.pushMessage("Erro", texto, level=Qgis.Critical, duration=duracao)

        elif tipo == "Sucesso":
            msg = bar.createMessage("Sucesso", texto)

            if caminho_pasta:
                botao_abrir_pasta = QPushButton("Abrir Pasta")
                botao_abrir_pasta.clicked.connect(lambda: os.startfile(caminho_pasta))
                msg.layout().insertWidget(1, botao_abrir_pasta)

            if caminho_arquivo:
                botao_executar = QPushButton("Executar")
                botao_executar.clicked.connect(lambda: os.startfile(caminho_arquivo))
                msg.layout().insertWidget(2, botao_executar)

            bar.pushWidget(msg, level=Qgis.Info, duration=duracao)

        else:
            # caso venha "Info" ou outro tipo
            bar.pushMessage("Info", texto, level=Qgis.Info, duration=duracao)

    def _setup_saga_controls(self):
        """
        Cria, dentro do scrollAreaAtividades, os controles de parâmetros
        específicos do SAGA (MINSLOPE e THRESHOLD).
        """
        container = self.scrollAreaAtividades.widget()
        if container is None:
            return

        layout = container.layout()
        if layout is None:
            layout = QVBoxLayout(container)
            container.setLayout(layout)

        # Grupo visual só pra organizar
        self.groupSagaParams = QGroupBox("Parâmetros SAGA - Drenagem", container)
        form = QFormLayout(self.groupSagaParams)

        # MINSLOPE
        label_minslope = QLabel("Declividade mínima (MINSLOPE %):", self.groupSagaParams)
        self.spinMINSLOPE = QDoubleSpinBox(self.groupSagaParams)
        self.spinMINSLOPE.setDecimals(3)
        self.spinMINSLOPE.setSingleStep(0.01)
        self.spinMINSLOPE.setRange(0.0, 10.0)   # 0% a 10%
        self.spinMINSLOPE.setValue(0.1)         # padrão: 0,1%

        form.addRow(label_minslope, self.spinMINSLOPE)

        # THRESHOLD
        label_threshold = QLabel("Ordem mínima de canal (THRESHOLD):", self.groupSagaParams)
        self.spinTHRESHOLD = QDoubleSpinBox(self.groupSagaParams)
        self.spinTHRESHOLD.setDecimals(0)       # queremos valores inteiros
        self.spinTHRESHOLD.setSingleStep(1.0)
        self.spinTHRESHOLD.setRange(1.0, 20.0)  # limite prático: 1 a 20
        self.spinTHRESHOLD.setValue(5.0)        # padrão típico

        form.addRow(label_threshold, self.spinTHRESHOLD)

        layout.addWidget(self.groupSagaParams)

    def _update_motor_ui(self):
        """
        Mostra/esconde os grupos de parâmetros conforme o motor selecionado.
        - SAGA: mostra groupSagaParams
        - TauDEM: mostra groupTauDemParams
        - WhiteboxTools: mostra groupWbtParams
        """
        is_saga = self.radioButtonSAGA.isChecked()
        is_taudem = self.radioButtonTauDem.isChecked()
        is_wbt = self.radioButtonTools.isChecked()

        if hasattr(self, "groupSagaParams"):
            self.groupSagaParams.setVisible(is_saga)

        if hasattr(self, "groupTauDemParams"):
            self.groupTauDemParams.setVisible(is_taudem)

        if hasattr(self, "groupWbtParams"):
            self.groupWbtParams.setVisible(is_wbt)

        # Atualiza também o estado do botão Executar
        self._update_executar_button_state()
        self._update_action_buttons_state()

    def on_executar_clicked(self):
        """
        Handler do botão Executar.
        """
        # garante que há um raster selecionado
        layer_id = self.comboBoxRaster.currentData()
        dem_layer = QgsProject.instance().mapLayer(layer_id)

        if not isinstance(dem_layer, QgsRasterLayer):
            self.mostrar_mensagem("Selecione um raster válido no combo antes de executar.", "Erro")
            return

        # Descobre qual motor está selecionado
        if self.radioButtonSAGA.isChecked():
            self._executar_saga_drenagem(dem_layer)

        elif self.radioButtonTauDem.isChecked():
            self._executar_taudem_drenagem(dem_layer)

        elif self.radioButtonTools.isChecked():
            # WhiteboxTools
            self._executar_whitebox_drenagem(dem_layer)

        else:
            self.mostrar_mensagem("Selecione um motor (SAGA, TauDEM ou WhiteboxTools) antes de executar.", "Erro")

    @staticmethod
    def _make_ascii_filename(original_path: str, prefix: str = "") -> str:
        base = os.path.basename(original_path)
        nfkd = unicodedata.normalize("NFKD", base)
        ascii_name = nfkd.encode("ascii", "ignore").decode("ascii")
        ascii_name = ascii_name.replace(" ", "_")
        ascii_name = re.sub(r"[^A-Za-z0-9._-]", "_", ascii_name)
        if prefix:
            ascii_name = f"{prefix}_{ascii_name}"
        return ascii_name

    @staticmethod
    def _create_safe_workspace(prefix: str = "ts_drenagem_") -> str:
        return tempfile.mkdtemp(prefix=prefix)

    def _needs_safe_copy(self, path: str) -> bool:
        """
        Retorna True se o caminho tiver caracteres não ASCII ou espaços,
        ou seja, se for mais seguro copiar para um caminho 'limpo'.
        """
        # Espaços no caminho
        if " " in path:
            return True

        # Qualquer caractere não-ASCII (acentos, ç, etc.)
        if any(ord(ch) > 127 for ch in path):
            return True

        return False

    def _get_safe_dem_layer(self, dem_layer: QgsRasterLayer) -> QgsRasterLayer:
        """
        Retorna uma camada raster com caminho 'seguro' (sem acentos/espaços),
        reutilizando uma cópia já existente se possível.

        - Se não precisar de cópia, devolve o próprio dem_layer.
        - Se precisar, usa um cache para não copiar o mesmo arquivo toda hora.
        """
        original_dem_path = dem_layer.source()

        # Se não for um arquivo no disco, nem tenta copiar
        if not os.path.isfile(original_dem_path):
            return dem_layer

        # Se o caminho já é "ok", usa diretamente
        if not self._needs_safe_copy(original_dem_path):
            return dem_layer

        # Garante que o cache exista
        if not hasattr(self, "_safe_dem_cache"):
            self._safe_dem_cache = {}

        safe_path = self._safe_dem_cache.get(original_dem_path)

        # Se já temos um caminho seguro em cache e o arquivo ainda existe, tenta usar
        if safe_path and os.path.exists(safe_path):
            safe_layer = QgsRasterLayer(safe_path, dem_layer.name())
            if safe_layer.isValid():
                return safe_layer
            # se a camada não for válida, vamos recriar a cópia

        # Não tinha cache ou estava inválido → criar/atualizar cópia
        base_temp = tempfile.gettempdir()
        cache_dir = os.path.join(base_temp, "ts_drenagem_dem_cache")
        os.makedirs(cache_dir, exist_ok=True)

        safe_name = self._make_ascii_filename(original_dem_path, prefix="dem")
        safe_path = os.path.join(cache_dir, safe_name)

        try:
            shutil.copy2(original_dem_path, safe_path)
        except Exception as e:
            self.mostrar_mensagem(f"Erro ao copiar DEM para caminho temporário seguro: {e}", "Erro")
            # fallback: usa o próprio DEM original
            return dem_layer

        # Atualiza cache
        self._safe_dem_cache[original_dem_path] = safe_path

        safe_layer = QgsRasterLayer(safe_path, dem_layer.name())
        if not safe_layer.isValid():
            self.mostrar_mensagem("Falha ao carregar DEM copiado para caminho seguro.", "Erro")
            return dem_layer  # fallback

        return safe_layer

    def _executar_saga_drenagem(self, dem_layer: QgsRasterLayer):
        """
        Dispara o processamento de drenagem com SAGA em segundo plano
        usando QgsProcessingAlgRunnerTask (sem travar a interface).
        """
        # 1) Lê parâmetros configurados na interface (SAGA)
        minslope = 0.1
        if hasattr(self, "spinMINSLOPE"):
            minslope = self.spinMINSLOPE.value()

        strahler_threshold = 5
        if hasattr(self, "spinTHRESHOLD"):
            strahler_threshold = int(self.spinTHRESHOLD.value())

        # 2) Verifica se o provider SAGA NextGen está disponível
        provider = QgsApplication.processingRegistry().providerById("sagang")
        if provider is None:
            self.mostrar_mensagem(
                "Provider 'SAGA Next Gen' (sagang) não está disponível no Processing.\n"
                "Verifique em Configurações ► Opções ► Processamento ► Provedores.", "Erro" )
            return

        # 3) Obtém DEM em caminho "seguro" (pode ser o próprio ou cópia cacheada)
        safe_dem_layer = self._get_safe_dem_layer(dem_layer)
        if not safe_dem_layer or not safe_dem_layer.isValid():
            self.mostrar_mensagem("Falha ao obter DEM em caminho seguro para processamento.", "Erro")
            return

        # 4) Cria e inicia o runner assíncrono
        #    Guardamos em self para o objeto não ser coletado pelo GC
        self._saga_runner = SagaDrenagemRunner(plugin=self, dem_layer=dem_layer, safe_dem_layer=safe_dem_layer, minslope=minslope, strahler_threshold=strahler_threshold)
        self._saga_runner.start()

    def _add_saga_raster(self, layer: QgsRasterLayer, short_code: str):
        """
        Adiciona um raster gerado pelo SAGA ao projeto,
        dentro do grupo 'SAGA - Drenagem', sem expandir o grupo.
        """
        log_prefix = "SAGA - Tempo Salvo Tools"

        if not isinstance(layer, QgsRasterLayer) or not layer.isValid():
            QgsMessageLog.logMessage(f"[{short_code}] Raster SAGA inválido, não será adicionado.", log_prefix, level=Qgis.Warning)
            return

        project = QgsProject.instance()

        # Não adiciona direto na raiz
        project.addMapLayer(layer, addToLegend=False)

        root = project.layerTreeRoot()
        group_name = "SAGA - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        # Garante que nem o grupo nem a camada fiquem expandidos
        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

    def _add_saga_vector(self, layer: QgsVectorLayer, short_code: str):
        """
        Adiciona um vetor gerado pelo SAGA ao projeto,
        dentro do grupo 'SAGA - Drenagem', sem expandir o grupo.
        """
        log_prefix = "SAGA - Tempo Salvo Tools"

        if not isinstance(layer, QgsVectorLayer) or not layer.isValid():
            QgsMessageLog.logMessage(f"[{short_code}] Vetor SAGA inválido, não será adicionado.", log_prefix, level=Qgis.Warning)
            return

        project = QgsProject.instance()

        # Não adiciona direto na raiz
        project.addMapLayer(layer, addToLegend=False)

        root = project.layerTreeRoot()
        group_name = "SAGA - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

    def _setup_taudem_controls(self):
        """
        Cria, dentro do scrollAreaAtividades, os controles de parâmetros
        específicos do TauDEM (MINSLOPE e THRESHOLD, por enquanto).
        """
        container = self.scrollAreaAtividades.widget()
        if container is None:
            return

        layout = container.layout()
        if layout is None:
            layout = QVBoxLayout(container)
            container.setLayout(layout)

        # Grupo visual para organizar os parâmetros do TauDEM
        self.groupTauDemParams = QGroupBox("Parâmetros TauDEM - Drenagem", container)
        form = QFormLayout(self.groupTauDemParams)

        # THRESHOLD TauDEM
        label_threshold_tau = QLabel("Ordem mínima de canal (THRESHOLD):", self.groupTauDemParams)
        self.spinTauTHRESHOLD = QDoubleSpinBox(self.groupTauDemParams)
        self.spinTauTHRESHOLD.setDecimals(0)       # inteiros
        self.spinTauTHRESHOLD.setSingleStep(10.0)
        self.spinTauTHRESHOLD.setRange(1.0, 1000.0)  # limite prático
        self.spinTauTHRESHOLD.setValue(100.0)        # padrão típico

        form.addRow(label_threshold_tau, self.spinTauTHRESHOLD)

        layout.addWidget(self.groupTauDemParams)

        # Inicialmente escondido; quem controla é o _update_motor_ui
        self.groupTauDemParams.setVisible(False)

    def _executar_taudem_drenagem(self, dem_layer: QgsRasterLayer):
        """
        Dispara o processamento de drenagem com TauDEM em segundo plano
        usando QgsTask, sem registrar provider no Processing.
        """
        # 1) Garante que o TauDEM está configurado (GUI/main thread)
        if not TAUDEM_CONFIG.setup_taudem_config():
            return

        # 2) Obtém DEM em caminho "seguro"
        safe_dem_layer = self._get_safe_dem_layer(dem_layer)
        if not safe_dem_layer or not safe_dem_layer.isValid():
            self.mostrar_mensagem("Falha ao obter DEM em caminho seguro para processamento TauDEM.", "Erro")
            return

        # 3) Workspace temporário
        workspace = self._create_safe_workspace(prefix="ts_taudem_")

        # 4) Lê o THRESHOLD da interface (área mínima para canal)
        tau_threshold = 5.0
        if hasattr(self, "spinTauTHRESHOLD"):
            tau_threshold = float(self.spinTauTHRESHOLD.value())

        # 5) Cria a task TauDEM (agora com threshold)
        task = TauDEMDrenagemTask( plugin=self, dem_layer=dem_layer, safe_dem_layer=safe_dem_layer, workspace=workspace, threshold=tau_threshold)

        task.taskCompleted.connect(lambda: self._on_taudem_task_finished(task, True))
        task.taskTerminated.connect(lambda: self._on_taudem_task_finished(task, False))

        QgsApplication.taskManager().addTask(task)

        self.mostrar_mensagem("Processamento de drenagem com TauDEM iniciado em segundo plano...", "Info")

    def _on_taudem_task_finished(self, task, success: bool):
        """
        Slot chamado pelos sinais taskCompleted (success=True)
        e taskTerminated (success=False).
        Cuida de adicionar ao projeto:
        - FEL, P, SD8, AD8 (rasters básicos)
        - SRC (raster binário de canais)
        - ORD (ordem de canais)
        - W (raster de bacias)
        - NET (vetor de rede de drenagem)
        """
        log_prefix = "TauDEM - Tempo Salvo Tools"

        QgsMessageLog.logMessage(f"_on_taudem_task_finished chamado. success={success}", log_prefix, level=Qgis.Info)

        # Falha / cancelamento
        if not success:
            err = getattr(task, "error_message", None)
            if err:
                self.mostrar_mensagem(err, "Erro", forcar=True)
            elif task.isCanceled():
                self.mostrar_mensagem("Processamento TauDEM cancelado pelo usuário.", "Erro", forcar=True)
            else:
                self.mostrar_mensagem("Ocorreu uma falha no processamento TauDEM.", "Erro", forcar=True)
            return

        # CRS original do DEM
        dem_crs = task.dem_layer.crs()

        # Rede de drenagem vetorial (NET)
        self._add_taudem_vector(path=task.net_path, name=f"{task.dem_name} - Rede de drenagem (TauDEM)", dem_crs=dem_crs, short_code="NET")

        # Rasters básicos
        self._add_taudem_raster(path=task.fel_path, name=f"{task.dem_name} - FEL (TauDEM)",  dem_crs=dem_crs, short_code="FEL")

        self._add_taudem_raster(path=task.p_path,  name=f"{task.dem_name} - D8 FlowDir (P)", dem_crs=dem_crs, short_code="P")

        self._add_taudem_raster(path=task.sd8_path, name=f"{task.dem_name} - Slope D8 (SD8)", dem_crs=dem_crs, short_code="SD8")

        self._add_taudem_raster(path=task.ad8_path, name=f"{task.dem_name} - Área de contribuição D8 (AD8)", dem_crs=dem_crs, short_code="AD8")

        # Threshold: raster de canais (SRC)
        self._add_taudem_raster(path=task.src_path, name=f"{task.dem_name} - Canais (SRC TauDEM)", dem_crs=dem_crs, short_code="SRC")

        # Ordem de canais (ORD) - opcional
        self._add_taudem_raster(path=task.ord_path, name=f"{task.dem_name} - Ordem de canais (ORD TauDEM)", dem_crs=dem_crs, short_code="ORD")

        # Bacias (W raster)
        self._add_taudem_raster(path=task.w_path, name=f"{task.dem_name} - Bacias (W TauDEM)", dem_crs=dem_crs, short_code="W")

        # Mensagem final (apenas UMA vez)
        self.mostrar_mensagem("Processamento de drenagem com TauDEM concluído (em segundo plano).", "Sucesso", forcar=True)

    def _add_taudem_vector(self, path: str, name: str, dem_crs, short_code: str):
        """
        Adiciona um vetor TauDEM ao projeto, sem expandir grupos no painel de camadas.
        """
        log_prefix = "TauDEM - Tempo Salvo Tools"

        if not path or not os.path.exists(path):
            QgsMessageLog.logMessage(f"[{short_code}] Arquivo vetorial não encontrado: {path}", log_prefix, level=Qgis.Warning)
            return

        layer = QgsVectorLayer(path, name, "ogr")
        if dem_crs and dem_crs.isValid():
            layer.setCrs(dem_crs)

        if not layer.isValid():
            self.mostrar_mensagem(f"Falha ao carregar vetor {short_code} do TauDEM.", "Erro", forcar=True)
            return

        project = QgsProject.instance()

        # Não adicionar direto na raiz
        project.addMapLayer(layer, addToLegend=False)

        root = project.layerTreeRoot()
        group_name = "TauDEM - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

    def _setup_whitebox_controls(self):
        """
        Cria, dentro do scrollAreaAtividades, os controles de parâmetros
        específicos do WhiteboxTools (por enquanto só THRESHOLD).
        """
        container = self.scrollAreaAtividades.widget()
        if container is None:
            return

        layout = container.layout()
        if layout is None:
            layout = QVBoxLayout(container)
            container.setLayout(layout)

        # Grupo visual para organizar os parâmetros do WhiteboxTools
        self.groupWbtParams = QGroupBox("Parâmetros WhiteboxTools - Drenagem", container)
        form = QFormLayout(self.groupWbtParams)

        # THRESHOLD WhiteboxTools
        label_threshold_wbt = QLabel("(THRESHOLD, em nº de células):", self.groupWbtParams)
        self.spinWbtTHRESHOLD = QDoubleSpinBox(self.groupWbtParams)
        self.spinWbtTHRESHOLD.setDecimals(0)       # inteiros
        self.spinWbtTHRESHOLD.setSingleStep(100.0)
        self.spinWbtTHRESHOLD.setRange(10.0, 1000000.0)
        self.spinWbtTHRESHOLD.setValue(1000.0)     # valor padrão inicial

        # Modo de bacias (Whitebox)
        form.addRow(QLabel("Modo de bacias:", self.groupWbtParams))

        self.radioWbtBasins = QRadioButton("Bacias simples (Basins)", self.groupWbtParams)
        self.radioWbtSubbasins = QRadioButton("Sub-bacias ligadas à rede (Subbasins)", self.groupWbtParams)

        # Por padrão, usar Subbasins (mais parecido com SAGA)
        self.radioWbtSubbasins.setChecked(True)

        # Coloca um embaixo do outro
        form.addRow(self.radioWbtBasins)
        form.addRow(self.radioWbtSubbasins)

        form.addRow(label_threshold_wbt, self.spinWbtTHRESHOLD)

        layout.addWidget(self.groupWbtParams)

        # Inicialmente escondido; quem controla é o _update_motor_ui
        self.groupWbtParams.setVisible(False)

    def _executar_whitebox_drenagem(self, dem_layer: QgsRasterLayer):
        """
        Dispara o processamento de drenagem com WhiteboxTools em segundo plano
        usando QgsProcessingAlgRunnerTask (sem travar a interface).
        """
        # Verifica se há um provider WhiteboxTools disponível
        provider = self._get_wbt_provider()
        if provider is None:
            self.mostrar_mensagem(
                "Provider 'WhiteboxTools' (wbt) não está disponível no Processing.\n"
                "Verifique se o plugin 'WhiteboxTools for Processing' está instalado e habilitado.", "Erro")
            return

        # DEM em caminho seguro (sem acentos/espaços)
        safe_dem_layer = self._get_safe_dem_layer(dem_layer)
        if not safe_dem_layer or not safe_dem_layer.isValid():
            self.mostrar_mensagem("Falha ao obter DEM em caminho seguro para processamento WhiteboxTools.", "Erro")
            return

        # THRESHOLD (número de células) da interface
        wbt_threshold = 1000.0
        if hasattr(self, "spinWbtTHRESHOLD"):
            wbt_threshold = float(self.spinWbtTHRESHOLD.value())

        # Cria e inicia o runner assíncrono
        self._whitebox_runner = WhiteboxDrenagemRunner(plugin=self, dem_layer=dem_layer, safe_dem_layer=safe_dem_layer, threshold=wbt_threshold)
        self._whitebox_runner.start()

    def _add_whitebox_raster(self, path: str, name: str, dem_crs, short_code: str):
        """
        Adiciona um raster gerado pelo WhiteboxTools ao projeto,
        colocando-o dentro do grupo 'WhiteboxTools - Drenagem'
        sem expandir o grupo no painel de camadas.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        if not path or not os.path.exists(path):
            QgsMessageLog.logMessage(f"[{short_code}] Arquivo raster não encontrado: {path}", log_prefix, level=Qgis.Warning)
            return

        layer = QgsRasterLayer(path, name, "gdal")
        if dem_crs and dem_crs.isValid():
            layer.setCrs(dem_crs)

        if not layer.isValid():
            self.mostrar_mensagem(f"Falha ao carregar raster {short_code} gerado pelo WhiteboxTools.", "Erro", forcar=True)
            return

        project = QgsProject.instance()

        # Não adiciona direto na raiz (evita expandir tudo)
        project.addMapLayer(layer, addToLegend=False)

        root = project.layerTreeRoot()
        group_name = "WhiteboxTools - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        # Garante que nem o grupo nem a camada fiquem expandidos
        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

    def _get_wbt_provider(self):
        """
        Retorna o provider WhiteboxTools (id normalmente 'wbt').

        Faz uma busca robusta:
        - tenta IDs conhecidos ('wbt', 'wbw', 'whitebox', 'whiteboxtools')
        - se não encontrar, procura por 'whitebox' no id ou no nome dos providers.
        """
        reg = QgsApplication.processingRegistry()

        # 1) Tentativas diretas por id
        for pid in ("wbt", "wbw", "whitebox", "whiteboxtools"):
            prov = reg.providerById(pid)
            if prov is not None:
                return prov

        # 2) Fallback: procura por 'whitebox' em id ou nome
        for prov in reg.providers():
            pid = prov.id().lower()
            pname = prov.name().lower()
            if "whitebox" in pid or "whitebox" in pname:
                return prov

        # 3) Log para depuração (para ver o que existe de fato)
        ids = [f"{p.id()} -> {p.name()}" for p in reg.providers()]
        QgsMessageLog.logMessage("WhiteboxTools não encontrado. Providers atuais:\n" + "\n".join(ids), "WhiteboxTools - Tempo Salvo Tools", level=Qgis.Warning)
        return None

    def _add_whitebox_vector(self, path: str, name: str, dem_crs, short_code: str):
        """
        Adiciona um vetor gerado pelo WhiteboxTools ao projeto,
        colocando-o dentro do grupo 'WhiteboxTools - Drenagem'
        sem expandir o grupo no painel de camadas.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        if not path or not os.path.exists(path):
            QgsMessageLog.logMessage(f"[{short_code}] Arquivo vetorial não encontrado: {path}", log_prefix, level=Qgis.Warning)
            return

        layer = QgsVectorLayer(path, name, "ogr")
        if dem_crs and dem_crs.isValid():
            layer.setCrs(dem_crs)

        if not layer.isValid():
            self.mostrar_mensagem(f"Falha ao carregar vetor {short_code} gerado pelo WhiteboxTools.", "Erro", forcar=True)
            return

        project = QgsProject.instance()

        # Não adiciona direto na raiz (evita expandir tudo)
        project.addMapLayer(layer, addToLegend=False)

        root = project.layerTreeRoot()
        group_name = "WhiteboxTools - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        # Garante que nem o grupo nem a camada fiquem expandidos
        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

    def _find_saga_filled_layer(self, dem_layer: QgsRasterLayer) -> QgsRasterLayer | None:
        """
        Tenta localizar o DEM preenchido gerado pelo fluxo SAGA
        para o MDT de entrada.

        Por padrão o SagaDrenagemRunner nomeia como:
        "<nome do MDT> - DEM preenchido"
        """
        if not isinstance(dem_layer, QgsRasterLayer):
            return None

        target_name = f"{dem_layer.name()} - DEM preenchido"

        for lyr in QgsProject.instance().mapLayers().values():
            if isinstance(lyr, QgsRasterLayer) and lyr.name() == target_name:
                return lyr

        return None

    def _calcular_profundidade_saga(self, dem_layer: QgsRasterLayer, filled_layer: QgsRasterLayer, h_min: float):
        """
        Cria um raster de profundidade de depressão a partir de:
            DEPTH = DEM_preenchido - DEM_original

        - Valores negativos são zerados.
        - Se h_min > 0, mantém apenas pixels com profundidade >= h_min.
        - Saída em GTiff dentro de uma pasta temporária "segura".
        - A camada gerada entra no grupo 'SAGA - Drenagem'.
        """
        log_prefix = "SAGA - Tempo Salvo Tools"

        # Entradas para o RasterCalculator
        entries: list[QgsRasterCalculatorEntry] = []

        e_dem = QgsRasterCalculatorEntry()
        e_dem.ref = "dem@1"
        e_dem.raster = dem_layer
        e_dem.bandNumber = 1
        entries.append(e_dem)

        e_fill = QgsRasterCalculatorEntry()
        e_fill.ref = "filled@1"
        e_fill.raster = filled_layer
        e_fill.bandNumber = 1
        entries.append(e_fill)

        # Expressão base: profundidade = filled - dem
        base_expr = "filled@1 - dem@1"

        # Garante que profundidade negativa vira 0 e aplica limiar, se existir
        if h_min <= 0.0:
            # profundidade > 0
            expr = f"({base_expr}) * (({base_expr}) > 0)"
        else:
            # profundidade >= h_min
            expr = f"({base_expr}) * (({base_expr}) >= {h_min})"

        # Workspace e arquivo de saída
        workspace = self._create_safe_workspace(prefix="ts_saga_depth_")
        out_name = self._make_ascii_filename(dem_layer.source(), prefix="depth")
        # garante extensão .tif
        if not out_name.lower().endswith(".tif"):
            out_name = f"{out_name}.tif"
        out_path = os.path.join(workspace, out_name)

        calc = QgsRasterCalculator(expr, out_path, "GTiff",  dem_layer.extent(), dem_layer.width(), dem_layer.height(), entries)

        res = calc.processCalculation()
        if res != 0:
            self.mostrar_mensagem("Erro ao calcular raster de profundidade mínima (DEM preenchido - DEM).", "Erro")
            return

        # Carrega a camada resultante
        titulo = f"{dem_layer.name()} - Profundidade mínima"
        if h_min > 0.0:
            titulo += f" ≥ {h_min:.2f} m"

        depth_layer = QgsRasterLayer(out_path, titulo, "gdal")
        if not depth_layer.isValid():
            self.mostrar_mensagem("Raster de profundidade mínima gerado, mas falhou ao ser carregado no QGIS.", "Erro")
            return

        # Adiciona ao projeto dentro do grupo 'SAGA - Drenagem',
        # garantindo que não fiquem várias camadas com o MESMO nome.
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group_name = "SAGA - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        # Remove camadas anteriores com o mesmo nome
        for node in list(group.findLayers()):
            lyr = node.layer()
            if isinstance(lyr, QgsRasterLayer) and lyr.name() == depth_layer.name():
                group.removeChildNode(node)
                project.removeMapLayer(lyr.id())

        # Agora adiciona a nova
        project.addMapLayer(depth_layer, addToLegend=False)
        group.addLayer(depth_layer)

        group.setExpanded(True)
        node = root.findLayer(depth_layer.id())
        if node:
            node.setExpanded(False)

        self.mostrar_mensagem("Raster de profundidade mínima (poças / depressões) gerado com sucesso.", "Sucesso")

        # Se o usuário pediu vetor, gera camadas de poças com área e volume
        if getattr(self, "checkBoxVetor", None) and self.checkBoxVetor.isChecked():
            self._vetorizar_pocas(depth_layer=depth_layer, dem_layer=dem_layer, group_name=group_name)

    def _escolher_motor_profundidade(self, dem_layer):
        """
        Decide qual motor (SAGA ou TauDEM) usar para profundidade de depressões
        para o DEM atual. Retorna uma tupla: (motor, layer_preenchido)
        motor ∈ {"taudem", "saga"} ou None.
        """
        if not isinstance(dem_layer, QgsRasterLayer):
            return None, None

        dem_name = dem_layer.name()
        project = QgsProject.instance()
        root = project.layerTreeRoot()

        # Procurar produto TauDEM (FEL) deste DEM
        grupo_taudem = root.findGroup("TauDEM - Drenagem")
        fel_layer = None
        if grupo_taudem:
            for child in grupo_taudem.findLayers():
                lyr = child.layer()
                if isinstance(lyr, QgsRasterLayer) and lyr.name().startswith(f"{dem_name} - FEL"):
                    fel_layer = lyr
                    break

        # Procurar produto SAGA (DEM preenchido) deste DEM
        grupo_saga = root.findGroup("SAGA - Drenagem")
        dem_preenchido_layer = None
        if grupo_saga:
            for child in grupo_saga.findLayers():
                lyr = child.layer()
                if isinstance(lyr, QgsRasterLayer) and lyr.name().startswith(f"{dem_name} - DEM preenchido"):
                    dem_preenchido_layer = lyr
                    break

        # 1) Respeita o radioButton, se possível
        if self.radioButtonTauDem.isChecked() and fel_layer:
            return "taudem", fel_layer

        if self.radioButtonSAGA.isChecked() and dem_preenchido_layer:
            return "saga", dem_preenchido_layer

        # 2) Fallback se o motor escolhido não tem produto
        # Se marcou TauDEM mas só tem SAGA
        if self.radioButtonTauDem.isChecked() and not fel_layer and dem_preenchido_layer:
            self.mostrar_mensagem(
                "Produtos TauDEM não encontrados para este MDT. "
                "Usando DEM preenchido do SAGA para calcular depressões.", "Info")
            return "saga", dem_preenchido_layer

        # Se marcou SAGA mas só tem TauDEM
        if self.radioButtonSAGA.isChecked() and not dem_preenchido_layer and fel_layer:
            self.mostrar_mensagem(
                "Produtos SAGA não encontrados para este MDT. "
                "Usando FEL do TauDEM para calcular depressões.", "Info")
            return "taudem", fel_layer

        # 3) Se nenhum radio resolve (ex: Whitebox selecionado) ou nenhum marcado,
        # adota uma prioridade fixa quando ambos existem
        if fel_layer and dem_preenchido_layer:
            # exemplo: priorizar TauDEM
            return "taudem", fel_layer

        # Só tem um dos dois
        if fel_layer:
            return "taudem", fel_layer
        if dem_preenchido_layer:
            return "saga", dem_preenchido_layer

        # Nada encontrado
        return None, None

    def _calcular_profundidade_taudem(self, dem_layer: QgsRasterLayer, fel_layer: QgsRasterLayer, h_min: float):
        """
        Cria um raster de profundidade de depressão a partir de:
            DEPTH = FEL - DEM_original

        - Valores negativos são zerados.
        - Se h_min > 0, mantém apenas pixels com profundidade >= h_min.
        - Saída em GTiff dentro de uma pasta temporária "segura".
        - A camada gerada entra no grupo 'TauDEM - Drenagem'.
        """
        log_prefix = "TauDEM - Tempo Salvo Tools"

        if not isinstance(dem_layer, QgsRasterLayer) or not dem_layer.isValid():
            self.mostrar_mensagem("MDT inválido para calcular profundidade (TauDEM).", "Erro")
            return

        if not isinstance(fel_layer, QgsRasterLayer) or not fel_layer.isValid():
            self.mostrar_mensagem("FEL (TauDEM) inválido para calcular profundidade.", "Erro")
            return

        # Entradas para o RasterCalculator
        entries: list[QgsRasterCalculatorEntry] = []

        e_fel = QgsRasterCalculatorEntry()
        e_fel.ref = "fel@1"
        e_fel.raster = fel_layer
        e_fel.bandNumber = 1
        entries.append(e_fel)

        e_dem = QgsRasterCalculatorEntry()
        e_dem.ref = "dem@1"
        e_dem.raster = dem_layer
        e_dem.bandNumber = 1
        entries.append(e_dem)

        # Expressão base: profundidade = FEL - DEM
        base_expr = "fel@1 - dem@1"

        # Zera profundidade negativa e aplica limiar, se existir
        if h_min <= 0.0:
            # profundidade > 0
            expr = f"({base_expr}) * (({base_expr}) > 0)"
        else:
            # profundidade >= h_min
            expr = f"({base_expr}) * (({base_expr}) >= {h_min})"

        # Workspace e arquivo de saída
        workspace = self._create_safe_workspace(prefix="ts_taudem_depth_")
        out_name = self._make_ascii_filename(dem_layer.source(), prefix="depth_taudem")
        if not out_name.lower().endswith(".tif"):
            out_name = f"{out_name}.tif"
        out_path = os.path.join(workspace, out_name)

        calc = QgsRasterCalculator(expr, out_path, "GTiff", dem_layer.extent(), dem_layer.width(), dem_layer.height(), entries)

        res = calc.processCalculation()
        if res != 0:
            self.mostrar_mensagem("Erro ao calcular raster de profundidade mínima (FEL - DEM, TauDEM).", "Erro")
            return

        # Nome da camada de saída
        titulo = f"{dem_layer.name()} - Profundidade mínima (TauDEM)"
        if h_min > 0.0:
            titulo += f" ≥ {h_min:.2f} m"

        # Cria a camada raster
        depth_layer = QgsRasterLayer(out_path, titulo, "gdal")
        if not depth_layer.isValid():
            self.mostrar_mensagem("Raster de profundidade mínima (TauDEM) gerado, mas falhou ao ser carregado.", "Erro")
            return

        # Adiciona ao projeto dentro do grupo 'TauDEM - Drenagem',
        # garantindo que só fique UMA camada com esse nome.
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group_name = "TauDEM - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        # Remove camadas anteriores com o mesmo nome dentro do grupo
        for node in list(group.findLayers()):
            lyr = node.layer()
            if isinstance(lyr, QgsRasterLayer) and lyr.name() == depth_layer.name():
                group.removeChildNode(node)
                project.removeMapLayer(lyr.id())

        # Agora adiciona somente a nova
        project.addMapLayer(depth_layer, addToLegend=False)
        group.addLayer(depth_layer)
        group.setExpanded(True)
        node = root.findLayer(depth_layer.id())
        if node:
            node.setExpanded(False)

        self.mostrar_mensagem("Raster de profundidade mínima (TauDEM) gerado com sucesso.", "Sucesso")

        # Se o usuário pediu vetor, gera poças com área e volume
        if getattr(self, "checkBoxVetor", None) and self.checkBoxVetor.isChecked():
            self._vetorizar_pocas(depth_layer=depth_layer, dem_layer=dem_layer, group_name=group_name)

    def _find_taudem_fel_layer(self, dem_layer: QgsRasterLayer) -> QgsRasterLayer | None:
        """
        Tenta localizar o FEL (PitRemove) gerado pelo TauDEM
        para o MDT de entrada.

        Nome esperado:
        "<nome do MDT> - FEL (TauDEM)"
        """
        if not isinstance(dem_layer, QgsRasterLayer):
            return None

        dem_name = dem_layer.name()
        project = QgsProject.instance()
        root = project.layerTreeRoot()

        grupo_taudem = root.findGroup("TauDEM - Drenagem")
        if not grupo_taudem:
            return None

        for node in grupo_taudem.findLayers():
            lyr = node.layer()
            if (
                isinstance(lyr, QgsRasterLayer)
                and lyr.name().startswith(f"{dem_name} - FEL")):
                return lyr

        return None

    def _vetorizar_pocas(self, depth_layer: QgsRasterLayer, dem_layer: QgsRasterLayer, group_name: str):
        """
        A partir de um raster de profundidade (DEPTH),
        gera uma camada vetorial de poças com área e volume.

        Volume ≈ área * profundidade_média (por poça).

        Se já existir uma camada "<MDT> - Poças (vetor)" no grupo alvo,
        ela é removida antes de adicionar a nova (evita duplicatas).
        """
        log_prefix = "Profundidade - Tempo Salvo Tools"

        if not isinstance(depth_layer, QgsRasterLayer) or not depth_layer.isValid():
            self.mostrar_mensagem("Camada de profundidade inválida para vetorizar poças.", "Erro")
            return

        depth_path = depth_layer.source()
        if not os.path.isfile(depth_path):
            self.mostrar_mensagem("Raster de profundidade não está em um arquivo físico (não é possível vetorizar).", "Erro")
            return

        # Workspace e caminhos temporários
        workspace = self._create_safe_workspace(prefix="ts_pocas_")
        ascii_name = self._make_ascii_filename(depth_path, prefix="pocas")
        if ascii_name.lower().endswith(".tif"):
            base = ascii_name[:-4]
        else:
            base = ascii_name

        mask_path = os.path.join(workspace, f"{base}_mask.tif")
        poly_tmp_path = os.path.join(workspace, f"{base}_poly_tmp.gpkg")
        out_vec_path = os.path.join(workspace, f"{base}_pocas.gpkg")

        # 1) Criar raster máscara (depth > 0) com QgsRasterCalculator
        entries: list[QgsRasterCalculatorEntry] = []

        e_depth = QgsRasterCalculatorEntry()
        e_depth.ref = "depth@1"
        e_depth.raster = depth_layer
        e_depth.bandNumber = 1
        entries.append(e_depth)

        expr = "depth@1 > 0"

        calc = QgsRasterCalculator(expr,  mask_path, "GTiff", depth_layer.extent(), depth_layer.width(), depth_layer.height(), entries)
        res = calc.processCalculation()
        if res != 0:
            self.mostrar_mensagem("Erro ao criar raster máscara das poças (depth > 0).", "Erro")
            return

        # 2) Polygonize da máscara (gdal:polygonize)
        params_poly = {
            "INPUT": mask_path,
            "BAND": 1,
            "FIELD": "ID",
            "EIGHT_CONNECTEDNESS": False,
            "OUTPUT": poly_tmp_path}

        try:
            result = processing.run("gdal:polygonize", params_poly, context=QgsProcessingContext(), feedback=QgsProcessingFeedback())
        except Exception as e:
            QgsMessageLog.logMessage(f"Erro em gdal:polygonize: {e}", log_prefix, level=Qgis.Critical)
            self.mostrar_mensagem("Erro ao vetorizar poças (gdal:polygonize).", "Erro")
            return

        poly_path = result.get("OUTPUT", poly_tmp_path)

        poly_tmp_layer = QgsVectorLayer(poly_path, "poças_tmp", "ogr")
        if not poly_tmp_layer.isValid():
            self.mostrar_mensagem("Falha ao carregar camada vetorial temporária de poças.", "Erro")
            return

        # 3) Criar camada de saída (memória) apenas com poças (ID > 0)
        crs = dem_layer.crs() if dem_layer and dem_layer.isValid() else depth_layer.crs()

        dem_name = dem_layer.name() if dem_layer else ""
        depth_name = depth_layer.name() if depth_layer else ""

        # Tenta aproveitar o “final” do nome da camada de profundidade
        # Ex.: depth_name = "MDT - Profundidade mínima ≥ 0.05 m"
        #      => nome_vec = "MDT - Poças - Profundidade mínima ≥ 0.05 m"
        if dem_name and depth_name.startswith(dem_name):
            suffix = depth_name[len(dem_name):]  # inclui " - Profundidade mínima..."
            nome_vec = f"{dem_name} - Poças{suffix}"
        elif dem_name:
            nome_vec = f"{dem_name} - Poças"
        else:
            nome_vec = "Poças"

        pocas_layer = QgsVectorLayer(f"Polygon?crs={crs.authid()}", nome_vec, "memory")
        prov = pocas_layer.dataProvider()

        prov.addAttributes([
            QgsField("id", QVariant.Int),
            QgsField("area_m2", QVariant.Double),
            QgsField("vol_m3", QVariant.Double)])
        pocas_layer.updateFields()

        id_field_name = "ID"
        if poly_tmp_layer.fields().indexFromName("ID") < 0:
            # fallback padrão do gdal:polygonize
            id_field_name = "DN"

        features_out = []
        idx = 1

        for f in poly_tmp_layer.getFeatures():
            try:
                val = float(f[id_field_name])
            except Exception:
                val = 0.0

            if val <= 0.0:
                # ignora o polígono de fundo (valor 0)
                continue

            geom = f.geometry()
            if geom is None or geom.isEmpty():
                continue

            new_f = QgsFeature(pocas_layer.fields())
            new_f.setGeometry(geom)
            area = geom.area()  # assume CRS métrico
            new_f["id"] = idx
            new_f["area_m2"] = area
            new_f["vol_m3"] = 0.0  # será preenchido após ZonalStatistics
            features_out.append(new_f)
            idx += 1

        if not features_out:
            self.mostrar_mensagem("Nenhuma poça encontrada para vetorizar (todas profundidades = 0).", "Info")
            return

        prov.addFeatures(features_out)
        pocas_layer.updateExtents()

        # 4) ZonalStatistics para profundidade média por poça
        depth_rlayer = QgsRasterLayer(depth_path, "depth_tmp")
        if not depth_rlayer.isValid():
            self.mostrar_mensagem(f"Raster de profundidade inválido: {depth_path}", "Erro")
            return

        zs = QgsZonalStatistics(pocas_layer, depth_rlayer, "DEP_", 1, QgsZonalStatistics.Mean)
        zs.calculateStatistics(None)

        # Descobre o campo DEP_* criado
        fields = pocas_layer.fields()
        idx_dep = -1
        for i, fld in enumerate(fields):
            if fld.name().upper().startswith("DEP_"):
                idx_dep = i
                break

        if idx_dep == -1:
            self.mostrar_mensagem("Campo de profundidade média (DEP_*) não encontrado após ZonalStatistics.", "Erro")
            return

        idx_area = fields.indexFromName("area_m2")
        idx_vol = fields.indexFromName("vol_m3")

        pocas_layer.startEditing()
        for f in pocas_layer.getFeatures():
            mean_depth = f[idx_dep]
            try:
                mean_depth = float(mean_depth)
            except (TypeError, ValueError):
                mean_depth = 0.0

            area = f["area_m2"]
            try:
                area = float(area)
            except (TypeError, ValueError):
                area = 0.0

            vol = area * mean_depth
            pocas_layer.changeAttributeValue(f.id(), idx_vol, vol)
        pocas_layer.commitChanges()

        # 5) Salvar em disco (GPKG) e adicionar ao projeto
        #    removendo camadas antigas com o mesmo nome dentro do grupo.
        res = QgsVectorFileWriter.writeAsVectorFormat(pocas_layer, out_vec_path, "utf-8", crs, "GPKG")

        # Compatível com retorno int ou (int, msg)
        if isinstance(res, tuple):
            err, _ = res
        else:
            err = res

        if err != QgsVectorFileWriter.NoError:
            self.mostrar_mensagem(f"Erro ao salvar camada vetorial de poças (código {err}).", "Erro")
            return

        final_layer = QgsVectorLayer(out_vec_path, nome_vec, "ogr")
        if not final_layer.isValid():
            self.mostrar_mensagem("Camada vetorial de poças salva, mas falhou ao ser carregada no QGIS.", "Erro")
            return

        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        # Remove camadas antigas "nome_vec" dentro do grupo
        for node in group.findLayers():
            lyr = node.layer()
            if lyr and lyr.name() == nome_vec:
                group.removeChildNode(node)
                project.removeMapLayer(lyr.id())

        # Agora adiciona apenas a nova
        project.addMapLayer(final_layer, addToLegend=False)
        group.addLayer(final_layer)
        group.setExpanded(True)

        node = root.findLayer(final_layer.id())
        if node:
            node.setExpanded(False)

        self.mostrar_mensagem("Camada vetorial de poças gerada com sucesso (área e volume).", "Sucesso")

    def _add_taudem_raster(self, path: str, name: str, dem_crs, short_code: str):
        """
        Adiciona um raster TauDEM ao projeto, sem expandir grupos no painel de camadas.
        Retorna a camada adicionada (ou None em caso de erro).
        """
        log_prefix = "TauDEM - Tempo Salvo Tools"

        if not path or not os.path.exists(path):
            QgsMessageLog.logMessage(f"[{short_code}] Arquivo raster não encontrado: {path}", log_prefix, level=Qgis.Warning)
            return None

        layer = QgsRasterLayer(path, name, "gdal")
        if dem_crs and dem_crs.isValid():
            layer.setCrs(dem_crs)

        if not layer.isValid():
            self.mostrar_mensagem(f"Falha ao carregar raster {short_code} do TauDEM.", "Erro", forcar=True)
            return None

        project = QgsProject.instance()

        # Não adiciona direto na raiz (evita comportamento padrão de expandir)
        project.addMapLayer(layer, addToLegend=False)

        # Coloca dentro de um grupo fixo (por exemplo “TauDEM - Drenagem”)
        root = project.layerTreeRoot()
        group_name = "TauDEM - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        group.addLayer(layer)

        # Garante que nem o grupo nem a camada fiquem expandidos
        group.setExpanded(True)
        node = root.findLayer(layer.id())
        if node:
            node.setExpanded(False)

        return layer

    def _calcular_profundidade_whitebox(self, dem_layer: QgsRasterLayer, h_min: float):
        """
        Calcula o raster de profundidade de depressões usando WhiteboxTools (DepthInSink).

        Passos:
        1) Executa DepthInSink no DEM (usando DEM 'seguro')
        2) Se h_min > 0, aplica limiar: depth >= h_min
        3) Adiciona raster ao grupo 'WhiteboxTools - Drenagem'
        4) Se checkBoxVetor estiver marcado, gera camada vetorial de poças (área e volume).
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        if not isinstance(dem_layer, QgsRasterLayer) or not dem_layer.isValid():
            self.mostrar_mensagem("Selecione um MDT válido no combo para calcular a profundidade (WhiteboxTools).", "Erro")
            return

        # Provider WhiteboxTools disponível?
        provider = self._get_wbt_provider()
        if provider is None:
            self.mostrar_mensagem(
                "Provider 'WhiteboxTools' não está disponível no Processing.\n"
                "Verifique se o plugin 'WhiteboxTools for Processing' está instalado e habilitado.", "Erro")
            return

        # DEM em caminho "seguro"
        safe_dem_layer = self._get_safe_dem_layer(dem_layer)
        dem_path = safe_dem_layer.source()

        if not os.path.isfile(dem_path):
            self.mostrar_mensagem("O MDT de entrada não está em um arquivo físico no disco (não é possível chamar WhiteboxTools).", "Erro")
            return

        # Workspace e caminho de saída
        workspace = self._create_safe_workspace(prefix="ts_wbt_depth_")
        base_ascii = self._make_ascii_filename(dem_path, prefix="depth_wbt")
        if not base_ascii.lower().endswith(".tif"):
            base_ascii += ".tif"
        depth_raw_path = os.path.join(workspace, base_ascii)

        # Resolver o algoritmo DepthInSink de forma robusta
        reg = QgsApplication.processingRegistry()
        prov_id = provider.id()

        alg = reg.algorithmById(f"{prov_id}:DepthInSink")

        if alg is None:
            # fallback: procurar por nome aproximado dentro do provider
            norm_tool = "depthinsink"
            for a in provider.algorithms():
                aid = a.id()
                base = aid.split(":")[1] if ":" in aid else aid
                norm_base = base.lower().replace("_", "")
                if norm_base == norm_tool:
                    alg = a
                    break

        if alg is None:
            self.mostrar_mensagem(
                "Algoritmo 'DepthInSink' do WhiteboxTools não foi encontrado no Processing.\n"
                "Verifique a instalação do plugin 'WhiteboxTools for Processing'.", "Erro")
            return

        # Descobrir parâmetros de entrada/saída (dem, output, etc.)
        in_param = None
        out_param = None
        for p in alg.parameterDefinitions():
            if p.isDestination():
                if out_param is None:
                    out_param = p.name()
            elif in_param is None:
                in_param = p.name()

        if not in_param or not out_param:
            self.mostrar_mensagem("Não foi possível identificar parâmetros de entrada/saída do 'DepthInSink' (WhiteboxTools).", "Erro")
            return

        params = {in_param: dem_path, out_param: depth_raw_path}

        # Executa o DepthInSink (sincrônico)
        context = QgsProcessingContext()
        context.setProject(QgsProject.instance())
        feedback = QgsProcessingFeedback()

        try:
            from qgis import processing
            result = processing.run(alg.id(), params, context=context, feedback=feedback)
        except Exception as e:
            QgsMessageLog.logMessage(f"Erro ao executar DepthInSink (WhiteboxTools): {e}", log_prefix, level=Qgis.Critical)
            self.mostrar_mensagem("Erro ao calcular profundidade de depressões com WhiteboxTools (DepthInSink).", "Erro")
            return

        depth_path = result.get(out_param, depth_raw_path)

        # Carrega o raster bruto de profundidade
        depth_raw_layer = QgsRasterLayer(depth_path, "depth_wbt_raw")
        if not depth_raw_layer.isValid():
            self.mostrar_mensagem("Raster de profundidade (WhiteboxTools) gerado, mas inválido para uso.", "Erro")
            return

        # Se h_min > 0, aplica limiar depth >= h_min
        final_path = depth_path
        if h_min > 0.0:
            entries: list[QgsRasterCalculatorEntry] = []

            e_depth = QgsRasterCalculatorEntry()
            e_depth.ref = "depth@1"
            e_depth.raster = depth_raw_layer
            e_depth.bandNumber = 1
            entries.append(e_depth)

            # Saída filtrada
            filt_name = self._make_ascii_filename(depth_path, prefix="depth_wbt_th")
            if not filt_name.lower().endswith(".tif"):
                filt_name += ".tif"
            final_path = os.path.join(workspace, filt_name)

            # Mantém apenas pixels com profundidade >= h_min
            expr = f"depth@1 * (depth@1 >= {h_min})"

            calc = QgsRasterCalculator(expr, final_path, "GTiff", depth_raw_layer.extent(), depth_raw_layer.width(), depth_raw_layer.height(), entries)

            res = calc.processCalculation()
            if res != 0:
                self.mostrar_mensagem("Erro ao aplicar limiar de profundidade mínima no raster do WhiteboxTools.", "Erro")
                return

        # Carrega raster final (com ou sem limiar)
        titulo = f"{dem_layer.name()} - Profundidade mínima (Whitebox)"
        if h_min > 0.0:
            titulo += f" ≥ {h_min:.2f} m"

        depth_layer = QgsRasterLayer(final_path, titulo, "gdal")
        if not depth_layer.isValid():
            self.mostrar_mensagem("Raster de profundidade (WhiteboxTools) gerado, mas falhou ao ser carregado no QGIS.", "Erro")
            return

        # Adiciona ao grupo 'WhiteboxTools - Drenagem', removendo duplicatas
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group_name = "WhiteboxTools - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        # Remove rasters antigos com o mesmo nome
        for node in list(group.findLayers()):
            lyr = node.layer()
            if isinstance(lyr, QgsRasterLayer) and lyr.name() == depth_layer.name():
                group.removeChildNode(node)
                project.removeMapLayer(lyr.id())

        project.addMapLayer(depth_layer, addToLegend=False)
        group.addLayer(depth_layer)
        group.setExpanded(True)
        node = root.findLayer(depth_layer.id())
        if node:
            node.setExpanded(False)

        self.mostrar_mensagem("Raster de profundidade mínima (WhiteboxTools - DepthInSink) gerado com sucesso.", "Sucesso")

        # Vetorizar poças, se solicitado
        if getattr(self, "checkBoxVetor", None) and self.checkBoxVetor.isChecked():
            self._vetorizar_pocas(depth_layer=depth_layer, dem_layer=dem_layer, group_name=group_name)

    def _get_wbt_algorithm(self, tool_name: str, human_name: str):
        """
        Resolve o algoritmo WhiteboxTools a partir do nome lógico da ferramenta.
        Ex.: tool_name = 'DepthInSink'
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        reg = QgsApplication.processingRegistry()
        provider = self._get_wbt_provider()
        if provider is None:
            self.mostrar_mensagem(
                "Provider 'WhiteboxTools' (wbt) não está disponível no Processing.\n"
                "Verifique se o plugin 'WhiteboxTools for Processing' está instalado e habilitado.",
                "Erro"
            )
            return None

        prov_id = provider.id()  # normalmente 'wbt'

        # 1) Tentativa direta: 'wbt:DepthInSink'
        alg_id_direct = f"{prov_id}:{tool_name}"
        alg = reg.algorithmById(alg_id_direct)
        if alg is not None:
            QgsMessageLog.logMessage(
                f"Algoritmo resolvido diretamente: '{alg_id_direct}'",
                log_prefix, level=Qgis.Info
            )
            return alg

        # 2) Fallback: compara ignorando maiúsculas/minúsculas e underscores
        norm_tool = tool_name.lower().replace("_", "")
        exact_match = None
        partial_match = None

        for a in provider.algorithms():
            aid = a.id()  # ex.: 'wbt:DepthInSink'
            base = aid.split(":")[1] if ":" in aid else aid
            norm_base = base.lower().replace("_", "")

            if norm_base == norm_tool:
                exact_match = a
                break

            if partial_match is None and (norm_tool in norm_base or norm_base in norm_tool):
                partial_match = a

        if exact_match is not None:
            QgsMessageLog.logMessage(
                f"Resolvendo '{tool_name}' para algoritmo '{exact_match.id()}' (WhiteboxTools).",
                log_prefix, level=Qgis.Info
            )
            return exact_match

        if partial_match is not None:
            QgsMessageLog.logMessage(
                f"Resolvendo '{tool_name}' (match parcial) para algoritmo '{partial_match.id()}' (WhiteboxTools).",
                log_prefix, level=Qgis.Info
            )
            return partial_match

        # 3) Não achou nada → logar alguns IDs disponíveis
        try:
            all_ids = [a.id() for a in provider.algorithms()]
            sample_ids = ", ".join(sorted(all_ids)[:20])
        except Exception:
            sample_ids = "não foi possível obter a lista de algoritmos."

        QgsMessageLog.logMessage(
            f"Algoritmo '{tool_name}' não encontrado no provider '{prov_id}'. "
            f"Alguns ids disponíveis: {sample_ids}",
            log_prefix, level=Qgis.Warning
        )

        self.mostrar_mensagem(
            f"Algoritmo WhiteboxTools '{human_name}' não encontrado "
            f"(id esperado parecido com '{prov_id}:{tool_name}').",
            "Erro"
        )
        return None

    def _build_wbt_in_out_params(self, alg, input_path: str, output_path: str, tool_desc: str):
        """
        Para ferramentas simples do WhiteboxTools (1 raster de entrada, 1 de saída),
        tenta adivinhar o primeiro parâmetro de entrada e o primeiro destino.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        in_name = None
        out_name = None

        for p in alg.parameterDefinitions():
            # Destino → saída
            if p.isDestination():
                if out_name is None:
                    out_name = p.name()
                continue

            # Primeiro parâmetro "normal" → entrada principal
            if in_name is None:
                in_name = p.name()

        QgsMessageLog.logMessage(f"{tool_desc}: param entrada='{in_name}', saída='{out_name}'", log_prefix, level=Qgis.Info)

        if not in_name or not out_name:
            QgsMessageLog.logMessage(f"{tool_desc}: não conseguiu identificar parâmetros de entrada/saída.", log_prefix, level=Qgis.Critical)
            self.mostrar_mensagem(
                f"Não foi possível identificar parâmetros de entrada/saída "
                f"para '{tool_desc}' no WhiteboxTools (veja o Registro de Mensagens).", "Erro")
            return None

        return {in_name: input_path, out_name: output_path}

    def _calcular_profundidade_whitebox(self, dem_layer: QgsRasterLayer, h_min: float):
        """
        Calcula a profundidade de depressões usando WhiteboxTools (DepthInSink).

        Passos:
        - Executa DepthInSink em cima do DEM (caminho seguro).
        - Aplica limiar mínimo h_min (m) via QgsRasterCalculator.
        - Adiciona o raster final ao grupo 'WhiteboxTools - Drenagem'.
        - Se checkBoxVetor estiver marcado, gera camada vetorial de poças.
        """
        if not isinstance(dem_layer, QgsRasterLayer) or not dem_layer.isValid():
            self.mostrar_mensagem("Selecione um MDT válido para calcular a profundidade (Whitebox).", "Erro")
            return

        # Provider / algoritmo
        provider = self._get_wbt_provider()
        if provider is None:
            # _get_wbt_provider já mostra mensagem
            return

        alg = self._get_wbt_algorithm("DepthInSink", "Depth in sink")
        if alg is None:
            return

        # DEM em caminho seguro (sem acentos/espaços)
        safe_dem_layer = self._get_safe_dem_layer(dem_layer)
        if not safe_dem_layer or not safe_dem_layer.isValid():
            self.mostrar_mensagem("Falha ao obter DEM em caminho seguro para DepthInSink (Whitebox).", "Erro")
            return

        dem_path = safe_dem_layer.source()

        # Workspace e nomes de saída
        workspace = self._create_safe_workspace(prefix="ts_wbt_depth_")
        base_ascii = self._make_ascii_filename(dem_path, prefix="DepthInSink")

        if not base_ascii.lower().endswith(".tif"):
            base_ascii += ".tif"

        depth_raw_path = os.path.join(workspace, base_ascii)

        # Monta parâmetros simples (entrada DEM, saída raster)
        params = self._build_wbt_in_out_params(alg, input_path=dem_path, output_path=depth_raw_path, tool_desc="DepthInSink")
        if params is None:
            return

        # Executa DepthInSink de forma síncrona
        context = QgsProcessingContext()
        context.setProject(QgsProject.instance())
        feedback = QgsProcessingFeedback()

        try:
            processing.run(alg.id(), params, context=context, feedback=feedback)
        except Exception as e:
            QgsMessageLog.logMessage(f"Erro ao executar DepthInSink (WhiteboxTools): {e}", "WhiteboxTools - Tempo Salvo Tools", level=Qgis.Critical)
            self.mostrar_mensagem("Erro ao executar DepthInSink (WhiteboxTools).", "Erro")
            return

        # Carrega o raster bruto do DepthInSink (sem limiar)
        depth_raw_layer = QgsRasterLayer(depth_raw_path, f"{dem_layer.name()} - DepthInSink (raw)", "gdal")
        if not depth_raw_layer.isValid():
            self.mostrar_mensagem("Raster DepthInSink gerado, mas falhou ao ser carregado.", "Erro")
            return

        # Agora aplica o limiar mínimo h_min (como nos outros motores)
        entries: list[QgsRasterCalculatorEntry] = []

        e_depth = QgsRasterCalculatorEntry()
        e_depth.ref = "depth@1"
        e_depth.raster = depth_raw_layer
        e_depth.bandNumber = 1
        entries.append(e_depth)

        base_expr = "depth@1"

        if h_min <= 0.0:
            # Mantém apenas valores > 0
            expr = f"({base_expr}) * (({base_expr}) > 0)"
        else:
            # Mantém apenas profundidades >= h_min
            expr = f"({base_expr}) * (({base_expr}) >= {h_min})"

        out_name = self._make_ascii_filename(depth_raw_path, prefix="depth_wbt")
        if not out_name.lower().endswith(".tif"):
            out_name += ".tif"
        out_path = os.path.join(workspace, out_name)

        calc = QgsRasterCalculator(expr, out_path, "GTiff", depth_raw_layer.extent(), depth_raw_layer.width(), depth_raw_layer.height(), entries)

        res = calc.processCalculation()
        if res != 0:
            self.mostrar_mensagem("Erro ao aplicar limiar mínimo à profundidade (WhiteboxTools).", "Erro")
            return

        # Nome amigável da camada final
        titulo = f"{dem_layer.name()} - Profundidade mínima (Whitebox)"
        if h_min > 0.0:
            titulo += f" ≥ {h_min:.2f} m"

        depth_layer = QgsRasterLayer(out_path, titulo, "gdal")
        if not depth_layer.isValid():
            self.mostrar_mensagem("Raster de profundidade mínima (Whitebox) gerado, mas falhou ao ser carregado no QGIS.", "Erro")
            return

        # Adiciona no grupo 'WhiteboxTools - Drenagem', evitando duplicatas de nome
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        group_name = "WhiteboxTools - Drenagem"
        group = root.findGroup(group_name)
        if group is None:
            group = root.addGroup(group_name)

        # Remove camadas anteriores com o mesmo nome dentro do grupo
        for node in list(group.findLayers()):
            lyr = node.layer()
            if isinstance(lyr, QgsRasterLayer) and lyr.name() == depth_layer.name():
                group.removeChildNode(node)
                project.removeMapLayer(lyr.id())

        # Agora adiciona somente a nova
        project.addMapLayer(depth_layer, addToLegend=False)
        group.addLayer(depth_layer)

        # Mantém o grupo aberto e as camadas colapsadas (como combinamos)
        group.setExpanded(True)
        node = root.findLayer(depth_layer.id())
        if node:
            node.setExpanded(False)

        self.mostrar_mensagem("Raster de profundidade mínima (WhiteboxTools / DepthInSink) gerado com sucesso.", "Sucesso")

        # Se o usuário pediu vetor, gera poças (área e volume)
        if getattr(self, "checkBoxVetor", None) and self.checkBoxVetor.isChecked():
            self._vetorizar_pocas(depth_layer=depth_layer, dem_layer=dem_layer, group_name=group_name)

    def on_pushButtonPronfundidade_clicked(self):
        """
        Handler do botão de profundidade de depressões.

        - Lê o MDT atual do comboBoxRaster
        - Lê a profundidade mínima do doubleSpinBoxMinimo
        - Se WhiteboxTools estiver selecionado, usa DepthInSink
        - Caso contrário, usa _escolher_motor_profundidade para decidir entre SAGA / TauDEM
        """
        # MDT atual
        layer_id = self.comboBoxRaster.currentData()
        dem_layer = QgsProject.instance().mapLayer(layer_id)

        if not isinstance(dem_layer, QgsRasterLayer):
            self.mostrar_mensagem("Selecione um MDT válido no combo antes de calcular a profundidade das depressões.", "Erro")
            return

        # Lê a profundidade mínima (em metros) uma única vez
        h_min = 0.01
        if hasattr(self, "doubleSpinBoxMinimo"):
            h_min = float(self.doubleSpinBoxMinimo.value())

        # 1) Se WhiteboxTools estiver selecionado, usar DepthInSink
        if self.radioButtonTools.isChecked():
            self._calcular_profundidade_whitebox(dem_layer, h_min)
            return

        # 2) Caso contrário, decide entre SAGA / TauDEM
        motor, layer_preenchido = self._escolher_motor_profundidade(dem_layer)

        if motor == "saga" and layer_preenchido is not None:
            # layer_preenchido = DEM preenchido SAGA
            self._calcular_profundidade_saga(dem_layer, layer_preenchido, h_min)

        elif motor == "taudem" and layer_preenchido is not None:
            # layer_preenchido = FEL TauDEM
            self._calcular_profundidade_taudem(dem_layer, layer_preenchido, h_min)

        else:
            self.mostrar_mensagem(
                "Não encontrei DEM preenchido (SAGA) nem FEL (TauDEM) para este MDT.\n"
                "Execute primeiro a drenagem com SAGA ou TauDEM.", "Erro")

    def _update_executar_button_state(self):
        """
        Habilita o pushButtonExecutar apenas quando:
        - existe um raster válido selecionado no comboBoxRaster; e
        - um dos motores (SAGA, TauDEM ou WhiteboxTools) está selecionado.
        """
        has_raster = (
            self.comboBoxRaster.count() > 0 and
            self.comboBoxRaster.currentData() is not None)

        motor_selected = (
            self.radioButtonSAGA.isChecked() or
            self.radioButtonTauDem.isChecked() or
            self.radioButtonTools.isChecked())

        self.pushButtonExecutar.setEnabled(has_raster and motor_selected)

    def _update_action_buttons_state(self):
        """
        Controla a habilitação dos botões Executar e Profundidade.

        - Executar:
            habilita se existir um MDT raster válido selecionado
            e algum motor (SAGA / TauDEM / WhiteboxTools) estiver selecionado.

        - Profundidade:
            além disso, verifica se a camada necessária para o cálculo
            já está presente para o motor escolhido:
              * Whitebox: só precisa do MDT
              * SAGA: precisa do DEM preenchido SAGA
              * TauDEM: precisa do FEL TauDEM
        """
        project = QgsProject.instance()

        # Verifica raster selecionado
        layer_id = self.comboBoxRaster.currentData()
        dem_layer = project.mapLayer(layer_id) if layer_id else None

        has_raster = isinstance(dem_layer, QgsRasterLayer) and dem_layer.isValid()

        # Verifica motor selecionado
        motor_saga = self.radioButtonSAGA.isChecked()
        motor_taudem = self.radioButtonTauDem.isChecked()
        motor_wbt = self.radioButtonTools.isChecked()

        motor_selected = motor_saga or motor_taudem or motor_wbt

        # pushButtonExecutar: MDT + motor
        can_run = has_raster and motor_selected

        # pushButtonPronfundidade: também precisa da camada "base"
        can_depth = False
        if can_run and dem_layer is not None:
            if motor_wbt:
                # Whitebox (DepthInSink) calcula tudo a partir do MDT
                can_depth = True
            elif motor_saga:
                # Precisa do DEM preenchido SAGA para este MDT
                saga_filled = self._find_saga_filled_layer(dem_layer)
                can_depth = saga_filled is not None
            elif motor_taudem:
                # Precisa do FEL TauDEM para este MDT
                fel_layer = self._find_taudem_fel_layer(dem_layer)
                can_depth = fel_layer is not None

        self.pushButtonExecutar.setEnabled(can_run)
        self.pushButtonPronfundidade.setEnabled(can_depth)

class WhiteboxDrenagemRunner:
    """
    Orquestra a drenagem via WhiteboxTools em 4 etapas usando QgsProcessingAlgRunnerTask:
    1) D8 pointer
    2) D8 flow accumulation
    3) Stream definition by threshold
    4) Watershed (bacias)
    OBS: usa o provider do WhiteboxTools já instalado na Caixa de Ferramentas do QGIS.
    """
    def __init__(self, plugin, dem_layer, safe_dem_layer, threshold: float):
        self.plugin = plugin
        self.dem_layer = dem_layer
        self.safe_dem_layer = safe_dem_layer
        self.dem_name = dem_layer.name()
        self.threshold = float(threshold)

        # Workspace para arquivos temporários desta execução
        self.workspace = plugin._create_safe_workspace(prefix="ts_wbt_")
        self.original_dem_path = dem_layer.source()

        # Nomes ASCII baseados no DEM original
        base_ascii = plugin._make_ascii_filename(self.original_dem_path)
        root, ext = os.path.splitext(base_ascii)

        self.pntr_path = os.path.join(self.workspace, f"pntr_{root}.tif")
        self.accum_path = os.path.join(self.workspace, f"accum_{root}.tif")
        self.streams_path = os.path.join(self.workspace, f"streams_{root}.tif")
        self.basins_path = os.path.join(self.workspace, f"basins_{root}.tif")
        self.basins_vec_path = os.path.join(self.workspace, f"basins_vec_{root}.gpkg")
        self.streams_vec_path = os.path.join(self.workspace, f"streams_vec_{root}.shp")

        # Contexto e feedback do Processing (devem viver enquanto as tasks existirem)
        self.context = QgsProcessingContext()
        self.context.setProject(QgsProject.instance())

        self.feedback_pointer = QgsProcessingFeedback()
        self.feedback_accum = QgsProcessingFeedback()
        self.feedback_streams = QgsProcessingFeedback()
        self.feedback_basins = QgsProcessingFeedback()
        self.feedback_basins_vec = QgsProcessingFeedback()
        self.feedback_streams_vec = QgsProcessingFeedback()

        # Referência às tasks
        self.task_pointer = None
        self.task_accum = None
        self.task_streams = None
        self.task_streams_vec = None
        self.task_basins_vec = None
        self.task_basins = None

        # Provider WhiteboxTools (id deve ser 'wbt')
        self._provider = None

    # Helpers internos
    def _get_provider(self):
        """
        Usa o helper do dock (RedesDrenagem) para descobrir o provider do WhiteboxTools.
        Geralmente o id é 'wbt'.
        """
        if self._provider is not None:
            return self._provider

        self._provider = self.plugin._get_wbt_provider()
        return self._provider

    def _get_algorithm(self, tool_name: str, human_name: str):
        """
        Resolve o algoritmo do WhiteboxTools a partir do nome da ferramenta.

        tool_name: nome lógico (ex.: 'D8Pointer', 'D8FlowAccumulation', 'ExtractStreams', 'Basins').
        human_name: nome amigável para mensagens de erro.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        reg = QgsApplication.processingRegistry()
        provider = self._get_provider()  # usa o helper que consulta o provider 'wbt'

        if provider is None:
            self.plugin.mostrar_mensagem(
                "Provider 'WhiteboxTools' (wbt) não está disponível no Processing. "
                "Verifique se o plugin 'WhiteboxTools for Processing' está instalado e habilitado.", "Erro")
            return None

        prov_id = provider.id()  # deve ser 'wbt'

        # 1) Tentativa direta: 'wbt:ToolName'
        alg_id_direct = f"{prov_id}:{tool_name}"
        alg = reg.algorithmById(alg_id_direct)
        if alg is not None:
            QgsMessageLog.logMessage(f"Algoritmo resolvido diretamente: '{alg_id_direct}'", log_prefix, level=Qgis.Info)
            return alg

        # 2) Igualdade ignorando maiúsculas/minúsculas e underscores
        norm_tool = tool_name.lower().replace("_", "")
        exact_match = None
        partial_match = None

        for a in provider.algorithms():
            aid = a.id()  # ex.: 'wbt:D8Pointer'
            base = aid.split(":")[1] if ":" in aid else aid
            norm_base = base.lower().replace("_", "")

            if norm_base == norm_tool:
                exact_match = a
                break

            if partial_match is None and (norm_tool in norm_base or norm_base in norm_tool):
                partial_match = a

        if exact_match is not None:
            QgsMessageLog.logMessage(f"Resolvendo '{tool_name}' para algoritmo '{exact_match.id()}' (WhiteboxTools).", log_prefix, level=Qgis.Info)
            return exact_match

        if partial_match is not None:
            QgsMessageLog.logMessage(f"Resolvendo '{tool_name}' (match parcial) para algoritmo '{partial_match.id()}' (WhiteboxTools).", log_prefix, level=Qgis.Info)
            return partial_match

        # 3) Não achou nada → log com alguns ids disponíveis
        try:
            all_ids = [a.id() for a in provider.algorithms()]
            sample_ids = ", ".join(sorted(all_ids)[:20])
        except Exception:
            sample_ids = "não foi possível obter a lista de algoritmos."

        QgsMessageLog.logMessage(
            f"Algoritmo '{tool_name}' não encontrado no provider '{prov_id}'. "
            f"Alguns ids disponíveis: {sample_ids}", log_prefix, level=Qgis.Warning)

        self.plugin.mostrar_mensagem(
            f"Algoritmo WhiteboxTools '{human_name}' não encontrado "
            f"(id esperado parecido com '{prov_id}:{tool_name}').", "Erro")
        return None

    def _build_simple_in_out_params(self, alg, input_path: str, output_path: str, tool_desc: str):
        """
        Para ferramentas simples (1 raster de entrada, 1 raster de saída),
        descobre o primeiro parâmetro NÃO-destino como entrada e o primeiro
        parâmetro destino como saída.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        in_name = None
        out_name = None

        for p in alg.parameterDefinitions():
            # Destino → saída
            if p.isDestination():
                if out_name is None:
                    out_name = p.name()
                continue

            # Primeiro parâmetro "normal" → entrada principal
            if in_name is None:
                in_name = p.name()

        QgsMessageLog.logMessage(f"{tool_desc}: param entrada='{in_name}', saída='{out_name}'", log_prefix, level=Qgis.Info)

        if not in_name or not out_name:
            QgsMessageLog.logMessage(f"{tool_desc}: não conseguiu identificar parâmetros de entrada/saída.", log_prefix, level=Qgis.Critical)
            self.plugin.mostrar_mensagem(
                f"Não foi possível identificar parâmetros de entrada/saída "
                f"para '{tool_desc}' no WhiteboxTools (veja o Registro de Mensagens).", "Erro")
            return None

        # Para os algoritmos do WhiteboxTools, é seguro passar caminhos de arquivo
        return {in_name: input_path, out_name: output_path}

    # Pipeline público
    def start(self):
        self.plugin.mostrar_mensagem("Processamento de drenagem com WhiteboxTools iniciado em segundo plano "
            "(etapa 1/4: D8 pointer)...", "Info")
        self._start_pointer()

    # Etapa 1: D8 pointer
    def _start_pointer(self):
        # D8Pointer
        alg = self._get_algorithm("D8Pointer", "D8 pointer")
        if alg is None:
            return

        dem_path = self.safe_dem_layer.source()

        # Na interface de Processing o D8Pointer usa 'dem' (entrada) e 'output' (saída)
        QgsMessageLog.logMessage("D8 pointer: param entrada='dem', saída='output'", "WhiteboxTools - Tempo Salvo Tools", level=Qgis.Info)

        params = {"dem": dem_path, "output": self.pntr_path}

        self.task_pointer = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_pointer)
        self.task_pointer.executed.connect(self._on_pointer_finished)

        QgsApplication.taskManager().addTask(self.task_pointer)

    def _on_pointer_finished(self, successful: bool, results: dict):
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        if not successful:
            QgsMessageLog.logMessage(f"D8 pointer terminou com falha. results={results}", log_prefix, level=Qgis.Critical)
            self.plugin.mostrar_mensagem("Erro ao executar 'D8 pointer' (WhiteboxTools). "
                "Verifique o painel 'Registro de Mensagens' para detalhes.", "Erro")
            return

        # Adiciona o pointer como raster opcional
        self.plugin._add_whitebox_raster(path=self.pntr_path, name=f"{self.dem_name} - D8 pointer (Whitebox)", dem_crs=self.dem_layer.crs(), short_code="PNTR")

        self.plugin.mostrar_mensagem("Executando WhiteboxTools: D8 flow accumulation...", "Info")
        self._start_accum()

    # Etapa 2: D8 flow accumulation
    def _start_accum(self):
        # D8FlowAccumulation
        alg = self._get_algorithm("D8FlowAccumulation", "D8 flow accumulation")
        if alg is None:
            return

        dem_path = self.safe_dem_layer.source()

        # No Processing o wrapper usa 'input' (DEM ou pntr) e 'output'
        QgsMessageLog.logMessage("D8 flow accumulation: param entrada='input', saída='output'", "WhiteboxTools - Tempo Salvo Tools", level=Qgis.Info)

        params = {"input": dem_path,          # DEM "seguro"
            "output": self.accum_path}  # raster de acumulação

        self.task_accum = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_accum)
        self.task_accum.executed.connect(self._on_accum_finished)

        QgsApplication.taskManager().addTask(self.task_accum)

    def _on_accum_finished(self, successful: bool, results: dict):
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        if not successful:
            QgsMessageLog.logMessage(f"D8 flow accumulation terminou com falha. results={results}", log_prefix, level=Qgis.Critical)
            self.plugin.mostrar_mensagem("Erro ao executar 'D8 flow accumulation' (WhiteboxTools).", "Erro")
            return

        self.plugin._add_whitebox_raster( path=self.accum_path, name=f"{self.dem_name} - Acumulação (Whitebox)", dem_crs=self.dem_layer.crs(), short_code="ACC")

        self.plugin.mostrar_mensagem("Executando WhiteboxTools: Stream definition by threshold...", "Info")
        self._start_streams()

    # Etapa 3: Stream definition by threshold
    def _start_streams(self):
        # ExtractStreams
        alg = self._get_algorithm("ExtractStreams", "Extract streams")
        if alg is None:
            return

        # Em WhiteboxTools: --flow_accum, --threshold, -o
        params = {"flow_accum": self.accum_path, "threshold": self.threshold, "output": self.streams_path}

        self.task_streams = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_streams)
        self.task_streams.executed.connect(self._on_streams_finished)

        QgsApplication.taskManager().addTask(self.task_streams)

    # Etapa 3b: Raster streams -> vetor
    def _start_streams_vector(self):
        """
        Converte o raster de canais (self.streams_path) em
        vetor de linhas usando 'RasterStreamsToVector' do WhiteboxTools.
        """
        alg = self._get_algorithm("raster_streams_to_vector", "Raster streams to vector")
        if alg is None:
            return

        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        # Só para debug: lista de parâmetros disponíveis
        try:
            param_names = [p.name() for p in alg.parameterDefinitions()]
            QgsMessageLog.logMessage("Raster streams to vector: params disponíveis = " + ", ".join(param_names), log_prefix, level=Qgis.Info)
        except Exception:
            pass

        # De acordo com a doc do WhiteboxTools
        params = {"streams": self.streams_path, "d8_pntr": self.pntr_path, "output": self.streams_vec_path, "esri_pntr": False} # Se o wrapper tiver esse parâmetro, tudo bem; se não tiver, é ignorado

        self.task_streams_vec = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_streams_vec)
        self.task_streams_vec.executed.connect(self._on_streams_vector_finished)

        QgsApplication.taskManager().addTask(self.task_streams_vec)

    def _on_streams_vector_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Raster streams to vector' (WhiteboxTools).", "Erro")
            return

        # Adiciona a rede vetorial de drenagem
        self.plugin._add_whitebox_vector(path=self.streams_vec_path, name=f"{self.dem_name} - Rede de drenagem (vetor Whitebox)", dem_crs=self.dem_layer.crs(), short_code="NET")

        # Agora que o vetor de canais já foi criado e o contexto de Processing está "livre",
        self.plugin.mostrar_mensagem("Executando WhiteboxTools: Watershed (bacias)...", "Info")
        self._start_basins()

    def _on_streams_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Stream definition by threshold' (WhiteboxTools).", "Erro")
            return

        # Adiciona o raster de canais
        self.plugin._add_whitebox_raster(path=self.streams_path, name=f"{self.dem_name} - Canais (raster Whitebox)", dem_crs=self.dem_layer.crs(), short_code="SRC")

        # Para não termos duas tasks de Processing mexendo com GDAL ao mesmo tempo.
        self.plugin.mostrar_mensagem("Executando WhiteboxTools: Raster streams to vector (rede vetorial)...", "Info")
        self._start_streams_vector()

    # Etapa 4: Watershed (bacias)
    def _start_basins(self):
        """
        Gera bacias pelo WhiteboxTools, escolhendo entre:
        - Basins (bacias simples a partir do D8 pointer), ou
        - Subbasins (sub-bacias dependentes da rede de canais / THRESHOLD).
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        # Lê a escolha feita na interface (dock)
        use_subbasins = (
            getattr(self.plugin, "radioWbtSubbasins", None)
            and self.plugin.radioWbtSubbasins.isChecked())
        use_basins_simple = (
            getattr(self.plugin, "radioWbtBasins", None)
            and self.plugin.radioWbtBasins.isChecked())

        # Default: se nada estiver marcado por algum motivo,
        # assume Subbasins (comportamento mais útil para drenagem)
        if not use_subbasins and not use_basins_simple:
            use_subbasins = True

        # Escolhe algoritmo e parâmetros conforme a opção
        if use_subbasins:
            # Sub-bacias dependentes da rede de canais (usa streams + d8_pntr)
            alg = self._get_algorithm("Subbasins", "Subbasins")
            if alg is None:
                return

            QgsMessageLog.logMessage("Subbasins: entrada='d8_pntr' + 'streams', saída='output'", log_prefix, level=Qgis.Info)

            params = {"d8_pntr": self.pntr_path, "streams": self.streams_path,  # raster de canais gerado com o THRESHOLD
                "output":  self.basins_path}

        else:
            # Bacias simples, só a partir do D8 pointer (ignora THRESHOLD)
            alg = self._get_algorithm("Basins", "Basins")
            if alg is None:
                return

            QgsMessageLog.logMessage(
                "Basins: entrada='d8_pntr', saída='output'",
                log_prefix, level=Qgis.Info)

            params = {"d8_pntr": self.pntr_path, "output":  self.basins_path}

        # Cria a task e conecta o callback
        self.task_basins = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_basins)
        self.task_basins.executed.connect(self._on_basins_finished)

        QgsApplication.taskManager().addTask(self.task_basins)

    def _on_basins_vec_finished(self, successful: bool, results: dict):
        """
        Callback da task 'gdal:polygonize' (vetorização das bacias).
        """
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao vetorizar bacias (gdal:polygonize).", "Erro")
            return

        # Caminho de saída (pelo resultado ou pelo path padrão)
        vec_path = results.get("OUTPUT") or self.basins_vec_path

        # Adiciona campos de área em m² e hectares
        self._add_area_fields_to_basins_layer(vec_path)

        # Adiciona a camada vetorial de bacias ao projeto
        self.plugin._add_whitebox_vector(
            path=vec_path,
            name=f"{self.dem_name} - Bacias (Whitebox, vetor)",
            dem_crs=self.dem_layer.crs(),
            short_code="BASINS_VEC")

        self.plugin.mostrar_mensagem(
            "Processamento de drenagem com WhiteboxTools concluído "
            "(incluindo bacias vetorizadas).", "Sucesso")

        # libera referência no plugin (opcional)
        if getattr(self.plugin, "_whitebox_runner", None) is self:
            self.plugin._whitebox_runner = None

    def _add_area_fields_to_basins_layer(self, vec_path: str):
        """
        Abre o vetor de bacias gerado (vec_path) e adiciona
        campos area_m2 e area_ha preenchidos a partir da geometria.
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        layer = QgsVectorLayer(vec_path, "basins_tmp", "ogr")
        if not layer.isValid():
            QgsMessageLog.logMessage(f"Camada de bacias inválida para cálculo de área: {vec_path}", log_prefix, level=Qgis.Warning)
            return

        pr = layer.dataProvider()

        # Cria campos se ainda não existirem
        fields_to_add = []
        if layer.fields().indexFromName("area_m2") < 0:
            fields_to_add.append(QgsField("area_m2", QVariant.Double))
        if layer.fields().indexFromName("area_ha") < 0:
            fields_to_add.append(QgsField("area_ha", QVariant.Double))

        if fields_to_add:
            pr.addAttributes(fields_to_add)
            layer.updateFields()

        idx_area_m2 = layer.fields().indexFromName("area_m2")
        idx_area_ha = layer.fields().indexFromName("area_ha")

        if idx_area_m2 < 0 or idx_area_ha < 0:
            QgsMessageLog.logMessage(
                "Não foi possível localizar campos area_m2/area_ha após adicioná-los.",
                log_prefix, level=Qgis.Warning
            )
            return

        # Usa QgsDistanceArea para garantir área em m²
        dist = QgsDistanceArea()
        dist.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())
        dist.setEllipsoid(QgsProject.instance().ellipsoid())

        layer.startEditing()
        for f in layer.getFeatures():
            geom = f.geometry()
            if geom is None or geom.isEmpty():
                continue

            area_m2 = dist.measureArea(geom)
            area_ha = area_m2 / 10000.0

            layer.changeAttributeValue(f.id(), idx_area_m2, area_m2)
            layer.changeAttributeValue(f.id(), idx_area_ha, area_ha)
        layer.commitChanges()

    def _on_basins_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Watershed' (WhiteboxTools).", "Erro")
            return

        # Adiciona o raster de bacias
        self.plugin._add_whitebox_raster(path=self.basins_path, name=f"{self.dem_name} - Bacias (Whitebox)", dem_crs=self.dem_layer.crs(),  short_code="BASINS")

        # Agora dispara a vetorização (raster -> vetor)
        self.plugin.mostrar_mensagem("Vetorizar bacias (gdal:polygonize)...", "Info")
        self._start_basins_vector()

    def _start_basins_vector(self):
        """
        Vetoriza o raster de bacias (self.basins_path) usando
        o algoritmo 'gdal:polygonize' (raster → vetor).
        """
        log_prefix = "WhiteboxTools - Tempo Salvo Tools"

        # Garante que o raster de bacias existe
        if not os.path.exists(self.basins_path):
            QgsMessageLog.logMessage(f"Raster de bacias não encontrado para vetorizar: {self.basins_path}", log_prefix, level=Qgis.Warning)
            self.plugin.mostrar_mensagem("Raster de bacias não encontrado para vetorizar (WhiteboxTools).", "Erro")
            return

        reg = QgsApplication.processingRegistry()
        alg = reg.algorithmById("gdal:polygonize")
        if alg is None:
            self.plugin.mostrar_mensagem(
                "Algoritmo 'gdal:polygonize' (Polygonize raster to vector) "
                "não foi encontrado no Processing.", "Erro")
            return

        # Parâmetros conforme o algoritmo GDAL Polygonize
        params = {
            "INPUT": self.basins_path,  # raster de bacias
            "BAND": 1,                  # primeira banda
            "FIELD": "BASIN_ID",        # nome do campo de atributo
            "EIGHT_CONNECTEDNESS": False,
            "OUTPUT": self.basins_vec_path}

        # Task para vetorizar bacias
        self.task_basins_vec = QgsProcessingAlgRunnerTask(alg, params, self.context, self.feedback_basins_vec)
        self.task_basins_vec.executed.connect(self._on_basins_vec_finished)

        QgsApplication.taskManager().addTask(self.task_basins_vec)

    def _on_basins_vector_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao vetorizar bacias (native:polygonize).", "Erro")
        else:
            # Caminho de saída retornado pelo algoritmo (ou o path que definimos)
            vec_path = results.get("OUTPUT") or self.basins_vec_path

            self.plugin._add_whitebox_vector(path=vec_path, name=f"{self.dem_name} - Bacias (vetor Whitebox)", dem_crs=self.dem_layer.crs(), short_code="BASINS_VEC")

            self.plugin.mostrar_mensagem(
                "Processamento de drenagem com WhiteboxTools concluído "
                "(bacias raster e vetoriais geradas).", "Sucesso")

        # libera referência no plugin (final do pipeline)
        if getattr(self.plugin, "_whitebox_runner", None) is self:
            self.plugin._whitebox_runner = None

class TauDEMDrenagemTask(QgsTask):
    """
    Tarefa em segundo plano para processar drenagem com TauDEM:
    1) pitremove   -> FEL
    2) d8flowdir   -> P, SD8
    3) aread8      -> AD8
    4) threshold   -> SRC (raster de canais), usando máscara do DEM
    5) streamnet   -> NET (canais vetoriais) e W (bacias)
    """
    def __init__(self, plugin, dem_layer, safe_dem_layer, workspace, threshold: float):
        super().__init__("Drenagem TauDEM (Tempo Salvo Tools)", QgsTask.CanCancel)

        self.plugin = plugin
        self.dem_layer = dem_layer
        self.safe_dem_layer = safe_dem_layer
        self.workspace = workspace
        self.dem_name = dem_layer.name()
        self.threshold = float(threshold)

        original_dem_path = dem_layer.source()

        # nomes ASCII baseados no DEM original
        ascii_name = self.plugin._make_ascii_filename(original_dem_path)
        root, ext = os.path.splitext(ascii_name)

        # Saídas básicas
        self.fel_path  = os.path.join(self.workspace, f"fel_{root}.tif")
        self.p_path    = os.path.join(self.workspace, f"p_{root}.tif")
        self.sd8_path  = os.path.join(self.workspace, f"sd8_{root}.tif")
        self.ad8_path  = os.path.join(self.workspace, f"ad8_{root}.tif")

        # Saídas de canais e bacias
        self.src_path   = os.path.join(self.workspace, f"src_{root}.tif")
        self.ord_path   = os.path.join(self.workspace, f"ord_{root}.tif")
        self.tree_path  = os.path.join(self.workspace, f"tree_{root}.txt")
        self.coord_path = os.path.join(self.workspace, f"coord_{root}.txt")
        self.net_path   = os.path.join(self.workspace, f"net_{root}.shp")
        self.w_path     = os.path.join(self.workspace, f"w_{root}.tif")

        self.error_message = None

    def run(self):
        """
        Executado em thread de background.
        Não chamar GUI aqui.
        """
        log_prefix = "TauDEM - Tempo Salvo Tools"
        QgsMessageLog.logMessage("Iniciando tarefa de drenagem TauDEM...", log_prefix, level=Qgis.Info)

        try:
            # caminho do DEM seguro (vai ser usado também como máscara)
            dem_path = self.safe_dem_layer.source()
            QgsMessageLog.logMessage(f"DEM seguro usado: {dem_path}", log_prefix, level=Qgis.Info)

            # 1) PitRemove
            if self.isCanceled():
                return False

            ok, out = TAUDEM_CONFIG.run_taudem_command("pitremove.exe", ["-z", dem_path, "-fel", self.fel_path], log_prefix=log_prefix)
            if not ok:
                self.error_message = f"Erro na etapa PitRemove: {out}"
                return False

            # 2) D8FlowDir
            if self.isCanceled():
                return False

            ok, out = TAUDEM_CONFIG.run_taudem_command("d8flowdir.exe", ["-fel", self.fel_path, "-p", self.p_path, "-sd8", self.sd8_path], log_prefix=log_prefix)
            if not ok:
                self.error_message = f"Erro na etapa D8 FlowDir: {out}"
                return False

            # 3) AreaD8
            if self.isCanceled():
                return False

            ok, out = TAUDEM_CONFIG.run_taudem_command("aread8.exe", ["-p", self.p_path, "-ad8", self.ad8_path], log_prefix=log_prefix)
            if not ok:
                self.error_message = f"Erro na etapa Área de Contribuição (aread8): {out}"
                return False

            # 4) Threshold (Stream Definition by Threshold) com máscara do DEM
            if self.isCanceled():
                return False

            ok, out = TAUDEM_CONFIG.run_taudem_command("threshold.exe", ["-ssa", self.ad8_path, "-src", self.src_path, "-thresh", str(int(self.threshold)), "-mask", dem_path], log_prefix=log_prefix)
            if not ok:
                self.error_message = f"Erro na etapa Stream Definition (threshold): {out}"
                return False

            # 5) StreamNet (rede de drenagem e bacias)
            if self.isCanceled():
                return False

            ok, out = TAUDEM_CONFIG.run_taudem_command("streamnet.exe", ["-fel", self.fel_path, "-p", self.p_path, "-ad8", self.ad8_path, "-src", self.src_path, "-ord", self.ord_path, "-tree", self.tree_path, "-coord", self.coord_path, "-net", self.net_path, "-w", self.w_path], log_prefix=log_prefix)
            if not ok:
                self.error_message = f"Erro na etapa StreamNet (canais/bacias): {out}"
                return False

            return True

        except Exception as e:
            tb = traceback.format_exc()
            self.error_message = f"Erro inesperado na tarefa TauDEM: {e}"
            QgsMessageLog.logMessage(tb, log_prefix, level=Qgis.Critical)
            return False

    def cancel(self):
        QgsMessageLog.logMessage("Tarefa TauDEM cancelada.", "TauDEM - Tempo Salvo Tools", level=Qgis.Info)
        super().cancel()

class TauDEMConfig(object):
    def __init__(self):
        self.taudem_dir = None   # pasta onde estão pitremove.exe, d8flowdir.exe, etc.
        self.mpiexec = None      # caminho do mpiexec.exe
        # Número de processos MPI = nº de CPUs (ou 4 se der None)
        self.nproc = max(1, os.cpu_count() or 4)

    def _find_file_in_path(self, filename: str):
        """Procura um executável no PATH do sistema."""
        for p in os.environ.get("PATH", "").split(os.pathsep):
            cand = os.path.join(p, filename)
            if os.path.isfile(cand):
                return cand
        return None

    def _find_taudem_dir(self):
        """
        Tenta localizar a pasta do TauDEM automaticamente.
        - Usa variável de ambiente TAUDEM_DIR (se existir)
        - Testa caminhos comuns no Windows
        - Se não achar, pede para o usuário escolher a pasta (contendo pitremove.exe)
        """
        candidates = []

        # 1) variável de ambiente
        env_dir = os.environ.get("TAUDEM_DIR")
        if env_dir:
            candidates.append(env_dir)

        # 2) caminhos padrão típicos de instalação TauDEM 5.x no Windows
        candidates.extend([
            r"C:\Program Files\TauDEM\TauDEM5Exe",
            r"C:\Program Files (x86)\TauDEM\TauDEM5Exe",
            r"C:\TauDEM5Exe"])

        for c in candidates:
            if c and os.path.isdir(c) and os.path.isfile(os.path.join(c, "pitremove.exe")):
                return c

        # 3) pede pro usuário escolher a pasta
        folder = QFileDialog.getExistingDirectory(
            iface.mainWindow(),
            "Selecione a pasta onde está instalado o TauDEM (contendo pitremove.exe)")
        if folder and os.path.isfile(os.path.join(folder, "pitremove.exe")):
            return folder

        return None

    def _find_mpiexec(self):
        """
        Tenta localizar o mpiexec.exe (Microsoft MPI)
        - Testa caminhos comuns
        - Tenta achar no PATH
        - Se não achar, pede para o usuário escolher o executável
        """
        candidates = [
            r"C:\Program Files\Microsoft MPI\Bin\mpiexec.exe",
            r"C:\Program Files (x86)\Microsoft MPI\Bin\mpiexec.exe"]

        for c in candidates:
            if os.path.isfile(c):
                return c

        from_path = self._find_file_in_path("mpiexec.exe")
        if from_path:
            return from_path

        exe_path, _ = QFileDialog.getOpenFileName(iface.mainWindow(), "Selecione o executável mpiexec.exe", "", "Executáveis (*.exe)")
        if exe_path and os.path.isfile(exe_path):
            return exe_path

        return None

    def setup_taudem_config(self) -> bool:
        """
        Inicializa a configuração do TauDEM (se ainda não estiver inicializada).
        Retorna True se tudo estiver OK, False se algo estiver faltando.
        """
        # Já configurado anteriormente
        if self.taudem_dir and self.mpiexec:
            return True

        taudem_dir = self._find_taudem_dir()
        if not taudem_dir:
            QMessageBox.warning(
                iface.mainWindow(),
                "TauDEM não localizado",
                "Não foi possível localizar a pasta do TauDEM.\n\n"
                "Verifique se o TauDEM está instalado e tente novamente.")
            return False

        mpiexec_path = self._find_mpiexec()
        if not mpiexec_path:
            QMessageBox.warning(
                iface.mainWindow(),
                "mpiexec.exe não localizado",
                "Não foi possível localizar o mpiexec.exe (Microsoft MPI).\n\n"
                "Instale o Microsoft MPI e tente novamente.")
            return False

        self.taudem_dir = taudem_dir
        self.mpiexec = mpiexec_path

        QMessageBox.information(
            iface.mainWindow(),
            "TauDEM configurado",
            "TauDEM configurado com sucesso!\n\n"
            f"Pasta TauDEM: {taudem_dir}\n"
            f"mpiexec: {mpiexec_path}\n"
            f"Núcleos (processos MPI): {self.nproc}")

        return True

    def is_configured(self) -> bool:
        """Retorna True se a configuração básica já foi feita."""
        return bool(self.taudem_dir and self.mpiexec)

    def run_taudem_command(self, exe_name: str, args: list, log_prefix: str = "TauDEM - Tempo Salvo Tools"):
        """
        Executa um comando TauDEM via mpiexec.

        IMPORTANTE: não chama nenhuma GUI aqui (para ser seguro em threads).

        exe_name: nome do executável TauDEM (ex.: 'pitremove.exe', 'd8flowdir.exe').
        args: lista de argumentos (ex.: ['-z', in_dem, '-fel', out_fel]).

        Retorna (success: bool, saida_texto: str).
        """
        if not self.is_configured():
            msg = (
                "TauDEM não está configurado. "
                "Chame TAUDEM_CONFIG.setup_taudem_config() no thread principal antes.")
            QgsMessageLog.logMessage(msg, log_prefix, level=Qgis.Critical)
            return False, msg

        exe_path = os.path.join(self.taudem_dir, exe_name)
        if not os.path.isfile(exe_path):
            msg = (
                f"{exe_name} não encontrado na pasta TauDEM:\n"
                f"{self.taudem_dir}")
            QgsMessageLog.logMessage(msg, log_prefix, level=Qgis.Critical)
            return False, msg

        cmd = [self.mpiexec, "-n", str(self.nproc), exe_path] + args

        QgsMessageLog.logMessage("Executando TauDEM:", log_prefix, level=Qgis.Info)
        QgsMessageLog.logMessage(" ".join(cmd), log_prefix, level=Qgis.Info)

        # Tentar rodar sem abrir janela de CMD no Windows
        startupinfo = None
        creationflags = 0
        if os.name == "nt":
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            creationflags = subprocess.CREATE_NO_WINDOW

        try:
            proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", startupinfo=startupinfo, creationflags=creationflags)
        except Exception as e:
            tb = traceback.format_exc()
            msg = f"Erro ao executar o TauDEM: {e}"
            QgsMessageLog.logMessage(tb, log_prefix, level=Qgis.Critical)
            return False, msg

        QgsMessageLog.logMessage("\n--- Saída TauDEM ---\n" + proc.stdout, log_prefix, level=Qgis.Info)

        if proc.returncode != 0:
            msg = (
                f"TauDEM retornou código de erro {proc.returncode}.\n"
                "Verifique os caminhos, permissões e o log acima.")
            QgsMessageLog.logMessage(msg, log_prefix, level=Qgis.Critical)
            return False, msg

        return True, proc.stdout
# Instância global de configuração
TAUDEM_CONFIG = TauDEMConfig()

class SagaDrenagemRunner:
    """
    Orquestra as 3 etapas SAGA em segundo plano usando QgsProcessingAlgRunnerTask:
    1) Fill sinks (Wang & Liu)
    2) Flow accumulation (one step)
    3) Channel network and drainage basins
    """

    def __init__(self, plugin, dem_layer, safe_dem_layer, minslope: float, strahler_threshold: int):
        self.plugin = plugin                    # instância de RedesDrenagem
        self.dem_layer = dem_layer
        self.safe_dem_layer = safe_dem_layer
        self.dem_name = dem_layer.name()
        self.minslope = float(minslope)
        self.strahler_threshold = int(strahler_threshold)

        # Workspace para arquivos temporários desta execução
        self.workspace = plugin._create_safe_workspace()
        self.original_dem_path = dem_layer.source()

        # Contexto e feedback do Processing
        # IMPORTANTES: precisam viver enquanto as tasks existirem
        self.context = QgsProcessingContext()
        self.context.setProject(QgsProject.instance())

        self.feedback_fill = QgsProcessingFeedback()
        self.feedback_flow = QgsProcessingFeedback()
        self.feedback_channel = QgsProcessingFeedback()

        # Referências às tasks (para não serem coletadas)
        self.task_fill = None
        self.task_flow = None
        self.task_channel = None

        # Caminho do DEM preenchido
        self.filled_safe_path = None
        self.filled_layer = None

    # API pública
    def start(self):
        self.plugin.mostrar_mensagem(
            "Processamento de drenagem com SAGA iniciado em segundo plano "
            "(etapa 1/3: preenchimento de depressões)...", "Info")
        self._start_fill()

    # Etapa 1: Fill Sinks
    def _start_fill(self):
        filled_safe_name = self.plugin._make_ascii_filename(
            self.original_dem_path,
            prefix="dem_filled")
        self.filled_safe_path = os.path.join(self.workspace, filled_safe_name)

        params_fill = {
            "ELEV": self.safe_dem_layer,                # DEM "seguro" (camada)
            "MINSLOPE": self.minslope,
            "FDIR": QgsProcessing.TEMPORARY_OUTPUT,
            "FILLED": self.filled_safe_path,           # caminho explícito
            "WSHED": QgsProcessing.TEMPORARY_OUTPUT}

        alg = QgsApplication.processingRegistry().algorithmById("sagang:fillsinkswangliu")
        if alg is None:
            self.plugin.mostrar_mensagem("Algoritmo 'sagang:fillsinkswangliu' não encontrado.", "Erro")
            return

        self.task_fill = QgsProcessingAlgRunnerTask(alg, params_fill, self.context, self.feedback_fill)
        # Sinal executed: (successful: bool, results: dict)
        self.task_fill.executed.connect(self._on_fill_finished)

        QgsApplication.taskManager().addTask(self.task_fill)

    def _on_fill_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Fill sinks (Wang & Liu)' com SAGA.", "Erro")
            return

        # Cria camada do DEM preenchido e adiciona ao projeto
        filled_layer_name = f"{self.dem_name} - DEM preenchido"
        self.filled_layer = QgsRasterLayer(self.filled_safe_path, filled_layer_name)

        if not self.filled_layer.isValid():
            self.plugin.mostrar_mensagem("Falha ao carregar o DEM preenchido gerado pelo SAGA.", "Erro")
            return

        # Adiciona em grupo 'SAGA - Drenagem'
        self.plugin._add_saga_raster(self.filled_layer, short_code="DEM_FILLED")

        # Próxima etapa
        self.plugin.mostrar_mensagem("Executando SAGA: Flow accumulation (one step)...", "Info")
        self._start_flow()

    # Etapa 2: Flow Accumulation
    def _start_flow(self):
        params_flow = {
            "DEM": self.filled_layer,                   # usa DEM preenchido
            "TCA": QgsProcessing.TEMPORARY_OUTPUT,
            "SCA": QgsProcessing.TEMPORARY_OUTPUT,
            "PREPROCESSING": 1}                         # já fizemos o fill

        alg = QgsApplication.processingRegistry().algorithmById("sagang:flowaccumulationonestep")
        if alg is None:
            self.plugin.mostrar_mensagem("Algoritmo 'sagang:flowaccumulationonestep' não encontrado.", "Erro" )
            return

        self.task_flow = QgsProcessingAlgRunnerTask(alg, params_flow, self.context,  self.feedback_flow)
        self.task_flow.executed.connect(self._on_flow_finished)

        QgsApplication.taskManager().addTask(self.task_flow)

    def _on_flow_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Flow accumulation (one step)'.", "Erro")
            # Se quiser ainda assim tentar canais, poderia chamar _start_channel() aqui
            return

        tca_path = results.get("TCA")
        sca_path = results.get("SCA")

        # TCA
        if tca_path:
            tca_layer = QgsRasterLayer(tca_path, f"{self.dem_name} - Acumulação (TCA)")
            if tca_layer.isValid():
                self.plugin._add_saga_raster(tca_layer, short_code="TCA")
            else:
                self.plugin.mostrar_mensagem("Falha ao carregar o raster de acumulação (TCA).", "Erro")

        # SCA
        if sca_path:
            sca_layer = QgsRasterLayer(sca_path, f"{self.dem_name} - Área específica (SCA)")
            if sca_layer.isValid():
                self.plugin._add_saga_raster(sca_layer, short_code="SCA")
            else:
                self.plugin.mostrar_mensagem("Falha ao carregar o raster de área específica (SCA).", "Erro")

        # Próxima etapa
        self.plugin.mostrar_mensagem("Executando SAGA: Channel network and drainage basins...", "Info")
        self._start_channel()

    # Etapa 3: Channel Network
    def _start_channel(self):
        params_channel = {
            "DEM": self.filled_layer,
            "DIRECTION": QgsProcessing.TEMPORARY_OUTPUT,
            "CONNECTION": QgsProcessing.TEMPORARY_OUTPUT,
            "ORDER": QgsProcessing.TEMPORARY_OUTPUT,
            "BASIN": QgsProcessing.TEMPORARY_OUTPUT,
            "SEGMENTS": QgsProcessing.TEMPORARY_OUTPUT,
            "BASINS": QgsProcessing.TEMPORARY_OUTPUT,
            "NODES": QgsProcessing.TEMPORARY_OUTPUT,
            "THRESHOLD": self.strahler_threshold,
            "SUBBASINS": True}

        alg = QgsApplication.processingRegistry().algorithmById("sagang:channelnetworkanddrainagebasins")
        if alg is None:
            self.plugin.mostrar_mensagem("Algoritmo 'sagang:channelnetworkanddrainagebasins' não encontrado.", "Erro")
            return

        self.task_channel = QgsProcessingAlgRunnerTask(alg, params_channel, self.context, self.feedback_channel)
        self.task_channel.executed.connect(self._on_channel_finished)

        QgsApplication.taskManager().addTask(self.task_channel)

    def _on_channel_finished(self, successful: bool, results: dict):
        if not successful:
            self.plugin.mostrar_mensagem("Erro ao executar 'Channel network and drainage basins'.", "Erro")
            return

        seg_path = results.get("SEGMENTS")
        basins_path = results.get("BASINS")

        # Canais
        if seg_path:
            canais_layer = QgsVectorLayer(seg_path, f"{self.dem_name} - Canais SAGA", "ogr")
            if canais_layer.isValid():
                self.plugin._add_saga_vector(canais_layer, short_code="SEGMENTS")
            else:
                self.plugin.mostrar_mensagem("Falha ao carregar a camada de canais gerada pelo SAGA.", "Erro")

        # Bacias
        if basins_path:
            bacias_layer = QgsVectorLayer(basins_path, f"{self.dem_name} - Bacias SAGA", "ogr")
            if bacias_layer.isValid():
                self.plugin._add_saga_vector(bacias_layer, short_code="BASINS")
            else:
                self.plugin.mostrar_mensagem("Falha ao carregar a camada de bacias gerada pelo SAGA.", "Erro")

        # opcional: libera referência no plugin
        if getattr(self.plugin, "_saga_runner", None) is self:
            self.plugin._saga_runner = None



