#!/usr/bin/env python3
"""Стенд совместимости с TCI-клиентом MSHV.

Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
так, как наш собственный стенд. Две его особенности определяют всё:

  1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
     RX2 (tci_trx = "1") строка `trx:0,false`
     не существует в природе.

  2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
     500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
     сообщает об ошибке инициализации.

Соответствие настроек: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
приёмник 1.

Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
TX-аудио не отправляются. Передающую часть включает явный --tx.

Запуск (приложение уже работает, TCI-сервер поднят):
    test/tci/mshv_sim.py --rx 1
    test/tci/mshv_sim.py --rx 0 --audio
Только стандартная библиотека; транспорт переиспользуется из live_rx_test.py.
"""

import argparse
import os
import struct
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from live_rx_test import WS, split_commands, parse_command   # noqa: E402

HDR = struct.Struct("<16I")

# Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
# частота 48 кГц и буфер TX 24 мс.
MSHV_CHANNELS = "2"
MSHV_SAMPLES = "2048"
MSHV_TYPE = "float32"
MSHV_RATE = "48000"
MSHV_TXBUF = "24"


class Verdict:
    def __init__(self):
        self.ok_count = 0
        self.fail_count = 0

    def ok(self, label, detail=""):
        self.ok_count += 1
        print(f"  ok   {label}" + (f" — {detail}" if detail else ""))

    def fail(self, label, detail=""):
        self.fail_count += 1
        print(f"  FAIL {label}" + (f" — {detail}" if detail else ""))

    def note(self, label, detail=""):
        print(f"  ..   {label}" + (f" — {detail}" if detail else ""))


class MshvClient:
    """Минимальная модель поведения совместимого TCI-клиента."""

    def __init__(self, ws, rx, verdict):
        self.ws = ws
        self.rx = str(rx)
        self.v = verdict
        # Состояние, которое MSHV ведёт по ФИЛЬТРОВАННОМУ потоку строк.
        self.tx_enable = False
        self.rx_enable = False
        self.rx_mute = False
        self.split_enable = False
        self.drive = 0
        self.is_trx = False
        self.vfo_hz = None
        self.mode = None
        self.device = "NONE"
        self.vendor = "ExpertSDR3"
        self.proto_id = 0        # 0, 1 = 1.5.0 и выше, 2 = 1.9.0 и выше
        self.audio_blocks = 0
        self.audio_foreign = 0   # блоки чужих приёмников (MSHV их выбрасывает)
        self.seen_for_me = []    # что вообще дошло до логики MSHV
        self.seen_all = []

    # ── приём ────────────────────────────────────────────────────────────
    def pump(self, seconds):
        deadline = time.monotonic() + seconds
        while time.monotonic() < deadline:
            frame = self.ws.recv_frame(min(0.2, deadline - time.monotonic()))
            if frame is None:
                continue
            opcode, payload = frame
            if opcode == 1:
                for command in split_commands(payload.decode("utf-8", "replace")):
                    self.on_text(command)
            elif opcode == 2:
                self.on_binary(payload)

    def on_text(self, command):
        name, args = parse_command(command)
        self.seen_all.append(command)
        args = args + ["", "", ""]

        # protocol разбирается ДО фильтра по приёмнику (так же у MSHV).
        if name == "protocol":
            self.vendor = args[0]
            parts = (args[1].split(".") + ["0", "0", "0"])[:3]
            v0, v1, v2 = (int(p) if p.isdigit() else 0 for p in parts)
            self.proto_id = 2
            if v0 < 2 and v1 < 9 and v2 < 1000:
                self.proto_id = 1
            if v0 < 2 and v1 < 5 and v2 < 1000:
                self.proto_id = 0
            return
        if name == "device":
            self.device = args[0]
            return

        # ★Фильтр MSHV: всё, адресованное не нашему приёмнику, не существует.
        if args[0] != self.rx:
            return
        self.seen_for_me.append(command)

        if name == "vfo" and args[1] == "0":
            self.vfo_hz = args[2]
        elif name == "modulation":
            self.mode = args[1]
        elif name == "trx":
            self.is_trx = args[1] == "true"
        elif name == "tx_enable":
            self.tx_enable = args[1] == "true"
        elif name == "rx_enable":
            self.rx_enable = args[1] == "true"
        elif name == "rx_mute":
            self.rx_mute = args[1] == "true"
        elif name == "split_enable":
            self.split_enable = args[1] == "true"
        elif name == "drive":
            self.drive = int(args[1]) if args[1].lstrip("-").isdigit() else 0

    def on_binary(self, payload):
        if len(payload) < HDR.size:
            return
        head = HDR.unpack(payload[:HDR.size])
        # Блоки других приёмников клиент игнорирует.
        if head[0] != int(self.rx):
            self.audio_foreign += 1
            return
        self.audio_blocks += 1

    # ── инициализация: единственная причина «Error To Initialize» ────────
    def initialize(self):
        for attempt in range(1, 6):
            self.ws.send_text(f"vfo:{self.rx},0;")
            self.pump(0.5)
            if self.vfo_hz is not None:
                self.v.ok(f"инициализация прошла с попытки {attempt}",
                          f"vfo:{self.rx},0,{self.vfo_hz}")
                return True
            self.v.note(f"попытка {attempt}: ответа vfo:{self.rx},0,… нет")
        self.v.fail("Error To Initialize TCI Server",
                    f"за 5 попыток не пришло vfo:{self.rx},0,<частота> — "
                    f"приёмника {self.rx} для сервера не существует")
        return False

    # ── то, что MSHV делает сразу после успешной инициализации ───────────
    def after_connect(self):
        if not self.tx_enable:
            self.ws.send_text(f"tx_enable:{self.rx},true;")
        if not self.rx_enable:
            self.ws.send_text(f"rx_enable:{self.rx},true;")
        if self.rx_mute:
            self.ws.send_text(f"rx_mute:{self.rx},false;")
        if self.split_enable:
            self.ws.send_text(f"split_enable:{self.rx},false;")
        if self.drive <= 0:
            self.ws.send_text(f"drive:{self.rx},25;")
        self.pump(1.0)

    def start_audio(self):
        self.ws.send_text(f"audio_samplerate:{MSHV_RATE};")
        if self.proto_id > 1:
            self.ws.send_text(f"audio_stream_sample_type:{MSHV_TYPE};")
            self.ws.send_text(f"audio_stream_channels:{MSHV_CHANNELS};")
            self.ws.send_text(f"audio_stream_samples:{MSHV_SAMPLES};")
            self.ws.send_text(f"tx_stream_audio_buffering:{MSHV_TXBUF};")
        self.ws.send_text(f"audio_start:{self.rx};")

    def stop_audio(self):
        self.ws.send_text(f"audio_stop:{self.rx};")


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=40001)
    ap.add_argument("--rx", type=int, default=0,
                    help="приёмник TCI: 0 = «TCI Client rx1» в MSHV, 1 = «rx2»")
    ap.add_argument("--audio", action="store_true",
                    help="запросить аудиопоток, как это делает MSHV")
    ap.add_argument("--seconds", type=float, default=3.0,
                    help="сколько слушать поток")
    ap.add_argument("--tx", action="store_true",
                    help="★ВКЛЮЧАЕТ ПЕРЕДАЧУ: короткий trx:<rx>,true,tci")
    args = ap.parse_args()

    v = Verdict()
    print(f"Стенд MSHV: приёмник {args.rx} "
          f"(в MSHV это «TCI Client rx{args.rx + 1}»)")
    try:
        ws = WS(args.host, args.port)
    except Exception as exc:
        v.fail("подключение к TCI-серверу", str(exc))
        print(f"\nИтого: {v.ok_count} ok, {v.fail_count} FAIL")
        return 1

    client = MshvClient(ws, args.rx, v)
    try:
        # Пачка инициализации приходит сама; MSHV её тоже читает.
        client.pump(1.5)
        v.ok("пачка инициализации получена",
             f"{client.vendor} {client.device}, protocol id {client.proto_id}")

        if not client.initialize():
            return 1

        client.after_connect()

        # Дальше — то, без чего MSHV работает молча неправильно.
        if client.tx_enable:
            v.ok(f"tx_enable для приёмника {args.rx} получен")
        else:
            v.fail(f"tx_enable для приёмника {args.rx} НЕ получен",
                   "MSHV начинает set_ptt со строки `if (!tci_tx_enable) return;` "
                   "— PTT будет молча мёртв")
        if client.mode:
            v.ok("модуляция известна", client.mode)
        else:
            v.fail("modulation для этого приёмника не приходила",
                   "MSHV не узнает вид связи")

        if args.audio:
            client.start_audio()
            client.pump(args.seconds)
            if client.audio_blocks > 0:
                v.ok("аудиопоток идёт", f"{client.audio_blocks} блоков")
            else:
                v.fail("блоков аудио нет",
                       f"чужих (не нашего приёмника) отброшено: {client.audio_foreign}")
            client.stop_audio()
            client.pump(0.3)

        if args.tx:
            print("  ..   ★передача: trx:%d,true,tci" % args.rx)
            client.ws.send_text(f"trx:{args.rx},true,tci;")
            client.pump(0.7)
            if client.is_trx:
                v.ok("MSHV увидел бы себя в эфире", f"trx:{args.rx},true")
            else:
                v.fail("подтверждения передачи для своего приёмника нет",
                       "MSHV фильтрует ответ по номеру приёмника: "
                       "строку с чужим номером он не увидит")
            client.ws.send_text(f"trx:{args.rx},false;")
            client.pump(0.5)

        print(f"\nСтрок, дошедших до логики MSHV: {len(client.seen_for_me)} "
              f"из {len(client.seen_all)} присланных")
    finally:
        ws.close()

    print(f"\nИтого: {v.ok_count} ok, {v.fail_count} FAIL")
    return 1 if v.fail_count else 0


if __name__ == "__main__":
    sys.exit(main())
