from qgis.core import QgsProject, QgsCoordinateReferenceSystem, QgsField, QgsFields, QgsFeature, QgsPoint, QgsWkbTypes, QgsVectorLayer, QgsGeometry, QgsPointXY
from qgis.PyQt.QtWidgets import QDialog, QHeaderView, QComboBox
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from qgis.gui  import QgsProjectionSelectionDialog
from PyQt5.QtCore import Qt, QTimer, QLocale
from typing import List, Tuple
from qgis.PyQt import uic
import os, sys

try:
    import win32com.client # pywin32
except ImportError:
    win32com = None

try:
    import win32gui # Para detectar janelas do LibreOffice Calc
except ImportError:
    win32gui = None

try:
    # A biblioteca ooo-dev-tools facilita usar UNO em Python “puro”
    from ooodev.office.calc import Calc
    from ooodev.loader.lo import Lo
except ImportError:
    Calc = Lo = None   # leitura Calc ficará limitada

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

class CamadaManager(QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(CamadaManager, self).__init__(parent)
        # Configura a interface do usuário a partir do Designer.
        self.setupUi(self)
        # Altera o título da janela
        self.setWindowTitle("Adicionar Camada Excel")

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

        # Preenche combo na abertura do diálogo
        self._atualizar_combo_pasta()

        # Inicia auto-refresh 
        self._timer_refresh = QTimer(self)
        self._timer_refresh.timeout.connect(self._atualizar_combo_pasta)
        self._timer_refresh.start(1000)   # atualiza a cada 1s

        # permite quebra de texto nas células
        self.tableViewDados.setWordWrap(True)

        # cabeçalhos
        hv = self.tableViewDados.horizontalHeader()
        vv = self.tableViewDados.verticalHeader()

        # colunas: esticam para preencher a largura disponível
        hv.setSectionResizeMode(QHeaderView.Stretch)

        # linhas: ajustam a altura ao conteúdo
        vv.setSectionResizeMode(QHeaderView.ResizeToContents)

        hh = self.tableViewDados.horizontalHeader()
        # permite que o usuário arraste as seções
        hh.setSectionsMovable(True)
        # define modo inicial como Interactive (pode redimensionar manualmente)
        hh.setSectionResizeMode(QHeaderView.Interactive)

        # SRC atual do projeto
        proj_crs = QgsProject.instance().crs()
        self.lineEdit_SRC.setText(f"{proj_crs.authid()} - {proj_crs.description()}")
        self.lineEdit_SRC.setReadOnly(True)   # evita edição direta, opcional

        # mensagens de erro apenas para leitura
        self.textEdit_Erros.setReadOnly(True)

        # botão começa desabilitado
        self.pushButtonAdicionar.setEnabled(False)

        # Conecta os sinais aos slots
        self.connect_signals()

    def connect_signals(self):

        self.comboBox_Pasta.currentIndexChanged.connect(self._on_pasta_changed)

        # sempre que mudar X, Y ou Z, refaz os combos para manter unicidade
        self.comboBoxX.currentIndexChanged.connect(self._atualizar_combos_xyz)
        self.comboBoxY.currentIndexChanged.connect(self._atualizar_combos_xyz)
        self.comboBoxZ.currentIndexChanged.connect(self._atualizar_combos_xyz)

        self.pushButtonSRC.clicked.connect(self._selecionar_crs)

        self.pushButtonAdicionar.clicked.connect(self._adicionar_camada_pontos)
        
        self.pushButtonFecha.clicked.connect(self.close)

    def closeEvent(self, event):
        if hasattr(self, "_timer_refresh"):
            self._timer_refresh.stop()
        super(CamadaManager, self).closeEvent(event)

    def showEvent(self, event):
        """
        Sempre que o diálogo for exibido, atualiza a lista de pastas
        de trabalho do Excel abertas.
        """
        super(CamadaManager, self).showEvent(event)
        # Atualiza o combo
        self._atualizar_combo_pasta()

        # Se houver algo válido no combo, dispara o carregamento automático
        if self.comboBox_Pasta.isEnabled():
            idx = self.comboBox_Pasta.currentIndex()
            if idx >= 0:
                # reutiliza o slot que já preenche o tableViewDados
                self._on_pasta_changed(idx)

    def _listar_workbooks_excel(self) -> List[str]:
        """
        Retorna os nomes dos workbooks abertos numa sessão já em execução do Excel.
        Se o Excel não estiver aberto ou não expor Workbooks, devolve [].
        """
        if win32com is None:  # pywin32 ausente
            return []

        try:
            # Conecta APENAS a uma instância já existente
            excel = win32com.client.GetActiveObject("Excel.Application")
        except Exception:
            # Excel não está rodando
            return []

        # Alguns ambientes podem não expor 'Workbooks' direito
        try:
            wbs = excel.Workbooks
            count = wbs.Count
        except AttributeError:
            return []

        nomes = []
        for i in range(1, count + 1):  # coleção COM é 1-based
            wb = wbs.Item(i)
            nome = os.path.basename(wb.FullName) if wb.FullName else wb.Name
            nomes.append(nome)
        return nomes

    def _listar_planilhas_calc(self) -> List[str]:
        """
        Varre janelas do Windows e retorna os nomes das planilhas do Calc abertas.
        Se o nome não tiver extensão, marca como (não salvo).
        """
        if win32gui is None:
            return []

        nomes: List[str] = []

        def _handler(hwnd, _):
            titulo = win32gui.GetWindowText(hwnd)
            # vamos aceitar qualquer janela que mencione "Calc"
            if not titulo or "Calc" not in titulo:
                return

            # separa título e sufixo de janela (" - LibreOffice Calc")
            if " - " in titulo:
                base = titulo.rsplit(" - ", 1)[0].strip()
            else:
                base = titulo.strip()

            nome = os.path.basename(base)
            if not nome:
                return

            # agora detecta extensão para saber se está salvo
            root, ext = os.path.splitext(nome)
            if ext:
                display = nome                        # ex: "MinhaPlanilha.ods"
            else:
                display = f"{nome} (não salvo)"       # ex: "Sem título 1 (não salvo)"

            nomes.append(display)

        win32gui.EnumWindows(_handler, None)
        # remove duplicatas mantendo ordem
        return list(dict.fromkeys(nomes))

    def _atualizar_combo_pasta(self) -> None:
        """
        Atualiza o comboBox_Pasta sem alterar a seleção corrente
        se ela ainda existir e apenas quando a lista realmente muda.
        """
        # Coleta arquivos abertos
        excel_files = self._listar_workbooks_excel()
        calc_files  = self._listar_planilhas_calc()

        # monta lista final (duplicatas já eliminadas)
        arquivos_vistos = list(dict.fromkeys(excel_files + calc_files))

        if not arquivos_vistos:
            arquivos_display = ["Nenhuma planilha aberta"]
        else:
            arquivos_display = (
                [f"(Excel) {a}" for a in excel_files] +
                [f"(Calc)  {a}" for a in calc_files])

        # Verifica se a lista mudou
        atuais = [self.comboBox_Pasta.itemText(i)
                  for i in range(self.comboBox_Pasta.count())]

        if arquivos_display == atuais:
            # Nada mudou → não faz nada
            return

        # Guarda seleção corrente
        selecao_atual = self.comboBox_Pasta.currentText()

        # Bloqueia sinais / repaints durante a troca
        self.comboBox_Pasta.blockSignals(True)
        self.comboBox_Pasta.setUpdatesEnabled(False)

        # recria itens
        self.comboBox_Pasta.clear()
        if arquivos_display == ["Nenhuma planilha aberta"]:
            self.comboBox_Pasta.addItem("Nenhuma planilha aberta")
            self.comboBox_Pasta.setEnabled(False)
        else:
            self.comboBox_Pasta.addItems(arquivos_display)
            self.comboBox_Pasta.setEnabled(True)

        # Restaura seleção se o item ainda existir
        if selecao_atual and selecao_atual in arquivos_display:
            self.comboBox_Pasta.setCurrentIndex(
                arquivos_display.index(selecao_atual))

        # reabilita sinais / repaints
        self.comboBox_Pasta.setUpdatesEnabled(True)
        self.comboBox_Pasta.blockSignals(False)

    def _on_pasta_changed(self, index: int):
        """
        Dispara carregamento da primeira aba da planilha selecionada
        (Excel ou Calc) para o tableViewDados.
        """
        if index < 0:
            return

        texto = self.comboBox_Pasta.itemText(index).strip()
        if texto == "Nenhuma planilha aberta":
            self._limpar_tableview()
            return

        # identifica origem pelo prefixo inserido em _atualizar_combo_pasta
        if texto.startswith("(Excel)"):
            arquivo = texto.replace("(Excel)", "", 1).strip()
            self._carregar_excel(arquivo)
        elif texto.startswith("(Calc)"):
            arquivo = texto.replace("(Calc)", "", 1).strip()
            self._carregar_calc(arquivo)
        else:
            self._mostrar_erro("Formato não reconhecido.")

    def _limpar_tableview(self):
        vazio = QStandardItemModel(0, 0, self.tableViewDados)
        self.tableViewDados.setModel(vazio)

        self._atualizar_estado_botao_adicionar()

    def _fmt(self, v):
        """
        Converte o valor para string:
        • int ou float inteiro  -> '123'          (sem '.0')
        • float não-inteiro    -> '123,456'      (ponto→vírgula p/ BR-pt)
        • None ou ''           -> ''             (vazio)
        • outros tipos         -> str(v)
        """
        if v is None or v == "":
            return ""
        if isinstance(v, (int, float)):
            # se for "inteiro exato", corta a parte decimal
            if float(v).is_integer():
                return str(int(v))
            # caso contrário formata e troca ponto por vírgula
            return str(v).replace(".", ",")
        return str(v)

    def _carregar_excel(self, nome_arq: str):
        """
        Abre o workbook que já está aberto no Excel, lê apenas a primeira aba
        e exibe no tableViewDados.  Colunas 100 % numéricas ficam somente‑leitura.
        """
        # Conexão COM 
        if win32com is None:
            self._mostrar_erro("pywin32 não instalado; não consigo ler Excel.")
            return

        try:
            xl = win32com.client.GetActiveObject("Excel.Application")
        except Exception:
            self._mostrar_erro("Excel não está em execução.")
            return

        # Procura o workbook já aberto
        wb = None
        for w in xl.Workbooks:
            if os.path.basename(w.FullName) == nome_arq or w.Name == nome_arq:
                wb = w
                break
        if wb is None:
            self._mostrar_erro(f"Workbook '{nome_arq}' não encontrado.")
            return

        # Coleta dados
        sht    = wb.Sheets(1) # primeira aba
        usados = sht.UsedRange
        dados  = usados.Value # tupla 2‑D
        if not dados:
            self._mostrar_erro("Planilha vazia.")
            return

        n_rows = usados.Rows.Count
        n_cols = usados.Columns.Count

        # Identifica colunas que contêm pelo menos um valor
        keep_cols = []
        for c in range(n_cols):
            tem_valor = any(
                (dados[r][c] not in (None, ""))
                for r in range(1, n_rows))
            if tem_valor or not (
                dados[0][c] is None or dados[0][c] == "" or isinstance(dados[0][c], (int, float))):
                # mantém se tem valor OU o cabeçalho não é “Null”
                keep_cols.append(c)

        # cabeçalhos filtrados
        headers = [
            "Null" if (dados[0][c] is None or dados[0][c] == "" or isinstance(dados[0][c], (int, float))) else str(dados[0][c])
            for c in keep_cols]

        # detecta colunas numéricas já filtradas
        numeric_cols = {
            i for i, c in enumerate(keep_cols)
            if all(
                (dados[r][c] in (None, "") or
                 isinstance(dados[r][c], (int, float)) or
                 str(dados[r][c]).replace(",", ".").replace(".", "", 1).isdigit())
                for r in range(1, n_rows))}

        # monta modelo
        modelo = QStandardItemModel(max(0, n_rows - 1), len(keep_cols), self.tableViewDados)

        for i, nome in enumerate(headers):
            modelo.setHeaderData(i, Qt.Horizontal, nome)

        for r in range(1, n_rows):
            for i, c in enumerate(keep_cols):
                valor = dados[r][c]
                item = QStandardItem(self._fmt(valor))
                if i in numeric_cols:
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                modelo.setItem(r - 1, i, item)

        # Aplica ao view 
        self.tableViewDados.setModel(modelo)
        self.tableViewDados.resizeRowsToContents()
        self._ajustar_colunas()

        # Atualiza combos e validações
        self._atualizar_combos_xyz()
        for cb in (self.comboBoxX, self.comboBoxY, self.comboBoxZ):
            self._validar_coluna_xyz(cb)

        # Validação de coerência com o CRS  ➜  mantém vermelho se necessário
        self._validar_xy_por_crs()

        self._atualizar_estado_botao_adicionar()

    def _colunas_numericas(self, matriz_dados) -> set[int]:
        """
        Recebe a matriz de dados (lista/tupla de linhas, sem cabeçalho)
        e devolve o conjunto de índices de colunas 100% numéricas
        (ignora células vazias).
        """
        num_cols = set()
        if not matriz_dados:
            return num_cols

        n_cols = len(matriz_dados[0])
        for c in range(n_cols):
            numeric = True
            for linha in matriz_dados:
                if c >= len(linha):
                    continue
                v = linha[c]
                if v in (None, ""):
                    continue
                try:
                    float(str(v).replace(",", "."))
                except ValueError:
                    numeric = False
                    break
            if numeric:
                num_cols.add(c)
        return num_cols

    def _carregar_calc(self, nome_arq: str):
        """
        Lê a primeira planilha já aberta no LibreOffice Calc, monta o modelo
        para o tableViewDados e deixa colunas 100 % numéricas como somente‑leitura.
        """
        # Conexão UNO
        if Calc is None or Lo is None:
            self._mostrar_erro(
                "Leitura direta do LibreOffice requer o pacote 'ooo-dev-tools'.\n"
                "Instale com: pip install ooo-dev-tools")
            return

        try:
            lo_inst = Lo.connect()
        except Exception:
            self._mostrar_erro("Não consegui conectar ao LibreOffice em execução.")
            return

        doc = None
        for comp in lo_inst.getDesktop().getComponents():
            if comp.supportsService("com.sun.star.sheet.SpreadsheetDocument"):
                url  = comp.getURL()
                base = os.path.basename(Lo.url_to_path(url)) if url else comp.Title
                if base == nome_arq:
                    doc = comp
                    break
        if doc is None:
            self._mostrar_erro(f"Planilha '{nome_arq}' não encontrada no Calc.")
            return

        # Coleta dados
        sht   = doc.Sheets.getByIndex(0)           # primeira aba
        array = Calc.get_array(sht)                # lista de listas
        if not array:
            self._mostrar_erro("Planilha vazia.")
            return

        n_rows = len(array)
        n_cols = max(len(r) for r in array)

        # Identifica colunas que contêm pelo menos um valor
        keep_cols = []
        for c in range(n_cols):
            tem_valor = any(
                (array[r][c] not in (None, ""))
                for r in range(1, n_rows))
            if tem_valor or not (
                primeira_linha[c] in (None, "") or isinstance(primeira_linha[c], (int, float))):
                keep_cols.append(c)

        # cabeçalhos filtrados
        headers = [
            "Null" if (primeira_linha[c] in (None, "") or isinstance(primeira_linha[c], (int, float))) else str(primeira_linha[c])
            for c in keep_cols]

        # matriz sem cabeçalho, apenas colunas mantidas
        matriz = [
            [linha[c] if c < len(linha) else "" for c in keep_cols]
            for linha in array[1:]]

        # detecta colunas numéricas
        numeric_cols = {
            i for i in range(len(keep_cols))
            if all(
                (row[i] in (None, "") or
                 isinstance(row[i], (int, float)) or
                 str(row[i]).replace(",", ".").replace(".", "", 1).isdigit())
                for row in matriz)}

        # monta modelo
        modelo = QStandardItemModel(len(matriz), len(keep_cols), self.tableViewDados)
        for i, nome in enumerate(headers):
            modelo.setHeaderData(i, Qt.Horizontal, nome)

        for r, linha in enumerate(matriz):
            for i, valor in enumerate(linha):
                item = QStandardItem(self._fmt(valor))
                if i in numeric_cols:
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                modelo.setItem(r, i, item)

        # Aplica ao view
        self.tableViewDados.setModel(modelo)
        self.tableViewDados.resizeRowsToContents()
        self._ajustar_colunas()

        # Combos e validações
        self._atualizar_combos_xyz()
        for cb in (self.comboBoxX, self.comboBoxY, self.comboBoxZ):
            self._validar_coluna_xyz(cb)

        # Validação de coerência com o CRS  ➜  mantém vermelho se necessário
        self._validar_xy_por_crs()

        self._atualizar_estado_botao_adicionar()

    def _mostrar_erro(self, msg: str):
        """
        Exibe a mensagem de erro no textEdit_Erros, em cor vermelha.
        """
        # opcional: limpar erros prévios
        self.textEdit_Erros.clear()

        # configura a cor do texto para vermelho
        self.textEdit_Erros.setTextColor(Qt.red)
        # adiciona a mensagem (com quebra de linha automática)
        self.textEdit_Erros.append(msg)

    def _ajustar_colunas(self):
        """
        Ajusta as colunas para o conteúdo ou para preencher o viewport,
        mas sempre em modo Interactive para permitir redimensionamento manual.
        """
        header = self.tableViewDados.horizontalHeader()
        model = self.tableViewDados.model()
        if model is None:
            return

        n_cols = model.columnCount()
        if n_cols == 0:
            return

        # 1) Modo temporário para medir conteúdo
        for col in range(n_cols):
            header.setSectionResizeMode(col, QHeaderView.ResizeToContents)
        total_req = sum(header.sectionSize(col) for col in range(n_cols))

        viewport_w = self.tableViewDados.viewport().width()

        # 2) Modo final: sempre Interactive
        for col in range(n_cols):
            header.setSectionResizeMode(col, QHeaderView.Interactive)

        if total_req < viewport_w:
            # cabe tudo → distribui espaço extra proporcionalmente
            extra = viewport_w - total_req
            per_col = extra // n_cols
            for col in range(n_cols):
                new_w = header.sectionSize(col) + per_col
                header.resizeSection(col, new_w)
            # corrige eventuais pixels restantes no último
            sobr = viewport_w - sum(header.sectionSize(c) for c in range(n_cols))
            if sobr > 0:
                last = n_cols - 1
                header.resizeSection(last, header.sectionSize(last) + sobr)
        else:
            # não cabe → garante largura mínima do conteúdo
            for col in range(n_cols):
                # a sectionSize já foi definida no ResizeToContents
                # mas podemos reforçar chamando resizeSection
                w = header.sectionSize(col)
                header.resizeSection(col, w)

    def _atualizar_combos_xyz(self):
        """
        Popula comboBoxX, comboBoxY e comboBoxZ com colunas numéricas, sem repetir.
        - Se o usuário já escolheu um valor válido, mantém essa escolha.
        - Se não houver escolha prévia, X ganha 'X', Y ganha 'Y', Z ganha 'Z' (quando existirem).
        """
        model = self.tableViewDados.model()
        if model is None:
            return

        # 1) coleta todos os cabeçalhos cujas colunas são 100% numéricas
        numeric_headers = []
        for c in range(model.columnCount()):
            hdr = model.headerData(c, Qt.Horizontal)
            ok = True
            for r in range(model.rowCount()):
                v = model.index(r, c).data() or ""
                if v == "":
                    continue
                try:
                    float(str(v).replace(",", "."))
                except ValueError:
                    ok = False
                    break
            if ok:
                numeric_headers.append(hdr)

        # 2) lê seleções atuais (se ainda válidas)
        sel = {
            "X": self.comboBoxX.currentText() if self.comboBoxX.currentText() in numeric_headers else None,
            "Y": self.comboBoxY.currentText() if self.comboBoxY.currentText() in numeric_headers else None,
            "Z": self.comboBoxZ.currentText() if self.comboBoxZ.currentText() in numeric_headers else None,
        }

        combos = [("X", self.comboBoxX),
                  ("Y", self.comboBoxY),
                  ("Z", self.comboBoxZ)]
        usados = set()

        # 3) preenche cada combo em ordem X → Y → Z
        for letra, combo in combos:
            combo.blockSignals(True)
            combo.clear()

            # só as colunas ainda não usadas
            disponiveis = [h for h in numeric_headers if h not in usados]
            if not disponiveis:
                combo.setEnabled(False)
                combo.blockSignals(False)
                continue

            # define seleção:
            # • se já existe sel[letra], mantém (desde que esteja em disponiveis)
            # • senão, na primeira alocação, prioriza letra == 'X'/'Y'/'Z'
            if sel[letra] and sel[letra] in disponiveis:
                escolha = sel[letra]
            else:
                # prioriza header igual à letra
                if letra in disponiveis:
                    escolha = letra
                else:
                    escolha = disponiveis[0]

            combo.addItems(disponiveis)
            combo.setCurrentText(escolha)
            usados.add(escolha)

            combo.setEnabled(True)
            combo.blockSignals(False)

        # 4) revalida e limpa erros
        self.textEdit_Erros.clear()
        for cb in (self.comboBoxX, self.comboBoxY, self.comboBoxZ):
            self._validar_coluna_xyz(cb)
        self._validar_xy_por_crs()

        self._atualizar_estado_botao_adicionar()

    def _validar_coluna_xyz(self, combo: QComboBox) -> bool:
        """
        • Para X e Y: se houver células vazias, pinta o combo de vermelho
          e registra a mensagem.
        • Para Z: células vazias são aceitáveis (assumidas 0),
          portanto nunca gera erro por vazio.
        Retorna True se a coluna está OK, False se há erro.
        """
        model = self.tableViewDados.model()
        if model is None or model.columnCount() == 0:
            return True

        header_txt = combo.currentText()
        if not header_txt:
            combo.setStyleSheet("")
            return True

        # índice da coluna correspondente
        col = next(
            (c for c in range(model.columnCount())
             if model.headerData(c, Qt.Horizontal) == header_txt),
            None)
        if col is None:
            combo.setStyleSheet("")
            return True

        # Verificação de vazios
        if combo is self.comboBoxZ:
            # Z aceita vazios -> não gera erro
            combo.setStyleSheet("")
            return True

        # X ou Y: vazios são erro
        vazias = [
            r + 2  # +2 para considerar a linha original da planilha
            for r in range(model.rowCount())
            if (model.index(r, col).data() or "").strip() == ""]

        if vazias:
            combo.setStyleSheet("QComboBox{background-color:#ffcccc;}")
            linhas = ", ".join(map(str, vazias))
            self._mostrar_erro(
                f"Valores ausentes na coluna '{header_txt}' nas linhas: {linhas}")
            return False

        combo.setStyleSheet("")
        return True

    def _selecionar_crs(self):
        """
        Abre o diálogo padrão de seleção de projeções do QGIS.
        Ao confirmar, o CRS escolhido é exibido no lineEdit_SRC.
        """
        dlg = QgsProjectionSelectionDialog(self)
        # inicializa com o SRC atual do projeto (opcional)
        dlg.setCrs(QgsProject.instance().crs())

        if dlg.exec_():          # usuário clicou OK
            crs = dlg.crs()
            self.lineEdit_SRC.setText(f"{crs.authid()} - {crs.description()}")
            # (opcional) armazenar em atributo se precisar usar depois:
            self.selected_crs = crs

        self._validar_xy_por_crs()

        self._atualizar_estado_botao_adicionar()

    def _validar_xy_por_crs(self):
        """
        Valida X e Y conforme o CRS. Se houver qualquer valor fora dos
        limites, pinta os combos X/Y de vermelho e mostra mensagem única.
        """
        model = self.tableViewDados.model()
        if model is None:
            return

        # obtém CRS
        crs_text = self.lineEdit_SRC.text().split(" - ")[0].strip()
        crs = QgsCoordinateReferenceSystem(crs_text) if crs_text else None
        modo_geo = crs.isValid() and crs.isGeographic()
        modo_utm = crs.isValid() and ("UTM" in crs.description()) and not modo_geo

        # helper de limite
        def fora_limites(v, eh_x):
            if modo_geo:
                return not (-180 <= v <= 180) if eh_x else not (-90 <= v <= 90)
            if modo_utm:
                return not (100000 <= v < 1000000) if eh_x else not (0 <= v <= 10000000)
            return False

        headers = {
            model.headerData(c, Qt.Horizontal): c
            for c in range(model.columnCount())}

        qualquer_erro = False
        for cb, eh_x in ((self.comboBoxX, True), (self.comboBoxY, False)):
            cb.setStyleSheet("")  # reset
            hdr = cb.currentText()
            if hdr not in headers:
                continue
            col = headers[hdr]
            for r in range(model.rowCount()):
                txt = (model.index(r, col).data() or "").strip()
                if not txt:
                    continue
                try:
                    v = float(txt.replace(",", "."))
                except ValueError:
                    qualquer_erro = True
                    break
                if fora_limites(v, eh_x):
                    qualquer_erro = True
                    break
            if qualquer_erro:
                cb.setStyleSheet("QComboBox{background-color:#ffcccc;}")

        # mensagem única
        if qualquer_erro:
            self.textEdit_Erros.clear()
            self.textEdit_Erros.setTextColor(Qt.red)
            self.textEdit_Erros.append("As coordenadas estão incoerentes na projeção selecionada")
        else:
            # se antes havia mensagem, podemos limpar
            self.textEdit_Erros.clear()

        self._atualizar_estado_botao_adicionar()

    def _guess_qgs_field(self, value) -> int:
        """
        Devolve o tipo de campo do QGIS (QVariant.Int, Double ou String).
        """
        from qgis.PyQt.QtCore import QVariant
        if isinstance(value, int):
            return QVariant.Int
        try:
            float(str(value).replace(",", "."))
            return QVariant.Double
        except Exception:
            return QVariant.String

    def _adicionar_camada_pontos(self):
        """
        Constrói e adiciona ao projeto QGIS uma camada de pontos
        baseada no tableViewDados.  Usa X/Y obrigatórios, Z opcional.
        """
        model = self.tableViewDados.model()
        if model is None or model.rowCount() == 0:
            self._mostrar_erro("Tabela vazia — nada a adicionar.")
            return

        # Índices das colunas X, Y, (Z)
        headers = {
            model.headerData(c, Qt.Horizontal): c
            for c in range(model.columnCount())}
        x_hdr = self.comboBoxX.currentText()
        y_hdr = self.comboBoxY.currentText()
        z_hdr = self.comboBoxZ.currentText() if self.comboBoxZ.currentText() else None

        if not x_hdr or not y_hdr:
            self._mostrar_erro("Defina colunas para X e Y antes de adicionar.")
            return
        col_x = headers.get(x_hdr)
        col_y = headers.get(y_hdr)
        col_z = headers.get(z_hdr) if z_hdr else None

        # CRS
        crs_auth = self.lineEdit_SRC.text().split(" - ")[0].strip()
        crs = QgsCoordinateReferenceSystem(crs_auth) \
            if crs_auth else QgsProject.instance().crs()
        if not crs.isValid():
            crs = QgsProject.instance().crs()

        # Extrai nome da planilha selecionada
        texto = self.comboBox_Pasta.currentText().strip()
        if texto.startswith("(Excel)"):
            nome_layer = texto.replace("(Excel)", "", 1).strip()
        elif texto.startswith("(Calc)"):
            nome_layer = texto.replace("(Calc)", "", 1).strip()
        else:
            nome_layer = texto

        # Criar camada em memória com esse nome
        is_3d = col_z is not None
        wkb = QgsWkbTypes.PointZ if is_3d else QgsWkbTypes.Point
        layer = QgsVectorLayer(
            f"{QgsWkbTypes.displayString(wkb)}?crs={crs.authid()}",
            nome_layer,"memory")

        pr = layer.dataProvider()

        # Campos (todos os cabeçalhos da tabela)
        fields = QgsFields()
        for c in range(model.columnCount()):
            sample_val = model.index(0, c).data()
            ftype = self._guess_qgs_field(sample_val)
            fields.append(QgsField(model.headerData(c, Qt.Horizontal), ftype))
        pr.addAttributes(fields)
        layer.updateFields()

        # Adicionar feições
        feats = []
        for r in range(model.rowCount()):
            x_txt = (model.index(r, col_x).data() or "").replace(",", ".")
            y_txt = (model.index(r, col_y).data() or "").replace(",", ".")
            if x_txt == "" or y_txt == "":
                continue

            try:
                x = float(x_txt)
                y = float(y_txt)
            except ValueError:
                continue

            # Cria a feição
            f = QgsFeature(layer.fields())

            # geometria 2D ou 3D
            if is_3d:
                z_txt = (model.index(r, col_z).data() or "").replace(",", ".")
                try:
                    z = float(z_txt)
                except ValueError:
                    z = 0.0
                geom = QgsGeometry.fromPoint(QgsPoint(x, y, z))
            else:
                geom = QgsGeometry.fromPointXY(QgsPointXY(x, y))

            f.setGeometry(geom)

            # atributos
            for c in range(model.columnCount()):
                f.setAttribute(c, model.index(r, c).data())

            feats.append(f)

        pr.addFeatures(feats)
        layer.updateExtents()

        # Adiciona ao projeto
        QgsProject.instance().addMapLayer(layer)

    def _atualizar_estado_botao_adicionar(self):
        """
        Habilita o pushButtonAdicionar apenas se:
          • Há dados na tabela (rowCount > 0) e
          • Não há mensagens no textEdit_Erros.
        """
        model = self.tableViewDados.model()
        has_data   = bool(model and model.rowCount() > 0)
        has_error  = bool(self.textEdit_Erros.toPlainText().strip())
        self.pushButtonAdicionar.setEnabled(has_data and not has_error)

