import sys
import os
import csv
import subprocess
import time
import base64

from openpyxl import load_workbook

from PySide6.QtCore import Qt, QThread, Signal, QRectF, QTimer
from PySide6.QtGui import QPixmap, QColor, QBrush, QPainter, QPen

try:
    from PySide6.QtGui import QAction  # PySide6 >= 6.2
except ImportError:
    from PySide6.QtWidgets import QAction  # older builds

from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QFrame,
    QMessageBox,
    QTextEdit,
    QProgressBar,
    QComboBox,
    QGridLayout,
    QFileDialog,
    QLineEdit,
    QTableWidget,
    QTableWidgetItem,
    QDialog,
    QHeaderView,
    QAbstractItemView,
    QScrollArea,
    QToolButton,
    QMenu,
    QSizePolicy,
    QSpacerItem,
    QStyledItemDelegate,
    QCompleter,
    QGroupBox,
    QFormLayout,
)



class PipelineWorker(QThread):
    """Executa run_project.py em thread separada — UI permanece responsiva."""

    finished   = Signal(float, str, str, int)   # elapsed, stdout, stderr, returncode
    log_line   = Signal(str)                     # linha de log em tempo real (Popen)

    def __init__(self, executable, run_file, input_file, base_dir):
        super().__init__()
        self._executable  = executable
        self._run_file    = run_file
        self._input_file  = input_file
        self._base_dir    = base_dir

    def run(self):
        import subprocess, time
        start = time.time()
        stdout_lines = []
        stderr_buf   = ""

        try:
            import sys as _sys, os as _os
            _frozen = getattr(_sys, "frozen", False)

            if _frozen:
                import importlib, io as _io

                # ── Garantir sys.path correto ANTES de qualquer import ────────
                _exe_dir = _os.path.dirname(_sys.executable)
                _internal = _os.path.join(_exe_dir, "_internal")
                _meipass  = getattr(_sys, "_MEIPASS", None)
                for _d in filter(None, [_exe_dir, _internal, _meipass]):
                    if _os.path.isdir(str(_d)) and str(_d) not in _sys.path:
                        _sys.path.insert(0, str(_d))

                _old_argv   = _sys.argv[:]
                _old_dir    = _os.getcwd()
                _old_stdout = _sys.stdout
                _old_stderr = _sys.stderr

                # input_file pode ser None se não selecionado — filtrar do argv
                _argv_extra = [self._input_file] if self._input_file else []
                _sys.argv = [self._run_file] + _argv_extra + ["--skip-tests", "--skip-bank-check"]
                _os.chdir(self._base_dir)

                # Patch stdout/stderr — streaming em tempo real via log_line.emit
                class _SafeWriter:
                    def __init__(self, emit_fn):
                        self._buf = _io.StringIO()
                        self._emit = emit_fn
                        self._pending = ""
                    def write(self, s):
                        try:
                            self._buf.write(s)
                            combined = self._pending + s
                            parts = combined.split('\n')
                            self._pending = parts[-1]
                            for line in parts[:-1]:
                                if line.strip():
                                    self._emit(line.rstrip())
                        except Exception:
                            pass
                        return len(s) if s else 0
                    def flush(self):
                        try:
                            if self._pending.strip():
                                self._emit(self._pending.rstrip())
                                self._pending = ""
                        except Exception:
                            pass
                    def getvalue(self): return self._buf.getvalue()
                    def fileno(self): raise _io.UnsupportedOperation("fileno")
                    def isatty(self): return False

                _buf = _SafeWriter(self.log_line.emit)
                _sys.stdout = _buf
                _sys.stderr = _buf

                # Desabilitar tqdm no modo frozen
                _orig_tqdm = None
                try:
                    import tqdm as _tqdm
                    _orig_tqdm = _tqdm.tqdm
                    class _NoTqdm(_tqdm.tqdm):
                        def __init__(self, *a, **kw):
                            kw["disable"] = True
                            super().__init__(*a, **kw)
                    _tqdm.tqdm = _NoTqdm
                except Exception:
                    pass

                _rc = 0
                try:
                    import run_project as _rp
                    importlib.reload(_rp)
                    try:
                        _rp.main()
                        _rc = 0
                    except SystemExit as _se:
                        _rc = _se.code if isinstance(_se.code, int) else 0
                except Exception as _ex:
                    import traceback as _tb
                    _rc = 1
                    _buf.write(f"PIPELINE ERROR: {_ex}\n{_tb.format_exc()}")
                finally:
                    _sys.stdout = _old_stdout
                    _sys.stderr = _old_stderr
                    _sys.argv   = _old_argv
                    _os.chdir(_old_dir)
                    try:
                        if _orig_tqdm: _tqdm.tqdm = _orig_tqdm
                    except Exception: pass

                # Flush qualquer linha parcial pendente, depois coletar tudo
                _buf.flush()
                for _ln in _buf.getvalue().splitlines():
                    if _ln.strip():
                        stdout_lines.append(_ln)
                elapsed = time.time() - start
                self.finished.emit(elapsed, "\n".join(stdout_lines), "", _rc)
                return

            # Modo dev: subprocess normal
            # Filtra None do input_file — run_project.py aceita nargs="?" (opcional)
            _cmd = [self._executable, self._run_file]
            if self._input_file:
                _cmd.append(self._input_file)
            _cmd += ["--skip-tests", "--skip-bank-check"]
            proc = subprocess.Popen(
                _cmd,
                cwd=self._base_dir,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                encoding="utf-8",
                errors="replace",
                creationflags=subprocess.CREATE_NO_WINDOW if _sys.platform == "win32" else 0,
            )

            # CORRECAO RISK-FE-001 (AUD-003):
            # A versao anterior lia stdout em loop e so lia stderr DEPOIS de
            # stdout fechar. Se o subprocess escrevesse >64KB em stderr, o buffer
            # do pipe ficava cheio, o processo bloqueava esperando leitura, stdout
            # nunca fechava e a UI travava permanentemente (deadlock).
            # Correcao: thread dedicada le stderr em paralelo com o loop de stdout.
            import threading as _threading
            _stderr_lines = []

            def _read_stderr(stream, buf):
                try:
                    for line in stream:
                        s = line.rstrip()
                        if s:
                            buf.append(s)
                except Exception:
                    pass

            _stderr_thread = _threading.Thread(
                target=_read_stderr,
                args=(proc.stderr, _stderr_lines),
                daemon=True,
            )
            _stderr_thread.start()

            # Lê stdout linha a linha → log em tempo real
            for line in proc.stdout:
                stripped = line.rstrip()
                if stripped:
                    stdout_lines.append(stripped)
                    self.log_line.emit(stripped)

            _stderr_thread.join(timeout=30)   # aguarda stderr com timeout
            proc.wait()
            stderr_buf = "\n".join(_stderr_lines)
            elapsed    = time.time() - start

            self.finished.emit(
                elapsed,
                "\n".join(stdout_lines),
                stderr_buf,
                proc.returncode,
            )

        except Exception as exc:
            elapsed = time.time() - start
            self.finished.emit(elapsed, "", str(exc), -1)


def _ui_base_dir():
    """Diretório engine/ — correto em dev e em .exe PyInstaller."""
    import sys as _sys
    if getattr(_sys, "frozen", False):
        return os.path.dirname(_sys.executable)
    # Em dev: front_end/engcut_ui.py -> sobe um nivel -> engine/
    return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))


def _load_engine_module(module_name):
    """Import a module from the engine root directory."""
    import importlib, sys as _s
    eng = _ui_base_dir()
    if eng not in _s.path:
        _s.path.insert(0, eng)
    return importlib.import_module(module_name)


def _license_gate(parent_widget=None):
    """Bloqueia a operação se a licença não estiver válida (fail-closed).

    Qualquer falha ao carregar ou validar o módulo de licença é tratada como
    bloqueio — ausência de evidência de licença válida NÃO é evidência de
    licença válida. Se bloqueado, exibe a tela de ativação; se o usuário não
    ativar, a aplicação é encerrada (ActivationDialog.reject() -> quit()).

    Retorna True somente se uma licença válida foi confirmada.
    """
    try:
        _v = _load_engine_module("license.validator")
        result = _v.validate()
        ok = bool(getattr(result, "ok", False))
    except Exception:
        ok = False
        result = None
    if ok:
        return True
    try:
        from front_end.activation_dialog import ActivationDialog
    except ImportError:
        from activation_dialog import ActivationDialog
    dlg = ActivationDialog(parent_widget, result=result)
    dlg.exec()
    try:
        _v2 = _load_engine_module("license.validator")
        return bool(getattr(_v2.validate(), "ok", False))
    except Exception:
        return False


class CyanHeaderView(QHeaderView):
    """Header that paints specific columns in cyan (#00B0F0)."""
    CYAN_COLS = set(range(5, 10))  # Bar Stock 1-5

    def __init__(self, orientation, parent=None):
        super().__init__(orientation, parent)
        self.setSectionsClickable(True)

    def paintSection(self, painter, rect, logical_index):
        painter.save()
        if logical_index in self.CYAN_COLS:
            painter.fillRect(rect, QColor("#00B0F0"))
            painter.setPen(QColor("#ffffff"))
            painter.setFont(self.font())
            text = self.model().headerData(logical_index, self.orientation(), Qt.DisplayRole) or ""
            painter.drawText(rect, Qt.AlignCenter | Qt.TextWordWrap, str(text))
            # border
            painter.setPen(QPen(QColor("#848C6E"), 1))
            painter.drawRect(rect.adjusted(0, 0, -1, -1))
        else:
            super().paintSection(painter, rect, logical_index)
        painter.restore()


class _PieChartWidget(QWidget):
    """Donut pie chart para COST BREAKDOWN — pintado via QPainter."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumHeight(100)
        self._slices = [("#32ff7e", 0.92), ("#4d8dff", 0.04), ("#ff7a2f", 0.04)]
        self._center_text = "--%"
        self._center_color = "#32ff7e"

    def update_data(self, yield_pct: float, stock_pct: float, scrap_pct: float):
        total = yield_pct + stock_pct + scrap_pct
        if total > 0:
            yield_pct = yield_pct / total
            stock_pct = stock_pct / total
            scrap_pct = scrap_pct / total
        else:
            yield_pct, stock_pct, scrap_pct = 0.92, 0.04, 0.04
        self._slices = [
            ("#32ff7e", yield_pct),
            ("#4d8dff", stock_pct),
            ("#ff7a2f", scrap_pct),
        ]
        self._center_text = f"{yield_pct * 100:.1f}%"
        self._center_color = "#32ff7e" if yield_pct >= 0.80 else "#ffbf3f"
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        w, h = self.width(), self.height()
        # Legenda é layout externo — usar altura total do widget para o círculo
        r = min(w // 2 - 2, h // 2 - 2)
        cx = w // 2
        cy = h // 2
        rect = QRectF(cx - r, cy - r, 2 * r, 2 * r)
        # Fatias no sentido horário a partir das 12h (span negativo = CW no Qt)
        start = 90 * 16
        full_circle = 360 * 16
        active = [(c, p) for c, p in self._slices if p > 0.0001]
        used = 0
        for i, (color, pct) in enumerate(active):
            if i == len(active) - 1:
                span = -(full_circle - used)   # último slice fecha exatamente 360°
            else:
                span = -round(pct * full_circle)
                used += -span
            painter.setBrush(QBrush(QColor(color)))
            painter.setPen(QPen(QColor("#0d141c"), 1))
            painter.drawPie(rect, start, span)
            start += span  # avança no sentido horário
        # Buraco central (donut) — 42% → anel mais espesso e visível
        inner_r = int(r * 0.42)
        inner_rect = QRectF(cx - inner_r, cy - inner_r, 2 * inner_r, 2 * inner_r)
        painter.setBrush(QBrush(QColor("#0d141c")))
        painter.setPen(Qt.NoPen)
        painter.drawEllipse(inner_rect)
        # Texto central
        painter.setPen(QColor(self._center_color))
        font = painter.font()
        font.setPointSize(9)
        font.setBold(True)
        painter.setFont(font)
        painter.drawText(inner_rect, Qt.AlignCenter, self._center_text)


class DescriptionComboDelegate(QStyledItemDelegate):
    """QComboBox com autocompletar na coluna Descrição do manual_table.
    Ao confirmar seleção, dispara on_committed(row, text) para auto-preencher
    as colunas dependentes (Cut Group, Product Code, Bar Stocks, etc.)."""

    def __init__(self, items=None, on_committed=None, parent=None):
        super().__init__(parent)
        self._items = list(items or [])
        self._on_committed = on_committed

    def update_items(self, items):
        self._items = list(items)

    def createEditor(self, parent, option, index):
        combo = QComboBox(parent)
        combo.setEditable(True)
        combo.addItem("")
        combo.addItems(self._items)
        comp = QCompleter(self._items, combo)
        comp.setCaseSensitivity(Qt.CaseInsensitive)
        comp.setFilterMode(Qt.MatchContains)
        combo.setCompleter(comp)
        combo.setStyleSheet(
            "QComboBox{background:#fff;color:#111;font-size:12px;}"
            "QComboBox QAbstractItemView{background:#fff;color:#111;}"
        )
        return combo

    def setEditorData(self, editor, index):
        val = index.data(Qt.DisplayRole) or ""
        i = editor.findText(val)
        editor.setCurrentIndex(i if i >= 0 else 0)
        if i < 0:
            editor.setCurrentText(val)

    def setModelData(self, editor, model, index):
        text = editor.currentText().strip()
        model.setData(index, text, Qt.EditRole)
        if self._on_committed and text:
            self._on_committed(index.row(), text)


class EngCutUI(QWidget):

    BRAND_LOGO_BASE64 = """ """

    def __init__(self):
        super().__init__()

        self.selected_input_file = ""
        self._current_template_path = None   # caminho do template original (não a cópia INPUT_sistema)
        self._criterios_desc_set = set()     # descrições do CRITERIOS — gate de validação pré-RUN
        self.input_is_valid = False

        self.setWindowTitle("ENGCUT Engine  v2025-05-30")
        _screen = QApplication.primaryScreen().availableGeometry()
        if _screen.width() >= 1700 and _screen.height() >= 980:
            self.resize(1620, 960)
        else:
            _w = max(1100, min(1620, _screen.width() - 30))
            _h = max(700,  min(960,  _screen.height() - 60))
            self.resize(_w, _h)
            self.move(
                _screen.x() + (_screen.width()  - _w) // 2,
                _screen.y() + (_screen.height() - _h) // 2,
            )

        self.setStyleSheet("""
            QWidget {
                background-color: #0b1118;
                color: #f2f6fb;
                font-family: Segoe UI;
                font-size: 13px;
            }

            QLabel {
                background: transparent;
            }

            QPushButton {
                background-color: #1b2530;
                border: 1px solid #31475c;
                color: #f2f6fb;
                padding: 4px 8px;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                min-height: 52px;
                max-height: 52px;
            }

            QPushButton:hover {
                background-color: #25384a;
                border: 1px solid #3da2ff;
            }

            QPushButton:disabled {
                background-color: #111820;
                color: #666666;
                border: 1px solid #222b34;
            }

            QTextEdit {
                background-color: #080d13;
                border: 1px solid #20384d;
                border-radius: 8px;
                font-family: Consolas;
                font-size: 12px;
                color: #d8e8f6;
                padding: 8px;
            }

            QProgressBar {
                border: 1px solid #26384a;
                border-radius: 5px;
                text-align: center;
                height: 18px;
                background-color: #0f1720;
            }

            QProgressBar::chunk {
                background-color: #1e90ff;
                border-radius: 5px;
            }

            QComboBox {
                background-color: #111922;
                border: 1px solid #2c4a63;
                color: #f2f6fb;
                padding: 8px;
                border-radius: 6px;
                min-width: 220px;
            }

            QComboBox:hover {
                border: 1px solid #3da2ff;
            }

            QFrame {
                background-color: #101820;
                border: none;
                border-radius: 10px;
            }
        """)

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(22, 10, 22, 10)
        main_layout.setSpacing(1)

        # =====================================================
        # HEADER
        # =====================================================

        header_frame = QFrame()
        header_frame.setStyleSheet("""
            QFrame {
                background-color: #0c141d;
                border: none;
                border-radius: 10px;
            }
        """)

        # Outer layout do frame: VBox com 2 linhas (logo+botões / email)
        header_outer = QVBoxLayout()
        header_outer.setContentsMargins(20, 20, 10, 12)
        header_outer.setSpacing(0)

        # Linha superior: logo | stretch | botões | status
        header_layout = QHBoxLayout()
        header_layout.setContentsMargins(0, 0, 0, 0)
        header_layout.setSpacing(0)

        brand_label = QLabel()
        brand_pixmap = QPixmap()
        brand_pixmap.loadFromData(
            base64.b64decode(self.BRAND_LOGO_BASE64)
        )
        brand_label.setPixmap(
            brand_pixmap.scaled(
                700,
                84,
                Qt.KeepAspectRatio,
                Qt.SmoothTransformation,
            )
        )
        brand_label.setMinimumWidth(200)
        brand_label.setFixedHeight(84)
        brand_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        # Email fica dentro do bloco do logo (abaixo da imagem, não linha separada do VBox)
        _email_lbl = QLabel("co.engcut@outlook.com")
        _email_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        _email_lbl.setIndent(95)
        _email_lbl.setFixedHeight(18)
        _email_lbl.setStyleSheet(
            "color: #888888; font-size: 12px; background: transparent; border: none;"
        )

        _logo_widget = QWidget()
        _logo_widget.setStyleSheet("background: transparent; border: none;")
        _logo_vbox = QVBoxLayout(_logo_widget)
        _logo_vbox.setContentsMargins(0, 0, 0, 0)
        _logo_vbox.setSpacing(0)
        _logo_vbox.addWidget(brand_label, 0, Qt.AlignLeft | Qt.AlignTop)
        _logo_vbox.addWidget(_email_lbl, 0, Qt.AlignLeft | Qt.AlignBottom)

        header_layout.addWidget(_logo_widget, 0, Qt.AlignLeft | Qt.AlignVCenter)
        header_layout.addStretch()

        self.top_status_label = QLabel("●  STATUS: READY\nEngine Initialized")
        self.top_status_label.setAlignment(Qt.AlignCenter)
        self.top_status_label.setWordWrap(True)
        self.top_status_label.setFixedHeight(60)
        self.top_status_label.setMinimumWidth(200)
        self.top_status_label.setStyleSheet("""
            font-size: 13px;
            font-weight: bold;
            color: #32ff7e;
            background-color: #0e2a20;
            border: 1px solid #1fbf6b;
            border-radius: 9px;
            padding: 4px 18px;
            min-height: 60px;
            max-height: 60px;
        """)
        header_layout.addWidget(self.top_status_label, 0, Qt.AlignVCenter)

        header_outer.addLayout(header_layout)

        # Admin buttons are inserted here (after creation) — stored for later access
        self._header_layout = header_layout
        # expiry_label mantido como attr para _license_startup_check — não adicionado ao layout
        self.license_expiry_label = QLabel("")
        self._status_container = self.top_status_label   # alias para compatibilidade

        header_frame.setLayout(header_outer)
        main_layout.addWidget(header_frame)

        # =====================================================
        # PROJECT STRIP
        # =====================================================

        project_strip = QFrame()
        self.project_strip = project_strip
        project_strip.setStyleSheet("""
            QFrame {
                background-color: #0f1720;
                border: 1px solid #275580;
                border-radius: 9px;
            }
        """)

        project_layout = QHBoxLayout()
        project_layout.setContentsMargins(18, 6, 18, 12)
        project_layout.setSpacing(18)

        # 1. PROJETO (B1)
        self.project_file_label = self.create_editable_strip_item("PROJETO", "No input file selected")
        if hasattr(self.project_file_label, "value_edit"):
            self.project_file_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # 2. DESCRIÇÃO (B6)
        self.project_description_label = self.create_editable_strip_item("DESCRIÇÃO", "")
        if hasattr(self.project_description_label, "value_edit"):
            self.project_description_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # 3. ENDEREÇO (B7)
        self.project_address_label = self.create_editable_strip_item("ENDEREÇO", "")
        if hasattr(self.project_address_label, "value_edit"):
            self.project_address_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # 4. CLIENTE (B8)
        self.project_client_label = self.create_editable_strip_item("CLIENTE", "")
        if hasattr(self.project_client_label, "value_edit"):
            self.project_client_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # 5. No. PROJETO (B5)
        self.project_number_label = self.create_editable_strip_item("No. PROJETO", "")
        if hasattr(self.project_number_label, "value_edit"):
            self.project_number_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # 6. DATA (B9)
        self.project_date_label = self.create_editable_strip_item("DATA", "")
        if hasattr(self.project_date_label, "value_edit"):
            self.project_date_label.value_edit.textChanged.connect(
                lambda: self.mark_unsaved() if hasattr(self, "mirror_status_label") else None
            )

        # Widgets mantidos como atributos (usados por update_unit_system e _apply_language_ui)
        # mas NÃO adicionados ao project_layout
        self.project_system_label = self.create_strip_item("", "SYSTEM", "IMPERIAL")
        self.project_units_label  = self.create_strip_item("", "INPUT UNITS (FIXED)", "Bars: ft    |    Pieces: in")
        self.project_output_label = self.create_strip_item("", "OUTPUT DISPLAY", "Decimal inches")

        project_layout.addWidget(self.project_file_label,        2)
        project_layout.addWidget(self.project_description_label, 2)
        project_layout.addWidget(self.project_address_label,     2)
        project_layout.addWidget(self.project_client_label,      1)
        project_layout.addWidget(self.project_number_label,      1)
        project_layout.addWidget(self.project_date_label,        1)

        # ── RUN PROJECT foi movido para a barra OPERATIONS ──────────────────

        project_strip.setLayout(project_layout)
        main_layout.addWidget(project_strip)

        # =====================================================
        # OPERATIONAL OVERVIEW + OPERATIONS + SETTINGS  (2-column layout)
        # =====================================================

        self.cost_value            = QLabel("$ 0.00")
        self.yield_value           = QLabel("0 %")   # YIELD COST (financeiro)
        self.yield_cut_value       = QLabel("0 %")   # YIELD CUT (eficiência de corte)
        self.stock_available_value = QLabel("$ 0.00") # inventário AVAILABLE do SQLite

        self.bars_box = self.create_bar_mix_card()   # mantido para bar_mix_big_value
        self.bars_box.hide()                          # não mostrado no KPI row (v16)
        self.cost_breakdown_box = self._create_cost_breakdown_card()
        self.pieces_box  = self.create_pieces_summary_card()
        self.waste_box   = self.create_waste_summary_card()
        # Barra empilhada: verde = reusável (STOCK), laranja = scrap
        self.waste_bar = QProgressBar()
        self.waste_bar.setRange(0, 100)
        self.waste_bar.setValue(0)
        self.waste_bar.setTextVisible(False)
        self.waste_bar.setFixedHeight(12)
        self.waste_bar.setStyleSheet("""
            QProgressBar {
                background-color: #7a2f00;
                border: 1px solid #25415a;
                border-radius: 5px;
            }
            QProgressBar::chunk {
                background-color: #4d8dff;
                border-radius: 4px;
            }
        """)
        self.waste_box.layout().insertWidget(2, self.waste_bar)

        self.cost_box    = self.create_kpi_card("◎", "TOTAL COST", self.cost_value, "Project cost", "#32ff7e",
                                                title_attr="_lbl_total_cost_card", subtitle_attr="_lbl_project_cost_sub")
        self.yield_box   = self.create_kpi_card("◉", "YIELD COST", self.yield_value, "YIELD COST", "#32ff7e",
                                                title_attr="_lbl_yield_card", subtitle_attr="_lbl_efficiency_sub")
        self.yield_value.setStyleSheet("""
            font-size: 25px;
            font-weight: bold;
            color: #32ff7e;
            background-color: #091017;
            border: 1px solid #32ff7e;
            border-radius: 8px;
            padding: 0px;
        """)
        # Barra de progresso colorida no card YIELD (verde → amarelo → vermelho, drive: YIELD CUT)
        self.yield_progress = QProgressBar()
        self.yield_progress.setRange(0, 100)
        self.yield_progress.setValue(0)
        self.yield_progress.setTextVisible(False)
        self.yield_progress.setFixedHeight(12)
        self.yield_progress.setStyleSheet("""
            QProgressBar {
                background-color: #091017;
                border: 1px solid #25415a;
                border-radius: 5px;
            }
            QProgressBar::chunk {
                background-color: #32ff7e;
                border-radius: 4px;
            }
        """)
        self.yield_box.layout().addWidget(self.yield_progress)

        self.control_box = self.create_control_status_card()

        self.update_bar_mix_preview()
        self.update_pieces_summary()
        self.update_waste_summary()
        self._refresh_available_stock_display()

        # ── 6 KPI cards v16: COST BREAKDOWN | PIECES | COST | WASTE | YIELD | CONTROL ──
        kpi_row = QHBoxLayout()
        kpi_row.setSpacing(6)
        for box in [self.cost_breakdown_box, self.pieces_box, self.cost_box,
                    self.waste_box, self.yield_box, self.control_box]:
            box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
            box.setMinimumWidth(148)
            kpi_row.addWidget(box, 1)

        # ── Right column — middle: OPERATIONS ────────────────────────────────
        # RUN PROJECT: primeiro botão da barra OPERATIONS
        self.run_button = QPushButton("▷  RUN PROJECT\nStart Optimization")
        self.run_button.setFixedHeight(52)
        self.run_button.setStyleSheet("""
            QPushButton {
                background-color: #123d91;
                border: 2px solid #3da2ff;
                color: #ffffff;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                text-align: center;
                min-height: 52px;
                max-height: 52px;
            }
            QPushButton:hover {
                background-color: #1766d7;
                border: 2px solid #77c7ff;
            }
            QPushButton:disabled {
                background-color: #111820;
                color: #444444;
                border: 1px solid #222b34;
            }
        """)

        # \u2500\u2500 INPUT button \u2014 dupla fun\u00e7\u00e3o: abrir arquivo atual / selecionar novo \u2500
        _input_btn_style = """
            QToolButton {
                background-color: #1a2e44;
                border: 1.5px solid #3a6080;
                color: #d8eeff;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                padding: 4px 14px 4px 8px;
                min-height: 52px;
                max-height: 52px;
                text-align: center;
            }
            QToolButton:hover {
                background-color: #254560;
                border: 1.5px solid #5aaada;
                color: #ffffff;
            }
            QToolButton:disabled {
                background-color: #111820;
                color: #444444;
                border: 1px solid #222b34;
            }
            QToolButton::menu-button {
                width: 26px;
                background-color: #1e4878;
                border-left: 1.5px solid #5aaada;
                border-top-right-radius: 8px;
                border-bottom-right-radius: 8px;
            }
            QToolButton::menu-button:hover {
                background-color: #2a5f9a;
            }
            QToolButton::menu-arrow {
                width: 10px;
                height: 10px;
            }
        """
        self.select_input_button = QToolButton()
        self.select_input_button.setText("\u25a3  INPUT FILE\nOpen / Select")
        self.select_input_button.setFixedHeight(52)
        self.select_input_button.setPopupMode(QToolButton.MenuButtonPopup)
        self.select_input_button.setStyleSheet(_input_btn_style)
        self.select_input_button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self.validate_input_button = QPushButton("VALIDATE INPUT")
        self.validate_input_button.setVisible(False)
        self.result_button       = QPushButton("\u25a4  OPEN RESULTS\nresult.xlsx")
        self.result_button.setFixedHeight(52)
        self.cost_result_button  = QPushButton("\u25c9  OPEN COST\ncost_result.xlsx")
        self.cost_result_button.setFixedHeight(52)
        self.audit_result_button = QPushButton("\u2611  OPEN AUDIT\naudit_engcut.xlsx")
        self.audit_result_button.setFixedHeight(52)
        self.stock_button        = QPushButton("\u25a3  OPEN STOCK\nstock_database.xlsx")
        self.stock_button.setFixedHeight(52)
        self.save_project_button = QPushButton("\u2b07  SAVE PROJECT\nSave snapshot")
        self.save_project_button.setFixedHeight(52)
        self.save_project_button.setStyleSheet("""
            QPushButton {
                background-color: #1a3a1a;
                border: 1.5px solid #2ecc71;
                color: #2ecc71;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                text-align: center;
                min-height: 52px;
                max-height: 52px;
            }
            QPushButton:hover {
                background-color: #245c24;
                border: 1.5px solid #58e88a;
                color: #ffffff;
            }
            QPushButton:disabled {
                background-color: #111820;
                color: #444444;
                border: 1px solid #222b34;
            }
        """)

        # \u2500\u2500 REPORTS \u25bc dropdown button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
        _reports_style = """
            QToolButton {
                background-color: #1b2d44;
                border: 1.5px solid #3a7abf;
                color: #a8d4ff;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                padding: 6px 10px;
                min-height: 60px;
                max-height: 60px;
            }
            QToolButton:hover {
                background-color: #274060;
                border: 1.5px solid #6eb4ff;
                color: #ffffff;
            }
            QToolButton::menu-indicator { image: none; }
        """
        self.reports_button = QToolButton()
        self.reports_button.setText("\u25c8  REPORTS \u25bc")
        self.reports_button.setFixedHeight(60)
        self.reports_button.setPopupMode(QToolButton.InstantPopup)
        self.reports_button.setStyleSheet(_reports_style)
        self.reports_button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)

        _menu_style = """
            QMenu {
                background-color: #0f1e30;
                border: 1px solid #2a4a6a;
                color: #d8eeff;
                font-size: 13px;
                padding: 4px;
            }
            QMenu::item { padding: 6px 24px 6px 16px; border-radius: 4px; }
            QMenu::item:selected { background-color: #1e4070; color: #ffffff; }
            QMenu::separator { height: 1px; background-color: #1e3d5a; margin: 4px 8px; }
        """
        reports_menu = QMenu(self.reports_button)
        reports_menu.setStyleSheet(_menu_style)

        # Direct items (single-tab files)
        act_ps  = QAction("\u2015  Project Summary",         self)
        act_bal = QAction("\u2015  Balance History (BALAN\u00c7O)", self)
        act_po  = QAction("\u2015  Purchase Order",          self)

        # Cutting Report submenu (03_Cutting_Report.xlsx \u2014 3 tabs)
        sub_cut = QMenu("\u25b8  Cutting Report", self)
        sub_cut.setStyleSheet(_menu_style)
        act_csum = QAction("  Summary",   self)
        act_cl   = QAction("  Cut List",  self)
        act_cp   = QAction("  Cut Plan",  self)
        sub_cut.addAction(act_csum)
        sub_cut.addAction(act_cl)
        sub_cut.addAction(act_cp)

        # Stock Report submenu (04_Stock_Report.xlsx \u2014 3 tabs)
        sub_stk = QMenu("\u25b8  Stock Report", self)
        sub_stk.setStyleSheet(_menu_style)
        act_stk  = QAction("  Stock",    self)
        act_scr  = QAction("  Scrap",    self)
        act_hist = QAction("  History",  self)
        sub_stk.addAction(act_stk)
        sub_stk.addAction(act_scr)
        sub_stk.addAction(act_hist)

        # Cost Report submenu (05_Cost_Report.xlsx \u2014 3 tabs)
        sub_cst = QMenu("\u25b8  Cost Report", self)
        sub_cst.setStyleSheet(_menu_style)
        act_cs  = QAction("  Cost Summary",  self)
        act_cb  = QAction("  Cost By Bar",   self)
        act_cbp = QAction("  Cost By Piece", self)
        sub_cst.addAction(act_cs)
        sub_cst.addAction(act_cb)
        sub_cst.addAction(act_cbp)

        reports_menu.addAction(act_ps)
        reports_menu.addAction(act_bal)
        reports_menu.addAction(act_po)
        reports_menu.addSeparator()
        reports_menu.addMenu(sub_cut)
        reports_menu.addSeparator()
        reports_menu.addMenu(sub_stk)
        reports_menu.addSeparator()
        reports_menu.addMenu(sub_cst)
        self.reports_button.setMenu(reports_menu)

        # Connect actions \u2014 direct items
        act_ps.triggered.connect(lambda: self._generate_report("project_summary"))
        act_bal.triggered.connect(lambda: self._generate_report("project_history"))
        act_po.triggered.connect(lambda: self._generate_report("purchase_order"))
        # Cutting Report sub-items \u2192 open at specific tab
        act_csum.triggered.connect(lambda: self._generate_report("cutting_report", tab_name="SUMMARY"))
        act_cl.triggered.connect(lambda: self._generate_report("cutting_report", tab_name="CUT LIST"))
        act_cp.triggered.connect(lambda: self._generate_report("cutting_report", tab_name="CUT PLAN"))
        # Stock Report sub-items \u2192 open at specific tab
        act_stk.triggered.connect(lambda: self._generate_report("stock_report", tab_name="STOCK"))
        act_scr.triggered.connect(lambda: self._generate_report("stock_report", tab_name="SCRAP"))
        act_hist.triggered.connect(lambda: self._generate_report("stock_report", tab_name="HISTORY"))
        # Cost Report sub-items \u2192 open at specific tab
        act_cs.triggered.connect(lambda: self._generate_report("cost_report", tab_name="COST SUMMARY"))
        act_cb.triggered.connect(lambda: self._generate_report("cost_report", tab_name="COST BY BAR"))
        act_cbp.triggered.connect(lambda: self._generate_report("cost_report", tab_name="COST BY PIECE"))

        # Store actions and submenus for language update
        self._report_actions  = {
            "project_summary":  act_ps,
            "project_history":  act_bal,
            "purchase_order":   act_po,
            "cut_summary":      act_csum,
            "cut_list":         act_cl,
            "cut_plan":         act_cp,
            "stock":            act_stk,
            "scrap":            act_scr,
            "history":          act_hist,
            "cost_summary":     act_cs,
            "cost_by_bar":      act_cb,
            "cost_by_piece":    act_cbp,
        }
        self._report_submenus = {
            "cutting_report": sub_cut,
            "stock_report":   sub_stk,
            "cost_report":    sub_cst,
        }

        # \u2500\u2500 PRINT QR LABELS button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
        self.qr_labels_button = QPushButton("\u25a4  PRINT QR LABELS")
        self.qr_labels_button.setFixedHeight(60)
        self.qr_labels_button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self.qr_labels_button.setStyleSheet("""
            QPushButton {
                background-color: #2a1a3a;
                border: 1px solid #9b59b6;
                color: #d7a4ff;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                text-align: center;
                min-height: 60px;
                max-height: 60px;
            }
            QPushButton:hover {
                background-color: #3d2255;
                border: 1px solid #c39bd3;
                color: #ffffff;
            }
            QPushButton:disabled {
                background-color: #111820;
                color: #444444;
                border: 1px solid #222b34;
            }
        """)

        # ── SUPPORT button ────────────────────────────────────────────────────
        self.support_button = QPushButton("✉  SUPPORT")
        self.support_button.setFixedHeight(60)
        self.support_button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self.support_button.setStyleSheet("""
            QPushButton {
                background-color: #0d2233;
                border: 2.0px solid #1e90ff;
                color: #7ec8ff;
                font-size: 12px;
                font-weight: bold;
                border-radius: 8px;
                text-align: center;
                padding: 6px 20px;
                min-height: 60px;
                max-height: 60px;
            }
            QPushButton:hover {
                background-color: #1a3a55;
                border: 2.0px solid #5ab0ff;
                color: #ffffff;
            }
        """)
        self.support_button.clicked.connect(self._open_support_center)

        # ── TOOLS ▼ dropdown button ───────────────────────────────────────────
        self.tools_button = QToolButton()
        self.tools_button.setText("🔧  TOOLS ▼")
        self.tools_button.setFixedHeight(60)
        self.tools_button.setPopupMode(QToolButton.InstantPopup)
        self.tools_button.setStyleSheet("""
            QToolButton {
                background-color: #1e1408;
                border: 1.5px solid #b07030;
                color: #ffb347;
                font-size: 12px;
                font-weight: bold;
                border-radius: 8px;
                padding: 6px 8px;
                min-height: 60px;
                max-height: 60px;
            }
            QToolButton:hover {
                background-color: #2d1e0a;
                border: 1.5px solid #e08040;
                color: #ffffff;
            }
            QToolButton::menu-indicator { image: none; }
        """)
        _tools_menu = QMenu(self.tools_button)
        _tools_menu.setStyleSheet("""
            QMenu {
                background-color: #150e04;
                border: 1px solid #3a2208;
                color: #ffcc88;
                font-size: 13px;
                padding: 4px;
            }
            QMenu::item { padding: 6px 24px 6px 16px; border-radius: 4px; }
            QMenu::item:selected { background-color: #3a2208; color: #ffffff; }
            QMenu::separator { height: 1px; background-color: #3a2208; margin: 4px 8px; }
        """)
        self._act_safe    = QAction("🧹  Limpar Outputs (Safe Clean)",         self)
        self._act_factory = QAction("⚠️   Factory Reset (limpar tudo + estoque)", self)
        self._act_cache   = QAction("🗑  Limpar Cache Python",                  self)
        _tools_menu.addAction(self._act_safe)
        _tools_menu.addAction(self._act_factory)
        _tools_menu.addSeparator()
        _tools_menu.addAction(self._act_cache)
        self.tools_button.setMenu(_tools_menu)
        self._act_safe.triggered.connect(self._do_clean_safe)
        self._act_factory.triggered.connect(self._do_clean_factory)
        self._act_cache.triggered.connect(self._do_clean_cache)

        self._lbl_operations_section = QLabel("OPERATIONS")
        ops_title = self._lbl_operations_section
        ops_title.setStyleSheet("""
            font-size: 15px; font-weight: bold;
            color: #4d8dff; letter-spacing: 1px;
        """)
        ops_btns = QHBoxLayout()
        ops_btns.setSpacing(10)
        # Ordem: RUN PROJECT | INPUT | OPEN STOCK | SAVE PROJECT
        # OPEN RESULTS / OPEN COST / OPEN AUDIT removidos (arquivos RAW; cobertos por REPORTS ▼)
        for btn in [self.run_button, self.select_input_button,
                    self.stock_button, self.save_project_button]:
            ops_btns.addWidget(btn)
        # reports_button and qr_labels_button are placed in the header bar (admin area)

        ops_frame = QFrame()
        ops_layout = QVBoxLayout()
        ops_layout.setContentsMargins(0, 6, 0, 0)
        ops_layout.setSpacing(8)
        ops_layout.addWidget(ops_title)
        ops_layout.addLayout(ops_btns)
        ops_frame.setLayout(ops_layout)

        # ── Right column — bottom: SETTINGS ──────────────────────────────────
        settings_frame = QFrame()
        settings_layout = QHBoxLayout()
        settings_layout.setContentsMargins(0, 6, 0, 0)
        settings_layout.setSpacing(10)

        self._lbl_settings_section = QLabel("SETTINGS")
        settings_title = self._lbl_settings_section
        settings_title.setMinimumWidth(160)
        settings_title.setAlignment(Qt.AlignCenter)
        settings_title.setStyleSheet("""
            font-size: 15px; font-weight: bold; color: #4d8dff;
            letter-spacing: 1px; background-color: transparent;
            border: none; border-radius: 7px; padding: 8px 10px;
        """)

        self.settings_summary_label = QLabel("")
        self.settings_summary_label.setWordWrap(True)
        self.settings_summary_label.setStyleSheet("""
            font-size: 13px; color: #d7e6f5;
            background-color: #0d141c; border: none;
            border-radius: 7px; padding: 9px 12px;
        """)

        self.settings_button = QPushButton("\u2699  SETTINGS")
        self.settings_button.setFixedWidth(150)
        self.settings_button.clicked.connect(self.open_settings_dialog)

        # \u2500\u2500 TEMPLATE selector dropdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
        self.template_button = QToolButton()
        self.template_button.setText("\U0001f4c2 TEMPLATE \u25bc")
        self.template_button.setFixedWidth(150)
        self.template_button.setPopupMode(QToolButton.InstantPopup)
        self.template_button.setStyleSheet("""
            QToolButton {
                background-color: #1b2530;
                border: 1px solid #4d8dff;
                color: #7ab4f5;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                padding: 4px 8px;
                min-height: 52px;
                max-height: 52px;
            }
            QToolButton:hover {
                background-color: #25384a;
                border: 1px solid #7ab4f5;
                color: #ffffff;
            }
            QToolButton::menu-indicator { image: none; }
        """)
        self._rebuild_template_menu()

        # -- INVENTORY standalone button ------------------------------------------
        self.inventory_button = QToolButton()
        self.inventory_button.setText("\U0001f4e6 INVENTORY ▼")
        self.inventory_button.setFixedWidth(150)
        self.inventory_button.setPopupMode(QToolButton.InstantPopup)
        self.inventory_button.setStyleSheet("""
            QToolButton {
                background-color: #0f2a1a;
                border: 1px solid #27ae60;
                color: #2ecc71;
                font-size: 13px;
                font-weight: bold;
                border-radius: 8px;
                padding: 4px 8px;
                min-height: 52px;
                max-height: 52px;
            }
            QToolButton:hover {
                background-color: #1a3d28;
                border: 1px solid #58d68d;
                color: #ffffff;
            }
            QToolButton::menu-indicator { image: none; }
        """)
        _inv_menu = QMenu(self.inventory_button)
        _inv_menu.setStyleSheet("""
            QMenu { background-color: #0a1f10; border: 1px solid #27ae60;
                    color: #b8f5c8; font-size: 12px; }
            QMenu::item { padding: 6px 20px; }
            QMenu::item:selected { background-color: #1a4025; color: #ffffff; }
        """)
        self._act_inv_open   = QAction("\U0001f4c2  Abrir INVENTORY.xlsx",       self)
        self._act_inv_import = QAction("\U0001f4e5  Importar do INVENTORY.xlsx", self)
        _inv_menu.addAction(self._act_inv_open)
        _inv_menu.addAction(self._act_inv_import)
        self.inventory_button.setMenu(_inv_menu)
        self._act_inv_open.triggered.connect(self._open_standalone_inventory)
        self._act_inv_import.triggered.connect(lambda: self._import_inventory_xlsx())

        # \u2500\u2500 LANGUAGE \u25bc dropdown button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
        self.language_button = QToolButton()
        self.language_button.setText("\ud83c\udf10  ENGLISH \u25bc")
        self.language_button.setFixedWidth(160)
        self.language_button.setFixedHeight(60)
        self.language_button.setPopupMode(QToolButton.InstantPopup)
        self.language_button.setStyleSheet("""
            QToolButton {
                background-color: #1a2a1a;
                border: 1.5px solid #27ae60;
                color: #2ecc71;
                font-size: 12px;
                font-weight: bold;
                border-radius: 8px;
                padding: 6px 8px;
                min-height: 60px;
                max-height: 60px;
            }
            QToolButton:hover {
                background-color: #243a24;
                border: 1.5px solid #58d68d;
                color: #ffffff;
            }
            QToolButton::menu-indicator { image: none; }
        """)
        lang_menu = QMenu(self.language_button)
        lang_menu.setStyleSheet("""
            QMenu {
                background-color: #0f1e18;
                border: 1px solid #1e4d30;
                color: #a8e6c0;
                font-size: 13px;
                padding: 4px;
            }
            QMenu::item { padding: 7px 20px; border-radius: 4px; }
            QMenu::item:selected { background-color: #1e5e38; color: #ffffff; }
        """)
        act_en = QAction("English",    self)
        act_pt = QAction("Portugu\u00eas",  self)
        act_es = QAction("Espa\u00f1ol",    self)
        lang_menu.addAction(act_en)
        lang_menu.addAction(act_pt)
        lang_menu.addAction(act_es)
        self.language_button.setMenu(lang_menu)
        act_en.triggered.connect(lambda: self._switch_language("en"))
        act_pt.triggered.connect(lambda: self._switch_language("pt"))
        act_es.triggered.connect(lambda: self._switch_language("es"))

        # Hidden backend controls
        self.selected_input_label = QLabel("No input file selected")
        self.input_status_label   = QLabel("READY AFTER SELECT   \u2713")
        self.input_system_combo   = QComboBox()
        self.input_system_combo.addItems(["IMPERIAL", "METRIC"])
        self.output_display_combo = QComboBox()
        self.fixed_units_label    = QLabel("Bars: ft   |   Pieces: in")
        self.bar_input_combo      = QLabel("ft")
        self.piece_input_combo    = QLabel("in")
        self.kerf_input           = QLineEdit()
        self.kerf_input.setText("0.125")
        self.tolerance_input      = QLineEdit()
        self.tolerance_input.setText("0.010")

        self.load_imperial_units()
        self.input_system_combo.currentTextChanged.connect(self.update_unit_system)
        self.output_display_combo.currentTextChanged.connect(self.update_output_display_label)
        self.output_display_combo.currentTextChanged.connect(lambda v: self.update_settings_summary())
        self.kerf_input.textChanged.connect(lambda v: self.update_settings_summary())
        self.tolerance_input.textChanged.connect(lambda v: self.update_settings_summary())
        self.update_settings_summary()

        settings_layout.addWidget(settings_title)
        settings_layout.addWidget(self.settings_summary_label, 1)
        settings_layout.addWidget(self.template_button)
        settings_layout.addWidget(self.inventory_button)
        # language_button is placed in the header bar (admin area)
        settings_layout.addWidget(self.settings_button)
        settings_frame.setLayout(settings_layout)

        self._lbl_overview_section = QLabel("OPERATIONAL OVERVIEW")
        overview_title = self._lbl_overview_section
        overview_title.setStyleSheet("""
            font-size: 15px; font-weight: bold;
            color: #4d8dff; letter-spacing: 1px;
        """)

        # ── BAR DETAIL (col esquerda) + TOTAL BARS (fundo da col esquerda) ────
        self.bar_detail_frame = self.create_bar_detail_frame()
        self.total_bars_box   = self._create_total_bars_box()

        _left_col = QFrame()
        _left_col.setStyleSheet("QFrame { background: transparent; border: none; }")
        _left_col.setMaximumWidth(560)
        _left_col.setMinimumWidth(360)
        _left_col_layout = QVBoxLayout()
        _left_col_layout.setContentsMargins(0, 0, 0, 0)
        _left_col_layout.setSpacing(6)
        _left_col_layout.addWidget(self.bar_detail_frame, 1)
        _left_col_layout.addWidget(self.total_bars_box, 0)
        _left_col.setLayout(_left_col_layout)

        # ── Coluna direita: título OVERVIEW + 6 KPI + OPS + SETTINGS ──────────
        _right_col = QFrame()
        _right_col.setStyleSheet("QFrame { background: transparent; border: none; }")
        _right_col_layout = QVBoxLayout()
        _right_col_layout.setContentsMargins(0, 0, 0, 0)
        _right_col_layout.setSpacing(10)
        _right_col_layout.addWidget(overview_title)
        _right_col_layout.addLayout(kpi_row)
        _right_col_layout.addWidget(ops_frame)
        _right_col_layout.addWidget(settings_frame)
        _right_col.setLayout(_right_col_layout)

        # ── Body: layout horizontal 2 colunas ─────────────────────────────────
        body_frame = QFrame()
        body_layout = QHBoxLayout()
        body_layout.setContentsMargins(14, 8, 14, 12)
        body_layout.setSpacing(10)
        body_layout.addWidget(_left_col, 0)
        body_layout.addWidget(_right_col, 1)
        body_frame.setLayout(body_layout)
        main_layout.addWidget(body_frame)



        # =====================================================
        # MANUAL INPUT GRID
        # =====================================================

        manual_frame = QFrame()

        manual_layout = QVBoxLayout()
        manual_layout.setContentsMargins(14, 10, 14, 14)
        manual_layout.setSpacing(8)

        self._lbl_manual_input_section = QLabel("MANUAL INPUT")
        manual_title = self._lbl_manual_input_section

        manual_title.setStyleSheet("""
            font-size: 15px;
            font-weight: bold;
            color: #4d8dff;
            letter-spacing: 1px;
        """)

        manual_toolbar = QHBoxLayout()
        manual_toolbar.setSpacing(6)

        self.manual_mode_label = QLabel("Direct input mode enabled")
        self.manual_mode_label.setStyleSheet("""
            font-size: 14px;
            color: #32ff7e;
            padding-left: 4px;
        """)

        # ── Mirror status indicator ───────────────────────────────────────
        self.mirror_status_label = QLabel("✓  SYNCED")
        self.mirror_status_label.setStyleSheet("""
            font-size: 13px;
            font-weight: bold;
            color: #2ecc71;
            padding: 4px 10px;
        """)

        self.add_row_button    = QPushButton("+ ADD ROW")
        self.remove_row_button = QPushButton("- REMOVE ROW")
        self.clear_input_button = QPushButton("CLEAR INPUT")

        self.add_row_button.setFixedHeight(52)
        self.remove_row_button.setFixedHeight(52)
        self.clear_input_button.setFixedHeight(52)

        self.add_row_button.clicked.connect(self.add_manual_row)
        self.remove_row_button.clicked.connect(self.remove_manual_row)
        self.clear_input_button.clicked.connect(self.clear_manual_input)

        _sync_style_load = """
            QPushButton {
                background-color: #0d2040;
                border: 1px solid #1e5080;
                color: #5ab4ff;
                padding: 4px 14px;
                font-size: 13px;
                font-weight: bold;
                border-radius: 7px;
                min-height: 52px;
                max-height: 52px;
            }
            QPushButton:hover {
                background-color: #163060;
                border: 1px solid #5ab4ff;
                color: #ffffff;
            }
            QPushButton:disabled {
                background-color: #09121c;
                color: #243040;
                border: 1px solid #111c28;
            }
        """

        _sync_style_save = """
            QPushButton {
                background-color: #0d3320;
                border: 1px solid #1e7a40;
                color: #2ecc71;
                padding: 4px 14px;
                font-size: 13px;
                font-weight: bold;
                border-radius: 7px;
                min-height: 52px;
                max-height: 52px;
            }
            QPushButton:hover {
                background-color: #155228;
                border: 1px solid #2ecc71;
                color: #ffffff;
            }
            QPushButton:disabled {
                background-color: #0a1810;
                color: #2a4a32;
                border: 1px solid #132018;
            }
        """

        # ── LOAD FROM EXCEL: xlsx → Tela ─────────────────────────────────
        self.load_from_excel_button = QPushButton("←  LOAD FROM EXCEL")
        self.load_from_excel_button.setFixedHeight(52)
        self.load_from_excel_button.setStyleSheet(_sync_style_load)
        self.load_from_excel_button.clicked.connect(self.load_from_excel)

        # ── SYNC TO EXCEL: Tela → xlsx (substitui SAVE TO EXCEL) ─────────
        self.sync_to_excel_button = QPushButton("SYNC TO EXCEL  →")
        self.sync_to_excel_button.setFixedHeight(52)
        self.sync_to_excel_button.setStyleSheet(_sync_style_save)
        self.sync_to_excel_button.clicked.connect(self.sync_table_to_excel)
        self.sync_to_excel_button.setEnabled(True)

        manual_toolbar.addWidget(self.manual_mode_label)
        manual_toolbar.addWidget(self.mirror_status_label)
        manual_toolbar.addStretch()
        manual_toolbar.addWidget(self.load_from_excel_button)
        manual_toolbar.addWidget(self.sync_to_excel_button)
        manual_toolbar.addWidget(self.add_row_button)
        manual_toolbar.addWidget(self.remove_row_button)
        manual_toolbar.addWidget(self.clear_input_button)

        self.manual_table = QTableWidget()

        self.manual_table.setColumnCount(19)
        self.manual_table.setRowCount(8)
        self.manual_table.setEditTriggers(
            QAbstractItemView.DoubleClicked |
            QAbstractItemView.SelectedClicked |
            QAbstractItemView.EditKeyPressed |
            QAbstractItemView.AnyKeyPressed
        )

        self.manual_table.setHorizontalHeaderLabels([
            "Description",
            "Product Code",
            "Cut Group",
            "Length\n(cm or in)",
            "Qty",
            "Bar Stock\n1 (m or ft)",
            "Bar Stock\n2 (m or ft)",
            "Bar Stock\n3 (m or ft)",
            "Bar Stock\n4 (m or ft)",
            "Bar Stock\n5 (m or ft)",
            "Min Reusable\nLength (cm or in)",
            "Weight\n(kg/m or lb/ft)",
            "Price Bar\n($/m or $/ft)",
            "junkyard\n($/kg or $/lb)",
            "User Tag",
            "Area / Zone",
            "Assembly Tag",
            "Piece ID\n(system)",
            "Observation",
        ])

        self.manual_table.setMinimumHeight(275)

        self.manual_table.setStyleSheet("""
            QTableWidget {
                background-color: #D0D6BC;
                alternate-background-color: #C8CEB5;
                border: 1px solid #848C6E;
                gridline-color: #848C6E;
                color: #000000;
                selection-background-color: #ABB493;
                selection-color: #000000;
                font-size: 12px;
            }

            QTableWidget::item {
                background-color: #D0D6BC;
                color: #000000;
                border: 0px;
                padding: 2px 4px;
            }

            QTableWidget::item:selected {
                background-color: #ABB493;
                color: #000000;
            }

            QHeaderView::section {
                background-color: #1f557f;
                color: white;
                padding: 7px;
                border: 1px solid #848C6E;
                font-weight: bold;
                font-size: 12px;
            }
        """)

        self.manual_table.setHorizontalHeader(CyanHeaderView(Qt.Horizontal, self.manual_table))
        self.manual_table.verticalHeader().setVisible(False)
        self.manual_table.setAlternatingRowColors(True)
        self.manual_table.setSelectionBehavior(QAbstractItemView.SelectItems)
        self.manual_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.manual_table.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter
        )
        self.manual_table.horizontalHeader().setMinimumHeight(52)
        self.manual_table.horizontalHeader().setSectionResizeMode(
            QHeaderView.Interactive
        )
        self.manual_table.verticalHeader().setDefaultSectionSize(28)

        manual_col_widths = [155, 175, 105, 85, 70, 82, 82, 82, 82, 82, 125, 88, 100, 90, 90, 95, 105, 90, 130]

        for col, width in enumerate(manual_col_widths):
            self.manual_table.setColumnWidth(col, width)


        for row in range(self.manual_table.rowCount()):
            self.manual_table.setRowHeight(row, 28)
            for col in range(self.manual_table.columnCount()):
                item = QTableWidgetItem("")
                item.setTextAlignment(self.manual_cell_alignment(col))
                self.manual_table.setItem(row, col, item)

        # Track edits → mark unsaved whenever user changes any cell
        self._table_loading = False   # flag to suppress signals during programmatic load
        self.manual_table.itemChanged.connect(self._on_table_item_changed)

        # Delegate de lista suspensa na coluna Description (col 0)
        self._criterios_delegate_data  = {}   # {DESC.upper(): {...}}
        self._criterios_delegate_names = []   # lista ordenada de descrições
        self._desc_delegate = DescriptionComboDelegate(
            items=[],
            on_committed=self._apply_description_autofill,
            parent=self.manual_table,
        )
        self.manual_table.setItemDelegateForColumn(0, self._desc_delegate)
        # Tenta popular o delegate assim que a tabela é criada
        self._load_criterios_delegate_data()

        # Enable Ctrl+C / Ctrl+V / Ctrl+X on the manual table
        self.manual_table.keyPressEvent = self._manual_table_key_press

        manual_layout.addWidget(manual_title)
        manual_layout.addLayout(manual_toolbar)
        manual_layout.addWidget(self.manual_table)

        manual_frame.setLayout(manual_layout)

        main_layout.addWidget(manual_frame)

        # =====================================================
        # STATUS / PROGRESS
        # =====================================================

        self.status_label = QLabel("STATUS: IDLE")
        self.status_label.setWordWrap(True)
        self.status_label.setStyleSheet("""
            font-size: 13px;
            font-weight: bold;
            color: #32ff7e;
            background-color: #0d141c;
            border: none;
            border-radius: 7px;
            padding: 8px;
        """)

        main_layout.addWidget(self.status_label)

        self.progress_bar = QProgressBar()
        self.progress_bar.setVisible(False)

        main_layout.addWidget(self.progress_bar)

        # =====================================================
        # PROCESS LOG
        # =====================================================

        log_frame = QFrame()
        log_layout = QVBoxLayout()
        log_layout.setContentsMargins(14, 10, 14, 14)
        log_layout.setSpacing(8)

        self._lbl_process_log_section = QLabel("PROCESS LOG")
        log_title = self._lbl_process_log_section
        log_title.setStyleSheet("""
            font-size: 15px;
            font-weight: bold;
            color: #4d8dff;
            letter-spacing: 1px;
        """)

        self.log_panel = QTextEdit()
        self.log_panel.setMinimumHeight(210)
        self.log_panel.setReadOnly(True)

        log_layout.addWidget(log_title)
        log_layout.addWidget(self.log_panel)

        log_frame.setLayout(log_layout)
        main_layout.addWidget(log_frame, 1)

        # ── Scroll container — allows free resize in both axes ──────────────
        scroll_container = QWidget()
        scroll_container.setLayout(main_layout)
        _min_w = min(1660, max(1100, QApplication.primaryScreen().availableGeometry().width() - 30))
        scroll_container.setMinimumWidth(_min_w)
        scroll_container.setMinimumHeight(700)

        scroll_area = QScrollArea()
        scroll_area.setWidget(scroll_container)
        scroll_area.setWidgetResizable(True)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setStyleSheet("""
            QScrollArea { background: transparent; border: none; }
            QScrollBar:vertical {
                background: #060d14;
                width: 8px;
                border-radius: 4px;
            }
            QScrollBar::handle:vertical {
                background: #1e3d5a;
                border-radius: 4px;
                min-height: 20px;
            }
            QScrollBar::handle:vertical:hover { background: #3a7abf; }
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }
            QScrollBar:horizontal {
                background: #060d14;
                height: 8px;
                border-radius: 4px;
            }
            QScrollBar::handle:horizontal {
                background: #1e3d5a;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover { background: #3a7abf; }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; }
        """)

        outer_layout = QVBoxLayout()
        outer_layout.setContentsMargins(0, 0, 0, 0)
        outer_layout.setSpacing(0)
        outer_layout.addWidget(scroll_area)

        self.setLayout(outer_layout)

        self.run_button.clicked.connect(self.run_project)

        self.result_button.clicked.connect(self.open_result)
        self.cost_result_button.clicked.connect(self.open_cost)
        self.audit_result_button.clicked.connect(self.open_audit)
        self.stock_button.clicked.connect(self.open_stock)
        self.save_project_button.clicked.connect(self.save_project)
        self.qr_labels_button.clicked.connect(self._print_qr_labels)

        # INPUT button: clique principal = abrir INPUT atual; menu = selecionar novo
        _input_menu_style = """
            QMenu {
                background-color: #0f1e30;
                border: 1px solid #2a4a6a;
                color: #d8eeff;
                font-size: 13px;
                padding: 4px;
            }
            QMenu::item { padding: 6px 24px 6px 16px; border-radius: 4px; }
            QMenu::item:selected { background-color: #1e4070; color: #ffffff; }
        """
        _input_menu = QMenu(self.select_input_button)
        _input_menu.setStyleSheet(_input_menu_style)
        _act_open = QAction("▶  Abrir INPUT atual no Excel", self)
        _act_select = QAction("◻  Selecionar novo arquivo INPUT...", self)
        _input_menu.addAction(_act_open)
        _input_menu.addAction(_act_select)
        self.select_input_button.setMenu(_input_menu)
        self.select_input_button.clicked.connect(self._open_current_input)
        _act_open.triggered.connect(self._open_current_input)
        _act_select.triggered.connect(self.select_input_file)

        self.validate_input_button.clicked.connect(
            self.validate_selected_input
        )

        # Move admin buttons (REPORTS, LANGUAGE, QR) into the header bar
        _hl = self._header_layout
        _hl.removeWidget(self.top_status_label)          # temporarily detach status label

        _hl.addWidget(self.reports_button, 0, Qt.AlignVCenter)
        _hl.addSpacing(25)
        _hl.addWidget(self.language_button, 0, Qt.AlignVCenter)
        _hl.addSpacing(25)
        _hl.addWidget(self.qr_labels_button, 0, Qt.AlignVCenter)
        _hl.addSpacing(25)
        _hl.addWidget(self.support_button, 0, Qt.AlignVCenter)
        _hl.addSpacing(25)
        _hl.addWidget(self.tools_button, 0, Qt.AlignVCenter)
        _hl.addSpacing(25)
        _hl.addWidget(self.top_status_label, 0, Qt.AlignVCenter)

        # Load language preference and apply to UI
        self._load_language()

        # Auto-load default INPUT_sistema.xlsx on startup
        self.load_input_file_preview()

        # Maximizar automaticamente em telas menores que 1700px de largura
        if QApplication.primaryScreen().availableGeometry().width() < 1700:
            QTimer.singleShot(0, self.showMaximized)

        self._apply_ui_config()

        # LICENSE-001: 1500ms garante que wmic (fingerprint) completou antes da validação
        QTimer.singleShot(1500, self._license_startup_check)

    # ── UI CONFIG ────────────────────────────────────────────────────────────

    def _apply_ui_config(self):
        """Aplica UI_CONFIG.json sobre os widgets nomeados da interface."""
        import json as _json, pathlib as _pl
        try:
            _cfg_path = _pl.Path(__file__).parent.parent / "documentation" / "ui_configuration" / "UI_CONFIG.json"
            if not _cfg_path.exists():
                return
            with open(_cfg_path, encoding="utf-8") as _f:
                C = _json.load(_f)
        except Exception:
            return

        def _g(*path, default=None):
            d = C
            for k in path:
                if not isinstance(d, dict):
                    return default
                d = d.get(k)
                if d is None:
                    return default
            return d

        _DISABLED = "background-color:#111820;color:#444444;border:1px solid #222b34;"

        # ── TOTAL COST — ocultar ──────────────────────────────────────────────
        if hasattr(self, "cost_box"):
            self.cost_box.setVisible(False)

        # ── PROJECT STRIP ─────────────────────────────────────────────────────
        ps = _g("PROJECT_STRIP") or {}
        if ps and hasattr(self, "project_strip"):
            bg  = ps.get("bg_color", "#0f1720")
            bc  = ps.get("border_color", "#275580")
            br  = ps.get("border_radius_px", 9)
            self.project_strip.setStyleSheet(
                f"QFrame{{background-color:{bg};border:1px solid {bc};border-radius:{br}px;}}"
            )

        # ── HEADER: STATUS ────────────────────────────────────────────────────
        st = _g("HEADER", "STATUS") or {}
        if st and hasattr(self, "top_status_label"):
            self.top_status_label.setStyleSheet(
                f"font-size:{st.get('font_size_px',13)}px;font-weight:bold;"
                f"color:{st.get('text_color','#32ff7e')};"
                f"background-color:{st.get('bg_color','#0e2a20')};"
                f"border:{st.get('border_px',2.0)}px solid {st.get('border_color','#1fbf6b')};"
                f"border-radius:9px;padding:4px 18px;min-height:60px;max-height:60px;"
            )

        # ── HEADER: REPORTS ▼ ─────────────────────────────────────────────────
        rl = _g("HEADER", "RELATORIOS") or {}
        if rl and hasattr(self, "reports_button"):
            bpx = rl.get("border_px", 2.0)
            ph  = rl.get("padding_h_px", 20)
            self.reports_button.setStyleSheet(
                f"QToolButton{{background-color:{rl.get('bg_color','#1b2d44')};"
                f"border:{bpx}px solid {rl.get('border_color','#3a7abf')};"
                f"color:{rl.get('text_color','#a8d4ff')};"
                f"font-size:{rl.get('font_size_px',13)}px;font-weight:bold;"
                f"border-radius:8px;padding:6px {ph}px;min-height:60px;max-height:60px;}}"
                f"QToolButton:hover{{background-color:{rl.get('bg_color_hover','#274060')};"
                f"border:{bpx}px solid {rl.get('border_color_hover','#6eb4ff')};color:#ffffff;}}"
                "QToolButton::menu-indicator{image:none;}"
            )

        # ── HEADER: QR LABELS ────────────────────────────────────────────────
        qr = _g("HEADER", "QR_LABELS") or {}
        if qr and hasattr(self, "qr_labels_button"):
            bpx = qr.get("border_px", 2.0)
            ph  = qr.get("padding_h_px", 20)
            self.qr_labels_button.setStyleSheet(
                f"QPushButton{{background-color:{qr.get('bg_color','#2a1a3a')};"
                f"border:{bpx}px solid {qr.get('border_color','#9b59b6')};"
                f"color:{qr.get('text_color','#d7a4ff')};"
                f"font-size:{qr.get('font_size_px',12)}px;font-weight:bold;"
                f"border-radius:8px;text-align:center;padding:6px {ph}px;min-height:60px;max-height:60px;}}"
                f"QPushButton:hover{{background-color:{qr.get('bg_color_hover','#3d2255')};"
                f"border:{bpx}px solid {qr.get('border_color_hover','#c39bd3')};color:#ffffff;}}"
                f"QPushButton:disabled{{{_DISABLED}}}"
            )

        # ── HEADER: SUPPORT ───────────────────────────────────────────────────
        su = _g("HEADER", "SUPORTE") or {}
        if su and hasattr(self, "support_button"):
            bpx = su.get("border_px", 2.0)
            ph  = su.get("padding_h_px", 20)
            self.support_button.setStyleSheet(
                f"QPushButton{{background-color:{su.get('bg_color','#0d2233')};"
                f"border:{bpx}px solid {su.get('border_color','#1e90ff')};"
                f"color:{su.get('text_color','#7ec8ff')};"
                f"font-size:{su.get('font_size_px',12)}px;font-weight:bold;"
                f"border-radius:8px;text-align:center;padding:6px {ph}px;min-height:60px;max-height:60px;}}"
                f"QPushButton:hover{{background-color:{su.get('bg_color_hover','#1a3a55')};"
                f"border:{bpx}px solid {su.get('border_color_hover','#5ab0ff')};color:#ffffff;}}"
            )

        # ── HEADER: TOOLS ▼ ───────────────────────────────────────────────────
        tl = _g("HEADER", "TOOLS") or {}
        if tl and hasattr(self, "tools_button"):
            bpx = tl.get("border_px", 2.0)
            ph  = tl.get("padding_h_px", 20)
            self.tools_button.setStyleSheet(
                f"QToolButton{{background-color:{tl.get('bg_color','#1e1408')};"
                f"border:{bpx}px solid {tl.get('border_color','#b07030')};"
                f"color:{tl.get('text_color','#ffb347')};"
                f"font-size:{tl.get('font_size_px',12)}px;font-weight:bold;"
                f"border-radius:8px;padding:6px {ph}px;min-height:60px;max-height:60px;}}"
                f"QToolButton:hover{{background-color:{tl.get('bg_color_hover','#2d1e0a')};"
                f"border:{bpx}px solid {tl.get('border_color_hover','#e08040')};color:#ffffff;}}"
                "QToolButton::menu-indicator{image:none;}"
            )

        # ── HEADER: LANGUAGE ▼ ────────────────────────────────────────────────
        lg = _g("HEADER", "LANGUAGE") or {}
        if lg and hasattr(self, "language_button"):
            bpx = lg.get("border_px", 2.0)
            self.language_button.setStyleSheet(
                f"QToolButton{{background-color:{lg.get('bg_color','#1a2a1a')};"
                f"border:{bpx}px solid {lg.get('border_color','#27ae60')};"
                f"color:{lg.get('text_color','#2ecc71')};"
                f"font-size:{lg.get('font_size_px',12)}px;font-weight:bold;"
                f"border-radius:8px;padding:6px 8px;min-height:60px;max-height:60px;}}"
                f"QToolButton:hover{{background-color:{lg.get('bg_color_hover','#243a24')};"
                f"border:{bpx}px solid {lg.get('border_color_hover','#58d68d')};color:#ffffff;}}"
                "QToolButton::menu-indicator{image:none;}"
            )

        # ── OPERATIONS BUTTONS ────────────────────────────────────────────────
        ob   = _g("OPERATIONS_BUTTONS") or {}
        og   = ob.get("geral", {})
        bh   = og.get("height_px", 56)
        _bgrad_global = 8

        def _ops_push(w, cfg):
            if not (w and cfg):
                return
            bpx = cfg.get("border_px", 1.5); bcor = cfg.get("border_color", "#3a5a7a")
            bhov = cfg.get("border_color_hover", bcor); bgc = cfg.get("bg_color", "#1a2030")
            bghov = cfg.get("bg_color_hover", bgc); tc = cfg.get("text_color", "#d0e8ff")
            fs  = cfg.get("font_size_px", 13); br = cfg.get("border_radius_px", _bgrad_global)
            w.setFixedHeight(bh)
            w.setStyleSheet(
                f"QPushButton{{background-color:{bgc};border:{bpx}px solid {bcor};"
                f"color:{tc};font-size:{fs}px;font-weight:bold;border-radius:{br}px;"
                f"text-align:center;min-height:{bh}px;max-height:{bh}px;}}"
                f"QPushButton:hover{{background-color:{bghov};border:{bpx}px solid {bhov};color:#ffffff;}}"
                f"QPushButton:disabled{{{_DISABLED}}}"
            )

        _ops_push(getattr(self, "run_button", None),          ob.get("RUN_PROJECT", {}))
        _ops_push(getattr(self, "stock_button", None),        ob.get("OPEN_STOCK", {}))
        _ops_push(getattr(self, "save_project_button", None), ob.get("SAVE_PROJECT", {}))

        inp = ob.get("INPUT_FILE", {})
        iw  = getattr(self, "select_input_button", None)
        if iw and inp:
            bpx = inp.get("border_px", 1.5); bcor = inp.get("border_color", "#3a6080")
            bhov = inp.get("border_color_hover", bcor); bgc = inp.get("bg_color", "#1a2e44")
            bghov = inp.get("bg_color_hover", bgc); tc = inp.get("text_color", "#d8eeff")
            fs  = inp.get("font_size_px", 13); br = inp.get("border_radius_px", _bgrad_global)
            iw.setFixedHeight(bh)
            iw.setStyleSheet(
                f"QToolButton{{background-color:{bgc};border:{bpx}px solid {bcor};"
                f"color:{tc};font-size:{fs}px;font-weight:bold;border-radius:{br}px;"
                f"padding:4px 14px 4px 8px;min-height:{bh}px;max-height:{bh}px;text-align:center;}}"
                f"QToolButton:hover{{background-color:{bghov};border:{bpx}px solid {bhov};color:#ffffff;}}"
                f"QToolButton:disabled{{{_DISABLED}}}"
                f"QToolButton::menu-button{{width:26px;background-color:#1e4878;"
                f"border-left:{bpx}px solid {bcor};border-top-right-radius:{br}px;border-bottom-right-radius:{br}px;}}"
                "QToolButton::menu-button:hover{background-color:#2a5f9a;}"
                "QToolButton::menu-arrow{width:10px;height:10px;}"
            )

        # ── KPI GERAL — background de cada card ───────────────────────────────
        kg = _g("KPI_CARDS", "geral") or {}
        bg_card  = kg.get("bg_color", "#0d141c")
        brad_card = kg.get("border_radius_px", 5)
        h_min    = kg.get("altura_minima_px", 165)
        for box in [self.cost_breakdown_box, self.pieces_box, self.waste_box,
                    self.yield_box, self.control_box]:
            if hasattr(box, "setStyleSheet"):
                box.setStyleSheet(
                    f"QFrame{{background-color:{bg_card};"
                    f"border:1px solid {kg.get('item_border_color','#25415a')};"
                    f"border-radius:{brad_card}px;}}"
                )
                box.setMinimumHeight(h_min)

        # ── KPI COST BREAKDOWN ────────────────────────────────────────────────
        cbd = _g("KPI_CARDS", "COST_BREAKDOWN") or {}
        if cbd:
            acc_cb   = cbd.get("accent_color", "#a66bff")
            total_fs = cbd.get("total_font_size_px", 23)
            bar_lfs  = cbd.get("bar_label_font_size_px", 11)
            h_min_cb = cbd.get("altura_minima_px", 204)
            if hasattr(self, "cost_breakdown_box"):
                self.cost_breakdown_box.setMinimumHeight(h_min_cb)
            if hasattr(self, "_cb_total_label"):
                self._cb_total_label.setStyleSheet(
                    f"background:#091017;color:{acc_cb};border:1px solid #25415a;"
                    f"border-radius:6px;font-size:{total_fs}px;font-weight:bold;"
                )
            if hasattr(self, "_cb_icon_label"):
                self._cb_icon_label.setStyleSheet(
                    f"font-size:11px;color:{acc_cb};background:#091017;"
                    "border:1px solid #25415a;border-radius:6px;"
                )
            if hasattr(self, "_lbl_cost_breakdown_card"):
                self._lbl_cost_breakdown_card.setStyleSheet(
                    f"font-size:15px;font-weight:bold;color:{acc_cb};background:#091017;"
                    "border:1px solid #25415a;border-radius:6px;padding:2px 4px;"
                )
            if hasattr(self, "_cb_material_label"):
                self._cb_material_label.setStyleSheet(
                    f"color:{acc_cb};font-size:10px;font-weight:bold;"
                    "background:transparent;border:none;"
                )
            if hasattr(self, "_cb_bar_widgets"):
                for _pb, _lbl, _color, _def in self._cb_bar_widgets:
                    _lbl.setStyleSheet(
                        f"color:{_color};font-size:{bar_lfs}px;font-weight:bold;"
                        "background:transparent;border:none;padding-left:2px;"
                    )

        # ── KPI PIECES ────────────────────────────────────────────────────────
        pc = _g("KPI_CARDS", "PIECES") or {}
        if pc:
            vc  = pc.get("valor_color", "#45d9ff")
            vbc = pc.get("valor_border_color", "#45d9ff")
            vbp = pc.get("valor_border_px", 2)
            vfs = pc.get("valor_font_size_px", 26)
            vcfs= pc.get("valor_cost_font_size_px", 18)
            if hasattr(self, "pieces_qty_value"):
                self.pieces_qty_value.setStyleSheet(
                    f"font-size:{vfs}px;font-weight:bold;color:{vc};"
                    f"background-color:#091017;border:{vbp}px solid {vbc};border-radius:7px;padding:0px;"
                )
            if hasattr(self, "pieces_cost_value"):
                self.pieces_cost_value.setStyleSheet(
                    f"font-size:{vcfs}px;font-weight:bold;color:{vc};"
                    "background-color:#091017;border:1px solid #25415a;border-radius:8px;padding:0px;"
                )

        # ── KPI YIELD ─────────────────────────────────────────────────────────
        yc = _g("KPI_CARDS", "YIELD") or {}
        if yc:
            acc = yc.get("accent_color", "#32ff7e")
            if hasattr(self, "yield_value"):
                self.yield_value.setStyleSheet(
                    f"font-size:{yc.get('valor_font_size_px',25)}px;font-weight:bold;color:{acc};"
                    "background-color:#091017;border:1px solid #32ff7e;border-radius:8px;padding:0px;"
                )
            if hasattr(self, "yield_progress"):
                bh_bar = yc.get("barra_height_px", 12)
                self.yield_progress.setFixedHeight(bh_bar)
                self.yield_progress.setStyleSheet(
                    f"QProgressBar{{background:{yc.get('barra_bg','#091017')};"
                    f"border:1px solid #25415a;border-radius:5px;}}"
                    f"QProgressBar::chunk{{background:{yc.get('barra_chunk','#32ff7e')};border-radius:4px;}}"
                )

        # ── KPI WASTE ─────────────────────────────────────────────────────────
        wc = _g("KPI_CARDS", "WASTE") or {}
        if wc and hasattr(self, "waste_bar"):
            bh_bar = wc.get("barra_height_px", 12)
            self.waste_bar.setFixedHeight(bh_bar)
            self.waste_bar.setStyleSheet(
                f"QProgressBar{{background-color:{wc.get('barra_bg','#7a2f00')};"
                f"border:1px solid #25415a;border-radius:5px;}}"
                f"QProgressBar::chunk{{background-color:{wc.get('barra_chunk_stock','#4d8dff')};border-radius:4px;}}"
            )

        # ── TOTAL BARS ────────────────────────────────────────────────────────
        tb = _g("TOTAL_BARS_WIDGET") or {}
        if tb:
            acc      = tb.get("cor_acento", "#a66bff")
            bg_f     = tb.get("bg_frame", "#0d0a1e")
            brd_f    = tb.get("brd_frame_color", "#3a1a60")
            ico_bg   = tb.get("ico_bg", "#060e16")
            ico_brd  = tb.get("ico_border_color", "#1e3050")
            tit_bg   = tb.get("titulo_bg", "#091017")
            tit_brd  = tb.get("titulo_border_color", "#25415a")
            tit_fs   = tb.get("titulo_font_size_px", 11)
            sub_col  = tb.get("sub_color", "#5a4a7a")
            sub_fs   = tb.get("sub_font_size_px", 10)
            val_bg   = tb.get("val_bg", "#060e16")
            val_brd  = tb.get("val_border_color", acc)
            val_bpx  = tb.get("val_border_px", 2)
            val_fs   = tb.get("val_font_size_px", 26)
            _brad_tb = _g("BORDAS", "total_bars_radius_px", default=8)
            _bpx_tb  = _g("BORDAS", "total_bars_borda_px", default=1)
            if hasattr(self, "total_bars_box"):
                self.total_bars_box.setStyleSheet(
                    f"QFrame{{background-color:{bg_f};border:{_bpx_tb}px solid {brd_f};"
                    f"border-radius:{_brad_tb}px;}}"
                )
            if hasattr(self, "_total_bars_icon"):
                _ico_sz = _g("ICONES", "total_bars_px", default=32)
                self._total_bars_icon.setFixedSize(_ico_sz, _ico_sz)
                self._total_bars_icon.setStyleSheet(
                    f"font-size:{_ico_sz//2}px;color:{acc};background:{ico_bg};"
                    f"border:1px solid {ico_brd};border-radius:7px;"
                )
            if hasattr(self, "_lbl_total_bars_section"):
                self._lbl_total_bars_section.setStyleSheet(
                    f"font-size:{tit_fs}px;font-weight:bold;color:{acc};"
                    f"background:{tit_bg};border:1px solid {tit_brd};"
                    f"border-radius:6px;padding:2px 4px;"
                )
            if hasattr(self, "_lbl_bars_in_project"):
                self._lbl_bars_in_project.setStyleSheet(
                    f"font-size:{sub_fs}px;color:{sub_col};background:transparent;border:none;"
                )
            if hasattr(self, "_total_bars_val_frame"):
                self._total_bars_val_frame.setStyleSheet(
                    f"QFrame{{background:{val_bg};border:{val_bpx}px solid {val_brd};border-radius:8px;}}"
                )
            if hasattr(self, "_lbl_bars_count"):
                self._lbl_bars_count.setStyleSheet(
                    f"font-size:{val_fs}px;font-weight:bold;color:{acc};"
                    "background:transparent;border:none;"
                )

    # ── LICENSE ───────────────────────────────────────────────────────────────

    def _license_startup_check(self):
        """Verifica licença na inicialização; mostra diálogo se BLOCKED."""
        try:
            from license.integrity_check import verify_manifest
            _integrity = verify_manifest()
            if not _integrity.ok:
                QMessageBox.critical(
                    self, "ENGCUT — Integridade",
                    f"{_integrity.message}\nReinstale o ENGCUT ou contate o suporte em engcut.io",
                )
                QApplication.quit()
                return
        except ImportError:
            QMessageBox.critical(
                self, "ENGCUT — Integridade",
                "Módulo de verificação de integridade ausente ou corrompido.\n"
                "Reinstale o ENGCUT ou contate o suporte em engcut.io",
            )
            QApplication.quit()
            return

        try:
            from license.validator import validate, LicenseStatus
        except ImportError:
            # Módulo de licença ausente ou corrompido NÃO é evidência de
            # licença válida — fail-closed: bloqueia e encerra a aplicação.
            QMessageBox.critical(
                self, "ENGCUT — Licença",
                "Módulo de licença ausente ou corrompido.\n"
                "Reinstale o ENGCUT ou contate o suporte em engcut.io",
            )
            QApplication.quit()
            return

        result = validate()

        # Exibir data de expiração na segunda linha do STATUS label
        _expiry = getattr(result, "expiry_date", getattr(result, "expiry", None))

        if result.status == LicenseStatus.BLOCKED:
            try:
                from front_end.activation_dialog import ActivationDialog
            except ImportError:
                from activation_dialog import ActivationDialog
            dlg = ActivationDialog(self, result=result)
            dlg.exec()
            # Se o usuário fechou sem ativar, a aplicação já foi encerrada pelo reject()
        elif result.status == LicenseStatus.TRIAL:
            days = getattr(result, "days_left", 0)
            _exp_line = f"\nExp: {_expiry}" if _expiry else "\nAtive para uso contínuo"
            self.top_status_label.setText(
                f"●  TRIAL — {days}d restantes{_exp_line}"
            )
            self.top_status_label.setStyleSheet("""
                font-size: 13px; font-weight: bold;
                color: #f0c050; background-color: #1a1400;
                border: 1px solid #806000;
                border-radius: 9px; padding: 12px 26px;
            """)

    def create_editable_strip_item(self, title, value):

        frame = QFrame()
        frame.setStyleSheet("""
            QFrame {
                background-color: transparent;
                border: 0px;
            }
        """)
        frame.setMinimumHeight(44)

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(2)

        title_label = QLabel(title)
        title_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        title_label.setStyleSheet("""
            font-size: 12px;
            font-weight: bold;
            color: #4d8dff;
        """)

        value_edit = QLineEdit(value)
        value_edit.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        value_edit.setMinimumHeight(26)
        value_edit.setStyleSheet("""
            QLineEdit {
                font-size: 12px;
                font-weight: bold;
                color: #ffffff;
                background-color: #091017;
                border: 1px solid #25415a;
                border-radius: 5px;
                padding: 3px 8px;
            }
            QLineEdit:focus {
                border: 1px solid #4d8dff;
                background-color: #0b1420;
            }
        """)

        frame.title_label = title_label
        frame.value_label = value_edit
        frame.value_edit = value_edit

        layout.addWidget(title_label)
        layout.addWidget(value_edit)

        frame.setLayout(layout)

        return frame

    def create_strip_item(self, icon, title, value):

        frame = QFrame()
        frame.setStyleSheet("""
            QFrame {
                background-color: transparent;
                border: 0px;
            }
        """)
        frame.setMinimumHeight(44)

        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(10)

        if str(icon).strip():
            icon_label = QLabel(icon)
            icon_label.setStyleSheet("""
                font-size: 26px;
                color: #4d8dff;
                padding-right: 4px;
            """)
            layout.addWidget(icon_label)

        text_layout = QVBoxLayout()
        text_layout.setSpacing(2)

        title_label = QLabel(title)
        title_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        title_label.setStyleSheet("""
            font-size: 12px;
            font-weight: bold;
            color: #4d8dff;
        """)

        value_label = QLabel(value)
        value_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        value_label.setWordWrap(False)
        value_label.setStyleSheet("""
            font-size: 15px;
            font-weight: bold;
            color: #ffffff;
        """)

        frame.title_label = title_label
        frame.value_label = value_label

        text_layout.addWidget(title_label)
        text_layout.addWidget(value_label)

        layout.addLayout(text_layout)

        frame.setLayout(layout)

        return frame

    def set_strip_value(self, strip_item, text):

        if hasattr(strip_item, "value_edit"):
            strip_item.value_edit.blockSignals(True)
            strip_item.value_edit.setText(str(text))
            strip_item.value_edit.blockSignals(False)
        elif hasattr(strip_item, "value_label"):
            strip_item.value_label.setText(str(text))

    def get_project_name(self):

        if hasattr(self, "project_file_label") and hasattr(self.project_file_label, "value_edit"):
            return self.project_file_label.value_edit.text().strip()

        if hasattr(self, "project_file_label") and hasattr(self.project_file_label, "value_label"):
            return self.project_file_label.value_label.text().strip()

        return ""

    def create_kpi_card(self, icon, title, value_label, subtitle, accent_color,
                        title_attr=None, subtitle_attr=None):

        frame = QFrame()
        frame.setMinimumHeight(165)
        frame.setStyleSheet(f"""
            QFrame {{
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }}
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(12, 10, 12, 10)
        layout.setSpacing(6)

        top_row = QHBoxLayout()

        icon_label = QLabel(icon)
        title_label = QLabel(title)
        self.style_card_title(icon_label, title_label, accent_color)
        if title_attr:
            setattr(self, title_attr, title_label)

        top_row.addWidget(icon_label)
        top_row.addWidget(title_label, 1)

        value_label.setMinimumHeight(48)
        value_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        value_label.setAlignment(Qt.AlignCenter)
        value_label.setStyleSheet(f"""
            font-size: 18px;
            font-weight: bold;
            color: {accent_color if title != 'TOTAL COST' else '#32ff7e'};
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 8px;
            padding: 0px;
        """)

        subtitle_label = QLabel(subtitle)
        subtitle_label.setAlignment(Qt.AlignCenter)
        subtitle_label.setStyleSheet("""
            font-size: 12px;
            color: #a7b4c2;
        """)
        if subtitle_attr:
            setattr(self, subtitle_attr, subtitle_label)

        layout.addLayout(top_row)
        layout.addWidget(value_label)
        layout.addWidget(subtitle_label)

        frame.setLayout(layout)

        return frame

    def create_metric_label_row(self, left_text, right_label, accent_color="#ffffff", bold=False, font_size=12,
                                store_as=None, left_color=None):

        row = QHBoxLayout()
        row.setContentsMargins(0, 0, 0, 0)
        row.setSpacing(6)

        left = QLabel(left_text)
        if store_as:
            setattr(self, store_as, left)

        weight = "bold" if bold else "normal"

        left.setMinimumWidth(95)
        left.setFixedHeight(21)
        left.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        _left_color = left_color if left_color else "#d7e6f5"
        left.setStyleSheet(f"""
            font-size: {font_size}px;
            font-weight: {weight};
            color: {_left_color};
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 3px;
            padding: 2px 4px;
        """)

        right_label.setMinimumWidth(95)
        right_label.setFixedHeight(21)
        right_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        right_label.setStyleSheet(f"""
            font-size: {font_size}px;
            font-weight: {weight};
            color: {accent_color};
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 3px;
            padding: 2px 4px;
        """)

        row.addWidget(left, 3)
        row.addWidget(right_label, 2)

        return row

    def style_card_title(self, icon_label, title_label, accent_color):
        _txt = title_label.text()
        _fs = 13 if len(_txt) <= 8 else 11

        icon_label.setFixedSize(24, 24)
        icon_label.setAlignment(Qt.AlignCenter)
        icon_label.setStyleSheet(f"""
            font-size: {_fs}px;
            font-weight: bold;
            color: {accent_color};
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 6px;
        """)

        title_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        title_label.setStyleSheet(f"""
            font-size: {_fs}px;
            font-weight: bold;
            color: {accent_color};
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 6px;
            padding: 2px 4px;
        """)

    def create_bar_mix_card(self):
        """KPI card TOTAL BARS — estilo uniforme com os outros 5 cards."""
        frame = QFrame()
        frame.setMinimumHeight(165)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(5)

        top_row = QHBoxLayout()
        icon_label  = QLabel("≡")
        title_label = QLabel("TOTAL BARS")
        self._lbl_total_bars = title_label
        self.style_card_title(icon_label, title_label, "#a66bff")
        top_row.addWidget(icon_label)
        top_row.addWidget(title_label)
        top_row.addStretch()

        self.bar_mix_big_value = QLabel("-")
        self.bar_mix_big_value.setMinimumHeight(48)
        self.bar_mix_big_value.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.bar_mix_big_value.setAlignment(Qt.AlignCenter)
        self.bar_mix_big_value.setStyleSheet("""
            font-size: 26px; font-weight: bold;
            color: #a66bff; background-color: #091017;
            border: 2px solid #3a1a60; border-radius: 7px;
        """)

        bars_sub = QLabel("bars")
        self._lbl_bars_sub = bars_sub
        bars_sub.setAlignment(Qt.AlignCenter)
        bars_sub.setStyleSheet("font-size: 12px; color: #a7b4c2;")

        # Compatibilidade com update_bar_mix_preview — cells e total preenchidos
        # pela create_bar_detail_frame (chamada após esta, no setup_ui)
        self.bar_mix_cells = []
        self.bar_mix_total = QLabel("")
        self.bar_mix_total.hide()

        layout.addLayout(top_row)
        layout.addWidget(self.bar_mix_big_value)
        layout.addWidget(bars_sub)
        layout.addStretch()

        frame.setLayout(layout)
        return frame

    def _create_cost_breakdown_card(self):
        """Card KPI 0 — COST BREAKDOWN: 3 barras horizontais (Yield/Stock/Scrap)."""
        frame = QFrame()
        frame.setMinimumHeight(204)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: 1px solid #1e2e3e;
                border-radius: 10px;
            }
        """)
        layout = QVBoxLayout()
        layout.setContentsMargins(8, 8, 8, 6)
        layout.setSpacing(4)

        # Linha de título
        title_row = QHBoxLayout()
        title_row.setSpacing(6)
        icon_lbl = QLabel("◎")
        icon_lbl.setFixedSize(24, 24)
        icon_lbl.setAlignment(Qt.AlignCenter)
        icon_lbl.setStyleSheet(
            "font-size:11px; color:#a66bff; background:#091017;"
            "border:1px solid #25415a; border-radius:6px;"
        )
        self._cb_icon_label = icon_lbl
        title_lbl = QLabel("COST BREAKDOWN")
        self._lbl_cost_breakdown_card = title_lbl
        title_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        title_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        title_lbl.setStyleSheet(
            "font-size:15px; font-weight:bold; color:#a66bff; background:#091017;"
            "border:1px solid #25415a; border-radius:6px; padding:2px 4px;"
        )
        title_row.addWidget(icon_lbl)
        title_row.addWidget(title_lbl, 1)
        layout.addLayout(title_row)

        # Sub-label: "Material comprado" (legenda do total)
        material_lbl = QLabel("Material comprado")
        self._cb_material_label = material_lbl
        material_lbl.setAlignment(Qt.AlignCenter)
        material_lbl.setStyleSheet(
            "color:#a66bff; font-size:10px; font-weight:bold;"
            "background:transparent; border:none;"
        )
        layout.addWidget(material_lbl)

        # Label do total — font_size aplicado por _apply_ui_config via JSON total_font_size_px
        total_lbl = QLabel(self.format_money(0))
        total_lbl.setAlignment(Qt.AlignCenter)
        total_lbl.setMinimumHeight(34)
        total_lbl.setStyleSheet(
            "background:#091017; color:#a66bff; border:1px solid #25415a;"
            "border-radius:6px; font-size:23px; font-weight:bold;"
        )
        self._cb_total_label = total_lbl
        layout.addWidget(total_lbl)

        # 4 barras: Líquido do projeto / Peças / Retalho p/ Estoque / Sucata/Descarte
        _bar_defs = [
            ("Líquido do projeto",  "#32ff7e"),
            ("Peças",               "#7ec8e3"),
            ("Retalho p/ Estoque",  "#4d8dff"),
            ("Sucata/Descarte",     "#ff7a2f"),
        ]
        self._cb_bar_widgets = []
        for lbl_txt, color in _bar_defs:
            pb = QProgressBar()
            pb.setRange(0, 100); pb.setValue(0); pb.setTextVisible(False)
            pb.setFixedHeight(14)
            pb.setStyleSheet(
                f"QProgressBar{{background:#091017;border:1px solid #25415a;border-radius:3px;}}"
                f"QProgressBar::chunk{{background:{color};border-radius:2px;}}"
            )
            lbl = QLabel(f"{lbl_txt}  –  {self.format_money(0)}")
            # font-size aplicado por _apply_ui_config via JSON bar_label_font_size_px
            lbl.setStyleSheet(
                f"color:{color};font-size:11px;font-weight:bold;"
                "background:transparent;border:none;padding-left:2px;"
            )
            self._cb_bar_widgets.append((pb, lbl, color, lbl_txt))
            layout.addWidget(pb)
            layout.addWidget(lbl)
            layout.addSpacing(2)

        layout.addStretch()
        frame.setLayout(layout)
        return frame

    def _update_cost_breakdown_bars(self, net_val=0.0, pieces_val=0.0, stock_val=0.0,
                                    scrap_val=0.0, material_val=0.0,
                                    net_lbl=None, pieces_lbl=None, stock_lbl=None,
                                    scrap_lbl=None, material_lbl=None):
        """Atualiza 4 barras horizontais do COST BREAKDOWN."""
        if not hasattr(self, "_cb_bar_widgets"):
            return
        values = [net_val, pieces_val, stock_val, scrap_val]
        labels = [
            net_lbl    or "Líquido do projeto",
            pieces_lbl or "Peças",
            stock_lbl  or "Retalho p/ Estoque",
            scrap_lbl  or "Sucata/Descarte",
        ]
        max_val = max(values) if any(v > 0 for v in values) else 1
        for (pb, lbl, color, _def), val, lbl_txt in zip(self._cb_bar_widgets, values, labels):
            pct = int(val / max_val * 100) if max_val > 0 else 0
            pb.setValue(pct)
            lbl.setText(f"{lbl_txt}  –  {self.format_money(val)}")
        if hasattr(self, "_cb_total_label"):
            self._cb_total_label.setText(self.format_money(material_val))
        if hasattr(self, "_cb_material_label"):
            self._cb_material_label.setText(material_lbl or "Material comprado")
        self._cb_raw_values = (net_val, pieces_val, stock_val, scrap_val)
        self._cb_material_value = material_val

    def _create_total_bars_box(self):
        """Caixa TOTAL BARS — fundo da coluna esquerda (v16)."""
        frame = QFrame()
        frame.setFixedHeight(64)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d0a1e;
                border: 1px solid #3a1a60;
                border-radius: 8px;
            }
        """)
        layout = QHBoxLayout()
        layout.setContentsMargins(8, 6, 8, 6)
        layout.setSpacing(8)

        # Ícone
        icon_lbl = QLabel("≡")
        icon_lbl.setFixedSize(32, 32)
        icon_lbl.setAlignment(Qt.AlignCenter)
        icon_lbl.setStyleSheet(
            "font-size:14px; color:#a66bff; background:#060e16;"
            "border:1px solid #1e3050; border-radius:7px;"
        )
        self._total_bars_icon = icon_lbl

        # Rótulos
        lbl_col = QVBoxLayout()
        lbl_col.setSpacing(0)
        lbl_col.setContentsMargins(0, 0, 0, 0)
        self._lbl_total_bars_section = QLabel("TOTAL BARS")
        self.style_card_title(icon_lbl, self._lbl_total_bars_section, "#a66bff")
        self._lbl_bars_in_project = QLabel("bars in project")
        self._lbl_bars_in_project.setStyleSheet(
            "font-size:10px; color:#5a4a7a; background:transparent; border:none;"
        )
        lbl_col.addWidget(self._lbl_total_bars_section)
        lbl_col.addWidget(self._lbl_bars_in_project)

        # Caixa de valor
        val_frame = QFrame()
        val_frame.setFixedSize(99, 50)
        val_frame.setStyleSheet(
            "QFrame { background:#060e16; border:2px solid #a66bff; border-radius:8px; }"
        )
        self._total_bars_val_frame = val_frame
        val_layout = QVBoxLayout()
        val_layout.setContentsMargins(2, 2, 2, 2)
        self._lbl_bars_count = QLabel("0")
        self._lbl_bars_count.setAlignment(Qt.AlignCenter)
        self._lbl_bars_count.setStyleSheet(
            "font-size:26px; font-weight:bold; color:#a66bff; background:transparent; border:none;"
        )
        val_layout.addWidget(self._lbl_bars_count)
        val_frame.setLayout(val_layout)

        layout.addWidget(icon_lbl, 0, Qt.AlignVCenter)
        layout.addLayout(lbl_col, 1)
        layout.addWidget(val_frame, 0, Qt.AlignVCenter)
        frame.setLayout(layout)
        return frame

    def create_bar_detail_frame(self):
        """Frame separado com tabela Cut Group / Qty / Solution ID / Yield."""
        frame = QFrame()
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(8, 4, 8, 4)
        layout.setSpacing(3)

        title_row = QHBoxLayout()
        icon_lbl = QLabel("≡")
        icon_lbl.setAlignment(Qt.AlignCenter)
        detail_title = QLabel("BAR DETAIL")
        self._lbl_bar_detail_title = detail_title
        self._icon_bar_detail = icon_lbl
        self.style_card_title(icon_lbl, detail_title, "#a66bff")
        title_row.addWidget(icon_lbl)
        title_row.addWidget(detail_title)
        title_row.addStretch()

        table = QTableWidget(8, 4)
        table.setHorizontalHeaderLabels(["Cut Group", "Qty", "Solution ID", "Yield"])
        table.verticalHeader().setVisible(False)
        table.setAlternatingRowColors(True)
        table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        table.setSelectionMode(QAbstractItemView.NoSelection)
        table.setFocusPolicy(Qt.NoFocus)
        table.setShowGrid(True)
        table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
        table.horizontalHeader().setStretchLastSection(True)
        table.setColumnWidth(0, 95)
        table.setColumnWidth(1, 36)
        table.setColumnWidth(2, 120)
        table.verticalHeader().setDefaultSectionSize(22)
        table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        table.setStyleSheet("""
            QTableWidget {
                background-color: #3A3838;
                alternate-background-color: #2E2C2C;
                border: 1px solid #848C6E;
                gridline-color: #5A5858;
                color: #ffffff;
                font-size: 13px;
                font-weight: bold;
            }
            QTableWidget::item {
                padding: 4px;
                color: #ffffff;
                font-weight: bold;
            }
            QHeaderView::section {
                background-color: #1f557f;
                color: white;
                padding: 6px;
                border: 1px solid #848C6E;
                font-weight: bold;
                font-size: 13px;
            }
        """)

        self.bar_mix_cells = []
        for row in range(8):
            row_cells = []
            for col in range(4):
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignCenter)
                table.setItem(row, col, item)
                row_cells.append(item)
            self.bar_mix_cells.append(row_cells)

        self._bar_table = table

        layout.addLayout(title_row)
        layout.addWidget(table)
        frame.setLayout(layout)
        return frame

    def create_pieces_summary_card(self):

        frame = QFrame()
        frame.setMinimumHeight(165)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(5)

        top_row = QHBoxLayout()

        icon_label = QLabel("◈")
        title_label = QLabel("TOTAL PIECES")
        self._lbl_total_pieces = title_label
        self.style_card_title(icon_label, title_label, "#45d9ff")

        self.pieces_qty_value = QLabel("-")
        self.pieces_cost_value = QLabel("$ 0.00")

        self.pieces_qty_value.setMinimumHeight(48)
        self.pieces_qty_value.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.pieces_qty_value.setAlignment(Qt.AlignCenter)
        self.pieces_qty_value.setStyleSheet("""
            font-size: 26px;
            font-weight: bold;
            color: #45d9ff;
            background-color: #091017;
            border: 2px solid #45d9ff;
            border-radius: 7px;
            padding: 0px;
        """)

        top_row.addWidget(icon_label)
        top_row.addWidget(title_label, 1)

        # pieces_cost_value mantido mas fora do layout (atualizado em update_pieces_summary)
        self.pieces_cost_value.setMinimumHeight(36)
        self.pieces_cost_value.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.pieces_cost_value.setAlignment(Qt.AlignCenter)
        self.pieces_cost_value.setStyleSheet("""
            font-size: 18px;
            font-weight: bold;
            color: #45d9ff;
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 8px;
            padding: 0px;
        """)

        qty_label = QLabel("Pieces")
        self._lbl_pieces_sub = qty_label
        qty_label.setAlignment(Qt.AlignCenter)
        qty_label.setStyleSheet("font-size: 11px; color: #a7b4c2;")

        # Parte de baixo: YIELD CUT (86.39%) em destaque
        self.yield_cut_value.setMinimumHeight(44)
        self.yield_cut_value.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.yield_cut_value.setAlignment(Qt.AlignCenter)
        self.yield_cut_value.setStyleSheet("""
            font-size: 30px;
            font-weight: bold;
            color: #45d9ff;
            background-color: #091017;
            border: 1px solid #45d9ff;
            border-radius: 7px;
            padding: 0px;
        """)

        yield_cut_label = QLabel("YIELD CUT")
        self._lbl_yield_cut_sub = yield_cut_label
        yield_cut_label.setAlignment(Qt.AlignCenter)
        yield_cut_label.setStyleSheet("font-size: 11px; color: #45d9ff; font-weight: bold;")

        layout.addLayout(top_row)
        layout.addWidget(self.pieces_qty_value)
        layout.addWidget(qty_label)
        layout.addWidget(self.yield_cut_value)
        layout.addWidget(yield_cut_label)
        layout.addStretch()

        frame.setLayout(layout)

        return frame

    def create_waste_summary_card(self):

        frame = QFrame()
        frame.setMinimumHeight(165)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(4)

        top_row = QHBoxLayout()

        icon_label = QLabel("◇")
        title_label = QLabel("TOTAL WASTE")
        self._lbl_total_waste = title_label
        self.style_card_title(icon_label, title_label, "#ffffff")

        top_row.addWidget(icon_label)
        top_row.addWidget(title_label, 1)

        self.waste_percent_value = QLabel("- %")
        self.waste_percent_value.setFixedHeight(40)
        self.waste_percent_value.setAlignment(Qt.AlignCenter)
        self.waste_percent_value.setStyleSheet("""
            font-size: 25px;
            font-weight: bold;
            color: #ffffff;
            background-color: #091017;
            border: 1px solid #25415a;
            border-radius: 7px;
            padding: 0px;
        """)

        self.waste_reusable_value = QLabel("$ 0.00")
        self.waste_scrap_value = QLabel("$ 0.00")
        self.waste_total_value = QLabel("$ 0.00")

        layout.addLayout(top_row)
        layout.addWidget(self.waste_percent_value)
        layout.addLayout(
            self.create_metric_label_row(
                "Stock Reusable",
                self.waste_reusable_value,
                "#4d8dff",
                True,
                font_size=13,
                store_as="_lbl_stock_reusable_sub",
                left_color="#4d8dff",
            )
        )
        layout.addLayout(
            self.create_metric_label_row(
                "Scrap/Junkyard",
                self.waste_scrap_value,
                "#ff7a2f",
                True,
                font_size=13,
                store_as="_lbl_scrap_junkyard_sub",
                left_color="#ff7a2f",
            )
        )
        layout.addLayout(
            self.create_metric_label_row(
                "Total",
                self.waste_total_value,
                "#ffffff",
                True,
                font_size=13,
            )
        )

        frame.setLayout(layout)

        return frame

    def create_control_status_card(self):

        frame = QFrame()
        frame.setMinimumHeight(165)
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: none;
                border-radius: 10px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(5)

        top_row = QHBoxLayout()

        icon_label = QLabel("☑")
        title_label = QLabel("CONTROL STATUS")
        self._lbl_control_status = title_label
        self.style_card_title(icon_label, title_label, "#4d8dff")

        top_row.addWidget(icon_label)
        top_row.addWidget(title_label, 1)

        self.audit_value = QLabel("Waiting")
        self.uncalculated_value = QLabel("0 pieces")
        self.runtime_value = QLabel("-")

        layout.addLayout(top_row)

        layout.addLayout(
            self.create_metric_label_row(
                "Audit Status",
                self.audit_value,
                "#4d8dff",
                True,
                font_size=13,
                store_as="_lbl_audit_status_sub",
            )
        )

        layout.addLayout(
            self.create_metric_label_row(
                "Uncalculated",
                self.uncalculated_value,
                "#ff4d4d",
                True,
                font_size=13,
                store_as="_lbl_uncalculated_sub",
            )
        )

        layout.addLayout(
            self.create_metric_label_row(
                "Run Time",
                self.runtime_value,
                "#ffbf3f",
                True,
                font_size=13,
                store_as="_lbl_run_time_sub",
            )
        )

        note_label = QLabel("Diagnostic indicators")
        self._lbl_diagnostic_sub = note_label
        note_label.setAlignment(Qt.AlignCenter)
        note_label.setStyleSheet("""
            font-size: 11px;
            color: #a7b4c2;
            padding-top: 4px;
        """)

        layout.addWidget(note_label)

        frame.setLayout(layout)

        return frame

    def create_config_box(self, title, description, widget):

        frame = QFrame()
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: 1px solid #25415a;
                border-radius: 8px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(5)

        title_label = QLabel(title)
        title_label.setStyleSheet("""
            font-size: 12px;
            font-weight: bold;
            color: #ffffff;
        """)

        desc_label = QLabel(description)
        desc_label.setStyleSheet("""
            font-size: 11px;
            color: #a7b4c2;
        """)

        layout.addWidget(title_label)
        layout.addWidget(desc_label)
        layout.addWidget(widget)

        frame.setLayout(layout)

        return frame

    def create_config_info(self, title, description, widget):

        frame = self.create_config_box(title, description, widget)
        return frame


    def create_small_config_input(self, title, widget):

        frame = QFrame()

        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: 1px solid #25415a;
                border-radius: 8px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(8, 6, 8, 6)
        layout.setSpacing(4)

        title_label = QLabel(title)

        title_label.setStyleSheet("""
            font-size: 11px;
            font-weight: bold;
            color: #ffffff;
        """)

        layout.addWidget(title_label)
        layout.addWidget(widget)

        frame.setLayout(layout)

        return frame

    def create_config_note(self, title, description):

        frame = QFrame()
        frame.setStyleSheet("""
            QFrame {
                background-color: #0d141c;
                border: 1px solid #25415a;
                border-radius: 8px;
            }
        """)

        layout = QVBoxLayout()
        layout.setContentsMargins(10, 8, 10, 8)

        title_label = QLabel(title)
        title_label.setStyleSheet("""
            font-size: 12px;
            font-weight: bold;
            color: #5ee7ff;
        """)

        desc_label = QLabel(description)
        desc_label.setWordWrap(True)
        desc_label.setStyleSheet("""
            font-size: 11px;
            color: #a7b4c2;
        """)

        layout.addWidget(title_label)
        layout.addWidget(desc_label)
        layout.addStretch()

        frame.setLayout(layout)

        return frame

    def create_box(self, title, value_label):

        return self.create_kpi_card(
            "●",
            title,
            value_label,
            "",
            "#4d8dff",
        )

    def load_imperial_units(self):

        self.output_display_combo.clear()

        self.bar_input_combo.setText("ft")
        self.piece_input_combo.setText("in")

        if hasattr(self, "fixed_units_label"):
            self.fixed_units_label.setText("Bars: ft   |   Pieces: in")

        self.output_display_combo.addItems([
            "Decimal inches",
            "Fractional inches",
            "Feet and fractional inches",
            "Decimal feet",
        ])

    def load_metric_units(self):

        self.output_display_combo.clear()

        self.bar_input_combo.setText("m")
        self.piece_input_combo.setText("cm")

        if hasattr(self, "fixed_units_label"):
            self.fixed_units_label.setText("Bars: m   |   Pieces: cm")

        self.output_display_combo.addItems([
            "m",
            "cm",
            "mm",
        ])

    def update_unit_system(self):

        current_system = self.input_system_combo.currentText()

        if current_system == "IMPERIAL":

            self.load_imperial_units()

            if hasattr(self, "project_system_label"):
                self.set_strip_value(
                    self.project_system_label,
                    "IMPERIAL"
                )

            if hasattr(self, "project_units_label"):
                self.set_strip_value(
                    self.project_units_label,
                    "Bars: ft    |    Pieces: in"
                )

        else:

            self.load_metric_units()

            if hasattr(self, "project_system_label"):
                self.set_strip_value(
                    self.project_system_label,
                    "METRIC"
                )

            if hasattr(self, "project_units_label"):
                self.set_strip_value(
                    self.project_units_label,
                    "Bars: m    |    Pieces: cm"
                )

        if hasattr(self, "kerf_input") and hasattr(self, "tolerance_input"):
            default_kerf, default_tolerance = self.get_default_kerf_and_tolerance()
            self.kerf_input.setText(default_kerf)
            self.tolerance_input.setText(default_tolerance)

        self.update_output_display_label()

        if hasattr(self, "settings_summary_label"):
            self.update_settings_summary()

        self.update_manual_table_headers()
        self.reformat_manual_table_cells()
        # Recarregar valores F-J e K-N do CRITERIOS com a nova unidade (não só relabelling)
        self._reload_criterios_into_table()

    def update_output_display_label(self):

        if not hasattr(self, "project_output_label"):
            return

        value = str(
            self.output_display_combo.currentText() or ""
        ).strip()

        self.set_strip_value(
            self.project_output_label,
            value
        )

        if hasattr(self, "settings_summary_label"):
            self.update_settings_summary()


    def get_current_length_unit_for_settings(self):

        if self.input_system_combo.currentText() == "IMPERIAL":
            return "in"

        return "mm"

    def get_default_kerf_and_tolerance(self):

        if self.input_system_combo.currentText() == "IMPERIAL":
            return "0.125", "0.010"

        return "3", "1"

    def update_settings_summary(self):

        if not hasattr(self, "settings_summary_label"):
            return

        unit_system = str(
            self.input_system_combo.currentText() or ""
        ).strip()

        bar_unit = str(
            self.bar_input_combo.text() or ""
        ).strip()

        piece_unit = str(
            self.piece_input_combo.text() or ""
        ).strip()

        output_display = str(
            self.output_display_combo.currentText() or ""
        ).strip()

        kerf = str(
            self.kerf_input.text() or ""
        ).strip()

        tolerance = str(
            self.tolerance_input.text() or ""
        ).strip()

        cut_unit = self.get_current_length_unit_for_settings()

        self.settings_summary_label.setText(
            f"{unit_system}  |  Bars: {bar_unit}  |  "
            f"Pieces: {piece_unit}  |  Output: {output_display}  |  "
            f"Kerf: {kerf} {cut_unit}  |  Tolerance: {tolerance} {cut_unit}"
        )

    def open_settings_dialog(self):
        try:
            _lm = _load_engine_module("language_manager")
            _t = _lm.t
        except Exception:
            _t = lambda k: k

        dialog = QDialog(self)
        dialog.setWindowTitle(_t("settings_dialog_title"))
        dialog.resize(520, 320)

        dialog.setStyleSheet(self.styleSheet())

        layout = QVBoxLayout()
        layout.setContentsMargins(18, 18, 18, 18)
        layout.setSpacing(12)

        title = QLabel(_t("settings_dialog_header"))
        title.setStyleSheet("""
            font-size: 18px;
            font-weight: bold;
            color: #4d8dff;
        """)

        unit_combo = QComboBox()
        unit_combo.addItems([
            "IMPERIAL",
            "METRIC"
        ])
        unit_combo.setCurrentText(
            self.input_system_combo.currentText()
        )

        output_combo = QComboBox()

        def refresh_output_options():

            current_output = self.output_display_combo.currentText()

            output_combo.clear()

            if unit_combo.currentText() == "IMPERIAL":
                output_combo.addItems([
                    _t("settings_out_decimal_inches"),
                    _t("settings_out_fractional_inches"),
                    _t("settings_out_feet_fractional"),
                    _t("settings_out_decimal_feet"),
                ])
            else:
                output_combo.addItems([
                    "m",
                    "cm",
                    "mm",
                ])

            if current_output in [
                output_combo.itemText(i)
                for i in range(output_combo.count())
            ]:
                output_combo.setCurrentText(current_output)

        refresh_output_options()

        kerf_edit = QLineEdit()
        kerf_edit.setText(self.kerf_input.text())

        tolerance_edit = QLineEdit()
        tolerance_edit.setText(self.tolerance_input.text())

        fixed_units_preview = QLabel("")
        fixed_units_preview.setStyleSheet("""
            font-size: 13px;
            color: #32ff7e;
            background-color: #0e2a20;
            border: 1px solid #245c43;
            border-radius: 6px;
            padding: 10px;
        """)

        _u = self.get_current_length_unit_for_settings()

        def update_preview():

            if unit_combo.currentText() == "IMPERIAL":
                fixed_units_preview.setText(_t("settings_rules_imperial"))
            else:
                fixed_units_preview.setText(_t("settings_rules_metric"))

        update_preview()

        unit_combo.currentTextChanged.connect(
            lambda value: (
                refresh_output_options(),
                update_preview()
            )
        )

        grid = QGridLayout()
        grid.setHorizontalSpacing(10)
        grid.setVerticalSpacing(10)

        grid.addWidget(QLabel(_t("settings_unit_system_lbl")), 0, 0)
        grid.addWidget(unit_combo, 0, 1)

        grid.addWidget(QLabel(_t("settings_output_display_lbl")), 1, 0)
        grid.addWidget(output_combo, 1, 1)

        grid.addWidget(QLabel(f"{_t('settings_kerf_lbl')} ({_u})"), 2, 0)
        grid.addWidget(kerf_edit, 2, 1)

        grid.addWidget(QLabel(f"{_t('settings_tolerance_lbl')} ({_u})"), 3, 0)
        grid.addWidget(tolerance_edit, 3, 1)

        grid.addWidget(QLabel(_t("settings_fixed_rules_lbl")), 4, 0)
        grid.addWidget(fixed_units_preview, 4, 1)

        buttons = QHBoxLayout()

        cancel_button = QPushButton(_t("btn_cancel"))
        save_button = QPushButton(_t("btn_save_settings"))

        cancel_button.clicked.connect(dialog.reject)

        def apply_settings():

            self.input_system_combo.setCurrentText(
                unit_combo.currentText()
            )

            self.output_display_combo.setCurrentText(
                output_combo.currentText()
            )

            self.kerf_input.setText(
                kerf_edit.text()
            )

            self.tolerance_input.setText(
                tolerance_edit.text()
            )

            self.update_settings_summary()

            if self.selected_input_file:
                if not self.sync_ui_config_to_input():
                    return

            dialog.accept()

        save_button.clicked.connect(apply_settings)

        buttons.addStretch()
        buttons.addWidget(cancel_button)
        buttons.addWidget(save_button)

        layout.addWidget(title)
        layout.addLayout(grid)
        layout.addStretch()
        layout.addLayout(buttons)

        dialog.setLayout(layout)
        dialog.exec()


    def set_input_status(self, text, color):

        self.input_status_label.setText(text)

        self.input_status_label.setStyleSheet(f"""
            font-size: 12px;
            color: {color};
            padding-left: 10px;
        """)

    def select_input_file(self):

        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Select Input File",
            "",
            "Excel Files (*.xlsx);;CSV Files (*.csv)",
        )

        if file_path:

            self.selected_input_file = file_path
            self._current_template_path = None   # usuário selecionou arquivo manual → sem template ativo

            file_name = os.path.basename(file_path)

            self.selected_input_label.setText(file_name)

            self.log(
                f"INPUT FILE SELECTED: {file_name}"
            )

            self.load_input_file_preview()

            try:
                self.validate_xlsx_input()
                self.input_is_valid = True
                self.set_input_status("INPUT STATUS: VALID", "#00ff99")
                self.log("INPUT READY FOR PROCESSING")
            except Exception as e:
                self.input_is_valid = False
                self.set_input_status("INPUT STATUS: INVALID", "#ff5555")
                self.log(f"INPUT VALIDATION WARNING: {str(e)}")

            # Carregar tabela da UI automaticamente ao selecionar novo INPUT
            try:
                from openpyxl import load_workbook as _lw_auto
                _wb_auto = _lw_auto(file_path, data_only=True)
                _ws_auto = _wb_auto["INPUT"] if "INPUT" in _wb_auto.sheetnames else _wb_auto.active
                self.load_manual_input_table_from_input(_ws_auto)
                _wb_auto.close()
                self.log(f"UI LOADED FROM: {file_name}")
            except Exception as _e_auto:
                self.log(f"AUTO-LOAD WARNING: {str(_e_auto)}")

    def validate_selected_input(self):

        self.log_panel.clear()

        if not self.selected_input_file:

            QMessageBox.warning(
                self,
                "INPUT VALIDATION",
                "No input file selected.",
            )

            return

        try:

            self.validate_xlsx_input()

            self.input_is_valid = True

            self.set_input_status(
                "INPUT STATUS: VALID",
                "#00ff99"
            )

            self.log(
                "INPUT VALIDATION PASSED"
            )

            QMessageBox.information(
                self,
                "INPUT VALIDATION",
                "Input file validated successfully.",
            )

        except Exception as e:

            self.input_is_valid = False

            self.set_input_status(
                "INPUT STATUS: INVALID",
                "#ff5555"
            )

            self.log(
                f"INPUT VALIDATION FAILED: {str(e)}"
            )

            QMessageBox.critical(
                self,
                "INPUT VALIDATION",
                str(e),
            )

    def validate_xlsx_input(self):

        self.log("VALIDATING XLSX INPUT...")

        input_file = self._resolve_input_file()
        wb = load_workbook(
            input_file,
            data_only=True,
        )

        required_sheets = [
            "INPUT",
        ]

        for sheet_name in required_sheets:

            if sheet_name not in wb.sheetnames:

                raise ValueError(
                    f"Missing required sheet: {sheet_name}"
                )

        ws = wb["INPUT"]

        project_name = ws["B1"].value
        unit_system = ws["B2"].value

        if not project_name:

            raise ValueError(
                "Project_Name is required."
            )

        if not unit_system:

            raise ValueError(
                "Unit_System is required."
            )

        row = 13

        found_piece = False

        while True:

            cut_group = ws[f"B{row}"].value
            length = ws[f"D{row}"].value
            qty = ws[f"E{row}"].value
            stock_1 = ws[f"F{row}"].value

            if (
                cut_group is None and
                length is None and
                qty is None
            ):
                break

            found_piece = True

            if not cut_group:

                raise ValueError(
                    f"Cut Group required at row {row}"
                )

            if length in (None, ""):

                raise ValueError(
                    f"Length required at row {row}"
                )

            if qty in (None, ""):

                raise ValueError(
                    f"Qty required at row {row}"
                )

            if stock_1 in (None, ""):

                raise ValueError(
                    f"Bar Stock 1 required at row {row}"
                )

            row += 1

        if not found_piece:

            raise ValueError(
                "No valid rows found."
            )

        system = self.input_system_combo.currentText()

        if system == "IMPERIAL":

            self.log(
                "UNIT RULE: bars=ft | pieces=in"
            )

        else:

            self.log(
                "UNIT RULE: bars=m | pieces=cm"
            )

    def validate_csv_input(self):
        pass

    def log(self, message):

        self.log_panel.append(message)

        QApplication.processEvents()

    def update_bar_mix_preview(self, rows=None, total=None):

        rows = rows or []

        unit_system = ""
        if hasattr(self, "input_system_combo"):
            unit_system = self.input_system_combo.currentText().upper()
        bar_unit = "ft" if unit_system == "IMPERIAL" else "m"

        if hasattr(self, "_bar_table") and hasattr(self, "bar_mix_cells"):
            table   = self._bar_table
            n_need  = max(len(rows), 8)   # mínimo 8 para visual
            if table.rowCount() != n_need:
                table.setRowCount(n_need)
                # Reconstrói referências de células para todas as linhas
                self.bar_mix_cells = []
                for _r in range(n_need):
                    _row_cells = []
                    for _c in range(4):
                        _it = table.item(_r, _c)
                        if _it is None:
                            _it = QTableWidgetItem("")
                            _it.setTextAlignment(Qt.AlignCenter)
                            table.setItem(_r, _c, _it)
                        _row_cells.append(_it)
                    self.bar_mix_cells.append(_row_cells)

            for index, cells in enumerate(self.bar_mix_cells):
                if index < len(rows):
                    row_data  = rows[index]
                    cut_group = row_data[0]
                    qty       = row_data[1]
                    size      = row_data[2]
                    yield_pct = row_data[3] if len(row_data) > 3 else ""
                    # size já vem como Solution ID pronto (ex: "(4.70 ft x3)")
                    size_str = str(size).strip()
                    values = [str(cut_group), str(qty), size_str, str(yield_pct)]
                else:
                    values = ["", "", "", ""]
                for i, cell in enumerate(cells):
                    if i < len(values):
                        cell.setText(values[i])

        display_total = total if total is not None else "-"

        if hasattr(self, "bar_mix_big_value"):
            self.bar_mix_big_value.setText(str(display_total))

        if hasattr(self, "bar_mix_total"):
            self.bar_mix_total.setText(f"TOTAL BARS: {display_total}")

        # v16: atualiza caixa TOTAL BARS na coluna esquerda
        if hasattr(self, "_lbl_bars_count"):
            self._lbl_bars_count.setText(str(display_total))

    def _refresh_available_stock_display(self):
        """Atualiza o campo 'Stock Reusable' (waste_reusable_value) com SUM(value) AVAILABLE do SQLite.

        Propaga para o card WASTE (sempre visível) — filosofia: Stock Reusable = inventário vivo.
        Chamado em startup, pós-pipeline e após adição manual de barras.
        """
        try:
            import sqlite3 as _sq3, os as _os, sys as _sys
            # Modo exe (PyInstaller COLLECT): sys.executable = engine/EngCut.exe
            _exe_dir = _os.path.dirname(_os.path.abspath(_sys.executable))
            _db = _os.path.join(_exe_dir, "database", "stock_database.db")
            # Fallback dev: __file__ = engine/front_end/engcut_ui.py → subir 2 níveis
            if not _os.path.exists(_db):
                _db = _os.path.join(
                    _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))),
                    "database", "stock_database.db"
                )
            if not _os.path.exists(_db):
                return
            _con = _sq3.connect(_db)
            _cur = _con.cursor()
            _cur.execute(
                "SELECT COALESCE(SUM(value), 0.0) FROM stock WHERE status='AVAILABLE'"
            )
            _total = float(_cur.fetchone()[0] or 0)
            _con.close()
            _fmt = self.format_money(_total)
            # Propaga para o campo visível "Stock Reusable" do card WASTE
            if hasattr(self, "waste_reusable_value"):
                self.waste_reusable_value.setText(_fmt)
            # Recalcula total do card WASTE com o novo valor de estoque
            if hasattr(self, "waste_scrap_value") and hasattr(self, "waste_total_value"):
                _scrap = self.parse_display_number(self.waste_scrap_value.text())
                self.waste_total_value.setText(self.format_money(_total + _scrap))
            # Mantém stock_available_value sincronizado (usado por testes)
            if hasattr(self, "stock_available_value"):
                self.stock_available_value.setText(_fmt)
        except Exception:
            pass

    def _rebuild_template_menu(self, lm=None):
        import os as _os, glob as _glob, sys as _sys
        _STEM_KEYS = {
            "aluminum": "tmpl_aluminum",
            "outros":   "tmpl_outros",
            "pipe":     "tmpl_pipe",
            "steel":    "tmpl_steel",
            "wood":     "tmpl_wood",
        }
        _translations = {}
        _btn_default = "\U0001f4c2 TEMPLATE ▼"
        try:
            _lm = lm or _load_engine_module("language_manager")
            for _stem, _key in _STEM_KEYS.items():
                _translations[_stem] = _lm.t(_key)
            _btn_default = _lm.t("tmpl_button")
        except Exception:
            pass

        _menu = QMenu(self.template_button)
        _menu.setStyleSheet("""
            QMenu {
                background-color: #0f1c2a;
                border: 1px solid #2d5a8e;
                color: #a0c4f0;
                font-size: 13px;
                padding: 4px;
            }
            QMenu::item { padding: 8px 20px; border-radius: 4px; }
            QMenu::item:selected { background-color: #1e3a5f; color: #ffffff; }
        """)
        _exe_dir = _os.path.dirname(_os.path.abspath(_sys.executable))
        _inp_dir = _os.path.join(_exe_dir, "inputs")
        if not _os.path.isdir(_inp_dir):
            _inp_dir = _os.path.join(
                _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "inputs"
            )
        _files = sorted([
            f for f in _glob.glob(_os.path.join(_inp_dir, "input_*.xlsx"))
            if _os.path.basename(f).upper() != "INPUT_SISTEMA.XLSX"
        ])
        if _files:
            for _f in _files:
                _stem = _os.path.splitext(_os.path.basename(_f))[0].lower().replace("input_", "").replace("_", "")
                _lbl = _translations.get(_stem, _os.path.splitext(_os.path.basename(_f))[0].lower().replace("input_", "").replace("_", " ").title())
                _act = QAction(_lbl, self)
                _act.triggered.connect(lambda _chk=False, _p=_f, _l=_lbl, _s=_stem: self._load_input_template(_p, _l, _s))
                _menu.addAction(_act)
        else:
            _act = QAction("(nenhum template em inputs/)", self)
            _act.setEnabled(False)
            _menu.addAction(_act)
        self.template_button.setMenu(_menu)

        # Atualiza label do botão se já tem template selecionado
        _sel = getattr(self, "_current_template_stem", None)
        if _sel and _sel in _translations:
            self.template_button.setText(f"\U0001f4c2 {_translations[_sel].upper()} ▼")
        elif not _sel:
            self.template_button.setText(_btn_default)

    def _load_input_template(self, template_path, label, stem=""):
        import shutil as _sh, os as _os, sys as _sys
        try:
            _exe_dir = _os.path.dirname(_os.path.abspath(_sys.executable))
            _tgt_dir = _os.path.join(_exe_dir, "inputs")
            if not _os.path.isdir(_tgt_dir):
                _tgt_dir = _os.path.join(
                    _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "inputs"
                )
            _tgt = _os.path.join(_tgt_dir, "INPUT_sistema.xlsx")
            _sh.copy2(template_path, _tgt)
            self._current_template_stem = stem
            # BUG-FIX: armazenar caminho do template original para "open" e para limpeza de cache
            self._current_template_path = str(template_path)
            # Template = fonte de verdade: sync e pipeline escrevem/leem aqui diretamente.
            # INPUT_sistema.xlsx (_tgt) recebe uma cópia inicial para compatibilidade com
            # testes que rodam run_project.py sem argumento de arquivo.
            self.selected_input_file = str(template_path)
            self.template_button.setText(f"\U0001f4c2 {label.upper()} ▼")
            # Atualizar label do INPUT com o nome do template selecionado
            if hasattr(self, "selected_input_label"):
                self.selected_input_label.setText(_os.path.basename(str(template_path)))
            # Carregar preview completo: atualiza PROJETO/DESCRIÇÃO/CLIENTE/ENDEREÇO/DATA
            # + unidade/kerf/tolerance + tabela de cortes — tudo sincronizado ao template ativo.
            self.load_input_file_preview()
            self.update_bar_mix_preview()
        except Exception as _e:
            from PySide6.QtWidgets import QMessageBox
            QMessageBox.warning(self, "Template", f"Erro ao carregar template:\n{_e}")

    def update_pieces_summary(self, pieces=None, value=None):

        self.pieces_qty_value.setText(
            str(pieces) if pieces is not None else "-"
        )

        self.pieces_cost_value.setText(
            str(value) if value is not None else "$ 0.00"
        )

    def update_waste_summary(
        self,
        percent=None,
        reusable=None,
        scrap=None,
        total=None,
        raw_reusable=None,
        raw_scrap=None,
    ):

        self.waste_percent_value.setText(
            str(percent) if percent is not None else "- %"
        )

        self.waste_reusable_value.setText(
            str(reusable) if reusable is not None else "$ 0.00"
        )

        self.waste_scrap_value.setText(
            str(scrap) if scrap is not None else "$ 0.00"
        )

        self.waste_total_value.setText(
            str(total) if total is not None else "$ 0.00"
        )

        # Atualizar barra empilhada verde (reusável) vs laranja (scrap)
        if hasattr(self, "waste_bar") and raw_reusable is not None and raw_scrap is not None:
            _total_raw = float(raw_reusable or 0) + float(raw_scrap or 0)
            _val = int(float(raw_reusable or 0) / _total_raw * 100) if _total_raw > 0 else 0
            self.waste_bar.setValue(_val)


    def parse_display_number(self, value):

        if value in (None, "", "N/A", "-"):
            return 0.0

        if isinstance(value, (int, float)):
            return float(value)

        text = str(value).strip()
        text = text.replace("$", "")

        match = __import__("re").search(r"[-+]?\d[\d.,]*", text)
        if not match:
            return 0.0

        number = match.group(0)

        if "," in number and "." in number:
            if number.rfind(",") > number.rfind("."):
                number = number.replace(".", "").replace(",", ".")
            else:
                number = number.replace(",", "")
        elif "," in number:
            number = number.replace(",", ".")

        try:
            return float(number)
        except Exception:
            return 0.0

    def format_money(self, value):

        v = f"{float(value):,.2f}"
        v = v.replace(",", "X").replace(".", ",").replace("X", ".")
        return f"$ {v}"

    def format_percent(self, value):

        if value is None:
            return "- %"

        value = float(value)

        if value <= 1.0:
            value = value * 100.0

        return f"{value:.2f} %"

    def _on_pipeline_finished(self, elapsed, stdout, stderr, returncode):
        """Chamado pela thread quando o pipeline termina."""

        self.run_button.setEnabled(True)

        try:
            _lm = _load_engine_module("language_manager")
            def _t(k): return _lm.t(k)
        except Exception:
            def _t(k): return k

        if returncode != 0:
            if stderr:
                self.log(stderr)
            _s_err = _t("status_error")
            _s_log = _t("check_process_log")
            self.status_label.setText(f"STATUS: {_s_err}")
            if hasattr(self, "top_status_label"):
                self.top_status_label.setText(f"●  STATUS: {_s_err}\n{_s_log}")
            return

        self.runtime_value.setText(f"{elapsed:.2f}s")
        self.progress_bar.setValue(85)

        self.update_dashboard_from_outputs()

        # Auto-generate all 6 reports so they are always fresh after a successful run.
        try:
            _rg = _load_engine_module("report_generator")
            _rg.generate_project_summary()
            _rg.generate_purchase_order()
            _rg.generate_cutting_report(active_tab="SUMMARY")
            _rg.generate_stock_report(active_tab="STOCK")
            _rg.generate_cost_report(active_tab="COST SUMMARY")
            _rg.generate_qr_labels()
            self.log("Relatórios gerados automaticamente.")
        except Exception as _rg_ex:
            self.log(f"[WARN] Geração automática de relatórios: {_rg_ex}")

        # Atualizar INVENTORY.xlsx STATUS/PROJETO pós-pipeline (peças consumidas → UTILIZADO)
        try:
            self._refresh_inventory_status()
        except Exception:
            pass

        self.progress_bar.setValue(100)

        # Read audit result set by update_dashboard_from_outputs — UI does NOT recalculate
        _audit_text = self.audit_value.text() if hasattr(self, "audit_value") else ""
        _is_certified = "FAIL" not in _audit_text.upper()

        if _is_certified:
            _status_key = "status_certified"
            _subtitle_key = "audit_passed_subtitle"
            _msg_key = "project_certified"
        else:
            _status_key = "status_review_required"
            _subtitle_key = "audit_failed_subtitle"
            _msg_key = "project_review_required"

        _status_str = _t(_status_key)
        _subtitle_str = _t(_subtitle_key)

        self.status_label.setText(f"STATUS: {_status_str}")
        if hasattr(self, "top_status_label"):
            self.top_status_label.setText(f"●  STATUS: {_status_str}\n{_subtitle_str}")

        self.log(f"PROJECT {_status_str}")

        QMessageBox.information(self, "ENGCUT", _t(_msg_key))


    def _resolve_input_file(self):
        """Return the active input file path.
        If the user hasn't selected a file yet, fall back to the default
        INPUT_sistema.xlsx next to the engine root."""
        if self.selected_input_file and os.path.exists(self.selected_input_file):
            return self.selected_input_file
        # Default: engine/inputs/INPUT_sistema.xlsx
        base_dir = _ui_base_dir()
        default = os.path.join(base_dir, "inputs", "INPUT_sistema.xlsx")
        if os.path.exists(default):
            return default
        return ""

    def get_outputs_dir(self):

        base_dir = _ui_base_dir()

        return os.path.join(base_dir, "outputs")

    def load_input_file_preview(self):

        input_file = self.selected_input_file or self._resolve_input_file()
        if not input_file:
            return

        try:
            wb = load_workbook(input_file, data_only=True)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active

            project_name   = ws["B1"].value or os.path.basename(input_file)
            unit_system    = str(ws["B2"].value or self.input_system_combo.currentText() or "IMPERIAL").upper()
            kerf_raw       = ws["B3"].value
            tolerance_raw  = ws["B4"].value
            # B5 = Currency (used pelo cost engine, não exibido na faixa)
            project_client = str(ws["B6"].value or "").strip()   # Nome_Cliente
            project_addr   = str(ws["B7"].value or "").strip()   # Endereço_do_Projeto
            project_desc   = str(ws["B8"].value or "").strip()   # Descrição_do_Projeto
            project_no     = str(ws["B9"].value or "").strip()   # Numero_do_Projeto
            _date_raw      = ws["B10"].value                     # Data_de_Inicio
            import datetime as _dt
            if isinstance(_date_raw, (_dt.datetime, _dt.date)):
                project_date = _date_raw.strftime("%d/%m/%Y")
            else:
                project_date = str(_date_raw or "").strip()
            output_unit    = str(ws["B11"].value or "").strip()

            if unit_system in ["METRIC", "MÉTRICO", "METRICO"]:
                self.input_system_combo.setCurrentText("METRIC")
            else:
                self.input_system_combo.setCurrentText("IMPERIAL")

            # Restore kerf / tolerance from B3 / B4
            if kerf_raw is not None:
                try:
                    self.kerf_input.setText(str(float(kerf_raw)))
                except (ValueError, TypeError):
                    pass
            if tolerance_raw is not None:
                try:
                    self.tolerance_input.setText(str(float(tolerance_raw)))
                except (ValueError, TypeError):
                    pass

            # Restore output display combo from B11
            output_unit_map = {
                "decimal_inches": "Decimal inches",
                "fractional_inches": "Fractional inches",
                "feet_and_fractional_inches": "Feet and fractional inches",
                "decimal_feet": "Decimal feet",
                "m": "m", "cm": "cm", "mm": "mm",
            }
            display_label = output_unit_map.get(output_unit.lower(), "")
            if display_label and display_label in [
                self.output_display_combo.itemText(i)
                for i in range(self.output_display_combo.count())
            ]:
                self.output_display_combo.setCurrentText(display_label)

            self.set_strip_value(self.project_file_label,        str(project_name))
            self.set_strip_value(self.project_description_label, project_desc)
            self.set_strip_value(self.project_number_label,      project_no)
            if hasattr(self, "project_address_label"):
                self.set_strip_value(self.project_address_label, project_addr)
            if hasattr(self, "project_client_label"):
                self.set_strip_value(self.project_client_label,  project_client)
            if hasattr(self, "project_date_label"):
                self.set_strip_value(self.project_date_label,    project_date)

            if not self.selected_input_file:
                self.selected_input_file = input_file

            if hasattr(self, "selected_input_label"):
                self.selected_input_label.setText(
                    os.path.basename(input_file)
                )

            self.update_settings_summary()
            self.load_manual_input_table_from_input(ws)
            wb.close()

        except Exception as e:
            import traceback
            self.log(f"INPUT PREVIEW ERROR: {str(e)}\n{traceback.format_exc()}")


    def sync_ui_config_to_input(self):

        if not self.selected_input_file:
            return True

        try:
            wb = load_workbook(self.selected_input_file)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active

            unit_system = str(
                self.input_system_combo.currentText() or "IMPERIAL"
            ).upper().strip()

            if unit_system not in ("IMPERIAL", "METRIC"):
                raise ValueError("Invalid unit system selected in UI.")

            output_display = str(
                self.output_display_combo.currentText() or ""
            ).strip()

            # B1 — project name
            pname = self.get_project_name()
            if pname and pname != "No input file selected":
                ws["B1"] = pname

            ws["B2"] = unit_system

            # B5 = Currency (não sobrescrever — gerenciado pelo cost engine)

            # B6 — Nome_Cliente
            if hasattr(self, "project_client_label") and hasattr(self.project_client_label, "value_edit"):
                _client = (self.project_client_label.value_edit.text() or "").strip()
                if _client:
                    ws["B6"] = _client

            # B7 — Endereço_do_Projeto
            if hasattr(self, "project_address_label") and hasattr(self.project_address_label, "value_edit"):
                _addr = (self.project_address_label.value_edit.text() or "").strip()
                if _addr:
                    ws["B7"] = _addr

            # B8 — Descrição_do_Projeto
            if hasattr(self, "project_description_label") and hasattr(self.project_description_label, "value_edit"):
                _proj_desc = (self.project_description_label.value_edit.text() or "").strip()
                if _proj_desc:
                    ws["B8"] = _proj_desc

            # B9 — Numero_do_Projeto
            if hasattr(self, "project_number_label") and hasattr(self.project_number_label, "value_edit"):
                _proj_no = (self.project_number_label.value_edit.text() or "").strip()
                if _proj_no:
                    ws["B9"] = _proj_no

            # B10 — Data_de_Inicio
            if hasattr(self, "project_date_label") and hasattr(self.project_date_label, "value_edit"):
                _date = (self.project_date_label.value_edit.text() or "").strip()
                if _date:
                    ws["B10"] = _date

            # Kerf (B3) and Tolerance (B4) — raw numeric value from UI
            # IMPERIAL: value in 'in'  |  METRIC: value in 'mm'
            # input_loader/converter reads the raw number directly
            cut_unit = "in" if unit_system == "IMPERIAL" else "mm"
            nf = f'#,##0.000\\ "{cut_unit}"'
            try:
                ws["B3"] = float(str(self.kerf_input.text() or "0").replace(",", "."))
                ws["B3"].number_format = nf
            except (ValueError, AttributeError):
                pass
            try:
                ws["B4"] = float(str(self.tolerance_input.text() or "0").replace(",", "."))
                ws["B4"].number_format = nf
            except (ValueError, AttributeError):
                pass

            if unit_system == "METRIC":
                if output_display not in ("m", "cm", "mm"):
                    output_display = "cm"
                ws["B11"] = output_display
            else:
                output_map = {
                    "Decimal inches": "decimal_inches",
                    "Fractional inches": "fractional_inches",
                    "Feet and fractional inches": "feet_and_fractional_inches",
                    "Decimal feet": "decimal_feet",
                    "decimal_inches": "decimal_inches",
                    "fractional_inches": "fractional_inches",
                    "feet_and_fractional_inches": "feet_and_fractional_inches",
                    "decimal_feet": "decimal_feet",
                }
                ws["B11"] = output_map.get(output_display, "decimal_inches")

            wb.save(self.selected_input_file)
            wb.close()

            self.log(
                f"INPUT SYNCED FROM UI - B2={ws['B2'].value} | B5={ws['B5'].value} | B6={ws['B6'].value} | B11={ws['B11'].value}"
            )

            return True

        except PermissionError:
            QMessageBox.critical(
                self,
                "INPUT FILE OPEN",
                "Close the INPUT file before RUN.",
            )
            self.log("INPUT SYNC ERROR - FILE OPEN")
            return False

        except Exception as e:
            QMessageBox.critical(
                self,
                "INPUT SYNC ERROR",
                str(e),
            )
            self.log(f"INPUT SYNC ERROR: {str(e)}")
            return False

    def load_manual_input_table_from_input(self, ws):

        if not hasattr(self, "manual_table"):
            return

        # Ler CRITERIOS sem data_only — evita cache vazio de fórmulas PROCV
        # Chave primária = Descrição (col A). Alternativas: Product Code (col B), Cut Group (col C).
        # KM-017: normalizar homóglifos Unicode (× U+00D7 → x) para match de perfis colados do Excel/Revit.
        _HG = str.maketrans({'×':'x','✕':'x','✖':'x','–':'-','—':'-','−':'-'})
        def _hg(v): return str(v).translate(_HG)
        _criterios = {}
        try:
            _input_path = self.selected_input_file or self._resolve_input_file()
            if _input_path:
                from openpyxl import load_workbook as _lw_c
                _wb_c = _lw_c(str(_input_path), data_only=False)
                if "CRITERIOS" in _wb_c.sheetnames:
                    _wsc = _wb_c["CRITERIOS"]
                    _imperial = "IMPERIAL" in str(ws["B2"].value or "").upper()
                    def _n(v):
                        try: return float(v) if v not in (None,"") else None
                        except: return None
                    for _rc in range(13, _wsc.max_row + 1):
                        _desc = str(_wsc.cell(_rc,1).value or "").strip()  # col A = Descrição
                        _pc   = str(_wsc.cell(_rc,2).value or "").strip()  # col B = Product Code
                        _cg   = str(_wsc.cell(_rc,3).value or "").strip()  # col C = Cut Group
                        if not _desc and not _pc and not _cg:
                            continue
                        if _imperial:
                            _t  = _n(_wsc.cell(_rc,4).value)
                            _bs = [_n(_wsc.cell(_rc,c).value) for c in range(13,18)]  # M-Q (ft)
                            _entry = {
                                "product_code": _pc,
                                "cut_group":    _cg,
                                "min_reusable": _t,
                                "weight":    _n(_wsc.cell(_rc,5).value),
                                "price_bar": _n(_wsc.cell(_rc,6).value),
                                "junkyard":  _n(_wsc.cell(_rc,7).value),
                                "bar_stocks": _bs,
                            }
                        else:
                            _t  = _n(_wsc.cell(_rc,8).value)
                            _bs = [_n(_wsc.cell(_rc,c).value) for c in range(18,23)]  # R-V (m)
                            _entry = {
                                "product_code": _pc,
                                "cut_group":    _cg,
                                "min_reusable": (_t * 100) if _t is not None else None,  # m → cm
                                "weight":    _n(_wsc.cell(_rc,9).value),
                                "price_bar": _n(_wsc.cell(_rc,10).value),
                                "junkyard":  _n(_wsc.cell(_rc,11).value),
                                "bar_stocks": _bs,
                            }
                        # Registrar por Descrição (primária), Product Code e Cut Group — normalizando homóglifos
                        for _k in (_hg(_desc).upper(), _hg(_pc).upper(), _hg(_cg).upper()):
                            if _k and _k not in _criterios:
                                _criterios[_k] = _entry
                _wb_c.close()
        except Exception:
            pass

        start_row = 13
        _ws_last_row = ws.max_row or (start_row + 200)
        max_rows = max(_ws_last_row - start_row + 1, 0)
        rows_to_load = []

        for excel_row in range(start_row, start_row + max_rows):
            # Lookup CRITERIOS: primário=Descrição (col A), fallback=Cut Group (col C), Product Code (col B)
            # Normalizar homóglifos antes do lookup — W4×13 (×=U+00D7) → W4x13
            _desc_val = _hg(str(ws.cell(excel_row, 1).value or "").strip()).upper()
            _pc_val   = _hg(str(ws.cell(excel_row, 2).value or "").strip()).upper()
            _cg_val   = _hg(str(ws.cell(excel_row, 3).value or "").strip()).upper()
            _cr = _criterios.get(_desc_val) or _criterios.get(_cg_val) or _criterios.get(_pc_val) or {}
            def _cv(col_excel, key):
                v = _cr.get(key)
                return v if v is not None else ws.cell(excel_row, col_excel).value
            def _bs_cv(idx, col_excel):
                _bar_stocks = _cr.get("bar_stocks")
                if _bar_stocks and idx < len(_bar_stocks) and _bar_stocks[idx] is not None:
                    return _bar_stocks[idx]
                return ws.cell(excel_row, col_excel).value
            values = [
                ws.cell(excel_row, 1).value,
                ws.cell(excel_row, 2).value or _cr.get("product_code") or None,  # Product Code: Excel > CRITERIOS
                ws.cell(excel_row, 3).value or _cr.get("cut_group") or None,     # Cut Group: Excel > CRITERIOS
                ws.cell(excel_row, 4).value,
                ws.cell(excel_row, 5).value,
                _bs_cv(0, 6),   # F = Bar Stock 1 (CRITERIOS-driven)
                _bs_cv(1, 7),   # G = Bar Stock 2
                _bs_cv(2, 8),   # H = Bar Stock 3
                _bs_cv(3, 9),   # I = Bar Stock 4
                _bs_cv(4, 10),  # J = Bar Stock 5
                _cv(11, "min_reusable"),
                _cv(12, "weight"),
                _cv(13, "price_bar"),
                _cv(14, "junkyard"),
                ws.cell(excel_row, 15).value,
                ws.cell(excel_row, 16).value,
                ws.cell(excel_row, 17).value,
                ws.cell(excel_row, 18).value,
                ws.cell(excel_row, 19).value,
            ]
            if any(v not in (None, "") for v in values):
                rows_to_load.append(values)

        n_load = len(rows_to_load)
        if n_load > self.manual_table.rowCount():
            self.manual_table.setRowCount(n_load)
            for _r in range(self.manual_table.rowCount()):
                self.manual_table.setRowHeight(_r, 28)

        unit_system = self.input_system_combo.currentText() if hasattr(self, "input_system_combo") else "METRIC"
        self.update_manual_table_headers()

        self._table_loading = True   # suppress itemChanged → mark_unsaved during load
        for row in range(self.manual_table.rowCount()):
            for col in range(self.manual_table.columnCount()):
                raw = None
                if row < len(rows_to_load):
                    raw = rows_to_load[row][col]
                value = self.format_manual_cell(col, raw, unit_system)
                item = self.manual_table.item(row, col)
                if item is None:
                    item = QTableWidgetItem("")
                    self.manual_table.setItem(row, col, item)
                item.setText(value)
                item.setTextAlignment(self.manual_cell_alignment(col))
        self._table_loading = False

        self.mark_synced()   # freshly loaded from file = synced

        # Atualiza delegate de lista suspensa com os perfis do CRITERIOS recém lido
        self._load_criterios_delegate_data()

    def format_manual_cell(self, col, raw, unit_system):
        """Format a manual table cell value with appropriate unit suffix and decimals.

        Column map:
          0  Description      → left as-is
          1  Cut Group        → left as-is
          2  Product Code     → left as-is
          3  Length (piece)   → METRIC: "170,00 cm"  IMPERIAL: "170,00 in"
          4  Qty              → integer
          5-9 Bar Stock 1-5   → METRIC: "6,50 m"     IMPERIAL: "6,50 ft"
          10 Min Reusable     → METRIC: "30,00 cm"   IMPERIAL: "30,00 in"
          11 Weight           → METRIC: "0,10 kg/m"  IMPERIAL: "0,10 lb/ft"
          12 Price Bar        → METRIC: "$ 2,00/m"   IMPERIAL: "$ 2,00/ft"
          13 junkyard         → METRIC: "$ 1,00/kg"  IMPERIAL: "$ 1,00/lb"
        """
        if raw is None or str(raw).strip() == "":
            return ""

        is_imperial = str(unit_system).upper().strip() == "IMPERIAL"

        # Cols 0, 1, 2 — text, no formatting needed
        if col in (0, 1, 2):
            return str(raw)

        # Cols 14-18 — User Tag, Area/Zone, Assembly Tag, Piece ID, Observation: plain text
        if col in (14, 15, 16, 17, 18):
            return str(raw)

        # Col 4 — Qty: integer
        if col == 4:
            try:
                return str(int(float(str(raw).replace(",", "."))))
            except (ValueError, TypeError):
                return str(raw)

        # Helper: parse to float safely
        try:
            value = float(str(raw).replace(",", "."))
        except (ValueError, TypeError):
            return str(raw)

        # Col 3 — Piece length
        if col == 3:
            unit = "in" if is_imperial else "cm"
            return f"{value:,.2f} {unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        # Cols 5-9 — Bar Stock lengths
        if 5 <= col <= 9:
            unit = "ft" if is_imperial else "m"
            return f"{value:,.2f} {unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        # Col 10 — Min Reusable Length
        if col == 10:
            unit = "in" if is_imperial else "cm"
            return f"{value:,.2f} {unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        # Col 11 — Weight
        if col == 11:
            unit = "lb/ft" if is_imperial else "kg/m"
            return f"{value:,.2f} {unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        # Col 12 — Price Bar
        if col == 12:
            unit = "ft" if is_imperial else "m"
            return f"$ {value:,.2f}/{unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        # Col 13 — junkyard
        if col == 13:
            unit = "lb" if is_imperial else "kg"
            return f"$ {value:,.2f}/{unit}".replace(",", "X").replace(".", ",").replace("X", ".")

        return str(raw)

    def update_manual_table_headers(self):
        """Update column headers to reflect current unit system and selected language."""
        if not hasattr(self, "manual_table"):
            return
        is_imperial = self.input_system_combo.currentText().upper() == "IMPERIAL"
        piece_unit  = "in" if is_imperial else "cm"
        bar_unit    = "ft" if is_imperial else "m"
        weight_unit = "lb/ft" if is_imperial else "kg/m"
        price_unit  = "$/ft"  if is_imperial else "$/m"
        junk_unit   = "$/lb"  if is_imperial else "$/kg"
        try:
            _lm = _load_engine_module("language_manager")
            def _t(k): return _lm.t(k)
        except Exception:
            def _t(k): return k
        _desc    = _t("ui_col_description")
        _cg      = _t("ui_col_cut_group")
        _pc      = _t("ui_col_product_code")
        _qty     = _t("ui_col_qty")
        _utag    = _t("ui_col_user_tag")
        _az      = _t("ui_col_area_zone")
        _atag    = _t("ui_col_assembly_tag")
        _pid     = _t("ui_col_piece_id")
        _obs     = _t("ui_col_observation")
        _junk    = _t("ui_col_junkyard")
        _length  = _t("length")
        _barstk  = _t("bar_stock")
        _minreu  = _t("min_reusable_length")
        _weight  = _t("weight")
        _pricebar = _t("price_bar")
        self.manual_table.setHorizontalHeaderLabels([
            _desc,
            _pc,
            _cg,
            f"{_length}\n({piece_unit})",
            _qty,
            f"{_barstk} 1\n({bar_unit})",
            f"{_barstk} 2\n({bar_unit})",
            f"{_barstk} 3\n({bar_unit})",
            f"{_barstk} 4\n({bar_unit})",
            f"{_barstk} 5\n({bar_unit})",
            f"{_minreu}\n({piece_unit})",
            f"{_weight}\n({weight_unit})",
            f"{_pricebar}\n({price_unit})",
            f"{_junk}\n({junk_unit})",
            _utag,
            _az,
            _atag,
            _pid,
            _obs,
        ])

    def reformat_manual_table_cells(self):
        """Re-apply unit formatting to all populated cells (called on unit system change).

        HOTFIX (29/06/2026): colunas 5-13 (Bar Stock 1-5, Min Reusable, Weight,
        Price Bar, Junkyard) são derivadas do CRITERIOS e dependem da unidade
        (ft vs m) — relabelar aqui mantendo o mesmo valor numerico produz um
        valor fisicamente errado (ex.: "24 ft" -> "24 m" em vez de "6,1 m").
        Essas colunas sao re-populadas exclusivamente por
        _reload_criterios_into_table(), chamada logo em seguida em
        update_unit_system(), que busca a coluna correta (M-Q ft / R-V m).
        """
        if not hasattr(self, "manual_table"):
            return
        unit_system = self.input_system_combo.currentText()
        _CRITERIOS_OWNED_COLS = set(range(5, 14))  # Bar Stock 1-5, Min Reusable, Weight, Price Bar, Junkyard
        self._table_loading = True
        for row in range(self.manual_table.rowCount()):
            for col in range(self.manual_table.columnCount()):
                if col in _CRITERIOS_OWNED_COLS:
                    continue
                item = self.manual_table.item(row, col)
                if item is None:
                    continue
                raw_text = item.text().strip()
                if not raw_text:
                    continue
                bare = self.strip_unit_suffix(raw_text)
                formatted = self.format_manual_cell(col, bare, unit_system)
                item.setText(formatted)
                item.setTextAlignment(self.manual_cell_alignment(col))
        self._table_loading = False
        # Reformatting counts as unsaved (unit system changed → Excel not yet updated)
        self.mark_unsaved()

    def _reload_criterios_into_table(self):
        """Recarrega F-J (bar stocks) e K-N (peso/preço/junk) do CRITERIOS para o novo unit system.
        Chamado após update_unit_system() para corrigir valores — não apenas labels."""
        if not hasattr(self, "manual_table"):
            return
        input_file = self.selected_input_file or self._resolve_input_file()
        if not input_file:
            return
        unit_system = self.input_system_combo.currentText().upper()
        is_imperial = (unit_system == "IMPERIAL")
        try:
            from openpyxl import load_workbook as _lw2
            _wb2 = _lw2(str(input_file), data_only=False)
            if "CRITERIOS" not in _wb2.sheetnames:
                _wb2.close()
                return
            _wsc = _wb2["CRITERIOS"]
            def _n(v):
                try: return float(v) if v not in (None, "") else None
                except: return None
            criterios = {}
            for _rc in range(13, _wsc.max_row + 1):
                _desc = str(_wsc.cell(_rc, 1).value or "").strip()
                _pc   = str(_wsc.cell(_rc, 2).value or "").strip()
                _cg   = str(_wsc.cell(_rc, 3).value or "").strip()
                if not _desc and not _pc and not _cg:
                    continue
                if is_imperial:
                    _t  = _n(_wsc.cell(_rc, 4).value)
                    _bs = [_n(_wsc.cell(_rc, c).value) for c in range(13, 18)]
                    _entry = {"min_reusable": _t, "weight": _n(_wsc.cell(_rc,5).value),
                              "price_bar": _n(_wsc.cell(_rc,6).value), "junkyard": _n(_wsc.cell(_rc,7).value),
                              "bar_stocks": _bs}
                else:
                    _t  = _n(_wsc.cell(_rc, 8).value)
                    _bs = [_n(_wsc.cell(_rc, c).value) for c in range(18, 23)]
                    _entry = {"min_reusable": (_t * 100) if _t is not None else None,
                              "weight": _n(_wsc.cell(_rc,9).value), "price_bar": _n(_wsc.cell(_rc,10).value),
                              "junkyard": _n(_wsc.cell(_rc,11).value), "bar_stocks": _bs}
                for _k in (_desc.upper(), _pc.upper(), _cg.upper()):
                    if _k and _k not in criterios:
                        criterios[_k] = _entry
            _wb2.close()
        except Exception:
            return

        self._table_loading = True
        for row in range(self.manual_table.rowCount()):
            desc_item = self.manual_table.item(row, 0)
            if not desc_item or not desc_item.text().strip():
                continue
            desc_key = self.strip_unit_suffix(desc_item.text()).strip().upper()
            cg_item  = self.manual_table.item(row, 2)
            cg_key   = (self.strip_unit_suffix(cg_item.text()).strip().upper() if cg_item else "")
            pc_item  = self.manual_table.item(row, 1)
            pc_key   = (self.strip_unit_suffix(pc_item.text()).strip().upper() if pc_item else "")
            cr = criterios.get(desc_key) or criterios.get(cg_key) or criterios.get(pc_key)
            if not cr:
                continue
            # Atualizar F-J (bar stocks, cols 5-9)
            bar_stocks = cr.get("bar_stocks") or []
            for i, col in enumerate(range(5, 10)):
                it = self.manual_table.item(row, col)
                if it is None:
                    it = QTableWidgetItem("")
                    self.manual_table.setItem(row, col, it)
                val = bar_stocks[i] if i < len(bar_stocks) and bar_stocks[i] is not None else None
                it.setText(self.format_manual_cell(col, val, unit_system))
                it.setTextAlignment(self.manual_cell_alignment(col))
            # Atualizar K-N (min_reusable=10, weight=11, price_bar=12, junkyard=13)
            for col, key in ((10, "min_reusable"), (11, "weight"), (12, "price_bar"), (13, "junkyard")):
                it = self.manual_table.item(row, col)
                if it is None:
                    it = QTableWidgetItem("")
                    self.manual_table.setItem(row, col, it)
                val = cr.get(key)
                it.setText(self.format_manual_cell(col, val, unit_system))
                it.setTextAlignment(self.manual_cell_alignment(col))
        self._table_loading = False
        self.mark_unsaved()

    def manual_cell_alignment(self, col):

        if col == 0:
            return Qt.AlignLeft | Qt.AlignVCenter

        if col in [1, 2]:
            return Qt.AlignCenter

        if col == 4:
            return Qt.AlignCenter

        # cols 14-18: User Tag, Area/Zone, Assembly Tag, Piece ID, Observation
        if col in [14, 15, 16]:
            return Qt.AlignCenter
        if col == 17:
            return Qt.AlignCenter
        if col == 18:
            return Qt.AlignLeft | Qt.AlignVCenter

        return Qt.AlignRight | Qt.AlignVCenter

    def apply_manual_table_format(self):

        if not hasattr(self, "manual_table"):
            return

        for row in range(self.manual_table.rowCount()):
            self.manual_table.setRowHeight(row, 28)
            for col in range(self.manual_table.columnCount()):
                item = self.manual_table.item(row, col)
                if item is None:
                    item = QTableWidgetItem("")
                    self.manual_table.setItem(row, col, item)

                item.setTextAlignment(self.manual_cell_alignment(col))

    def clear_manual_input(self):
        """Limpa SOMENTE a tela. NÃO toca no INPUT_sistema.xlsx."""
        if not hasattr(self, "manual_table"):
            return

        reply = QMessageBox.question(
            self,
            "CLEAR INPUT",
            "Clear all rows from the screen?\n\n"
            "The Excel file will NOT be affected.",
            QMessageBox.Yes | QMessageBox.No,
        )
        if reply != QMessageBox.Yes:
            return

        self._table_loading = True
        self.manual_table.clearContents()
        self.manual_table.setRowCount(8)
        for row in range(self.manual_table.rowCount()):
            self.manual_table.setRowHeight(row, 28)
            for col in range(self.manual_table.columnCount()):
                item = QTableWidgetItem("")
                item.setTextAlignment(self.manual_cell_alignment(col))
                self.manual_table.setItem(row, col, item)
        self._table_loading = False
        self.mark_unsaved()
        self.log("SCREEN cleared — Excel file unchanged.")

    # ── Mirror control ────────────────────────────────────────────────────────

    def _manual_table_key_press(self, event):
        """Enable Ctrl+C, Ctrl+V, Ctrl+X on the manual input table."""
        from PySide6.QtCore import QEvent
        from PySide6.QtGui import QKeySequence, QKeyEvent
        from PySide6.QtWidgets import QApplication

        key = event.key()
        mod = event.modifiers()

        if mod == Qt.ControlModifier and key == Qt.Key_C:
            # Copy selected cells to clipboard as tab-separated
            selected = self.manual_table.selectedRanges()
            if not selected:
                return
            rng = selected[0]
            rows = []
            for r in range(rng.topRow(), rng.bottomRow() + 1):
                row_data = []
                for c in range(rng.leftColumn(), rng.rightColumn() + 1):
                    item = self.manual_table.item(r, c)
                    row_data.append(item.text() if item else "")
                rows.append("\t".join(row_data))
            QApplication.clipboard().setText("\n".join(rows))

        elif mod == Qt.ControlModifier and key == Qt.Key_X:
            # Cut: copy then clear
            self._manual_table_key_press(
                type('E', (), {'key': lambda s: Qt.Key_C,
                               'modifiers': lambda s: Qt.ControlModifier})()
            )
            for idx in self.manual_table.selectedItems():
                idx.setText("")

        elif mod == Qt.ControlModifier and key == Qt.Key_V:
            # Paste from clipboard — auto-expands rows as needed (unlimited)
            text = QApplication.clipboard().text()
            if not text:
                return
            start_row = self.manual_table.currentRow()
            start_col = self.manual_table.currentColumn()
            if start_row < 0:
                start_row = 0
            if start_col < 0:
                start_col = 0
            self._table_loading = True
            for r, line in enumerate(text.split("\n")):
                if not line:
                    continue
                tr = start_row + r
                # Auto-expand: insert new rows when pasting beyond current count
                while tr >= self.manual_table.rowCount():
                    new_r = self.manual_table.rowCount()
                    self.manual_table.insertRow(new_r)
                    self.manual_table.setRowHeight(new_r, 28)
                    for col in range(self.manual_table.columnCount()):
                        _it = QTableWidgetItem("")
                        _it.setTextAlignment(self.manual_cell_alignment(col))
                        self.manual_table.setItem(new_r, col, _it)
                for c, cell_text in enumerate(line.split("\t")):
                    tc = start_col + c
                    if tc < self.manual_table.columnCount():
                        item = self.manual_table.item(tr, tc)
                        if item is None:
                            item = QTableWidgetItem()
                            self.manual_table.setItem(tr, tc, item)
                        item.setText(cell_text.strip())
            self._table_loading = False
            self.mark_unsaved()

        elif key in (Qt.Key_Delete, Qt.Key_Backspace):
            # Apaga conteúdo das células selecionadas
            self._table_loading = True
            for item in self.manual_table.selectedItems():
                item.setText("")
            self._table_loading = False
            self.mark_unsaved()

        else:
            QTableWidget.keyPressEvent(self.manual_table, event)

    def _on_table_item_changed(self, item):
        """Called when user edits any cell. Marks mirror as out of sync."""
        if getattr(self, "_table_loading", False):
            return
        self.mark_unsaved()

    # ------------------------------------------------------------------
    # Delegate de lista suspensa — Descrição (col 0 do manual_table)
    # ------------------------------------------------------------------

    def _load_criterios_delegate_data(self, input_path=None):
        """Lê aba CRITERIOS do INPUT e popula o delegate de lista suspensa."""
        import os as _os2
        if input_path is None:
            input_path = getattr(self, "selected_input_file", None) or self._resolve_input_file()
        self._criterios_delegate_data  = {}
        self._criterios_delegate_names = []
        if not input_path or not _os2.path.exists(str(input_path)):
            return
        try:
            from openpyxl import load_workbook as _lw2
            _wb = _lw2(str(input_path), data_only=True, read_only=True)
            if "CRITERIOS" not in _wb.sheetnames:
                _wb.close()
                return
            _ws = _wb["CRITERIOS"]
            def _flt(v):
                try:
                    return float(v) if v not in (None, "") else None
                except Exception:
                    return None
            for _r in range(13, _ws.max_row + 1):
                _desc = str(_ws.cell(_r, 1).value or "").strip()
                if not _desc:
                    continue
                _key = _desc.upper()
                if _key in self._criterios_delegate_data:
                    continue
                self._criterios_delegate_names.append(_desc)
                self._criterios_delegate_data[_key] = {
                    "description":  _desc,
                    "product_code": _ws.cell(_r, 2).value,
                    "cut_group":    _ws.cell(_r, 3).value,
                    "threshold_in": _flt(_ws.cell(_r, 4).value),
                    "weight_lb_ft": _flt(_ws.cell(_r, 5).value),
                    "price_bar_ft": _flt(_ws.cell(_r, 6).value),
                    "junk_lb":      _flt(_ws.cell(_r, 7).value),
                    "threshold_m":  _flt(_ws.cell(_r, 8).value),
                    "weight_kg_m":  _flt(_ws.cell(_r, 9).value),
                    "price_bar_m":  _flt(_ws.cell(_r, 10).value),
                    "junk_kg":      _flt(_ws.cell(_r, 11).value),
                    "bar_stocks_ft": [_flt(_ws.cell(_r, c).value) for c in range(13, 18)],
                    "bar_stocks_m":  [_flt(_ws.cell(_r, c).value) for c in range(18, 23)],
                }
            _wb.close()
        except Exception:
            pass
        # Salvar conjunto CRITERIOS-only ANTES de enriquecer com estoque
        self._criterios_desc_set = set(self._criterios_delegate_data.keys())
        # Enriquecer dropdown com descrições de barras AVAILABLE no estoque
        try:
            import stock_backend_router as _sbr_dd
            _av_items = _sbr_dd.load_stock()
            _seen_profiles = set(self._criterios_delegate_data.keys())
            for _av in _av_items:
                if str(_av.get("status","")).upper() != "AVAILABLE":
                    continue
                _prof = str(_av.get("profile") or _av.get("cut_group") or "").strip()
                if not _prof:
                    continue
                _pk = _prof.upper()
                if _pk not in _seen_profiles:
                    _seen_profiles.add(_pk)
                    self._criterios_delegate_names.append(_prof)
                    self._criterios_delegate_data[_pk] = {
                        "description":   _prof,
                        "product_code":  None,
                        "cut_group":     _av.get("cut_group"),
                        "threshold_in":  None, "weight_lb_ft": None,
                        "price_bar_ft":  None, "junk_lb":      None,
                        "threshold_m":   None, "weight_kg_m":  None,
                        "price_bar_m":   None, "junk_kg":      None,
                        "bar_stocks_ft": [],   "bar_stocks_m": [],
                        "_stock_only":   True,
                    }
        except Exception:
            pass
        if hasattr(self, "_desc_delegate"):
            self._desc_delegate.update_items(self._criterios_delegate_names)

    def _apply_description_autofill(self, row, desc):
        """Auto-preenche as colunas dependentes quando o usuário seleciona uma Descrição."""
        if not hasattr(self, "_criterios_delegate_data"):
            return
        crit = self._criterios_delegate_data.get(desc.strip().upper(), {})
        if not crit:
            return
        unit_sys = (
            self.input_system_combo.currentText()
            if hasattr(self, "input_system_combo") else "METRIC"
        ).upper()
        imperial = (unit_sys == "IMPERIAL")
        if imperial:
            bar_stocks = [v for v in (crit.get("bar_stocks_ft") or []) if v is not None]
            weight = crit.get("weight_lb_ft")
            price  = crit.get("price_bar_ft")
            junk   = crit.get("junk_lb")
            thresh = crit.get("threshold_in")
        else:
            bar_stocks = [v for v in (crit.get("bar_stocks_m") or []) if v is not None]
            weight = crit.get("weight_kg_m")
            price  = crit.get("price_bar_m")
            junk   = crit.get("junk_kg")
            _tm = crit.get("threshold_m")
            thresh = (_tm * 100) if _tm is not None else None  # m → cm

        autofill = {
            1:  crit.get("product_code"), # Product Code
            2:  crit.get("cut_group"),    # Cut Group
            10: thresh,                    # Min Reusable
            11: weight,                    # Weight
            12: price,                     # Price Bar
            13: junk,                      # Junkyard
        }
        for i in range(5):
            autofill[5 + i] = bar_stocks[i] if i < len(bar_stocks) else None

        self._table_loading = True
        try:
            for col, val in autofill.items():
                if row >= self.manual_table.rowCount() or col >= self.manual_table.columnCount():
                    continue
                it = self.manual_table.item(row, col)
                if it is None:
                    it = QTableWidgetItem("")
                    self.manual_table.setItem(row, col, it)
                it.setText(self.format_manual_cell(col, val, unit_sys) if val is not None else "")
                it.setTextAlignment(self.manual_cell_alignment(col))
        finally:
            self._table_loading = False
        self.mark_unsaved()

    def mark_unsaved(self):
        """Visual indicator: tela divergiu do Excel — texto traduzido."""
        if hasattr(self, "mirror_status_label"):
            try:
                _lm = _load_engine_module("language_manager")
                txt = _lm.t("lbl_unsaved")
            except Exception:
                txt = "⚠  UNSAVED CHANGES"
            self.mirror_status_label.setText(txt)
            self.mirror_status_label.setStyleSheet("""
                font-size: 13px;
                font-weight: bold;
                color: #f39c12;
                padding: 4px 10px;
            """)
        if hasattr(self, "sync_to_excel_button"):
            self.sync_to_excel_button.setEnabled(True)

    def mark_synced(self):
        """Visual indicator: tela = Excel — texto traduzido."""
        if hasattr(self, "mirror_status_label"):
            try:
                _lm = _load_engine_module("language_manager")
                txt = _lm.t("lbl_synced")
            except Exception:
                txt = "✓  SYNCED"
            self.mirror_status_label.setText(txt)
            self.mirror_status_label.setStyleSheet("""
                font-size: 13px;
                font-weight: bold;
                color: #2ecc71;
                padding: 4px 10px;
            """)
        # Keep SYNC TO EXCEL always enabled — user may want to sync at any time
        if hasattr(self, "sync_to_excel_button"):
            self.sync_to_excel_button.setEnabled(True)

    def add_manual_row(self):
        """Append one blank row to the table."""
        if not hasattr(self, "manual_table"):
            return
        row = self.manual_table.rowCount()
        self.manual_table.insertRow(row)
        self.manual_table.setRowHeight(row, 28)
        for col in range(self.manual_table.columnCount()):
            item = QTableWidgetItem("")
            item.setTextAlignment(self.manual_cell_alignment(col))
            self.manual_table.setItem(row, col, item)
        self.mark_unsaved()

    def remove_manual_row(self):
        """Remove selected rows; if no selection, remove the last row. No minimum row limit."""
        if not hasattr(self, "manual_table"):
            return
        rows = self.manual_table.rowCount()
        if rows == 0:
            return
        selected = sorted(
            {idx.row() for idx in self.manual_table.selectedIndexes()},
            reverse=True
        )
        if selected:
            for r in selected:
                self.manual_table.removeRow(r)
        else:
            self.manual_table.removeRow(rows - 1)
        self.mark_unsaved()

    def strip_unit_suffix(self, text):
        """Remove formatted unit suffix to recover raw numeric value for Excel."""
        s = str(text).strip()
        # Remove currency prefix
        s = s.replace("$", "").strip()
        # Remove known unit suffixes (order matters: longer first)
        for suffix in (" lb/ft", " kg/m", " lb/ft", " kg/m",
                       "/ft", "/m", "/kg", "/lb",
                       " cm", " mm", " in", " ft", " m"):
            if s.endswith(suffix):
                s = s[: -len(suffix)].strip()
                break
        # Convert PT-BR decimal to float-compatible
        # Format is "1.234,56" → "1234.56"
        if "," in s and "." in s:
            s = s.replace(".", "").replace(",", ".")
        elif "," in s:
            s = s.replace(",", ".")
        return s.strip()

    def _collect_table_rows(self):
        """Return list of dicts with raw values from the screen table (unit suffixes stripped)."""
        rows = []
        table_cols = self.manual_table.columnCount()
        for row in range(self.manual_table.rowCount()):
            desc = self.manual_table.item(row, 0)
            cg   = self.manual_table.item(row, 1)
            has_content = (
                (desc and desc.text().strip()) or
                (cg   and cg.text().strip())
            )
            if not has_content:
                continue
            cells = []
            for col in range(table_cols):
                item = self.manual_table.item(row, col)
                raw_text = item.text().strip() if item else ""
                if raw_text == "":
                    cells.append(None)
                elif col in (0, 1, 2, 14, 15, 16, 18):
                    cells.append(raw_text)
                elif col == 17:
                    try:
                        cells.append(int(raw_text))
                    except ValueError:
                        cells.append(raw_text)
                elif col == 4:
                    try:
                        cells.append(int(self.strip_unit_suffix(raw_text).replace(",", "").replace(".", "") or 0))
                    except ValueError:
                        cells.append(raw_text)
                else:
                    bare = self.strip_unit_suffix(raw_text)
                    try:
                        cells.append(float(bare) if bare else None)
                    except ValueError:
                        cells.append(raw_text)
            rows.append(cells)
        return rows

    def _collect_excel_rows(self):
        """Return list of raw value lists from INPUT_sistema.xlsx rows 13+.

        Usa data_only=False para colunas com fórmulas PROCV (K/L/M/N = cols 11-14)
        a fim de evitar que openpyxl retorne None quando não há cache de fórmula.
        As colunas PROCV são excluídas do diff — são calculadas pelo Excel.
        """
        input_file = self._resolve_input_file()
        if not input_file:
            return []
        # Colunas PROCV (1-based): K=11, L=12, M=13, N=14 — excluir do diff
        PROCV_COLS = {11, 12, 13, 14}
        try:
            wb = load_workbook(input_file, data_only=True)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active
            rows = []
            for excel_row in range(13, 13 + 200):
                values = []
                for c in range(1, 20):
                    if c in PROCV_COLS:
                        # Ignorar fórmulas PROCV — não participam do diff
                        values.append("__PROCV__")
                    else:
                        values.append(ws.cell(excel_row, c).value)
                # Linha tem conteúdo se alguma coluna não-PROCV tem valor
                has_content = any(
                    v not in (None, "", "__PROCV__")
                    for i, v in enumerate(values, 1)
                    if i not in PROCV_COLS
                )
                if has_content:
                    rows.append(values)
                elif not rows:
                    continue
                else:
                    break
            wb.close()
            return rows
        except Exception:
            return []

    def _build_diff_report(self, screen_rows, excel_rows):
        """Compare screen vs Excel rows. Return (has_diff, report_text).

        Colunas marcadas como __PROCV__ (K/L/M/N) são ignoradas no diff —
        são fórmulas calculadas pelo Excel, não editáveis pelo usuário.
        """
        COL_NAMES = [
            "Description", "Product Code", "Cut Group", "Length", "Qty",
            "Bar 1", "Bar 2", "Bar 3", "Bar 4", "Bar 5",
            "Min Reusable", "Weight", "Price Bar", "Junkyard",
            "User Tag", "Area/Zone", "Assembly Tag", "Piece ID", "Observation"
        ]
        max_rows = max(len(screen_rows), len(excel_rows))
        diffs = []

        for i in range(max_rows):
            s_row = screen_rows[i] if i < len(screen_rows) else None
            e_row = excel_rows[i]  if i < len(excel_rows)  else None

            if s_row is None:
                diffs.append(f"  Row {i+1}: SCREEN has no row — EXCEL has: {e_row[0]} / {e_row[1]}")
                continue
            if e_row is None:
                diffs.append(f"  Row {i+1}: SCREEN has: {s_row[0]} / {s_row[1]} — EXCEL has no row")
                continue

            row_diffs = []
            for col in range(min(len(s_row), len(e_row))):
                sv = s_row[col]
                ev = e_row[col]
                # Ignorar colunas PROCV — não fazem parte do diff editável
                if ev == "__PROCV__" or sv == "__PROCV__":
                    continue
                # Numeric comparison with tolerance
                try:
                    if sv is not None and ev is not None:
                        if abs(float(str(sv).replace(",", ".")) -
                               float(str(ev).replace(",", "."))) > 0.0001:
                            row_diffs.append(f"{COL_NAMES[col]}: screen={sv} / excel={ev}")
                    elif sv != ev:
                        row_diffs.append(f"{COL_NAMES[col]}: screen={sv!r} / excel={ev!r}")
                except (ValueError, TypeError):
                    if str(sv) != str(ev):
                        row_diffs.append(f"{COL_NAMES[col]}: screen={sv!r} / excel={ev!r}")

            if row_diffs:
                label = s_row[0] or e_row[0] or f"Row {i+1}"
                diffs.append(f"  [{label} | {s_row[1] or e_row[1]}]  " + " | ".join(row_diffs))

        has_diff = bool(diffs)
        if not has_diff:
            report = "✓  Screen and Excel are identical. No changes will be made."
        else:
            report = f"⚠  {len(diffs)} difference(s) found:\n\n" + "\n".join(diffs)
            report += f"\n\n─────────────────────────────────────────\n"
            report += f"Screen rows: {len(screen_rows)}   |   Excel rows: {len(excel_rows)}\n"
            report += f"Confirming will CLEAR Excel data rows and write the screen data."
        return has_diff, report

    def _sync_table_to_excel_silent(self):
        """Grava os dados da tela diretamente no Excel SEM dialog, SEM confirmação.

        Chamado automaticamente antes de cada RUN para garantir que o pipeline
        sempre leia o estado atual da UI — idêntico em VS Code e .exe.

        Retorna True em sucesso, False em falha (mensagem exibida internamente).
        """
        input_file = self._resolve_input_file()
        if not input_file:
            QMessageBox.warning(self, "NO FILE",
                "No input file found.\nSelect a file via SELECT INPUT FILE.")
            return False

        screen_rows = self._collect_table_rows()

        # Safety guard: if the table has no content, skip the sync entirely to
        # prevent an empty/unloaded table from overwriting existing Excel data
        # (e.g. startup before auto-load completes, or after an accidental clear).
        if not screen_rows:
            return True

        try:
            wb = load_workbook(input_file)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active

            START_ROW  = 13
            # HOTFIX (29/06/2026): K-N (Threshold/Weight/Price/Junkyard) eram
            # tratadas como "fórmulas PROCV" e puladas na gravação, mas o
            # template real não usa VLOOKUP vivo nessas células para todas as
            # linhas — o valor correto (já calculado via CRITERIOS pela UI,
            # igual ao tratamento de Bar Stock F-J) precisa ser persistido no
            # xlsx, senão a coluna K (Threshold) fica sempre vazia (confirmado
            # em produção: INPUT_sistema.xlsx linha 13-17, col K = None).
            PROCV_COLS = set()
            table_cols = self.manual_table.columnCount()

            # Limpar linhas de dados (preservando colunas com fórmulas PROCV)
            clear_end = START_ROW + max(len(screen_rows), 50) + 10
            for r in range(START_ROW, clear_end):
                for c in range(1, table_cols + 1):
                    if c not in PROCV_COLS:
                        ws.cell(row=r, column=c).value = None

            # Número de formatos por coluna (0-based)
            unit_system = str(self.input_system_combo.currentText() or "IMPERIAL").upper()
            is_imperial = (unit_system == "IMPERIAL")
            piece_nf = '0.00\\ "in"'  if is_imperial else '0.00\\ "cm"'
            bar_nf   = '0.0\\ "ft"'   if is_imperial else '0.0\\ "m"'
            mrl_nf   = '0.00\\ "in"'  if is_imperial else '0.00\\ "cm"'
            COL_NF = {
                3: piece_nf,
                5: bar_nf, 6: bar_nf, 7: bar_nf, 8: bar_nf, 9: bar_nf,
                10: mrl_nf,
            }

            # Gravar linhas da tela (pula colunas PROCV)
            for i, cells in enumerate(screen_rows):
                excel_row = START_ROW + i
                for col, cell_value in enumerate(cells):
                    col_1based = col + 1
                    if col_1based in PROCV_COLS:
                        continue
                    c = ws.cell(row=excel_row, column=col_1based)
                    c.value = cell_value
                    if col in COL_NF and cell_value is not None:
                        c.number_format = COL_NF[col]

            # Garantir lista suspensa em col A (CRITERIOS!$A$13:$A$200).
            # openpyxl preserva DataValidation em load/save, mas re-aplica
            # preventivamente para sobreviver a saves parciais ou templates
            # novos que ainda nao tenham a validacao.
            from openpyxl.worksheet.datavalidation import DataValidation as _DV
            _col_a_formula = 'CRITERIOS!$A$13:$A$200'
            _has_dv = any(
                getattr(dv, 'formula1', '') == _col_a_formula
                for dv in ws.data_validations.dataValidation
            )
            if not _has_dv:
                _dv = _DV(type='list', formula1=_col_a_formula,
                          allow_blank=True, showErrorMessage=False,
                          showInputMessage=False)
                _dv.sqref = f'A{START_ROW}:A1000'
                ws.add_data_validation(_dv)

            wb.save(input_file)
            wb.close()

            self.input_is_valid = True
            self.set_input_status("INPUT STATUS: VALID", "#00ff99")
            self.mark_synced()
            self.log(f"AUTO-SYNC ✓ — {len(screen_rows)} rows → {os.path.basename(input_file)}")
            return True

        except PermissionError:
            QMessageBox.critical(self, "FILE OPEN",
                "Close the INPUT file in Excel before running.")
            self.log("AUTO-SYNC ERROR — file open in Excel")
            return False
        except Exception as e:
            import traceback
            QMessageBox.critical(self, "AUTO-SYNC ERROR",
                f"Failed to sync table to Excel before RUN:\n{e}\n\n{traceback.format_exc()[:600]}")
            self.log(f"AUTO-SYNC ERROR: {e}")
            return False

    def sync_table_to_excel(self):
        """SYNC TO EXCEL: compara tela vs xlsx, mostra diff, grava após confirmação."""
        # Recarregar CRITERIOS do disco para capturar edições feitas no Excel
        # (ex: novo perfil adicionado após a última carga do template)
        self._load_criterios_delegate_data()
        input_file = self._resolve_input_file()
        if not input_file:
            QMessageBox.warning(self, "NO FILE",
                "No input file found.\nSelect a file via SELECT INPUT FILE.")
            return

        screen_rows = self._collect_table_rows()
        excel_rows  = self._collect_excel_rows()

        # Include project name in diff check
        screen_project = self.get_project_name()
        try:
            wb_check = load_workbook(input_file, data_only=True)
            ws_check = wb_check["INPUT"] if "INPUT" in wb_check.sheetnames else wb_check.active
            excel_project = str(ws_check["B1"].value or "").strip()
            wb_check.close()
        except Exception:
            excel_project = ""

        has_diff, report = self._build_diff_report(screen_rows, excel_rows)

        if screen_project != excel_project:
            has_diff = True
            report = f"  Project name: screen={repr(screen_project)} / excel={repr(excel_project)}\n" + report

        # ── Diff dialog ───────────────────────────────────────────────────
        try:
            _lm_sy = _load_engine_module("language_manager")
            _tsy = _lm_sy.t
        except Exception:
            _tsy = lambda k: k
        dialog = QDialog(self)
        dialog.setWindowTitle(_tsy("sync_dialog_title"))
        dialog.setMinimumWidth(780)
        dialog.setMinimumHeight(380)
        dialog.setStyleSheet("""
            QDialog   { background-color: #0c1520; color: #d8eeff; }
            QLabel    { background: transparent; }
            QTextEdit { background-color: #060d14; color: #b8d8f0;
                        border: 1px solid #1e3d5a; border-radius: 6px;
                        font-family: Consolas; font-size: 12px; padding: 8px; }
            QPushButton { border-radius: 7px; font-weight: bold;
                          font-size: 12px; padding: 9px 22px; }
        """)

        d_layout = QVBoxLayout()
        d_layout.setSpacing(10)
        d_layout.setContentsMargins(16, 16, 16, 14)

        title_lbl = QLabel(_tsy("sync_dialog_header"))
        title_lbl.setStyleSheet("font-size: 13px; font-weight: bold; color: #5ab4ff;")
        d_layout.addWidget(title_lbl)

        report_box = QTextEdit()
        report_box.setReadOnly(True)
        report_box.setPlainText(report)
        d_layout.addWidget(report_box)

        btn_row = QHBoxLayout()
        btn_row.addStretch()

        cancel_btn = QPushButton("✕  " + _tsy("btn_cancel").capitalize())
        cancel_btn.setStyleSheet("""
            QPushButton { background-color: #1a1a2e; border: 1px solid #3a3a5a;
                          color: #8888aa; }
            QPushButton:hover { background-color: #252540; color: #ccccff; }
        """)
        cancel_btn.clicked.connect(dialog.reject)

        confirm_btn = QPushButton(_tsy("sync_btn_confirm"))
        confirm_btn.setStyleSheet("""
            QPushButton { background-color: #0d3320; border: 1px solid #1e7a40;
                          color: #2ecc71; }
            QPushButton:hover { background-color: #155228; border: 1px solid #2ecc71;
                                color: #ffffff; }
        """)
        confirm_btn.clicked.connect(dialog.accept)

        btn_row.addWidget(cancel_btn)
        btn_row.addWidget(confirm_btn)
        d_layout.addLayout(btn_row)
        dialog.setLayout(d_layout)

        if dialog.exec() != QDialog.Accepted:
            return

        # ── Write to Excel ────────────────────────────────────────────────
        try:
            wb = load_workbook(input_file)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active

            # Sync B1 — project name from UI PROJECT field
            project_name = self.get_project_name()
            if not project_name or project_name == "No input file selected":
                # fallback: read existing B1 value
                project_name = ws["B1"].value or ""
            ws["B1"] = project_name
            unit_system = str(self.input_system_combo.currentText() or "IMPERIAL").upper().strip()
            ws["B2"] = unit_system
            cut_unit = "in" if unit_system == "IMPERIAL" else "mm"
            nf = f'#,##0.000\\ "{cut_unit}"'
            try:
                ws["B3"] = float(str(self.kerf_input.text() or "0").replace(",", "."))
                ws["B3"].number_format = nf
            except (ValueError, AttributeError):
                pass
            try:
                ws["B4"] = float(str(self.tolerance_input.text() or "0").replace(",", "."))
                ws["B4"].number_format = nf
            except (ValueError, AttributeError):
                pass
            output_display = str(self.output_display_combo.currentText() or "").strip()
            if unit_system == "METRIC":
                ws["B11"] = output_display if output_display in ("m", "cm", "mm") else "cm"
            else:
                output_map = {
                    "Decimal inches": "decimal_inches",
                    "Fractional inches": "fractional_inches",
                    "Feet and fractional inches": "feet_and_fractional_inches",
                    "Decimal feet": "decimal_feet",
                }
                ws["B11"] = output_map.get(output_display, "decimal_inches")

            # Clear ALL data rows (13 → safe max) — todas as colunas inclusive K/L/M/N
            # (K/L/M/N eram PROCV no template antigo; HOTFIX 29/06 as converteu para
            # valores diretos — sync_ui_config_to_input já usa PROCV_COLS=set())
            START_ROW = 13
            PROCV_COLS = set()
            table_cols = self.manual_table.columnCount()
            for r in range(START_ROW, START_ROW + max(len(screen_rows), 50) + 10):
                for c in range(1, table_cols + 1):
                    if c not in PROCV_COLS:
                        ws.cell(row=r, column=c).value = None

            # Column number_formats — update to match current unit system
            # IMPERIAL: piece=in, bar=ft  |  METRIC: piece=cm, bar=m
            is_imperial = (unit_system == "IMPERIAL")
            piece_nf = '0.00\\ "in"'  if is_imperial else '0.00\\ "cm"'
            bar_nf   = '0.0\\ "ft"'   if is_imperial else '0.0\\ "m"'
            mrl_nf   = '0.00\\ "in"'  if is_imperial else '0.00\\ "cm"'
            # col index (0-based) → number_format
            COL_NF = {
                3:  piece_nf,          # Length (piece)
                5:  bar_nf, 6: bar_nf, 7: bar_nf, 8: bar_nf, 9: bar_nf,  # Bar Stock 1-5
                10: mrl_nf,            # Min Reusable Length
            }

            # Write screen rows — todas as colunas (PROCV_COLS = set(), nenhuma excluída)
            for i, cells in enumerate(screen_rows):
                excel_row = START_ROW + i
                for col, cell_value in enumerate(cells):
                    col_1based = col + 1
                    if col_1based in PROCV_COLS:
                        continue
                    c = ws.cell(row=excel_row, column=col_1based)
                    c.value = cell_value
                    if col in COL_NF and cell_value is not None:
                        c.number_format = COL_NF[col]

            wb.save(input_file)
            wb.close()

            # Register as selected file if was using default
            if not self.selected_input_file:
                self.selected_input_file = input_file

            # Auto-validate after sync — file was just written by this system
            # Force valid regardless; log any validation issues for debugging only
            self.input_is_valid = True
            self.set_input_status("INPUT STATUS: VALID", "#00ff99")
            try:
                self.validate_xlsx_input()
            except Exception as ve:
                self.log(f"POST-SYNC VALIDATION NOTE: {ve}")
                # Still valid — sync just succeeded

            # Reload table from the saved file to apply unit formatting
            try:
                wb2 = load_workbook(input_file, data_only=True)
                ws2 = wb2["INPUT"] if "INPUT" in wb2.sheetnames else wb2.active
                self.load_manual_input_table_from_input(ws2)
                wb2.close()
            except Exception:
                pass

            self.mark_synced()
            self.log(f"SYNC TO EXCEL ✓ — {len(screen_rows)} rows written to "
                     f"{os.path.basename(input_file)}")

        except PermissionError:
            QMessageBox.critical(self, "FILE OPEN",
                "Close the INPUT file in Excel before syncing.")
            self.log("SYNC ERROR — file open in Excel")
        except Exception as e:
            QMessageBox.critical(self, "SYNC ERROR", str(e))
            self.log(f"SYNC ERROR: {str(e)}")

    def load_from_excel(self):
        """LOAD FROM EXCEL: xlsx → Tela. Sobrescreve a tela com os dados do xlsx."""
        input_file = self._resolve_input_file()
        if not input_file:
            QMessageBox.warning(self, "NO FILE",
                "No input file found.\nSelect a file via SELECT INPUT FILE.")
            return

        # Warn if there are unsaved changes on screen
        if hasattr(self, "mirror_status_label") and "UNSAVED" in self.mirror_status_label.text():
            reply = QMessageBox.question(
                self,
                "UNSAVED CHANGES",
                "The screen has unsaved changes that will be lost.\n\n"
                "Load from Excel anyway?",
                QMessageBox.Yes | QMessageBox.No,
            )
            if reply != QMessageBox.Yes:
                return

        try:
            wb = load_workbook(input_file, data_only=True)
            ws = wb["INPUT"] if "INPUT" in wb.sheetnames else wb.active

            # HOTFIX (29/06/2026): LOAD FROM EXCEL só carregava a tabela de
            # peças (load_manual_input_table_from_input), nunca os campos de
            # cabeçalho do projeto (B1/B2/B3/B4/B6-B11) — projeto, descrição,
            # endereço, cliente, número e data ficavam sempre com o valor
            # anterior da tela. Mesma leitura usada em load_input_file_preview
            # (linha ~3724), que já é comprovadamente correta.
            project_name   = ws["B1"].value or os.path.basename(input_file)
            unit_system    = str(ws["B2"].value or self.input_system_combo.currentText() or "IMPERIAL").upper()
            kerf_raw       = ws["B3"].value
            tolerance_raw  = ws["B4"].value
            project_client = str(ws["B6"].value or "").strip()
            project_addr   = str(ws["B7"].value or "").strip()
            project_desc   = str(ws["B8"].value or "").strip()
            project_no     = str(ws["B9"].value or "").strip()
            _date_raw      = ws["B10"].value
            import datetime as _dt
            if isinstance(_date_raw, (_dt.datetime, _dt.date)):
                project_date = _date_raw.strftime("%d/%m/%Y")
            else:
                project_date = str(_date_raw or "").strip()
            output_unit    = str(ws["B11"].value or "").strip()

            if unit_system in ["METRIC", "MÉTRICO", "METRICO"]:
                self.input_system_combo.setCurrentText("METRIC")
            else:
                self.input_system_combo.setCurrentText("IMPERIAL")

            if kerf_raw is not None:
                try:
                    self.kerf_input.setText(str(float(kerf_raw)))
                except (ValueError, TypeError):
                    pass
            if tolerance_raw is not None:
                try:
                    self.tolerance_input.setText(str(float(tolerance_raw)))
                except (ValueError, TypeError):
                    pass

            output_unit_map = {
                "decimal_inches": "Decimal inches",
                "fractional_inches": "Fractional inches",
                "feet_and_fractional_inches": "Feet and fractional inches",
                "decimal_feet": "Decimal feet",
                "m": "m", "cm": "cm", "mm": "mm",
            }
            display_label = output_unit_map.get(output_unit.lower(), "")
            if display_label and display_label in [
                self.output_display_combo.itemText(i)
                for i in range(self.output_display_combo.count())
            ]:
                self.output_display_combo.setCurrentText(display_label)

            self.set_strip_value(self.project_file_label,        str(project_name))
            self.set_strip_value(self.project_description_label, project_desc)
            self.set_strip_value(self.project_number_label,      project_no)
            if hasattr(self, "project_address_label"):
                self.set_strip_value(self.project_address_label, project_addr)
            if hasattr(self, "project_client_label"):
                self.set_strip_value(self.project_client_label,  project_client)
            if hasattr(self, "project_date_label"):
                self.set_strip_value(self.project_date_label,    project_date)

            if hasattr(self, "selected_input_label"):
                self.selected_input_label.setText(os.path.basename(input_file))

            self.update_settings_summary()
            self.load_manual_input_table_from_input(ws)
            wb.close()
            if not self.selected_input_file:
                self.selected_input_file = input_file
            self.mark_synced()
            self.log(f"LOAD FROM EXCEL ✓ — {os.path.basename(input_file)}")
        except PermissionError:
            QMessageBox.critical(self, "FILE OPEN",
                "Close the INPUT file in Excel before loading.")
        except Exception as e:
            QMessageBox.critical(self, "LOAD ERROR", str(e))
            self.log(f"LOAD ERROR: {str(e)}")

    def update_dashboard_from_outputs(self):
        # ── DO NOT REMOVE this def — called by run_project after pipeline ──

        outputs_dir = self.get_outputs_dir()
        result_path = os.path.join(outputs_dir, "result.xlsx")
        cost_path = os.path.join(outputs_dir, "cost_result.xlsx")
        audit_path = os.path.join(outputs_dir, "audit_engcut.xlsx")

        total_bars = 0
        total_pieces_report = 0
        total_material = 0.0
        total_waste = 0.0
        bar_rows = []
        runtime_seconds = 0.0

        # -----------------------------------------------------
        # RESULT.XLSX → TOTAL BARS / PIECES / WASTE / YIELD
        # -----------------------------------------------------
        import re as _re_dash

        def _parse_inch_frac(v):
            """Converte '106 13/16"' → 106.8125 preservando a fração.
            parse_display_number trunca para 106.0; esta função é precisa."""
            if v is None:
                return 0.0
            s = str(v).replace('"', '').strip()
            m = _re_dash.match(r'^(\d+)\s+(\d+)/(\d+)$', s)
            if m:
                return int(m.group(1)) + int(m.group(2)) / int(m.group(3))
            m2 = _re_dash.match(r'^(\d+)/(\d+)$', s)
            if m2:
                return int(m2.group(1)) / int(m2.group(2))
            try:
                return float(s.replace(',', '.'))
            except Exception:
                return 0.0

        if os.path.exists(result_path):
            wb_result = load_workbook(result_path, data_only=True)

            if "REPORT" in wb_result.sheetnames:
                ws = wb_result["REPORT"]

                for row in ws.iter_rows(min_row=2, values_only=True):
                    if not row or not row[0]:
                        continue
                    # Skip piece-data rows — bar group rows have numeric qty_bars in col D (index 3)
                    # Piece rows have piece names in col A and no bar combination in col B
                    if row[1] is None:
                        continue
                    # Skip if col D (qty_bars) is not numeric — it's a piece row
                    try:
                        _qty_test = int(self.parse_display_number(row[3]))
                    except Exception:
                        continue
                    if _qty_test == 0:
                        continue

                    cut_group   = row[0]
                    solution_id = row[2] if len(row) > 2 else ""   # col C — Solution ID
                    qty_bars    = int(self.parse_display_number(row[3])) if len(row) > 3 else 0
                    qty_pieces  = int(self.parse_display_number(row[4])) if len(row) > 4 else 0
                    material    = _parse_inch_frac(row[7]) if len(row) > 7 else 0.0
                    waste       = _parse_inch_frac(row[9]) if len(row) > 9 else 0.0
                    yield_raw   = row[10] if len(row) > 10 else None  # col K — Yield Operacional

                    total_bars += qty_bars
                    total_pieces_report += qty_pieces
                    total_material += material
                    total_waste += waste

                    if qty_bars > 0:
                        # ── Coluna Size: usa Solution ID (col C) do REPORT ───────────────
                        # O Solution ID já contém a descrição completa e correta,
                        # ex: "(4.70 ft x3)" ou "(11.70 ft x5) + (15.00 ft x20)".
                        # É a informação que o operador precisa ver para entender
                        # exatamente quais barras foram usadas em cada grupo.
                        size_val = str(solution_id).strip() if solution_id else ""

                        # Format yield as PT-BR percentage
                        try:
                            y = float(str(yield_raw).replace(",", ".")) if yield_raw is not None else None
                            if y is not None:
                                if y <= 1.0:
                                    y = y * 100
                                yield_str = f"{y:.2f}%".replace(".", ",")
                            else:
                                yield_str = ""
                        except (ValueError, TypeError):
                            yield_str = ""
                        bar_rows.append((str(cut_group), qty_bars, size_val, yield_str))

            if "RANKING" in wb_result.sheetnames:
                ws_rank = wb_result["RANKING"]
                for row in ws_rank.iter_rows(min_row=2, values_only=True):
                    if row and len(row) >= 12:
                        runtime_seconds += self.parse_display_number(row[11])

        self.update_bar_mix_preview(
            rows=bar_rows,
            total=total_bars,
        )

        waste_percent = (total_waste / total_material) if total_material else None

        # -----------------------------------------------------
        # COST_RESULT.XLSX → COST / PIECE VALUE / WASTE VALUE
        # -----------------------------------------------------
        total_cost = 0.0
        pieces_value = 0.0
        fabricated_pieces = None
        waste_value_total = 0.0
        stock_reusable_value = 0.0
        scrap_value = 0.0
        stock_reuse_value = 0.0   # (+) Retalho consumido do estoque (custo, não crédito)

        if os.path.exists(cost_path):
            wb_cost = load_workbook(cost_path, data_only=True)

            if "COST_PROJECT" in wb_cost.sheetnames:
                ws = wb_cost["COST_PROJECT"]
                for row in ws.iter_rows(values_only=True):
                    label = str(row[0] or "").strip().lower() if row else ""
                    value = self.parse_display_number(row[1]) if row and len(row) > 1 else 0.0

                    if "material comprado" in label:
                        total_cost = value
                    elif "preço total peças" in label or "preco total pecas" in label:
                        pieces_value = value
                    elif "retalho consumido" in label:
                        stock_reuse_value = value
                    elif "retalhos gerados" in label:
                        stock_reusable_value = value
                    elif "junkyard" in label or "ferro velho" in label:
                        scrap_value = value

            if "COST_PIECES" in wb_cost.sheetnames:
                ws = wb_cost["COST_PIECES"]
                fabricated_pieces = max(0, ws.max_row - 4)

            if "COST_BARS" in wb_cost.sheetnames:
                ws = wb_cost["COST_BARS"]
                # column K = Waste Value
                for row in ws.iter_rows(min_row=2, values_only=True):
                    if row and len(row) >= 11:
                        waste_value_total += self.parse_display_number(row[10])

        if fabricated_pieces is None:
            fabricated_pieces = total_pieces_report

        self.cost_value.setText(
            self.format_money(total_cost if total_cost > 0 else stock_reuse_value)
        )

        self.update_pieces_summary(
            pieces=fabricated_pieces,
            value=self.format_money(pieces_value),
        )

        # Fallback: preserva scrap_value para _net_value quando sem dados de COST_PROJECT.
        if waste_value_total and not scrap_value and not stock_reusable_value:
            scrap_value = waste_value_total

        # RESÍDUO TOTAL card: exibir CUSTO ao preço de BARRA (base consistente com %)
        # waste_value_total = custo total do resíduo ao preço de compra = 1.66% × material
        # scrap_value (= $0.38 recovery income) fica apenas em _net_value e DETALHAMENTO
        _waste_bar = waste_value_total if waste_value_total else max(0.0, stock_reusable_value + scrap_value)
        _scrap_bar = max(0.0, _waste_bar - stock_reusable_value)

        self.update_waste_summary(
            percent=self.format_percent(waste_percent),
            reusable=self.format_money(stock_reusable_value),
            scrap=self.format_money(_scrap_bar),
            total=self.format_money(_waste_bar),
            raw_reusable=stock_reusable_value,
            raw_scrap=_scrap_bar,
        )

        # Barras horizontais COST BREAKDOWN (4 barras: Líquido / Peças / Retalho / Sucata)
        # Fórmula: Motor (total_geral) = Material + Estoque Consumido − Sucata − Retalho
        _net_value = max(0.0, total_cost + stock_reuse_value - stock_reusable_value - scrap_value)
        try:
            _lm_leg = _load_engine_module("language_manager")
            _lbl_m = _lm_leg.t("cost_legend_material")
            _lbl_n = _lm_leg.t("cost_legend_net")
            _lbl_p = _lm_leg.t("cost_legend_pieces")
            _lbl_s = _lm_leg.t("cost_legend_reusable")
            _lbl_w = _lm_leg.t("cost_legend_scrap")
        except Exception:
            _lbl_m = "Material comprado"
            _lbl_n = "Líquido do projeto"
            _lbl_p, _lbl_s, _lbl_w = "Peças", "Retalho p/ Estoque", "Sucata/Descarte"
        self._update_cost_breakdown_bars(
            net_val=_net_value,
            pieces_val=max(0.0, pieces_value),
            stock_val=max(0.0, stock_reusable_value),
            scrap_val=max(0.0, scrap_value),
            material_val=max(0.0, total_cost),
            net_lbl=_lbl_n,
            pieces_lbl=_lbl_p,
            stock_lbl=_lbl_s,
            scrap_lbl=_lbl_w,
            material_lbl=_lbl_m,
        )

        # -----------------------------------------------------
        # AUDIT_ENGCUT.XLSX → AUDIT / UNCALCULATED
        # -----------------------------------------------------
        audit_failures = 0
        uncalculated_qty = 0
        audit_status = "Waiting"

        if os.path.exists(audit_path):
            wb_audit = load_workbook(audit_path, data_only=True)

            if "SUMMARY" in wb_audit.sheetnames:
                ws = wb_audit["SUMMARY"]
                for row in ws.iter_rows(values_only=True):
                    label = str(row[0] or "").strip().lower() if row else ""
                    value = self.parse_display_number(row[1]) if row and len(row) > 1 else 0.0

                    if "failures" in label:
                        audit_failures += int(value)

            if "UNCALCULATED" in wb_audit.sheetnames:
                ws = wb_audit["UNCALCULATED"]
                for row in ws.iter_rows(min_row=2, values_only=True):
                    if row and row[0] and "no" not in str(row[0]).lower():
                        qty = int(self.parse_display_number(row[6])) if len(row) > 6 else 1
                        uncalculated_qty += qty

            audit_status = "PASS" if audit_failures == 0 else f"FAIL ({audit_failures})"

        self.audit_value.setText(audit_status)
        self.uncalculated_value.setText(f"{uncalculated_qty} pieces")

        if runtime_seconds:
            self.runtime_value.setText(f"{runtime_seconds:.2f}s")

        # YIELD COST: eficiência financeira = custo de peças / custo total de barras
        # pieces_value / cost_basis = fração do custo de barras que efetivamente virou peças.
        # Fórmula anterior (material - scrap_recovery) / material incluía custo de resíduo
        # no numerador, superestimando eficiência (~1.46pp acima do YIELD CORTE).
        # Fallback: usa _net_value quando pieces_value não disponível (projetos legados).
        _cost_basis = total_cost + stock_reuse_value
        if _cost_basis > 0 and pieces_value > 0:
            _yield_cost = pieces_value / _cost_basis
        elif _cost_basis > 0:
            _yield_cost = _net_value / _cost_basis
        else:
            _yield_cost = 0.0
        self.yield_value.setText(self.format_percent(_yield_cost) if _cost_basis > 0 else "0 %")

        # YIELD CUT: eficiência de corte = 1 - waste%
        # Projetos 100% estoque: total_material=0 (sem barras novas no cutting report)
        # → proxy financeiro: 1 - scrap / (peças + scrap + retalho_estoque)
        if waste_percent is not None:
            _yield_cut = 1.0 - float(waste_percent)
        else:
            _total_out = pieces_value + scrap_value + stock_reusable_value
            _yield_cut = (1.0 - scrap_value / _total_out) if _total_out > 0 else None
        self.yield_cut_value.setText(
            self.format_percent(_yield_cut) if _yield_cut is not None else "0 %"
        )

        # Atualizar barra de progresso do YIELD com cor dinâmica (drive: YIELD CUT)
        if hasattr(self, "yield_progress") and _yield_cut is not None:
            _yp = max(0, min(100, int(_yield_cut * 100)))
            self.yield_progress.setValue(_yp)
            _ycolor = "#32ff7e" if _yp >= 85 else ("#FFD700" if _yp >= 70 else "#ff4444")
            self.yield_progress.setStyleSheet(f"""
                QProgressBar {{
                    background-color: #091017;
                    border: 1px solid #25415a;
                    border-radius: 5px;
                }}
                QProgressBar::chunk {{
                    background-color: {_ycolor};
                    border-radius: 4px;
                }}
            """)

        self._refresh_available_stock_display()
        self.log("DASHBOARD UPDATED FROM OUTPUT FILES")

    def run_project(self):

        # CORRECAO RISK-FE-003 (AUD-003):
        # Guard isRunning() como segunda linha de defesa contra dupla execucao.
        # A desabilitacao do botao (RISK-FE-002) e a primeira linha de defesa,
        # mas pode falhar em race conditions de double-click. Este guard e atomico.
        if hasattr(self, "_worker") and self._worker.isRunning():
            return

        if not _license_gate(self):
            return

        try:

            if not self.selected_input_file:
                self.selected_input_file = self._resolve_input_file()

            if not self.selected_input_file:
                QMessageBox.warning(
                    self,
                    "INPUT VALIDATION",
                    "No input file found.",
                )
                return

            # ── SYNC OBRIGATÓRIO: garante que o Excel reflita exatamente
            # o estado atual da UI antes de qualquer leitura pelo pipeline.
            # Isso é mandatório em VS Code E no .exe — sem exceção.
            # ─────────────────────────────────────────────────────────────
            # 1. Tabela de dados (linhas 13+): sync silencioso sem dialog
            sync_ok = self._sync_table_to_excel_silent()
            if not sync_ok:
                return  # erro de gravação — mensagem já exibida

            # 2. Configurações (B1-B4, B11): unit system, kerf, tolerance, output
            if not self.sync_ui_config_to_input():
                return

            # 3. Gate CRITERIOS: toda descrição do INPUT deve ter specs no CRITERIOS
            if not self._validate_criterios_for_run():
                return

            self.progress_bar.setVisible(True)
            self.progress_bar.setValue(10)

            try:
                _lm_run = _load_engine_module("language_manager")
                _status_running = f"STATUS: {_lm_run.t('status_running')}"
                _step1_msg = _lm_run.t("step_starting_pipeline")
            except Exception:
                _status_running = "STATUS: RUNNING..."
                _step1_msg = "STEP 1 - STARTING ENGCUT PIPELINE"

            self.status_label.setText(_status_running)

            self.log(_step1_msg)

            # ── Lança pipeline em thread separada (UI não trava) ──────────
            base_dir = _ui_base_dir()
            run_file = os.path.join(base_dir, "run_project.py")

            self._worker = PipelineWorker(
                sys.executable,
                run_file,
                self.selected_input_file,
                base_dir,
            )
            self._worker.log_line.connect(self.log)
            self._worker.finished.connect(self._on_pipeline_finished)

            # CORRECAO RISK-FE-002 (AUD-003):
            # Botao desabilitado ANTES de start() para eliminar a janela de race
            # condition onde um double-click entre start() e setEnabled(False)
            # criava um segundo worker sobrescrevendo self._worker.
            self.run_button.setEnabled(False)
            self._worker.start()

        except Exception as e:

            try:
                _lm_exc = _load_engine_module("language_manager")
                _s_err2 = _lm_exc.t("status_error")
                _s_log2 = _lm_exc.t("check_process_log")
            except Exception:
                _s_err2 = "ERROR"
                _s_log2 = "Check process log"

            self.status_label.setText(f"STATUS: {_s_err2}")

            if hasattr(self, "top_status_label"):
                self.top_status_label.setText(f"●  STATUS: {_s_err2}\n{_s_log2}")

            self.log(f"ERROR: {str(e)}")

            QMessageBox.critical(
                self,
                "ERROR",
                str(e),
            )

    def closeEvent(self, event):
        """
        CORRECAO RISK-FE-006 (AUD-003):
        A versao anterior nao tinha closeEvent, o que permitia fechar a janela
        enquanto o pipeline ainda rodava. Isso deixava o QThread orfao e podia
        causar gravacoes parciais no banco de dados.

        Comportamento corrigido:
          - Se nenhum pipeline rodando: fecha imediatamente.
          - Se pipeline rodando: pede confirmacao ao usuario.
            - Se usuario cancelar: ignora o evento (nao fecha).
            - Se usuario confirmar: encerra o worker graciosamente e fecha.
        """
        if hasattr(self, "_worker") and self._worker.isRunning():
            reply = QMessageBox.question(
                self,
                "Pipeline em execucao",
                "O pipeline esta rodando. Fechar agora pode corromper dados.\n\nFechar mesmo assim?",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No,
            )
            if reply == QMessageBox.No:
                event.ignore()
                return
            # Encerra thread graciosamente (aguarda ate 3s)
            self._worker.quit()
            self._worker.wait(3000)
        event.accept()

    def save_project(self):
        """
        Salva um snapshot do projeto calculado.
        Copia os 3 outputs (result, cost, audit), o INPUT e os 6 relatórios
        oficiais (01-06, regenerados a partir do estado atual) para uma
        subpasta de saved_projects/ nomeada com o projeto e a data/hora atual.
        Não altera nenhum arquivo de entrada existente — é uma cópia pura.
        """
        import shutil
        from datetime import datetime
        from PySide6.QtWidgets import QMessageBox

        if not _license_gate(self):
            return

        base_dir = _ui_base_dir()
        outputs_dir = os.path.join(base_dir, "outputs")

        # Verificar se há outputs para salvar
        result_path = os.path.join(outputs_dir, "result.xlsx")
        if not os.path.exists(result_path):
            QMessageBox.warning(
                self, "SAVE PROJECT",
                "Nenhum resultado encontrado.\n\nRode o projeto primeiro (RUN PROJECT)."
            )
            return

        # Nome do projeto ativo — lido do campo visível na tela
        project_name = self.get_project_name() or "PROJETO"
        safe_name = "".join(c if c.isalnum() or c in " _-" else "_" for c in project_name).strip()
        safe_name = safe_name[:60]

        # Formato: NOME_MM-DD-YYYY (ex: "PROJETO MARCELO 2_06-26-2026")
        date_str = datetime.now().strftime("%m-%d-%Y")
        folder_name = f"{safe_name}_{date_str}"

        # Salvar diretamente em saved_projects/ sem diálogo
        saved_root = os.path.join(base_dir, "saved_projects")
        os.makedirs(saved_root, exist_ok=True)
        snapshot_dir = os.path.join(saved_root, folder_name)
        os.makedirs(snapshot_dir, exist_ok=True)

        # Arquivos a copiar
        files_to_copy = [
            (os.path.join(outputs_dir, "result.xlsx"),      "result.xlsx"),
            (os.path.join(outputs_dir, "cost_result.xlsx"), "cost_result.xlsx"),
            (os.path.join(outputs_dir, "audit_engcut.xlsx"),"audit_engcut.xlsx"),
        ]

        # Copiar também o INPUT usado
        input_file = getattr(self, "selected_input_file", None) or \
                     os.path.join(base_dir, "inputs", "INPUT_sistema.xlsx")
        if input_file and os.path.exists(input_file):
            files_to_copy.append((input_file, "INPUT_sistema.xlsx"))

        copied = []
        failed = []
        for src, dest_name in files_to_copy:
            if os.path.exists(src):
                try:
                    shutil.copy2(src, os.path.join(snapshot_dir, dest_name))
                    copied.append(dest_name)
                except Exception as e:
                    failed.append(f"{dest_name}: {e}")
            else:
                failed.append(f"{dest_name}: não encontrado")

        # Gerar (atualizar) e copiar os 6 relatórios oficiais para snapshot/reports/
        report_files = [
            "01_Project_Summary.xlsx",
            "02_Purchase_Order.xlsx",
            "03_Cutting_Report.xlsx",
            "04_Stock_Report.xlsx",
            "05_Cost_Report.xlsx",
            "06_QR_Labels.xlsx",
        ]
        try:
            _rg = _load_engine_module("report_generator")
            _rg.generate_project_summary()
            _rg.generate_purchase_order()
            _rg.generate_cutting_report(active_tab="SUMMARY")
            _rg.generate_stock_report(active_tab="STOCK")
            _rg.generate_cost_report(active_tab="COST SUMMARY")
            _rg.generate_qr_labels()
        except Exception as e:
            failed.append(f"relatórios (geração): {e}")

        reports_src_dir = os.path.join(outputs_dir, "reports")
        reports_dest_dir = os.path.join(snapshot_dir, "reports")
        os.makedirs(reports_dest_dir, exist_ok=True)
        for name in report_files:
            src = os.path.join(reports_src_dir, name)
            if os.path.exists(src):
                try:
                    shutil.copy2(src, os.path.join(reports_dest_dir, name))
                    copied.append(f"reports/{name}")
                except Exception as e:
                    failed.append(f"reports/{name}: {e}")
            else:
                failed.append(f"reports/{name}: não encontrado")

        # Feedback
        msg = f"✓ Projeto salvo em:\n{snapshot_dir}\n\nArquivos copiados:"
        for f in copied:
            msg += f"\n  • {f}"
        if failed:
            msg += f"\n\n⚠ Não copiados:"
            for f in failed:
                msg += f"\n  • {f}"

        self.log(f"SAVE PROJECT ✓ — {snapshot_dir}")

        reply = QMessageBox.information(
            self, "SAVE PROJECT — OK", msg,
            QMessageBox.Ok | QMessageBox.Open,
            QMessageBox.Ok,
        )
        if reply == QMessageBox.Open:
            os.startfile(snapshot_dir)

    def _open_current_input(self):
        """Abre o arquivo INPUT atual no Excel (função primária do botão INPUT).
        Quando um template está ativo, abre o arquivo ORIGINAL do template (não a cópia INPUT_sistema)."""
        _tmpl = getattr(self, "_current_template_path", None)
        if _tmpl and os.path.exists(_tmpl):
            input_path = _tmpl
        else:
            input_path = self.selected_input_file or self._resolve_input_file()
        if input_path and os.path.exists(input_path):
            if sys.platform == "win32":
                os.startfile(input_path)
            elif sys.platform == "darwin":
                import subprocess as _sp
                _sp.Popen(["open", input_path])
            else:
                import subprocess as _sp
                _sp.Popen(["xdg-open", input_path])
        else:
            QMessageBox.warning(
                self, "INPUT NÃO ENCONTRADO",
                "Nenhum arquivo INPUT selecionado ou arquivo não encontrado.\n"
                "Use '◻  Selecionar novo arquivo INPUT...' para escolher um arquivo."
            )

    def open_result(self):

        self.open_file("../outputs/result.xlsx")

    def open_cost(self):

        self.open_file("../outputs/cost_result.xlsx")

    def open_audit(self):

        self.open_file("../outputs/audit_engcut.xlsx")

    def open_stock(self):
        """Modal: OPEN stock_database ou ADD TO STOCK."""
        stock_path = os.path.abspath(
            os.path.join(_ui_base_dir(), "database", "stock_database.xlsx")
        )
        try:
            _lm_sd = _load_engine_module("language_manager")
            _ts = _lm_sd.t
        except Exception:
            _ts = lambda k: k
        dialog = QDialog(self)
        dialog.setWindowTitle(_ts("stock_dialog_title"))
        dialog.setMinimumWidth(420)
        dialog.setStyleSheet("""
            QDialog { background-color: #0c1520; color: #d8eeff; }
            QLabel  { background: transparent; }
            QPushButton { border-radius: 7px; font-weight: bold;
                          font-size: 12px; padding: 10px 18px; min-height: 44px; }
        """)
        layout = QVBoxLayout()
        layout.setSpacing(12); layout.setContentsMargins(20, 20, 20, 20)
        title = QLabel(_ts("stock_dialog_header"))
        title.setStyleSheet("font-size: 14px; font-weight: bold; color: #4d8dff;")
        layout.addWidget(title)
        sep = QFrame(); sep.setFrameShape(QFrame.HLine)
        sep.setStyleSheet("color: #1e3d5a;"); layout.addWidget(sep)
        btn_open = QPushButton("▤  OPEN  stock_database.xlsx")
        btn_open.setStyleSheet("QPushButton{background-color:#1b2530;border:1px solid #31475c;color:#d8eeff;}QPushButton:hover{background-color:#253545;}")
        btn_add = QPushButton(_ts("stock_btn_add"))
        btn_add.setStyleSheet("QPushButton{background-color:#0f3a1a;border:1.5px solid #2ecc71;color:#2ecc71;}QPushButton:hover{background-color:#1a5428;}")
        btn_cancel = QPushButton(_ts("btn_cancel"))
        btn_cancel.setStyleSheet("QPushButton{background-color:transparent;border:1px solid #31475c;color:#7a9ab5;}")
        layout.addWidget(btn_open); layout.addWidget(btn_add); layout.addWidget(btn_cancel)
        dialog.setLayout(layout)
        btn_cancel.clicked.connect(dialog.reject)
        def do_open():
            dialog.accept()
            if os.path.exists(stock_path):
                import subprocess, sys as _sys
                if _sys.platform == "win32": os.startfile(stock_path)
                elif _sys.platform == "darwin": subprocess.Popen(["open", stock_path])
                else: subprocess.Popen(["xdg-open", stock_path])
            else:
                QMessageBox.warning(self, "NOT FOUND", "Not found:\n" + stock_path)
        def do_add():
            dialog.accept()
            self._add_to_stock_modal(stock_path)
        btn_open.clicked.connect(do_open); btn_add.clicked.connect(do_add)
        dialog.exec()

    def _add_to_stock_modal(self, stock_path):
        """Modal para adicionar barra nova ou retalho ao banco de estoque."""
        if not _license_gate(self):
            return
        from PySide6.QtWidgets import QRadioButton, QButtonGroup
        try:
            _lm_am = _load_engine_module("language_manager")
            _ta = _lm_am.t
        except Exception:
            _ta = lambda k: k
        dialog = QDialog(self)
        dialog.setWindowTitle(_ta("stock_add_dialog_title"))
        dialog.setMinimumWidth(500)
        dialog.setStyleSheet("""
            QDialog{background-color:#0c1520;color:#d8eeff;}
            QLabel{background:transparent;color:#b8d8f0;font-size:12px;}
            QLineEdit,QComboBox{background-color:#0d1a28;color:#d8eeff;
                border:1px solid #1e3d5a;border-radius:5px;
                padding:6px 8px;font-size:12px;min-height:28px;}
            QRadioButton{color:#d8eeff;font-size:12px;}
            QPushButton{border-radius:7px;font-weight:bold;
                font-size:12px;padding:9px 20px;min-height:40px;}
        """)
        layout = QVBoxLayout()
        layout.setSpacing(10); layout.setContentsMargins(20,18,20,18)
        title2 = QLabel(_ta("stock_add_dialog_header"))
        title2.setStyleSheet("font-size:13px;font-weight:bold;color:#2ecc71;")
        layout.addWidget(title2)
        sep2 = QFrame(); sep2.setFrameShape(QFrame.HLine)
        sep2.setStyleSheet("color:#1e3d5a;"); layout.addWidget(sep2)
        # Tipo
        type_layout = QHBoxLayout()
        rb_nova    = QRadioButton(_ta("stock_type_new_bar"))
        rb_retalho = QRadioButton(_ta("stock_type_scrap"))
        rb_nova.setChecked(True)
        rb_nova.setStyleSheet("color:#32ff7e;font-weight:bold;")
        rb_retalho.setStyleSheet("color:#ff7a2f;font-weight:bold;")
        bg = QButtonGroup(dialog)
        bg.addButton(rb_nova, 1); bg.addButton(rb_retalho, 2)
        lbl_t = QLabel(_ta("stock_type_lbl")); lbl_t.setStyleSheet("font-weight:bold;color:#4d8dff;")
        type_layout.addWidget(lbl_t); type_layout.addStretch()
        type_layout.addWidget(rb_nova); type_layout.addSpacing(16)
        type_layout.addWidget(rb_retalho)
        layout.addLayout(type_layout)
        sep3 = QFrame(); sep3.setFrameShape(QFrame.HLine)
        sep3.setStyleSheet("color:#1e3d5a;"); layout.addWidget(sep3)
        def addrow(lbl, w):
            h = QHBoxLayout(); l2 = QLabel(lbl); l2.setFixedWidth(140)
            h.addWidget(l2); h.addWidget(w); layout.addLayout(h)
        # Cut Group — carregado da aba CRITERIOS (mandante), não do INPUT do
        # projeto ativo. Mesma fonte da validação A13:A200 da coluna A do INPUT:
        # todo perfil cadastrado em CRITERIOS deve aparecer aqui, mesmo que
        # ainda não tenha sido usado em nenhum projeto.
        # Exibe a descrição do perfil (col A), mas o item data carrega o
        # Cut Group real (col C) — é o que é de fato gravado no banco.
        cb_group = QComboBox(); cb_group.setEditable(True)
        try:
            from openpyxl import load_workbook as _lw2
            _wb2 = _lw2(self.selected_input_file, data_only=True)
            _ws2 = _wb2["CRITERIOS"] if "CRITERIOS" in _wb2.sheetnames else _wb2.active
            _seen = set()
            for _rc2 in range(13, 201):
                _desc2 = str(_ws2.cell(_rc2, 1).value or "").strip()
                _cg2   = str(_ws2.cell(_rc2, 3).value or "").strip()
                if _desc2 and _desc2 not in _seen:
                    cb_group.addItem(_desc2, _cg2 or _desc2)
                    _seen.add(_desc2)
            _wb2.close()
            if cb_group.count() == 0:
                for _fb in ["2x10", "4x4", "6x6"]:
                    cb_group.addItem(_fb, _fb)
        except Exception:
            for _fb in ["2x10", "2x4", "4x4", "6x6", "Outros"]:
                cb_group.addItem(_fb, _fb)
        addrow("Cut Group:", cb_group)
        le_len = QLineEdit(); le_len.setPlaceholderText(_ta("stock_placeholder_length"))
        addrow(_ta("stock_length_lbl"), le_len)
        le_qty = QLineEdit("1")
        addrow(_ta("stock_qty_lbl"), le_qty)
        le_orig = QLineEdit(); le_orig.setPlaceholderText(_ta("stock_placeholder_origin"))
        addrow(_ta("stock_origin_lbl"), le_orig)
        le_obs = QLineEdit(); le_obs.setPlaceholderText(_ta("stock_placeholder_optional"))
        addrow(_ta("stock_observation_lbl"), le_obs)
        sep4 = QFrame(); sep4.setFrameShape(QFrame.HLine)
        sep4.setStyleSheet("color:#1e3d5a;"); layout.addWidget(sep4)
        # Importação em lote via aba INVENTORY do INPUT_sistema.xlsx
        btn_inv = QPushButton(_ta("inv_import_btn"))
        btn_inv.setStyleSheet(
            "QPushButton{background-color:#0d1f30;border:1.5px solid #4d8dff;"
            "color:#7ab4f5;font-size:11px;padding:6px;border-radius:6px;}"
            "QPushButton:hover{background-color:#1a3550;color:#ffffff;}"
        )
        layout.addWidget(btn_inv)
        sep5 = QFrame(); sep5.setFrameShape(QFrame.HLine)
        sep5.setStyleSheet("color:#1e3d5a;"); layout.addWidget(sep5)
        brow = QHBoxLayout()
        btn_sv = QPushButton(_ta("stock_save_btn"))
        btn_sv.setStyleSheet("QPushButton{background-color:#0f3a1a;border:1.5px solid #2ecc71;color:#2ecc71;}QPushButton:hover{background-color:#1a5428;}")
        btn_cn = QPushButton(_ta("btn_cancel"))
        btn_cn.setStyleSheet("QPushButton{background-color:transparent;border:1px solid #31475c;color:#7a9ab5;}")
        brow.addWidget(btn_cn); brow.addWidget(btn_sv)
        layout.addLayout(brow); dialog.setLayout(layout)
        btn_cn.clicked.connect(dialog.reject)
        btn_inv.clicked.connect(lambda: self._import_inventory_xlsx(dialog))

        def save_entry():
            _typed = cb_group.currentText().strip()
            _idx   = cb_group.findText(_typed)
            cg = str(cb_group.itemData(_idx)).strip() if _idx >= 0 and cb_group.itemData(_idx) else _typed
            ltxt = le_len.text().strip().replace(",", ".")
            qtxt = le_qty.text().strip()
            orig = le_orig.text().strip() or _ta("stock_origin_manual")
            obs  = le_obs.text().strip()
            tipo = "NOVA" if rb_nova.isChecked() else "RETALHO"
            if not cg:
                QMessageBox.warning(dialog, _ta("dlg_validation_title"), _ta("stock_validate_no_group")); return
            try:
                ln = float(ltxt)
                if ln <= 0: raise ValueError
            except ValueError:
                QMessageBox.warning(dialog, _ta("dlg_validation_title"), _ta("stock_validate_bad_length")); return
            try:
                qty = int(qtxt)
                if qty <= 0: raise ValueError
            except ValueError:
                QMessageBox.warning(dialog, _ta("dlg_validation_title"), _ta("stock_validate_bad_qty")); return
            try:
                import datetime as _dt3, sys as _sys, os as _os
                _engine_dir = _os.path.abspath(
                    _os.path.join(_os.path.dirname(os.path.abspath(__file__)), "..")
                )
                if _engine_dir not in _sys.path:
                    _sys.path.insert(0, _engine_dir)
                import importlib as _il
                import stock_repository as _sr
                import stock_backend_router as _sbr
                _il.reload(_sr)
                _il.reload(_sbr)
                _sr.ensure_stock_cost_rules_loaded_from_input_json()

                # Buscar peso e preço direto na aba CRITERIOS do INPUT
                # pelo cut_group — independente de o grupo estar no projeto ativo.
                _peso_unit = 0.0
                _preco_unit = 0.0
                _junk_unit = 0.0
                # Unit system lido do INPUT Excel (fonte de verdade). Não usar
                # STOCK_UNIT_SYSTEM que só é atualizado após o pipeline rodar
                # (input_001.json). Entrada manual antes do pipeline resultava em
                # fórmula METRIC aplicada a barra IMPERIAL: 40ft → 40×1000=40000mm
                # em vez de 40×12=480in — causando waste=40000-280=39720 errado.
                _unit_sys_raw = "METRIC"
                try:
                    from openpyxl import load_workbook as _lw_crit
                    # Fallback para o INPUT padrão se selected_input_file não disponível
                    _input_path = self.selected_input_file
                    if not _input_path or not _os.path.exists(str(_input_path)):
                        _input_path = _os.path.join(_engine_dir, "inputs", "INPUT_sistema.xlsx")
                    _wb_crit = _lw_crit(_input_path, data_only=True)
                    # Ler Unit_System da aba INPUT (independente de input_001.json)
                    _ws_inp_tab = _wb_crit["INPUT"] if "INPUT" in _wb_crit.sheetnames else _wb_crit.active
                    for _rw_us in _ws_inp_tab.iter_rows(min_row=1, max_row=15, values_only=True):
                        if _rw_us and str(_rw_us[0] or "").strip() == "Unit_System" and _rw_us[1]:
                            _unit_sys_raw = str(_rw_us[1]).strip()
                            break
                    _unit_sys = _unit_sys_raw.upper()
                    if "CRITERIOS" in _wb_crit.sheetnames:
                        _ws_crit = _wb_crit["CRITERIOS"]
                        for _rc in range(13, _ws_crit.max_row + 1):
                            _desc_val = str(_ws_crit.cell(_rc, 1).value or "").strip()
                            _pc_val   = str(_ws_crit.cell(_rc, 2).value or "").strip()
                            _cg_val   = str(_ws_crit.cell(_rc, 3).value or "").strip()
                            if _cg_val == cg or _pc_val == cg or _desc_val == cg:
                                if "IMPERIAL" in _unit_sys:
                                    _peso_unit  = float(_ws_crit.cell(_rc, 5).value or 0)
                                    _preco_unit = float(_ws_crit.cell(_rc, 6).value or 0)
                                    _junk_unit  = float(_ws_crit.cell(_rc, 7).value or 0)
                                else:
                                    _peso_unit  = float(_ws_crit.cell(_rc, 9).value or 0)
                                    _preco_unit = float(_ws_crit.cell(_rc, 10).value or 0)
                                    _junk_unit  = float(_ws_crit.cell(_rc, 11).value or 0)
                                break
                    _wb_crit.close()
                except Exception:
                    pass

                _now = _dt3.datetime.now()
                _ck  = cg.upper().replace(" ", "")
                _is_imp    = "IMPERIAL" in _unit_sys_raw.upper()
                _unit_disp = "ft" if _is_imp else "m"
                # Backend ativo (SQLite por padrão, ver stock_backend_router.py) —
                # load_stock() roteado, nunca o stock_repository legado direto,
                # senão a contagem de sequência ignora o banco real.
                _existing = _sbr.load_stock()
                _ids  = {s["stock_id"] for s in _existing}
                _cnt  = sum(
                    1 for s in _existing
                    if str(s.get("cut_group") or "").upper().replace(" ", "") == _ck
                )
                stock_items   = []
                history_items = []
                for _i in range(qty):
                    _sq = _cnt + _i + 1
                    _id = f"STK-{_ck}-{_sq:03d}"
                    while _id in _ids:
                        _sq += 1; _id = f"STK-{_ck}-{_sq:03d}"
                    _ids.add(_id)
                    _peso_total = round(_peso_unit * ln, 4) if _peso_unit else None
                    _valor      = round(_preco_unit * ln, 2) if _preco_unit else None
                    # length_internal sempre em mm (METRIC) ou in (IMPERIAL) —
                    # campo lido prioritariamente pelo backend SQLite
                    # (stock_backend_router._xlsx_dict_to_stock_item); sem ele
                    # o item gravado fica com comprimento incorreto no SQLite.
                    _len_internal = round(ln * 12.0, 6) if _is_imp else round(ln * 1000.0, 6)
                    stock_items.append({
                        "stock_id":        _id,
                        "status":          "AVAILABLE",
                        "cut_group":       cg,
                        "profile":         cg,
                        "length":          ln,
                        "length_internal": _len_internal,
                        "unit":            _unit_disp,
                        "source_project":  orig,
                        "source_bar_id":   tipo,
                        "created_at":      _now.isoformat(timespec="seconds"),
                        "observation":     obs,
                        "linear_weight":   _peso_unit  or None,
                        "weight":          _peso_total,
                        "value":           _valor,
                        "residual_value":  _valor,
                    })
                    history_items.append({
                        "timestamp":            _now.isoformat(timespec="seconds"),
                        "event":                "ENTRADA_MANUAL",
                        "stock_id":             _id,
                        "cut_group":            cg,
                        "stock_length":         ln,
                        "stock_length_display": ln,
                        # report_generator.py divide por 12 (IMPERIAL) ou 1000 (METRIC)
                        # ao ler do SQLite — gravar em unidades internas (in ou mm),
                        # igual ao padrão de stock_consumer.py.
                        "remnant_length":       _len_internal,
                        # KM-022 SV-008: ENTRADA_MANUAL não produz peça cortada;
                        # a barra inteira vai para estoque. piece_length DEVE ser 0.
                        "piece_length":         0,
                        "piece_length_display": 0,
                        "unit":                 _unit_disp,
                        "project":              orig,
                        "source_bar_id":        tipo,
                        "operator":             "Entrada Manual",
                        "old_status":           "",
                        "new_status":           "AVAILABLE",
                    })
                # Gravação via stock_backend_router — roteia para o backend
                # ativo (SQLite por padrão). Chamar stock_repository direto
                # gravava sempre no XLSX legado, que os relatórios não leem
                # quando o backend ativo é SQLite (item 1 do bug report).
                _sbr.append_stock(stock_items)
                _sbr.append_history(history_items)

                # Dispositivo de detecção automática (stock_entry_validator.py)
                # Verifica integridade de cada entrada recém-gravada no SQLite.
                # SV-001 a SV-010: length > 0, range plausível, financeiros,
                # HISTORY existente, piece_length=0 para ENTRADA_MANUAL, etc.
                try:
                    from stock_entry_validator import (
                        validate_stock_entries, format_violations
                    )
                    _ids_added = [itm["stock_id"] for itm in stock_items]
                    _viol = validate_stock_entries(_ids_added, unit_system=_unit_sys_raw)
                    if _viol:
                        _crit = [vv for vv in _viol if vv.severity == "CRITICAL"]
                        _errs = [vv for vv in _viol if vv.severity == "ERROR"]
                        _msg  = format_violations(_viol)
                        self.log(f"[STOCK VALIDATOR] {len(_viol)} violação(ões) detectada(s):\n{_msg}")
                        if _crit or _errs:
                            QMessageBox.warning(
                                self,
                                "Alerta de Integridade — ENGCUT",
                                f"Inconsistências detectadas no registro de estoque:\n\n{_msg}"
                                "\n\nVerifique o banco de dados antes de prosseguir.",
                            )
                    else:
                        self.log("[STOCK VALIDATOR] OK — entradas verificadas sem violações")
                except Exception as _ve:
                    self.log(f"[STOCK VALIDATOR] Erro no validador: {_ve}")

                dialog.accept()
                self._refresh_available_stock_display()
                self.log(f"STOCK: {qty}x {cg} {ln}{_unit_disp} ({tipo}) - {orig}")
                QMessageBox.information(self, _ta("stock_updated_title"),
                    str(qty) + " barra(s) adicionada(s).\nCut Group: " + cg + " | Comprimento: " + str(ln))
            except Exception as _exc:
                import traceback as _tb
                QMessageBox.critical(dialog, _ta("stock_save_error_title"),
                    "Erro:\n" + str(_exc) + "\n\n" + _tb.format_exc()[:800])

        btn_sv.clicked.connect(save_entry)
        dialog.exec()

    # ── Inventory Import ──────────────────────────────────────────────────────

    def _import_inventory_xlsx(self, parent_dialog=None, source_path=None):
        """
        Importa em lote a aba INVENTORY para o banco de stock.

        Prioridade de fonte:
          1. source_path (passado explicitamente)
          2. inputs/INVENTORY.xlsx (standalone, independente de template)
          3. Aba INVENTORY do INPUT ativo (fallback legado)

        Fluxo:
          1. Abre a fonte e localiza a aba INVENTORY.
          2. Lê linhas cujo campo ID (col A) está vazio → são itens novos.
          3. Para cada linha nova: gera IDs INV-{CG}-NNN, grava no banco SQLite.
          4. Escreve range de IDs e STATUS="IMPORTED" de volta na planilha.
          5. Exibe resumo ao usuário.

        Anti-duplicata: linhas com ID preenchido são ignoradas — nunca reimportadas.
        Unidade: lida da coluna UNIDADE da própria linha (ft/m/in/cm/mm).
        Preços: lookup por DESCRIÇÃO (col A do CRITERIOS) — mais preciso que CUT GROUP.
        IDs: prefixo INV- distingue de entrada manual (STK-) e drops de corte (STK00NNN).
        """
        import sys as _sys, os as _os, traceback as _tb
        from PySide6.QtWidgets import QMessageBox

        _engine_dir = _os.path.abspath(
            _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..")
        )
        if _engine_dir not in _sys.path:
            _sys.path.insert(0, _engine_dir)

        # ── Resolver arquivo fonte da INVENTORY ───────────────────────────────
        _standalone = _os.path.join(_engine_dir, "inputs", "INVENTORY.xlsx")
        if source_path and _os.path.exists(str(source_path)):
            _inv_file = str(source_path)
        elif _os.path.exists(_standalone):
            _inv_file = _standalone
        else:
            # fallback: aba INVENTORY dentro do INPUT ativo
            _inv_file = getattr(self, "selected_input_file", None)
            if not _inv_file or not _os.path.exists(str(_inv_file)):
                _inv_file = _os.path.join(_engine_dir, "inputs", "INPUT_sistema.xlsx")

        if not _os.path.exists(_inv_file):
            try:
                _lm_inv = _load_engine_module("language_manager")
            except Exception:
                _lm_inv = None
            _msg_no = _lm_inv.t("inv_no_file") if _lm_inv else "No INVENTORY file found."
            QMessageBox.warning(parent_dialog or self, "INVENTORY", _msg_no)
            return

        # ── Arquivo de CRITERIOS: sempre do INPUT ativo (tem specs/preços) ────
        _crit_file = getattr(self, "selected_input_file", None)
        if not _crit_file or not _os.path.exists(str(_crit_file)):
            _crit_file = _os.path.join(_engine_dir, "inputs", "INPUT_sistema.xlsx")

        try:
            from openpyxl import load_workbook as _lwb
            import stock_backend_router as _sbr
            import stock_repository as _sr
            from datetime import datetime as _dt

            # ── 1. Ler aba INVENTORY ──────────────────────────────────────────
            wb_r = _lwb(_inv_file, data_only=True)
            if "INVENTORY" not in wb_r.sheetnames:
                wb_r.close()
                try:
                    _lm_inv2 = _load_engine_module("language_manager")
                    _msg_ns = _lm_inv2.t("inv_no_sheet")
                except Exception:
                    _msg_ns = "INVENTORY tab not found in:\n" + str(_inv_file)
                QMessageBox.warning(parent_dialog or self, "INVENTORY", _msg_ns)
                return

            ws_inv = wb_r["INVENTORY"]

            # ── 2. Ler CRITERIOS do INPUT ativo — lookup por DESCRIÇÃO ────────
            # Chave = Descrição (col A), mais precisa que CUT GROUP para homônimos.
            ws_crit = None
            if _os.path.exists(_crit_file):
                try:
                    _wb_crit = _lwb(_crit_file, data_only=True)
                    ws_crit  = _wb_crit["CRITERIOS"] if "CRITERIOS" in _wb_crit.sheetnames else None
                except Exception:
                    ws_crit = None

            # ── 3. Carregar estoque existente para sequência de IDs ───────────
            _existing_stock = _sbr.load_stock()
            _now_iso = _dt.now().isoformat(timespec="seconds")

            # Mapa: cg_clean → próximo seq INV-
            def _next_inv_seq(cg_clean):
                prefix = f"INV-{cg_clean}-"
                max_n = 0
                for s in _existing_stock:
                    sid = str(s.get("stock_id", ""))
                    if sid.startswith(prefix):
                        try:
                            max_n = max(max_n, int(sid[len(prefix):]))
                        except ValueError:
                            pass
                return max_n + 1

            # ── 4. Mapear preços do CRITERIOS por DESCRIÇÃO (col A) ──────────
            # Descrição é mais precisa que CUT GROUP — evita homônimos.
            # Fallback por CUT GROUP se descrição não bater.
            _crit_map = {}  # desc_upper → {peso, preco, junk}; também indexado por cg_upper
            if ws_crit:
                for _rc in range(13, ws_crit.max_row + 1):
                    _desc_v = str(ws_crit.cell(_rc, 1).value or "").strip().upper()  # col A
                    _cg_v   = str(ws_crit.cell(_rc, 3).value or "").strip().upper()  # col C
                    _entry  = {
                        "peso_imp":  _sr.safe_float(ws_crit.cell(_rc, 5).value),
                        "preco_imp": _sr.safe_float(ws_crit.cell(_rc, 6).value),
                        "junk_imp":  _sr.safe_float(ws_crit.cell(_rc, 7).value),
                        "peso_met":  _sr.safe_float(ws_crit.cell(_rc, 9).value),
                        "preco_met": _sr.safe_float(ws_crit.cell(_rc, 10).value),
                        "junk_met":  _sr.safe_float(ws_crit.cell(_rc, 11).value),
                    }
                    if _desc_v and _desc_v not in _crit_map:
                        _crit_map[_desc_v] = _entry  # primário: por descrição
                    if _cg_v and _cg_v not in _crit_map:
                        _crit_map[_cg_v] = _entry    # fallback: por cut group

            # ── 5. Detectar layout ────────────────────────────────────────────
            # v3 (atual): header row 9, data row 10
            # v2-old: header row 7, data row 8
            # v1: header row 3, data row 4
            _c3 = str(ws_inv.cell(3, 1).value or "").strip().upper()
            _c7 = str(ws_inv.cell(7, 1).value or "").strip().upper()
            _c9 = str(ws_inv.cell(9, 1).value or "").strip().upper()

            if "ID" in _c9:
                # v3: A=ID B=ST C=NATUREZA D=ORIG E=DT F=DESC G=CG H=COMP I=UNID J=QTY
                #     K=PRECO_U L=PRECO_T M=TIPO N=OBS O=PROJ
                _data_min_row = 10
                _idx = {"id":0,"st":1,"nat":2,"orig":3,"dt":4,"desc":5,"cg":6,"comp":7,
                        "unid":8,"qty":9,"preco_u":10,"preco_t":11,"tipo":12,"obs":13,"proj":14}
                _is_v3 = True
            elif "ID" in _c7:
                # v2-old: A=ID B=ST C=NATUREZA D=DATA E=DESC F=CG G=COMP H=UNID I=QTY
                #         J=PRECO_U K=PRECO_T L=TIPO M=ORIG N=PROJ O=OBS
                _data_min_row = 8
                _idx = {"id":0,"st":1,"nat":2,"dt":3,"desc":4,"cg":5,"comp":6,
                        "unid":7,"qty":8,"preco_u":9,"preco_t":10,"tipo":11,"orig":12,"proj":13,"obs":14}
                _is_v3 = False
            else:
                # v1 legacy: A=ID B=STATUS C=DATA D=DESC E=CG F=COMP G=UNID H=QTD I=TIPO J=ORIG K=OBS
                _data_min_row = 4
                _idx = {"id":0,"st":1,"dt":2,"desc":3,"cg":4,"comp":5,
                        "unid":6,"qty":7,"tipo":8,"orig":9,"obs":10,
                        "nat":-1,"preco_u":-1,"preco_t":-1,"proj":-1}
                _is_v3 = False
            _is_v2_plus = "ID" in _c7 or "ID" in _c9  # v2-old ou v3 (tem NATUREZA)

            def _gcol(row, key, default=""):
                ci = _idx.get(key, -1)
                if ci < 0 or ci >= len(row):
                    return default
                return str(row[ci].value or "").strip()

            rows_to_import = []  # (row_idx, data_dict)
            for row in ws_inv.iter_rows(min_row=_data_min_row, values_only=False):
                cell_id  = row[_idx["id"]]
                _cg_ci   = _idx["cg"]
                _comp_ci = _idx["comp"]
                if _cg_ci >= len(row) or _comp_ci >= len(row):
                    continue
                cell_cg  = row[_cg_ci]
                cell_len = row[_comp_ci]
                if not cell_cg.value and not cell_len.value:
                    continue  # linha vazia
                _reprice_only = False
                if cell_id.value:
                    # Linha já importada: só reprecifica se for v3 e PREÇO UNITÁRIO ainda vazio.
                    # Isso corrige itens importados antes do fix do lookup de CRITERIOS.
                    _pu_ci = _idx.get("preco_u", -1)
                    _pu_existing = row[_pu_ci].value if 0 <= _pu_ci < len(row) else None
                    if not _is_v3 or _pu_existing is not None:
                        continue  # truly skip: não é v3 ou preço já preenchido
                    _reprice_only = True  # ID presente mas PU vazio → só writeback de preço

                _cg   = _gcol(row, "cg")
                _len  = _gcol(row, "comp").replace(",", ".")
                _unit = _gcol(row, "unid").lower()
                _qty  = _gcol(row, "qty", "1")
                _tipo = _gcol(row, "tipo", "NOVA").upper()
                _orig = _gcol(row, "orig", "Inventário")
                _desc = _gcol(row, "desc")
                _obs  = _gcol(row, "obs")
                _data = _gcol(row, "dt")

                if not _cg or not _len:
                    continue
                try:
                    _ln  = float(_len)
                    _qty = int(float(_qty))
                    if _ln <= 0 or _qty <= 0:
                        continue
                except (ValueError, TypeError):
                    continue

                rows_to_import.append({
                    "row_idx":      cell_id.row,
                    "cg":           _cg,
                    "ln":           _ln,
                    "unit":         _unit or "m",
                    "qty":          _qty,
                    "tipo":         "NOVA" if "NOVA" in _tipo else "RETALHO",
                    "orig":         _orig or "Inventário",
                    "desc":         _desc,
                    "obs":          _obs,
                    "data":         _data,
                    "is_v3":        _is_v3,
                    "is_v2p":       _is_v2_plus,
                    "reprice_only": _reprice_only,
                    "existing_id":  str(cell_id.value).strip() if _reprice_only else None,
                })

            wb_r.close()

            if not rows_to_import:
                QMessageBox.information(
                    parent_dialog or self, "INVENTORY",
                    "Nenhuma linha nova encontrada na aba INVENTORY.\n"
                    "Linhas com ID preenchido são ignoradas (já importadas)."
                )
                return

            # ── 5. Gerar IDs e gravar no banco ────────────────────────────────
            stock_items   = []
            history_items = []
            writeback     = []  # (row_idx, id_range, status)

            _seq_cache = {}  # cg_clean → seq atual

            UNIT_IS_IMP = {"ft", "in", "decimal_inches", "fractional_inches", "decimal_feet"}

            for rd in rows_to_import:
                _cg       = rd["cg"]
                _ck       = _cg.upper().replace(" ", "")
                _ln       = rd["ln"]
                _unit     = rd["unit"]
                _qty      = rd["qty"]
                _is_imp   = _unit in UNIT_IS_IMP

                # length_internal: mesmo critério do save_entry (fix bug entrada manual)
                if _is_imp:
                    _len_int  = round(_ln * 12.0, 6) if _unit == "ft" else round(_ln, 6)
                    _unit_db  = "ft"
                else:
                    _unit_db  = "m"
                    if _unit == "m":
                        _len_int = round(_ln * 1000.0, 6)
                    elif _unit == "cm":
                        _len_int = round(_ln * 10.0, 6)
                    else:  # mm
                        _len_int = round(_ln, 6)

                # Preços do CRITERIOS: lookup por descrição (primário) → cut group (fallback)
                _desc_key = rd["desc"].upper() if rd["desc"] else ""
                # Fallback usa _cg.upper().strip() (espaços preservados) para bater com a
                # chave _cg_v do mapa — _ck remove espaços e causaria miss silencioso.
                _crit = _crit_map.get(_desc_key) or _crit_map.get(_cg.upper().strip(), {})
                if _is_imp:
                    _peso_u  = _crit.get("peso_imp",  0.0)
                    _preco_u = _crit.get("preco_imp", 0.0)
                else:
                    _peso_u  = _crit.get("peso_met",  0.0)
                    _preco_u = _crit.get("preco_met", 0.0)

                _peso_tot = round(_peso_u  * _ln, 4) if _peso_u  else None
                _valor    = round(_preco_u * _ln, 2) if _preco_u else None

                # Sequência de IDs
                if rd.get("reprice_only"):
                    # ID já existe: apenas writeback de preço, sem criar novo stock
                    writeback.append({
                        "row":          rd["row_idx"],
                        "id_range":     None,   # não sobrescreve ID/STATUS existentes
                        "preco_u":      _preco_u,
                        "preco_t":      round(_preco_u * _ln * rd["qty"], 2) if _preco_u else 0.0,
                        "is_v3":        rd["is_v3"],
                        "is_v2p":       rd["is_v2p"],
                        "reprice_only": True,
                    })
                    continue

                if _ck not in _seq_cache:
                    _seq_cache[_ck] = _next_inv_seq(_ck)

                _first_id = None
                _last_id  = None
                for _i in range(_qty):
                    _seq = _seq_cache[_ck]
                    _id  = f"INV-{_ck}-{_seq:03d}"
                    _seq_cache[_ck] += 1
                    if _first_id is None:
                        _first_id = _id
                    _last_id = _id

                    stock_items.append({
                        "stock_id":        _id,
                        "status":          "AVAILABLE",
                        "cut_group":       _cg,
                        "profile":         _desc or _cg,
                        "length":          _ln,
                        "length_internal": _len_int,
                        "unit":            _unit_db,
                        "source_project":  rd["orig"],
                        "source_bar_id":   rd["tipo"],
                        "created_at":      _now_iso,
                        "observation":     rd["obs"],
                        "linear_weight":   _peso_u  or None,
                        "weight":          _peso_tot,
                        "value":           _valor,
                        "residual_value":  _valor,
                    })
                    history_items.append({
                        "timestamp":            _now_iso,
                        "event":                "INVENTORY_IMPORT",
                        "stock_id":             _id,
                        "cut_group":            _cg,
                        "stock_length":         _len_int,
                        "stock_length_display": _ln,
                        "remnant_length":       _len_int,
                        "piece_length":         _len_int,
                        "piece_length_display": _ln,
                        "unit":                 _unit_db,
                        "project":              rd["orig"],
                        "source_bar_id":        rd["tipo"],
                        "operator":             "Importação INVENTORY",
                        "old_status":           "",
                        "new_status":           "AVAILABLE",
                    })

                _id_range = _first_id if _first_id == _last_id else f"{_first_id} a {_last_id}"
                writeback.append({
                    "row":          rd["row_idx"],
                    "id_range":     _id_range,
                    "preco_u":      _preco_u,
                    "preco_t":      round(_preco_u * _ln * rd["qty"], 2) if _preco_u else 0.0,
                    "is_v3":        rd["is_v3"],
                    "is_v2p":       rd["is_v2p"],
                    "reprice_only": False,
                })

            if stock_items:
                _sbr.append_stock(stock_items)
            if history_items:
                _sbr.append_history(history_items)

            # ── 6. Write-back IDs + STATUS (+PREÇO) na planilha fonte ────────
            wb_w = _lwb(_inv_file)
            ws_w = wb_w["INVENTORY"]
            for _wb in writeback:
                _r = _wb["row"]
                if not _wb.get("reprice_only"):
                    # Item novo: grava ID e STATUS
                    ws_w.cell(_r, 1).value = _wb["id_range"]
                    ws_w.cell(_r, 2).value = "DISPONÍVEL"
                # Preço: gravado para novos E para reprecificação
                if _wb["is_v2p"]:
                    _pu_col = self._INV_COL_PU if _wb["is_v3"] else 10  # K ou J (v2-old)
                    _pt_col = self._INV_COL_PT if _wb["is_v3"] else 11  # L ou K (v2-old)
                    if _wb["preco_u"]:
                        ws_w.cell(_r, _pu_col).value = round(_wb["preco_u"], 4)
                    if _wb["preco_t"]:
                        ws_w.cell(_r, _pt_col).value = round(_wb["preco_t"], 2)
            wb_w.save(_inv_file)
            wb_w.close()

            # Atualizar resumo financeiro após import
            self._refresh_inventory_status(_inv_file)

            # ── 7. Atualizar display e notificar ──────────────────────────────
            self._refresh_available_stock_display()
            n_bars     = len(stock_items)
            n_reprice  = sum(1 for w in writeback if w.get("reprice_only"))
            n_rows     = len(rows_to_import)
            _src_label = _os.path.basename(_inv_file)
            _log_parts = []
            if n_bars:    _log_parts.append(f"{n_bars} barra(s) importada(s)")
            if n_reprice: _log_parts.append(f"{n_reprice} preço(s) atualizado(s)")
            self.log(f"INVENTORY: {' | '.join(_log_parts)} de {n_rows} linha(s)")
            _msg_lines = []
            if n_bars:
                _msg_lines.append(f"{n_bars} barra(s) adicionada(s) ao banco de estoque.")
            if n_reprice:
                _msg_lines.append(f"{n_reprice} item(ns) com preço atualizado (importados sem preço anteriormente).")
            _msg_lines += [f"\nLinhas processadas: {n_rows}", f"Fonte: {_src_label}"]
            if n_bars:
                _msg_lines.append("Essas barras serão consumidas pelo pipeline normalmente.")
            QMessageBox.information(
                parent_dialog or self,
                "INVENTORY IMPORTADO",
                "\n".join(_msg_lines),
            )
            # Recarregar delegate — novas descrições do inventário já disponíveis
            self._load_criterios_delegate_data()
            if parent_dialog:
                parent_dialog.accept()

        except Exception as _exc:
            QMessageBox.critical(
                parent_dialog or self,
                "ERRO — IMPORTAÇÃO INVENTORY",
                "Erro ao importar inventário:\n" + str(_exc) +
                "\n\n" + _tb.format_exc()[:1000]
            )

    # ── INVENTORY standalone ──────────────────────────────────────────────────

    def _open_standalone_inventory(self):
        """Abre inputs/INVENTORY.xlsx no Excel. Cria o arquivo se não existir."""
        import os as _os, sys as _sys
        _engine_dir = _os.path.abspath(
            _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..")
        )
        _inv_path = _os.path.join(_engine_dir, "inputs", "INVENTORY.xlsx")
        if not _os.path.exists(_inv_path):
            self._create_standalone_inventory(_inv_path)
        else:
            # Migrar layout antigo → v3 (header row 9) automaticamente
            try:
                from openpyxl import load_workbook as _lwb_chk
                _wb_chk = _lwb_chk(_inv_path, read_only=True, data_only=True)
                _ws_chk = _wb_chk["INVENTORY"] if "INVENTORY" in _wb_chk.sheetnames else None
                _needs_migrate = False
                _old_layout = ""
                if _ws_chk:
                    _c3 = str(_ws_chk.cell(3, 1).value or "").strip().upper()
                    _c7 = str(_ws_chk.cell(7, 1).value or "").strip().upper()
                    _c9 = str(_ws_chk.cell(9, 1).value or "").strip().upper()
                    if _c3.startswith("ID"):
                        _needs_migrate = True; _old_layout = "v1 (11 colunas)"
                    elif "ID" in _c7 and "ID" not in _c9:
                        _needs_migrate = True; _old_layout = "v2 (15 colunas, resumo anterior)"
                _wb_chk.close()
                if _needs_migrate:
                    from PySide6.QtWidgets import QMessageBox
                    _r = QMessageBox.question(self, "INVENTORY — Atualização de Layout",
                        f"O INVENTORY.xlsx está no layout antigo ({_old_layout}).\n\n"
                        "O ENGCUT usa agora o layout v3 com resumo financeiro por categoria\n"
                        "(AÇO / ALUMÍNIO / MADEIRA / TUBULAÇÃO / OUTROS).\n\n"
                        "Os dados importados estão preservados no banco de estoque.\n"
                        "Deseja recriar o arquivo no novo formato?",
                        QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
                    if _r == QMessageBox.Yes:
                        _os.remove(_inv_path)
                        self._create_standalone_inventory(_inv_path)
            except Exception:
                pass
        # Re-aplicar labels i18n no arquivo existente (garante idioma ativo mesmo sem recriar)
        self._localize_inventory_labels_file(_inv_path)
        # Atualizar STATUS/PROJETO antes de abrir
        self._refresh_inventory_status(_inv_path)
        if _os.path.exists(_inv_path):
            if _sys.platform == "win32":
                _os.startfile(_inv_path)
            elif _sys.platform == "darwin":
                import subprocess as _sp
                _sp.Popen(["open", _inv_path])
            else:
                import subprocess as _sp
                _sp.Popen(["xdg-open", _inv_path])

    # Layout v3 constants (shared between create/refresh/import)
    _INV_CATS     = ["Aço", "Alumínio", "Madeira", "Tubulação", "Outros"]
    # Col positions (1-based) for data rows in v3 layout
    _INV_COL_ID   = 1   # A auto
    _INV_COL_ST   = 2   # B auto STATUS
    _INV_COL_NAT  = 3   # C NATUREZA
    _INV_COL_ORIG = 4   # D ORIGEM
    _INV_COL_DT   = 5   # E DATA
    _INV_COL_DESC = 6   # F DESCRICAO
    _INV_COL_CG   = 7   # G CUT GROUP
    _INV_COL_COMP = 8   # H COMPRIMENTO
    _INV_COL_UNIT = 9   # I UNIDADE
    _INV_COL_QTY  = 10  # J QUANTIDADE
    _INV_COL_PU   = 11  # K PREÇO UNITÁRIO auto
    _INV_COL_PT   = 12  # L PREÇO TOTAL auto
    _INV_COL_TIPO = 13  # M TIPO
    _INV_COL_OBS  = 14  # N OBSERVACAO
    _INV_COL_PROJ = 15  # O PROJETO auto
    _INV_COL_REMN = 16  # P RETALHO GERADO auto
    _INV_HDR_ROW  = 9   # Header row
    _INV_DAT_ROW  = 10  # First data row

    def _localize_inventory_labels_file(self, path):
        """Re-aplica labels i18n nas linhas de metadados do INVENTORY.xlsx existente.
        Só escreve linhas 1-2, 3-7 (resumo) e 9 (cabeçalhos) — dados das linhas 10+ intocados."""
        import os as _os
        if not _os.path.exists(str(path)):
            return
        try:
            from openpyxl import load_workbook as _lwb
            _lm = _load_engine_module("language_manager")
            _active = getattr(self, "_current_lang_code", None)
            if _active and getattr(_lm, "_lang", "en") != _active:
                _lm.load(_active)
            _t = _lm.t

            _wb = _lwb(path)
            if "INVENTORY" not in _wb.sheetnames:
                _wb.close(); return
            _ws = _wb["INVENTORY"]

            # Linha 1: título
            _ws["A1"].value = _t("inv_title")
            # Linha 2: instrução
            _ws["A2"].value = _t("inv_instruction")
            # Linha 3: resumo label + categorias display
            _ws.cell(3, 1).value = _t("inv_summary_lbl")
            for _ci, _k in enumerate(
                ("inv_cat_steel", "inv_cat_aluminum", "inv_cat_wood", "inv_cat_pipe", "inv_cat_other"),
                start=3
            ):
                _ws.cell(3, _ci).value = _t(_k)
            # Linhas 4, 5, 7: totais e saldo
            _ws.cell(4, 1).value = _t("inv_total_available")
            _ws.cell(5, 1).value = _t("inv_total_used")
            _ws.cell(7, 1).value = _t("inv_balance_lbl")
            # Linha 9: cabeçalhos de colunas
            _hdr = [
                ("ID",                         True),
                ("STATUS",                     True),
                (_t("inv_col_nature"),         False),
                (_t("inv_col_origin"),         False),
                (_t("inv_col_date"),           False),
                (_t("inv_col_desc"),           False),
                ("CUT GROUP",                  False),
                (_t("inv_col_length"),         False),
                (_t("inv_col_unit"),           False),
                (_t("inv_col_qty"),            False),
                (_t("inv_col_unit_price"),     True),
                (_t("inv_col_total_price"),    True),
                (_t("inv_col_type"),           False),
                (_t("inv_col_obs"),            False),
                (_t("inv_col_project"),        True),
                (_t("inv_col_remnant"),        True),
            ]
            for _ci, (_lbl, _is_auto) in enumerate(_hdr, start=1):
                _c = _ws.cell(9, _ci)
                if _c.value is not None:  # só atualiza colunas que existem
                    _c.value = _lbl + ("\n(auto)" if _is_auto else "")

            _wb.save(path)
            _wb.close()
        except Exception:
            pass  # falha silenciosa — não bloqueia abertura do arquivo

    def _create_standalone_inventory(self, path):
        """Cria inputs/INVENTORY.xlsx — layout v3 (15 colunas + bloco resumo por categoria)."""
        try:
            from openpyxl import Workbook as _WB
            from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
            from openpyxl.utils import get_column_letter
            from openpyxl.worksheet.datavalidation import DataValidation
            _lm = _load_engine_module("language_manager")
            # Garantir idioma ativo da sessão (evita default "en" quando _internal/config.json ausente)
            _active_lang = getattr(self, "_current_lang_code", None)
            if _active_lang and getattr(_lm, "_lang", "en") != _active_lang:
                _lm.load(_active_lang)

            # Tema claro — fundo branco, navy apenas no título e cabeçalhos
            _NAVY      = PatternFill("solid", fgColor="0D2137")   # navy: título + cabeçalhos
            _WHITE     = PatternFill("solid", fgColor="FFFFFF")   # branco: células normais
            _LIGHT     = PatternFill("solid", fgColor="F0F4F8")   # azul-cinza bem claro: linhas alt
            _AUTO_FILL = PatternFill("solid", fgColor="FADADD")   # rosa claro: colunas auto
            _HDR_FILL  = _NAVY
            _HDR_FONT  = Font(bold=True, color="FFFFFF", size=10)
            _AUTO_FONT = Font(italic=True, color="8B0000", size=9)  # vermelho escuro: colunas auto
            _USR_FONT  = Font(color="000000", size=10)              # preto puro: colunas do usuário
            _THIN = Side(style="thin", color="B0C4D8")
            _BDR  = Border(left=_THIN, right=_THIN, top=_THIN, bottom=_THIN)
            _CTR  = Alignment(horizontal="center", vertical="center", wrap_text=True)
            _LFT  = Alignment(horizontal="left",   vertical="center")
            _RGT  = Alignment(horizontal="right",  vertical="center")
            _MONEY_FONT = Font(name="Calibri", size=10, color="000000")

            # (label, width, is_auto)  ← ordem definitiva
            _COLS = [
                ("ID",                              14, True),   # A
                ("STATUS",                          13, True),   # B
                (_lm.t("inv_col_nature"),           13, False),  # C
                (_lm.t("inv_col_origin"),           20, False),  # D
                (_lm.t("inv_col_date"),             13, False),  # E
                (_lm.t("inv_col_desc"),             26, False),  # F
                ("CUT GROUP",                       14, False),  # G
                (_lm.t("inv_col_length"),           12, False),  # H
                (_lm.t("inv_col_unit"),              9, False),  # I
                (_lm.t("inv_col_qty"),              10, False),  # J
                (_lm.t("inv_col_unit_price"),       14, True),   # K auto
                (_lm.t("inv_col_total_price"),      13, True),   # L auto
                (_lm.t("inv_col_type"),             10, False),  # M
                (_lm.t("inv_col_obs"),              22, False),  # N
                (_lm.t("inv_col_project"),          20, True),   # O auto
                (_lm.t("inv_col_remnant"),          24, True),   # P auto
            ]
            _NC = len(_COLS)  # 16
            _LAST_COL = get_column_letter(_NC)
            _CATS = self._INV_CATS  # ["Aço","Alumínio","Madeira","Tubulação","Outros"] — PT codes (SUMIFS)
            _CAT_DISP = [_lm.t(k) for k in (
                "inv_cat_steel", "inv_cat_aluminum", "inv_cat_wood",
                "inv_cat_pipe", "inv_cat_other",
            )]

            wb = _WB()
            ws = wb.active
            ws.title = "INVENTORY"

            # ── Linha 1: título ───────────────────────────────────────────────
            ws.row_dimensions[1].height = 28
            ws.merge_cells(f"A1:{_LAST_COL}1")
            _t = ws["A1"]
            _t.value = _lm.t("inv_title")
            _t.font  = Font(bold=True, color="7AB4F5", size=12)
            _t.fill  = _NAVY; _t.alignment = _CTR

            # ── Linha 2: instrução ────────────────────────────────────────────
            ws.row_dimensions[2].height = 20
            ws.merge_cells(f"A2:{_LAST_COL}2")
            _n = ws["A2"]
            _n.value = _lm.t("inv_instruction")
            _n.font  = Font(italic=True, color="4A7A9B", size=9)
            _n.fill  = _WHITE; _n.alignment = _LFT

            # ── Bloco resumo por categoria (linhas 3-7) ───────────────────────
            _DISP_CLR = "27AE60"
            _UTIL_CLR = "E74C3C"
            _SALD_CLR = "9B59B6"

            # Linha 3: cabeçalho do resumo (navy — igual header de colunas)
            ws.row_dimensions[3].height = 24
            ws.merge_cells("A3:B3")
            _c = ws.cell(3, 1); _c.value = _lm.t("inv_summary_lbl")
            _c.font = Font(bold=True, color="FFFFFF", size=11)
            _c.fill = _NAVY; _c.alignment = _CTR
            for _ci, _cat_disp in enumerate(_CAT_DISP, start=3):  # C=3 .. G=7
                _cc = ws.cell(3, _ci)
                _cc.value = _cat_disp
                _cc.font  = Font(bold=True, color="FFFFFF", size=10)
                _cc.fill  = _NAVY; _cc.alignment = _CTR; _cc.border = _BDR

            # Fórmulas SUMIFS por categoria — atualizam automaticamente a cada edição no Excel
            # Categorias: Aço(C), Alumínio(D), Madeira(E), Tubulação(F), Outros(G)
            _CAT_NAMES = [c for c in _CATS]  # preserva ordem original
            _CAT_COLS  = [get_column_letter(3 + i) for i in range(len(_CAT_NAMES))]

            # Linha 4: TOTAL DISPONÍVEL — fundo branco
            ws.row_dimensions[4].height = 22
            ws.merge_cells("A4:B4")
            _c = ws.cell(4, 1); _c.value = _lm.t("inv_total_available")
            _c.font = Font(bold=True, color=_DISP_CLR, size=11)
            _c.fill = _WHITE; _c.alignment = _LFT
            for _ci, (_cat_name, _col_l) in enumerate(zip(_CAT_NAMES, _CAT_COLS), start=3):
                _cc = ws.cell(4, _ci)
                _cc.value = f'=SUMIFS($L$10:$L$5000,$C$10:$C$5000,"{_cat_name}",$B$10:$B$5000,"DISPONÍVEL")'
                _cc.font = Font(bold=True, color=_DISP_CLR, size=10)
                _cc.fill = _WHITE; _cc.number_format = '"$"#,##0.00'; _cc.alignment = _CTR; _cc.border = _BDR

            # Linha 5: TOTAL UTILIZADO — fundo branco
            ws.row_dimensions[5].height = 22
            ws.merge_cells("A5:B5")
            _c = ws.cell(5, 1); _c.value = _lm.t("inv_total_used")
            _c.font = Font(bold=True, color=_UTIL_CLR, size=11)
            _c.fill = _WHITE; _c.alignment = _LFT
            for _ci, _cat_name in enumerate(_CAT_NAMES, start=3):
                _cc = ws.cell(5, _ci)
                _cc.value = f'=SUMIFS($L$10:$L$5000,$C$10:$C$5000,"{_cat_name}",$B$10:$B$5000,"UTILIZADO")'
                _cc.font = Font(bold=True, color=_UTIL_CLR, size=10)
                _cc.fill = _WHITE; _cc.number_format = '"$"#,##0.00'; _cc.alignment = _CTR; _cc.border = _BDR

            # Linha 6: separador (branco, altura mínima)
            ws.row_dimensions[6].height = 6

            # Linha 7: SALDO GLOBAL — fórmulas (DISPONÍVEL − UTILIZADO por categoria)
            ws.row_dimensions[7].height = 24
            _c = ws.cell(7, 1); _c.value = _lm.t("inv_balance_lbl")
            _c.font = Font(bold=True, color=_SALD_CLR, size=11)
            _c.fill = _WHITE; _c.alignment = _LFT
            for _ci, _col_l in enumerate(_CAT_COLS, start=3):
                _cc = ws.cell(7, _ci)
                _cc.value = f"={_col_l}4-{_col_l}5"
                _cc.font = Font(bold=True, color=_SALD_CLR, size=10)
                _cc.fill = _WHITE; _cc.number_format = '"$"#,##0.00'; _cc.alignment = _CTR; _cc.border = _BDR
            # B7: soma de todos os saldos por categoria
            _saldo_sum = "+".join([f"{_col_l}7" for _col_l in _CAT_COLS])
            _ct = ws.cell(7, 2); _ct.value = f"={_saldo_sum}"
            _ct.font = Font(bold=True, color=_SALD_CLR, size=11)
            _ct.fill = _WHITE; _ct.number_format = '"$"#,##0.00'; _ct.alignment = _CTR; _ct.border = _BDR

            # Linha 8: separador (branco)
            ws.row_dimensions[8].height = 8

            # ── Linha 9: cabeçalho das colunas ────────────────────────────────
            ws.row_dimensions[9].height = 36
            for ci, (label, width, is_auto) in enumerate(_COLS, start=1):
                c = ws.cell(9, ci)
                c.value = label + ("\n(auto)" if is_auto else "")
                c.font  = _HDR_FONT; c.fill = _HDR_FILL
                c.alignment = _CTR;  c.border = _BDR
                ws.column_dimensions[get_column_letter(ci)].width = width

            # ── Validações de dados ───────────────────────────────────────────
            _dv_sqref_fmt = "{}10:{}5000"

            dv_n = DataValidation(type="list",
                                  formula1='"Aço,Madeira,Alumínio,Tubulação,Outros"',
                                  allow_blank=True, showDropDown=False)
            dv_n.showErrorMessage = False
            ws.add_data_validation(dv_n)
            dv_n.sqref = _dv_sqref_fmt.format("C", "C")

            dv_u = DataValidation(type="list", formula1='"ft,m,in,cm,mm"',
                                  allow_blank=True, showDropDown=False)
            dv_u.showErrorMessage = False
            ws.add_data_validation(dv_u)
            dv_u.sqref = _dv_sqref_fmt.format("I", "I")

            dv_t = DataValidation(type="list", formula1='"NOVA,RETALHO"',
                                  allow_blank=True, showDropDown=False)
            dv_t.showErrorMessage = False
            ws.add_data_validation(dv_t)
            dv_t.sqref = _dv_sqref_fmt.format("M", "M")

            # ── 5 linhas de dados estilizadas (linhas 10-14) ──────────────────
            # Auto cols (rosa): A=1 B=2 K=11 L=12 O=15 P=16
            # Linhas pares=branco, ímpares=azul-cinza claro
            _AUTO_COLS  = {1, 2, 11, 12, 15, 16}
            _MONEY_COLS = {11, 12}
            _CTR_COLS   = {1, 2, 8, 9, 10, 11, 12, 13, 15, 16}
            for ri in range(5):
                _r = 10 + ri
                ws.row_dimensions[_r].height = 20
                _row_fill = _WHITE if ri % 2 == 0 else _LIGHT
                for ci in range(1, _NC + 1):
                    cell = ws.cell(_r, ci)
                    cell.fill   = _AUTO_FILL if ci in _AUTO_COLS else _row_fill
                    cell.border = _BDR
                    if ci in _MONEY_COLS:  # K=11, L=12: direita, sem itálico, Calibri 10
                        cell.font          = _MONEY_FONT
                        cell.alignment     = _RGT
                        cell.number_format = '"$"#,##0.00'
                    elif ci in _AUTO_COLS:
                        cell.font      = _AUTO_FONT
                        cell.alignment = _CTR
                    else:
                        cell.font      = _USR_FONT
                        cell.alignment = _CTR if ci in _CTR_COLS else _LFT

            ws.freeze_panes = "A10"
            ws.sheet_properties.tabColor = "27AE60"
            wb.save(path)
            self.log(f"INVENTORY.xlsx criado (v3 i18n): {path}")
        except Exception as _exc:
            from PySide6.QtWidgets import QMessageBox
            QMessageBox.warning(self, "INVENTORY", f"Erro ao criar INVENTORY.xlsx:\n{_exc}")

    def _refresh_inventory_status(self, inv_file=None):
        """Espelha STATUS (col B) e PROJETO (col O) do INVENTORY.xlsx com SQLite.
        Recalcula bloco resumo por categoria (rows 4-5 por cat, row 7 saldo global).
        Falha silenciosa — nunca bloqueia o fluxo."""
        import os as _os
        if inv_file is None:
            _engine_dir = _os.path.abspath(
                _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..")
            )
            inv_file = _os.path.join(_engine_dir, "inputs", "INVENTORY.xlsx")
        if not _os.path.exists(str(inv_file)):
            return
        try:
            from openpyxl import load_workbook as _lwb
            import stock_backend_router as _sbr

            # Mapa SQLite: stock_id → {status, dest_project}
            _sm = {}
            for _it in _sbr.load_stock():
                _sid = str(_it.get("stock_id", "")).strip()
                if _sid:
                    _sm[_sid] = {
                        "status":  str(_it.get("status", "")).upper(),
                        "project": _it.get("dest_project") or _it.get("destination_project") or "",
                    }

            # Mapa reverso: inv_id → [ids de retalhos gerados] (STOCK + SCRAP)
            # bar_origin formato: "{cut_group}_{inv_id}" → ex: "Wood 30_INV-WOOD30-001"
            _remnant_map = {}
            for _it in _sbr.load_stock():
                _bar = str(_it.get("source_bar_id", "") or "")
                if "_INV-" in _bar:
                    _inv_ref = "INV-" + _bar.split("_INV-", 1)[1]
                    _rid = str(_it.get("stock_id", "")).strip()
                    if _rid:
                        _remnant_map.setdefault(_inv_ref, []).append(_rid)
            for _it in _sbr.load_scrap():
                _bar = str(_it.get("source_bar_id", "") or "")
                if "_INV-" in _bar:
                    _inv_ref = "INV-" + _bar.split("_INV-", 1)[1]
                    _rid = str(_it.get("scrap_id", "")).strip()
                    if _rid:
                        _remnant_map.setdefault(_inv_ref, []).append(_rid)

            def _parse_ids(val):
                if not val:
                    return []
                s = str(val).strip()
                if " a " not in s:
                    return [s]
                parts = s.split(" a ")
                if len(parts) != 2:
                    return [s]
                s0, s1 = parts[0].strip(), parts[1].strip()
                p0 = s0.rfind("-"); p1 = s1.rfind("-")
                if p0 < 0 or p1 < 0:
                    return [s0, s1]
                try:
                    prefix = s0[:p0 + 1]
                    n1, n2 = int(s0[p0 + 1:]), int(s1[p1 + 1:])
                    return [f"{prefix}{n:03d}" for n in range(n1, n2 + 1)]
                except ValueError:
                    return [s0, s1]

            wb = _lwb(inv_file)
            ws = wb["INVENTORY"]

            # Detectar layout — skip se v1 (header row 3) ou v2-old (header row 7)
            _c3  = str(ws.cell(3, 1).value or "").strip().upper()
            _c7  = str(ws.cell(7, 1).value or "").strip().upper()
            _c9  = str(ws.cell(9, 1).value or "").strip().upper()
            if _c3.startswith("ID"):
                wb.close(); return  # v1
            if "ID" in _c7 and "ID" not in _c9:
                wb.close(); return  # v2-old (header row 7) — não suportado; pedir migração
            if "ID" not in _c9:
                wb.close(); return  # layout desconhecido

            # v3: atualiza STATUS (col B), PROJETO (col O) e RETALHO GERADO (col P).
            # Resumo financeiro (rows 4-7) é gerenciado por fórmulas SUMIFS no Excel.
            # Col P existe apenas em INVENTORY.xlsx v3.1+ (detecta pelo header).
            _has_remn_col = "RETALHO" in str(
                ws.cell(self._INV_HDR_ROW, self._INV_COL_REMN).value or ""
            ).upper()

            for _row in ws.iter_rows(min_row=self._INV_DAT_ROW):
                _id_val = _row[self._INV_COL_ID - 1].value  # 0-based
                if not _id_val:
                    continue
                _ids = _parse_ids(_id_val)
                if not _ids:
                    continue

                _any_avail = False
                _proj = ""
                _remn_ids = []
                for _sid in _ids:
                    _info = _sm.get(_sid, {})
                    if _info.get("status") == "AVAILABLE":
                        _any_avail = True
                    elif not _proj:
                        _proj = _info.get("project", "")
                    _remn_ids.extend(_remnant_map.get(_sid, []))

                _new_st = "DISPONÍVEL" if _any_avail else "UTILIZADO"
                _rn = _row[0].row
                ws.cell(_rn, self._INV_COL_ST).value   = _new_st
                ws.cell(_rn, self._INV_COL_PROJ).value = _proj if not _any_avail else ""
                if _has_remn_col:
                    ws.cell(_rn, self._INV_COL_REMN).value = (
                        " | ".join(_remn_ids) if _remn_ids else ""
                    )

            # Resumo por categoria: gerenciado por fórmulas SUMIFS no Excel —
            # _refresh_inventory_status() atualiza apenas STATUS e PROJETO.
            wb.save(inv_file)
            wb.close()
        except Exception:
            pass

    def _validate_criterios_for_run(self):
        """Verifica que toda descrição do INPUT existe no CRITERIOS.
        Se encontrar materiais sem especificações, exibe popup e retorna False."""
        # Recarregar do disco para capturar edições feitas no Excel após o último load
        self._load_criterios_delegate_data()
        if not hasattr(self, "manual_table") or not self._criterios_desc_set:
            return True  # sem dados suficientes para validar
        from PySide6.QtWidgets import QMessageBox
        missing = []
        seen    = set()
        for row in range(self.manual_table.rowCount()):
            item = self.manual_table.item(row, 0)
            if not item:
                continue
            desc = item.text().strip()
            if not desc:
                continue
            key = desc.upper()
            if key in seen:
                continue
            seen.add(key)
            if key not in self._criterios_desc_set:
                missing.append(desc)
        if missing:
            _list = "\n".join(f"  •  {d}" for d in missing)
            QMessageBox.warning(
                self, "CRITERIOS INCOMPLETO",
                "Os seguintes materiais do INPUT não possuem\n"
                "especificações na aba CRITERIOS:\n\n"
                f"{_list}\n\n"
                "Preencha ao menos um tamanho de barra para cada\n"
                "material no CRITERIOS antes de executar o projeto.\n\n"
                "Dica: se o material veio do INVENTORY, adicione-o\n"
                "à aba CRITERIOS do template ativo com comprimento\n"
                "de barra e preço."
            )
            return False
        return True

    # ── Support Center ────────────────────────────────────────────────────────

    def _open_support_center(self):
        dlg = SupportCenterDialog(parent=self)
        dlg.exec()

    # ── Language Support ──────────────────────────────────────────────────────

    def _load_language(self):
        """Load saved language preference and apply to UI."""
        try:
            _lm = _load_engine_module("language_manager")
            _lm.init()
            self._current_lang_code = getattr(_lm, "_lang", "en")
            self._apply_language_ui(_lm)
        except Exception:
            pass

    def _switch_language(self, lang_code):
        """Switch to a new language, save preference, and refresh UI."""
        try:
            _lm = _load_engine_module("language_manager")
            _lm.load(lang_code)
            _lm.save_preference()
            self._current_lang_code = lang_code
            self._apply_language_ui(_lm)
        except Exception as exc:
            QMessageBox.warning(self, "Language", f"Could not switch language: {exc}")

    def _apply_language_ui(self, lm):
        """Update all translatable UI elements with the current language."""
        def t(k): return lm.t(k)

        # Main operation buttons
        self.run_button.setText(t("btn_run"))
        self.select_input_button.setText(t("btn_select_input"))
        self.result_button.setText(t("btn_open_results"))
        self.cost_result_button.setText(t("btn_open_cost"))
        self.audit_result_button.setText(t("btn_open_audit"))
        self.stock_button.setText(t("btn_open_stock"))
        self.save_project_button.setText(t("btn_save_project"))
        self.reports_button.setText(t("btn_reports"))
        self.qr_labels_button.setText(t("btn_qr_labels"))
        _lang_names = {"en": "English", "pt": "Português", "es": "Español"}
        _lc = getattr(self, "_current_lang_code", "en")
        self.language_button.setText(f"🌐  {_lang_names.get(_lc, 'LANGUAGE')} ▼")
        if hasattr(self, "support_button"):
            self.support_button.setText(t("btn_support"))
        if hasattr(self, "tools_button"):
            self.tools_button.setText("🔧  " + t("btn_tools") + " ▼")
        if hasattr(self, "_act_safe"):
            self._act_safe.setText("🧹  " + t("tools_safe_clean"))
        if hasattr(self, "_act_factory"):
            self._act_factory.setText("⚠️   " + t("tools_factory_reset"))
        if hasattr(self, "_act_cache"):
            self._act_cache.setText("🗑  " + t("tools_cache_clean"))

        # INVENTORY action labels
        if hasattr(self, "_act_inv_open"):
            self._act_inv_open.setText(t("inv_open_btn"))
        if hasattr(self, "_act_inv_import"):
            self._act_inv_import.setText(t("inv_import_btn"))

        # Template selector — rebuild menu with translated labels
        if hasattr(self, "template_button"):
            self._rebuild_template_menu(lm)

        # Report menu action labels
        if hasattr(self, "_report_actions"):
            labels = {
                "project_summary":  "―  " + t("menu_project_summary"),
                "project_history":  "―  " + t("menu_project_history"),
                "purchase_order":   "―  " + t("menu_purchase_order"),
                "cut_summary":      "  " + t("menu_cut_summary"),
                "cut_list":         "  " + t("menu_cut_list"),
                "cut_plan":         "  " + t("menu_cut_plan"),
                "stock":            "  " + t("menu_stock"),
                "scrap":            "  " + t("menu_scrap"),
                "history":          "  " + t("menu_history"),
                "cost_summary":     "  " + t("menu_cost_summary"),
                "cost_by_bar":      "  " + t("menu_cost_by_bar"),
                "cost_by_piece":    "  " + t("menu_cost_by_piece"),
            }
            for key, action in self._report_actions.items():
                if key in labels:
                    action.setText(labels[key])
        # Report submenu titles
        if hasattr(self, "_report_submenus"):
            sub_titles = {
                "cutting_report": "▸  " + t("menu_cutting_report"),
                "stock_report":   "▸  " + t("menu_stock_report"),
                "cost_report":    "▸  " + t("menu_cost_report"),
            }
            for key, sub in self._report_submenus.items():
                if key in sub_titles:
                    sub.setTitle(sub_titles[key])

        # Dashboard card titles
        _card_title_map = {
            "_lbl_total_bars":          "total_bars",
            "_lbl_total_pieces":        "total_pieces",
            "_lbl_total_waste":         "total_waste_card",
            "_lbl_total_cost_card":     "total_cost_card",
            "_lbl_yield_card":          "yield_card",
            "_lbl_control_status":      "control_status_card",
            "_lbl_cost_breakdown_card": "cost_breakdown",
            "_lbl_total_bars_section":  "total_bars",
        }
        for attr, key in _card_title_map.items():
            if hasattr(self, attr):
                lbl = getattr(self, attr)
                lbl.setText(t(key))
                # re-apply adaptive font size after language change
                _txt = lbl.text()
                _fs = 15 if len(_txt) <= 12 else (13 if len(_txt) <= 16 else 11)
                _cur_ss = lbl.styleSheet()
                import re as _re
                lbl.setStyleSheet(_re.sub(r'font-size:\s*\d+px', f'font-size:{_fs}px', _cur_ss))

        # Dashboard sub-labels
        _sub_label_map = {
            "_lbl_pieces_sub":          "pieces_sub",
            "_lbl_piece_value_sub":     "piece_value_sub",
            "_lbl_stock_reusable_sub":  "stock_reusable_sub",
            "_lbl_scrap_junkyard_sub":  "scrap_junkyard_sub",
            "_lbl_efficiency_sub":      "efficiency_sub",
            "_lbl_yield_cut_sub":       "yield_cut_sub",
            "_lbl_project_cost_sub":    "project_cost_sub",
            "_lbl_audit_status_sub":    "audit_status_sub",
            "_lbl_uncalculated_sub":    "uncalculated_sub",
            "_lbl_run_time_sub":        "run_time_sub",
            "_lbl_diagnostic_sub":      "diagnostic_indicators_sub",
        }
        for attr, key in _sub_label_map.items():
            if hasattr(self, attr):
                getattr(self, attr).setText(t(key))

        # Section labels
        _section_map = {
            "_lbl_operations_section":    "operations_title",
            "_lbl_settings_section":      "ui_lbl_settings",
            "_lbl_overview_section":      "ui_lbl_operational_overview",
            "_lbl_manual_input_section":  "ui_lbl_manual_input",
            "_lbl_process_log_section":   "process_log",
            "_lbl_bar_detail_title":      "bar_detail",
            "_lbl_bars_in_project":       "bars_in_project_sub",
        }
        for attr, key in _section_map.items():
            if hasattr(self, attr):
                lbl = getattr(self, attr)
                lbl.setText(t(key))
                # re-apply adaptive font for styled card titles
                if attr == "_lbl_bar_detail_title":
                    _txt = lbl.text()
                    # 15px for short (EN), 13px for anything longer — matches TOTAL DE BARRAS
                    _fs = 15 if len(_txt) <= 12 else 13
                    import re as _re
                    lbl.setStyleSheet(_re.sub(r'font-size:\s*\d+px', f'font-size:{_fs}px', lbl.styleSheet()))
                    if hasattr(self, "_icon_bar_detail"):
                        _ic = self._icon_bar_detail
                        _ic.setStyleSheet(_re.sub(r'font-size:\s*\d+px', f'font-size:{_fs}px', _ic.styleSheet()))

        # Toolbar buttons with translated labels
        _btn_map = {
            "add_row_button":        "btn_add_row",
            "remove_row_button":     "btn_remove_row",
            "clear_input_button":    "btn_clear_input",
            "load_from_excel_button": "btn_load_excel",
            "sync_to_excel_button":  "btn_sync_excel",
            "settings_button":       "btn_settings",
        }
        for attr, key in _btn_map.items():
            if hasattr(self, attr):
                getattr(self, attr).setText(t(key))

        # Direct input / sync status labels
        if hasattr(self, "manual_mode_label"):
            self.manual_mode_label.setText(t("direct_input_mode_enabled"))
        if hasattr(self, "mirror_status_label"):
            _cur = self.mirror_status_label.text()
            if "✓" in _cur or "SYNC" in _cur.upper() or "SINC" in _cur.upper():
                self.mirror_status_label.setText(t("lbl_synced"))
            elif "⚠" in _cur or "UNSAV" in _cur.upper() or "ALTER" in _cur.upper() or "CAMB" in _cur.upper():
                self.mirror_status_label.setText(t("lbl_unsaved"))

        # Project strip title labels
        _strip_title_map = {
            "project_file_label":        "strip_project",
            "project_number_label":      "strip_project_no",
            "project_description_label": "strip_description",
            "project_system_label":      "strip_system",
            "project_units_label":       "strip_input_units",
            "project_output_label":      "strip_output_display",
        }
        for _attr, _key in _strip_title_map.items():
            _w = getattr(self, _attr, None)
            if _w and hasattr(_w, "title_label"):
                _w.title_label.setText(t(_key))

        # Input table column headers (language + unit-aware)
        self.update_manual_table_headers()

        # BAR DETAIL table headers
        if hasattr(self, "_bar_table"):
            self._bar_table.setHorizontalHeaderLabels([
                t("ui_col_cut_group"),
                t("ui_col_qty"),
                t("bar_detail_col_solution"),
                t("bar_detail_col_yield"),
            ])

        # COST BREAKDOWN barras — re-render com idioma atual preservando valores
        if hasattr(self, "_cb_raw_values") and hasattr(self, "_cb_bar_widgets"):
            _nv, _pv, _sv, _wv = self._cb_raw_values
            _mv = getattr(self, "_cb_material_value", 0.0)
            self._update_cost_breakdown_bars(
                net_val=_nv, pieces_val=_pv, stock_val=_sv, scrap_val=_wv,
                material_val=_mv,
                net_lbl=t("cost_legend_net"),
                pieces_lbl=t("cost_legend_pieces"),
                stock_lbl=t("cost_legend_reusable"),
                scrap_lbl=t("cost_legend_scrap"),
                material_lbl=t("cost_legend_material"),
            )

    # ── Report Generation ─────────────────────────────────────────────────────

    @staticmethod
    def _report_popup(parent, icon, title: str, message: str) -> None:
        """Show a QMessageBox styled for report notifications."""
        # Colapsa linhas em branco duplas numa única quebra
        message = message.replace("\n\n", "\n")
        box = QMessageBox(icon, title, message, QMessageBox.Ok, parent)
        box.setStyleSheet(
            "QMessageBox {"
            "  background-color: #FDE9D9;"
            "}"
            "QLabel {"
            "  color: #FFFFFF;"
            "  font-weight: normal;"
            "  qproperty-alignment: AlignCenter;"
            "}"
            "QPushButton {"
            "  color: #333333;"
            "  background-color: #F0C0A0;"
            "  border: 1px solid #D49070;"
            "  border-radius: 4px;"
            "  padding: 4px 16px;"
            "}"
            "QPushButton:hover {"
            "  background-color: #F8D0B8;"
            "}"
        )
        # Centraliza o QLabel da mensagem via API Qt
        _lbl = box.findChild(QLabel, "qt_msgbox_label")
        if _lbl:
            _lbl.setAlignment(Qt.AlignCenter)
        # Largura 20% maior que o tamanho natural do conteúdo
        grid = box.layout()
        grid.addItem(
            QSpacerItem(int(box.sizeHint().width() * 0.2), 0,
                        QSizePolicy.Minimum, QSizePolicy.Expanding),
            grid.rowCount(), 0, 1, grid.columnCount(),
        )
        box.exec()

    def _generate_report(self, report_type, tab_name=None):
        """Generate an official report, open it at the correct tab, and show result."""
        if not _license_gate(self):
            return
        try:
            _rg = _load_engine_module("report_generator")
            _lm = _load_engine_module("language_manager")
            output_path = _rg.generate_report(report_type, tab_name=tab_name)
            os.startfile(output_path)
            self._report_popup(
                self,
                QMessageBox.Information,
                "ENGCUT REPORTS",
                _lm.t("msg_report_success") + f"\n\n{os.path.basename(output_path)}",
            )
        except FileNotFoundError as exc:
            try:
                _lm = _load_engine_module("language_manager")
                msg = _lm.t("msg_no_results")
            except Exception:
                msg = "No results found. Run the project first."
            self._report_popup(self, QMessageBox.Warning, "ENGCUT REPORTS", msg + f"\n\n{exc}")
        except Exception as exc:
            try:
                _lm = _load_engine_module("language_manager")
                msg = _lm.t("msg_report_error", err=str(exc))
            except Exception:
                msg = f"Error generating report:\n{exc}"
            self._report_popup(self, QMessageBox.Critical, "ENGCUT REPORTS", msg)

    # ── Tools — environment clean / reset ────────────────────────────────────

    def _do_clean_safe(self):
        """Limpar Outputs: apaga outputs/ e temp/, preserva banco de estoque."""
        try:
            _lm = _load_engine_module("language_manager")
            _t = _lm.t
        except Exception:
            _t = lambda k: k
        _reply = QMessageBox.question(
            self, _t("tools_dlg_safe_title"),
            _t("tools_confirm_safe_body"),
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )
        if _reply != QMessageBox.Yes:
            return
        try:
            import shutil as _sh
            from pathlib import Path as _P
            _root = _P(_ui_base_dir())
            for _d in [_root / "outputs", _root / "temp"]:
                if _d.exists():
                    _sh.rmtree(_d)
                _d.mkdir(parents=True, exist_ok=True)
            for _pat in ("~$*.xlsx", "~$*.xlsm"):
                for _p in _root.rglob(_pat):
                    try:
                        _p.unlink()
                    except Exception:
                        pass
            self.log(_t("tools_log_safe_done"))
            QMessageBox.information(self, _t("tools_dlg_safe_title"), _t("tools_dlg_safe_done"))
        except Exception as _e:
            self.log(f"[ERRO] Safe Clean: {_e}")
            QMessageBox.critical(self, _t("tools_dlg_safe_title"), _t("tools_error_clean_body").format(error=_e))

    def _do_clean_factory(self):
        """Factory Reset: limpa tudo, inclusive banco de estoque."""
        try:
            _lm = _load_engine_module("language_manager")
            _t = _lm.t
        except Exception:
            _t = lambda k: k
        _reply = QMessageBox.question(
            self, "⚠ " + _t("tools_dlg_factory_title"),
            _t("tools_confirm_factory_body"),
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )
        if _reply != QMessageBox.Yes:
            return

        import shutil as _sh
        from pathlib import Path as _P
        _root = _P(_ui_base_dir())
        _skipped = []

        # 1. Limpar pastas de saída — arquivos em uso (WinError 32) são ignorados
        def _on_rmtree_error(func, path, exc_info):
            _skipped.append(os.path.basename(path))

        for _d in [_root / "outputs", _root / "temp"]:
            if _d.exists():
                _sh.rmtree(str(_d), onerror=_on_rmtree_error)
            try:
                _d.mkdir(parents=True, exist_ok=True)
            except Exception:
                pass

        for _pat in ("~$*.xlsx", "~$*.xlsm"):
            for _p in _root.rglob(_pat):
                try:
                    _p.unlink()
                except Exception:
                    pass

        # 2. Apagar cache de entrada gerado — input_001.json é derivado do INPUT xlsx,
        #    não é arquivo do usuário; deve ser removido para evitar que um RUN pós-reset
        #    sem novo projeto reuse dados do projeto anterior.
        _inp_cache = _root / "inputs" / "input_001.json"
        try:
            if _inp_cache.exists():
                _inp_cache.unlink()
        except Exception as _e2:
            _skipped.append(f"input_001.json ({_e2})")

        # 3. Reset banco de estoque — executa SEMPRE, independente de erros anteriores
        _db_reset_ok = False
        _db_err = ""
        try:
            _sbr = _load_engine_module("stock_backend_router")
            _sbr.reset_stock_database()
            _db_reset_ok = True
        except Exception as _e3:
            _db_err = str(_e3)

        if not _db_reset_ok:
            self.log(f"[ERRO] Factory Reset — banco não resetado: {_db_err}")
            QMessageBox.critical(self, _t("tools_dlg_factory_title"),
                                 f"Erro ao resetar banco de estoque:\n{_db_err}")
            return

        if _skipped:
            self.log(f"[AVISO] Factory Reset concluído (arquivos em uso ignorados: {len(_skipped)})")
            QMessageBox.warning(self, _t("tools_dlg_factory_title"),
                                "Reset concluído.\n\nAlguns arquivos em uso não foram removidos "
                                "(feche o Excel/relatório e repita se necessário):\n• " +
                                "\n• ".join(_skipped[:5]))
        else:
            self.log(_t("tools_log_factory_done"))
            QMessageBox.information(self, _t("tools_dlg_factory_title"), _t("tools_dlg_factory_done"))

    def _do_clean_cache(self):
        """Limpar Cache Python: remove __pycache__ e arquivos .pyc."""
        try:
            _lm = _load_engine_module("language_manager")
            _t = _lm.t
        except Exception:
            _t = lambda k: k
        _reply = QMessageBox.question(
            self, _t("tools_dlg_cache_title"),
            "Remover arquivos de cache Python (__pycache__, *.pyc)?\n\n"
            "O cache será recriado automaticamente na próxima execução.",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )
        if _reply != QMessageBox.Yes:
            return
        try:
            import shutil as _sh
            from pathlib import Path as _P
            _root = _P(_ui_base_dir())
            for _cache in _root.rglob("__pycache__"):
                if _cache.is_dir():
                    _sh.rmtree(_cache, ignore_errors=True)
            for _pyc in _root.rglob("*.pyc"):
                try:
                    _pyc.unlink()
                except Exception:
                    pass
            self.log(_t("tools_log_cache_done"))
            QMessageBox.information(self, _t("tools_dlg_cache_title"), _t("tools_dlg_cache_done"))
        except Exception as _e:
            self.log(f"[ERRO] Cache Clean: {_e}")
            QMessageBox.critical(self, _t("tools_dlg_cache_title"), f"Erro ao limpar cache:\n{_e}")

    # ── QR Labels ─────────────────────────────────────────────────────────────

    def _print_qr_labels(self):
        """Generate 06_QR_Labels.xlsx filled with real QR data and open it."""
        if not _license_gate(self):
            return
        try:
            _rg = _load_engine_module("report_generator")
            _lm = _load_engine_module("language_manager")
            output_path = _rg.generate_qr_labels()
            os.startfile(output_path)
            QMessageBox.information(
                self,
                "ENGCUT QR LABELS",
                _lm.t("msg_qr_success") + f"\n\n{os.path.basename(output_path)}",
            )
        except FileNotFoundError as exc:
            try:
                _lm = _load_engine_module("language_manager")
                msg = _lm.t("msg_no_results")
            except Exception:
                msg = "No results found. Run the project first."
            QMessageBox.warning(self, "ENGCUT QR LABELS", msg + f"\n\n{exc}")
        except Exception as exc:
            try:
                _lm = _load_engine_module("language_manager")
                msg = _lm.t("msg_qr_error", err=str(exc))
            except Exception:
                msg = f"Error generating QR Labels:\n{exc}"
            QMessageBox.critical(self, "ENGCUT QR LABELS", msg)

    # ─────────────────────────────────────────────────────────────────────────

    def open_file(self, relative_path):

        base_dir = os.path.join(_ui_base_dir(), "front_end")

        file_path = os.path.abspath(
            os.path.join(base_dir, relative_path)
        )

        if os.path.exists(file_path):

            os.startfile(file_path)

        else:

            print(f"FILE NOT FOUND: {file_path}")


# =============================================================================
# SUPPORT CENTER DIALOG
# =============================================================================

class SupportCenterDialog(QDialog):
    """Professional Support Center — information, diagnostics and technical support."""

    _SUPPORT_EMAIL = "co.engcut@outlook.com"
    _VERSION       = "1.0.0"
    _BUILD         = "RC_COMERCIAL"
    _BUILD_DATE    = "27/06/2026"

    # ── Palette ───────────────────────────────────────────────────────────────
    _C_BG     = "#0d1117"
    _C_CARD   = "#0d1f2d"
    _C_BORDER = "#1e3a5f"
    _C_ACCENT = "#1e90ff"
    _C_GREEN  = "#2ecc71"
    _C_YELLOW = "#f39c12"
    _C_RED    = "#e74c3c"
    _C_TEXT   = "#c9d1d9"
    _C_DIM    = "#6e7681"
    _C_WHITE  = "#ffffff"
    _C_SEP    = "#132030"

    def __init__(self, parent=None):
        super().__init__(parent)
        self._last_diag_path   = None
        self._inst_vals: dict  = {}
        self._lic_vals:  dict  = {}
        self._diag_vals: dict  = {}
        self._btn_folder       = None
        self._btn_copy         = None
        self._btn_copy_lic     = None
        self._btn_copy_mid     = None
        self._lic_key_value    = ""
        self._machine_id_value = ""
        self._setup_ui()
        self._load_data()

    # ── UI ────────────────────────────────────────────────────────────────────

    def _setup_ui(self):
        self.setWindowTitle("SUPPORT CENTER — EngCut")
        self.setFixedSize(960, 880)
        self.setModal(True)
        self.setStyleSheet(f"""
            QDialog {{
                background-color: {self._C_BG};
                font-family: Calibri, Arial, sans-serif;
            }}
            QLabel {{
                color: {self._C_TEXT};
                background: transparent;
                border: none;
            }}
            QPushButton {{
                background-color: #0d1f2d;
                color: {self._C_TEXT};
                border: 1px solid {self._C_BORDER};
                border-radius: 4px;
                font-size: 15px;
                font-weight: bold;
                text-align: left;
                padding: 0 10px;
            }}
            QPushButton:hover {{
                background-color: #1e3a5f;
                border-color: {self._C_ACCENT};
                color: {self._C_WHITE};
            }}
            QPushButton:disabled {{
                color: #2a3a4a;
                border-color: #0f1e2d;
                background-color: #080f18;
            }}
        """)

        root = QVBoxLayout(self)
        root.setContentsMargins(18, 16, 18, 14)
        root.setSpacing(10)

        # ── Title ──────────────────────────────────────────────────────────
        hdr = QLabel("  SUPPORT CENTER")
        hdr.setAlignment(Qt.AlignCenter)
        hdr.setStyleSheet(f"""
            color: {self._C_WHITE};
            font-size: 15px;
            font-weight: bold;
            letter-spacing: 3px;
            padding: 2px 0 4px 0;
            background: transparent;
        """)
        root.addWidget(hdr)

        # ── 2 × 2 card grid ────────────────────────────────────────────────
        grid = QGridLayout()
        grid.setSpacing(10)
        grid.setColumnStretch(0, 1)
        grid.setColumnStretch(1, 1)
        grid.setRowStretch(0, 1)
        grid.setRowStretch(1, 1)

        grid.addWidget(self._make_installation_card(), 0, 0)
        grid.addWidget(self._make_license_card(),      0, 1)
        grid.addWidget(self._make_diagnostics_card(),  1, 0)
        grid.addWidget(self._make_support_card(),      1, 1)
        root.addLayout(grid, stretch=1)

        # ── Footer ─────────────────────────────────────────────────────────
        foot_sep = QFrame()
        foot_sep.setFrameShape(QFrame.HLine)
        foot_sep.setFixedHeight(1)
        foot_sep.setStyleSheet(f"background: {self._C_SEP}; border: none;")
        root.addWidget(foot_sep)

        foot_row = QHBoxLayout()
        foot_row.setContentsMargins(0, 2, 0, 0)
        foot_lbl = QLabel(
            f"EngCut  v{self._VERSION}    ·    Build {self._BUILD}    ·    Commercial License"
        )
        foot_lbl.setStyleSheet(f"color: {self._C_DIM}; font-size: 12px; background: transparent;")
        foot_row.addWidget(foot_lbl)
        foot_row.addStretch()

        close_btn = QPushButton("  Close  ")
        close_btn.setFixedHeight(36)
        close_btn.setStyleSheet(f"""
            QPushButton {{
                background-color: #0d1f2d;
                color: {self._C_WHITE};
                border: 1px solid {self._C_ACCENT};
                border-radius: 4px;
                font-size: 15px;
                font-weight: bold;
                padding: 0 18px;
                text-align: center;
            }}
            QPushButton:hover {{ background-color: {self._C_ACCENT}; }}
        """)
        close_btn.clicked.connect(self.close)
        foot_row.addWidget(close_btn)
        root.addLayout(foot_row)

    # ── Card builder ──────────────────────────────────────────────────────────

    def _make_card(self, icon: str, title: str) -> tuple:
        """Returns (QFrame card, QVBoxLayout content area)."""
        frame = QFrame()
        frame.setObjectName("sc_card")
        frame.setStyleSheet(f"""
            QFrame#sc_card {{
                background-color: {self._C_CARD};
                border: 1px solid {self._C_BORDER};
                border-radius: 6px;
            }}
        """)
        lay = QVBoxLayout(frame)
        lay.setContentsMargins(14, 12, 14, 12)
        lay.setSpacing(6)

        title_lbl = QLabel(f"{icon}   {title}")
        title_lbl.setStyleSheet(f"""
            color: {self._C_ACCENT};
            font-size: 15px;
            font-weight: bold;
            letter-spacing: 1px;
            background: transparent;
            border: none;
        """)
        lay.addWidget(title_lbl)

        sep = QFrame()
        sep.setFixedHeight(1)
        sep.setStyleSheet(f"background: {self._C_SEP}; border: none;")
        lay.addWidget(sep)

        content = QVBoxLayout()
        content.setSpacing(0)
        content.setContentsMargins(0, 4, 0, 0)
        lay.addLayout(content)
        return frame, content

    def _info_grid(self, fields: list) -> tuple:
        """QGridLayout with label → QLabel value mapping. All values start at same column."""
        g = QGridLayout()
        g.setContentsMargins(0, 0, 0, 0)
        g.setHorizontalSpacing(14)
        g.setVerticalSpacing(6)
        g.setColumnMinimumWidth(0, 148)
        g.setColumnStretch(1, 1)
        refs = {}
        for i, (key, label) in enumerate(fields):
            l = QLabel(label)
            l.setStyleSheet(f"color: {self._C_DIM}; font-size: 15px; background: transparent; border: none;")
            v = QLabel("—")
            v.setStyleSheet(f"color: {self._C_TEXT}; font-size: 15px; font-weight: bold; background: transparent; border: none;")
            g.addWidget(l, i, 0, Qt.AlignLeft | Qt.AlignVCenter)
            g.addWidget(v, i, 1, Qt.AlignLeft | Qt.AlignVCenter)
            refs[key] = v
        return g, refs

    def _action_btn(self, text: str, primary: bool = False) -> QPushButton:
        b = QPushButton(text)
        b.setFixedHeight(36)
        if primary:
            b.setStyleSheet(f"""
                QPushButton {{
                    background-color: #0d2a4a;
                    color: {self._C_WHITE};
                    border: 1px solid {self._C_ACCENT};
                    border-radius: 4px;
                    font-size: 15px;
                    font-weight: bold;
                    text-align: left;
                    padding: 0 10px;
                }}
                QPushButton:hover {{ background-color: {self._C_ACCENT}; }}
            """)
        return b

    def _card_sep(self) -> QFrame:
        f = QFrame()
        f.setFixedHeight(1)
        f.setStyleSheet(f"background: {self._C_SEP}; border: none;")
        return f

    # ── Four cards ────────────────────────────────────────────────────────────

    def _make_installation_card(self) -> QFrame:
        frame, content = self._make_card("🖥", "INSTALLATION INFORMATION")
        g, self._inst_vals = self._info_grid([
            ("software",   "Software"),
            ("version",    "Version"),
            ("build",      "Build"),
            ("build_date", "Build Date"),
            ("os",         "Operating System"),
            ("arch",       "Architecture"),
            ("language",   "Language"),
            ("database",   "Database"),
            ("run_mode",   "Run Mode"),
        ])
        content.addLayout(g)
        content.addStretch()
        return frame

    def _make_license_card(self) -> QFrame:
        frame, content = self._make_card("🔑", "LICENSE INFORMATION")
        g, self._lic_vals = self._info_grid([
            ("status",     "Status"),
            ("plan",       "Plan"),
            ("license_no", "License Key"),
            ("activated",  "Issued"),
            ("expires",    "Expires"),
            ("days_left",  "Days Remaining"),
            ("customer",   "Customer"),
            ("company",    "Company"),
        ])
        content.addLayout(g)
        content.addWidget(self._card_sep())

        self._btn_copy_mid = self._action_btn("⎘   Copy Machine ID", primary=True)
        self._btn_copy_lic = self._action_btn("⎘   Copy License Key")
        self._btn_copy_mid.clicked.connect(self._copy_machine_id)
        self._btn_copy_lic.clicked.connect(self._copy_license_key)
        content.addWidget(self._btn_copy_mid)
        content.addWidget(self._btn_copy_lic)
        content.addStretch()
        return frame

    def _make_diagnostics_card(self) -> QFrame:
        frame, content = self._make_card("📦", "SYSTEM DIAGNOSTICS")

        g, self._diag_vals = self._info_grid([
            ("date", "Last Generated"),
            ("file", "File Name"),
            ("loc",  "Location"),
            ("size", "File Size"),
        ])
        content.addLayout(g)
        content.addWidget(self._card_sep())
        content.setSpacing(4)

        btn_gen         = self._action_btn("⬇   Generate Diagnostic", primary=True)
        self._btn_folder = self._action_btn("📁   Open Folder")
        self._btn_copy   = self._action_btn("⎘   Copy File Path")
        btn_log          = self._action_btn("📋   Open Log Folder")

        self._btn_folder.setEnabled(False)
        self._btn_copy.setEnabled(False)

        btn_gen.clicked.connect(self._generate_diagnostic)
        self._btn_folder.clicked.connect(self._open_diag_folder)
        self._btn_copy.clicked.connect(self._copy_diag_path)
        btn_log.clicked.connect(self._open_log_folder)

        for b in (btn_gen, self._btn_folder, self._btn_copy, btn_log):
            content.addWidget(b)
        content.addStretch()
        return frame

    def _make_support_card(self) -> QFrame:
        frame, content = self._make_card("🛠", "TECHNICAL SUPPORT")
        content.setSpacing(5)

        support_lbl = QLabel("Support")
        support_lbl.setStyleSheet(f"color: {self._C_DIM}; font-size: 15px; background: transparent; border: none;")
        content.addWidget(support_lbl)

        email_lbl = QLabel(
            f'<a href="mailto:{self._SUPPORT_EMAIL}" '
            f'style="color:{self._C_ACCENT}; text-decoration:none; font-weight:bold; font-size:15px;">'
            f'{self._SUPPORT_EMAIL}</a>'
        )
        email_lbl.setTextFormat(Qt.RichText)
        email_lbl.setOpenExternalLinks(True)
        email_lbl.setStyleSheet("font-size: 15px; background: transparent; border: none;")
        content.addWidget(email_lbl)

        content.addWidget(self._card_sep())

        btn_email  = self._action_btn("✉   Create Support Email")
        btn_docs   = self._action_btn("📖   Open Documentation")
        btn_update = self._action_btn("🔄   Check for Updates")

        btn_email.clicked.connect(self._create_support_email)
        btn_docs.clicked.connect(self._open_documentation)
        btn_update.clicked.connect(self._check_for_updates)

        for b in (btn_email, btn_docs, btn_update):
            content.addWidget(b)
        content.addStretch()
        return frame

    # ── Data loading ──────────────────────────────────────────────────────────

    def _load_data(self):
        self._populate_installation()
        self._populate_license()

    def _set_i(self, key, text, color=""):
        self._set_val(self._inst_vals, key, text, color)

    def _set_l(self, key, text, color=""):
        self._set_val(self._lic_vals, key, text, color)

    def _set_d(self, key, text, color=""):
        self._set_val(self._diag_vals, key, text, color)

    def _set_val(self, refs, key, text, color=""):
        lbl = refs.get(key)
        if lbl is None:
            return
        lbl.setText(text)
        style = f"font-size: 15px; font-weight: bold; background: transparent; border: none;"
        style += f" color: {color};" if color else f" color: {self._C_TEXT};"
        lbl.setStyleSheet(style)

    def _populate_installation(self):
        import platform as _pl, sys as _sys

        self._set_i("software",   "EngCut",        color=self._C_WHITE)
        self._set_i("version",    self._VERSION,   color=self._C_WHITE)
        self._set_i("build",      self._BUILD)
        self._set_i("build_date", self._BUILD_DATE)

        try:
            self._set_i("os",   f"{_pl.system()} {_pl.release()}")
        except Exception:
            self._set_i("os", "Windows")

        try:
            self._set_i("arch", _pl.architecture()[0])
        except Exception:
            self._set_i("arch", "64bit")

        try:
            _lm = _load_engine_module("language_manager")
            lang_code = getattr(_lm, "_lang", "en") or "en"
            self._set_i("language", {"en": "English", "pt": "Português", "es": "Español"}.get(lang_code, lang_code.upper()))
        except Exception:
            self._set_i("language", "English")

        try:
            backend = os.environ.get("ENGCUT_STOCK_BACKEND", "sqlite").lower()
            self._set_i("database", "SQLite" if backend == "sqlite" else "Excel/xlsx")
        except Exception:
            self._set_i("database", "SQLite")

        run_mode = "Executable" if getattr(_sys, "frozen", False) else "Development"
        self._set_i("run_mode", run_mode)

        try:
            self._machine_id_value = _load_engine_module("license.machine_id").get_machine_id()
        except Exception:
            self._machine_id_value = ""

    def _populate_license(self):
        import json, traceback as _tb

        try:
            _v = _load_engine_module("license.validator")
            result = _v.validate()
            status = result.status
            plan   = result.plan or "—"
            days   = result.days_left

            if status == "LICENSED":
                self._set_l("status", "🟢  Active",      color=self._C_GREEN)
            elif status == "DEV_BYPASS":
                self._set_l("status", "🟢  Development", color=self._C_GREEN)
            elif status == "TRIAL":
                self._set_l("status", "🟡  Trial",       color=self._C_YELLOW)
            elif status == "OFFLINE":
                self._set_l("status", "🟡  Offline",     color=self._C_YELLOW)
            else:
                self._set_l("status", "🔴  Expired",     color=self._C_RED)

            self._set_l("plan", plan, color=self._C_WHITE)

            if plan.upper() == "PERPETUAL":
                self._set_l("days_left", "No Expiration", color=self._C_GREEN)
            elif days > 30:
                self._set_l("days_left", f"{days} days", color=self._C_GREEN)
            elif days > 7:
                self._set_l("days_left", f"{days} days", color=self._C_YELLOW)
            elif days > 0:
                self._set_l("days_left", f"{days} days", color=self._C_RED)
            else:
                self._set_l("days_left", "—", color=self._C_DIM)

        except Exception:
            self._set_l("status",   "🔴  License information unavailable", color=self._C_RED)
            self._set_l("plan",     "Please contact support",              color=self._C_DIM)
            for k in ("license_no", "activated", "expires", "days_left", "customer", "company"):
                self._set_l(k, "—", color=self._C_DIM)
            return

        # Read .lic file directly — no private API
        try:
            lic_raw = {}
            for candidate in (
                os.path.join(_ui_base_dir(), "database", ".license"),
                os.path.join(_ui_base_dir(), "license", "engcut.lic"),
            ):
                if os.path.exists(candidate):
                    with open(candidate, encoding="utf-8") as _f:
                        lic_raw = json.load(_f)
                    break

            lic_key = lic_raw.get("license_key") or "—"
            self._lic_key_value = lic_key if lic_key != "—" else ""
            self._set_l("license_no", lic_key)

            _is_perpetual = lic_raw.get("plan", "").upper() == "PERPETUAL"
            for field_key, lic_field in (("activated", "issued_at"), ("expires", "expires")):
                raw = lic_raw.get(lic_field, "")
                if field_key == "expires" and _is_perpetual:
                    self._set_l("expires", "Lifetime", color=self._C_GREEN)
                elif raw:
                    try:
                        from datetime import datetime as _dt
                        self._set_l(field_key, _dt.fromisoformat(raw).strftime("%d/%m/%Y"))
                    except Exception:
                        self._set_l(field_key, raw)
                else:
                    self._set_l(field_key, "—", color=self._C_DIM)

            self._set_l("customer", lic_raw.get("customer") or "—",
                        color="" if lic_raw.get("customer") else self._C_DIM)
            self._set_l("company",  lic_raw.get("company")  or "—",
                        color="" if lic_raw.get("company")  else self._C_DIM)

        except Exception:
            self._set_l("license_no", "—", color=self._C_DIM)
            self._set_l("activated",  "—", color=self._C_DIM)
            self._set_l("expires",    "—", color=self._C_DIM)
            self._set_l("customer",   "—", color=self._C_DIM)
            self._set_l("company",    "—", color=self._C_DIM)

    def _copy_license_key(self):
        from PySide6.QtWidgets import QApplication
        txt = self._lic_key_value
        if txt:
            QApplication.clipboard().setText(txt)
            self._btn_copy_lic.setText("✓   Copied!")
            QTimer.singleShot(2000, lambda: self._btn_copy_lic.setText("⎘   Copy License Key"))

    def _copy_machine_id(self):
        from PySide6.QtWidgets import QApplication
        try:
            mid = self._machine_id_value or _load_engine_module("license.machine_id").get_machine_id()
        except Exception:
            mid = ""
        if mid:
            QApplication.clipboard().setText(mid)
            self._btn_copy_mid.setText("✓   Copied!")
            QTimer.singleShot(2000, lambda: self._btn_copy_mid.setText("⎘   Copy Machine ID"))

    # ── Diagnostics ───────────────────────────────────────────────────────────

    def _generate_diagnostic(self):
        import zipfile, datetime, platform as _pl, json

        try:
            base     = _ui_base_dir()
            diag_dir = os.path.join(base, "outputs", "diagnostics")
            os.makedirs(diag_dir, exist_ok=True)

            ts       = datetime.datetime.now()
            zip_name = f"EngCut_Diagnostic_{ts.strftime('%Y%m%d_%H%M%S')}.zip"
            zip_path = os.path.join(diag_dir, zip_name)

            try:
                machine_id = _load_engine_module("license.machine_id").get_machine_id()
            except Exception:
                machine_id = "N/A"

            try:
                r = _load_engine_module("license.validator").validate()
                lic_summary = {"status": r.status, "plan": r.plan, "days_left": r.days_left}
            except Exception:
                lic_summary = {"status": "unavailable"}

            import sys as _sys
            diag_info = {
                "software":     "EngCut",
                "version":      self._VERSION,
                "build":        self._BUILD,
                "build_date":   self._BUILD_DATE,
                "generated_at": ts.isoformat(),
                "machine_id":   machine_id,
                "platform":     _pl.platform(),
                "python":       _sys.version,
                "architecture": _pl.architecture()[0],
                "processor":    _pl.processor(),
                "frozen":       getattr(_sys, "frozen", False),
                "license":      lic_summary,
            }

            with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
                zf.writestr("diagnostic.json", json.dumps(diag_info, indent=2, ensure_ascii=False))
                out_dir = os.path.join(base, "outputs")
                if os.path.isdir(out_dir):
                    for log in sorted([f for f in os.listdir(out_dir) if f.endswith(".log")], reverse=True)[:5]:
                        zf.write(os.path.join(out_dir, log), f"logs/{log}")
                for md in ("VERSION.md", "BUILD_INFO.md", "MANIFEST.md", "RELEASE_NOTES.md"):
                    p = os.path.join(base, md)
                    if os.path.exists(p):
                        zf.write(p, md)

            size_kb = os.path.getsize(zip_path) / 1024
            self._last_diag_path = zip_path
            self._set_d("date", ts.strftime("%d/%m/%Y  %H:%M:%S"), color=self._C_WHITE)
            self._set_d("file", zip_name)
            self._set_d("loc",  diag_dir)
            self._set_d("size", f"{size_kb:.1f} KB")
            self._btn_folder.setEnabled(True)
            self._btn_copy.setEnabled(True)

            QMessageBox.information(
                self, "SUPPORT CENTER — EngCut",
                f"Diagnostic package generated successfully.\n\nFile: {zip_name}\nLocation: {diag_dir}"
            )

        except Exception:
            import traceback as _tb
            print(f"[SUPPORT] Diagnostic error:\n{_tb.format_exc()}")
            QMessageBox.warning(self, "SUPPORT CENTER",
                "Unable to generate the diagnostic package.\nPlease contact support.")

    def _open_diag_folder(self):
        if self._last_diag_path:
            folder = os.path.dirname(self._last_diag_path)
            if os.path.isdir(folder):
                os.startfile(folder)

    def _copy_diag_path(self):
        if self._last_diag_path:
            QApplication.clipboard().setText(self._last_diag_path)
            QMessageBox.information(self, "SUPPORT CENTER", "File path copied to clipboard.")

    def _open_log_folder(self):
        log_dir = os.path.join(_ui_base_dir(), "outputs")
        if os.path.isdir(log_dir):
            os.startfile(log_dir)
        else:
            QMessageBox.information(self, "SUPPORT CENTER", f"Log folder not found:\n{log_dir}")

    # ── Support actions ───────────────────────────────────────────────────────

    def _create_support_email(self):
        import urllib.parse, webbrowser as _wb, sys as _sys, platform as _pl2

        try:
            machine_id = _load_engine_module("license.machine_id").get_machine_id()
        except Exception:
            machine_id = "N/A"

        try:
            lic_status = _load_engine_module("license.validator").validate().status
        except Exception:
            lic_status = "N/A"

        subject = urllib.parse.quote(f"EngCut {self._VERSION} — Support Request")
        body_text = (
            f"EngCut Version: {self._VERSION}\n"
            f"Build: {self._BUILD}\n"
            f"License Status: {lic_status}\n"
            f"Machine ID: {machine_id}\n"
            f"Platform: {_pl2.platform()}\n\n"
            "--- Please describe your issue below ---\n\n"
        )
        _wb.open(f"mailto:{self._SUPPORT_EMAIL}?subject={urllib.parse.quote(f'EngCut {self._VERSION} — Support Request')}&body={urllib.parse.quote(body_text)}")

    def _open_documentation(self):
        doc_dir = os.path.join(_ui_base_dir(), "documentation")
        if os.path.isdir(doc_dir):
            os.startfile(doc_dir)
        else:
            QMessageBox.information(self, "SUPPORT CENTER",
                f"Documentation folder not found.\n\nContact: {self._SUPPORT_EMAIL}")

    def _check_for_updates(self):
        QMessageBox.information(
            self, "CHECK FOR UPDATES",
            f"You are running EngCut v{self._VERSION}\n\n"
            "Automatic update check will be available in a future version.\n\n"
            f"For updates, contact:\n{self._SUPPORT_EMAIL}"
        )


# =============================================================================
# ENTRY POINT
# =============================================================================

if __name__ == "__main__":

    app = QApplication(sys.argv)

    # ── OBS Window Capture — Compatibilidade Windows ───────────────────────
    # Causa raiz: Qt cria janelas popup (QMenu, QComboBox, QFileDialog,
    # QDialog, QMessageBox) como HWNDs independentes. O OBS Window Capture
    # (método BitBlt) captura apenas o HWND-alvo e ignora esses popups.
    # Agravante: quando efeitos "fade" estão ativos no Windows, o Qt adiciona
    # WS_EX_LAYERED nos HWNDs de popup — tornando-os invisíveis ao BitBlt.
    #
    # Correção A — desabilita por processo os efeitos Qt que disparam
    #   WS_EX_LAYERED (UI_FadeMenu, UI_AnimateMenu, etc.).
    # Correção B — filtro de evento instalado na QApplication: remove
    #   WS_EX_LAYERED de qualquer janela top-level ao ser exibida,
    #   cobrindo QComboBox, QMenu, QFileDialog, QDialog e QMessageBox.
    #
    # Sem impacto em visual, layout, funcionamento ou fluxo da interface.
    # No-op em plataformas não-Windows.
    _obs_filter = None  # referência em escopo — impede que o GC destrua o filtro
    if sys.platform == "win32":
        try:
            import ctypes as _ct
            from PySide6.QtCore import QObject as _QObject, QEvent as _QEvent
            from PySide6.QtWidgets import QWidget as _QWidget

            # Correção A: desabilita efeitos fade/animate neste processo.
            # Esses efeitos adicionam WS_EX_LAYERED nos HWNDs de popup do Qt,
            # impedindo que o OBS BitBlt os capture.
            for _eff in (
                Qt.UIEffect.UI_FadeMenu,
                Qt.UIEffect.UI_AnimateMenu,
                Qt.UIEffect.UI_AnimateCombo,
                Qt.UIEffect.UI_FadeTooltip,
                Qt.UIEffect.UI_AnimateTooltip,
                Qt.UIEffect.UI_AnimateToolBox,
            ):
                try:
                    app.setEffectEnabled(_eff, False)
                except Exception:
                    pass

            # Correção B: filtro de evento — strip WS_EX_LAYERED no show.
            # Cobre casos residuais onde A não suprime a flag (ex.: QFileDialog
            # usa janela nativa do Windows que pode receber WS_EX_LAYERED
            # independentemente das preferências de efeito do processo Qt).
            _GWL_EXSTYLE   = -20
            _WS_EX_LAYERED = 0x00080000
            _u32 = _ct.windll.user32

            class _ObsPopupFilter(_QObject):
                """Remove WS_EX_LAYERED de janelas top-level ao serem exibidas."""

                def eventFilter(self, obj, event):
                    if event.type() == _QEvent.Type.Show:
                        try:
                            if isinstance(obj, _QWidget) and obj.isWindow():
                                hwnd = int(obj.winId())
                                if hwnd:
                                    ex = _u32.GetWindowLongW(hwnd, _GWL_EXSTYLE)
                                    if ex & _WS_EX_LAYERED:
                                        _u32.SetWindowLongW(
                                            hwnd, _GWL_EXSTYLE,
                                            ex & ~_WS_EX_LAYERED,
                                        )
                        except Exception:
                            pass
                    return False

            _obs_filter = _ObsPopupFilter()
            app.installEventFilter(_obs_filter)

        except Exception:
            pass
    # ── Fim compatibilidade OBS ────────────────────────────────────────────

    window = EngCutUI()

    window.showMaximized()

    sys.exit(app.exec())
