"""
Módulo Calibration - FastChecker II 3.0
Ajusta Power on Tag Forward e Reverse usando Tag Padrão #124; mesmo tema e fluxo do Threshold.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import sys
import ctypes
import struct
import threading
import time
import numpy as np
from typing import Optional, List, Dict, Any, Tuple

try:
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    from matplotlib.ticker import MultipleLocator
    MATPLOTLIB_OK = True
except ImportError:
    MATPLOTLIB_OK = False

from .calibration_reference_table import (
    TAG_REFERENCE_EPC,
    get_reference_table,
    get_reference_forward_reverse,
)
from .calibration_profile import build_profile, save_profile, load_profile
from .calibration_service import (
    set_active_profile,
    has_active_calibration,
    get_active_profile,
    clear_active_calibration,
)
from .correction_table import apply_corrections_to_result
from .i18n import get_translator as _get_translator, t
from .app_console import fc_log

try:
    from src.core.port_manager import get_port_manager
except ImportError:
    get_port_manager = None

# Reutiliza hardware do Threshold (mesma DLL e constantes de segurança)
try:
    from . import threshold_module as th
    rfid_sdk = getattr(th, "rfid_sdk", None)
    HARDWARE_AVAILABLE = getattr(th, "HARDWARE_AVAILABLE", False)
    BAUD_RATE = getattr(th, "BAUD_RATE", 115200)
    RFID_CMD_SET_TXPOWER = getattr(th, "RFID_CMD_SET_TXPOWER", 0x10)
    RFID_CMD_SET_FREQ_TABLE = getattr(th, "RFID_CMD_SET_FREQ_TABLE", 0x14)
    RFID_CMD_INV_TAG = getattr(th, "RFID_CMD_INV_TAG", 0x80)
    RFID_CMD_GET_TEMPERATURE = getattr(th, "RFID_CMD_GET_TEMPERATURE", 0x34)
    RFID_CMD_GET_PORT_LOSS = getattr(th, "RFID_CMD_GET_PORT_LOSS", 0x32)
    RxAdcTable = getattr(th, "RxAdcTable", [0x0000, 0xFFFF])
    REFLECTED_POWER_LIMIT = getattr(th, "REFLECTED_POWER_LIMIT", 10.0)
    TEMPERATURE_LIMIT = getattr(th, "TEMPERATURE_LIMIT", 50.0)
    HEATING_POWER_DBM = getattr(th, "HEATING_POWER_DBM", 25)
except Exception as e:
    fc_log("calibration", f"Import threshold_module failed: {e}")
    rfid_sdk = None
    HARDWARE_AVAILABLE = False
    BAUD_RATE = 115200
    RFID_CMD_SET_TXPOWER = 0x10
    RFID_CMD_SET_FREQ_TABLE = 0x14
    RFID_CMD_INV_TAG = 0x80
    RFID_CMD_GET_TEMPERATURE = 0x34
    RFID_CMD_GET_PORT_LOSS = 0x32
    RxAdcTable = [0x0000, 0xFFFF]
    REFLECTED_POWER_LIMIT = 10.0
    TEMPERATURE_LIMIT = 50.0
    HEATING_POWER_DBM = 25

# Passo de potência ao diminuir até o threshold (dB)
POWER_STEP_DB = 0.5
# Passo de frequência predefinido na UI de calibração (MHz)
DEFAULT_FREQ_STEP_MHZ = 1
# Inventário ao confirmar EPC: só aceita TAG_REFERENCE_EPC (ignora tags mais fortes no campo).
EPC_READ_TARGET_MAX_ATTEMPTS = 80
# Intervalo (ms) entre tentativas na UI — devolve o loop de eventos do Tk (evita tela branca).
EPC_READ_UI_YIELD_MS = 15


def _hw_read_temperature(port: int) -> Optional[float]:
    """Lê temperatura do hardware com a porta já aberta. Usado por Calibration e Verificação."""
    if not HARDWARE_AVAILABLE or rfid_sdk is None:
        return None
    try:
        out = ctypes.create_string_buffer(64)
        out_len = ctypes.c_uint(0)
        if rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_TEMPERATURE, None, 0, out, ctypes.byref(out_len)) == 0:
            if out_len.value >= 3:
                temp_val = struct.unpack(">h", out.raw[1:3])[0]
                return temp_val / 100.0
    except Exception as e:
        fc_log("calibration", f"_hw_read_temperature: {e}")
    return None


def _hw_read_reflected_power(port: int) -> Optional[float]:
    """Lê potência refletida (dBm) com a porta já aberta. Usado por Calibration e Verificação."""
    if not HARDWARE_AVAILABLE or rfid_sdk is None:
        return None
    try:
        out = ctypes.create_string_buffer(64)
        out_len = ctypes.c_uint(0)
        status = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_PORT_LOSS, b"", 0, out, ctypes.byref(out_len))
        if status == 0 and out_len.value >= 3:
            adc_value = struct.unpack(">H", out.raw[1:3])[0]
            reflected_dbm = -25 + next(
                (i for i, val in enumerate(RxAdcTable) if adc_value <= val), len(RxAdcTable) - 1
            )
            return float(reflected_dbm)
    except Exception as e:
        fc_log("calibration", f"_hw_read_reflected_power: {e}")
    return None


def _calc_vswr_from_reflected_power(tx_power_dbm: float, reflected_power_dbm: float) -> float:
    """
    Converte potência refletida (dBm) para VSWR (número adimensional), igual ao Threshold/FastSurance.
    Retorna valor entre 1.0 e 10.0 (ex.: 1.02 = sistema bem casado).
    """
    try:
        tx_power_w = 10 ** (tx_power_dbm / 10) / 1000
        reflected_power_w = 10 ** (reflected_power_dbm / 10) / 1000
        if tx_power_w <= 0:
            return 1.0
        reflection_coefficient = (reflected_power_w / tx_power_w) ** 0.5
        reflection_coefficient = max(0.0, min(1.0, reflection_coefficient))
        if reflection_coefficient < 1:
            vswr = (1 + reflection_coefficient) / (1 - reflection_coefficient)
        else:
            vswr = 1.0
        return max(1.0, min(10.0, vswr))
    except Exception:
        return 1.0


def _power_on_tag_forward_reverse(
    module_power: float,
    rssi: float,
    attenuator_db: float,
    distance_m: float,
    freq_mhz: float,
) -> Tuple[float, float]:
    """Mesmas fórmulas que apply_corrections_to_result (incl. tabela CSV no forward), sem perfil ativo."""
    base = {
        "freq_mhz": float(freq_mhz),
        "threshold_power": float(module_power),
        "threshold_rssi": float(rssi),
        "module_power": float(module_power),
        "rssi": float(rssi),
    }
    out = apply_corrections_to_result(
        base,
        float(attenuator_db),
        float(distance_m),
        apply_profile_calibration=False,
        backscatter_attenuator_db_factor=1.0,
    )
    return (out["power_on_tag_forward"], out["power_on_tag_reversed"])


def _raw_step_frequency_list(freq_min: int, freq_max: int, freq_step: int) -> List[int]:
    """Gera 860,865,..., incluindo freq_max quando não cai no passo (ex.: 861–940 step 5 → termina em 940).

    Converte min/max/step para inteiros: limites de licença e clamps podem vir como float MHz.
    """
    try:
        fm = int(round(float(freq_min)))
        fM = int(round(float(freq_max)))
        st = int(round(float(freq_step)))
    except (TypeError, ValueError):
        return []
    if st <= 0:
        st = 1
    if fm > fM:
        return []
    freqs = list(range(fm, fM + 1, st))
    if not freqs:
        freqs = [fm]
    if freqs[-1] < fM:
        freqs.append(fM)
    return freqs


def validate_frequency_against_license(freq: float, license_limits: Dict[str, Any]) -> bool:
    """Igual ThresholdModule._validate_frequency_against_license (sem depender da classe)."""
    if not license_limits:
        return True
    if not license_limits.get("is_licensed", False):
        return True
    freq_ranges = license_limits.get("freq_ranges", [])
    excluded_ranges = license_limits.get("excluded_ranges", [])
    if freq_ranges:
        for range_min, range_max in freq_ranges:
            if range_min <= freq <= range_max:
                for excl_min, excl_max in excluded_ranges:
                    if excl_min <= freq <= excl_max:
                        return False
                return True
        return False
    min_freq = license_limits.get("min_freq", 800)
    max_freq = license_limits.get("max_freq", 1000)
    return min_freq <= freq <= max_freq


def _expand_calibration_frequency_list(
    freq_min: int, freq_max: int, freq_step: int, license_limits: Dict[str, Any]
) -> List[int]:
    raw = _raw_step_frequency_list(freq_min, freq_max, freq_step)
    valid: List[int] = []
    for f in raw:
        if validate_frequency_against_license(float(f), license_limits):
            valid.append(f)
        else:
            fc_log("calibration", f"{f} MHz skipped (outside license band or exclusion).")
    return valid


def _interpolate_corrections_to_allowed_1mhz(
    corrections: List[Dict[str, float]],
    allowed_freqs_1mhz: List[int],
) -> List[Dict[str, float]]:
    if not corrections:
        return []
    if not allowed_freqs_1mhz:
        return []

    points = sorted(corrections, key=lambda p: float(p.get("freq_mhz", 0.0)))
    src_freqs = [float(p["freq_mhz"]) for p in points]
    src_fwd = [float(p["correction_forward_dbm"]) for p in points]
    src_rev = [float(p["correction_reverse_dbm"]) for p in points]

    out: List[Dict[str, float]] = []
    for f_int in sorted(set(allowed_freqs_1mhz)):
        f = float(f_int)
        if f <= src_freqs[0]:
            fwd_val = src_fwd[0]
            rev_val = src_rev[0]
        elif f >= src_freqs[-1]:
            fwd_val = src_fwd[-1]
            rev_val = src_rev[-1]
        else:
            for i in range(len(src_freqs) - 1):
                f0, f1 = src_freqs[i], src_freqs[i + 1]
                if f0 <= f <= f1:
                    if f1 == f0:
                        t_lin = 0.0
                    else:
                        t_lin = (f - f0) / (f1 - f0)
                    fwd_val = src_fwd[i] + t_lin * (src_fwd[i + 1] - src_fwd[i])
                    rev_val = src_rev[i] + t_lin * (src_rev[i + 1] - src_rev[i])
                    break
        out.append(
            {
                "freq_mhz": f,
                "correction_forward_dbm": round(float(fwd_val), 2),
                "correction_reverse_dbm": round(float(rev_val), 2),
            }
        )
    return out


def _resolve_active_license_limits(app_shell) -> Dict[str, Any]:
    defaults: Dict[str, Any] = {
        "min_freq": 800,
        "max_freq": 1000,
        "min_power": 5,
        "max_power": 25,
        "is_licensed": False,
        "freq_ranges": [],
        "excluded_ranges": [],
        "license_name": None,
    }
    if app_shell is None:
        return dict(defaults)

    token = getattr(app_shell, "active_license_token", None)
    try:
        if not token and hasattr(app_shell, "_load_active_license"):
            token = app_shell._load_active_license()
    except Exception:
        pass

    if not token or not hasattr(app_shell, "license_manager"):
        cached = getattr(app_shell, "license_limits", None)
        return dict(cached) if isinstance(cached, dict) else dict(defaults)

    try:
        lic_data, _ = app_shell.license_manager.parse_and_validate_token(token)
        if not lic_data:
            cached = getattr(app_shell, "license_limits", None)
            return dict(cached) if isinstance(cached, dict) else dict(defaults)

        limits = dict(defaults)
        limits["is_licensed"] = True
        limits["license_name"] = lic_data.get("licName")

        freq_raw = str(lic_data.get("freqRange", "") or "").strip()
        if freq_raw:
            for chunk in freq_raw.split(";"):
                if "@" not in chunk:
                    continue
                a, b = chunk.split("@", 1)
                f_min = float(a.replace(",", "."))
                f_max = float(b.replace(",", "."))
                limits["freq_ranges"].append((f_min, f_max))
            if limits["freq_ranges"]:
                all_edges = [x for rg in limits["freq_ranges"] for x in rg]
                limits["min_freq"] = min(all_edges)
                limits["max_freq"] = max(all_edges)

        gap_raw = str(lic_data.get("freqGap", "") or "").strip()
        if gap_raw:
            for chunk in gap_raw.split(";"):
                if "@" not in chunk:
                    continue
                a, b = chunk.split("@", 1)
                g_min = float(a.replace(",", "."))
                g_max = float(b.replace(",", "."))
                limits["excluded_ranges"].append((g_min, g_max))

        power_raw = str(lic_data.get("powRange", "") or "").strip()
        if "@" in power_raw:
            p_min_s, p_max_s = power_raw.split("@", 1)
            limits["min_power"] = int(float(p_min_s))
            limits["max_power"] = int(float(p_max_s))

        return limits
    except Exception:
        cached = getattr(app_shell, "license_limits", None)
        return dict(cached) if isinstance(cached, dict) else dict(defaults)


def _hw_apply_power_and_freq(port: int, freq: float, power: float) -> bool:
    """Aplica potência e frequência no leitor. Usado por calibração e verificação."""
    if not HARDWARE_AVAILABLE or rfid_sdk is None:
        return False
    try:
        out = ctypes.create_string_buffer(64)
        out_len = ctypes.c_uint(0)
        power_val = int(power * 100)
        power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, "big") * 2
        r1 = rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, out, ctypes.byref(out_len))
        freq_data = b"\x01" + int(freq * 1000).to_bytes(3, "big")
        r2 = rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, out, ctypes.byref(out_len))
        time.sleep(0.02)
        return r1 == 0 and r2 == 0
    except Exception:
        return False


def _hw_single_read_rssi(
    port: int,
    freq: float,
    power: float,
    get_cancelled: Optional[Any] = None,
) -> Optional[float]:
    """Uma leitura de RSSI na (freq, power). Porta deve estar aberta pelo caller."""
    if not HARDWARE_AVAILABLE or rfid_sdk is None:
        return None
    get_cancelled = get_cancelled or (lambda: False)
    try:
        if not _hw_apply_power_and_freq(port, freq, power):
            return None
        out = ctypes.create_string_buffer(256)
        out_len = ctypes.c_uint(0)
        inv_data = bytes([0x00, 0x64])
        for _ in range(20):
            if get_cancelled():
                return None
            res = rfid_sdk.UHF_RFID_Set(RFID_CMD_INV_TAG, ctypes.c_char_p(inv_data), 2, out, ctypes.byref(out_len))
            if res == 0 and out_len.value >= 16:
                detected = out.raw[2:14].hex().upper()
                if detected == TAG_REFERENCE_EPC.upper():
                    rssi_bytes = out.raw[out_len.value - 3 : out_len.value - 1]
                    rssi_raw = struct.unpack(">h", rssi_bytes)[0]
                    return float(int(rssi_raw)) / 10.0
            time.sleep(0.05)
    except Exception as e:
        fc_log("calibration", f"_hw_single_read_rssi: {e}")
    return None


def _hw_find_threshold_and_rssi(
    port: int,
    freq: float,
    max_power: float,
    min_power: float,
    get_cancelled: Optional[Any] = None,
) -> Tuple[Optional[float], Optional[float]]:
    """Começa na potência máxima e diminui até o threshold; retorna (potência_threshold, rssi) ou (None, None)."""
    if not HARDWARE_AVAILABLE or rfid_sdk is None:
        return (None, None)
    get_cancelled = get_cancelled or (lambda: False)
    power = max_power
    last_ok_power = None
    last_ok_rssi = None
    while power >= min_power and not get_cancelled():
        rssi = _hw_single_read_rssi(port, freq, power, get_cancelled)
        if rssi is not None:
            last_ok_power = power
            last_ok_rssi = rssi
            power -= POWER_STEP_DB
        else:
            break
    return (last_ok_power, last_ok_rssi)


class CalibrationModule(tk.Frame):
    """Tela de calibração: 2 gráficos (Forward/Reverse), EPC, Iniciar/Parar/Salvar/Importar."""

    def __init__(self, parent, app_shell=None, com_port: int = 4):
        super().__init__(parent)
        self.parent = parent
        self.app_shell = app_shell
        self.com_port = com_port
        self.module_name = "Calibration"
        self.translator = _get_translator()
        self.license_limits = self._get_license_limits()
        self._calibration_cancelled = False
        self._calibration_thread = None
        self._last_profile = None  # perfil salvo/carregado para ativar
        self._clear_requested = False

        # Estado EPC
        self._epc_confirmed = False
        self._read_epc_value = None

        # Dados da última calibração (para gráficos)
        self._corrections: List[Dict[str, float]] = []
        self._freqs: List[float] = []
        self._forward_corrections: List[float] = []
        self._reverse_corrections: List[float] = []

        self._build_ui()
        self.translator.add_language_change_listener(self._on_language_changed)

        # Mostra imediatamente a calibração ativa (se houver)
        try:
            self._update_active_calibration_label()
        except Exception:
            pass

    def destroy(self):
        try:
            self.translator.remove_language_change_listener(self._on_language_changed)
        except Exception:
            pass
        super().destroy()

    def _update_active_calibration_label(self):
        """
        Atualiza o texto que indica qual calibração está ativa no sistema.
        Usa o mesmo padrão de texto do módulo Threshold.
        """
        from .calibration_service import has_active_calibration, get_active_profile

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

        if not has_active_calibration():
            text = f"{t('threshold.calibration_status')} {t('threshold.calibration_none')}"
        else:
            profile = get_active_profile() or {}
            name = profile.get("profile_name") or profile.get("name") or ""
            if not name:
                freq_min = profile.get("freq_min_mhz", "")
                freq_max = profile.get("freq_max_mhz", "")
                step = profile.get("freq_step_mhz", "")
                name = f"{freq_min}-{freq_max}MHz / step {step}MHz"
            text = f"{t('threshold.calibration_status')} {name}"

        self.active_calibration_label.config(text=text)

    def _on_language_changed(self, _old_lang, _new_lang):
        """Callback quando o idioma é alterado."""
        self.after(0, self.refresh_language)

    def refresh_language(self):
        """Atualiza todos os textos da interface quando o idioma é alterado."""
        try:
            if not hasattr(self, "params_frame"):
                return
            self.params_frame.config(text=t("calibration.params_frame"))
            if hasattr(self, "_label_freq_min"):
                self._label_freq_min.config(text=t("calibration.freq_min"))
            if hasattr(self, "_label_freq_max"):
                self._label_freq_max.config(text=t("calibration.freq_max"))
            if hasattr(self, "_label_step"):
                self._label_step.config(text=t("calibration.step_freq"))
            if hasattr(self, "_label_distance"):
                self._label_distance.config(text=t("calibration.distance"))
            if hasattr(self, "_label_attenuator"):
                self._label_attenuator.config(text=t("calibration.attenuator"))
            if hasattr(self, "_label_antenna"):
                self._label_antenna.config(text=t("calibration.antenna"))
            if hasattr(self, "_label_environment"):
                self._label_environment.config(text=t("calibration.environment"))
            if hasattr(self, "epc_frame"):
                self.epc_frame.config(text=t("calibration.standard_tag"))
            if hasattr(self, "btn_read_epc"):
                self.btn_read_epc.config(text=t("calibration.read_epc"))
            if hasattr(self, "btn_start_stop"):
                if hasattr(self, "_calibration_thread") and self._calibration_thread and self._calibration_thread.is_alive():
                    self.btn_start_stop.config(text=t("calibration.stop"))
                else:
                    self.btn_start_stop.config(text=t("calibration.start"))
            if hasattr(self, "btn_save"):
                self.btn_save.config(text=t("calibration.save"))
            if hasattr(self, "btn_import"):
                self.btn_import.config(text=t("calibration.import"))
            if hasattr(self, "btn_clear"):
                self.btn_clear.config(text=t("calibration.clear"))
            if hasattr(self, "btn_verification"):
                self.btn_verification.config(text=t("calibration.verification_btn"))
            if hasattr(self, "_label_epc_expected"):
                self._label_epc_expected.config(text=t("calibration.epc_expected") + " " + TAG_REFERENCE_EPC)
            if hasattr(self, "epc_status_label") and not self._epc_confirmed and self.epc_status_label.cget("text") != t("calibration.reading"):
                self.epc_status_label.config(text=t("calibration.read_epc_hint"))
            if MATPLOTLIB_OK and hasattr(self, "ax1"):
                self.ax1.set_title(t("calibration.graph_forward"))
                self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
            if MATPLOTLIB_OK and hasattr(self, "ax2"):
                self.ax2.set_title(t("calibration.graph_reverse"))
                self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
            if hasattr(self, "canvas1"):
                self.canvas1.draw()
            if hasattr(self, "canvas2"):
                self.canvas2.draw()

            # Atualiza também o texto da calibração ativa
            if hasattr(self, "active_calibration_label"):
                self._update_active_calibration_label()
        except Exception as e:
            fc_log("calibration", f"refresh_language: {e}")

    def _get_license_limits(self) -> dict:
        return _resolve_active_license_limits(self.app_shell)

    def _apply_license_frequency_range(self) -> Tuple[int, int]:
        limits = self._get_license_limits()
        freq_min = int(round(float(limits.get("min_freq", 800))))
        freq_max = int(round(float(limits.get("max_freq", 1000))))
        self.var_freq_min.set(str(freq_min))
        self.var_freq_max.set(str(freq_max))
        self.license_limits = limits
        return (freq_min, freq_max)

    def refresh_license_from_app_shell(self):
        try:
            self._apply_license_frequency_range()
        except Exception as e:
            fc_log("calibration", f"refresh_license_from_app_shell: {e}")

    def _get_temperature_with_port(self, port: int) -> Optional[float]:
        return _hw_read_temperature(port)

    def _read_reflected_power_with_port(self, port: int) -> Optional[float]:
        return _hw_read_reflected_power(port)

    def _get_port_and_manager(self):
        """Retorna (port_number, port_manager). Usa port_manager.com_port quando disponível."""
        pm = getattr(self.app_shell, "port_manager", None) if self.app_shell else None
        if pm is None and get_port_manager:
            pm = get_port_manager()
        port = getattr(pm, "com_port", None) if pm else None
        if port is None:
            port = self.com_port
        return (port, pm)

    def _run_safety_check(self) -> Tuple[bool, bool]:
        """
        Verifica temperatura e VSWR (potência refletida) antes da calibração.
        Atualiza os labels do painel de segurança. Retorna (temp_ok, vswr_ok).
        """
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            self.safety_temperature_label.config(text="--°C")
            self.safety_vswr_label.config(text="--")
            return (True, True)
        port, pm = self._get_port_and_manager()
        acquired = False
        if pm:
            if not pm.acquire_port("Calibration", timeout=3.0):
                self.safety_temperature_label.config(text="--°C")
                self.safety_vswr_label.config(text="--")
                return (False, False)
            acquired = True
        try:
            if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
                self.safety_temperature_label.config(text="--°C")
                self.safety_vswr_label.config(text="--")
                return (False, False)
            temp = self._get_temperature_with_port(port)
            reflected = self._read_reflected_power_with_port(port)
            if temp is not None:
                self.safety_temperature_label.config(text=f"{temp:.1f}°C")
                if temp >= TEMPERATURE_LIMIT:
                    self.safety_temperature_label.config(foreground="red")
                elif temp >= 45:
                    self.safety_temperature_label.config(foreground="orange")
                else:
                    self.safety_temperature_label.config(foreground="green")
            else:
                self.safety_temperature_label.config(text="--°C", foreground="gray")
            if reflected is not None:
                vswr_ratio = _calc_vswr_from_reflected_power(HEATING_POWER_DBM, reflected)
                self.safety_vswr_label.config(text=f"{vswr_ratio:.2f}")
                if reflected > REFLECTED_POWER_LIMIT:
                    self.safety_vswr_label.config(foreground="red")
                else:
                    self.safety_vswr_label.config(foreground="green")
            else:
                self.safety_vswr_label.config(text="--", foreground="gray")
            temp_ok = temp is None or temp < TEMPERATURE_LIMIT
            vswr_ok = reflected is None or reflected <= REFLECTED_POWER_LIMIT
            return (temp_ok, vswr_ok)
        finally:
            try:
                rfid_sdk.UHF_RFID_Close(port)
            except Exception:
                pass
            if pm and acquired:
                pm.release_port("Calibration")

    def _build_ui(self):
        self.config(bg="#F5F5F5")
        # --- Parâmetros ---
        self.params_frame = ttk.LabelFrame(self, text=t("calibration.params_frame"), padding=8)
        self.params_frame.pack(fill="x", padx=10, pady=5)

        self._label_freq_min = ttk.Label(self.params_frame, text=t("calibration.freq_min"))
        self._label_freq_min.grid(row=0, column=0, sticky="w", padx=(0, 4))
        self.var_freq_min = tk.StringVar(value="")
        self.entry_freq_min = ttk.Entry(self.params_frame, textvariable=self.var_freq_min, width=8, state="readonly")
        self.entry_freq_min.grid(row=0, column=1, padx=(0, 12))

        self._label_freq_max = ttk.Label(self.params_frame, text=t("calibration.freq_max"))
        self._label_freq_max.grid(row=0, column=2, sticky="w", padx=(0, 4))
        self.var_freq_max = tk.StringVar(value="")
        self.entry_freq_max = ttk.Entry(self.params_frame, textvariable=self.var_freq_max, width=8, state="readonly")
        self.entry_freq_max.grid(row=0, column=3, padx=(0, 12))

        self._label_step = ttk.Label(self.params_frame, text=t("calibration.step_freq"))
        self._label_step.grid(row=0, column=4, sticky="w", padx=(12, 4))
        self.var_freq_step = tk.StringVar(value=str(DEFAULT_FREQ_STEP_MHZ))
        self.combo_freq_step = ttk.Combobox(self.params_frame, textvariable=self.var_freq_step, values=["1", "5"], width=6, state="readonly")
        self.combo_freq_step.grid(row=0, column=5, padx=(0, 12))

        self._label_distance = ttk.Label(self.params_frame, text=t("calibration.distance"))
        self._label_distance.grid(row=1, column=0, sticky="w", padx=(0, 4))
        self.var_distance = tk.StringVar(value="")
        self.entry_distance = ttk.Entry(self.params_frame, textvariable=self.var_distance, width=8)
        self.entry_distance.grid(row=1, column=1, padx=(0, 12))

        self._label_attenuator = ttk.Label(self.params_frame, text=t("calibration.attenuator"))
        self._label_attenuator.grid(row=1, column=2, sticky="w", padx=(0, 4))
        self.var_attenuator = tk.StringVar(value="")
        self.entry_attenuator = ttk.Entry(self.params_frame, textvariable=self.var_attenuator, width=8)
        self.entry_attenuator.grid(row=1, column=3, padx=(0, 12))

        self._label_antenna = ttk.Label(self.params_frame, text=t("calibration.antenna"))
        self._label_antenna.grid(row=2, column=0, sticky="w", padx=(0, 4))
        self.var_antenna = tk.StringVar(value="")
        self.entry_antenna = ttk.Entry(self.params_frame, textvariable=self.var_antenna, width=20)
        self.entry_antenna.grid(row=2, column=1, padx=(0, 12))

        self._label_environment = ttk.Label(self.params_frame, text=t("calibration.environment"))
        self._label_environment.grid(row=2, column=2, sticky="w", padx=(0, 4))
        self.var_environment = tk.StringVar(value="")
        self.entry_environment = ttk.Entry(self.params_frame, textvariable=self.var_environment, width=30)
        self.entry_environment.grid(row=2, column=3, sticky="ew", padx=(0, 12))

        # --- Sistema de segurança (temperatura e VSWR - igual Threshold/FastSurance) ---
        safety_frame = ttk.LabelFrame(self, text=t("threshold.safety_system"), padding=8)
        safety_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(safety_frame, text=t("threshold.temperature")).grid(row=0, column=0, sticky="w", padx=(0, 4))
        self.safety_temperature_label = tk.Label(safety_frame, text="--°C", fg="gray", font=("Helvetica", 10, "bold"))
        self.safety_temperature_label.grid(row=0, column=1, padx=(0, 16))
        ttk.Label(safety_frame, text=t("threshold.vswr")).grid(row=0, column=2, sticky="w", padx=(0, 4))
        self.safety_vswr_label = tk.Label(safety_frame, text="--", fg="gray", font=("Helvetica", 10, "bold"))
        self.safety_vswr_label.grid(row=0, column=3, sticky="w")

        # --- Tag padrão / EPC ---
        epc_frame = ttk.LabelFrame(self, text=t("calibration.standard_tag"), padding=8)
        epc_frame.pack(fill="x", padx=10, pady=5)

        self._label_epc_expected = ttk.Label(epc_frame, text=t("calibration.epc_expected") + " " + TAG_REFERENCE_EPC)
        self._label_epc_expected.grid(row=0, column=0, columnspan=2, sticky="w")
        self.epc_status_label = ttk.Label(epc_frame, text=t("calibration.read_epc_hint"), foreground="gray")
        self.epc_status_label.grid(row=1, column=0, sticky="w", pady=2)
        self.btn_read_epc = ttk.Button(epc_frame, text=t("calibration.read_epc"), command=self._on_read_epc)
        self.btn_read_epc.grid(row=1, column=1, padx=8)

        # --- Botões principais ---
        btn_frame = ttk.Frame(self)
        btn_frame.pack(fill="x", padx=10, pady=5)

        self.btn_start_stop = ttk.Button(btn_frame, text=t("calibration.start"), command=self._on_start_calibration, state="disabled")
        self.btn_start_stop.pack(side="left", padx=2)
        self.btn_save = ttk.Button(btn_frame, text=t("calibration.save"), command=self._on_save)
        self.btn_save.pack(side="left", padx=2)
        self.btn_import = ttk.Button(btn_frame, text=t("calibration.import"), command=self._on_import)
        self.btn_import.pack(side="left", padx=2)
        self.btn_clear = ttk.Button(btn_frame, text=t("calibration.clear"), command=self._on_clear)
        self.btn_clear.pack(side="left", padx=2)
        self.btn_verification = ttk.Button(btn_frame, text=t("calibration.verification_btn"), command=self._on_open_verification)
        self.btn_verification.pack(side="left", padx=2)

        self.progress_var = tk.DoubleVar(value=0.0)
        self.progress = ttk.Progressbar(self, variable=self.progress_var, maximum=100)
        self.progress.pack(fill="x", padx=10, pady=4)
        self.status_label = ttk.Label(self, text="")
        self.status_label.pack(anchor="w", padx=10)

        # Indicação da calibração ativa (mesmo texto do módulo Threshold)
        self.active_calibration_label = ttk.Label(
            self,
            text=f"{t('threshold.calibration_status')} {t('threshold.calibration_none')}",
            foreground="purple",
        )
        self.active_calibration_label.pack(anchor="w", padx=10, pady=(0, 4))

        # --- Gráficos (mesmo tema Threshold: 2 gráficos) ---
        if MATPLOTLIB_OK:
            fig_frame = ttk.Frame(self)
            fig_frame.pack(fill="both", expand=True, padx=10, pady=5)
            self.fig1 = Figure(figsize=(5, 2.5), dpi=100, facecolor="#F5F5F5")
            self.ax1 = self.fig1.add_subplot(111)
            self.ax1.set_title(t("calibration.graph_forward"))
            self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
            self.ax1.grid(True)
            self.canvas1 = FigureCanvasTkAgg(self.fig1, master=fig_frame)
            self.canvas1.get_tk_widget().pack(side="left", fill="both", expand=True)
            self.fig2 = Figure(figsize=(5, 2.5), dpi=100, facecolor="#F5F5F5")
            self.ax2 = self.fig2.add_subplot(111)
            self.ax2.set_title(t("calibration.graph_reverse"))
            self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
            self.ax2.grid(True)
            self.canvas2 = FigureCanvasTkAgg(self.fig2, master=fig_frame)
            self.canvas2.get_tk_widget().pack(side="left", fill="both", expand=True)
            self._setup_graph_tooltips()
        else:
            ttk.Label(self, text=t("calibration.matplotlib_unavailable")).pack(pady=10)
        self._apply_license_frequency_range()

    def _on_read_epc(self):
        """Lê EPC do leitor (assíncrono); só libera Iniciar se for a tag padrão."""
        if getattr(self, "_epc_read_busy", False):
            return
        self._epc_read_busy = True
        try:
            self.btn_read_epc.config(state="disabled")
        except tk.TclError:
            self._epc_read_busy = False
            return
        self.epc_status_label.config(text=t("calibration.reading"), foreground="gray")
        self._epc_read_ctx = None
        self.after(EPC_READ_UI_YIELD_MS, self._read_epc_async_open)

    def _read_epc_async_open(self):
        """Abre porta, aplica potência/freq; agenda tentativas de inventário uma a uma."""
        if not getattr(self, "_epc_read_busy", False):
            return
        try:
            if not self.winfo_exists():
                self._read_epc_async_cleanup_and_finish(None, None)
                return
        except tk.TclError:
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        target = TAG_REFERENCE_EPC.upper()
        port = None
        pm = None
        acquired = False
        try:
            limits = self._get_license_limits()
            power = float(limits.get("max_power", 25))
            freq_s = self.var_freq_min.get().strip()
            freq = float(freq_s) if freq_s else 800.0
            freq = max(limits.get("min_freq", 800), min(limits.get("max_freq", 1000), freq))
            port, pm = self._get_port_and_manager()
            if pm:
                if not pm.acquire_port("CalibrationEPCRead", timeout=3.0):
                    self._read_epc_async_cleanup_and_finish(None, None)
                    return
                acquired = True
            if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
                if pm and acquired:
                    try:
                        pm.release_port("CalibrationEPCRead")
                    except Exception:
                        pass
                self._read_epc_async_cleanup_and_finish(None, None)
                return
            out = ctypes.create_string_buffer(256)
            out_len = ctypes.c_uint(0)
            power_val = int(power * 100)
            power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, "big") * 2
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, out, ctypes.byref(out_len))
            freq_data = b"\x01" + int(freq * 1000).to_bytes(3, "big")
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, out, ctypes.byref(out_len))
            self._epc_read_ctx = {
                "port": port,
                "pm": pm,
                "acquired": acquired,
                "target": target,
                "inv_data": bytes([0x00, 0x64]),
                "attempt": 0,
            }
        except Exception as e:
            fc_log("calibration", f"_read_epc_async_open: {e}")
            try:
                if port is not None and rfid_sdk is not None:
                    rfid_sdk.UHF_RFID_Close(port)
            except Exception:
                pass
            if pm and acquired:
                try:
                    pm.release_port("CalibrationEPCRead")
                except Exception:
                    pass
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        self.after(EPC_READ_UI_YIELD_MS, self._read_epc_async_step)

    def _read_epc_async_step(self):
        """Uma tentativa de inventário por tick de evento (mantém a UI responsiva)."""
        if not getattr(self, "_epc_read_busy", False):
            return
        ctx = getattr(self, "_epc_read_ctx", None)
        if not ctx:
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        try:
            if not self.winfo_exists():
                self._read_epc_async_abort_hw()
                return
        except tk.TclError:
            self._read_epc_async_abort_hw()
            return
        if ctx["attempt"] >= EPC_READ_TARGET_MAX_ATTEMPTS:
            self._read_epc_async_close_hw()
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        ctx["attempt"] += 1
        port = ctx["port"]
        target = ctx["target"]
        try:
            out = ctypes.create_string_buffer(256)
            out_len = ctypes.c_uint(0)
            res = rfid_sdk.UHF_RFID_Set(
                RFID_CMD_INV_TAG, ctypes.c_char_p(ctx["inv_data"]), 2, out, ctypes.byref(out_len)
            )
            if res == 0 and out_len.value >= 14:
                epc_hex = out.raw[2:14].hex().upper()
                if epc_hex == target:
                    rssi_val = None
                    if out_len.value >= 16:
                        rssi_bytes = out.raw[out_len.value - 3 : out_len.value - 1]
                        rssi_raw = struct.unpack(">h", rssi_bytes)[0]
                        rssi_val = float(int(rssi_raw)) / 10.0
                    self._read_epc_async_close_hw()
                    self._read_epc_async_cleanup_and_finish(epc_hex, rssi_val)
                    return
        except Exception as e:
            fc_log("calibration", f"_read_epc_async_step: {e}")
            self._read_epc_async_close_hw()
            self._read_epc_async_cleanup_and_finish(None, None)
            return
        self.after(EPC_READ_UI_YIELD_MS, self._read_epc_async_step)

    def _read_epc_async_close_hw(self):
        ctx = getattr(self, "_epc_read_ctx", None)
        if not ctx:
            return
        try:
            rfid_sdk.UHF_RFID_Close(ctx["port"])
        except Exception:
            pass
        pm, acquired = ctx.get("pm"), ctx.get("acquired")
        if pm and acquired:
            try:
                pm.release_port("CalibrationEPCRead")
            except Exception:
                pass

    def _read_epc_async_abort_hw(self):
        self._read_epc_async_close_hw()
        self._epc_read_ctx = None
        self._epc_read_busy = False

    def _read_epc_async_cleanup_and_finish(
        self, epc: Optional[str], rssi: Optional[float]
    ):
        self._epc_read_ctx = None
        self._read_epc_busy_finish_ui(epc, rssi)

    def _read_epc_busy_finish_ui(self, epc: Optional[str], rssi: Optional[float]):
        """Fecha ciclo de leitura e atualiza widgets (sempre no thread principal)."""
        self._epc_read_busy = False
        try:
            thread_alive = (
                getattr(self, "_calibration_thread", None) is not None
                and self._calibration_thread.is_alive()
            )
            if not thread_alive:
                self.btn_read_epc.config(state="normal")
        except tk.TclError:
            pass
        if epc is None:
            self.epc_status_label.config(
                text=t("calibration.epc_not_detected"),
                foreground="red",
            )
            self._epc_confirmed = False
            self.btn_start_stop.config(state="disabled")
            try:
                messagebox.showinfo(
                    t("calibration.tag_not_detected_title"),
                    t("calibration.tag_not_detected_msg"),
                    parent=self.winfo_toplevel(),
                )
            except tk.TclError:
                pass
            return
        self._read_epc_value = epc
        if epc.upper() == TAG_REFERENCE_EPC.upper():
            self.epc_status_label.config(
                text=t("calibration.epc_ok").format(epc=epc), foreground="green"
            )
            self._epc_confirmed = True
            self.btn_start_stop.config(state="normal")
        else:
            self.epc_status_label.config(
                text=t("calibration.epc_wrong").format(epc=epc), foreground="red"
            )
            self._epc_confirmed = False
            self.btn_start_stop.config(state="disabled")

    def _read_epc_and_rssi(self) -> Tuple[Optional[str], Optional[float]]:
        """Uso síncrono legado: uma sequência curta até a tag de referência (evitar na UI)."""
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            return (None, None)
        target = TAG_REFERENCE_EPC.upper()
        max_try = min(12, EPC_READ_TARGET_MAX_ATTEMPTS)
        try:
            limits = self._get_license_limits()
            power = float(limits.get("max_power", 25))
            freq_s = self.var_freq_min.get().strip()
            freq = float(freq_s) if freq_s else 800.0
            freq = max(limits.get("min_freq", 800), min(limits.get("max_freq", 1000), freq))

            port, pm = self._get_port_and_manager()
            acquired = False
            if pm:
                if not pm.acquire_port("CalibrationEPCRead", timeout=3.0):
                    return (None, None)
                acquired = True
            if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
                if pm and acquired:
                    try:
                        pm.release_port("CalibrationEPCRead")
                    except Exception:
                        pass
                return (None, None)
            try:
                out = ctypes.create_string_buffer(256)
                out_len = ctypes.c_uint(0)
                power_val = int(power * 100)
                power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, "big") * 2
                rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, out, ctypes.byref(out_len))
                freq_data = b"\x01" + int(freq * 1000).to_bytes(3, "big")
                rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, out, ctypes.byref(out_len))
                time.sleep(0.05)
                inv_data = bytes([0x00, 0x64])
                for _ in range(max_try):
                    res = rfid_sdk.UHF_RFID_Set(
                        RFID_CMD_INV_TAG, ctypes.c_char_p(inv_data), 2, out, ctypes.byref(out_len)
                    )
                    if res == 0 and out_len.value >= 14:
                        epc_hex = out.raw[2:14].hex().upper()
                        if epc_hex == target:
                            rssi_val = None
                            if out_len.value >= 16:
                                rssi_bytes = out.raw[out_len.value - 3 : out_len.value - 1]
                                rssi_raw = struct.unpack(">h", rssi_bytes)[0]
                                rssi_val = float(int(rssi_raw)) / 10.0
                            return (epc_hex, rssi_val)
                    time.sleep(0.03)
            finally:
                try:
                    rfid_sdk.UHF_RFID_Close(port)
                except Exception:
                    pass
                if pm and acquired:
                    try:
                        pm.release_port("CalibrationEPCRead")
                    except Exception:
                        pass
        except Exception as e:
            fc_log("calibration", f"_read_epc_and_rssi: {e}")
        return (None, None)

    def _apply_power_and_frequency(self, freq: float, power: float) -> bool:
        return _hw_apply_power_and_freq(self.com_port, freq, power)

    def _single_read_rssi(self, freq: float, power: float) -> Optional[float]:
        return _hw_single_read_rssi(self.com_port, freq, power, lambda: self._calibration_cancelled)

    def _find_threshold_and_rssi(
        self,
        freq: float,
        max_power: float,
        min_power: float,
        port: Optional[int] = None,
    ) -> Tuple[Optional[float], Optional[float]]:
        p = self.com_port if port is None else port
        return _hw_find_threshold_and_rssi(p, freq, max_power, min_power, lambda: self._calibration_cancelled)

    def _on_start_calibration(self):
        self._clear_requested = False
        freq_min, freq_max = self._apply_license_frequency_range()
        if not self._epc_confirmed:
            messagebox.showwarning(
                t('calibration.warning_epc_confirm_title'),
                t('calibration.warning_epc_confirm_msg'),
                parent=self.winfo_toplevel())
            return
        if not self.var_distance.get().strip():
            messagebox.showwarning(
                t('calibration.warning_params_title'),
                t('calibration.params_distance_fill'),
                parent=self.winfo_toplevel())
            return
        try:
            freq_step_s = self.var_freq_step.get().strip()
            freq_step = (
                int(float(freq_step_s))
                if freq_step_s in ("1", "5")
                else DEFAULT_FREQ_STEP_MHZ
            )
            if freq_step not in (1, 5):
                freq_step = DEFAULT_FREQ_STEP_MHZ
            distance = float(self.var_distance.get().replace(",", "."))
            attenuator = float(self.var_attenuator.get().replace(",", ".") or "0")
        except (ValueError, TypeError) as e:
            messagebox.showerror(
                t('threshold.error'),
                t('calibration.error_invalid_params_detail_msg').format(error=str(e)),
                parent=self.winfo_toplevel())
            return
        limits = self._get_license_limits()
        freq_min = int(round(float(limits.get("min_freq", freq_min))))
        freq_max = int(round(float(limits.get("max_freq", freq_max))))
        max_power = float(limits.get("max_power", 25))
        min_power = float(limits.get("min_power", 5))
        if freq_min >= freq_max:
            messagebox.showerror(
                t('threshold.error'),
                t('calibration.params_freq_order'),
                parent=self.winfo_toplevel())
            return

        freq_scan = _expand_calibration_frequency_list(freq_min, freq_max, freq_step, limits)
        raw_list = _raw_step_frequency_list(freq_min, freq_max, freq_step)
        skipped_by_license = [f for f in raw_list if f not in set(freq_scan)]
        if not freq_scan:
            messagebox.showerror(
                t("calibration.save_error_title"),
                t("calibration.no_freq_allowed"),
                parent=self.winfo_toplevel(),
            )
            return
        if skipped_by_license:
            messagebox.showwarning(
                t("calibration.freq_skipped_title"),
                t("calibration.freq_skipped_msg").format(
                    mhz=", ".join(str(x) for x in skipped_by_license),
                    last=max(freq_scan),
                ),
                parent=self.winfo_toplevel(),
            )

        # Verificação de segurança (temperatura e VSWR) igual ao Threshold/FastSurance
        temp_ok, vswr_ok = self._run_safety_check()
        if not temp_ok:
            messagebox.showerror(
                "Segurança",
                f"Temperatura igual ou acima do limite ({TEMPERATURE_LIMIT}°C). Calibração bloqueada por segurança do hardware.",
                parent=self.winfo_toplevel(),
            )
            return
        if not vswr_ok:
            messagebox.showerror(
                "Segurança",
                f"Potência refletida (VSWR) acima do limite ({REFLECTED_POWER_LIMIT} dBm). Verifique o sistema irradiante antes de calibrar.",
                parent=self.winfo_toplevel(),
            )
            return

        self._calibration_cancelled = False
        self.btn_start_stop.config(text=t("calibration.stop"), command=self._on_stop_calibration)
        self.btn_read_epc.config(state="disabled")
        self.progress_var.set(0)
        self.status_label.config(text=t("calibration.running"))
        self._corrections = []
        self._freqs = []
        self._forward_corrections = []
        self._reverse_corrections = []

        def run():
            try:
                self._run_calibration_worker(
                    freq_scan, freq_min, freq_max, freq_step, distance, attenuator, max_power, min_power
                )
            finally:
                self.after(0, self._calibration_finished)

        self._calibration_thread = threading.Thread(target=run, daemon=True)
        self._calibration_thread.start()

    def _run_calibration_worker(
        self,
        freqs: List[int],
        freq_min: int,
        freq_max: int,
        freq_step: int,
        distance: float,
        attenuator: float,
        max_power: float,
        min_power: float,
    ):
        if not freqs:
            self.after(0, lambda: self.status_label.config(text=t("calibration.failed")))
            return
        total = len(freqs)
        port, pm = self._get_port_and_manager()
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            self.after(0, lambda: self.status_label.config(text=t("calibration.hardware_unavailable")))
            return
        acquired = False
        if pm and not pm.acquire_port("Calibration", timeout=5.0):
            self.after(0, lambda: self.status_label.config(text=t("calibration.port_busy")))
            return
        if pm:
            acquired = True
        if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
            self.after(0, lambda: self.status_label.config(text=t("calibration.port_open_fail")))
            if pm and acquired:
                pm.release_port("Calibration")
            return
        try:
            for i, freq in enumerate(freqs):
                if self._calibration_cancelled:
                    break
                self.after(0, lambda i=i, t=total: self.progress_var.set(100.0 * (i + 1) / t))
                self.after(0, lambda f=freq: self.status_label.config(text=t("calibration.searching_freq").format(freq=f)))
                threshold_power, rssi = self._find_threshold_and_rssi(float(freq), max_power, min_power, port=port)
                if threshold_power is None or rssi is None:
                    fc_log("calibration", f"No measurement at {freq} MHz (tag/threshold or hardware).")
                    continue
                module_power = threshold_power
                fwd_meas, rev_meas = _power_on_tag_forward_reverse(module_power, rssi, attenuator, distance, float(freq))
                ref_fwd, ref_rev = get_reference_forward_reverse(float(freq))
                c_fwd = ref_fwd - fwd_meas
                c_rev = ref_rev - rev_meas
                self._corrections.append({
                    "freq_mhz": float(freq),
                    "correction_forward_dbm": round(c_fwd, 2),
                    "correction_reverse_dbm": round(c_rev, 2),
                })
                self._freqs.append(float(freq))
                self._forward_corrections.append(c_fwd)
                self._reverse_corrections.append(c_rev)
                self.after(0, self._refresh_graphs)

            allowed_1mhz = _expand_calibration_frequency_list(freq_min, freq_max, 1, self._get_license_limits())
            if allowed_1mhz:
                expanded = _interpolate_corrections_to_allowed_1mhz(self._corrections, allowed_1mhz)
                if expanded:
                    self._corrections = expanded
                    self._freqs = [float(p["freq_mhz"]) for p in expanded]
                    self._forward_corrections = [float(p["correction_forward_dbm"]) for p in expanded]
                    self._reverse_corrections = [float(p["correction_reverse_dbm"]) for p in expanded]
                    self.after(0, self._refresh_graphs)
            self._last_profile = build_profile(
                self._corrections,
                freq_min,
                freq_max,
                distance,
                attenuator,
                self.var_antenna.get().strip() or None,
                self.var_environment.get().strip() or None,
                TAG_REFERENCE_EPC,
                freq_step_mhz=freq_step,
            )
        finally:
            try:
                rfid_sdk.UHF_RFID_Close(port)
            except Exception:
                pass
            if pm and acquired:
                try:
                    pm.release_port("Calibration")
                except Exception:
                    pass

    def _refresh_graphs(self):
        if not MATPLOTLIB_OK or not self._freqs:
            return
        try:
            self.ax1.clear()
            self.ax1.set_title(t("calibration.graph_forward"))
            self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
            self.ax1.plot(self._freqs, self._forward_corrections, "b-o", markersize=4)
            self.ax1.grid(True)
            self.fig1.tight_layout()
            self.canvas1.draw()
            self.ax2.clear()
            self.ax2.set_title(t("calibration.graph_reverse"))
            self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
            self.ax2.plot(self._freqs, self._reverse_corrections, "r-o", markersize=4)
            self.ax2.grid(True)
            self.fig2.tight_layout()
            self._setup_graph_tooltips()
            self.canvas2.draw()
        except Exception as e:
            fc_log("calibration", f"_refresh_graphs: {e}")

    def _setup_graph_tooltips(self):
        """Configura tooltip de hover nos gráficos da calibração."""
        try:
            self.annot1 = self.ax1.annotate(
                "",
                xy=(0, 0),
                xytext=(12, 12),
                textcoords="offset points",
                bbox=dict(boxstyle="round,pad=0.35", fc="#FFFDE7", ec="#F57F17", alpha=0.95),
                arrowprops=dict(arrowstyle="->", color="#F57F17", alpha=0.8),
                annotation_clip=False,
            )
            self.annot1.set_clip_on(False)
            self.annot1.set_visible(False)

            self.annot2 = self.ax2.annotate(
                "",
                xy=(0, 0),
                xytext=(12, 12),
                textcoords="offset points",
                bbox=dict(boxstyle="round,pad=0.35", fc="#FFFDE7", ec="#F57F17", alpha=0.95),
                arrowprops=dict(arrowstyle="->", color="#F57F17", alpha=0.8),
                annotation_clip=False,
            )
            self.annot2.set_clip_on(False)
            self.annot2.set_visible(False)

            if not getattr(self, "_graph1_hover_cid", None):
                self._graph1_hover_cid = self.canvas1.mpl_connect("motion_notify_event", self._on_hover_graph1)
            if not getattr(self, "_graph2_hover_cid", None):
                self._graph2_hover_cid = self.canvas2.mpl_connect("motion_notify_event", self._on_hover_graph2)
        except Exception as e:
            fc_log("calibration", f"_setup_graph_tooltips: {e}")

    def _on_hover_graph1(self, event):
        self._handle_graph_hover(event, self.ax1, self.annot1, self.canvas1)

    def _on_hover_graph2(self, event):
        self._handle_graph_hover(event, self.ax2, self.annot2, self.canvas2)

    def _handle_graph_hover(self, event, ax, annot, canvas):
        """Mostra valor do ponto no hover, como nos outros módulos."""
        try:
            if event.inaxes != ax:
                if annot.get_visible():
                    annot.set_visible(False)
                    canvas.draw_idle()
                return

            for line in ax.lines:
                contains, index = line.contains(event)
                if not contains:
                    continue

                point_idx = index["ind"][0]
                x_coord = line.get_xdata()[point_idx]
                y_coord = line.get_ydata()[point_idx]

                # Reposiciona tooltip para evitar corte nas bordas do gráfico.
                x_offset = 12
                y_offset = 12
                text_ha = "left"
                text_va = "bottom"
                canvas_width, canvas_height = canvas.get_width_height()
                if event.x is not None and event.x > (canvas_width * 0.78):
                    x_offset = -12
                    text_ha = "right"
                if event.y is not None and event.y > (canvas_height * 0.78):
                    y_offset = -12
                    text_va = "top"

                annot.xy = (x_coord, y_coord)
                annot.xytext = (x_offset, y_offset)
                annot.set_ha(text_ha)
                annot.set_va(text_va)
                annot.set_text(f"{x_coord:.2f} MHz -> {y_coord:.2f} dBm")
                annot.set_visible(True)
                canvas.draw_idle()
                return

            if annot.get_visible():
                annot.set_visible(False)
                canvas.draw_idle()
        except Exception as e:
            fc_log("calibration", f"_handle_graph_hover: {e}")

    def _calibration_finished(self):
        if self._clear_requested:
            self.btn_start_stop.config(text=t("calibration.start"), command=self._on_start_calibration, state="disabled")
            self.btn_read_epc.config(state="normal")
            self.progress_var.set(0.0)
            self.status_label.config(text="")
            self._clear_requested = False
            return
        self.btn_start_stop.config(text=t("calibration.start"), command=self._on_start_calibration, state="normal")
        self.btn_read_epc.config(state="normal")
        if self._calibration_cancelled:
            self.status_label.config(text=t("calibration.cancelled"))
        elif self._corrections:
            self.status_label.config(text=t("calibration.done"))
            self._refresh_graphs()
        else:
            self.status_label.config(text=t("calibration.failed"))

    def _on_stop_calibration(self):
        self._calibration_cancelled = True

    def _on_clear(self):
        """Stop + clear: interrompe teste, limpa UI; min/max de freq. mantêm-se da licença."""
        self._clear_requested = True
        self._calibration_cancelled = True
        if getattr(self, "_epc_read_busy", False):
            self._read_epc_async_abort_hw()

        self._apply_license_frequency_range()
        self.var_freq_step.set(str(DEFAULT_FREQ_STEP_MHZ))
        self.var_distance.set("")
        self.var_attenuator.set("")
        self.var_antenna.set("")
        self.var_environment.set("")
        for entry in (
            self.entry_distance,
            self.entry_attenuator,
            self.entry_antenna,
            self.entry_environment,
        ):
            try:
                entry.delete(0, tk.END)
            except Exception:
                pass
        try:
            self.combo_freq_step.set(str(DEFAULT_FREQ_STEP_MHZ))
        except Exception:
            pass
        self.params_frame.update_idletasks()
        self.update_idletasks()
        # Dados de calibração e estado
        self._corrections = []
        self._freqs = []
        self._forward_corrections = []
        self._reverse_corrections = []
        self._last_profile = None
        clear_active_calibration()
        self.progress_var.set(0.0)
        self.status_label.config(text="")
        self._epc_confirmed = False
        self.epc_status_label.config(text=t("calibration.read_epc_hint"), foreground="gray")
        try:
            self.btn_read_epc.config(state="normal")
        except tk.TclError:
            pass
        self.btn_start_stop.config(text=t("calibration.start"), command=self._on_start_calibration, state="disabled")
        if hasattr(self, "safety_temperature_label"):
            self.safety_temperature_label.config(text="--°C", fg="gray")
        if hasattr(self, "safety_vswr_label"):
            self.safety_vswr_label.config(text="--", fg="gray")
        if MATPLOTLIB_OK:
            try:
                self.ax1.clear()
                self.ax1.set_title(t("calibration.graph_forward"))
                self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
                self.ax1.grid(True)
                self.fig1.tight_layout()
                self.canvas1.draw()
                self.ax2.clear()
                self.ax2.set_title(t("calibration.graph_reverse"))
                self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
                self.ax2.grid(True)
                self.fig2.tight_layout()
                self.canvas2.draw()
            except Exception as e:
                fc_log("calibration", f"_on_clear graphs: {e}")

        # Atualiza indicação de calibração ativa (nenhuma após limpar)
        try:
            self._update_active_calibration_label()
        except Exception:
            pass

    def _on_save(self):
        if not self._last_profile and not self._corrections:
            messagebox.showwarning(t("calibration.no_data_save_title"), t("calibration.no_data_save_msg"), parent=self.winfo_toplevel())
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[
                (t('export_popup.filetype_json'), "*.json"),
                (t('export_popup.filetype_all'), "*.*"),
            ],
            title=t("calibration.save_profile_title"),
        )
        if not path:
            return
        limits = self._get_license_limits()
        try:
            freq_min = int(round(float(limits.get("min_freq", 800))))
            freq_max = int(round(float(limits.get("max_freq", 1000))))
            freq_step = int(
                float(self.var_freq_step.get().strip() or str(DEFAULT_FREQ_STEP_MHZ))
            )
            if freq_step not in (1, 5):
                freq_step = DEFAULT_FREQ_STEP_MHZ
            distance = float((self.var_distance.get() or "0.5").replace(",", "."))
            attenuator = float((self.var_attenuator.get() or "0").replace(",", "."))
        except (ValueError, TypeError):
            messagebox.showerror(t("calibration.save_error_title"), t("calibration.fill_before_save"), parent=self.winfo_toplevel())
            return
        profile = build_profile(
            self._corrections,
            freq_min,
            freq_max,
            distance,
            attenuator,
            self.var_antenna.get().strip() or None,
            self.var_environment.get().strip() or None,
            TAG_REFERENCE_EPC,
            freq_step_mhz=freq_step,
        )
        # Nome da calibração = nome do arquivo sem extensão (ex.: "Calibration 1.json" -> "Calibration 1")
        profile["profile_name"] = os.path.splitext(os.path.basename(path))[0]
        profile["calibration_source_file"] = os.path.basename(path)
        if save_profile(profile, path):
            set_active_profile(profile)
            self.status_label.config(text=t("calibration.profile_saved").format(path=path))
            messagebox.showinfo(t("calibration.saved_ok_title"), t("calibration.saved_ok_msg"), parent=self.winfo_toplevel())
            try:
                self._update_active_calibration_label()
            except Exception:
                pass
        else:
            messagebox.showerror(t("calibration.save_error_title"), t("calibration.save_error_msg"), parent=self.winfo_toplevel())

    def _apply_loaded_profile(self, profile: Dict[str, Any], path: str, msg_title: str = None):
        """Aplica um perfil carregado/importado: atualiza dados, gráficos, campos do formulário e ativa o perfil."""
        if msg_title is None:
            msg_title = t("calibration.loaded_title")
        corrections = profile.get("corrections", [])
        self._corrections = list(corrections)
        self._freqs = [p["freq_mhz"] for p in corrections]
        self._forward_corrections = [p["correction_forward_dbm"] for p in corrections]
        self._reverse_corrections = [p["correction_reverse_dbm"] for p in corrections]
        self._last_profile = profile
        # Restaura os parâmetros de calibração nos campos do formulário
        self.var_freq_min.set(str(profile.get("freq_min_mhz", 800)))
        self.var_freq_max.set(str(profile.get("freq_max_mhz", 1000)))
        self.var_freq_step.set(str(profile.get("freq_step_mhz", DEFAULT_FREQ_STEP_MHZ)))
        self.var_distance.set(str(profile.get("distance_m", 0.5)))
        self.var_attenuator.set(str(profile.get("attenuator_db", 0)))
        self.var_antenna.set(profile.get("antenna_info") or "")
        self.var_environment.set(profile.get("environment_info") or "")
        # Nome da calibração = nome do arquivo sem extensão se não estiver no JSON (ex.: "Calibration 1.json" -> "Calibration 1")
        if not profile.get("profile_name") and path:
            profile["profile_name"] = os.path.splitext(os.path.basename(path))[0]
        if path:
            profile["calibration_source_file"] = os.path.basename(path)
        set_active_profile(profile)
        self._refresh_graphs()
        self.status_label.config(text=t("calibration.profile_saved").format(path=path))
        messagebox.showinfo(msg_title, t("calibration.profile_loaded_msg"), parent=self.winfo_toplevel())
        # Atualiza o label "Calibração: ..." para refletir a calibração agora ativa (importação de perfil)
        try:
            self._update_active_calibration_label()
        except Exception:
            pass

    def _on_import(self):
        path = filedialog.askopenfilename(
            filetypes=[
                (t('export_popup.filetype_json'), "*.json"),
                (t('export_popup.filetype_all'), "*.*"),
            ],
            title=t("calibration.import_profile_title"),
        )
        if not path:
            return
        profile = load_profile(path)
        if not profile:
            messagebox.showerror(t("calibration.save_error_title"), t("calibration.import_error"), parent=self.winfo_toplevel())
            return
        self._apply_loaded_profile(profile, path, t("calibration.import"))

    def _on_open_verification(self):
        """Abre a janela de Verificação da calibração."""
        win = tk.Toplevel(self.winfo_toplevel())
        win.title(t("calibration.verification_window_title"))
        win.geometry("1000x620")
        CalibrationVerificationPanel(win, com_port=self.com_port, app_shell=self.app_shell).pack(fill="both", expand=True)


class CalibrationVerificationPanel(tk.Frame):
    """
    Painel de verificação da calibração: 2 gráficos com curvas de referência (tabeladas)
    e curvas medidas corrigidas (sobreposição quando a calibração está correta).
    """

    def __init__(self, parent, app_shell=None, com_port: int = 4):
        super().__init__(parent)
        self.app_shell = app_shell
        self.com_port = com_port
        self._verify_cancelled = False
        self._verify_clear_requested = False
        self._verify_thread = None
        self._ref_freqs: List[float] = []
        self._ref_forward: List[float] = []
        self._ref_reverse: List[float] = []
        self._meas_freqs: List[float] = []
        self._meas_forward: List[float] = []
        self._meas_reverse: List[float] = []
        self._build_ui()
        self._update_observation_labels()
        self._plot_reference_curves()
        def _on_focus(_evt):
            self._update_observation_labels()
            self._update_verify_button_state()
        self.bind("<FocusIn>", _on_focus)
        self.after(500, self._refresh_safety_display)

    def _refresh_safety_display(self):
        """Atualiza os valores de temperatura e VSWR na janela (sem bloquear a verificação)."""
        try:
            self._run_safety_check()
        except Exception as e:
            fc_log("calibration", f"Verification _refresh_safety_display: {e}")

    def _get_license_limits(self) -> dict:
        return _resolve_active_license_limits(self.app_shell)

    def _get_port_and_manager(self):
        """Retorna (port_number, port_manager). Usa port_manager.com_port quando disponível."""
        pm = getattr(self.app_shell, "port_manager", None) if self.app_shell else None
        if pm is None and get_port_manager:
            pm = get_port_manager()
        port = getattr(pm, "com_port", None) if pm else None
        if port is None:
            port = self.com_port
        return (port, pm)

    def _run_safety_check(self) -> Tuple[Optional[bool], Optional[bool]]:
        """
        Verifica temperatura e VSWR antes da verificação. Atualiza labels.
        Retorna (temp_ok, vswr_ok). (None, None) = falha ao abrir porta.
        """
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            self.safety_temperature_label.config(text="--°C", bg="#F5F5F5")
            self.safety_vswr_label.config(text="--", bg="#F5F5F5")
            return (True, True)
        port, pm = self._get_port_and_manager()
        acquired = False
        if pm:
            if not pm.acquire_port("CalibrationVerification", timeout=3.0):
                self.safety_temperature_label.config(text="--°C", bg="#F5F5F5")
                self.safety_vswr_label.config(text="--", bg="#F5F5F5")
                return (None, None)
            acquired = True
        try:
            if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
                self.safety_temperature_label.config(text="--°C", bg="#F5F5F5")
                self.safety_vswr_label.config(text="--", bg="#F5F5F5")
                return (None, None)
            temp = _hw_read_temperature(port)
            reflected = _hw_read_reflected_power(port)
            if temp is not None:
                self.safety_temperature_label.config(text=f"{temp:.1f}°C", bg="#F5F5F5")
                if temp >= TEMPERATURE_LIMIT:
                    self.safety_temperature_label.config(foreground="red")
                elif temp >= 45:
                    self.safety_temperature_label.config(foreground="orange")
                else:
                    self.safety_temperature_label.config(foreground="green")
            else:
                self.safety_temperature_label.config(text="--°C", foreground="gray", bg="#F5F5F5")
            if reflected is not None:
                vswr_ratio = _calc_vswr_from_reflected_power(HEATING_POWER_DBM, reflected)
                self.safety_vswr_label.config(text=f"{vswr_ratio:.2f}", bg="#F5F5F5")
                if reflected > REFLECTED_POWER_LIMIT:
                    self.safety_vswr_label.config(foreground="red")
                else:
                    self.safety_vswr_label.config(foreground="green")
            else:
                self.safety_vswr_label.config(text="--", foreground="gray", bg="#F5F5F5")
            temp_ok = temp is None or temp < TEMPERATURE_LIMIT
            vswr_ok = reflected is None or reflected <= REFLECTED_POWER_LIMIT
            return (temp_ok, vswr_ok)
        finally:
            try:
                rfid_sdk.UHF_RFID_Close(port)
            except Exception:
                pass
            if pm and acquired:
                pm.release_port("CalibrationVerification")

    def _build_ui(self):
        self.config(bg="#F5F5F5")
        obs_frame = ttk.LabelFrame(self, text="Observação", padding=8)
        obs_frame.pack(fill="x", padx=10, pady=5)
        self.obs_label_intro = ttk.Label(obs_frame, text="", wraplength=700)
        self.obs_label_intro.pack(anchor="w")
        self.obs_label_name = ttk.Label(obs_frame, text="", wraplength=700)
        self.obs_label_name.pack(anchor="w", pady=(2, 0))
        self.obs_label_conditions = ttk.Label(obs_frame, text="", wraplength=700)
        self.obs_label_conditions.pack(anchor="w", pady=(2, 0))
        safety_frame = ttk.LabelFrame(self, text=t("threshold.safety_system"), padding=8)
        safety_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(safety_frame, text=t("threshold.temperature")).grid(row=0, column=0, sticky="w", padx=(0, 4))
        self.safety_temperature_label = tk.Label(
            safety_frame, text="--°C", fg="gray", font=("Helvetica", 10, "bold"), bg="#F5F5F5"
        )
        self.safety_temperature_label.grid(row=0, column=1, padx=(0, 16))
        ttk.Label(safety_frame, text=t("threshold.vswr")).grid(row=0, column=2, sticky="w", padx=(0, 4))
        self.safety_vswr_label = tk.Label(
            safety_frame, text="--", fg="gray", font=("Helvetica", 10, "bold"), bg="#F5F5F5"
        )
        self.safety_vswr_label.grid(row=0, column=3, sticky="w")
        params_frame = ttk.LabelFrame(self, text=t("calibration.verification_freq_frame"), padding=8)
        params_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(params_frame, text=t("calibration.freq_min")).grid(row=0, column=0, sticky="w", padx=(0, 4))
        self.var_freq_min = tk.StringVar(value="")
        self.entry_freq_min = ttk.Entry(params_frame, textvariable=self.var_freq_min, width=8, state="readonly")
        self.entry_freq_min.grid(row=0, column=1, padx=(0, 12))
        ttk.Label(params_frame, text=t("calibration.freq_max")).grid(row=0, column=2, sticky="w", padx=(0, 4))
        self.var_freq_max = tk.StringVar(value="")
        self.entry_freq_max = ttk.Entry(params_frame, textvariable=self.var_freq_max, width=8, state="readonly")
        self.entry_freq_max.grid(row=0, column=3, padx=(0, 12))
        ttk.Label(params_frame, text=t("calibration.step_freq")).grid(row=0, column=4, sticky="w", padx=(12, 4))
        self.var_freq_step = tk.StringVar(value=str(DEFAULT_FREQ_STEP_MHZ))
        self.combo_freq_step = ttk.Combobox(params_frame, textvariable=self.var_freq_step, values=["1", "5"], width=6, state="disabled")
        self.combo_freq_step.grid(row=0, column=5, padx=(0, 12))
        self.status_label = ttk.Label(self, text=t("calibration.status_place_tag"))
        self.status_label.pack(anchor="w", padx=10, pady=2)
        btn_frame = ttk.Frame(self)
        btn_frame.pack(fill="x", padx=10, pady=4)
        self.btn_verify = ttk.Button(btn_frame, text=t("calibration.btn_start_verify"), command=self._on_verify)
        self.btn_verify.pack(side="left", padx=2)
        self.btn_stop = ttk.Button(btn_frame, text=t("calibration.stop"), command=self._on_stop_verify, state="disabled")
        self.btn_stop.pack(side="left", padx=2)
        self.btn_clear = ttk.Button(btn_frame, text=t("calibration.clear"), command=self._on_clear_verification)
        self.btn_clear.pack(side="left", padx=2)
        self._update_verify_button_state()
        self.progress_var = tk.DoubleVar(value=0.0)
        self.progress = ttk.Progressbar(self, variable=self.progress_var, maximum=100)
        self.progress.pack(fill="x", padx=10, pady=4)
        if MATPLOTLIB_OK:
            fig_frame = ttk.Frame(self)
            fig_frame.pack(fill="both", expand=True, padx=10, pady=5)
            self.fig1 = Figure(figsize=(5, 2.5), dpi=100, facecolor="#F5F5F5")
            self.ax1 = self.fig1.add_subplot(111)
            self.ax1.set_title(t("calibration.graph_forward_ref"))
            self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
            self._apply_verification_y_subgrid(self.ax1)
            self.canvas1 = FigureCanvasTkAgg(self.fig1, master=fig_frame)
            self.canvas1.get_tk_widget().pack(side="left", fill="both", expand=True)
            self.fig2 = Figure(figsize=(5, 2.5), dpi=100, facecolor="#F5F5F5")
            self.ax2 = self.fig2.add_subplot(111)
            self.ax2.set_title(t("calibration.graph_reverse_ref"))
            self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
            self._apply_verification_y_subgrid(self.ax2)
            self.canvas2 = FigureCanvasTkAgg(self.fig2, master=fig_frame)
            self.canvas2.get_tk_widget().pack(side="left", fill="both", expand=True)
        else:
            ttk.Label(self, text=t("calibration.matplotlib_unavailable_short")).pack(pady=10)
        self._fill_freq_from_profile()

    def _apply_verification_y_subgrid(self, ax):
        """Aplica subgrade no eixo Y com passo de 0.5 dBm sem rótulos extras."""
        ax.yaxis.set_minor_locator(MultipleLocator(0.5))
        ax.tick_params(axis="y", which="minor", labelleft=False)
        ax.grid(True, which="major", linestyle="-", linewidth=0.8, alpha=0.35)
        ax.grid(True, which="minor", axis="y", linestyle=":", linewidth=0.6, alpha=0.45)

    def _update_observation_labels(self):
        """Atualiza os rótulos de observação: calibração ativa salva e nome/identificação da calibração."""
        if not has_active_calibration():
            self.obs_label_intro.config(text=t("calibration.obs_no_active"))
            self.obs_label_name.config(text=t("calibration.obs_verification_name").format(ident="—"))
            self.obs_label_conditions.config(text="")
            return
        profile = get_active_profile()
        if not profile:
            self.obs_label_intro.config(text=t("calibration.obs_verification_uses"))
            self.obs_label_name.config(text=t("calibration.obs_verification_name").format(ident="—"))
            self.obs_label_conditions.config(text="")
            return
        self.obs_label_intro.config(text=t("calibration.obs_verification_intro"))
        name = profile.get("profile_name") or profile.get("name") or ""
        if name:
            ident = name
        else:
            tag_epc = profile.get("tag_epc", "")
            freq_min = profile.get("freq_min_mhz", "")
            freq_max = profile.get("freq_max_mhz", "")
            step = profile.get("freq_step_mhz", "")
            created = profile.get("created_utc", "")
            if created:
                try:
                    created = created.replace("Z", "").split("T")[0]
                except Exception:
                    pass
            ident = t("calibration.profile_ident").format(tag_epc=tag_epc, freq_min=freq_min, freq_max=freq_max, step=step)
            if created:
                ident += f" | {created}"
        self.obs_label_name.config(text=t("calibration.obs_verification_name").format(ident=ident))
        dist = profile.get("distance_m", "")
        att = profile.get("attenuator_db", "")
        ant = (profile.get("antenna_info") or "").strip()
        env = (profile.get("environment_info") or "").strip()
        parts = [f"Distância: {dist} m", f"Atenuador: {att} dB"]
        if ant:
            parts.append(f"Antena: {ant}")
        if env:
            parts.append(f"Ambiente: {env}")
        self.obs_label_conditions.config(
            text=t("calibration.obs_conditions").format(parts="; ".join(parts))
        )

    def _on_clear_verification(self):
        """Stop + clear: interrompe verificação, limpa curvas; frequências voltam ao perfil ativo."""
        self._verify_clear_requested = True
        self._verify_cancelled = True
        self._meas_freqs = []
        self._meas_forward = []
        self._meas_reverse = []
        self._fill_freq_from_profile()
        self.progress_var.set(0.0)
        if hasattr(self, "safety_temperature_label"):
            self.safety_temperature_label.config(text="--°C", fg="gray")
        if hasattr(self, "safety_vswr_label"):
            self.safety_vswr_label.config(text="--", fg="gray")
        self._update_verify_button_state()
        self.status_label.config(text=t("calibration.verification_cleared"))
        self._refresh_graphs()

    def _fill_freq_from_profile(self):
        """Preenche freq mín/máx/step com os valores do perfil ativo, se houver."""
        if not has_active_calibration():
            self.var_freq_min.set("")
            self.var_freq_max.set("")
            self.var_freq_step.set("")
            return
        profile = get_active_profile()
        if not profile:
            self.var_freq_min.set("")
            self.var_freq_max.set("")
            self.var_freq_step.set("")
            return
        self.var_freq_min.set(str(profile.get("freq_min_mhz", "")))
        self.var_freq_max.set(str(profile.get("freq_max_mhz", "")))
        self.var_freq_step.set(str(profile.get("freq_step_mhz", "")))

    def _plot_reference_curves(self):
        """Plota as curvas de referência (tabeladas) nos dois gráficos."""
        table = get_reference_table()
        if not table:
            return
        self._ref_freqs = [float(p[0]) for p in table]
        self._ref_forward = [p[1] for p in table]
        self._ref_reverse = [p[2] for p in table]
        self._refresh_graphs()

    def _refresh_graphs(self):
        if not MATPLOTLIB_OK:
            return
        try:
            self.ax1.clear()
            self.ax1.set_title(t("calibration.graph_forward_ref"))
            self.ax1.set_xlabel(t("calibration.freq_mhz_axis"))
            self._apply_verification_y_subgrid(self.ax1)
            if self._ref_freqs:
                self.ax1.plot(self._ref_freqs, self._ref_forward, "b-", label=t("calibration.label_reference"), linewidth=1.5)
            if self._meas_freqs and self._meas_forward:
                self.ax1.plot(self._meas_freqs, self._meas_forward, "g-o", label=t("calibration.label_measured"), markersize=4)
            if self._ref_freqs or self._meas_freqs:
                self.ax1.legend(loc="best", fontsize=8)
            self.fig1.tight_layout()
            self.canvas1.draw()
            self.ax2.clear()
            self.ax2.set_title(t("calibration.graph_reverse_ref"))
            self.ax2.set_xlabel(t("calibration.freq_mhz_axis"))
            self._apply_verification_y_subgrid(self.ax2)
            if self._ref_freqs:
                self.ax2.plot(self._ref_freqs, self._ref_reverse, "b-", label=t("calibration.label_reference"), linewidth=1.5)
            if self._meas_freqs and self._meas_reverse:
                self.ax2.plot(self._meas_freqs, self._meas_reverse, "r-o", label=t("calibration.label_measured"), markersize=4)
            if self._ref_freqs or self._meas_freqs:
                self.ax2.legend(loc="best", fontsize=8)
            self.fig2.tight_layout()
            self.canvas2.draw()
        except Exception as e:
            fc_log("calibration", f"Verification _refresh_graphs: {e}")

    def _update_verify_button_state(self):
        if has_active_calibration():
            self._fill_freq_from_profile()
            self.btn_verify.config(state="normal")
            self.status_label.config(text=t("calibration.status_place_tag"))
        else:
            self.btn_verify.config(state="disabled")
            self.status_label.config(text=t("calibration.obs_no_active"))

    def _on_verify(self):
        self._verify_clear_requested = False
        profile = get_active_profile()
        if not has_active_calibration() or not profile:
            messagebox.showwarning(
                t("calibration.no_active_title"),
                t("calibration.no_active_msg"),
                parent=self.winfo_toplevel(),
            )
            self._update_verify_button_state()
            return
        self._fill_freq_from_profile()
        limits = self._get_license_limits()
        try:
            freq_min = int(float(profile.get("freq_min_mhz", "")))
            freq_max = int(float(profile.get("freq_max_mhz", "")))
            freq_step = int(float(profile.get("freq_step_mhz", DEFAULT_FREQ_STEP_MHZ)))
            if freq_step not in (1, 5):
                freq_step = DEFAULT_FREQ_STEP_MHZ
        except (ValueError, TypeError):
            messagebox.showerror(t("calibration.save_error_title"), t("calibration.params_invalid"), parent=self.winfo_toplevel())
            return
        freq_min = max(limits.get("min_freq", 800), min(limits.get("max_freq", 1000), freq_min))
        freq_max = max(limits.get("min_freq", 800), min(limits.get("max_freq", 1000), freq_max))
        distance = float(profile.get("distance_m", 0.5))
        attenuator = float(profile.get("attenuator_db", 0))
        max_power = float(limits.get("max_power", 25))
        min_power = float(limits.get("min_power", 5))
        if freq_min >= freq_max:
            messagebox.showerror(t("calibration.save_error_title"), t("calibration.params_freq_order"), parent=self.winfo_toplevel())
            return
        temp_ok, vswr_ok = self._run_safety_check()
        self.update_idletasks()
        if temp_ok is None and vswr_ok is None:
            messagebox.showerror(
                t("threshold.safety_system"),
                t("calibration.safety_com_fail"),
                parent=self.winfo_toplevel(),
            )
            return
        if not temp_ok:
            messagebox.showerror(
                t("threshold.safety_system"),
                t("calibration.safety_temp").format(limit=TEMPERATURE_LIMIT),
                parent=self.winfo_toplevel(),
            )
            return
        if not vswr_ok:
            messagebox.showerror(
                t("threshold.safety_system"),
                t("calibration.safety_vswr").format(limit=REFLECTED_POWER_LIMIT),
                parent=self.winfo_toplevel(),
            )
            return
        freq_scan = _expand_calibration_frequency_list(freq_min, freq_max, freq_step, limits)
        if not freq_scan:
            messagebox.showerror(
                t("calibration.save_error_title"),
                t("calibration.no_freq_allowed"),
                parent=self.winfo_toplevel(),
            )
            return

        self._verify_cancelled = False
        self._meas_freqs = []
        self._meas_forward = []
        self._meas_reverse = []
        self.btn_verify.config(state="disabled")
        self.btn_stop.config(state="normal")
        self.progress_var.set(0)
        self.status_label.config(text=t("calibration.verification_running"))

        def run():
            try:
                self._run_verify_worker(freq_scan, distance, attenuator, max_power, min_power)
            finally:
                self.after(0, self._verify_finished)

        self._verify_thread = threading.Thread(target=run, daemon=True)
        self._verify_thread.start()

    def _run_verify_worker(
        self,
        freqs: List[int],
        distance: float,
        attenuator: float,
        max_power: float,
        min_power: float,
    ):
        if not freqs:
            self.after(0, lambda: self.status_label.config(text=t("calibration.failed")))
            return
        total = len(freqs)
        port, pm = self._get_port_and_manager()
        if not HARDWARE_AVAILABLE or rfid_sdk is None:
            self.after(0, lambda: self.status_label.config(text=t("calibration.hardware_unavailable")))
            return
        acquired = False
        if pm and not pm.acquire_port("CalibrationVerification", timeout=5.0):
            self.after(0, lambda: self.status_label.config(text=t("calibration.port_busy")))
            return
        if pm:
            acquired = True
        if rfid_sdk.UHF_RFID_Open(port, BAUD_RATE) != 0:
            self.after(0, lambda: self.status_label.config(text=t("calibration.port_open_fail")))
            if pm and acquired:
                pm.release_port("CalibrationVerification")
            return
        get_cancelled = lambda: self._verify_cancelled
        try:
            for i, freq in enumerate(freqs):
                if self._verify_cancelled:
                    break
                self.after(0, lambda i=i, t=total: self.progress_var.set(100.0 * (i + 1) / t))
                self.after(0, lambda f=freq: self.status_label.config(text=t("calibration.verifying_freq").format(freq=f)))
                threshold_power, rssi = _hw_find_threshold_and_rssi(port, float(freq), max_power, min_power, get_cancelled)
                if threshold_power is None or rssi is None:
                    fc_log("calibration", f"Verification: no measurement at {freq} MHz (tag/threshold or hardware).")
                    continue
                full = apply_corrections_to_result(
                    {
                        "freq_mhz": float(freq),
                        "threshold_power": float(threshold_power),
                        "threshold_rssi": float(rssi),
                        "module_power": float(threshold_power),
                        "rssi": float(rssi),
                    },
                    float(attenuator),
                    float(distance),
                    apply_profile_calibration=True,
                )
                fwd_corrected = full["power_on_tag_forward"]
                rev_corrected = full["power_on_tag_reversed"]
                self._meas_freqs.append(float(freq))
                self._meas_forward.append(fwd_corrected)
                self._meas_reverse.append(rev_corrected)
                self.after(0, self._refresh_graphs)
            self.after(0, lambda: self.status_label.config(text=t("calibration.verification_done")))
        finally:
            try:
                rfid_sdk.UHF_RFID_Close(port)
            except Exception:
                pass
            if pm and acquired:
                try:
                    pm.release_port("CalibrationVerification")
                except Exception:
                    pass

    def _verify_finished(self):
        if self._verify_clear_requested:
            self.btn_stop.config(state="disabled")
            self.progress_var.set(0.0)
            self._verify_clear_requested = False
            self._update_verify_button_state()
            return
        self.btn_verify.config(state="normal")
        self.btn_stop.config(state="disabled")
        self._update_verify_button_state()

    def _on_stop_verify(self):
        self._verify_cancelled = True
