# -*- coding: utf-8 -*-

# /***************************************************************************
#  VectorToMap - QGIS Plugin
#  Automates the generation of print layouts for vector features.
#  Author: Matheus Durso Neves Caetano
# 
#  Copyright (C) 2026 Matheus Durso Neves Caetano
# 
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
# 
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#  ***************************************************************************/

import math
import re
import unicodedata
import os
import gc
import traceback
import sys
# --- NOVO: Força o Python a enxergar as bibliotecas dentro da pasta do plugin ---
plugin_dir = os.path.dirname(__file__)
if plugin_dir not in sys.path:
    sys.path.insert(0, plugin_dir)
# -------------------------------------------------------------------------------
import sentry_sdk
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QTranslator, QCoreApplication, Qt, QTimer, QUrl
from qgis.PyQt.QtGui import QIcon, QPixmap, QColor, QFont
from qgis.PyQt.QtWidgets import (
    QAction, QCheckBox, QTableWidgetItem, QFrame, QPushButton,
    QDialogButtonBox, QButtonGroup, QApplication, QGridLayout,
    QFileDialog, QMessageBox, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QFontDialog, QGraphicsDropShadowEffect)
from qgis.core import (
    QgsProject, QgsPrintLayout, QgsLayoutSize, QgsUnitTypes, QgsLayoutItemMap, 
    QgsLayoutPoint, QgsMapLayerProxyModel, QgsFeatureRequest, QgsSettings, 
    QgsLayoutExporter, QgsRectangle, QgsCoordinateTransform, QgsLayoutItemPage, 
    QgsLayoutItemLabel, QgsLayoutItem, QgsWkbTypes, QgsExpression, QgsVectorLayer, NULL,
    Qgis, QgsMessageLog
)
# Resources generated by Plugin Builder
from .resources import *
from .vector_to_map_dialog import VectorToMapDialog


class BoasVindasDialog(QDialog):
    """Janela customizada de boas-vindas com consentimento explícito, integrada ao estilo QGIS."""
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # --- NOVO: Atalho interno para tradução ---
        def tr(texto):
            return QCoreApplication.translate('@default', texto)
        # ------------------------------------------

        self.setWindowTitle("VectorToMap")
        self.resize(450, 220) 
        
        self.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(20, 20, 20, 20)
        layout.setSpacing(15)

        # Envelopando o título
        lbl_titulo = QLabel(tr("Bem-vindo ao VectorToMap!"))
        fonte_titulo = lbl_titulo.font()
        fonte_titulo.setPointSize(fonte_titulo.pointSize() + 4) 
        fonte_titulo.setBold(True)
        lbl_titulo.setFont(fonte_titulo)
        layout.addWidget(lbl_titulo)

        # Envelopando o texto principal
        texto = tr(
            "Para manter este plugin sempre funcionando perfeitamente, nós "
            "gostaríamos da sua ajuda.\n\n"
            "Você aceita enviar relatórios automáticos e totalmente anônimos "
            "caso algum erro ocorra no sistema? Nenhuma informação pessoal, "
            "nome de arquivo ou dado geográfico será enviado."
        )
        lbl_texto = QLabel(texto)
        lbl_texto.setWordWrap(True)
        
        fonte_texto = lbl_texto.font()
        fonte_texto.setPointSize(fonte_texto.pointSize() + 1)
        lbl_texto.setFont(fonte_texto)
        layout.addWidget(lbl_texto)

        layout.addSpacing(20) 
        layout.addStretch()  

        layout_botoes = QHBoxLayout()
        
        # Envelopando os botões
        self.btn_recusar = QPushButton(tr("Não aceito, apenas começar"))
        self.btn_recusar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_recusar.clicked.connect(self.reject) 

        self.btn_aceitar = QPushButton(tr("Aceitar e Começar"))
        self.btn_aceitar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_aceitar.setDefault(True) 
        self.btn_aceitar.clicked.connect(self.accept) 

        layout_botoes.addWidget(self.btn_recusar)
        layout_botoes.addStretch()
        layout_botoes.addWidget(self.btn_aceitar)

        layout.addLayout(layout_botoes)


class VectorToMap:

    ####################################################################################
    # --- 1. CICLO DE VIDA E CONFIGURAÇÕES INICIAIS ---
    ####################################################################################

    def __init__(self, iface):
        """Initialize the plugin and define global state variables."""

        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.actions = []
        self.menu = self.tr(u'&VectorToMap')
        self.first_start = True
        self.is_rendering = False # Flag to prevent infinite rendering loops
        self.colunas_atuais = []
        
        self.preview_data_cache = None
        self.ultimo_id_camada = None
        self.cache_val_atlas = None
        self.clones_preview = []
        
        # Determina a língua de exibição
        locale = QgsSettings().value('locale/userLocale')[0:2] 

        if locale != 'pt':
            # O path vai buscar vector_to_map_en.qm ou vector_to_map_es.qm
            path = os.path.join(self.plugin_dir, 'i18n', f'vector_to_map_{locale}.qm')
            
            # Se o espanhol não existir por algum motivo, ele volta pro inglês
            if not os.path.exists(path):
                path = os.path.join(self.plugin_dir, 'i18n', 'vector_to_map_en.qm')

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

    #Indica um trecho que deve ser traduzido
    def tr(self, message):
        """Translate strings using the QGIS internationalization system."""

        return QCoreApplication.translate('VectorToMap', message)
    

    #Determina variáveis de sistema e configuração de aparência
    def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None):
        """Helper function to add buttons and menus to the QGIS interface."""

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        if status_tip: action.setStatusTip(status_tip)
        if whats_this: action.setWhatsThis(whats_this)
        if add_to_toolbar: self.iface.addToolBarIcon(action)
        if add_to_menu: self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action
    

    def initGui(self):
        """Cria as entradas de menu e a barra de ferramentas personalizada."""

        icon_path = ':/plugins/vector_to_map/icon.png'
        
        # 1. CRIAÇÃO DA BARRA DE FERRAMENTAS EXCLUSIVA
        self.toolbar = self.iface.addToolBar(u'VectorToMap')
        self.toolbar.setObjectName(u'VectorToMap')

        #Botão Render Preview
        self.btn_render = QPushButton(self.tr("Preview"))

        # 2. CRIAÇÃO DA AÇÃO PRINCIPAL
        self.action = self.add_action(
            icon_path,
            text=self.tr(u'VectorToMap'),
            callback=self.run,
            parent=self.iface.mainWindow(),
            add_to_toolbar=True,
            add_to_menu=True
        )

        # Adiciona atalho para o plugin
        self.action.setShortcut("Ctrl+Alt+V")


    def unload(self):
        """Limpa os recursos do plugin ao ser desativado."""
        
        # 1. Limpa as camadas de preview da memória
        self.limpar_clones_preview()

        # 2. Remove as ações dos menus e barras
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.iface.removeToolBarIcon(action)
        
        # 3. Limpeza segura da Toolbar (Foco QGIS 4.0)
        if hasattr(self, 'toolbar') and self.toolbar:
            if not sip.isdeleted(self.toolbar):
                self.iface.mainWindow().removeToolBar(self.toolbar)
                # No QGIS 4.0, deleteLater() é a forma elegante de dizer ao Qt: 
                # "Delete isso assim que terminar de processar os eventos atuais"
                self.toolbar.deleteLater() 
            del self.toolbar

        # 4. Limpeza segura do Diálogo
        if hasattr(self, 'dlg') and self.dlg:
            if not sip.isdeleted(self.dlg):
                self.dlg.close()
                self.dlg.deleteLater()
            del self.dlg

        # 5. Coleta de lixo final
        gc.collect()

    
    def _verificar_consentimento_sentry(self):
        """Pergunta ao usuário apenas na primeira vez que ele abre o plugin."""
        settings = QgsSettings()
        
        # Verifica se já fizemos essa pergunta no passado
        ja_perguntou = settings.value("/VectorToMap/sentry_ja_perguntou", False, type=bool)
        
        if not ja_perguntou:
            # Chama a NOSSA janela estilizada
            dialog = BoasVindasDialog(self.iface.mainWindow())
            
            # Mostra a janela e congela o QGIS até o usuário clicar em algo
            # Se clicou em Aceitar, exec_() retorna 1. Se clicou em Recusar, retorna 0.
            aceitou = (dialog.exec_() == 1)
            
            # Salva a escolha na memória do QGIS
            settings.setValue("/VectorToMap/enviar_erros_sentry", aceitou)
            settings.setValue("/VectorToMap/sentry_ja_perguntou", True)

        # Retorna a escolha salva
        return settings.value("/VectorToMap/enviar_erros_sentry", False, type=bool)


    def inicializar_sentry(self):
        """Liga o motor do Sentry se o usuário permitiu."""
        self.sentry_ativo = False
        try:
            sentry_sdk.init(
                dsn="https://3a3fd55bd680f6cc5594929bec0c7609@o4511038786240512.ingest.de.sentry.io/4511038808457296",
                send_default_pii=False, 
                release="vectortomap@1.1.0" 
            )
            self.sentry_ativo = True
            
        except ImportError:
            # Falha silenciosa se a biblioteca não estiver na pasta
            pass
        except Exception:
            # Qualquer outro erro de inicialização é ignorado para não travar o plugin
            pass

    
    # ##################################################################################
    # --- 2. INTERFACE E DIÁLOGO (REFATORADO v0.4.4) ---
    # ##################################################################################

    def run(self):
        """Maestro da Interface: Inicializa a UI e orquestra a montagem da janela."""
        
        if self.first_start:
            self.first_start = False
            
            # --- Verifica consentimento e liga o Sentry ---
            aceitou_sentry = self._verificar_consentimento_sentry()
            if aceitou_sentry:
                self.inicializar_sentry()
            # ----------------------------------------------------

            self.dlg = VectorToMapDialog()

            # --- Força os botões de Maximizar e Minimizar a aparecerem ---
            self.dlg.setWindowFlags(self.dlg.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint | Qt.WindowType.WindowMinimizeButtonHint)
            # -------------------------------------------------------------------

            # 1. Aplica as cores e estilos visuais
            self._aplicar_estilos()

            # 2. Preenche as listas e cria os botões personalizados
            self._configurar_widgets_iniciais()

            # 3. Conecta os cliques e gatilhos de eventos
            self._conectar_sinais()

            # 4. Ajusta o tamanho, posição e comportamento da janela no Windows/Mac
            self._configurar_janela()

            # 5. Prepara os textos para tradução
            self.setup_ui_strings()  
        
        # --- Sincroniza a ComboBox com a camada selecionada no QGIS ---
        camada_ativa = self.iface.activeLayer()
        # Verifica se existe uma camada selecionada E se ela é do tipo Vetorial
        if camada_ativa and isinstance(camada_ativa, QgsVectorLayer):
            self.dlg.mMapLayerComboBox.setLayer(camada_ativa)
        # --------------------------------------------------------------------

        self.dlg.show()


    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE INTERFACE ---
    # ------------------------------------------------------------------------------------

    def _aplicar_estilos(self):
        """Injeta o CSS consolidado (Azul Profissional & Clean) e esconde barras padrão."""
        estilo_consolidado = """
            /* 1. ESTILO GERAL DAS BARRAS DE PROGRESSO */
            QProgressBar {
                border: 1px solid #dcdcdc;
                border-radius: 4px;
                text-align: center;
                background-color: #f0f0f0;
                height: 14px;
                font-size: 10px;
                color: #333;
            }
            QProgressBar::chunk {
                background-color: #3498db; /* Azul Claro */
                border-radius: 3px;
            }

            /* 2. BARRA DA PREVIEW (ULTRA-FINA ESTILO YOUTUBE) */
            QProgressBar#previewProgressBar {
                border: 1px solid #dcdcdc;
                border-radius: 4px;
                background-color: #f0f0f0;
                height: 3px;
                color: #333; 
            }
            QProgressBar#previewProgressBar::chunk {
                background-color: #3498db;
                border-radius: 3px;
            }

            /* 3. BARRAS DE ROLAGEM MODERNAS (SCROLLBARS) */
            QScrollBar:vertical, QScrollBar:horizontal {
                border: none;
                background: #f8f8f8;
                width: 8px;
                height: 8px;
                margin: 0px;
            }
            QScrollBar::handle {
                background: #cccccc;
                border-radius: 4px;
                min-height: 20px;
                min-width: 20px;
            }
            QScrollBar::handle:hover {
                background: #707070; /* cinza */
            }
            QScrollBar::add-line, QScrollBar::sub-line {
                border: none;
                background: none;
                height: 0px;
                width: 0px;
            }
            /* 4. PADRONIZAÇÃO DOS WIDGETS DE TEXTO */
            QCheckBox, QLabel, QRadioButton, QgsMapLayerComboBox, QComboBox {
                font-size: 9pt; 
            }
        """
        self.dlg.setStyleSheet(estilo_consolidado)
        self.dlg.previewProgressBar.hide()
        self.dlg.progressBar.hide()
        self.dlg.lbl_preview.setFrameShape(QFrame.Shape.StyledPanel)

        # --- NOVO: Efeito de Sombra na Folha de Preview ---
        sombra = QGraphicsDropShadowEffect()
        sombra.setBlurRadius(20) 
        sombra.setXOffset(5)
        sombra.setYOffset(5)
        sombra.setColor(QColor(0, 0, 0, 80)) 

        if hasattr(self.dlg, 'lbl_preview'):
            self.dlg.lbl_preview.setGraphicsEffect(sombra)
        # --------------------------------------------------


    def _configurar_widgets_iniciais(self):
        """Popula as ComboBoxes, recria os botões na ordem certa e define variáveis de estado."""
        # Isolamento dos Radio Buttons
        self.grupo_orientacao = QButtonGroup(self.dlg)
        self.grupo_orientacao.addButton(self.dlg.rb_retrato)
        self.grupo_orientacao.addButton(self.dlg.rb_paisagem)

        self.grupo_escala = QButtonGroup(self.dlg)
        self.grupo_escala.addButton(self.dlg.rb_escala_auto)
        self.grupo_escala.addButton(self.dlg.rb_escala_fixa)

        # Setup Tamanhos de Página
        self.dlg.combo_tamanho_pagina.clear()
        tamanhos_disponiveis = [
            ("A5 (148 x 210 mm)", (148.0, 210.0)),
            ("A4 (210 x 297 mm)", (210.0, 297.0)),
            ("A3 (297 x 420 mm)", (297.0, 420.0)),
            ("A2 (420 x 594 mm)", (420.0, 594.0)),
            ("A1 (594 x 841 mm)", (594.0, 841.0)),
            ("A0 (841 x 1189 mm)", (841.0, 1189.0))
        ]
        for texto, dimensoes in tamanhos_disponiveis:
            self.dlg.combo_tamanho_pagina.addItem(texto, dimensoes)
        self.dlg.combo_tamanho_pagina.setCurrentIndex(1) # Força o A4

        # Setup Escala
        self.dlg.combo_escala_fixa.clear()
        escalas = [
            ("1:1.000", 1000), ("1:5.000", 5000), ("1:10.000", 10000),
            ("1:50.000", 50000), ("1:100.000", 100000), 
            ("1:250.000", 250000), ("1:500.000", 500000), ("1:1.000.000", 1000000),
            ("1:2.500.000", 2500000), ("1:5.000.000", 5000000), ("1:7.500.000", 7500000),
            ("1:10.000.000", 10000000)
        ]
        for texto, valor in escalas:
            self.dlg.combo_escala_fixa.addItem(texto, valor)
        self.dlg.combo_escala_fixa.setCurrentIndex(2)

        # Filtro de Camadas Vectoriais
        self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)

        # Setup de Presets
        self.dlg.combo_presets.setItemText(0, self.tr("Mapa Quadrado"))
        self.dlg.combo_presets.setItemData(0, "quadrado")
        self.dlg.combo_presets.setItemText(1, self.tr("Mapa Horizontal"))
        self.dlg.combo_presets.setItemData(1, "horizontal")
        self.dlg.combo_presets.addItem(self.tr("Mapa Vertical"), "vertical")

        # Setup de Cor de Fundo
        if hasattr(self.dlg, 'btn_cor_fundo'):
            self.dlg.btn_cor_fundo.setColor(QColor(255, 255, 255, 255))
            self.dlg.btn_cor_fundo.setAllowOpacity(True)
            self.dlg.btn_cor_fundo.setToolTip(self.tr("Escolha a cor de fundo do mapa. Reduza a Opacidade para 0% para exportar transparente."))

        # Reconstrução Segura dos Botões
        try:
            self.dlg.button_box.accepted.disconnect()
            self.dlg.button_box.rejected.disconnect()
        except: pass

        self.dlg.button_box.clear()
        self.btn_export = QPushButton(self.tr("Exportar"))
        self.btn_render = QPushButton(self.tr("Preview"))
        self.btn_ok = QPushButton(self.tr("OK"))
        self.btn_cancel = QPushButton(self.tr("Cancelar"))

        self.dlg.button_box.addButton(self.btn_export, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_render, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_ok, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_cancel, QDialogButtonBox.ButtonRole.ActionRole)

        # --- NOVO: SETUP DO TÍTULO E ALINHAMENTO ---
        self.fonte_titulo = QFont("Arial", 20, QFont.Weight.Bold)

        if hasattr(self.dlg, 'lineEdit_titulo'):
            self.dlg.lineEdit_titulo.textChanged.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'btn_fonte_titulo'):
            self.dlg.btn_fonte_titulo.clicked.connect(self.escolher_fonte_titulo)
            
        # Preenche a ComboBox de Alinhamento e conecta ao gerador de Preview
        if hasattr(self.dlg, 'combo_alinhamento_titulo'):
            self.dlg.combo_alinhamento_titulo.clear()
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Esquerda"), Qt.AlignmentFlag.AlignLeft)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Centro"), Qt.AlignmentFlag.AlignHCenter)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Direita"), Qt.AlignmentFlag.AlignRight)
            self.dlg.combo_alinhamento_titulo.setCurrentIndex(1) # Força o "Centro" como padrão
            self.dlg.combo_alinhamento_titulo.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
        # -------------------------------------------

        # Variáveis de Estado
        self.export_info = None
        self.abort_processing = False
        self.is_rendering = False
        
        # Setup Timer Preview
        self.timer_preview = QTimer()
        self.timer_preview.setSingleShot(True)


    def _conectar_sinais(self):
        """Faz todas as ligações entre as ações do usuário (cliques) e as funções lógicas."""
        # Botões Principais
        self.btn_export.clicked.connect(self.preparar_exportacao)
        self.btn_render.clicked.connect(self.atualizar_preview)
        self.btn_ok.clicked.connect(self.processar_clique_ok)
        self.btn_cancel.clicked.connect(self.cancelar_e_fechar)

        # Timer e Preview Auto
        self.timer_preview.timeout.connect(self.atualizar_preview)
        if hasattr(self.dlg, 'chk_preview_auto'):
            self.dlg.chk_preview_auto.setChecked(False)
            self.dlg.chk_preview_auto.stateChanged.connect(
                lambda state: self.timer_preview.start(250) if state == Qt.CheckState.Checked else None
            )

        # Gatilhos de Escala e Camada
        self.dlg.mMapLayerComboBox.layerChanged.connect(self.configurar_escala_ao_mudar_camada)
        self.dlg.mMapLayerComboBox.layerChanged.connect(self.disparar_preview_se_autorizado)
        self.dlg.rb_escala_auto.toggled.connect(self.validar_geometria_escala)
        self.dlg.rb_escala_fixa.toggled.connect(self.validar_geometria_escala)
        
        # Gatilhos de Layout
        self.dlg.chk_selecionar_todos.stateChanged.connect(self.marcar_desmarcar_todos)
        self.dlg.chk_numeracao.toggled.connect(self.disparar_preview_se_autorizado)
        if hasattr(self.dlg, 'chk_apenas_mapa'): self.dlg.chk_apenas_mapa.stateChanged.connect(self.disparar_preview_se_autorizado)
        if hasattr(self.dlg, 'btn_cor_fundo'): self.dlg.btn_cor_fundo.colorChanged.connect(self.disparar_preview_se_autorizado)

        # Gatilhos de Hierarquia e Trava de Estilos
        self.dlg.chk_filtrar_feicoes.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_exibir_so_camada_atual.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_travar_camadas.stateChanged.connect(self.atualizar_estado_travar_estilos)

        # Monitoramento em Massa para Disparo de Preview
        widgets_preview = [
            self.dlg.combo_tamanho_pagina, self.dlg.combo_presets, self.dlg.combo_atlas,
            self.dlg.rb_retrato, self.dlg.rb_paisagem, self.dlg.combo_escala_fixa,
            self.dlg.chk_modo_formulario, self.dlg.chk_modo_individual, self.dlg.chk_exibir_atributos,
            self.dlg.rb_escala_auto, self.dlg.rb_escala_fixa, 
            self.dlg.chk_filtrar_feicoes, self.dlg.chk_exibir_so_camada_atual
        ]
        for w in widgets_preview:
            if hasattr(w, 'currentIndexChanged'): w.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
            elif hasattr(w, 'stateChanged'): w.stateChanged.connect(self.disparar_preview_se_autorizado)
            elif hasattr(w, 'toggled'): w.toggled.connect(self.disparar_preview_se_autorizado)
            
        # Salva o comportamento original de redimensionamento do QGIS
        original_resize = self.dlg.resizeEvent
        
        def novo_resize_event(event):
            self.disparar_preview_se_autorizado()
            original_resize(event) # Repassa a ordem para o Qt organizar os botões
            
        self.dlg.resizeEvent = novo_resize_event

        # Força o estado inicial dos gatilhos
        self.configurar_escala_ao_mudar_camada()
        self.atualizar_hierarquia_camadas()


    def _configurar_janela(self):
        """Define ícones, comportamento do sistema operacional, restaura tamanho e centraliza."""
        self.dlg.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.dlg.setWindowTitle("VectorToMap")
        
        self.dlg.setWindowFlags(
            Qt.WindowType.Window | 
            Qt.WindowType.WindowMinimizeButtonHint | 
            Qt.WindowType.WindowMaximizeButtonHint | 
            Qt.WindowType.WindowCloseButtonHint
        )

        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            self.dlg.lbl_aviso_feicoes.hide()

        # Restore window geometry
        settings = QgsSettings()
        geometria = settings.value("/VectorToMap/geometry")
        if geometria: 
            self.dlg.restoreGeometry(geometria)
        else: 
            self.dlg.resize(1050, 750) 
        
        if hasattr(self.dlg, 'splitter'): 
            self.dlg.splitter.setSizes([550, 500]) 

        # Centralizar na Tela
        screen_geometry = QApplication.primaryScreen().availableGeometry()
        centro_tela = screen_geometry.center()
        geometria_janela = self.dlg.frameGeometry()
        geometria_janela.moveCenter(centro_tela)
        self.dlg.move(geometria_janela.topLeft())
    

    def preparar_exportacao(self):
        """Abre o diálogo, sugere o último diretório e configura a exportação."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        # --- RECUPERA O ÚLTIMO DIRETÓRIO ---
        settings = QgsSettings()
        ultimo_dir = settings.value("/VectorToMap/ultimo_diretorio_exportacao", os.path.expanduser("~"))

        # Monta um nome base amigável já no diretório correto
        nome_sugerido = os.path.join(ultimo_dir, f"Mapas_{camada.name()}")
        
        # 1. ADICIONAMOS O SVG AQUI NO FILTRO
        filtros = "PDF (*.pdf);;SVG (*.svg);;PNG (*.png);;JPEG (*.jpg)"

        caminho_arquivo, filtro_selecionado = QFileDialog.getSaveFileName(
            self.dlg, self.tr("Exportar Mapas"), nome_sugerido, filtros
        )

        if caminho_arquivo:
            # --- SALVA O DIRETÓRIO ESCOLHIDO PARA A PRÓXIMA VEZ ---
            diretorio_escolhido = os.path.dirname(caminho_arquivo)
            settings.setValue("/VectorToMap/ultimo_diretorio_exportacao", diretorio_escolhido)

            # 2. ADICIONAMOS A IDENTIFICAÇÃO DO SVG AQUI
            ext = ".pdf"
            if "PNG" in filtro_selecionado: ext = ".png"
            elif "JPEG" in filtro_selecionado: ext = ".jpg"
            elif "SVG" in filtro_selecionado: ext = ".svg" 

            # Adiciona a extensão se o usuário esqueceu de digitar
            if not caminho_arquivo.lower().endswith(ext):
                caminho_arquivo += ext

            # 3. FORÇAMOS INDIVIDUAL PARA SVG TAMBÉM
            is_individual = self.dlg.chk_export_individual.isChecked() or ext in [".png", ".jpg", ".svg"]

            # Armazena as configurações e dispara o gerador
            self.export_info = {
                'path': caminho_arquivo,
                'ext': ext,
                'individual': is_individual
            }
            self.processar_clique_ok()


    def criar_camada_temporaria(self, camada_original, feicoes, nome_pagina, is_preview=False):
        """Cria uma memory layer. Se for preview, faz oculto. Se for final, joga numa pasta acima da original."""
                        
        crs = camada_original.crs().authid()
        geom_type = camada_original.wkbType()
        geom_string = QgsWkbTypes.displayString(geom_type)
        
        # Forma mais robusta de montar a URI para memory layers
        uri = f"{QgsWkbTypes.displayString(camada_original.wkbType())}?crs={camada_original.crs().authid()}"
        camada_temp = QgsVectorLayer(uri, f"Mapa_{nome_pagina}", "memory")

        # --- NOVO: A BLINDAGEM DO CRS ---
        # Força a cópia exata do CRS, mesmo que a camada original não tenha código EPSG
        camada_temp.setCrs(camada_original.crs())
        
        provider = camada_temp.dataProvider()
        provider.addAttributes(camada_original.fields())
        camada_temp.updateFields()
        
        provider.addFeatures(feicoes)
        camada_temp.updateExtents() 
        
        # Copia o Renderer (cores, preenchimentos)
        camada_temp.setRenderer(camada_original.renderer().clone())
        
        # Opcional: Copia os rótulos também, se existirem
        if camada_original.labelsEnabled():
            camada_temp.setLabeling(camada_original.labeling().clone())
            camada_temp.setLabelsEnabled(True)
        
        if is_preview:
            # PREVIEW: O QGIS carrega na memória do projeto, mas esconde da legenda
            QgsProject.instance().addMapLayer(camada_temp, False)
        else:
            # MAPA FINAL: Cria um grupo posicionado imediatamente acima da camada original
            root = QgsProject.instance().layerTreeRoot()
            
            # --- NOVA LÓGICA DE GRUPO DINÂMICO ---
            # Define o nome do grupo específico para esta camada
            nome_grupo_temp = f"Temp - VectorToMap ({camada_original.name()})"
            
            # Procura se o grupo ESPECÍFICO desta camada já existe
            grupo = root.findGroup(nome_grupo_temp)
            
            if not grupo:
                # 1. Encontra o "nó" da camada original na árvore do QGIS
                no_camada_original = root.findLayer(camada_original.id())
                
                if no_camada_original:
                    # 2. Descobre quem é o "pai" (pode ser a raiz ou uma pasta existente)
                    no_pai = no_camada_original.parent()
                    
                    # 3. Descobre a posição (índice) exata da camada original dentro desse pai
                    idx_insercao = 0
                    for i, filho in enumerate(no_pai.children()):
                        if filho == no_camada_original:
                            idx_insercao = i
                            break
                            
                    # 4. Insere o grupo específico nessa mesma posição, empurrando a original para baixo
                    grupo = no_pai.insertGroup(idx_insercao, nome_grupo_temp)
                else:
                    # Fallback de segurança caso a camada original não seja encontrada
                    grupo = root.insertGroup(0, nome_grupo_temp)
            
            # Adiciona a camada de forma invisível à raiz, e depois move para o nosso grupo
            QgsProject.instance().addMapLayer(camada_temp, False) 
            grupo.addLayer(camada_temp)
            
        return camada_temp


    def limpar_clones_preview(self):
        """Remove da memória apenas os clones gerados invisivelmente para a preview."""
        
        if hasattr(self, 'clones_preview') and self.clones_preview:
            QgsProject.instance().removeMapLayers(self.clones_preview)
            self.clones_preview.clear()


    def atualizar_hierarquia_camadas(self):
        """Controla a dependência obrigatória entre as opções de camada."""
        filtrar = self.dlg.chk_filtrar_feicoes.isChecked()
        exibir_atual = self.dlg.chk_exibir_so_camada_atual.isChecked()

        self.dlg.chk_travar_camadas.blockSignals(True)

        if filtrar or exibir_atual:
            self.dlg.chk_travar_camadas.setChecked(True)
            self.dlg.chk_travar_camadas.setEnabled(False)
        else:
            self.dlg.chk_travar_camadas.setEnabled(True)

        self.dlg.chk_travar_camadas.blockSignals(False)

        # NOVO: Garante que a caixa de estilos acompanha a mudança automática do "Travar Camadas"
        self.atualizar_estado_travar_estilos()

        # --- NOVO: Controle do aviso visual de feições ---
        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            if filtrar:
                self.dlg.lbl_aviso_feicoes.show()
                self.dlg.lbl_aviso_feicoes.setText(self.tr("⚠️ Aviso: O filtro de feições cria camadas temporárias para cada feição ou grupo de feições."))
            else:
                self.dlg.lbl_aviso_feicoes.hide()
    

    def atualizar_estado_travar_estilos(self):
        """Habilita o 'Travar Estilos' apenas se o 'Travar Camadas' estiver marcado."""
        
        travado = self.dlg.chk_travar_camadas.isChecked()
        self.dlg.chk_travar_estilos.setEnabled(travado)
        
        # Se o 'Travar Camadas' for desmarcado, garantimos que o de estilos desmarque também
        if not travado:
            self.dlg.chk_travar_estilos.blockSignals(True)
            self.dlg.chk_travar_estilos.setChecked(False)
            self.dlg.chk_travar_estilos.blockSignals(False)


    def setup_ui_strings(self):
        """Centraliza todos os Tooltips e textos da interface para tradução."""

        self.dlg.chk_modo_formulario.setToolTip(self.tr("Exibe todos os atributos em um bloco único de texto (HTML)."))
        self.dlg.chk_modo_individual.setToolTip(self.tr("Cria uma linha horizontal para cada feição (evita sobreposições)."))
        self.dlg.chk_selecionar_todos.setToolTip(self.tr("Marca ou desmarca todos os campos da lista de uma vez."))
        self.dlg.combo_atlas.setToolTip(self.tr("Campo da tabela usado para agrupar as feições em cada página."))
        self.dlg.combo_presets.setToolTip(self.tr("Define o tamanho do mapa na folha (ex: 75% da página ou Quadrado)."))
        self.btn_render.setToolTip(self.tr("Gera uma prévia do layout com as configurações atuais."))
        self.btn_export.setToolTip(self.tr("Exporta os mapas diretamente como PDF, PNG ou JPG."))


    def cancelar_e_fechar(self):
        """Gerencia se o clique no Cancelar deve parar o motor ou fechar a janela."""

        self.limpar_clones_preview()

        if self.is_rendering:
            # Se estiver processando, apenas levantamos a bandeira de parada
            self.abort_processing = True
            self.iface.messageBar().pushMessage(
                self.tr("Aviso"), 
                self.tr("Cancelando processamento... A janela permanecerá aberta."), 
                level=3, 
                duration=3
            )
        else:
            # Se não houver nada rodando, o comportamento de fechar é seguro
            self.dlg.reject()

    #################################################################################
    # --- 3. GESTÃO DE ATRIBUTOS E WIDGETS ---
    #################################################################################

    def atualizar_lista_atributos(self):
        """Limpa a interface e reconstrói a lista com isolamento total."""

        self.dlg.chk_modo_formulario.blockSignals(True)
        self.dlg.chk_modo_individual.blockSignals(True)
        self.dlg.chk_modo_formulario.setChecked(False)
        self.dlg.chk_modo_individual.setChecked(False)
        self.dlg.chk_modo_formulario.setEnabled(False) 
        self.dlg.chk_modo_individual.setEnabled(False) 

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        self.dlg.tableWidget.setRowCount(0)
        self.dlg.tableWidget.setColumnCount(0)
        self.colunas_atuais = []

        self.dlg.chk_selecionar_todos.blockSignals(True)
        self.dlg.chk_selecionar_todos.setChecked(False)
        self.dlg.chk_selecionar_todos.blockSignals(False)

        self.dlg.combo_atlas.clear()
        self.dlg.combo_atlas.addItem(self.tr("--- Sem Agrupamento (Cada Feição) ---"), None)

        if not camada: 
            self.dlg.chk_modo_formulario.blockSignals(False)
            self.dlg.chk_modo_individual.blockSignals(False)
            return

        self.atualizar_lista_colunas(camada)
        self.dlg.chk_modo_formulario.blockSignals(False)
        self.dlg.chk_modo_individual.blockSignals(False)
        self.atualizar_estado_modos_exibicao()


    def atualizar_lista_colunas(self, camada):
        """Reconstrói a grade de 3 colunas com distribuição balanceada descendente."""

        container = self.dlg.scrollAreaWidgetContents
        layout_atual = container.layout()

        if layout_atual:
            while layout_atual.count():
                item = layout_atual.takeAt(0)
                if item.widget(): item.widget().deleteLater()
            if not isinstance(layout_atual, QGridLayout):
                sip.delete(layout_atual)
                layout_atual = QGridLayout(container)
                container.setLayout(layout_atual)
        else:
            layout_atual = QGridLayout(container)
            container.setLayout(layout_atual)

        layout_atual.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.colunas_atuais = [] 
        
        campos = list(camada.fields())
        N = len(campos)
        if N == 0: return

        if N <= 2:
            distribuicao = [N, 0, 0]
        elif N <= 4:
            c1 = math.ceil(N / 2)
            distribuicao = [c1, N - c1, 0]
        else:
            base = N // 3
            resto = N % 3
            distribuicao = [base + (1 if resto > 0 else 0), base + (1 if resto > 1 else 0), base]

        idx_campo = 0
        for col_index, limite_linhas in enumerate(distribuicao):
            for row_index in range(limite_linhas):
                if idx_campo < N:
                    campo = campos[idx_campo]
                    nome = campo.name()
                    cb = QCheckBox(nome)
                    self.colunas_atuais.append(cb)
                    cb.stateChanged.connect(self.atualizar_tabela_por_selecao)                    
                    cb.stateChanged.connect(self.verificar_estado_selecionar_todos)
                    cb.stateChanged.connect(self.atualizar_estado_modos_exibicao)
                    cb.stateChanged.connect(self.disparar_preview_se_autorizado)
                    layout_atual.addWidget(cb, row_index, col_index)
                    self.dlg.combo_atlas.addItem(nome, nome)
                    idx_campo += 1


    def marcar_desmarcar_todos(self, state):
        """Bulk check/uncheck attributes using the control list."""

        check = (state == Qt.CheckState.Checked)
        for cb in self.colunas_atuais:
            cb.setChecked(check)


    def verificar_estado_selecionar_todos(self):
        """Synchronize the master 'Select All' checkbox state."""

        container = self.dlg.scrollAreaWidgetContents
        cbs = container.findChildren(QCheckBox)
        total = len(cbs)
        marcados = sum(1 for cb in cbs if cb.isChecked())
        self.dlg.chk_selecionar_todos.blockSignals(True)
        self.dlg.chk_selecionar_todos.setChecked(total > 0 and total == marcados)
        self.dlg.chk_selecionar_todos.blockSignals(False)


    def atualizar_estado_modos_exibicao(self):
        """Manage accessibility for display modes using the control list."""

        tem_selecao = any(cb.isChecked() for cb in self.colunas_atuais)
        self.dlg.chk_modo_formulario.setEnabled(tem_selecao)
        self.dlg.chk_modo_individual.setEnabled(tem_selecao)
        if tem_selecao:
            if not self.dlg.chk_modo_formulario.isChecked() and not self.dlg.chk_modo_individual.isChecked():
                self.dlg.chk_modo_formulario.setChecked(True)
        else:
            self.dlg.chk_modo_formulario.setChecked(False)
            self.dlg.chk_modo_individual.setChecked(False)


    def atualizar_tabela_por_selecao(self):
        """Synchronize the sample data table with the user's attribute selection."""

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        tabela = self.dlg.tableWidget
        if not camada: return
        
        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]
        tabela.setColumnCount(len(colunas))
        tabela.setHorizontalHeaderLabels(colunas)
        
        if not colunas:
            tabela.setRowCount(0); return
            
        num_linhas = min(10, camada.featureCount())
        tabela.setRowCount(num_linhas)
        for i, feicao in enumerate(camada.getFeatures(QgsFeatureRequest().setLimit(num_linhas))):
            for j, col in enumerate(colunas):
                tabela.setItem(i, j, QTableWidgetItem(str(feicao[col])))
        tabela.resizeColumnsToContents()

    #####################################################################################
    # --- 4. LÓGICA GEOGRÁFICA E ESCALA ---
    #####################################################################################

    def configurar_escala_ao_mudar_camada(self):
        """Define o botão padrão inicial ao trocar de camada."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        
        # Agora a Escala Automática é o padrão para qualquer tipo de geometria
        self.dlg.rb_escala_auto.setChecked(True)
        
        self.atualizar_lista_atributos()
        self.validar_geometria_escala()


    def validar_geometria_escala(self):
        """Gerencia a liberação dos botões de escala."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        self.dlg.combo_escala_fixa.blockSignals(True)
        
        # Habilita a Escala Automática para todos os casos (Pontos, Linhas, Polígonos)
        self.dlg.rb_escala_auto.setEnabled(True)
        self.dlg.combo_escala_fixa.setEnabled(self.dlg.rb_escala_fixa.isChecked())

        if self.dlg.rb_escala_fixa.isChecked():
            self.dlg.combo_escala_fixa.setCurrentIndex(2) # 1:10.000 como default visual

        self.dlg.combo_escala_fixa.blockSignals(False)
        self.disparar_preview_se_autorizado()

    ####################################################################################
    # --- 5. RENDERIZAÇÃO E LAYOUT ---
    ####################################################################################

    def obter_dados_preview(self):
        """Atualiza o cache de feições apenas se a camada ou o agrupamento mudar."""

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada:
            self.preview_data_cache = None
            return None

        campo_atlas = self.dlg.combo_atlas.currentData()
        
        # CONDIÇÃO DE INVALIDAÇÃO:
        # Só buscamos as feições no QGIS se:
        # 1. O cache está vazio
        # 2. A camada selecionada mudou
        # 3. O campo de agrupamento (Atlas) mudou
        if (self.preview_data_cache is None or 
            self.ultimo_id_camada != camada.id() or 
            self.cache_val_atlas != campo_atlas):
            
            self.ultimo_id_camada = camada.id()
            self.cache_val_atlas = campo_atlas
            
            # Buscamos a primeira feição disponível (limit 1 para ser instantâneo)
            # Nota: Não usamos Subset aqui para ter a feição completa em cache
            request_amostra = QgsFeatureRequest().setLimit(1)
            sample_f = next(camada.getFeatures(request_amostra), None)
            
            if sample_f:
                if campo_atlas is None:
                    self.preview_data_cache = [sample_f]
                else:
                    # Se houver atlas, buscamos todas as feições do mesmo grupo
                    val_amostra = sample_f.attribute(campo_atlas)
                    # --- NOVA LÓGICA DE FILTRO NULL ---                    
                    if val_amostra is None or val_amostra == NULL:
                        filtro = f'"{campo_atlas}" IS NULL'
                    else:
                        filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val_amostra)}'
                    # ----------------------------------
                    request_filtrado = QgsFeatureRequest().setFilterExpression(filtro)
                    self.preview_data_cache = list(camada.getFeatures(request_filtrado))
            else:
                self.preview_data_cache = None
        
        return self.preview_data_cache
    
    
    def atualizar_preview(self):
        """Renders a sample layout preview usando cache de feições."""

        if not self.dlg or sip.isdeleted(self.dlg):
            return        

        if self.is_rendering or not self.dlg.isVisible(): 
            return
        
        self.is_rendering = True
        self.dlg.previewProgressBar.show() 
        self.dlg.previewProgressBar.setRange(0, 0) 
        QCoreApplication.processEvents()

        self.limpar_clones_preview()

        layout = None
        try:
            # 1. Recupera feições da memória (instantâneo se já carregadas)
            feicoes = self.obter_dados_preview()
            camada = self.dlg.mMapLayerComboBox.currentLayer()

            if not feicoes or not camada:
                self.dlg.lbl_preview.clear()
                return

            # 2. Prepara o Layout
            layout = QgsPrintLayout(QgsProject.instance())
            layout.initializeDefaults()
            
            # 3. Monta o design (Lógica estritamente visual)
            nome_sufixo_preview = "Preview"
            campo_atlas = self.dlg.combo_atlas.currentData()
            if campo_atlas and feicoes:
                val_atr = feicoes[0].attribute(campo_atlas)
                val_str = "Null" if val_atr is None else str(val_atr).strip()
                nome_sufixo_preview = re.sub(r'[\\/*?:"<>|]', "", val_str).strip()[:8].strip()

            self.montar_design_da_pagina(
                layout, 
                camada, 
                feicoes, 
                self.dlg.combo_presets.currentData(),
                "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem",
                is_preview=True,
                nome_sufixo=nome_sufixo_preview 
            )

            # 4. Exporta para imagem
            exporter = QgsLayoutExporter(layout)
            image = exporter.renderPageToImage(0)
            if not image.isNull():
                self.dlg.lbl_preview.setPixmap(QPixmap.fromImage(image).scaled(
                    self.dlg.lbl_preview.size(), 
                    Qt.AspectRatioMode.KeepAspectRatio, 
                    Qt.TransformationMode.SmoothTransformation
                ))

        # --- NOVO: Captura falhas de renderização da Preview ---
        except Exception as e:
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                except Exception:
                    pass
            QgsMessageLog.logMessage(f"Erro detalhado: {e}", "VectorToMap", Qgis.MessageLevel.Warning)
        # -------------------------------------------------------

        finally:
            if layout:
                layout.pageCollection().clear()
                sip.delete(layout)
            
            self.limpar_clones_preview()

            self.is_rendering = False
            self.dlg.previewProgressBar.hide()
            self.dlg.previewProgressBar.setRange(0, 100)
            QCoreApplication.processEvents()


    def disparar_preview_se_autorizado(self):
        """Start the preview timer only if auto-update is checked."""

        if hasattr(self.dlg, 'chk_preview_auto') and self.dlg.chk_preview_auto.isChecked():
            self.timer_preview.start(450)


    def processar_clique_ok(self):
        """Processes configurations and generates the multi-page layout."""
        
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        
        self.abort_processing = False
        
        try:
            container = self.dlg.scrollAreaWidgetContents
            colunas_selecionadas = [cb.text() for cb in container.findChildren(QCheckBox) if cb.isChecked()]

            paginas_dados = []
            campo_atlas = self.dlg.combo_atlas.currentData()
            
            # 1. Pegamos os nomes das colunas que o usuário quer VER no mapa
            colunas_para_carregar = list(colunas_selecionadas)
            if campo_atlas and campo_atlas not in colunas_para_carregar:
                colunas_para_carregar.append(campo_atlas)

            if campo_atlas is None:
                # CORREÇÃO: Sem Subset! Puxa a feição inteira para não quebrar a simbologia
                request = QgsFeatureRequest() 
                for f in camada.getFeatures(request): 
                    paginas_dados.append({'feicoes': [f]})
            
            else:
                idx_campo = camada.fields().indexOf(campo_atlas)
                valores_unicos = camada.uniqueValues(idx_campo)
                
                # --- NOVA LÓGICA DE ORDENAÇÃO (NULL POR ÚLTIMO) ---
                valores_validos = []
                tem_nulo = False
                
                for val in valores_unicos:
                    if val is None or val == NULL:
                        tem_nulo = True
                    else:
                        valores_validos.append(val)
                
                # Ordena os valores válidos (usando key=str para evitar erro se houver mistura de números e texto)
                valores_validos.sort(key=lambda x: str(x) if x is not None else "")
                
                # Adiciona o Nulo no final, se existir
                valores_ordenados = valores_validos
                if tem_nulo:
                    valores_ordenados.append(NULL)
                # ---------------------------------------------------

                for val in valores_ordenados:
                    if val is None or val == NULL:
                        filtro = f'"{campo_atlas}" IS NULL'
                    else:
                        filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val)}'

                    request = QgsFeatureRequest().setFilterExpression(filtro)
                    feicoes_do_grupo = list(camada.getFeatures(request))
                    if feicoes_do_grupo:
                        paginas_dados.append({'feicoes': feicoes_do_grupo})
            
            if paginas_dados: 
                self.criar_layout_multi_paginas(
                    self.dlg.combo_presets.currentData(), camada, 
                    "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem", 
                    self.dlg.combo_tamanho_pagina.currentData(), 
                    paginas_dados, colunas_selecionadas, None,
                    export_info=getattr(self, 'export_info', None) # Passa o dicionário
                )
                
        # --- NOVO: Proteção contra quebras de banco de dados/camada corrompida ---
        except Exception as e:
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                    sentry_sdk.flush(timeout=2.0)
                except Exception:
                    pass
            self.iface.messageBar().pushMessage(self.tr("Erro"), f"Falha ao ler atributos da camada: {str(e)}", level=2)
        # -------------------------------------------------------------------------
        
        finally:
            self.export_info = None # Reseta a variável para o botão OK padrão


    # ====================================================================================
    # --- ORQUESTRAÇÃO DE LAYOUTS E EXPORTAÇÃO ---
    # ====================================================================================

    def criar_layout_multi_paginas(self, preset, camada, orientacao, tamanho_pg, paginas_dados, colunas, modos, export_info=None):
        """Maestro: Gera o layout definitivo ou exporta os arquivos delegando as tarefas."""

        self.is_rendering = True
        self.abort_processing = False
        
        try:
            project = QgsProject.instance()
            manager = project.layoutManager()
            
            # 1. Inicializa Variáveis de Exportação
            is_individual = export_info.get('individual', False) if export_info else False
            base_path = export_info.get('path', '') if export_info else ''
            ext = export_info.get('ext', '') if export_info else ''
            base_sem_ext = base_path[:-len(ext)] if ext else base_path

            # 2. Pre-flight Check: Verificação de Sobrescrita Segura
            if export_info:
                pode_continuar = self._verificar_sobrescrita_arquivos(base_sem_ext, ext, paginas_dados, is_individual)
                if not pode_continuar:
                    self.is_rendering = False
                    self.abort_processing = True
                    return # Aborta o processo pacificamente

            # 3. Configuração da UI
            self.dlg.progressBar.show()
            self.dlg.progressBar.setRange(0, len(paginas_dados))
            self.dlg.progressBar.setValue(0)

            # 4. Roteamento de Trabalho: Exportação Avulsa vs Layout Acumulado
            layout_persistente = None
            
            if is_individual:
                self._exportar_paginas_individuais(project, camada, paginas_dados, preset, orientacao, base_sem_ext, ext)
            else:
                layout_persistente = self._montar_layout_persistente(project, manager, camada, paginas_dados, preset, orientacao)

            # 5. Finalização e Fechamento
            if not self.abort_processing:
                if export_info:
                    # Se pediu PDF único, exportamos o layout que acumulou as páginas
                    if not is_individual and ext == ".pdf" and layout_persistente:
                        layout_persistente.refresh()
                        exporter = QgsLayoutExporter(layout_persistente)
                        settings = QgsLayoutExporter.PdfExportSettings()
                        settings.dpi = 300
                        exporter.exportToPdf(base_path, settings)
                        
                        # Limpa da memória pois já virou arquivo
                        layout_persistente.pageCollection().clear()
                        sip.delete(layout_persistente)

                    # --- NOVO: Mensagem de Sucesso Nativa do QGIS ---
                    dir_path = os.path.dirname(base_path)
                    url_pasta = QUrl.fromLocalFile(dir_path).toString()
                    
                    texto_sucesso = self.tr("Layout exportado com sucesso para")
                    
                    # Formatação fiel à barra nativa: link com a cor verde escuro do QGIS e o caminho escrito
                    mensagem_html = f"{texto_sucesso} <a href='{url_pasta}' style='color: #005e00; text-decoration: underline;'>{dir_path}</a>"
                    
                    # level=3 (Fundo Verde de Sucesso) | duration=0 (Fica fixada até o usuário fechar)
                    self.iface.messageBar().pushMessage(self.tr("Exportar layout"), mensagem_html, level=3, duration=0)
                    # ---------------------------------------------------
                else:
                    # Se clicou em "OK" apenas para visualizar no QGIS
                    if layout_persistente:
                        layout_persistente.refresh()
                        manager.addLayout(layout_persistente)
                        self.iface.openLayoutDesigner(layout_persistente)
            else:
                self.iface.messageBar().pushMessage(self.tr("Cancelado"), self.tr("O processamento foi interrompido."), level=2)

        except Exception as e:
            erro_detalhado = traceback.format_exc()
            
            # --- ENVIO PARA O SENTRY ---
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)                    
                    # O PULO DO GATO: Força o envio imediato esperando até 2 segundos
                    sentry_sdk.flush(timeout=2.0) 
                except Exception:
                    pass
            # ---------------------------

            self.iface.messageBar().pushMessage(
                self.tr("Erro"), 
                f"{str(e)}. {self.tr('Veja o console para detalhes.')}", 
                level=2
            )
            QgsMessageLog.logMessage(erro_detalhado, "VectorToMap", Qgis.MessageLevel.Critical)

        finally:
            self.is_rendering = False
            self.dlg.progressBar.hide()
            gc.collect()


    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE EXPORTAÇÃO E LAYOUT (ESPECIALISTAS) ---
    # ------------------------------------------------------------------------------------

    def _verificar_sobrescrita_arquivos(self, base_sem_ext, ext, paginas_dados, is_individual):
        """Pre-flight: Avalia o disco antes de rodar o motor pesado."""
        arquivos_existentes = False
        
        if is_individual:
            for i, dados in enumerate(paginas_dados):
                nome_teste = self._gerar_nome_arquivo_pagina(dados, i)
                if os.path.exists(f"{base_sem_ext}_{nome_teste}{ext}"):
                    arquivos_existentes = True
                    break
        else:
            if os.path.exists(f"{base_sem_ext}{ext}"):
                arquivos_existentes = True

        if arquivos_existentes:
            resposta = QMessageBox.question(
                self.dlg,
                self.tr("Confirmar Sobrescrita"),
                self.tr("Arquivos com os mesmos nomes já existem no diretório de destino.\n\nDeseja sobrescrevê-los?"),
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No
            )
            return resposta == QMessageBox.StandardButton.Yes
        return True


    def _gerar_nome_arquivo_pagina(self, dados, index):
        """Gera um sufixo limpo de até 8 caracteres baseado no Atlas ou sequencial."""
        campo_atlas = self.dlg.combo_atlas.currentData()
        if campo_atlas and dados.get('feicoes'):
            val_atr = dados['feicoes'][0].attribute(campo_atlas)
            nome = "Null" if val_atr is None or val_atr == NULL else str(val_atr).strip()
            return re.sub(r'[\\/*?:"<>|]', "", nome).strip()[:8].strip()
        return str(index + 1)


    def _exportar_paginas_individuais(self, project, camada, paginas_dados, preset, orientacao, base_sem_ext, ext):
        """Modo Descartável: Cria, exporta e destrói um layout para cada página."""
        for i, dados in enumerate(paginas_dados):
            if self.abort_processing: break

            nome_arquivo = self._gerar_nome_arquivo_pagina(dados, i)
            
            layout_temp = QgsPrintLayout(project)
            layout_temp.initializeDefaults()
            
            self.montar_design_da_pagina(layout_temp, camada, dados['feicoes'], preset, orientacao, pagina_index=0, nome_sufixo=nome_arquivo)
            layout_temp.refresh()
            
            caminho_final = f"{base_sem_ext}_{nome_arquivo}{ext}"
            exporter = QgsLayoutExporter(layout_temp)
            
            if ext in [".png", ".jpg", ".jpeg"]:
                img_settings = QgsLayoutExporter.ImageExportSettings()
                img_settings.dpi = 300 
                exporter.exportToImage(caminho_final, img_settings)
            elif ext == ".svg":
                svg_settings = QgsLayoutExporter.SvgExportSettings()
                svg_settings.exportLabelsToPaths = False # Mantém textos editáveis
                exporter.exportToSvg(caminho_final, svg_settings)
            else:
                pdf_settings = QgsLayoutExporter.PdfExportSettings()
                pdf_settings.dpi = 300
                exporter.exportToPdf(caminho_final, pdf_settings)
                
            # Limpa memória instantaneamente
            layout_temp.pageCollection().clear()
            sip.delete(layout_temp)

            self.dlg.progressBar.setValue(i + 1)
            QCoreApplication.processEvents()


    def _montar_layout_persistente(self, project, manager, camada, paginas_dados, preset, orientacao):
        """Modo Acumulador: Adiciona todas as páginas num único layout do QGIS."""
        nome_camada = re.sub(r'[^a-zA-Z0-9_]', '_', unicodedata.normalize('NFD', camada.name()).encode('ascii', 'ignore').decode('utf-8'))
        layout_name = f"VectorToMap_{nome_camada}"
        
        # Garante nome único no QGIS
        contador = 1
        while manager.layoutByName(layout_name):
            layout_name = f"VectorToMap_{nome_camada}_{contador}"
            contador += 1
            
        layout = QgsPrintLayout(project)
        layout.initializeDefaults()
        layout.setName(layout_name)

        for i, dados in enumerate(paginas_dados):
            if self.abort_processing: break
            
            nome_sufixo = self._gerar_nome_arquivo_pagina(dados, i)

            if i > 0: 
                layout.pageCollection().addPage(QgsLayoutItemPage(layout))
                
            self.montar_design_da_pagina(layout, camada, dados['feicoes'], preset, orientacao, pagina_index=i, nome_sufixo=nome_sufixo)

            # Coleta de lixo programada para evitar estouro de memória em dezenas de páginas
            if i > 0 and i % 20 == 0:
                layout.refresh()
                gc.collect()

            self.dlg.progressBar.setValue(i + 1)
            QCoreApplication.processEvents()
            
        return layout


    # ====================================================================================
    # --- RENDERIZAÇÃO E DESIGN (ARQUITETURA REFATORADA v0.4.3) ---
    # ====================================================================================

    def montar_design_da_pagina(self, layout, camada, feicoes_da_pagina, preset, orientacao, pagina_index=0, is_preview=False, nome_sufixo=""):
        """Maestro: Orquestra a montagem completa da página chamando sub-funções menores."""

        # 1. Prepara o papel (Tamanho, Geometria e Fundo)
        geometria, apenas_mapa, cor_fundo = self._configurar_papel_e_fundo(
            layout, pagina_index, preset, orientacao, is_preview
        )

        # --- NOVO: 1.5 Desenha o Título no topo ---
        if not apenas_mapa:
            self._adicionar_titulo(layout, geometria)
        # ------------------------------------------

        # 2. Cria o item de mapa e o posiciona na página
        map_item = self._adicionar_item_mapa(
            layout, geometria, apenas_mapa, cor_fundo
        )

        # 3. Calcula o zoom geográfico exato das feições
        self._aplicar_extensao_e_escala(
            map_item, camada, feicoes_da_pagina, geometria['w_map'], geometria['h_map']
        )

        # 4. Define quais camadas (TOC e clones) aparecem no mapa
        self._gerenciar_visibilidade_camadas(
            map_item, camada, feicoes_da_pagina, is_preview, nome_sufixo, pagina_index
        )

        # 5. Adiciona os textos e tabela de atributos (se houver espaço)
        self._renderizar_textos_e_atributos(
            layout, feicoes_da_pagina, preset, orientacao, geometria, apenas_mapa
        )

        # 6. Adiciona numeração da página no rodapé
        if not apenas_mapa:
            self.adicionar_numeracao_pagina(layout, geometria['w_pg'], geometria['h_pg'], geometria['y_zero'])


    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE MONTAGEM ---
    # ------------------------------------------------------------------------------------

    def _configurar_papel_e_fundo(self, layout, pagina_index, preset, orientacao, is_preview):
        """Calcula dimensões, recortes e o comportamento de fundo (transparência/PNG)."""
        dim = self.dlg.combo_tamanho_pagina.currentData()
        if not dim: dim = (210.0, 297.0)

        w_pg, h_pg = (dim[1], dim[0]) if orientacao == "Paisagem" else (dim[0], dim[1])
        pagina = layout.pageCollection().pages()[pagina_index]
        pagina.setPageSize(QgsLayoutSize(w_pg, h_pg, QgsUnitTypes.LayoutMillimeters))

        y_zero_folha = pagina_index * (h_pg + 10)
        margin_padrao = 10.0

        # Modelos Pré-definidos
        if preset == "horizontal":
            header_h = 35.0
            h_util = h_pg - (2 * margin_padrao) - header_h
            w_map_f = w_pg - (2 * margin_padrao)
            
            if orientacao == "Paisagem":
                # Paisagem: Exatamente como era antes (intocado)
                h_map_f = h_util * 0.75
                x_map_f = margin_padrao
                y_map_f = y_zero_folha + margin_padrao + header_h
            else:
                # Retrato: Nova altura (75% da LARGURA) mantendo o centro vertical intacto
                altura_antiga = h_util * 0.75
                y_antigo = y_zero_folha + margin_padrao + header_h
                centro_y = y_antigo + (altura_antiga / 2.0)
                
                h_map_f = w_map_f * 0.75
                x_map_f = margin_padrao
                y_map_f = centro_y - (h_map_f / 2.0)
                
        elif preset == "vertical":
            if orientacao == "Retrato":
                w_map_f = w_pg - (2 * margin_padrao)
                x_map_f = margin_padrao
                y_map_f = y_zero_folha + margin_padrao + 30.0
                h_map_f = w_pg + 10.0 
            else:
                largura_util = w_pg - (2 * margin_padrao)
                h_map_f = h_pg - (2 * margin_padrao)
                w_map_f = largura_util * 0.50
                x_map_f = w_pg - margin_padrao - w_map_f
                y_map_f = y_zero_folha + margin_padrao
                
        else: # "quadrado"
            if orientacao == "Retrato":
                w_map_f, h_map_f = w_pg - (2 * margin_padrao), w_pg - (2 * margin_padrao)
                x_map_f, y_map_f = margin_padrao, y_zero_folha + 40.0
            else:
                h_map_f = h_pg - (2 * margin_padrao)
                w_map_f = h_map_f
                x_map_f, y_map_f = w_pg - margin_padrao - w_map_f, y_zero_folha + margin_padrao

        # Checagem de Fundo e Exportação Avulsa
        apenas_mapa = hasattr(self.dlg, 'chk_apenas_mapa') and self.dlg.chk_apenas_mapa.isChecked()
        cor_fundo = QColor(255, 255, 255, 255)
        if hasattr(self.dlg, 'btn_cor_fundo'):
            cor_fundo = self.dlg.btn_cor_fundo.color()

        if apenas_mapa:
            w_pg, h_pg = w_map_f, h_map_f
            x_map_f = 0.0
            y_zero_folha = pagina_index * (h_pg + 10)
            y_map_f = y_zero_folha
            pagina.setPageSize(QgsLayoutSize(w_pg, h_pg, QgsUnitTypes.LayoutMillimeters))

        is_png_export = False
        if not is_preview:
            info = getattr(self, 'export_info', {}) or {}
            if info.get('ext', '').lower() == '.png': is_png_export = True

        if apenas_mapa and is_png_export:
            pagina.setBackgroundEnabled(True)
            if pagina.pageStyleSymbol(): pagina.pageStyleSymbol().setColor(QColor(255, 255, 255, 0))
        else:
            pagina.setBackgroundEnabled(True)
            if pagina.pageStyleSymbol(): pagina.pageStyleSymbol().setColor(QColor(255, 255, 255, 255))

        geometria = {
            'w_pg': w_pg, 'h_pg': h_pg, 'w_map': w_map_f, 'h_map': h_map_f,
            'x_map': x_map_f, 'y_map': y_map_f, 'y_zero': y_zero_folha, 'margin': margin_padrao
        }
        return geometria, apenas_mapa, cor_fundo
    

    def escolher_fonte_titulo(self):
        """Abre a janela nativa do Windows/Mac para escolher fonte, tamanho e estilo."""
        fonte, ok = QFontDialog.getFont(self.fonte_titulo, self.dlg, self.tr("Escolher Fonte do Título"))
        if ok:
            self.fonte_titulo = fonte
            self.disparar_preview_se_autorizado()


    def _adicionar_titulo(self, layout, geo):
        """Renderiza o título a 1cm das margens superiores e laterais."""
        if not hasattr(self.dlg, 'lineEdit_titulo'): return
        
        texto = self.dlg.lineEdit_titulo.text().strip()
        if not texto: return

        lbl_titulo = QgsLayoutItemLabel(layout)
        lbl_titulo.setText(texto)
        lbl_titulo.setFont(self.fonte_titulo)
        
        # --- NOVO: Lê a ComboBox para decidir o alinhamento ---
        alinhamento = Qt.AlignmentFlag.AlignHCenter # Backup caso não ache a ComboBox
        if hasattr(self.dlg, 'combo_alinhamento_titulo'):
            alinhamento = self.dlg.combo_alinhamento_titulo.currentData()
            
        lbl_titulo.setHAlign(alinhamento)
        # ------------------------------------------------------
        
        lbl_titulo.setVAlign(Qt.AlignmentFlag.AlignTop)
        
        x = geo['margin'] 
        y = geo['y_zero'] + geo['margin'] 
        largura = geo['w_pg'] - (2 * geo['margin'])
        altura_livre = 60.0 
        
        lbl_titulo.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        lbl_titulo.attemptResize(QgsLayoutSize(largura, altura_livre, QgsUnitTypes.LayoutMillimeters))
        
        # --- NOVO: Garante que o título flutue sempre por cima do mapa ---
        lbl_titulo.setZValue(50) 
        # -----------------------------------------------------------------
        
        layout.addLayoutItem(lbl_titulo)


    def _adicionar_item_mapa(self, layout, geo, apenas_mapa, cor_fundo):
        """Cria e injeta o quadro do mapa no layout."""
        map_item = QgsLayoutItemMap(layout)
        map_item.setFrameEnabled(not apenas_mapa) 
        map_item.setBackgroundEnabled(True)
        map_item.setBackgroundColor(cor_fundo)
        layout.addLayoutItem(map_item)
        
        map_item.attemptResize(QgsLayoutSize(geo['w_map'], geo['h_map'], QgsUnitTypes.LayoutMillimeters))
        map_item.attemptMove(QgsLayoutPoint(geo['x_map'], geo['y_map'], QgsUnitTypes.LayoutMillimeters))
        map_item.setZValue(0)
        return map_item


    def _aplicar_extensao_e_escala(self, map_item, camada, feicoes_da_pagina, w_map, h_map):
        """Gerencia o BoundingBox, CRS e regras de zoom máximo do mapa."""
        if not feicoes_da_pagina: return

        ext = QgsRectangle()
        ext.setMinimal()
        for f in feicoes_da_pagina: ext.combineExtentWith(f.geometry().boundingBox())
        
        project_crs = QgsProject.instance().crs()
        trans = QgsCoordinateTransform(camada.crs(), project_crs, QgsProject.instance().transformContext())
        ext_proj = trans.transformBoundingBox(ext)
        
        is_coordenada_unica = (ext_proj.width() == 0 and ext_proj.height() == 0)
        if is_coordenada_unica: 
            respiro = 0.0001 if project_crs.isGeographic() else 1.0
            ext_proj.grow(respiro)
        
        map_item.setExtent(ext_proj)
        
        if self.dlg.rb_escala_fixa.isChecked():
            escala_val = self.dlg.combo_escala_fixa.currentData()
            map_item.setScale(float(escala_val) if escala_val else 10000.0)
        else:
            if is_coordenada_unica:
                escala_final = 10000.0
            else:
                unit_to_mm = QgsUnitTypes.fromUnitToUnitFactor(project_crs.mapUnits(), QgsUnitTypes.DistanceMillimeters)
                scale_w = (ext_proj.width() * unit_to_mm) / w_map
                scale_h = (ext_proj.height() * unit_to_mm) / h_map
                escala_calculada = max(scale_w, scale_h) * 1.25
                escala_final = 10000.0 if escala_calculada < 500.0 else escala_calculada
        
            map_item.setScale(escala_final)

        map_item.refresh()
        map_item.attemptResize(QgsLayoutSize(w_map, h_map, QgsUnitTypes.LayoutMillimeters))
        # Ajusta posição sem alterar o tamanho consolidado
        map_item.attemptMove(QgsLayoutPoint(map_item.pos().x(), map_item.pos().y(), QgsUnitTypes.LayoutMillimeters))


    def atualizar_mapa_para_canvas(self, map_item):
        """Força o layout a refletir a tela principal do QGIS sem travar as camadas."""
        # 1. Desliga qualquer trava de camadas ou estilos
        map_item.setKeepLayerSet(False)
        map_item.setKeepLayerStyles(False)
        
        # 2. Invalida o cache atual para forçar o QGIS a redesenhar com base no canvas
        map_item.invalidateCache()
        
        # 3. Atualiza o item no layout
        map_item.refresh()


    def _gerenciar_visibilidade_camadas(self, map_item, camada, feicoes_da_pagina, is_preview, nome_sufixo, pagina_index):
        """Avalia as regras do TOC, cria camadas clone (se necessário) e trava os estilos."""
        camada_alvo = camada

        # 1. Criação do Clone para Filtro
        if self.dlg.chk_filtrar_feicoes.isChecked():
            nome_camada_limpo = re.sub(r'[^a-zA-Z0-9_]', '_', unicodedata.normalize('NFD', camada.name()).encode('ascii', 'ignore').decode('utf-8'))
            nome_temp = f"{nome_camada_limpo}_{nome_sufixo}" if nome_sufixo else f"{nome_camada_limpo}_{pagina_index + 1}"
            camada_alvo = self.criar_camada_temporaria(camada, feicoes_da_pagina, nome_temp, is_preview)
            if is_preview: self.clones_preview.append(camada_alvo.id())

        # =====================================================================
        # NOVA LÓGICA DE BLINDAGEM: Trava Saudável vs Mapa Livre
        # =====================================================================
        
        is_isolado = self.dlg.chk_exibir_so_camada_atual.isChecked()
        is_filtrado = self.dlg.chk_filtrar_feicoes.isChecked()
        is_travado_manualmente = self.dlg.chk_travar_camadas.isChecked()

        # CENÁRIO A: O usuário ativou qualquer opção que exige alterar/congelar camadas
        if is_isolado or is_filtrado or is_travado_manualmente:
            
            camadas_finais_para_layout = []
            
            if is_isolado:
                camadas_finais_para_layout = [camada_alvo]
            else:
                root = QgsProject.instance().layerTreeRoot()
                for layer in root.layerOrder():
                    is_visible_in_toc = root.findLayer(layer.id()).isVisible()
                    if layer.id() == camada.id():
                        if is_visible_in_toc or is_filtrado:
                            if camada_alvo not in camadas_finais_para_layout: 
                                camadas_finais_para_layout.append(camada_alvo)
                    else:
                        if is_visible_in_toc and layer.id() not in self.clones_preview and layer.id() != camada_alvo.id():
                            camadas_finais_para_layout.append(layer)
            
            # Injeta a lista e OBRIGATORIAMENTE aplica a trava para não bugar o QGIS
            map_item.setLayers(camadas_finais_para_layout)
            map_item.setKeepLayerSet(True)
            
            # Trava estilos se solicitado
            if hasattr(self.dlg, 'chk_travar_estilos') and self.dlg.chk_travar_estilos.isChecked():
                map_item.setKeepLayerStyles(True)

        # CENÁRIO B: Exportação normal sem restrições. O mapa nasce 100% livre.
        else:
            self.atualizar_mapa_para_canvas(map_item)

        # =====================================================================

        map_item.refresh()

        # Oculta da Legend principal se for exportação final com clone
        if self.dlg.chk_filtrar_feicoes.isChecked() and not is_preview:
            no_da_camada = QgsProject.instance().layerTreeRoot().findLayer(camada_alvo.id())
            if no_da_camada: no_da_camada.setItemVisibilityChecked(False)


    def _renderizar_textos_e_atributos(self, layout, feicoes_da_pagina, preset, orientacao, geo, apenas_mapa):
        """Maestro: Organiza as dimensões e delega a renderização para os modos selecionados."""
        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]

        # Regra de escape rápido: se não tem o que renderizar, saímos imediatamente
        if not (colunas and feicoes_da_pagina and hasattr(self.dlg, 'chk_exibir_atributos') and self.dlg.chk_exibir_atributos.isChecked() and not apenas_mapa):
            return

        # 1. Isola a matemática: Descobre as coordenadas exatas das caixas de texto
        geom = self._calcular_geometria_textos(preset, orientacao, geo)

        # 2. Roteia para o modo de exibição escolhido
        if self.dlg.chk_modo_formulario.isChecked():
            self._renderizar_modo_formulario(layout, feicoes_da_pagina, colunas, geom)

        if self.dlg.chk_modo_individual.isChecked():
            self._renderizar_modo_individual(layout, feicoes_da_pagina, colunas, geom, preset, orientacao)


    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE RENDERIZAÇÃO DE TEXTOS (ESPECIALISTAS) ---
    # ------------------------------------------------------------------------------------

    def _calcular_geometria_textos(self, preset, orientacao, geo):
        """Calcula a área disponível (X, Y, Largura, Altura) baseando-se no layout."""
        limite_fundo = geo['y_zero'] + geo['h_pg'] - geo['margin']
        geom = {'limite_fundo': limite_fundo}

        # --- NOVA LÓGICA UNIFICADA ---
        # Se for Retrato (qualquer mapa) OU se for Paisagem no "Mapa Horizontal",
        # os textos ficam embaixo do mapa, divididos em 1/3 (esquerda) e 2/3 (direita)
        if orientacao == "Retrato" or preset == "horizontal":
            largura_util = geo['w_pg'] - (2 * geo['margin'])
            terco_x = geo['margin'] + (largura_util * 0.33) 
            
            geom['x_form'] = geo['margin']
            geom['y_form'] = geo['y_map'] + geo['h_map'] + 3.0
            geom['w_form'] = (largura_util * 0.33) - 2.0
            geom['h_form'] = limite_fundo - geom['y_form']
            
            geom['x_ind_start'] = terco_x + 2.0
            geom['y_ind_min'] = geo['y_map'] + geo['h_map'] + 2.0
            
        # --- LÓGICA ESPECÍFICA PARA PAISAGEM (Vertical e Quadrado) ---
        elif preset == "vertical":
            # Textos ficam na lateral esquerda, centralizados
            geom['x_form'] = geo['margin']
            h_util = geo['h_map']
            h_top = h_util * 0.34 
            geom['y_form'] = geo['y_map'] + h_top 
            geom['h_form'] = h_util * 0.33       
            geom['w_form'] = (geo['x_map'] - geo['margin']) - 5.0 
            geom['x_ind_start'] = geom['x_form']
            geom['y_ind_min'] = geom['y_form'] + geom['h_form'] 
            
        else: # "quadrado"
            # Textos ficam na lateral esquerda, usando a metade inferior
            geom['x_form'] = geo['margin']
            geom['y_form'] = geo['y_map'] + (geo['h_map'] / 2)
            geom['w_form'] = geo['x_map'] - (2 * geo['margin'])

            geom['x_ind_start'] = geom['x_form']
            if self.dlg.chk_modo_individual.isChecked():
                geom['h_form'] = (limite_fundo - geom['y_form']) * 0.5
                geom['y_ind_min'] = geom['y_form'] + geom['h_form'] + 2.0
            else:
                geom['h_form'] = limite_fundo - geom['y_form']
                geom['y_ind_min'] = geom['y_form']

        return geom


    def _inserir_label_no_layout(self, layout, texto, x, y, w=None, h=None, is_html=False, auto_resize=False):
        """Fábrica de rótulos: Instancia, configura e posiciona um texto sem repetir código da API do QGIS."""
        lbl = QgsLayoutItemLabel(layout)
        
        if is_html:
            mode = QgsLayoutItemLabel.ContentMode.ModeHtml if hasattr(QgsLayoutItemLabel, 'ContentMode') else (QgsLayoutItemLabel.ModeHtml if hasattr(QgsLayoutItemLabel, 'ModeHtml') else 1)
            lbl.setMode(mode)
        else:
            lbl.setFrameEnabled(True)

        lbl.setText(texto)
        
        # Otimiza o tamanho automático com folga para o Modo Individual
        if auto_resize:
            lbl.adjustSizeToText()
            w = lbl.rect().width() + 2.0
            h = lbl.rect().height()
            
        if w is not None and h is not None:
            lbl.attemptResize(QgsLayoutSize(w, h, QgsUnitTypes.LayoutMillimeters))
            
        lbl.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        layout.addLayoutItem(lbl)
        
        # Retorna a largura útil (garantindo que seja um float (0.0) para não dar erro na soma)
        return float(w) if w is not None else 0.0


    def _renderizar_modo_formulario(self, layout, feicoes_da_pagina, colunas, geom):
        """Constrói o bloco de texto único em HTML."""
        txt_html = ""
        for f in feicoes_da_pagina:                    
            for col in colunas:
                try: 
                    txt_html += f"<b>{col}:</b> {str(f.attribute(col) or '').strip()}<br>"
                except: continue
        
        self._inserir_label_no_layout(
            layout, txt_html, 
            geom['x_form'], geom['y_form'], 
            geom['w_form'], geom['h_form'], 
            is_html=True
        )


    def _renderizar_modo_individual(self, layout, feicoes_da_pagina, colunas, geom, preset, orientacao):
        """Constrói as caixas de texto individuais em grade ou em formato de lista."""

        campo_atlas = self.dlg.combo_atlas.currentData() 
        altura_linha = 5.5
        xi, yi = geom['x_ind_start'], geom['y_ind_min']

        if campo_atlas is None:
            # Sem Atlas: Distribui as colunas de UMA feição numa "Grade" (Grid)
            f = feicoes_da_pagina[0]
            
            # --- NOVA REGRA DE COLUNAS ---
            if orientacao == "Retrato":
                limite_colunas = 3 # Todos os mapas Retrato usam 3 colunas
            else:
                # No modo Paisagem:
                if preset == "horizontal":
                    limite_colunas = 5 # Espaço largo (embaixo do mapa)
                else:
                    limite_colunas = 3 # Espaço curto (lateral esquerda)
            # -----------------------------

            for idx, col in enumerate(colunas):
                if idx > 0 and idx % limite_colunas == 0:
                    yi += altura_linha
                    xi = geom['x_ind_start']
                try:
                    val = f.attribute(col)
                    texto = f"{col}: {str(val).strip() if val is not None else ''}"
                    largura_ocupada = self._inserir_label_no_layout(layout, texto, xi, yi, auto_resize=True)
                    xi += largura_ocupada + 2.0 
                except: continue
        else:
            # Com Atlas: Cria uma linha em formato de "Lista" para CADA feição do grupo
            altura_total = len(feicoes_da_pagina) * altura_linha
            yi = (geom['limite_fundo'] - altura_total) if (geom['y_ind_min'] + altura_total) > geom['limite_fundo'] else geom['y_ind_min']

            for f in feicoes_da_pagina:
                xi = geom['x_ind_start']
                for col in colunas:
                    try:
                        val = f.attribute(col)
                        texto = f"{col}: {str(val).strip() if val is not None else ''}"
                        largura_ocupada = self._inserir_label_no_layout(layout, texto, xi, yi, auto_resize=True)
                        xi += largura_ocupada + 2.0
                    except: continue
                yi += altura_linha


    def adicionar_numeracao_pagina(self, layout, w_pg, h_pg, y_zero_folha):
        """Adiciona o número da página no canto inferior direito."""
        
        if not self.dlg.chk_numeracao.isChecked(): return
        label_page = QgsLayoutItemLabel(layout)
        label_page.setText("[% @layout_page %]") 
        label_page.adjustSizeToText() 
        label_page.setReferencePoint(QgsLayoutItem.LowerRight)
        x, y = w_pg - 5, y_zero_folha + h_pg - 5
        label_page.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        label_page.setZValue(100)
        layout.addLayoutItem(label_page)