"""Десктоп-клиент голосового чата для Windows.

Установка:
    pip install PySide6 sounddevice websockets

Запуск:
    python client_desktop.py

Сборка .exe (на машине с Windows):
    pip install pyinstaller
    pyinstaller --onefile --windowed --name VoiceChat client_desktop.py
"""
import asyncio
import json
import struct
import sys
import threading
import uuid as uuid_mod
from queue import Empty, Queue

import sounddevice as sd
import websockets
from PySide6.QtCore import Qt, QTimer, Signal, QObject
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
    QLineEdit, QPushButton, QListWidget, QListWidgetItem, QMessageBox,
)

SAMPLE_RATE = 48000
FRAME_SAMPLES = 960          # 20 мс
TALK_THRESHOLD = 0.015
DEFAULT_SERVER = "wss://mychatkimi.duckdns.org/ws"


# ---------- Поток сети (asyncio) ----------

class NetworkThread(QObject):
    """WebSocket в отдельном потоке. Общается с GUI через сигналы/очереди."""

    joined = Signal(str, str)            # id, room
    users = Signal(list)                 # [{id, name}]
    user_joined = Signal(dict)           # {id, name}
    user_left = Signal(str)              # id
    talking = Signal(str, bool)          # id, talking
    error = Signal(str)
    closed = Signal()

    def __init__(self, server_url, name, room, parent=None):
        super().__init__(parent)
        self.server_url = server_url
        self.name = name
        self.room = room
        self.my_id = None
        self.mic_queue = Queue(maxsize=200)   # кадры на отправку (bytes PCM)
        self.play_queues = {}                 # id отправителя -> Queue
        self.play_lock = threading.Lock()
        self.muted = True
        self._stop = threading.Event()
        self._talking_state = False
        self._thread = threading.Thread(target=self._run, daemon=True)

    def start(self):
        self._thread.start()

    def stop(self):
        self._stop.set()

    def set_muted(self, muted):
        self.muted = muted

    def get_play_frame(self, user_id):
        """Забрать кадр на воспроизведение (вызывается из audio-потока)."""
        q = self.play_queues.get(user_id)
        if not q:
            return None
        try:
            return q.get_nowait()
        except Empty:
            return None

    def play_user_ids(self):
        with self.play_lock:
            return list(self.play_queues.keys())

    def _run(self):
        try:
            asyncio.run(self._main())
        except Exception as e:
            if not self._stop.is_set():
                self.error.emit(str(e))

    async def _main(self):
        async with websockets.connect(self.server_url, ping_interval=None) as ws:
            await ws.send(json.dumps({"type": "join", "room": self.room, "name": self.name}))
            send_task = asyncio.create_task(self._sender(ws))
            recv_task = asyncio.create_task(self._receiver(ws))
            done, pending = await asyncio.wait(
                [send_task, recv_task], return_when=asyncio.FIRST_EXCEPTION)
            for t in pending:
                t.cancel()

    async def _sender(self, ws):
        while not self._stop.is_set():
            try:
                frame = self.mic_queue.get_nowait()
            except Empty:
                await asyncio.sleep(0.005)  # отдаём управление event loop
                continue
            if self.muted:
                continue
            # детектор активности
            samples = struct.unpack(f"<{len(frame)//4}f", frame)
            peak = max((abs(s) for s in samples[::4]), default=0)
            talking = peak > TALK_THRESHOLD
            if talking != self._talking_state:
                self._talking_state = talking
                await ws.send(json.dumps({"type": "talking", "talking": talking}))
            await ws.send(frame)

    async def _receiver(self, ws):
        async for msg in ws:
            if self._stop.is_set():
                break
            if isinstance(msg, bytes):
                user_id = msg[:36].decode(errors="replace")
                with self.play_lock:
                    q = self.play_queues.get(user_id)
                    if q is None:
                        q = Queue(maxsize=50)
                        self.play_queues[user_id] = q
                try:
                    q.put_nowait(bytes(msg[36:]))
                except Exception:
                    pass  # переполнение — выбрасываем кадр
            else:
                data = json.loads(msg)
                t = data.get("type")
                if t == "joined":
                    self.my_id = data["id"]
                    self.joined.emit(data["id"], data["room"])
                elif t == "users":
                    self.users.emit(data["users"])
                elif t == "user_joined":
                    self.user_joined.emit(data["user"])
                elif t == "user_left":
                    uid = data["id"]
                    with self.play_lock:
                        self.play_queues.pop(uid, None)
                    self.user_left.emit(uid)
                elif t == "talking":
                    self.talking.emit(data["id"], bool(data["talking"]))


# ---------- Аудио ----------

class AudioEngine:
    """Захват микрофона и воспроизведение через sounddevice."""

    def __init__(self, net: NetworkThread):
        self.net = net
        self.in_stream = None
        self.out_stream = None
        self._acc = bytearray()

    def start(self):
        self.in_stream = sd.InputStream(
            samplerate=SAMPLE_RATE, channels=1, dtype="float32",
            blocksize=FRAME_SAMPLES, callback=self._on_input)
        self.out_stream = sd.OutputStream(
            samplerate=SAMPLE_RATE, channels=1, dtype="float32",
            blocksize=FRAME_SAMPLES, callback=self._on_output)
        self.in_stream.start()
        self.out_stream.start()

    def stop(self):
        for s in (self.in_stream, self.out_stream):
            if s:
                try:
                    s.stop(); s.close()
                except Exception:
                    pass

    def _on_input(self, indata, frames, time_info, status):
        self._acc.extend(indata.tobytes())
        while len(self._acc) >= FRAME_SAMPLES * 4:
            frame = bytes(self._acc[:FRAME_SAMPLES * 4])
            del self._acc[:FRAME_SAMPLES * 4]
            try:
                self.net.mic_queue.put_nowait(frame)
            except Exception:
                pass  # очередь полна — пропускаем

    def _on_output(self, outdata, frames, time_info, status):
        import numpy as np
        mixed = np.zeros(frames, dtype="float32")
        for uid in self.net.play_user_ids():
            frame = self.net.get_play_frame(uid)
            if frame and len(frame) == frames * 4:
                mixed += np.frombuffer(frame, dtype="float32")
        # защита от клиппинга
        peak = np.max(np.abs(mixed)) if len(mixed) else 0
        if peak > 0.95:
            mixed *= 0.95 / peak
        outdata[:] = mixed.reshape(-1, 1)


# ---------- GUI ----------

class LoginWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Voice Chat — вход")
        self.resize(320, 200)
        lay = QVBoxLayout(self)
        lay.addWidget(QLabel("🎙️ Голосовой чат"))
        self.name_edit = QLineEdit(); self.name_edit.setPlaceholderText("Твоё имя")
        self.room_edit = QLineEdit("main"); self.room_edit.setPlaceholderText("Комната")
        self.server_edit = QLineEdit(DEFAULT_SERVER); self.server_edit.setPlaceholderText("Сервер")
        self.join_btn = QPushButton("Подключиться")
        lay.addWidget(self.name_edit); lay.addWidget(self.room_edit)
        lay.addWidget(self.server_edit); lay.addWidget(self.join_btn)
        self.join_btn.clicked.connect(self._join)

    def _join(self):
        name = self.name_edit.text().strip()
        if not name:
            QMessageBox.warning(self, "Ошибка", "Введи имя")
            return
        self.chat = ChatWindow(
            self.server_edit.text().strip(),
            name,
            self.room_edit.text().strip() or "main")
        self.chat.show()
        self.close()


class ChatWindow(QWidget):
    def __init__(self, server_url, name, room):
        super().__init__()
        self.setWindowTitle(f"Voice Chat — {room}")
        self.resize(360, 420)

        lay = QVBoxLayout(self)
        lay.addWidget(QLabel(f"Комната: <b>{room}</b>"))
        self.user_list = QListWidget()
        lay.addWidget(self.user_list)

        btn_lay = QHBoxLayout()
        self.mute_btn = QPushButton("🔇 Mute")
        self.leave_btn = QPushButton("Покинуть")
        btn_lay.addWidget(self.mute_btn); btn_lay.addWidget(self.leave_btn)
        lay.addLayout(btn_lay)

        self.items = {}   # user_id -> QListWidgetItem
        self.muted = True

        self.net = NetworkThread(server_url, name, room)
        self.net.joined.connect(self._on_joined)
        self.net.users.connect(self._render_users)
        self.net.user_joined.connect(self._add_user)
        self.net.user_left.connect(self._remove_user)
        self.net.talking.connect(self._on_talking)
        self.net.error.connect(lambda e: QMessageBox.critical(self, "Ошибка сети", e))
        self.net.start()

        self.mute_btn.clicked.connect(self._toggle_mute)
        self.leave_btn.clicked.connect(self.close)

    def _on_joined(self, my_id, room):
        self.my_id = my_id
        try:
            self.audio = AudioEngine(self.net)
            self.audio.start()
        except Exception as e:
            QMessageBox.warning(self, "Аудио", f"Не удалось открыть микрофон: {e}")
        self._toggle_mute()  # начинаем размьюченным (после включения аудио)

    def _render_users(self, users):
        self.user_list.clear()
        self.items.clear()
        for u in users:
            self._add_user(u)

    def _add_user(self, user):
        uid = user["id"]
        name = user["name"] + (" (ты)" if uid == getattr(self, "my_id", None) else "")
        item = QListWidgetItem(f"⚫ {name}")
        item.setData(Qt.UserRole, uid)
        self.items[uid] = item
        self.user_list.addItem(item)

    def _remove_user(self, uid):
        item = self.items.pop(uid, None)
        if item:
            self.user_list.takeItem(self.user_list.row(item))

    def _on_talking(self, uid, talking):
        item = self.items.get(uid)
        if item:
            text = item.text()
            item.setText(("🟢" if talking else "⚫") + text[1:])

    def _toggle_mute(self):
        self.muted = not self.muted
        self.net.set_muted(self.muted)
        self.mute_btn.setText("🔇 Mute" if self.muted else "🎙️ Говорю")

    def closeEvent(self, event):
        self.net.stop()
        if hasattr(self, "audio"):
            self.audio.stop()
        event.accept()


def main():
    app = QApplication(sys.argv)
    w = LoginWindow()
    w.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
