from __future__ import annotations

import shutil
import sys
import tempfile
import threading
import traceback
from pathlib import Path
from typing import Callable

from PySide6.QtCore import (
    QEasingCurve,
    QEvent,
    QObject,
    QPoint,
    QPointF,
    QPropertyAnimation,
    QRectF,
    QSize,
    Qt,
    QTimer,
    QUrl,
    Signal,
    Property,
)
from PySide6.QtNetwork import QLocalServer, QLocalSocket

from PySide6.QtGui import (
    QAction,
    QColor,
    QDesktopServices,
    QFont,
    QIcon,
    QKeyEvent,
    QLinearGradient,
    QRadialGradient,
    QPainter,
    QPainterPath,
    QPen,
    QPixmap,
)
from PySide6.QtWidgets import (
    QAbstractButton,
    QApplication,
    QButtonGroup,
    QCheckBox,
    QComboBox,
    QDialog,
    QFrame,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMenu,
    QMessageBox,
    QPushButton,
    QScrollArea,
    QSizeGrip,
    QSizePolicy,
    QStackedWidget,
    QSystemTrayIcon,
    QTextEdit,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

from .core.audio_engine import (
    DirectVoiceEngine,
    PhraseRecorder,
    choose_default_input,
    choose_vb_cable_output,
    device_name,
    find_device_by_name,
    list_devices,
    play_wav,
)
from .core.config import AppConfig
from .core.openai_pipeline import OpenAITranslationPipeline
from .core.ptt_controller import GlobalPTT
from .core.startup_manager import set_start_with_windows
from .core.updater import UpdateInfo, check_for_update

BASE = (Path(getattr(sys, "_MEIPASS")) / "app") if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") else Path(__file__).resolve().parent
ASSETS = BASE / "assets"
VERSION = "9.11.2"


class C:
    BG = "#020811"
    BG2 = "#06111b"
    PANEL = "#04111d"
    PANEL2 = "#071823"
    LINE = "#143a52"
    LINE_HI = "#0a7eb6"
    CYAN = "#00b8ff"
    CYAN_DIM = "#0a6f9b"
    ORANGE = "#ff9800"
    ORANGE_DIM = "#9c5b00"
    GREEN = "#68d52b"
    TEXT = "#e8edf0"
    MUTED = "#9aa8b2"
    RED = "#ff4f5e"




class TechBackdrop(QWidget):
    """Painted cosmic/HUD background. No buttons or text are baked into it."""

    def __init__(self, parent: QWidget | None = None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_StyledBackground, False)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = QRectF(self.rect())

        base = QLinearGradient(rect.topLeft(), rect.bottomRight())
        base.setColorAt(0.0, QColor(1, 6, 12))
        base.setColorAt(0.45, QColor(3, 12, 22))
        base.setColorAt(1.0, QColor(1, 5, 10))
        painter.fillRect(rect, base)

        # Soft cyan and orange light pools, similar to the approved mock-up.
        for cx, cy, radius, color in (
            (rect.width() * 0.18, rect.height() * 0.22, rect.width() * 0.48, QColor(0, 134, 210, 36)),
            (rect.width() * 0.83, rect.height() * 0.48, rect.width() * 0.42, QColor(255, 132, 0, 22)),
        ):
            glow = QRadialGradient(QPointF(cx, cy), radius)
            glow.setColorAt(0.0, color)
            glow.setColorAt(1.0, QColor(color.red(), color.green(), color.blue(), 0))
            painter.fillRect(rect, glow)

        # Technical grid.
        painter.setPen(QPen(QColor(18, 68, 96, 28), 1))
        step = 36
        for x in range(0, self.width(), step):
            painter.drawLine(x, 0, x, self.height())
        for y in range(0, self.height(), step):
            painter.drawLine(0, y, self.width(), y)

        # Deterministic star field.
        for i in range(86):
            x = (i * 97 + 31) % max(1, self.width())
            y = (i * 53 + 17) % max(1, self.height())
            alpha = 28 + (i * 13) % 70
            size = 1 if i % 5 else 2
            painter.fillRect(QRectF(x, y, size, size), QColor(92, 193, 255, alpha))

        # Decorative circuit traces around the edges.
        painter.setPen(QPen(QColor(0, 157, 224, 70), 1.1))
        m = 14
        for offset in (0, 8):
            path = QPainterPath()
            path.moveTo(m + offset, 70 + offset)
            path.lineTo(m + 42 + offset, 28 + offset)
            path.lineTo(self.width() * .31, 28 + offset)
            path.lineTo(self.width() * .34, 14 + offset)
            painter.drawPath(path)
        painter.setPen(QPen(QColor(255, 145, 0, 58), 1.1))
        path = QPainterPath()
        path.moveTo(self.width() - 18, self.height() * .3)
        path.lineTo(self.width() - 42, self.height() * .3 + 24)
        path.lineTo(self.width() - 42, self.height() * .72)
        path.lineTo(self.width() - 70, self.height() * .72 + 28)
        painter.drawPath(path)

        # Dark vignette keeps text readable.
        vignette = QRadialGradient(rect.center(), max(rect.width(), rect.height()) * .72)
        vignette.setColorAt(0.58, QColor(0, 0, 0, 0))
        vignette.setColorAt(1.0, QColor(0, 0, 0, 120))
        painter.fillRect(rect, vignette)
        super().paintEvent(event)


def _octagon(rect: QRectF, cut: float = 12.0) -> QPainterPath:
    path = QPainterPath()
    path.moveTo(rect.left() + cut, rect.top())
    path.lineTo(rect.right() - cut, rect.top())
    path.lineTo(rect.right(), rect.top() + cut)
    path.lineTo(rect.right(), rect.bottom() - cut)
    path.lineTo(rect.right() - cut, rect.bottom())
    path.lineTo(rect.left() + cut, rect.bottom())
    path.lineTo(rect.left(), rect.bottom() - cut)
    path.lineTo(rect.left(), rect.top() + cut)
    path.closeSubpath()
    return path


def set_tip(widget: QWidget, text: str, enabled: bool = True) -> None:
    widget.setProperty("tooltip_text", text)
    widget.setToolTip(text if enabled else "")


def apply_tooltips(root: QWidget, enabled: bool) -> None:
    for widget in [root, *root.findChildren(QWidget)]:
        text = widget.property("tooltip_text")
        if text:
            widget.setToolTip(str(text) if enabled else "")


class SingleInstanceGuard(QObject):
    """Allow only one running copy and ask the first copy to show itself.

    Uses Qt local IPC (named pipe on Windows / local socket elsewhere), so no
    extra package or administrator rights are required.
    """

    activation_requested = Signal()

    def __init__(self, server_name: str = "TiHiY_Voice_Translator_single_instance", parent: QObject | None = None):
        super().__init__(parent)
        self.server_name = server_name
        self.server = QLocalServer(self)
        self.server.newConnection.connect(self._accept_connections)
        self.is_primary = False
        self._pending_activation = False

    def acquire(self) -> bool:
        """Return True for the first instance, False for every later launch."""
        if self.is_primary:
            return True

        # First try the already running copy.
        if self._notify_existing_instance():
            return False

        # No active server was reachable. Try to become the primary instance.
        if self.server.listen(self.server_name):
            self.is_primary = True
            return True

        # A second process may have won the race between probe and listen.
        if self._notify_existing_instance():
            return False

        # On Unix a crashed process can leave a stale socket path. Removing it
        # is safe only after two connection attempts failed.
        QLocalServer.removeServer(self.server_name)
        if self.server.listen(self.server_name):
            self.is_primary = True
            return True

        # Fail closed: never start a duplicate if ownership is ambiguous.
        return False

    def _notify_existing_instance(self) -> bool:
        socket = QLocalSocket(self)
        socket.connectToServer(self.server_name)
        connected = socket.waitForConnected(350)
        if not connected:
            socket.abort()
            socket.deleteLater()
            return False
        socket.write(b"SHOW")
        socket.flush()
        socket.waitForBytesWritten(200)
        socket.disconnectFromServer()
        socket.deleteLater()
        return True

    def _accept_connections(self) -> None:
        accepted = False
        while self.server.hasPendingConnections():
            socket = self.server.nextPendingConnection()
            if socket is not None:
                accepted = True
                socket.readyRead.connect(socket.readAll)
                socket.disconnected.connect(socket.deleteLater)
                QTimer.singleShot(250, socket.disconnectFromServer)
        if accepted:
            self._pending_activation = True
            self.activation_requested.emit()

    def consume_pending_activation(self) -> bool:
        pending = self._pending_activation
        self._pending_activation = False
        return pending

    def close(self) -> None:
        if self.server.isListening():
            self.server.close()
        if self.is_primary:
            QLocalServer.removeServer(self.server_name)
        self.is_primary = False


class Bridge(QObject):
    log = Signal(str)
    transcript = Signal(str)
    translation = Signal(str)
    ptt_pressed = Signal()
    ptt_released = Signal()
    ptt_toggle = Signal()
    processing_done = Signal()
    processing_failed = Signal(str)
    level = Signal(float)


class HudPanel(QFrame):
    def __init__(
        self,
        title: str = "",
        accent: str = C.CYAN,
        title_color: str | None = None,
        parent: QWidget | None = None,
    ):
        super().__init__(parent)
        self.accent = QColor(accent)
        self.title_color = title_color or accent
        self.setAttribute(Qt.WA_StyledBackground, True)
        self.setStyleSheet("background:transparent;border:none;")
        self.outer = QVBoxLayout(self)
        self.outer.setContentsMargins(20, 14, 20, 18)
        self.outer.setSpacing(8)
        if title:
            title_row = QHBoxLayout()
            wave = QLabel("≋", self)
            wave.setStyleSheet(f"color:{self.title_color};font-size:22px;font-weight:700;background:transparent;")
            self.title_label = QLabel(title, self)
            self.title_label.setStyleSheet(f"color:{self.title_color};font-size:19px;font-weight:700;background:transparent;letter-spacing:1px;")
            title_row.addWidget(wave)
            title_row.addWidget(self.title_label)
            title_row.addStretch(1)
            self.outer.addLayout(title_row)
        else:
            self.title_label = None
        self.content = QVBoxLayout()
        self.content.setContentsMargins(0, 0, 0, 0)
        self.content.setSpacing(10)
        self.outer.addLayout(self.content, 1)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = QRectF(self.rect()).adjusted(1.5, 1.5, -1.5, -1.5)
        path = _octagon(rect, 13)
        gradient = QLinearGradient(rect.topLeft(), rect.bottomRight())
        gradient.setColorAt(0.0, QColor(5, 20, 34, 248))
        gradient.setColorAt(0.45, QColor(2, 10, 18, 247))
        gradient.setColorAt(1.0, QColor(1, 7, 13, 248))
        painter.fillPath(path, gradient)

        glow = QRadialGradient(QPointF(rect.left() + rect.width() * .18, rect.top() + rect.height() * .18), rect.width() * .7)
        glow.setColorAt(0.0, QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 35))
        glow.setColorAt(1.0, QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 0))
        painter.fillPath(path, glow)

        painter.setClipPath(path)
        painter.setPen(QPen(QColor(18, 72, 103, 34), 1))
        step = 30
        for x in range(0, self.width(), step):
            painter.drawLine(x, 0, x, self.height())
        for y in range(0, self.height(), step):
            painter.drawLine(0, y, self.width(), y)
        painter.setClipping(False)

        painter.setPen(QPen(QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 205), 1.7))
        painter.drawPath(path)
        painter.setPen(QPen(QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 70), 1))
        painter.drawPath(_octagon(rect.adjusted(7, 7, -7, -7), 9))

        # Bright segmented rails and small corner details.
        painter.setPen(QPen(self.accent, 2.1))
        top_end = int(rect.left() + min(250, rect.width() * .38))
        painter.drawLine(int(rect.left() + 34), int(rect.top()), top_end, int(rect.top()))
        painter.drawLine(int(rect.right() - min(175, rect.width() * .28)), int(rect.bottom()), int(rect.right() - 34), int(rect.bottom()))
        painter.setPen(QPen(QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 115), 1.0))
        painter.drawLine(int(rect.left() + 17), int(rect.top() + 24), int(rect.left() + 17), int(rect.top() + 64))
        painter.drawLine(int(rect.right() - 17), int(rect.bottom() - 64), int(rect.right() - 17), int(rect.bottom() - 24))
        super().paintEvent(event)


class TitleBar(QFrame):
    def __init__(self, window: "FramelessWindow", title: str, icon: Path | None = None):
        super().__init__(window)
        self.window = window
        self.drag_pos: QPoint | None = None
        self.setFixedHeight(58)
        self.setStyleSheet("background:transparent;border:none;")
        layout = QHBoxLayout(self)
        layout.setContentsMargins(16, 4, 10, 4)
        layout.setSpacing(10)
        if icon and icon.exists():
            icon_label = QLabel(self)
            pix = QPixmap(str(icon)).scaled(38, 38, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            icon_label.setPixmap(pix)
            icon_label.setFixedSize(40, 40)
            layout.addWidget(icon_label)
        title_label = QLabel(title, self)
        title_label.setStyleSheet(f"color:{C.TEXT};font-size:20px;background:transparent;border:none;")
        layout.addWidget(title_label)
        layout.addStretch(1)
        self.min_btn = self._button("—", "Згорнути", self.window.showMinimized)
        self.max_btn = self._button("□", "Розгорнути / відновити", self.window.toggle_maximize)
        self.close_btn = self._button("×", "Закрити", self.window.close, close=True)
        layout.addWidget(self.min_btn)
        layout.addWidget(self.max_btn)
        layout.addWidget(self.close_btn)

    def _button(self, text: str, tip: str, callback: Callable, close: bool = False) -> QToolButton:
        button = QToolButton(self)
        button.setText(text)
        button.setFixedSize(46, 42)
        button.setCursor(Qt.PointingHandCursor)
        hover = C.RED if close else C.LINE_HI
        button.setStyleSheet(
            f"QToolButton{{color:{C.TEXT};font-size:23px;background:transparent;border:none;border-radius:5px;}}"
            f"QToolButton:hover{{background:{hover};}}"
        )
        set_tip(button, tip)
        button.clicked.connect(callback)
        return button

    def paintEvent(self, event):
        painter = QPainter(self)
        rect = QRectF(self.rect())
        grad = QLinearGradient(rect.topLeft(), rect.topRight())
        grad.setColorAt(0.0, QColor(2, 11, 20, 252))
        grad.setColorAt(0.55, QColor(1, 7, 13, 252))
        grad.setColorAt(1.0, QColor(3, 12, 20, 252))
        painter.fillRect(rect, grad)
        painter.setPen(QPen(QColor(0, 132, 190, 70), 1))
        painter.drawLine(0, self.height() - 1, self.width() * .72, self.height() - 1)
        painter.setPen(QPen(QColor(255, 143, 0, 65), 1))
        painter.drawLine(self.width() * .72, self.height() - 1, self.width(), self.height() - 1)
        painter.setPen(QPen(QColor(0, 170, 235, 45), 1))
        painter.drawLine(17, 8, 150, 8)
        super().paintEvent(event)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.drag_pos = event.globalPosition().toPoint() - self.window.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event):
        if self.drag_pos is not None and event.buttons() & Qt.LeftButton and not self.window.isMaximized():
            self.window.move(event.globalPosition().toPoint() - self.drag_pos)
            event.accept()

    def mouseReleaseEvent(self, event):
        self.drag_pos = None
        super().mouseReleaseEvent(event)

    def mouseDoubleClickEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.window.toggle_maximize()
            event.accept()


class FramelessWindow(QMainWindow):
    def __init__(self, title: str):
        super().__init__()
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, False)
        self.setWindowTitle(title)
        self.root = TechBackdrop(self)
        self.root.setObjectName("root")
        self.root.setStyleSheet(f"QWidget#root{{background:transparent;border:1px solid {C.LINE};}}")
        self.root_layout = QVBoxLayout(self.root)
        self.root_layout.setContentsMargins(0, 0, 0, 0)
        self.root_layout.setSpacing(0)
        self.setCentralWidget(self.root)
        self.titlebar = TitleBar(self, title, ASSETS / "avatar.png")
        self.root_layout.addWidget(self.titlebar)

    def toggle_maximize(self):
        if self.isMaximized():
            self.showNormal()
            self.titlebar.max_btn.setText("□")
        else:
            self.showMaximized()
            self.titlebar.max_btn.setText("❐")

    def center_window(self):
        screen = self.screen().availableGeometry() if self.screen() else QApplication.primaryScreen().availableGeometry()
        self.move(screen.center() - self.rect().center())


class ToggleSwitch(QAbstractButton):
    def __init__(self, checked: bool = False, parent: QWidget | None = None):
        super().__init__(parent)
        self.setCheckable(True)
        self.setChecked(checked)
        self.setCursor(Qt.PointingHandCursor)
        self.setFixedSize(54, 29)
        self._position = 1.0 if checked else 0.0
        self._animation = QPropertyAnimation(self, b"position", self)
        self._animation.setDuration(140)
        self._animation.setEasingCurve(QEasingCurve.OutCubic)
        self.toggled.connect(self._animate)

    def _animate(self, checked: bool):
        self._animation.stop()
        self._animation.setStartValue(self._position)
        self._animation.setEndValue(1.0 if checked else 0.0)
        self._animation.start()

    def get_position(self) -> float:
        return self._position

    def set_position(self, value: float):
        self._position = value
        self.update()

    position = Property(float, get_position, set_position)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        track = QRectF(1, 4, self.width() - 2, self.height() - 8)
        painter.setPen(QPen(QColor("#4d6473"), 1))
        painter.setBrush(QColor("#148b1a") if self.isChecked() else QColor("#17232c"))
        painter.drawRoundedRect(track, track.height() / 2, track.height() / 2)
        diameter = self.height() - 8
        x = 2 + self._position * (self.width() - diameter - 4)
        painter.setPen(Qt.NoPen)
        painter.setBrush(QColor("#f3f6f7"))
        painter.drawEllipse(QRectF(x, 4, diameter, diameter))


class LevelMeter(QWidget):
    def __init__(self, parent: QWidget | None = None):
        super().__init__(parent)
        self._level = 0.0
        self.setMinimumSize(90, 18)
        self.setMaximumHeight(20)

    def set_level(self, level: float):
        self._level = max(0.0, min(1.0, float(level)))
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        segments = 12
        gap = 3
        width = max(2, (self.width() - gap * (segments - 1)) / segments)
        for i in range(segments):
            active = i < int(round(self._level * segments))
            if active:
                color = C.GREEN if i < 9 else C.ORANGE
            else:
                color = "#28343c"
            painter.fillRect(QRectF(i * (width + gap), 2, width, self.height() - 4), QColor(color))


class Waveform(QWidget):
    def __init__(self, accent: str, parent: QWidget | None = None):
        super().__init__(parent)
        self.accent = QColor(accent)
        self.setFixedHeight(34)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        mid = self.height() / 2
        painter.setPen(QPen(QColor(self.accent.red(), self.accent.green(), self.accent.blue(), 170), 1.2))
        count = 70
        step = self.width() / max(1, count - 1)
        for i in range(count):
            amp = (3 + ((i * 17) % 13)) if i % 9 in (0, 1) else 2
            x = i * step
            painter.drawLine(QPointF(x, mid - amp), QPointF(x, mid + amp))


class StatusPill(QFrame):
    def __init__(self, icon: str, text: str, accent: str = C.CYAN, parent: QWidget | None = None):
        super().__init__(parent)
        self.accent = accent
        layout = QHBoxLayout(self)
        layout.setContentsMargins(12, 5, 12, 5)
        layout.setSpacing(8)
        self.icon_label = QLabel(icon)
        self.icon_label.setStyleSheet(f"color:{accent};font-size:21px;background:transparent;")
        self.label = QLabel(text)
        self.label.setStyleSheet(f"color:{C.TEXT};font-size:14px;background:transparent;")
        self.dot = QLabel("●")
        self.dot.setStyleSheet(f"color:{C.GREEN};font-size:13px;background:transparent;")
        layout.addWidget(self.icon_label)
        layout.addWidget(self.label, 1)
        layout.addWidget(self.dot)
        self.setStyleSheet(f"QFrame{{background:#06121d;border:1px solid {C.LINE};border-radius:7px;}}")

    def set_text(self, text: str):
        self.label.setText(text)

    def set_ok(self, ok: bool):
        self.dot.setStyleSheet(f"color:{C.GREEN if ok else C.ORANGE};font-size:13px;background:transparent;")


class KeyCaptureDialog(QDialog):
    def __init__(self, current: str, parent: QWidget | None = None):
        super().__init__(parent)
        self.key_name = current or "F8"
        self.setWindowTitle("Клавіша PTT")
        self.setModal(True)
        self.setFixedSize(410, 190)
        self.setStyleSheet(f"background:{C.BG};color:{C.TEXT};")
        layout = QVBoxLayout(self)
        label = QLabel("Натисніть одну клавішу для PTT")
        label.setStyleSheet("font-size:17px;font-weight:600;")
        layout.addWidget(label)
        self.value = QLabel(self.key_name)
        self.value.setAlignment(Qt.AlignCenter)
        self.value.setStyleSheet(f"background:{C.BG2};border:1px solid {C.ORANGE};border-radius:7px;font-size:25px;padding:12px;")
        layout.addWidget(self.value)
        row = QHBoxLayout()
        cancel = QPushButton("Скасувати")
        save = QPushButton("Застосувати")
        for button in (cancel, save):
            button.setCursor(Qt.PointingHandCursor)
            button.setStyleSheet(button_style())
        save.setStyleSheet(button_style(C.ORANGE))
        cancel.clicked.connect(self.reject)
        save.clicked.connect(self.accept)
        row.addStretch(1)
        row.addWidget(cancel)
        row.addWidget(save)
        layout.addLayout(row)

    def keyPressEvent(self, event: QKeyEvent):
        if event.isAutoRepeat():
            return
        key = event.key()
        if Qt.Key_F1 <= key <= Qt.Key_F35:
            name = f"F{key - Qt.Key_F1 + 1}"
        elif key == Qt.Key_Space:
            name = "SPACE"
        elif key == Qt.Key_Return or key == Qt.Key_Enter:
            name = "ENTER"
        elif key == Qt.Key_Tab:
            name = "TAB"
        elif key == Qt.Key_Escape:
            name = "ESC"
        else:
            text = event.text().strip()
            name = text.upper() if text else event.keyCombination().toCombined().__str__()
        if name:
            self.key_name = name
            self.value.setText(name)


def button_style(accent: str = C.LINE_HI) -> str:
    is_orange = accent == C.ORANGE
    edge = C.ORANGE if is_orange else C.LINE_HI
    hover = "#ffc05a" if is_orange else "#55d4ff"
    top = "#6d3b00" if is_orange else "#0a2940"
    bottom = "#211303" if is_orange else "#04131f"
    return (
        "QPushButton,QToolButton{"
        f"color:{C.TEXT};border:1px solid {edge};border-radius:6px;"
        f"background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 {top},stop:.48 #071521,stop:1 {bottom});"
        "padding:8px 14px;font-size:14px;}"
        f"QPushButton:hover,QToolButton:hover{{border:2px solid {hover};"
        f"background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 {top},stop:1 #0a2232);}}"
        "QPushButton:pressed,QToolButton:pressed{background:#020a11;padding-top:10px;}"
        "QPushButton:disabled{color:#63717a;border-color:#263642;background:#06101a;}"
    )


def combo_style() -> str:
    return (
        f"QComboBox{{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #0a1d2a,stop:1 #04101a);"
        f"color:{C.TEXT};border:1px solid #38576a;border-radius:6px;"
        "padding:8px 34px 8px 12px;font-size:14px;min-height:20px;}"
        f"QComboBox:hover,QComboBox:focus{{border:1px solid {C.CYAN};background:#081d2b;}}"
        "QComboBox::drop-down{border-left:1px solid #203d4d;width:30px;}"
        "QComboBox::down-arrow{image:none;border-left:5px solid transparent;border-right:5px solid transparent;"
        "border-top:6px solid #c9d7de;margin-right:8px;}"
        f"QComboBox QAbstractItemView{{background:#04111c;color:{C.TEXT};border:1px solid {C.LINE_HI};"
        "selection-background-color:#0a5276;outline:none;padding:5px;}"
    )


def line_style() -> str:
    return (
        f"QLineEdit{{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #091b28,stop:1 #030d16);"
        f"color:{C.TEXT};border:1px solid #38576a;border-radius:6px;padding:9px 11px;font-size:14px;}}"
        f"QLineEdit:hover,QLineEdit:focus{{border:1px solid {C.CYAN};}}"
    )


def checkbox_style() -> str:
    return (
        f"QCheckBox{{color:{C.TEXT};background:transparent;font-size:15px;spacing:10px;}}"
        "QCheckBox::indicator{width:20px;height:20px;border:1px solid #547184;border-radius:3px;background:#04111b;}"
        f"QCheckBox::indicator:hover{{border:1px solid {C.CYAN};background:#09263a;}}"
        f"QCheckBox::indicator:checked{{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #31c8ff,stop:1 #0877b1);border:1px solid #82e4ff;}}"
    )


def text_edit_style(accent: str, font_size: int = 14) -> str:
    return (
        f"QTextEdit{{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 rgba(2,13,23,225),stop:1 rgba(1,7,13,238));"
        f"color:{C.TEXT};border:none;font-size:{font_size}px;padding:10px;selection-background-color:{accent};}}"
        "QScrollBar:vertical{background:#03101a;width:9px;margin:2px;}"
        "QScrollBar::handle:vertical{background:#2b5269;border:1px solid #39708f;border-radius:4px;min-height:25px;}"
        "QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical{height:0;}"
    )


class HeaderWidget(QFrame):
    def __init__(self, parent: QWidget | None = None):
        super().__init__(parent)
        self.setMinimumHeight(154)
        self.setMaximumHeight(218)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.setStyleSheet("background:transparent;border:none;")
        layout = QHBoxLayout(self)
        self.layout_ref = layout
        layout.setContentsMargins(12, 8, 12, 8)
        layout.setSpacing(14)

        self.left = QLabel()
        self.left.setAlignment(Qt.AlignCenter)
        self.left.setMinimumWidth(220)
        self.left.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.left.setPixmap(QPixmap(str(ASSETS / "hero_character.png")))
        self.left.setScaledContents(True)
        layout.addWidget(self.left, 3)

        center = QVBoxLayout()
        self.center_layout = center
        center.addStretch(1)
        title_row = QHBoxLayout()
        self.tihiy_label = QLabel("TiHiY")
        self.voice_label = QLabel("VOICE TRANSLATOR")
        title_row.addStretch(1)
        title_row.addWidget(self.tihiy_label)
        title_row.addSpacing(10)
        title_row.addWidget(self.voice_label)
        title_row.addStretch(1)
        center.addLayout(title_row)
        self.subtitle_label = QLabel("Розумний голосовий перекладач у реальному часі")
        self.subtitle_label.setAlignment(Qt.AlignCenter)
        center.addWidget(self.subtitle_label)
        center.addStretch(1)
        layout.addLayout(center, 6)

        self.right = QLabel()
        self.right.setAlignment(Qt.AlignCenter)
        self.right.setMinimumWidth(220)
        self.right.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.right.setPixmap(QPixmap(str(ASSETS / "hero_ship.png")))
        self.right.setScaledContents(True)
        layout.addWidget(self.right, 3)
        self.set_compact(False)

    def set_compact(self, compact: bool, ultra_compact: bool = False):
        if ultra_compact:
            height = 122
            image_min = 155
            title_size = 27
            subtitle_size = 13
            margins = (8, 5, 8, 5)
            spacing = 8
        elif compact:
            height = 150
            image_min = 190
            title_size = 33
            subtitle_size = 15
            margins = (10, 6, 10, 6)
            spacing = 10
        else:
            height = 202
            image_min = 280
            title_size = 43
            subtitle_size = 18
            margins = (12, 8, 12, 8)
            spacing = 14
        self.setMinimumHeight(height)
        self.setMaximumHeight(height + (10 if not ultra_compact else 0))
        self.left.setMinimumWidth(image_min)
        self.right.setMinimumWidth(image_min)
        self.layout_ref.setContentsMargins(*margins)
        self.layout_ref.setSpacing(spacing)
        self.tihiy_label.setStyleSheet(
            f"color:{C.TEXT};font-size:{title_size}px;font-weight:800;background:transparent;"
        )
        self.voice_label.setStyleSheet(
            f"color:{C.ORANGE};font-size:{title_size}px;font-weight:800;background:transparent;"
        )
        self.subtitle_label.setStyleSheet(
            f"color:{C.MUTED};font-size:{subtitle_size}px;background:transparent;"
        )

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = QRectF(self.rect()).adjusted(1, 1, -1, -1)
        path = _octagon(rect, 13)
        gradient = QLinearGradient(rect.topLeft(), rect.topRight())
        gradient.setColorAt(0, QColor("#071a29"))
        gradient.setColorAt(.35, QColor("#020b14"))
        gradient.setColorAt(.68, QColor("#020b14"))
        gradient.setColorAt(1, QColor("#071725"))
        painter.fillPath(path, gradient)
        cyan_glow = QRadialGradient(QPointF(rect.left() + rect.width() * .14, rect.center().y()), rect.width() * .32)
        cyan_glow.setColorAt(0, QColor(0, 155, 231, 45))
        cyan_glow.setColorAt(1, QColor(0, 155, 231, 0))
        painter.fillPath(path, cyan_glow)
        orange_glow = QRadialGradient(QPointF(rect.right() - rect.width() * .12, rect.center().y()), rect.width() * .27)
        orange_glow.setColorAt(0, QColor(255, 137, 0, 30))
        orange_glow.setColorAt(1, QColor(255, 137, 0, 0))
        painter.fillPath(path, orange_glow)
        painter.setPen(QPen(QColor(C.LINE_HI), 1.25))
        painter.drawPath(path)
        painter.setPen(QPen(QColor(0, 183, 255, 110), 1))
        painter.drawLine(int(rect.left()+28), int(rect.top()+9), int(rect.left()+rect.width()*.42), int(rect.top()+9))
        painter.setPen(QPen(QColor(255, 145, 0, 90), 1))
        painter.drawLine(int(rect.right()-rect.width()*.27), int(rect.bottom()-9), int(rect.right()-28), int(rect.bottom()-9))
        for i in range(36):
            x = int(rect.left() + 26 + ((i * 131) % max(1, int(rect.width()-52))))
            y = int(rect.top() + 18 + ((i * 47) % max(1, int(rect.height()-36))))
            painter.fillRect(x, y, 1, 1, QColor(71, 174, 232, 75))
        super().paintEvent(event)


class MainWindow(FramelessWindow):
    def __init__(self, start_runtime: bool = True):
        super().__init__("TiHiY Voice Translator")
        self.config = AppConfig.load()
        self.bridge = Bridge()
        self.audio = DirectVoiceEngine(
            status_cb=lambda message: self._runtime_status(message),
            level_cb=lambda value: self.bridge.level.emit(value),
        )
        self.recorder: PhraseRecorder | None = None
        self.ptt: GlobalPTT | None = None
        self.processing = False
        self.paused = False
        self._toggle_recording_active = False
        self._runtime_started = False
        self._allow_log = True
        self._devices: list[dict] = []
        screen = QApplication.primaryScreen().availableGeometry()
        available_width = max(900, screen.width() - 24)
        available_height = max(600, screen.height() - 24)
        target_width = min(1600, available_width)
        target_height = min(900, available_height)
        minimum_width = min(980, available_width)
        minimum_height = min(620, available_height)
        self.resize(target_width, target_height)
        self.setMinimumSize(minimum_width, minimum_height)
        self._main_compact_state: tuple[bool, bool] | None = None
        self._build_ui()
        self._apply_main_responsive(force=True)
        self.settings = SettingsWindow(self.config, self)
        self.settings.settings_saved.connect(self.apply_settings)
        self._connect_signals()
        self._build_tray()
        self.refresh_devices()
        self._load_main_controls()
        self.clear_workspaces()
        apply_tooltips(self, self.config.show_tooltips)
        QTimer.singleShot(0, self._fit_main_to_screen)
        if start_runtime:
            QTimer.singleShot(0, lambda: self.start_runtime(silent=True))
        if self.config.auto_update and self.config.update_manifest_url.strip():
            QTimer.singleShot(2500, lambda: self.settings.check_updates(silent=True))

    def _fit_main_to_screen(self):
        screen = self.screen().availableGeometry() if self.screen() else QApplication.primaryScreen().availableGeometry()
        maximum_width = max(900, screen.width() - 24)
        maximum_height = max(580, screen.height() - 24)
        new_width = min(self.width(), maximum_width)
        new_height = min(self.height(), maximum_height)
        if new_width != self.width() or new_height != self.height():
            self.resize(new_width, new_height)
        self.center_window()

    def _build_ui(self):
        body = QWidget(self.root)
        body.setAttribute(Qt.WA_StyledBackground, False)
        body_layout = QVBoxLayout(body)
        self.main_body_layout = body_layout
        body_layout.setContentsMargins(10, 8, 10, 8)
        body_layout.setSpacing(8)
        self.root_layout.addWidget(body, 1)

        self.header = HeaderWidget()
        body_layout.addWidget(self.header)

        control_row = QHBoxLayout()
        self.control_row_layout = control_row
        control_row.setSpacing(10)
        self.pause_button = QPushButton("Ⅱ  ПАУЗА")
        self.pause_button.setCursor(Qt.PointingHandCursor)
        self.pause_button.setMinimumWidth(170)
        self.pause_button.setStyleSheet(button_style(C.CYAN))
        set_tip(self.pause_button, "Призупинити або відновити роботу")
        self.pause_button.clicked.connect(self.toggle_pause)
        control_row.addWidget(self.pause_button, 2)

        self.mode_combo = QComboBox()
        self.mode_combo.addItem("РЕЖИМ: ПРЯМИЙ ГОЛОС + PTT", "direct_ptt")
        self.mode_combo.addItem("РЕЖИМ: ЛИШЕ PTT-ПЕРЕКЛАД", "ptt_only")
        self.mode_combo.setStyleSheet(combo_style())
        set_tip(self.mode_combo, "Режим роботи програми")
        self.mode_combo.activated.connect(self._main_mode_changed)
        control_row.addWidget(self.mode_combo, 4)

        self.main_mic_combo = QComboBox()
        self.main_mic_combo.setStyleSheet(combo_style())
        set_tip(self.main_mic_combo, "Виберіть мікрофон")
        self.main_mic_combo.activated.connect(self._main_device_changed)
        control_row.addWidget(self.main_mic_combo, 4)

        self.main_output_combo = QComboBox()
        self.main_output_combo.setStyleSheet(combo_style())
        set_tip(self.main_output_combo, "Виберіть CABLE Input / VB-CABLE")
        self.main_output_combo.activated.connect(self._main_device_changed)
        control_row.addWidget(self.main_output_combo, 4)

        self.ptt_top_button = QPushButton("PTT: F8")
        self.ptt_top_button.setCursor(Qt.PointingHandCursor)
        self.ptt_top_button.setMinimumWidth(170)
        self.ptt_top_button.setStyleSheet(button_style(C.ORANGE))
        set_tip(self.ptt_top_button, "Змінити клавішу PTT у налаштуваннях")
        self.ptt_top_button.clicked.connect(self.open_settings_ptt)
        control_row.addWidget(self.ptt_top_button, 2)
        body_layout.addLayout(control_row)

        panels_row = QHBoxLayout()
        panels_row.setSpacing(12)
        self.said_panel = HudPanel("СКАЗАНО", C.CYAN)
        self.said = QTextEdit()
        self.said.setReadOnly(True)
        self.said.setPlaceholderText("")
        self.said.setStyleSheet(text_edit_style(C.CYAN, 14))
        self.said_panel.content.addWidget(self.said, 1)
        self.said_panel.content.addWidget(Waveform(C.CYAN))
        panels_row.addWidget(self.said_panel, 1)

        self.swap_button = QPushButton("⇄")
        self.swap_button.setFixedSize(64, 64)
        self.swap_button.setCursor(Qt.PointingHandCursor)
        self.swap_button.setStyleSheet(button_style(C.ORANGE) + "QPushButton{font-size:26px;}")
        set_tip(self.swap_button, "Поміняти мови місцями")
        self.swap_button.clicked.connect(self.swap_languages)
        swap_wrap = QVBoxLayout()
        swap_wrap.addStretch(1)
        swap_wrap.addWidget(self.swap_button)
        swap_wrap.addStretch(1)
        panels_row.addLayout(swap_wrap)

        self.translation_panel = HudPanel("ПЕРЕКЛАД", C.ORANGE)
        self.translated = QTextEdit()
        self.translated.setReadOnly(True)
        self.translated.setPlaceholderText("")
        self.translated.setStyleSheet(text_edit_style(C.ORANGE, 14))
        self.translation_panel.content.addWidget(self.translated, 1)
        self.translation_panel.content.addWidget(Waveform(C.ORANGE))
        panels_row.addWidget(self.translation_panel, 1)
        self.panels_row_layout = panels_row
        body_layout.addLayout(panels_row, 9)

        action_row = QHBoxLayout()
        action_row.addStretch(2)
        actions = QVBoxLayout()
        actions.setSpacing(8)
        self.ptt_button = QPushButton("🎙  УТРИМУЙТЕ F8, ЩОБ ГОВОРИТИ")
        self.ptt_button.setMinimumHeight(60)
        self.ptt_button.setCursor(Qt.PointingHandCursor)
        self.ptt_button.setStyleSheet(
            f"QPushButton{{color:#ffe3ae;font-size:22px;font-weight:800;"
            f"background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #a05a00,stop:.48 #6b3900,stop:1 #2c1700);"
            f"border:2px solid {C.ORANGE};border-radius:8px;padding:10px;}}"
            "QPushButton:hover{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #c46d00,stop:1 #492600);"
            "border:2px solid #ffd17a;}"
            "QPushButton:pressed{background:#2c1500;padding-top:13px;}"
        )
        set_tip(self.ptt_button, "Утримуйте кнопку або глобальну клавішу PTT")
        self.ptt_button.pressed.connect(self._mouse_ptt_pressed)
        self.ptt_button.released.connect(self._mouse_ptt_released)
        actions.addWidget(self.ptt_button)
        self.settings_button = QPushButton("⚙  ВІДКРИТИ НАЛАШТУВАННЯ")
        self.settings_button.setMinimumHeight(46)
        self.settings_button.setCursor(Qt.PointingHandCursor)
        self.settings_button.setStyleSheet(button_style())
        set_tip(self.settings_button, "Відкрити налаштування програми")
        self.settings_button.clicked.connect(self.open_settings)
        actions.addWidget(self.settings_button)
        action_row.addLayout(actions, 7)
        action_row.addStretch(2)
        body_layout.addLayout(action_row)

        self.log_panel = HudPanel("ЖУРНАЛ ПОДІЙ", C.LINE_HI)
        log_row = QHBoxLayout()
        log_row.setSpacing(10)
        self.log_view = QTextEdit()
        self.log_view.setReadOnly(True)
        self.log_view.setPlaceholderText("")
        self.log_view.setStyleSheet(text_edit_style(C.LINE_HI, 12))
        log_row.addWidget(self.log_view, 1)
        self.clear_button = QPushButton("ОЧИСТИТИ")
        self.clear_button.setCursor(Qt.PointingHandCursor)
        self.clear_button.setMinimumWidth(96)
        self.clear_button.setMaximumWidth(118)
        self.clear_button.setMinimumHeight(34)
        self.clear_button.setMaximumHeight(38)
        self.clear_button.setStyleSheet(button_style())
        self.clear_button.clicked.connect(self.log_view_clear)
        set_tip(self.clear_button, "Очистити журнал")
        log_row.addWidget(self.clear_button, 0, Qt.AlignTop)
        self.log_panel.content.addLayout(log_row, 1)
        self.log_panel.setMinimumHeight(96)
        self.log_panel.setMaximumHeight(132)
        body_layout.addWidget(self.log_panel, 0)

        bottom = QHBoxLayout()
        self.status_dot = QLabel("●")
        self.status_dot.setStyleSheet(f"color:{C.GREEN};font-size:16px;")
        self.status_text = QLabel("Готово до роботи")
        self.status_text.setStyleSheet(f"color:{C.GREEN};font-size:14px;")
        bottom.addWidget(self.status_dot)
        bottom.addWidget(self.status_text)
        bottom.addStretch(1)
        for name, file, url in (
            ("YouTube", "youtube.svg", "https://www.youtube.com/@TiHiY-DED"),
            ("Twitch", "twitch.svg", "https://www.twitch.tv/tihiy_ded"),
            ("Discord", "discord.svg", "https://discord.gg/cFsj4ffvzN"),
            ("Подякувати", "donate.svg", "https://donatello.to/TiHiY-DED"),
        ):
            button = QToolButton()
            button.setIcon(QIcon(str(ASSETS / "social" / file)))
            button.setIconSize(QSize(28, 28))
            button.setFixedSize(52, 44)
            button.setCursor(Qt.PointingHandCursor)
            button.setStyleSheet(button_style())
            set_tip(button, name)
            button.clicked.connect(lambda checked=False, link=url: QDesktopServices.openUrl(QUrl(link)))
            bottom.addWidget(button)
        version = QLabel(f"v{VERSION}")
        version.setStyleSheet(f"color:{C.MUTED};font-size:13px;margin-left:12px;")
        bottom.addWidget(version)
        grip = QSizeGrip(self)
        grip.setFixedSize(18, 18)
        bottom.addWidget(grip)
        body_layout.addLayout(bottom)

    def _apply_main_responsive(self, force: bool = False):
        compact = self.height() < 780 or self.width() < 1320
        ultra = self.height() < 690 or self.width() < 1080
        state = (compact, ultra)
        if not force and state == self._main_compact_state:
            return
        self._main_compact_state = state

        self.header.set_compact(compact, ultra)
        self.titlebar.setFixedHeight(50 if compact else 58)
        margins = (7, 5, 7, 5) if ultra else ((8, 6, 8, 6) if compact else (10, 8, 10, 8))
        self.main_body_layout.setContentsMargins(*margins)
        self.main_body_layout.setSpacing(6 if compact else 8)
        self.control_row_layout.setSpacing(6 if compact else 10)

        if ultra:
            log_min, log_max = 82, 98
            text_size = 12
            ptt_height, ptt_font = 46, 17
            settings_height = 38
        elif compact:
            log_min, log_max = 92, 112
            text_size = 13
            ptt_height, ptt_font = 50, 19
            settings_height = 40
        else:
            log_min, log_max = 100, 132
            text_size = 14
            ptt_height, ptt_font = 60, 22
            settings_height = 46

        self.log_panel.setMinimumHeight(log_min)
        self.log_panel.setMaximumHeight(log_max)
        self.said.setStyleSheet(text_edit_style(C.CYAN, text_size))
        self.translated.setStyleSheet(text_edit_style(C.ORANGE, text_size))
        self.log_view.setStyleSheet(text_edit_style(C.LINE_HI, 11 if compact else 12))
        self.ptt_button.setMinimumHeight(ptt_height)
        self.ptt_button.setMaximumHeight(ptt_height + 4)
        self.ptt_button.setStyleSheet(
            f"QPushButton{{color:#ffe3ae;font-size:{ptt_font}px;font-weight:800;"
            f"background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #a05a00,stop:.48 #6b3900,stop:1 #2c1700);"
            f"border:2px solid {C.ORANGE};border-radius:8px;padding:8px;}}"
            "QPushButton:hover{background:qlineargradient(x1:0,y1:0,x2:0,y2:1,stop:0 #c46d00,stop:1 #492600);"
            "border:2px solid #ffd17a;}"
            "QPushButton:pressed{background:#2c1500;padding-top:11px;}"
        )
        self.settings_button.setMinimumHeight(settings_height)
        self.settings_button.setMaximumHeight(settings_height + 4)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        if hasattr(self, "header"):
            self._apply_main_responsive()

    def _connect_signals(self):
        self.bridge.log.connect(self.append_log)
        self.bridge.transcript.connect(self.said.setPlainText)
        self.bridge.translation.connect(self.translated.setPlainText)
        self.bridge.ptt_pressed.connect(self.on_ptt_press)
        self.bridge.ptt_released.connect(self.on_ptt_release)
        self.bridge.ptt_toggle.connect(self.toggle_ptt_recording)
        self.bridge.processing_done.connect(self.on_processing_done)
        self.bridge.processing_failed.connect(self.on_processing_failed)
        self.bridge.level.connect(self.settings.set_input_level)

    def _build_tray(self):
        self.tray = QSystemTrayIcon(QIcon(str(ASSETS / "avatar.png")), self)
        menu = QMenu()
        show_action = QAction("Відкрити TiHiY Voice Translator", self)
        show_action.triggered.connect(self.show_and_raise)
        quit_action = QAction("Вихід", self)
        quit_action.triggered.connect(self.quit_application)
        menu.addAction(show_action)
        menu.addSeparator()
        menu.addAction(quit_action)
        self.tray.setContextMenu(menu)
        self.tray.activated.connect(lambda reason: self.show_and_raise() if reason == QSystemTrayIcon.Trigger else None)
        self.tray.show()

    def show_and_raise(self):
        if self.isMinimized():
            self.showNormal()
        else:
            self.show()
        self.raise_()
        self.activateWindow()
        # Windows sometimes refuses focus unless the active-window flag is
        # briefly toggled after a second process requests activation.
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)

    def clear_workspaces(self):
        self.said.clear()
        self.translated.clear()
        self.log_view.clear()

    def log_view_clear(self):
        self.log_view.clear()

    def append_log(self, text: str):
        if not self._allow_log:
            return
        self.log_view.append(text)

    def set_status(self, text: str, ok: bool = True):
        color = C.GREEN if ok else C.ORANGE
        self.status_dot.setStyleSheet(f"color:{color};font-size:16px;")
        self.status_text.setStyleSheet(f"color:{color};font-size:14px;")
        self.status_text.setText(text)

    def _runtime_status(self, message: str):
        if message.startswith("started"):
            self.set_status("Прямий голос активний", True)
        elif message == "stopped" and not self.paused:
            self.set_status("Готово до роботи", True)

    def refresh_devices(self):
        try:
            self._devices = list_devices()
        except Exception:
            self._devices = []

        inputs = [item for item in self._devices if item["inputs"] > 0]
        outputs = [item for item in self._devices if item["outputs"] > 0]
        input_indexes = {int(item["index"]) for item in inputs}
        output_indexes = {int(item["index"]) for item in outputs}
        changed = False

        if self.config.input_device not in input_indexes:
            recovered = find_device_by_name(
                self._devices, self.config.input_device_name, direction="input"
            )
            self.config.input_device = recovered if recovered >= 0 else choose_default_input(self._devices)
            changed = True

        if self.config.output_device not in output_indexes:
            recovered = find_device_by_name(
                self._devices, self.config.output_device_name, direction="output"
            )
            self.config.output_device = recovered if recovered >= 0 else choose_vb_cable_output(self._devices)
            changed = True

        resolved_input_name = device_name(self._devices, self.config.input_device)
        resolved_output_name = device_name(self._devices, self.config.output_device)
        if resolved_input_name != self.config.input_device_name:
            self.config.input_device_name = resolved_input_name
            changed = True
        if resolved_output_name != self.config.output_device_name:
            self.config.output_device_name = resolved_output_name
            changed = True
        if changed:
            self.config.save()

        self.main_mic_combo.blockSignals(True)
        self.main_output_combo.blockSignals(True)
        self.main_mic_combo.clear()
        self.main_output_combo.clear()
        if inputs:
            for item in inputs:
                self.main_mic_combo.addItem(f"🎙  {item['name']}", item["index"])
        else:
            self.main_mic_combo.addItem("🎙  МІКРОФОН НЕ ЗНАЙДЕНО", -1)
        if outputs:
            for item in outputs:
                self.main_output_combo.addItem(f"☁  {item['name']}", item["index"])
        else:
            self.main_output_combo.addItem("☁  VB-CABLE НЕ ЗНАЙДЕНО", -1)
        self._select_combo_data(self.main_mic_combo, self.config.input_device)
        self._select_combo_data(self.main_output_combo, self.config.output_device)
        self.main_mic_combo.blockSignals(False)
        self.main_output_combo.blockSignals(False)
        self.settings.load_devices(self._devices)

    @staticmethod
    def _select_combo_data(combo: QComboBox, value):
        index = combo.findData(value)
        if index >= 0:
            combo.setCurrentIndex(index)

    def _load_main_controls(self):
        suffix = "" if self.config.ptt_autostart else " (ГЛОБАЛЬНО ВИМК.)"
        self.ptt_top_button.setText(f"PTT: {self.config.ptt_key}{suffix}")
        if self.config.ptt_mode == "Перемикач":
            self.ptt_button.setText(f"🎙  НАТИСНІТЬ {self.config.ptt_key}, ЩОБ ПОЧАТИ / ЗАВЕРШИТИ")
        else:
            self.ptt_button.setText(f"🎙  УТРИМУЙТЕ {self.config.ptt_key}, ЩОБ ГОВОРИТИ")
        self.mode_combo.setCurrentIndex(0 if self.config.direct_voice_enabled else 1)

    def _main_mode_changed(self):
        self.config.direct_voice_enabled = self.mode_combo.currentData() == "direct_ptt"
        self.config.save()
        self.apply_settings()

    def _main_device_changed(self):
        self.config.input_device = int(self.main_mic_combo.currentData() or -1)
        self.config.output_device = int(self.main_output_combo.currentData() or -1)
        self.config.input_device_name = device_name(self._devices, self.config.input_device)
        self.config.output_device_name = device_name(self._devices, self.config.output_device)
        self.config.save()
        self.settings.load_from_config()
        self.apply_settings()

    def open_settings(self):
        self.settings.load_from_config()
        self.settings.show()
        self.settings.raise_()
        self.settings.activateWindow()

    def open_settings_ptt(self):
        self.open_settings()
        self.settings.scroll_to(self.settings.ptt_panel)

    def _global_ptt_pressed(self):
        if self.config.ptt_mode == "Перемикач":
            self.bridge.ptt_toggle.emit()
        else:
            self.bridge.ptt_pressed.emit()

    def _global_ptt_released(self):
        if self.config.ptt_mode != "Перемикач":
            self.bridge.ptt_released.emit()

    def _mouse_ptt_pressed(self):
        if self.config.ptt_mode == "Перемикач":
            self.bridge.ptt_toggle.emit()
        else:
            self.bridge.ptt_pressed.emit()

    def _mouse_ptt_released(self):
        if self.config.ptt_mode != "Перемикач":
            self.bridge.ptt_released.emit()

    def start_runtime(self, silent: bool = False):
        self.stop_runtime()
        ptt_ok = not self.config.ptt_autostart
        if self.config.ptt_autostart:
            try:
                self.ptt = GlobalPTT(
                    self.config.ptt_key,
                    self._global_ptt_pressed,
                    self._global_ptt_released,
                )
                self.ptt.start()
                ptt_ok = True
            except Exception as exc:
                if not silent:
                    self.append_log(f"PTT не запущено: {exc}")
        audio_ok = True
        if self.config.direct_voice_enabled and not self.paused:
            audio_ok = self.start_direct_voice(silent=silent)
        self._runtime_started = True
        devices_ok = self.config.input_device >= 0 and self.config.output_device >= 0
        if not devices_ok:
            self.set_status("Виберіть мікрофон і VB-CABLE", False)
        elif ptt_ok and audio_ok:
            self.set_status("Усі системи працюють у штатному режимі", True)
        else:
            self.set_status("Перевірте PTT або аудіопристрої", False)

    def start_direct_voice(self, silent: bool = False) -> bool:
        if self.config.input_device < 0 or self.config.output_device < 0:
            if not silent:
                self.append_log("Виберіть мікрофон і VB-CABLE у налаштуваннях.")
            return False
        try:
            self.audio.start(
                self.config.input_device,
                self.config.output_device,
                self.config.samplerate,
                self.config.output_mode,
            )
            if not silent:
                self.append_log("Прямий голос запущено через VB-CABLE.")
            return True
        except Exception as exc:
            if not silent:
                self.append_log(f"Не вдалося запустити прямий голос: {exc}")
            return False

    def stop_runtime(self):
        if self.recorder is not None:
            try:
                self.recorder.cancel()
            except Exception:
                pass
            self.recorder = None
        self._toggle_recording_active = False
        try:
            self.audio.stop()
        except Exception:
            pass
        if self.ptt:
            try:
                self.ptt.stop()
            except Exception:
                pass
            self.ptt = None

    def apply_settings(self):
        self.refresh_devices()
        self._load_main_controls()
        apply_tooltips(self, self.config.show_tooltips)
        self.start_runtime(silent=False)
        self.append_log("Налаштування застосовано.")

    def toggle_pause(self):
        self.paused = not self.paused
        if self.paused:
            self.stop_runtime()
            self.pause_button.setText("▶  ПРОДОВЖИТИ")
            self.set_status("Роботу призупинено", False)
            self.append_log("Роботу призупинено.")
        else:
            self.pause_button.setText("Ⅱ  ПАУЗА")
            self.start_runtime(silent=False)

    def swap_languages(self):
        self.config.source_language, self.config.target_language = self.config.target_language, self.config.source_language
        self.config.save()
        self.settings.load_from_config()
        self.append_log("Напрямок перекладу змінено.")

    def toggle_ptt_recording(self):
        if self.recorder is not None:
            self.on_ptt_release()
        elif not self.processing:
            self.on_ptt_press()

    def on_ptt_press(self):
        if self.processing or self.paused:
            return
        if self.config.input_device < 0:
            self.append_log("PTT: мікрофон не вибрано.")
            self.set_status("Мікрофон не вибрано", False)
            return
        if self.config.output_device < 0:
            self.append_log("PTT: вихід VB-CABLE не вибрано.")
            self.set_status("VB-CABLE не вибрано", False)
            return
        if not self.config.api_key.strip():
            self.append_log("PTT: вкажіть OpenAI API-ключ у налаштуваннях.")
            self.set_status("OpenAI API-ключ не вказано", False)
            return
        try:
            self.said.clear()
            self.translated.clear()
            self.audio.set_muted(True)
            self.recorder = PhraseRecorder(self.config.input_device, self.config.samplerate)
            self.recorder.start()
            self._toggle_recording_active = True
            self.ptt_button.setText("🔴  ГОВОРІТЬ...")
            self.set_status("Запис голосу", False)
            self.append_log("Запис розпочато.")
        except Exception as exc:
            self.audio.set_muted(False)
            self.recorder = None
            self.append_log(f"Не вдалося почати запис: {exc}")

    def on_ptt_release(self):
        if not self.recorder or self.processing:
            return
        recorder = self.recorder
        self.recorder = None
        self._toggle_recording_active = False
        self.processing = True
        self.ptt_button.setText("⏳  ПЕРЕКЛАДАЮ...")
        self.set_status("Розпізнавання та переклад", False)
        threading.Thread(target=self._process_recording, args=(recorder,), daemon=True).start()

    def _process_recording(self, recorder: PhraseRecorder):
        workdir = Path(tempfile.mkdtemp(prefix="tihiy_voice_"))
        source = workdir / "speech.wav"
        output = workdir / "translated.wav"
        try:
            duration = recorder.stop_to_wav(source)
            if duration < 0.25:
                raise RuntimeError("Запис надто короткий.")
            pipeline = OpenAITranslationPipeline(
                self.config.api_key,
                self.config.transcription_model,
                self.config.translation_model,
                self.config.tts_model,
                self.config.tts_voice,
            )
            text = pipeline.transcribe(source, self.config.source_language)
            if not text:
                raise RuntimeError("Мову не розпізнано.")
            self.bridge.transcript.emit(text)
            translated = pipeline.translate(text, self.config.source_language, self.config.target_language)
            if not translated:
                raise RuntimeError("Переклад порожній.")
            self.bridge.translation.emit(translated)
            pipeline.synthesize(translated, output)
            play_wav(output, self.config.output_device, self.config.output_mode)
            self.bridge.log.emit("Переклад відтворено через VB-CABLE.")
            self.bridge.processing_done.emit()
        except Exception as exc:
            self.bridge.processing_failed.emit(str(exc))
        finally:
            shutil.rmtree(workdir, ignore_errors=True)

    def on_processing_done(self):
        self.processing = False
        self._toggle_recording_active = False
        self.audio.set_muted(False)
        self._load_main_controls()
        if self.paused:
            self.set_status("Роботу призупинено", False)
        else:
            self.set_status("Готово до роботи", True)

    def on_processing_failed(self, error: str):
        self.processing = False
        self._toggle_recording_active = False
        self.audio.set_muted(False)
        self._load_main_controls()
        self.set_status("Помилка перекладу", False)
        self.append_log(f"Помилка PTT: {error}")
        if "api key" in error.lower():
            QMessageBox.warning(self, "OpenAI API", "Вкажіть API-ключ у налаштуваннях.")

    def quit_application(self):
        self.config.close_behavior = "exit"
        self.stop_runtime()
        self.settings.close()
        QApplication.instance().quit()

    def closeEvent(self, event):
        if self.config.close_behavior == "tray":
            event.ignore()
            self.hide()
            self.tray.showMessage("TiHiY Voice Translator", "Програма продовжує працювати у треї.")
            return
        self.stop_runtime()
        self.settings.close()
        event.accept()


class SettingsWindow(FramelessWindow):
    settings_saved = Signal()
    update_checked = Signal(object)
    update_failed = Signal(str)

    LANGUAGES = [
        ("Українська", "uk"), ("Англійська", "en"), ("Німецька", "de"),
        ("Французька", "fr"), ("Іспанська", "es"), ("Польська", "pl"),
    ]
    VOICES = [
        ("Marin — рекомендовано", "marin"),
        ("Cedar — рекомендовано", "cedar"),
        ("Alloy", "alloy"), ("Ash", "ash"), ("Ballad", "ballad"),
        ("Coral", "coral"), ("Echo", "echo"), ("Fable", "fable"),
        ("Nova", "nova"), ("Onyx", "onyx"), ("Sage", "sage"),
        ("Shimmer", "shimmer"), ("Verse", "verse"),
    ]

    def __init__(self, config: AppConfig, owner: MainWindow):
        super().__init__("TiHiY Voice Translator — Налаштування")
        self.config = config
        self.owner = owner
        self._devices: list[dict] = []
        screen = QApplication.primaryScreen().availableGeometry()
        available_width = max(760, screen.width() - 24)
        available_height = max(560, screen.height() - 24)
        target_width = min(1680, available_width)
        target_height = min(960, available_height)
        minimum_width = min(900, available_width)
        minimum_height = min(600, available_height)
        self.resize(target_width, target_height)
        self.setMinimumSize(minimum_width, minimum_height)
        self._responsive_single_column: bool | None = None
        self._settings_compact_state: tuple[bool, bool] | None = None
        self._first_show = True
        self._build_ui()
        self.update_checked.connect(self._show_update_result)
        self.update_failed.connect(self._show_update_error)
        self.load_from_config()
        self._apply_responsive_layout(force=True)
        QTimer.singleShot(0, self._fit_to_screen)

    def _build_ui(self):
        body = QWidget(self.root)
        body.setAttribute(Qt.WA_StyledBackground, False)
        body_layout = QVBoxLayout(body)
        self.settings_body_layout = body_layout
        body_layout.setContentsMargins(10, 8, 10, 8)
        body_layout.setSpacing(10)
        self.root_layout.addWidget(body, 1)

        content_row = QHBoxLayout()
        content_row.setSpacing(12)
        sidebar = HudPanel("", C.CYAN)
        sidebar.setMinimumWidth(170)
        sidebar.setMaximumWidth(230)
        sidebar.setMinimumWidth(150)
        sidebar.setMaximumWidth(230)
        self.sidebar = sidebar
        side_layout = sidebar.content
        self.nav_group = QButtonGroup(self)
        self.nav_group.setExclusive(True)
        navs = [
            ("⚙  Загальні", "general"),
            ("文  Переклад", "translation"),
            ("🔊  Аудіо", "audio"),
            ("⌨  PTT і керування", "ptt"),
            ("⇩  Оновлення", "updates"),
            ("ⓘ  Про програму", "about"),
        ]
        self.nav_buttons: dict[str, QPushButton] = {}
        for index, (text, key) in enumerate(navs):
            button = QPushButton(text)
            button.setCheckable(True)
            button.setCursor(Qt.PointingHandCursor)
            button.setMinimumHeight(50)
            button.setStyleSheet(
                f"QPushButton{{text-align:left;background:qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #06131e,stop:1 #030b12);"
                f"color:{C.TEXT};border:1px solid {C.LINE};border-radius:6px;padding:12px;font-size:15px;}}"
                f"QPushButton:hover{{background:#09283b;border-color:{C.CYAN};}}"
                f"QPushButton:checked{{background:qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #0a5278,stop:1 #071828);"
                f"border:2px solid {C.CYAN};color:#ffffff;}}"
            )
            self.nav_group.addButton(button, index)
            side_layout.addWidget(button)
            self.nav_buttons[key] = button
        self.nav_buttons["general"].setChecked(True)
        side_layout.addStretch(1)
        content_row.addWidget(sidebar)

        self.scroll = QScrollArea()
        self.scroll.setWidgetResizable(True)
        self.scroll.setFrameShape(QFrame.NoFrame)
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.scroll.setStyleSheet(
            f"QScrollArea{{background:transparent;border:none;}}"
            "QScrollBar:vertical{background:#06111b;width:10px;}"
            "QScrollBar::handle:vertical{background:#345267;border-radius:5px;min-height:35px;}"
        )
        scroll_body = QWidget()
        scroll_body.setMinimumWidth(0)
        scroll_body.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        scroll_body.setStyleSheet("background:transparent;")
        grid = QGridLayout(scroll_body)
        self.settings_grid = grid
        self.settings_scroll_body = scroll_body
        grid.setContentsMargins(0, 0, 4, 0)
        grid.setHorizontalSpacing(12)
        grid.setVerticalSpacing(12)

        self.general_panel = HudPanel("ЗАПУСК ТА ІНТЕРФЕЙС", C.CYAN, C.ORANGE)
        general = QVBoxLayout()
        self.chk_startup = QCheckBox("Запускати разом з Windows")
        self.chk_tray = QCheckBox("Згорнути у трей замість закриття")
        self.chk_tooltips = QCheckBox("Показувати підказки (tooltips)")
        for check, tip in (
            (self.chk_startup, "Автоматично запускати програму після входу до Windows"),
            (self.chk_tray, "Продовжувати роботу програми у системному треї"),
            (self.chk_tooltips, "Показувати підказки лише при наведенні"),
        ):
            check.setStyleSheet(checkbox_style())
            set_tip(check, tip)
            general.addWidget(check)
        theme_row = QHBoxLayout()
        theme_row.addWidget(self._label("Тема інтерфейсу"))
        self.theme_combo = self._combo([("TiHiY (За замовчуванням)", "tihiy")])
        theme_row.addWidget(self.theme_combo, 1)
        general.addLayout(theme_row)
        general.addStretch(1)
        self.general_panel.content.addLayout(general)
        grid.addWidget(self.general_panel, 0, 0)

        self.translation_settings_panel = HudPanel("ДВИГУН І ПЕРЕКЛАД", C.CYAN, C.ORANGE)
        trans = QGridLayout()
        trans.setHorizontalSpacing(10)
        trans.setVerticalSpacing(10)
        trans.addWidget(self._label("OpenAI API Key"), 0, 0, 1, 3)
        self.api_edit = QLineEdit()
        self.api_edit.setEchoMode(QLineEdit.Password)
        self.api_edit.setMinimumWidth(0)
        self.api_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        self.api_edit.setStyleSheet(line_style())
        set_tip(self.api_edit, "Ключ зберігається локально у профілі користувача")
        trans.addWidget(self.api_edit, 1, 0, 1, 2)
        self.eye_button = QToolButton()
        self.eye_button.setText("◉")
        self.eye_button.setCursor(Qt.PointingHandCursor)
        self.eye_button.setStyleSheet(button_style())
        self.eye_button.clicked.connect(self.toggle_api)
        set_tip(self.eye_button, "Показати або приховати API-ключ")
        trans.addWidget(self.eye_button, 1, 2)
        trans.addWidget(self._label("Напрямок перекладу"), 2, 0, 1, 3)
        self.source_combo = self._combo(self.LANGUAGES)
        self.swap_languages_button = QPushButton("⇄")
        self.swap_languages_button.setCursor(Qt.PointingHandCursor)
        self.swap_languages_button.setStyleSheet(button_style(C.ORANGE))
        self.swap_languages_button.clicked.connect(self.swap_languages)
        self.target_combo = self._combo(self.LANGUAGES)
        trans.addWidget(self.source_combo, 3, 0)
        trans.addWidget(self.swap_languages_button, 3, 1)
        trans.addWidget(self.target_combo, 3, 2)
        trans.addWidget(self._label("Голос перекладу"), 4, 0)
        self.voice_combo = self._combo(self.VOICES)
        set_tip(self.voice_combo, "Голос OpenAI TTS для озвучення перекладу")
        trans.addWidget(self.voice_combo, 4, 1, 1, 2)
        trans.addWidget(self._label("Режим прямого голосу"), 5, 0, 1, 2)
        self.direct_switch = ToggleSwitch()
        set_tip(self.direct_switch, "Передавати ваш голос напряму до VB-CABLE між перекладами")
        trans.addWidget(self.direct_switch, 5, 2, alignment=Qt.AlignRight)
        trans.setColumnStretch(0, 1)
        trans.setColumnStretch(1, 0)
        trans.setColumnStretch(2, 1)
        self.translation_settings_panel.content.addLayout(trans)
        grid.addWidget(self.translation_settings_panel, 0, 1)

        self.audio_panel = HudPanel("АУДІО", C.CYAN, C.ORANGE)
        audio = QGridLayout()
        audio.setHorizontalSpacing(10)
        audio.setVerticalSpacing(11)
        audio.addWidget(self._label("Мікрофон"), 0, 0)
        self.input_combo = self._combo([])
        audio.addWidget(self.input_combo, 0, 1)
        self.input_meter = LevelMeter()
        audio.addWidget(self.input_meter, 0, 2)
        audio.addWidget(self._label("Вихід VB-CABLE"), 1, 0)
        self.output_combo = self._combo([])
        audio.addWidget(self.output_combo, 1, 1)
        self.output_meter = LevelMeter()
        audio.addWidget(self.output_meter, 1, 2)
        audio.addWidget(self._label("Режим виводу"), 2, 0)
        self.output_mode_combo = self._combo([("Stereo", "Stereo"), ("Mono", "Mono")])
        audio.addWidget(self.output_mode_combo, 2, 1)
        audio.addWidget(self._label("Частота дискретизації"), 3, 0)
        self.rate_combo = self._combo([("48000 Hz", 48000), ("44100 Hz", 44100), ("16000 Hz", 16000)])
        audio.addWidget(self.rate_combo, 3, 1)
        audio.setColumnStretch(0, 0)
        audio.setColumnStretch(1, 1)
        audio.setColumnStretch(2, 0)
        self.audio_panel.content.addLayout(audio)
        grid.addWidget(self.audio_panel, 1, 0)

        self.ptt_panel = HudPanel("PTT І КЕРУВАННЯ", C.CYAN, C.ORANGE)
        ptt = QGridLayout()
        ptt.setHorizontalSpacing(10)
        ptt.setVerticalSpacing(11)
        ptt.addWidget(self._label("PTT клавіша"), 0, 0)
        self.ptt_key_edit = QLineEdit()
        self.ptt_key_edit.setReadOnly(True)
        self.ptt_key_edit.setAlignment(Qt.AlignCenter)
        self.ptt_key_edit.setStyleSheet(line_style())
        ptt.addWidget(self.ptt_key_edit, 0, 1)
        self.ptt_edit_button = QToolButton()
        self.ptt_edit_button.setText("✎")
        self.ptt_edit_button.setCursor(Qt.PointingHandCursor)
        self.ptt_edit_button.setStyleSheet(button_style(C.ORANGE))
        self.ptt_edit_button.clicked.connect(self.change_ptt_key)
        set_tip(self.ptt_edit_button, "Змінити клавішу PTT")
        ptt.addWidget(self.ptt_edit_button, 0, 2)
        ptt.addWidget(self._label("Режим PTT"), 1, 0)
        self.ptt_mode_combo = self._combo([
            ("Утримувати для передачі", "Утримувати для передачі"),
            ("Натиснути для початку / завершення", "Перемикач"),
        ])
        ptt.addWidget(self.ptt_mode_combo, 1, 1, 1, 2)
        ptt.addWidget(self._label("Індикатор активності"), 2, 0, 1, 2)
        self.indicator_switch = ToggleSwitch()
        ptt.addWidget(self.indicator_switch, 2, 2, alignment=Qt.AlignRight)
        ptt.addWidget(self._label("Автозапуск PTT при старті"), 3, 0, 1, 2)
        self.ptt_autostart_switch = ToggleSwitch()
        ptt.addWidget(self.ptt_autostart_switch, 3, 2, alignment=Qt.AlignRight)
        ptt.setColumnStretch(0, 0)
        ptt.setColumnStretch(1, 1)
        ptt.setColumnStretch(2, 0)
        self.ptt_panel.content.addLayout(ptt)
        grid.addWidget(self.ptt_panel, 1, 1)

        self.updates_panel = HudPanel("ОНОВЛЕННЯ", C.CYAN, C.ORANGE)
        updates = QGridLayout()
        updates.setHorizontalSpacing(10)
        updates.setVerticalSpacing(12)
        updates.addWidget(self._label("Перевіряти оновлення автоматично"), 0, 0)
        self.auto_update_switch = ToggleSwitch()
        updates.addWidget(self.auto_update_switch, 0, 1, alignment=Qt.AlignRight)
        updates.addWidget(self._label("Канал оновлень"), 1, 0)
        self.update_channel_combo = self._combo([
            ("Стабільний (Рекомендовано)", "Стабільний (Рекомендовано)"),
            ("Тестовий", "Тестовий"),
        ])
        updates.addWidget(self.update_channel_combo, 1, 1)
        self.check_update_button = QPushButton("ПЕРЕВІРИТИ ЗАРАЗ")
        self.check_update_button.setCursor(Qt.PointingHandCursor)
        self.check_update_button.setStyleSheet(button_style())
        self.check_update_button.clicked.connect(self.check_updates)
        updates.addWidget(self.check_update_button, 2, 1)
        updates.setColumnStretch(0, 1)
        updates.setColumnStretch(1, 1)
        self.updates_panel.content.addLayout(updates)
        grid.addWidget(self.updates_panel, 2, 0)

        self.about_panel = HudPanel("ПРО ПРОГРАМУ", C.CYAN, C.ORANGE)
        self.about_grid = QGridLayout()
        self.about_grid.setHorizontalSpacing(16)
        self.about_grid.setVerticalSpacing(10)
        self.about_avatar = QLabel()
        self.about_avatar.setAlignment(Qt.AlignCenter)
        self.about_avatar.setPixmap(QPixmap(str(ASSETS / "avatar.png")).scaled(130, 130, Qt.KeepAspectRatio, Qt.SmoothTransformation))
        self.about_avatar.setFixedSize(140, 140)
        self.about_info_widget = QWidget()
        self.about_info_widget.setStyleSheet("background:transparent;")
        info = QVBoxLayout(self.about_info_widget)
        info.setContentsMargins(0, 0, 0, 0)
        self.about_name = QLabel("TiHiY Voice Translator")
        self.about_name.setStyleSheet(f"color:{C.TEXT};font-size:20px;font-weight:600;")
        self.about_description = QLabel("Розумний голосовий перекладач у реальному часі.")
        self.about_description.setWordWrap(True)
        self.about_description.setStyleSheet(f"color:{C.MUTED};font-size:14px;")
        self.about_version = QLabel(f"Версія: v{VERSION}\n© 2026 TiHiY Labs. Усі права захищено.")
        self.about_version.setWordWrap(True)
        self.about_version.setStyleSheet(f"color:{C.MUTED};font-size:13px;")
        self.about_links_widget = QWidget()
        self.about_links_widget.setStyleSheet("background:transparent;")
        links = QHBoxLayout(self.about_links_widget)
        links.setContentsMargins(0, 0, 0, 0)
        links.setSpacing(8)
        self.site_button = QPushButton("◎  Сайт програми  ↗")
        self.support_button = QPushButton("◉  Підтримка  ↗")
        for button in (self.site_button, self.support_button):
            button.setCursor(Qt.PointingHandCursor)
            button.setStyleSheet(button_style())
            button.setMinimumWidth(0)
            button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        self.site_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.youtube.com/@TiHiY-DED")))
        self.support_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://donatello.to/TiHiY-DED")))
        links.addWidget(self.site_button)
        links.addWidget(self.support_button)
        info.addWidget(self.about_name)
        info.addWidget(self.about_description)
        info.addWidget(self.about_version)
        info.addStretch(1)
        info.addWidget(self.about_links_widget)
        self.about_grid.addWidget(self.about_avatar, 0, 0, alignment=Qt.AlignCenter)
        self.about_grid.addWidget(self.about_info_widget, 0, 1)
        self.about_grid.setColumnStretch(1, 1)
        self.about_panel.content.addLayout(self.about_grid)
        grid.addWidget(self.about_panel, 2, 1)

        self._settings_panels = [
            self.general_panel,
            self.translation_settings_panel,
            self.audio_panel,
            self.ptt_panel,
            self.updates_panel,
            self.about_panel,
        ]
        for panel in self._settings_panels:
            panel.setMinimumWidth(0)
            panel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        grid.setColumnStretch(0, 1)
        grid.setColumnStretch(1, 1)
        self.scroll.setWidget(scroll_body)
        content_row.addWidget(self.scroll, 1)
        body_layout.addLayout(content_row, 1)

        bottom = QHBoxLayout()
        self.cancel_button = QPushButton("СКАСУВАТИ")
        self.cancel_button.setCursor(Qt.PointingHandCursor)
        self.cancel_button.setStyleSheet(button_style())
        self.cancel_button.clicked.connect(self.cancel)
        bottom.addWidget(self.cancel_button)
        bottom.addStretch(1)
        self.save_button = QPushButton("≪   ЗБЕРЕГТИ ЗМІНИ   ≫")
        self.save_button.setCursor(Qt.PointingHandCursor)
        self.save_button.setMinimumWidth(240)
        self.save_button.setStyleSheet(button_style(C.ORANGE))
        self.save_button.clicked.connect(self.save)
        bottom.addWidget(self.save_button)
        grip = QSizeGrip(self)
        grip.setFixedSize(18, 18)
        bottom.addWidget(grip)
        body_layout.addLayout(bottom)

        mapping = {
            "general": self.general_panel,
            "translation": self.translation_settings_panel,
            "audio": self.audio_panel,
            "ptt": self.ptt_panel,
            "updates": self.updates_panel,
            "about": self.about_panel,
        }
        for key, button in self.nav_buttons.items():
            button.clicked.connect(lambda checked=False, panel=mapping[key]: self.scroll_to(panel))

    def showEvent(self, event):
        super().showEvent(event)
        QTimer.singleShot(0, self._fit_to_screen)

    def _fit_to_screen(self):
        screen_obj = self.owner.screen() if self.owner and self.owner.screen() else QApplication.primaryScreen()
        screen = screen_obj.availableGeometry()
        maximum_width = max(760, screen.width() - 24)
        maximum_height = max(560, screen.height() - 24)
        self.setMinimumSize(min(900, maximum_width), min(600, maximum_height))

        if self._first_show:
            preferred_width = min(1680, int(maximum_width * 0.98))
            preferred_height = min(960, int(maximum_height * 0.97))
            self.resize(preferred_width, preferred_height)
            self._first_show = False
        else:
            self.resize(min(self.width(), maximum_width), min(self.height(), maximum_height))

        self._apply_responsive_layout(force=True)
        self.center_window()

    def _apply_responsive_layout(self, force: bool = False):
        if not hasattr(self, "settings_grid") or not hasattr(self, "_settings_panels"):
            return

        viewport_width = self.scroll.viewport().width() if self.scroll.viewport().width() > 0 else max(0, self.width() - 250)
        compact = self.height() < 780 or self.width() < 1380
        very_compact = self.width() < 1080 or self.height() < 680
        single_column = viewport_width < 1160
        state = (compact, very_compact)

        if force or single_column != self._responsive_single_column:
            self._responsive_single_column = single_column
            for panel in self._settings_panels:
                self.settings_grid.removeWidget(panel)

            if single_column:
                for row, panel in enumerate(self._settings_panels):
                    self.settings_grid.addWidget(panel, row, 0)
                self.settings_grid.setColumnStretch(0, 1)
                self.settings_grid.setColumnStretch(1, 0)
                self.settings_grid.setColumnMinimumWidth(1, 0)
            else:
                positions = (
                    (self.general_panel, 0, 0),
                    (self.translation_settings_panel, 0, 1),
                    (self.audio_panel, 1, 0),
                    (self.ptt_panel, 1, 1),
                    (self.updates_panel, 2, 0),
                    (self.about_panel, 2, 1),
                )
                for panel, row, column in positions:
                    self.settings_grid.addWidget(panel, row, column)
                self.settings_grid.setColumnStretch(0, 1)
                self.settings_grid.setColumnStretch(1, 1)

        if force or state != self._settings_compact_state:
            self._settings_compact_state = state
            sidebar_width = 200 if very_compact else (220 if compact else 230)
            nav_height = 42 if very_compact else (48 if compact else 56)
            self.sidebar.setFixedWidth(sidebar_width)
            for button in self.nav_buttons.values():
                button.setMinimumHeight(nav_height)
                button.setMaximumHeight(nav_height + 2)

            self.titlebar.setFixedHeight(48 if compact else 58)
            self.settings_body_layout.setContentsMargins(6 if compact else 10, 5 if compact else 8, 6 if compact else 10, 5 if compact else 8)
            self.settings_body_layout.setSpacing(7 if compact else 10)
            self.save_button.setMinimumWidth(210 if very_compact else (260 if compact else 360))
            self.cancel_button.setMinimumWidth(120 if compact else 150)

            panel_margins = (14, 10, 14, 12) if compact else (20, 14, 20, 18)
            for panel in self._settings_panels:
                panel.outer.setContentsMargins(*panel_margins)
                panel.outer.setSpacing(6 if compact else 8)

            avatar_size = 96 if very_compact else (112 if compact else 140)
            self.about_avatar.setFixedSize(avatar_size, avatar_size)
            self.about_avatar.setPixmap(
                QPixmap(str(ASSETS / "avatar.png")).scaled(
                    avatar_size - 8, avatar_size - 8, Qt.KeepAspectRatio, Qt.SmoothTransformation
                )
            )

        # Reflow the about panel itself when its available width is narrow.
        about_vertical = viewport_width < (720 if single_column else 610)
        self.about_grid.removeWidget(self.about_avatar)
        self.about_grid.removeWidget(self.about_info_widget)
        if about_vertical:
            self.about_grid.addWidget(self.about_avatar, 0, 0, alignment=Qt.AlignCenter)
            self.about_grid.addWidget(self.about_info_widget, 1, 0)
            self.about_grid.setColumnStretch(0, 1)
            self.about_grid.setColumnStretch(1, 0)
        else:
            self.about_grid.addWidget(self.about_avatar, 0, 0, alignment=Qt.AlignCenter)
            self.about_grid.addWidget(self.about_info_widget, 0, 1)
            self.about_grid.setColumnStretch(0, 0)
            self.about_grid.setColumnStretch(1, 1)

        self.settings_scroll_body.setMinimumWidth(0)
        self.settings_grid.invalidate()
        self.settings_grid.activate()
        self.settings_scroll_body.adjustSize()
        self.settings_scroll_body.updateGeometry()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self._apply_responsive_layout()

    def _label(self, text: str) -> QLabel:
        label = QLabel(text)
        label.setWordWrap(True)
        label.setMinimumWidth(0)
        label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        label.setStyleSheet(f"color:{C.TEXT};font-size:14px;background:transparent;")
        return label

    def _combo(self, items: list[tuple[str, object]]) -> QComboBox:
        combo = QComboBox()
        combo.setMinimumWidth(0)
        combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        combo.setStyleSheet(combo_style())
        for text, data in items:
            combo.addItem(text, data)
        return combo

    def scroll_to(self, widget: QWidget):
        self.scroll.ensureWidgetVisible(widget, 30, 30)

    def load_devices(self, devices: list[dict]):
        self._devices = devices
        input_value = self.config.input_device
        output_value = self.config.output_device
        self.input_combo.blockSignals(True)
        self.output_combo.blockSignals(True)
        self.input_combo.clear()
        self.output_combo.clear()
        inputs = [d for d in devices if d["inputs"] > 0]
        outputs = [d for d in devices if d["outputs"] > 0]
        if inputs:
            for item in inputs:
                self.input_combo.addItem(item["name"], item["index"])
        else:
            self.input_combo.addItem("Мікрофон не знайдено", -1)
        if outputs:
            for item in outputs:
                self.output_combo.addItem(item["name"], item["index"])
        else:
            self.output_combo.addItem("VB-CABLE не знайдено", -1)
        self._set_combo_data(self.input_combo, input_value)
        self._set_combo_data(self.output_combo, output_value)
        self.input_combo.blockSignals(False)
        self.output_combo.blockSignals(False)

    @staticmethod
    def _set_combo_data(combo: QComboBox, value):
        index = combo.findData(value)
        if index >= 0:
            combo.setCurrentIndex(index)

    def load_from_config(self):
        self.chk_startup.setChecked(self.config.start_with_windows)
        self.chk_tray.setChecked(self.config.close_behavior == "tray")
        self.chk_tooltips.setChecked(self.config.show_tooltips)
        self.api_edit.setText(self.config.api_key)
        self._set_combo_data(self.source_combo, self.config.source_language)
        self._set_combo_data(self.target_combo, self.config.target_language)
        self._set_combo_data(self.voice_combo, self.config.tts_voice)
        self.direct_switch.setChecked(self.config.direct_voice_enabled)
        self.ptt_key_edit.setText(self.config.ptt_key)
        self._set_combo_data(self.rate_combo, self.config.samplerate)
        self._set_combo_data(self.output_mode_combo, self.config.output_mode)
        self._set_combo_data(self.ptt_mode_combo, self.config.ptt_mode)
        self.indicator_switch.setChecked(self.config.indicator_enabled)
        self.ptt_autostart_switch.setChecked(self.config.ptt_autostart)
        self.auto_update_switch.setChecked(self.config.auto_update)
        self._set_combo_data(self.update_channel_combo, self.config.update_channel)
        self.load_devices(self.owner._devices)
        apply_tooltips(self, self.config.show_tooltips)

    def set_input_level(self, level: float):
        self.input_meter.set_level(level)
        self.output_meter.set_level(level if self.config.direct_voice_enabled else 0.0)

    def toggle_api(self):
        self.api_edit.setEchoMode(QLineEdit.Normal if self.api_edit.echoMode() == QLineEdit.Password else QLineEdit.Password)

    def swap_languages(self):
        source = self.source_combo.currentData()
        target = self.target_combo.currentData()
        self._set_combo_data(self.source_combo, target)
        self._set_combo_data(self.target_combo, source)

    def change_ptt_key(self):
        dialog = KeyCaptureDialog(self.ptt_key_edit.text(), self)
        if dialog.exec() == QDialog.Accepted:
            self.ptt_key_edit.setText(dialog.key_name)

    def check_updates(self, silent: bool = False):
        manifest_url = (self.config.update_manifest_url or "").strip()
        if not manifest_url:
            if not silent:
                QMessageBox.information(
                    self,
                    "Оновлення",
                    "URL маніфесту оновлень ще не налаштований.\n"
                    "Його можна додати у settings.json або у майбутній офіційній збірці.",
                )
            return
        self.check_update_button.setEnabled(False)
        self.check_update_button.setText("ПЕРЕВІРКА...")
        channel = "beta" if "Бета" in str(self.update_channel_combo.currentData()) else "stable"

        def worker():
            try:
                result = check_for_update(manifest_url, VERSION, channel)
                result = (result, silent)
                self.update_checked.emit(result)
            except Exception as exc:
                self.update_failed.emit(("silent:" if silent else "") + str(exc))

        threading.Thread(target=worker, daemon=True).start()

    def _reset_update_button(self):
        self.check_update_button.setEnabled(True)
        self.check_update_button.setText("ПЕРЕВІРИТИ ЗАРАЗ")

    def _show_update_result(self, payload):
        self._reset_update_button()
        info, silent = payload
        if not isinstance(info, UpdateInfo):
            return
        if info.update_available:
            details = f"Доступна версія {info.latest_version}."
            if info.notes:
                details += f"\n\n{info.notes}"
            if info.download_url:
                answer = QMessageBox.question(
                    self,
                    "Доступне оновлення",
                    details + "\n\nВідкрити сторінку завантаження?",
                )
                if answer == QMessageBox.Yes:
                    QDesktopServices.openUrl(QUrl(info.download_url))
            else:
                QMessageBox.information(self, "Доступне оновлення", details)
        elif not silent:
            QMessageBox.information(self, "Оновлення", f"Встановлена актуальна версія {VERSION}.")

    def _show_update_error(self, message: str):
        self._reset_update_button()
        silent = message.startswith("silent:")
        clean = message.removeprefix("silent:")
        if not silent:
            QMessageBox.warning(self, "Оновлення", clean)

    def collect(self):
        self.config.start_with_windows = self.chk_startup.isChecked()
        self.config.close_behavior = "tray" if self.chk_tray.isChecked() else "exit"
        self.config.show_tooltips = self.chk_tooltips.isChecked()
        self.config.api_key = self.api_edit.text().strip()
        self.config.source_language = str(self.source_combo.currentData())
        self.config.target_language = str(self.target_combo.currentData())
        self.config.tts_voice = str(self.voice_combo.currentData())
        self.config.direct_voice_enabled = self.direct_switch.isChecked()
        self.config.input_device = int(self.input_combo.currentData() or -1)
        self.config.output_device = int(self.output_combo.currentData() or -1)
        self.config.input_device_name = device_name(self._devices, self.config.input_device)
        self.config.output_device_name = device_name(self._devices, self.config.output_device)
        self.config.output_mode = str(self.output_mode_combo.currentData())
        self.config.samplerate = int(self.rate_combo.currentData())
        self.config.ptt_key = self.ptt_key_edit.text().strip().upper() or "F8"
        self.config.ptt_mode = str(self.ptt_mode_combo.currentData())
        self.config.indicator_enabled = self.indicator_switch.isChecked()
        self.config.ptt_autostart = self.ptt_autostart_switch.isChecked()
        self.config.auto_update = self.auto_update_switch.isChecked()
        self.config.update_channel = str(self.update_channel_combo.currentData())

    def save(self):
        self.collect()
        try:
            set_start_with_windows(self.config.start_with_windows)
        except Exception as exc:
            QMessageBox.warning(self, "Автозапуск", str(exc))
        self.config.save()
        apply_tooltips(self, self.config.show_tooltips)
        self.settings_saved.emit()
        self.close()

    def cancel(self):
        self.load_from_config()
        self.close()


def _save_screenshot(widget: QWidget, path: str):
    widget.show()
    QApplication.processEvents()
    QTimer.singleShot(250, lambda: (widget.grab().save(path), QApplication.instance().quit()))


def main() -> int:
    app = QApplication(sys.argv)
    app.setApplicationName("TiHiY Voice Translator")
    app.setOrganizationName("TiHiY")
    app.setQuitOnLastWindowClosed(False)
    app.setStyle("Fusion")

    screenshot_mode = "--screenshot-main" in sys.argv or "--screenshot-settings" in sys.argv
    allow_multiple = screenshot_mode or "--allow-multiple" in sys.argv
    instance_guard: SingleInstanceGuard | None = None
    if not allow_multiple:
        instance_guard = SingleInstanceGuard(parent=app)
        if not instance_guard.acquire():
            # The first copy receives SHOW and restores itself from tray/minimize.
            return 0
        app.aboutToQuit.connect(instance_guard.close)

    try:
        no_runtime = "--no-runtime" in sys.argv
        window = MainWindow(start_runtime=not no_runtime)
        if instance_guard is not None:
            instance_guard.activation_requested.connect(window.show_and_raise)
            if instance_guard.consume_pending_activation():
                QTimer.singleShot(0, window.show_and_raise)
        if "--screenshot-main" in sys.argv:
            index = sys.argv.index("--screenshot-main")
            _save_screenshot(window, sys.argv[index + 1])
        elif "--screenshot-settings" in sys.argv:
            index = sys.argv.index("--screenshot-settings")
            window.settings.show()
            _save_screenshot(window.settings, sys.argv[index + 1])
        elif "--background" not in sys.argv:
            window.show()
        return app.exec()
    except Exception:
        crash = Path(tempfile.gettempdir()) / "TiHiY_Voice_Translator_crash.log"
        crash.write_text(traceback.format_exc(), encoding="utf-8")
        raise
    finally:
        if instance_guard is not None:
            instance_guard.close()


if __name__ == "__main__":
    raise SystemExit(main())
