import json
import os
import sys

from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout,
    QPushButton, QLineEdit, QSpinBox, QDoubleSpinBox,
    QLabel, QMessageBox, QApplication, QFileDialog,
    QDialog, QTabWidget, QFrame, QListWidget, QListWidgetItem,
)

from camera.insight import InSightCamera
from camera.basler import BaslerCamera
from ai.detector import Detector
from db.sql_client import SQLClient
from plc.plc_client import PLCClient
from paths import resolve_path, to_project_relative
from utils.path_helper import get_path
from utils.touch_keyboard import show_touch_keyboard, hide_touch_keyboard
from logger import log_action

ADMIN_PASSWORD = "1234"

# ── 카드 스타일 ──────────────────────────────────────────────────────── #
_CARD = (
    "QFrame {"
    "  background:#1a1a1a;"
    "  border:1px solid #2a2a2a;"
    "  border-radius:8px;"
    "}"
)
_STATUS_OK   = "color:#1D9E75; font-size:12px; background:transparent; border:none;"
_STATUS_FAIL = "color:#E24B4A; font-size:12px; background:transparent; border:none;"

_BTN_CARD_CONNECT = (
    "QPushButton {"
    "  background:#1D9E75; color:#E1F5EE;"
    "  border:none; border-radius:4px;"
    "  min-height:43px; max-height:43px;"
    "  padding:0 19px; font-size:14px;"
    "}"
    "QPushButton:hover { background:#20b585; }"
    "QPushButton:disabled { background:#145f48; color:#5a9e7e; }"
)
_BTN_CARD_DISCONNECT = (
    "QPushButton {"
    "  background:#3D1515; color:#F09595;"
    "  border:none; border-radius:4px;"
    "  min-height:43px; max-height:43px;"
    "  padding:0 19px; font-size:14px;"
    "}"
    "QPushButton:hover { background:#4a1818; }"
    "QPushButton:disabled { background:#222222; color:#666666; }"
)
_BTN_ADMIN = (
    "QPushButton {"
    "  background:#333333; color:#aaaaaa;"
    "  border:1px solid #555555; border-radius:4px;"
    "  font-size:13px; padding:0 16px;"
    "}"
    "QPushButton:hover { background:#444444; color:#ffffff; }"
)

# ── 관리자 다이얼로그 내부 스타일 ────────────────────────────────────── #
_GRP = (
    "QGroupBox {"
    "  background:#222222; border:1px solid #333333; border-radius:6px;"
    "  margin-top:14px; padding:14px 12px 12px 12px;"
    "}"
    "QGroupBox::title { color:#aaaaaa; subcontrol-origin:margin; left:10px; padding:0 4px; }"
)
_BTN_DLG = (
    "QPushButton {"
    "  background:#333333; color:#ffffff; border:1px solid #555555;"
    "  border-radius:4px; min-height:56px; font-size:14px;"
    "}"
    "QPushButton:hover { background:#444444; }"
)
_BTN_DLG_PRIMARY = (
    "QPushButton {"
    "  background:#1D9E75; color:#ffffff; border:none;"
    "  border-radius:4px; min-height:56px; font-size:14px; font-weight:bold;"
    "}"
    "QPushButton:hover { background:#20b585; }"
)


# ══════════════════════════════════════════════════════════════════════ #
# PasswordDialog
# ══════════════════════════════════════════════════════════════════════ #

class PasswordDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("관리자 인증")
        self.setModal(True)
        self.setFixedSize(360, 570)
        self.setStyleSheet("background:#1a1a1a;")
        self._pw = ""
        self._locked = False   # 오입력 후 잠시 입력 차단
        self._build_ui()

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

    def _build_ui(self):
        layout = QVBoxLayout(self)
        layout.setContentsMargins(24, 28, 24, 24)
        layout.setSpacing(12)

        title = QLabel("관리자 인증")
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet("color:#ffffff; font-size:17px; font-weight:bold;")
        layout.addWidget(title)

        desc = QLabel("비밀번호를 입력하세요.")
        desc.setAlignment(Qt.AlignCenter)
        desc.setStyleSheet("color:#888888; font-size:13px;")
        layout.addWidget(desc)

        # 4자리 점 표시
        self._dot_lbl = QLabel("○   ○   ○   ○")
        self._dot_lbl.setAlignment(Qt.AlignCenter)
        self._dot_lbl.setStyleSheet(
            "color:#444444; font-size:30px; background:transparent;"
        )
        layout.addWidget(self._dot_lbl)
        layout.addSpacing(6)

        # 키패드
        pad = QGridLayout()
        pad.setSpacing(8)
        pad.setContentsMargins(0, 0, 0, 0)

        for idx, digit in enumerate("123456789"):
            btn = self._pad_btn(digit, font_size=22)
            btn.clicked.connect(lambda _, d=digit: self._on_digit(d))
            pad.addWidget(btn, idx // 3, idx % 3)

        btn_del = self._pad_btn("⌫", bg="#252525", fg="#888888", font_size=20)
        btn_del.clicked.connect(self._on_backspace)

        btn_0 = self._pad_btn("0", font_size=22)
        btn_0.clicked.connect(lambda: self._on_digit("0"))

        btn_ok = self._pad_btn("확인", bg="#1D9E75", fg="#E1F5EE", font_size=16)
        btn_ok.clicked.connect(self._on_confirm)

        pad.addWidget(btn_del, 3, 0)
        pad.addWidget(btn_0,   3, 1)
        pad.addWidget(btn_ok,  3, 2)

        layout.addLayout(pad)
        layout.addSpacing(4)

        btn_cancel = QPushButton("취소")
        btn_cancel.setFixedHeight(52)
        btn_cancel.setStyleSheet(
            "QPushButton { background:#222222; color:#777777; border:none;"
            "  border-radius:6px; font-size:15px; }"
            "QPushButton:pressed { background:#1a1a1a; }"
        )
        btn_cancel.clicked.connect(self.reject)
        layout.addWidget(btn_cancel)

    @staticmethod
    def _pad_btn(text: str, bg="#2a2a2a", fg="#ffffff", font_size=22) -> QPushButton:
        btn = QPushButton(text)
        btn.setFixedHeight(68)
        btn.setStyleSheet(
            f"QPushButton {{ background:{bg}; color:{fg}; border:none;"
            f"  border-radius:6px; font-size:{font_size}px; }}"
            f"QPushButton:pressed {{ background:#111111; }}"
        )
        return btn

    # ── 입력 로직 ────────────────────────────────────────────────────── #

    def _on_digit(self, digit: str):
        if self._locked or len(self._pw) >= 4:
            return
        self._pw += digit
        self._refresh_dots()
        if len(self._pw) == 4:
            # 4자리 완성 → 120ms 후 자동 확인 (시각 피드백 확보)
            QTimer.singleShot(120, self._on_confirm)

    def _on_backspace(self):
        if self._locked:
            return
        self._pw = self._pw[:-1]
        self._refresh_dots()

    def _on_confirm(self):
        if self._pw == ADMIN_PASSWORD:
            self.accept()
            return
        # 오입력: 빨간 점 → 600ms 후 초기화
        self._locked = True
        self._pw = ""
        self._dot_lbl.setText("●   ●   ●   ●")
        self._dot_lbl.setStyleSheet(
            "color:#E24B4A; font-size:30px; background:transparent;"
        )
        QTimer.singleShot(600, self._reset_dots)

    def _reset_dots(self):
        self._locked = False
        self._refresh_dots()

    def _refresh_dots(self):
        n = len(self._pw)
        dots = "   ".join("●" if i < n else "○" for i in range(4))
        color = "#1D9E75" if n > 0 else "#444444"
        self._dot_lbl.setText(dots)
        self._dot_lbl.setStyleSheet(
            f"color:{color}; font-size:30px; background:transparent;"
        )

    def keyPressEvent(self, event):
        # 터치 전용 — 키보드 입력 차단 (Escape 제외)
        if event.key() == Qt.Key_Escape:
            self.reject()


# ══════════════════════════════════════════════════════════════════════ #
# AdminSettingsDialog
# ══════════════════════════════════════════════════════════════════════ #

class _TouchCheckList(QListWidget):
    """행 전체 탭으로 체크 토글 (터치 화면용)."""

    def mousePressEvent(self, event):
        item = self.itemAt(event.pos())
        if item is not None:
            item.setCheckState(
                Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
            )
            return
        super().mousePressEvent(event)


class AdminSettingsDialog(QDialog):
    def __init__(self, settings_page, parent=None):
        super().__init__(parent)
        self._sp = settings_page
        self.setWindowTitle("관리자 설정")
        self.setModal(True)
        self.setFixedSize(900, 700)
        self.setStyleSheet("background:#1a1a1a;")
        self._build_ui()
        self._populate()

    # ── UI 뼈대 ─────────────────────────────────────────────────────── #

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(0, 0, 0, 0)
        root.setSpacing(0)

        self._tabs = QTabWidget()
        self._tabs.setStyleSheet(
            "QTabWidget::pane { border:none; background:#1a1a1a; }"
            "QTabBar::tab {"
            "  background:#222222; color:#888888;"
            "  padding:10px 24px; font-size:14px; border:none;"
            "  border-right:1px solid #333333;"
            "}"
            "QTabBar::tab:selected { background:#2e2e2e; color:#ffffff; font-weight:bold; }"
            "QTabBar::tab:hover    { background:#2a2a2a; color:#cccccc; }"
        )

        self._tabs.addTab(self._build_tab_cognex(),   "코그넥스")
        self._tabs.addTab(self._build_tab_basler(),   "Basler")
        self._tabs.addTab(self._build_tab_db(),       "DB")
        self._tabs.addTab(self._build_tab_mes(),      "MES 제품")
        self._tabs.addTab(self._build_tab_ai(),       "AI 모델")
        self._tabs.addTab(self._build_tab_conveyor(), "컨베이어")
        self._tabs.addTab(self._build_tab_plc(),      "PLC")

        root.addWidget(self._tabs, stretch=1)
        root.addWidget(self._build_bottom_bar())

    @staticmethod
    def _tab_wrap(inner_widget) -> QWidget:
        w = QWidget()
        w.setStyleSheet("background:#1a1a1a;")
        layout = QVBoxLayout(w)
        layout.setContentsMargins(32, 24, 32, 20)
        layout.setSpacing(16)
        layout.addWidget(inner_widget)
        layout.addStretch()
        return w

    @staticmethod
    def _btn_pair(*btns) -> QWidget:
        w = QWidget()
        row = QHBoxLayout(w)
        row.setContentsMargins(0, 0, 0, 0)
        row.setSpacing(8)
        for b in btns:
            row.addWidget(b, stretch=1)
        return w

    # ── 탭 1 — 코그넥스 ─────────────────────────────────────────────── #

    def _build_tab_cognex(self) -> QWidget:
        g = self._make_group("코그넥스 In-Sight 2000C")
        form = QFormLayout(g)
        form.setHorizontalSpacing(16)
        form.setVerticalSpacing(12)

        self._cognex_ip = QLineEdit()
        self._cognex_ip.setPlaceholderText("예: 169.254.0.1")
        self._cognex_ip.setFixedHeight(42)

        self._cognex_port = QSpinBox()
        self._cognex_port.setRange(1, 65535)
        self._cognex_port.setFixedHeight(42)

        btn_connect = QPushButton("연결")
        btn_connect.setFixedHeight(56)
        btn_connect.setStyleSheet(_BTN_DLG)
        btn_connect.clicked.connect(self._on_cognex_connect)

        btn_save = QPushButton("저장")
        btn_save.setFixedHeight(56)
        btn_save.setStyleSheet(_BTN_DLG)
        btn_save.clicked.connect(self._on_cognex_save)

        form.addRow("IP 주소", self._cognex_ip)
        form.addRow("포트",    self._cognex_port)
        form.addRow("",        self._btn_pair(btn_connect, btn_save))
        return self._tab_wrap(g)

    # ── 탭 2 — Basler ───────────────────────────────────────────────── #

    def _build_tab_basler(self) -> QWidget:
        g = self._make_group("Basler USB 카메라")
        form = QFormLayout(g)
        form.setHorizontalSpacing(16)
        form.setVerticalSpacing(12)

        self._basler_exposure = QSpinBox()
        self._basler_exposure.setRange(100, 1000000)
        self._basler_exposure.setFixedHeight(42)

        self._basler_gain = QSpinBox()
        self._basler_gain.setRange(0, 100)
        self._basler_gain.setFixedHeight(42)

        btn_connect = QPushButton("연결")
        btn_connect.setFixedHeight(56)
        btn_connect.setStyleSheet(_BTN_DLG)
        btn_connect.clicked.connect(self._sp._on_basler_connect)

        btn_apply = QPushButton("설정 적용")
        btn_apply.setFixedHeight(56)
        btn_apply.setStyleSheet(_BTN_DLG)
        btn_apply.clicked.connect(self._on_basler_apply)

        form.addRow("노출 (µs)", self._basler_exposure)
        form.addRow("게인",      self._basler_gain)
        form.addRow("",          self._btn_pair(btn_connect, btn_apply))
        return self._tab_wrap(g)

    # ── 탭 3 — DB ───────────────────────────────────────────────────── #

    def _build_tab_db(self) -> QWidget:
        g = self._make_group("MS SQL Server DB")
        form = QFormLayout(g)
        form.setHorizontalSpacing(16)
        form.setVerticalSpacing(12)

        self._db_server = QLineEdit()
        self._db_server.setPlaceholderText("예: Wizis.iptime.org,20220")
        self._db_server.setFixedHeight(42)

        self._db_database = QLineEdit()
        self._db_database.setFixedHeight(42)

        self._db_username = QLineEdit()
        self._db_username.setFixedHeight(42)

        self._db_password = QLineEdit()
        self._db_password.setEchoMode(QLineEdit.Password)
        self._db_password.setFixedHeight(42)

        btn_connect = QPushButton("연결")
        btn_connect.setFixedHeight(56)
        btn_connect.setStyleSheet(_BTN_DLG)
        btn_connect.clicked.connect(self._on_db_connect)

        btn_save = QPushButton("저장")
        btn_save.setFixedHeight(56)
        btn_save.setStyleSheet(_BTN_DLG)
        btn_save.clicked.connect(self._on_db_save)

        form.addRow("서버",     self._db_server)
        form.addRow("DB명",     self._db_database)
        form.addRow("사용자명", self._db_username)
        form.addRow("비밀번호", self._db_password)
        form.addRow("",         self._btn_pair(btn_connect, btn_save))
        return self._tab_wrap(g)

    # ── 탭 3b — MES 제품 선택 ───────────────────────────────────────── #

    def _build_tab_mes(self) -> QWidget:
        w = QWidget()
        w.setStyleSheet("background:#1a1a1a;")
        layout = QVBoxLayout(w)
        layout.setContentsMargins(32, 24, 32, 20)
        layout.setSpacing(12)

        hint = QLabel(
            "제품 등록 탭의 'MES 불러오기'에 표시할 제품을 선택합니다.\n"
            "DB 연결 후 목록을 불러온 뒤 체크하고 저장하세요."
        )
        hint.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
        hint.setWordWrap(True)
        layout.addWidget(hint)

        btn_row = QHBoxLayout()
        btn_row.setSpacing(8)

        btn_load = QPushButton("목록 불러오기")
        btn_load.setFixedHeight(42)
        btn_load.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
        btn_load.clicked.connect(self._on_mes_load)

        btn_all = QPushButton("전체 선택")
        btn_all.setFixedHeight(42)
        btn_all.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
        btn_all.clicked.connect(lambda: self._on_mes_set_all(True))

        btn_none = QPushButton("전체 해제")
        btn_none.setFixedHeight(42)
        btn_none.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
        btn_none.clicked.connect(lambda: self._on_mes_set_all(False))

        btn_row.addWidget(btn_load, stretch=2)
        btn_row.addWidget(btn_all, stretch=1)
        btn_row.addWidget(btn_none, stretch=1)
        layout.addLayout(btn_row)

        self._mes_list = _TouchCheckList()
        self._mes_list.setSelectionMode(QListWidget.NoSelection)
        self._mes_list.setMinimumHeight(380)
        self._mes_list.setStyleSheet("""
            QListWidget {
                background:#1a1a1a; border:1px solid #333333;
                border-radius:4px; outline:none; font-size:15px;
            }
            QListWidget::item {
                padding:12px 16px; border-bottom:1px solid #2a2a2a;
            }
            QListWidget::indicator { width:0px; height:0px; }
        """)
        self._mes_list.itemChanged.connect(self._on_mes_item_changed)
        layout.addWidget(self._mes_list, stretch=1)

        self._mes_count_lbl = QLabel("선택: 0 / 0")
        self._mes_count_lbl.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
        layout.addWidget(self._mes_count_lbl)

        btn_save = QPushButton("선택 저장")
        btn_save.setFixedHeight(56)
        btn_save.setStyleSheet(_BTN_DLG_PRIMARY)
        btn_save.clicked.connect(self._on_mes_save)
        layout.addWidget(btn_save)

        return w

    # ── 탭 4 — AI 모델 ──────────────────────────────────────────────── #

    def _build_tab_ai(self) -> QWidget:
        g = self._make_group("AI 모델 (YOLOv8)")
        layout = QVBoxLayout(g)
        layout.setSpacing(10)

        path_row = QHBoxLayout()
        self._ai_path_edit = QLineEdit()
        self._ai_path_edit.setReadOnly(True)
        self._ai_path_edit.setFixedHeight(42)
        self._ai_path_edit.setPlaceholderText("모델 경로 미설정")

        btn_browse = QPushButton("파일 선택")
        btn_browse.setFixedHeight(42)
        btn_browse.setFixedWidth(120)
        btn_browse.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
        btn_browse.clicked.connect(self._on_ai_browse)

        path_row.addWidget(self._ai_path_edit, stretch=1)
        path_row.addWidget(btn_browse)

        btn_load = QPushButton("로드")
        btn_load.setFixedHeight(56)
        btn_load.setStyleSheet(_BTN_DLG_PRIMARY)
        btn_load.clicked.connect(self._on_ai_load)

        btn_unload = QPushButton("해제")
        btn_unload.setFixedHeight(56)
        btn_unload.setStyleSheet(_BTN_DLG)
        btn_unload.clicked.connect(self._on_ai_unload)

        self._ai_info_lbl = QLabel("")
        self._ai_info_lbl.setStyleSheet("color:#888888; font-size:12px; background:transparent;")
        self._ai_info_lbl.setWordWrap(True)

        layout.addLayout(path_row)
        layout.addWidget(self._btn_pair(btn_load, btn_unload))
        layout.addWidget(self._ai_info_lbl)
        return self._tab_wrap(g)

    # ── 탭 5 — 컨베이어 ─────────────────────────────────────────────── #

    def _build_tab_conveyor(self) -> QWidget:
        g = self._make_group("컨베이어 설정")
        form = QFormLayout(g)
        form.setHorizontalSpacing(16)
        form.setVerticalSpacing(12)

        self._conv_distance = QDoubleSpinBox()
        self._conv_distance.setRange(1.0, 10000.0)
        self._conv_distance.setDecimals(1)
        self._conv_distance.setSuffix(" cm")
        self._conv_distance.setValue(100.0)
        self._conv_distance.setFixedHeight(42)

        self._conv_speed = QDoubleSpinBox()
        self._conv_speed.setRange(0.1, 10000.0)
        self._conv_speed.setDecimals(1)
        self._conv_speed.setSuffix(" cm/s")
        self._conv_speed.setValue(30.0)
        self._conv_speed.setFixedHeight(42)

        self._conv_delay_lbl = QLabel("3.33 s")
        self._conv_delay_lbl.setStyleSheet(
            "color:#44cc88; font-size:14px; font-weight:bold; background:transparent;"
        )

        self._conv_distance.valueChanged.connect(self._on_conveyor_changed)
        self._conv_speed.valueChanged.connect(self._on_conveyor_changed)

        btn_apply = QPushButton("적용")
        btn_apply.setFixedHeight(56)
        btn_apply.setStyleSheet(_BTN_DLG_PRIMARY)
        btn_apply.clicked.connect(self._on_conveyor_apply)

        form.addRow("카메라 간 거리", self._conv_distance)
        form.addRow("벨트 속도",      self._conv_speed)
        form.addRow("계산된 딜레이",  self._conv_delay_lbl)
        form.addRow("",               btn_apply)
        return self._tab_wrap(g)

    # ── 탭 6 — PLC ──────────────────────────────────────────────────── #

    def _build_tab_plc(self) -> QWidget:
        g = self._make_group("PLC 설정 (MELSEC Q06UDEHCPU · MC 프로토콜 3E)")
        form = QFormLayout(g)
        form.setHorizontalSpacing(16)
        form.setVerticalSpacing(12)

        self._plc_ip = QLineEdit()
        self._plc_ip.setPlaceholderText("예: 192.168.3.39")
        self._plc_ip.setFixedHeight(42)

        self._plc_port = QSpinBox()
        self._plc_port.setRange(1, 65535)
        self._plc_port.setFixedHeight(42)

        btn_connect = QPushButton("연결")
        btn_connect.setFixedHeight(56)
        btn_connect.setStyleSheet(_BTN_DLG)
        btn_connect.clicked.connect(self._on_plc_connect)

        form.addRow("IP",  self._plc_ip)
        form.addRow("포트", self._plc_port)
        form.addRow("",    btn_connect)
        return self._tab_wrap(g)

    def _on_plc_connect(self):
        ip   = self._plc_ip.text().strip()
        port = self._plc_port.value()
        log_action(f"[설정] PLC 연결 시도: {ip}:{port}")
        if not ip:
            QMessageBox.warning(self, "입력 오류", "IP 주소를 입력하세요.")
            return
        if self._sp._plc_client and self._sp._plc_client.is_connected():
            self._sp._plc_client.disconnect()
        client = PLCClient()
        ok = client.connect(ip, port)
        if ok:
            self._sp._plc_client = client
            self._sp._set_plc_connected(True)
            self._sp._save_config({"plc": {"ip": ip, "port": port}})
            self._sp._config.setdefault("plc", {}).update({"ip": ip, "port": port})
            if self._sp._update_plc_cb:
                self._sp._update_plc_cb(client)
            self._sp.plc_status_changed.emit(True)
            QMessageBox.information(self, "연결 성공", f"PLC 연결 성공\n{ip}:{port}")
        else:
            QMessageBox.critical(self, "연결 실패", f"PLC에 연결할 수 없습니다.\n{ip}:{port}")

    # ── 하단 버튼 바 ─────────────────────────────────────────────────── #

    def _build_bottom_bar(self) -> QWidget:
        bar = QWidget()
        bar.setStyleSheet("background:#111111;")
        bar.setFixedHeight(72)
        row = QHBoxLayout(bar)
        row.setContentsMargins(24, 8, 24, 8)
        row.setSpacing(12)

        btn_save_all = QPushButton("전체 설정 저장")
        btn_save_all.setFixedHeight(56)
        btn_save_all.setStyleSheet(_BTN_DLG_PRIMARY)
        btn_save_all.clicked.connect(self._on_save_all)

        btn_close = QPushButton("닫기")
        btn_close.setFixedHeight(56)
        btn_close.setStyleSheet(_BTN_DLG)
        btn_close.clicked.connect(self.accept)

        # 터치 키보드 표시/숨김 토글 버튼 (물리 키보드 없는 터치 모니터용)
        self._kb_btn = QPushButton("\u2328")   # ⌨ 키보드 글리프
        self._kb_btn.setCheckable(True)
        self._kb_btn.setFixedSize(56, 56)
        self._kb_btn.setToolTip("터치 키보드 표시 / 숨기기")
        self._kb_btn.setStyleSheet(
            "QPushButton {"
            "  background:#2a2a2a; color:#cccccc; border:1px solid #555555;"
            "  border-radius:4px; font-size:24px;"
            "}"
            "QPushButton:hover { background:#333333; color:#ffffff; }"
            "QPushButton:checked { background:#1D9E75; color:#ffffff; border:none; }"
        )
        self._kb_btn.toggled.connect(self._on_toggle_keyboard)

        row.addWidget(self._kb_btn)
        row.addWidget(btn_save_all, stretch=2)
        row.addWidget(btn_close, stretch=1)
        return bar

    # ── 터치 키보드 토글 ─────────────────────────────────────────────── #

    def _on_toggle_keyboard(self, checked: bool):
        if checked:
            if self._show_touch_keyboard():
                log_action("[설정] 터치 키보드 표시")
            else:
                # 실패 시 토글 상태 원복 (시그널 재발생 방지)
                self._kb_btn.blockSignals(True)
                self._kb_btn.setChecked(False)
                self._kb_btn.blockSignals(False)
                QMessageBox.warning(
                    self, "터치 키보드",
                    "터치 키보드를 열 수 없습니다.\n"
                    "Windows 터치 키보드 또는 화상 키보드를 사용할 수 있는지 확인하세요.",
                )
        else:
            self._hide_touch_keyboard()
            log_action("[설정] 터치 키보드 숨김")

    def _show_touch_keyboard(self) -> bool:
        """Windows 터치 키보드(TabTip)를 ITipInvocation COM으로 표시한다."""
        return show_touch_keyboard()

    def _hide_touch_keyboard(self):
        """표시 중인 터치 키보드를 숨긴다."""
        hide_touch_keyboard()

    def done(self, result: int):
        # 키보드를 켠 상태에서만 숨김 (Toggle은 반전이라 미사용 시 닫으면 오히려 켜짐)
        if self._kb_btn.isChecked():
            self._hide_touch_keyboard()
        super().done(result)

    @staticmethod
    def _make_group(title: str):
        from PyQt5.QtWidgets import QGroupBox
        g = QGroupBox(title)
        g.setStyleSheet(_GRP)
        return g

    # ── 필드 채우기 ──────────────────────────────────────────────────── #

    def _populate(self):
        cfg = self._sp._config

        cognex = cfg.get("cognex", {})
        self._cognex_ip.setText(cognex.get("ip", ""))
        self._cognex_port.setValue(cognex.get("port", 23))

        basler = cfg.get("basler", {})
        self._basler_exposure.setValue(basler.get("exposure", 10000))
        self._basler_gain.setValue(basler.get("gain", 20))

        conv = cfg.get("conveyor", {})
        self._conv_distance.setValue(conv.get("distance_cm", 100.0))
        self._conv_speed.setValue(conv.get("speed_cms", 30.0))
        self._on_conveyor_changed()

        db = cfg.get("db", {})
        self._db_server.setText(db.get("server", ""))
        self._db_database.setText(db.get("database", ""))
        self._db_username.setText(db.get("username", ""))
        self._db_password.setText(db.get("password", ""))

        ai = cfg.get("ai", {})
        if "model_path" in ai:
            self._ai_path_edit.setText(to_project_relative(ai["model_path"]))

        if self._sp._detector and self._sp._detector.is_loaded():
            names = ", ".join(self._sp._detector.class_names)
            self._ai_info_lbl.setText(
                f"클래스: {names}  ({len(self._sp._detector.class_names)}개)"
            )

        plc = cfg.get("plc", {})
        self._plc_ip.setText(plc.get("ip", ""))
        self._plc_port.setValue(plc.get("port", 5010))

    # ── 코그넥스 탭 슬롯 ─────────────────────────────────────────────── #

    def _on_cognex_connect(self):
        ip   = self._cognex_ip.text().strip()
        port = self._cognex_port.value()
        log_action(f"[설정] Cognex 연결 시도: {ip}:{port}")
        if not ip:
            QMessageBox.warning(self, "입력 오류", "IP 주소를 입력하세요.")
            return
        if self._sp._insight and self._sp._insight._sock is not None:
            self._sp._insight.disconnect()
        new_cam = InSightCamera()
        new_cam.connect(ip, port)
        if new_cam._sock is None:
            QMessageBox.critical(
                self, "연결 실패",
                f"In-Sight 카메라에 연결할 수 없습니다.\nIP: {ip}  포트: {port}",
            )
            return
        self._sp._insight = new_cam
        if self._sp._update_insight_cb:
            self._sp._update_insight_cb(new_cam)
        self._sp._set_cognex_connected(True)
        self._sp._save_cognex_config(ip, port)
        self._sp.cognex_status_changed.emit(True)
        QMessageBox.information(self, "연결 성공", f"In-Sight 카메라 연결 성공\n{ip}:{port}")

    def _on_cognex_save(self):
        ip   = self._cognex_ip.text().strip()
        port = self._cognex_port.value()
        self._sp._save_cognex_config(ip, port)
        QMessageBox.information(self, "저장", "코그넥스 설정이 저장되었습니다.")

    # ── Basler 탭 슬롯 ───────────────────────────────────────────────── #

    def _on_basler_apply(self):
        log_action(f"[설정] Basler 설정 적용: 노출={self._basler_exposure.value()}µs  게인={self._basler_gain.value()}")
        if not self._sp._basler or self._sp._basler.camera is None:
            QMessageBox.warning(self, "경고", "Basler 카메라가 연결되어 있지 않습니다.")
            return
        exposure = self._basler_exposure.value()
        gain     = self._basler_gain.value()
        errors   = []
        try:
            self._sp._basler.camera.ExposureAuto.SetValue("Off")
            self._sp._basler.camera.ExposureTime.SetValue(float(exposure))
        except Exception as e:
            errors.append(f"노출: {e}")
        try:
            self._sp._basler.camera.Gain.SetValue(float(gain))
        except Exception as e:
            errors.append(f"게인: {e}")
        if errors:
            QMessageBox.warning(self, "설정 적용 오류", "\n".join(errors))
        else:
            self._sp._save_config({"basler": {"exposure": exposure, "gain": gain}})
            self._sp._config.setdefault("basler", {}).update(
                {"exposure": exposure, "gain": gain}
            )
            QMessageBox.information(self, "적용 완료", f"노출: {exposure} µs  게인: {gain}")

    # ── DB 탭 슬롯 ───────────────────────────────────────────────────── #

    def _on_db_connect(self):
        server   = self._db_server.text().strip()
        database = self._db_database.text().strip()
        username = self._db_username.text().strip()
        password = self._db_password.text()
        log_action(f"[설정] DB 연결 시도: {server}/{database}")
        if not server or not database:
            QMessageBox.warning(self, "경고", "서버와 DB명을 입력해주세요.")
            return
        client = SQLClient()
        ok = client.connect(server, database, username, password)
        if ok:
            self._sp._db_client = client
            self._sp._set_db_connected(True)
            cfg = {"server": server, "database": database,
                   "username": username, "password": password}
            self._sp._save_config({"db": cfg})
            self._sp._config.setdefault("db", {}).update(cfg)
            if self._sp._update_db_cb:
                self._sp._update_db_cb(client)
            QMessageBox.information(self, "연결 성공", f"DB 연결 성공\n{server}/{database}")
        else:
            QMessageBox.critical(
                self, "연결 실패",
                "DB 연결에 실패했습니다.\n서버 주소, DB명, 계정 정보를 확인하세요.",
            )

    def _on_db_save(self):
        cfg = {
            "server":   self._db_server.text().strip(),
            "database": self._db_database.text().strip(),
            "username": self._db_username.text().strip(),
            "password": self._db_password.text(),
        }
        self._sp._save_config({"db": cfg})
        self._sp._config.setdefault("db", {}).update(cfg)
        QMessageBox.information(self, "저장", "DB 설정이 저장되었습니다.")

    # ── MES 제품 탭 슬롯 ─────────────────────────────────────────────── #

    def _get_mes_selected_ids(self) -> list:
        ids = []
        for i in range(self._mes_list.count()):
            item = self._mes_list.item(i)
            if item.checkState() == Qt.Checked:
                ids.append(item.data(Qt.UserRole))
        return ids

    def _update_mes_count(self):
        total = self._mes_list.count()
        selected = len(self._get_mes_selected_ids())
        self._mes_count_lbl.setText(f"선택: {selected} / {total}")

    @staticmethod
    def _style_mes_item(item):
        name = item.data(Qt.UserRole + 1) or item.text().lstrip("✓ ").lstrip("  ")
        item.setData(Qt.UserRole + 1, name)
        if item.checkState() == Qt.Checked:
            item.setText(f"✓  {name}")
            item.setBackground(QColor("#666666"))
            item.setForeground(QColor("#ffffff"))
        else:
            item.setText(f"    {name}")
            item.setBackground(QColor("#1e1e1e"))
            item.setForeground(QColor("#888888"))

    def _on_mes_item_changed(self, item):
        self._style_mes_item(item)
        self._update_mes_count()

    def _on_mes_set_all(self, checked: bool):
        state = Qt.Checked if checked else Qt.Unchecked
        self._mes_list.blockSignals(True)
        for i in range(self._mes_list.count()):
            item = self._mes_list.item(i)
            item.setCheckState(state)
            self._style_mes_item(item)
        self._mes_list.blockSignals(False)
        self._update_mes_count()

    def _on_mes_load(self):
        client = self._sp._db_client
        if not client or not client.is_connected():
            QMessageBox.warning(
                self, "경고",
                "DB가 연결되어 있지 않습니다.\n"
                "DB 탭에서 먼저 연결해주세요.",
            )
            return

        items = client.get_all_articles()
        if not items:
            QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
            return

        saved_ids = set(self._sp._config.get("mes", {}).get("selected_article_ids", []))

        self._mes_list.blockSignals(True)
        self._mes_list.clear()
        for item in items:
            li = QListWidgetItem(item["article"])
            li.setSizeHint(QSize(0, 52))
            li.setFlags(li.flags() | Qt.ItemIsUserCheckable)
            li.setData(Qt.UserRole, item["article_id"])
            li.setData(Qt.UserRole + 1, item["article"])
            li.setToolTip(
                f"ID: {item['article_id']}  |  모델: {item.get('buyer_article_no', '')}"
            )
            li.setCheckState(
                Qt.Checked if item["article_id"] in saved_ids else Qt.Unchecked
            )
            self._style_mes_item(li)
            self._mes_list.addItem(li)
        self._mes_list.blockSignals(False)
        self._update_mes_count()
        log_action(f"[설정] MES 제품 목록 불러오기: {len(items)}개")

    def _on_mes_save(self):
        selected_ids = self._get_mes_selected_ids()
        log_action(f"[설정] MES 제품 선택 저장: {len(selected_ids)}개")
        self._sp._save_config({"mes": {"selected_article_ids": selected_ids}})
        self._sp._config.setdefault("mes", {})["selected_article_ids"] = selected_ids
        QMessageBox.information(
            self, "저장",
            f"{len(selected_ids)}개 제품이 MES 불러오기 목록에 저장되었습니다.",
        )

    # ── AI 탭 슬롯 ───────────────────────────────────────────────────── #

    def _on_ai_browse(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "YOLOv8 모델 선택", "", "YOLOv8 모델 (*.pt)"
        )
        if path:
            self._ai_path_edit.setText(to_project_relative(path))

    def _on_ai_load(self):
        path = self._ai_path_edit.text().strip()
        log_action(f"[설정] AI 모델 로드: {path}")
        if not path:
            QMessageBox.warning(self, "경고", "모델 경로를 먼저 선택하세요.")
            return
        ok = self._sp._do_load_model(path)
        if ok:
            names = ", ".join(self._sp._detector.class_names)
            self._ai_info_lbl.setText(
                f"클래스: {names}  ({len(self._sp._detector.class_names)}개)"
            )
            self._sp._set_ai_loaded(True)
            QMessageBox.information(self, "로드 완료", f"모델 로드 완료\n{path}")
        else:
            self._sp._set_ai_loaded(False)
            QMessageBox.critical(self, "로드 실패", f"모델을 로드할 수 없습니다.\n{path}")

    def _on_ai_unload(self):
        log_action("[설정] AI 모델 해제")
        if self._sp._detector:
            self._sp._detector._model      = None
            self._sp._detector.model_path  = None
        self._sp._set_ai_loaded(False)
        self._ai_info_lbl.setText("")

    # ── 컨베이어 탭 슬롯 ─────────────────────────────────────────────── #

    def _on_conveyor_changed(self):
        dist  = self._conv_distance.value()
        speed = self._conv_speed.value()
        self._conv_delay_lbl.setText(f"{dist / speed:.2f} s" if speed > 0 else "—")

    def _on_conveyor_apply(self):
        dist  = self._conv_distance.value()
        speed = self._conv_speed.value()
        delay = dist / speed if speed > 0 else 0.0
        log_action(f"[설정] 컨베이어 적용: {dist}cm / {speed}cm·s → 딜레이 {delay:.2f}s")
        try:
            self._sp._save_config({"conveyor": {"distance_cm": dist, "speed_cms": speed}})
            self._sp._config.setdefault("conveyor", {}).update(
                {"distance_cm": dist, "speed_cms": speed}
            )
            self._sp.belt_settings_changed.emit(delay)
            QMessageBox.information(
                self, "적용 완료",
                f"거리: {dist} cm  /  속도: {speed} cm/s\n딜레이: {delay:.2f}s",
            )
        except Exception as e:
            QMessageBox.critical(self, "저장 실패", str(e))

    # ── 전체 저장 ────────────────────────────────────────────────────── #

    def _on_save_all(self):
        log_action("[설정] 전체 설정 저장")
        data = {
            "cognex": {
                "ip":   self._cognex_ip.text().strip(),
                "port": self._cognex_port.value(),
            },
            "basler": {
                "exposure": self._basler_exposure.value(),
                "gain":     self._basler_gain.value(),
            },
            "db": {
                "server":   self._db_server.text().strip(),
                "database": self._db_database.text().strip(),
                "username": self._db_username.text().strip(),
                "password": self._db_password.text(),
            },
            "ai": {
                "model_path": to_project_relative(self._ai_path_edit.text().strip()),
            },
            "conveyor": {
                "distance_cm": self._conv_distance.value(),
                "speed_cms":   self._conv_speed.value(),
            },
            "plc": {
                "ip":   self._plc_ip.text().strip(),
                "port": self._plc_port.value(),
            },
            "mes": {
                "selected_article_ids": (
                    self._get_mes_selected_ids()
                    if self._mes_list.count() > 0
                    else self._sp._config.get("mes", {}).get("selected_article_ids", [])
                ),
            },
        }
        try:
            self._sp._save_config(data)
            for k, v in data.items():
                if isinstance(self._sp._config.get(k), dict):
                    self._sp._config[k].update(v)
                else:
                    self._sp._config[k] = v
            QMessageBox.information(self, "저장 완료", "전체 설정이 저장되었습니다.")
        except Exception as e:
            QMessageBox.critical(self, "저장 실패", str(e))


# ══════════════════════════════════════════════════════════════════════ #
# SettingsPage (작업자 메인 화면)
# ══════════════════════════════════════════════════════════════════════ #

class SettingsPage(QWidget):
    cognex_status_changed = pyqtSignal(bool)
    basler_status_changed = pyqtSignal(bool)
    belt_settings_changed = pyqtSignal(float)
    plc_status_changed    = pyqtSignal(bool)

    def __init__(self, insight_cam, basler_cam, config: dict,
                 detector=None, update_insight_cb=None,
                 update_basler_cb=None, update_detector_cb=None,
                 update_db_cb=None, update_plc_cb=None,
                 plc_client=None, parent=None):
        super().__init__(parent)
        self._insight            = insight_cam
        self._basler             = basler_cam
        self._config             = config
        self._detector           = detector
        self._update_insight_cb  = update_insight_cb
        self._update_basler_cb   = update_basler_cb
        self._update_detector_cb = update_detector_cb
        self._update_db_cb       = update_db_cb
        self._update_plc_cb      = update_plc_cb
        self._db_client          = None
        self._plc_client         = plc_client

        self._build_ui()
        self._sync_connection_status()
        self._auto_load_model()

    # ── UI 구성 ──────────────────────────────────────────────────────── #

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(40, 32, 40, 32)
        root.setSpacing(0)

        # 상단 행: 페이지 제목 + 관리자 버튼
        top_row = QHBoxLayout()
        top_row.setContentsMargins(0, 0, 0, 16)

        title_lbl = QLabel("연결 상태")
        title_lbl.setStyleSheet("color:#888888; font-size:14px;")
        top_row.addWidget(title_lbl)
        top_row.addStretch()

        admin_btn = QPushButton("⚙  관리자 설정")
        admin_btn.setFixedHeight(40)
        admin_btn.setStyleSheet(_BTN_ADMIN)
        admin_btn.clicked.connect(self._on_admin_settings)
        top_row.addWidget(admin_btn)

        root.addLayout(top_row)

        # 2×2 카드 그리드 — 카드 고정 너비 500px, 왼쪽 정렬
        self._cognex_card = self._build_card(
            "코그넥스 In-Sight 2000C",
            icon_color="#042C53", icon_text="IN",
            connect_cb=self._on_cognex_connect_quick,
            disconnect_cb=self._on_cognex_disconnect,
        )
        self._basler_card = self._build_card(
            "Basler USB 카메라",
            icon_color="#26215C", icon_text="BA",
            connect_cb=self._on_basler_connect,
            disconnect_cb=self._on_basler_disconnect,
        )
        self._db_card = self._build_card(
            "MS SQL Server DB",
            icon_color="#412402", icon_text="DB",
            connect_cb=self._on_db_connect_quick,
            disconnect_cb=self._on_db_disconnect,
        )
        self._ai_card = self._build_card(
            "AI 모델 (YOLOv8)",
            icon_color="#085041", icon_text="AI",
            connect_label="로드",
            disconnect_label="해제",
            connect_cb=self._on_ai_load_quick,
            disconnect_cb=self._on_ai_unload,
        )
        self._plc_card = self._build_card(
            "PLC (MELSEC Q06UDEHCPU)",
            icon_color="#3B1A04", icon_text="PLC",
            connect_cb=self._on_plc_connect_quick,
            disconnect_cb=self._on_plc_disconnect,
        )

        grid = QGridLayout()
        grid.setSpacing(10)
        grid.setContentsMargins(0, 0, 0, 0)
        grid.addWidget(self._cognex_card["frame"], 0, 0)
        grid.addWidget(self._basler_card["frame"], 0, 1)
        grid.addWidget(self._db_card["frame"],     1, 0)
        grid.addWidget(self._ai_card["frame"],     1, 1)
        grid.addWidget(self._plc_card["frame"],    2, 0)

        # 그리드를 왼쪽 정렬로 감싸는 컨테이너
        grid_container = QWidget()
        grid_container.setLayout(grid)

        outer = QHBoxLayout()
        outer.setContentsMargins(0, 0, 0, 0)
        outer.addWidget(grid_container)
        outer.addStretch()

        root.addLayout(outer)
        root.addStretch()

    def _build_card(self, title: str,
                    icon_color: str = "#333333", icon_text: str = "",
                    connect_label: str = "연결",
                    disconnect_label: str = "연결 해제",
                    connect_cb=None, disconnect_cb=None) -> dict:
        frame = QFrame()
        frame.setStyleSheet(_CARD)
        frame.setFixedSize(500, 96)

        row = QHBoxLayout(frame)
        row.setContentsMargins(16, 0, 16, 0)
        row.setSpacing(14)

        # 아이콘 박스
        icon = QLabel(icon_text)
        icon.setFixedSize(36, 36)
        icon.setAlignment(Qt.AlignCenter)
        icon.setStyleSheet(
            f"background:{icon_color}; color:#ffffff;"
            "border-radius:6px; font-size:13px; font-weight:500;"
        )

        # 제목 + 상태 (세로 스택)
        info_w = QWidget()
        info_w.setStyleSheet("background:transparent;")
        info_v = QVBoxLayout(info_w)
        info_v.setContentsMargins(0, 0, 0, 0)
        info_v.setSpacing(2)

        title_lbl = QLabel(title)
        title_lbl.setStyleSheet(
            "color:#cccccc; font-size:14px; background:transparent; border:none;"
        )

        status_lbl = QLabel("● 연결 안됨")
        status_lbl.setStyleSheet(_STATUS_FAIL)

        info_v.addWidget(title_lbl)
        info_v.addWidget(status_lbl)

        # 버튼 쌍
        btn_connect = QPushButton(connect_label)
        btn_connect.setFixedHeight(43)
        btn_connect.setStyleSheet(_BTN_CARD_CONNECT)
        if connect_cb:
            btn_connect.clicked.connect(connect_cb)

        btn_disconnect = QPushButton(disconnect_label)
        btn_disconnect.setFixedHeight(43)
        btn_disconnect.setStyleSheet(_BTN_CARD_DISCONNECT)
        btn_disconnect.setEnabled(False)
        if disconnect_cb:
            btn_disconnect.clicked.connect(disconnect_cb)

        row.addWidget(icon)
        row.addWidget(info_w, stretch=1)
        row.addWidget(btn_connect)
        row.addWidget(btn_disconnect)

        return {
            "frame":          frame,
            "status_lbl":     status_lbl,
            "btn_connect":    btn_connect,
            "btn_disconnect": btn_disconnect,
        }

    # ── 관리자 설정 ──────────────────────────────────────────────────── #

    def _on_admin_settings(self):
        dlg = PasswordDialog(self)
        if dlg.exec_() == QDialog.Accepted:
            AdminSettingsDialog(self, self).exec_()

    # ── 연결 상태 동기화 (외부에서도 호출 가능) ──────────────────────── #

    def _sync_connection_status(self):
        self._set_cognex_connected(bool(self._insight and self._insight.is_connected()))
        self._set_basler_connected(bool(self._basler  and self._basler.is_connected()))
        self._set_db_connected(bool(self._db_client and self._db_client.is_connected()))
        if self._detector:
            self._set_ai_loaded(self._detector.is_loaded())
        self._set_plc_connected(bool(self._plc_client and self._plc_client.is_connected()))

    def _set_card_state(self, card: dict, connected: bool,
                        ok_text: str, fail_text: str):
        if connected:
            card["status_lbl"].setText(f"● {ok_text}")
            card["status_lbl"].setStyleSheet(_STATUS_OK)
        else:
            card["status_lbl"].setText(f"● {fail_text}")
            card["status_lbl"].setStyleSheet(_STATUS_FAIL)
        card["btn_connect"].setEnabled(not connected)
        card["btn_disconnect"].setEnabled(connected)

    def _set_cognex_connected(self, connected: bool):
        self._set_card_state(self._cognex_card, connected, "연결됨", "연결 안됨")

    def _set_basler_connected(self, connected: bool):
        self._set_card_state(self._basler_card, connected, "연결됨", "연결 안됨")

    def _set_db_connected(self, connected: bool):
        self._set_card_state(self._db_card, connected, "연결됨", "연결 안됨")

    def _set_ai_loaded(self, loaded: bool):
        self._set_card_state(self._ai_card, loaded, "로드됨", "로드 안됨")

    def _set_plc_connected(self, connected: bool):
        self._set_card_state(self._plc_card, connected, "연결됨", "연결 안됨")

    # ── 빠른 연결 (카드 버튼 — config.json 값 그대로 사용) ──────────── #

    def _on_cognex_connect_quick(self):
        cfg  = self._config.get("cognex", {})
        ip   = cfg.get("ip", "").strip()
        port = cfg.get("port", 23)
        log_action(f"[설정] Cognex 연결: {ip}:{port}")
        if not ip:
            QMessageBox.warning(
                self, "설정 필요",
                "관리자 설정에서 코그넥스 IP를 먼저 설정해주세요.",
            )
            return
        if self._insight and self._insight._sock is not None:
            self._insight.disconnect()
        new_cam = InSightCamera()
        new_cam.connect(ip, port)
        if new_cam._sock is None:
            QMessageBox.critical(
                self, "연결 실패",
                f"In-Sight 카메라에 연결할 수 없습니다.\n{ip}:{port}",
            )
            return
        self._insight = new_cam
        if self._update_insight_cb:
            self._update_insight_cb(new_cam)
        self._set_cognex_connected(True)
        self.cognex_status_changed.emit(True)

    def _on_cognex_disconnect(self):
        log_action("[설정] Cognex 연결 해제")
        if self._insight:
            self._insight.disconnect()
        self._set_cognex_connected(False)
        self.cognex_status_changed.emit(False)

    def _on_basler_connect(self):
        log_action("[설정] Basler 연결")
        if self._basler and self._basler.camera is not None:
            self._basler.disconnect()
        new_cam = BaslerCamera()
        new_cam.connect()
        if new_cam.camera is None:
            QMessageBox.critical(
                self, "연결 실패",
                "Basler 카메라에 연결할 수 없습니다.\nUSB 연결 및 전원을 확인하세요.",
            )
            return
        self._basler = new_cam
        if self._update_basler_cb:
            self._update_basler_cb(new_cam)
        self._set_basler_connected(True)
        self.basler_status_changed.emit(True)

    def _on_basler_disconnect(self):
        log_action("[설정] Basler 연결 해제")
        if self._basler:
            self._basler.disconnect()
        self._set_basler_connected(False)
        self.basler_status_changed.emit(False)

    def _on_db_connect_quick(self):
        cfg      = self._config.get("db", {})
        server   = cfg.get("server", "").strip()
        log_action(f"[설정] DB 연결: {server}")
        database = cfg.get("database", "").strip()
        username = cfg.get("username", "").strip()
        password = cfg.get("password", "")
        if not server or not database:
            QMessageBox.warning(
                self, "설정 필요",
                "관리자 설정에서 DB 서버 정보를 먼저 설정해주세요.",
            )
            return
        client = SQLClient()
        ok = client.connect(server, database, username, password)
        if ok:
            self._db_client = client
            self._set_db_connected(True)
            if self._update_db_cb:
                self._update_db_cb(client)
        else:
            QMessageBox.critical(
                self, "연결 실패",
                "DB 연결에 실패했습니다.\n관리자 설정에서 서버 정보를 확인하세요.",
            )

    def _on_db_disconnect(self):
        log_action("[설정] DB 연결 해제")
        if self._db_client:
            self._db_client.disconnect()
            self._db_client = None
        if self._update_db_cb:
            self._update_db_cb(None)
        self._set_db_connected(False)

    def _on_ai_load_quick(self):
        path = self._config.get("ai", {}).get("model_path", "").strip()
        log_action(f"[설정] AI 모델 로드: {path}")
        if not path:
            QMessageBox.warning(
                self, "설정 필요",
                "관리자 설정에서 AI 모델 경로를 먼저 설정해주세요.",
            )
            return
        ok = self._do_load_model(path)
        if ok:
            self._set_ai_loaded(True)
        else:
            QMessageBox.critical(self, "로드 실패", f"모델을 로드할 수 없습니다.\n{path}")

    def _on_ai_unload(self):
        log_action("[설정] AI 모델 해제")
        if self._detector:
            self._detector._model     = None
            self._detector.model_path = None
        self._set_ai_loaded(False)

    def _on_plc_connect_quick(self):
        cfg  = self._config.get("plc", {})
        ip   = cfg.get("ip", "").strip()
        port = cfg.get("port", 5010)
        log_action(f"[설정] PLC 연결: {ip}:{port}")
        if not ip:
            QMessageBox.warning(
                self, "설정 필요",
                "관리자 설정에서 PLC IP를 먼저 설정해주세요.",
            )
            return
        client = PLCClient()
        ok = client.connect(ip, port)
        if ok:
            self._plc_client = client
            self._set_plc_connected(True)
            if self._update_plc_cb:
                self._update_plc_cb(client)
            self.plc_status_changed.emit(True)
        else:
            QMessageBox.critical(
                self, "연결 실패",
                f"PLC에 연결할 수 없습니다.\n{ip}:{port}",
            )

    def _on_plc_disconnect(self):
        log_action("[설정] PLC 연결 해제")
        if self._plc_client:
            self._plc_client.disconnect()
            self._plc_client = None
        if self._update_plc_cb:
            self._update_plc_cb(None)
        self._set_plc_connected(False)
        self.plc_status_changed.emit(False)

    # ── AI 모델 로드 공통 ────────────────────────────────────────────── #

    def _do_load_model(self, path: str) -> bool:
        if self._detector is None:
            self._detector = Detector()
        abs_path = resolve_path(path)
        ok = self._detector.load_model(abs_path)
        if ok:
            saved_path = to_project_relative(abs_path)
            self._save_config({"ai": {"model_path": saved_path}})
            self._config.setdefault("ai", {})["model_path"] = saved_path
            if self._update_detector_cb:
                self._update_detector_cb(self._detector)
        return ok

    def _auto_load_model(self):
        path = self._config.get("ai", {}).get("model_path", "")
        abs_path = resolve_path(path)
        if abs_path and os.path.exists(abs_path):
            ok = self._do_load_model(abs_path)
            if ok:
                self._set_ai_loaded(True)

    # ── config 읽기 / 쓰기 ───────────────────────────────────────────── #

    def _load_config(self) -> dict:
        try:
            with open(get_path("config.json"), "r", encoding="utf-8") as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            return {}

    def _save_config(self, data: dict):
        cfg_path = get_path("config.json")
        try:
            with open(cfg_path, "r", encoding="utf-8") as f:
                existing = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            existing = {}
        for key, val in data.items():
            if isinstance(val, dict) and isinstance(existing.get(key), dict):
                existing[key].update(val)
            else:
                existing[key] = val
        with open(cfg_path, "w", encoding="utf-8") as f:
            json.dump(existing, f, ensure_ascii=False, indent=2)

    def _save_cognex_config(self, ip: str, port: int):
        try:
            self._save_config({"cognex": {"ip": ip, "port": port}})
            self._config.setdefault("cognex", {}).update({"ip": ip, "port": port})
        except Exception as e:
            print(f"[설정] config.json 저장 실패: {e}")
