import sys
import json
from socket import *
from datetime import datetime
import logging
import os
import time
import binascii
import requests
import ast
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

HOST = "192.168.127.254"
PORT = 1993
# --- vMix ---
_VMIX_URL = "http://127.0.0.1:8088/API/"
_VMIX_INPUT = "SCOREBUG"  # или твой номер/имя входа, если жёстко задан
_VMIX_COOLDOWN = 5.0
_vmix_suppress_until = 0.0
_vmix_online = None  # None/True/False
_vmix_state = {  # сюда складываем последнее известное состояние
    "TIMER.Text": None,
    "24sec.Text": None,
    "SCORE1.Text": None,
    "SCORE2.Text": None,
    # при желании добавь картинки и др. поля:
    # "24SECBACKGROUND.Source": None,
}

# --- MOXA reconnect/watchdog ---
_SOCK_RECV_TIMEOUT = 2.0  # socket recv() timeout
_IDLE_RECONNECT = 5.0  # если нет данных столько сек — реконнект
_RETRY_BACKOFF = 1.0
_RETRY_BACKOFF_MAX = 10.0
_last_rx_mono = 0.0
_attack_last = None           # последнее принятое значение в секундах (float)
_attack_last_t = 0.0          # когда его приняли (monotonic)
_attack_off_t = -1.0          # когда видели OFF (monotonic)
_ATTACK_OFF_GRACE = 0.8       # окно (сек) после OFF: первое новое значение принимаем всегда
_ATTACK_UP_GLITCH = 0.3       # если выросло больше чем на 0.3 c без OFF — игнорируем
# --- анти-дребезг основного таймера ---
_hold_zero_active = False       # сейчас удерживаем "0.0"
_hold_zero_t0 = 0.0             # момент начала удержания
_HOLD_ZERO_SEC = 1.0            # сколько секунд держать "0.0" (можно 0.8–1.5)
_last_timer_text = None         # последнее отправленное значение



session = requests.Session()
adapter = HTTPAdapter(
    max_retries=Retry(total=0, connect=0, read=0, redirect=0, status=0)
)
session.mount("http://", adapter)
session.mount("https://", adapter)
session.headers.update({"Connection": "keep-alive"})


def hexspace(string, length):
    return " ".join(string[i : i + length] for i in range(0, len(string), length))


def _vmix_resend_full_state():
    """Переотправляет ВСЕ последние значения, когда vMix вернулся онлайн."""
    for name, val in _vmix_state.items():
        if val is None:
            continue
        _send_to_vmix(name, val)


def _send_to_vmix(name: str, value):
    """Низкоуровневый вызов vMix API (без защиты)."""
    try:
        kind = name.split(".")[1]
    except Exception:
        kind = "Text"
    par = "SetText" if kind == "Text" else "SetImage"

    params = {
        "Function": par,
        "Input": _VMIX_INPUT,
        "SelectedName": name,
        "Value": value,
    }
    # короткий таймаут, чтобы не блокировать
    session.get(_VMIX_URL, params=params, timeout=0.25)


def send_data(name: str, value):
    """
    «Мягкая» отправка в vMix: не роняем процесс при оффлайне,
    вводим cooldown, и при появлении онлайна переотправляем весь стейт.
    """
    global _vmix_suppress_until, _vmix_online

    # запоминаем последнее значение
    _vmix_state[name] = value

    now = time.monotonic()
    if now < _vmix_suppress_until:
        return False

    try:
        _send_to_vmix(name, value)
        # если раньше были оффлайн — объявим «online» и добьём состояние
        if _vmix_online is not True:
            print("[vMix] online")
            _vmix_online = True
            _vmix_resend_full_state()
        return True
    except requests.exceptions.RequestException as e:
        # оффлайн
        if _vmix_online is not False:
            msg = f"[vMix] offline: {e.__class__.__name__}: {e}"
            print(msg)
            _vmix_online = False
        _vmix_suppress_until = now + _VMIX_COOLDOWN
        return False


def parse_new(line):
    global _attack_last, _attack_last_t, _attack_off_t
    global _hold_zero_active, _hold_zero_t0, _last_timer_text

    # Преобразуем входящие байты в читаемый HEX
    if line == b"\xff":
        return

    try:
        cdata = binascii.hexlify(line)
        ddata = cdata.decode("utf-8").upper()
        edata = hexspace(ddata, 2)
    except Exception:
        return

    # Разделяем поток на пакеты по сигнатуре начала
    parts = [seg.strip() for seg in edata.split("FF FF 51 7E") if seg.strip()]
    if not parts:
        return

    for i in parts:
        arr = i.split()
        print(arr)
        # ---------------------- 24 СЕКУНД – OFF / RESET ----------------------
        if "89 4E C8 05" in i:
            print(arr, "возможно гашение/включение")
            send_data("24sec.Text", "")
            _attack_last = None
            _attack_off_t = time.monotonic()
            continue
        if "79 84 C0 08" in i:
            print(arr, '2/4 секунды значение')
            idx = None
            for k in range(len(arr) - 3):
                if arr[k:k+4] == ["79", "84", "C0", "08"]:
                    idx = k
                    break
            if idx is None or len(arr) <= idx + 13:
                continue

            seconds_24 = int(arr[idx + 10])
            tenths_24 = int(arr[idx + 11])

            # Гашение при > 24 или 0.0
            if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0):
                send_data("24sec.Text", "")
                _attack_last = None
                continue

            # Формирование отображаемого значения
            shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24)
            if shown in ("0", "0.0"):
                shown = ""

            # Анти-рывок: не принимаем «рост» без OFF
            now = time.monotonic()
            new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0)
            accept = False

            if _attack_last is None:
                accept = True
            elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE:
                accept = True
            else:
                accept = new_val <= _attack_last + _ATTACK_UP_GLITCH

            if accept:
                send_data("24sec.Text", shown)
                _attack_last = new_val
                _attack_last_t = now
            continue
        if "79 84 C8 06" in i:
            idx = None
            for k in range(len(arr) - 3):
                if arr[k:k+4] == ["79", "84", "C8", "06"]:
                    idx = k
                    break
            if idx is None or len(arr) <= idx + 13:
                continue

            seconds_24 = int(arr[idx + 10])
            tenths_24 = int(arr[idx + 11])

            # Гашение при > 24 или 0.0
            if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0):
                send_data("24sec.Text", "")
                _attack_last = None
                continue

            # Формирование отображаемого значения
            shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24)
            if shown in ("0", "0.0"):
                shown = ""

            # Анти-рывок: не принимаем «рост» без OFF
            now = time.monotonic()
            new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0)
            accept = False

            if _attack_last is None:
                accept = True
            elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE:
                accept = True
            else:
                accept = new_val <= _attack_last + _ATTACK_UP_GLITCH

            if accept:
                send_data("24sec.Text", shown)
                _attack_last = new_val
                _attack_last_t = now
            continue

        # ---------------------- 24 СЕКУНД – VALUE ----------------------
        if "79 84 C0 0A" in i:
            # print(arr, '24 секунды значение')
            idx = None
            for k in range(len(arr) - 3):
                if arr[k:k+4] == ["79", "84", "C0", "0A"]:
                    idx = k
                    break
            if idx is None or len(arr) <= idx + 13:
                continue

            seconds_24 = int(arr[idx + 12])
            tenths_24 = int(arr[idx + 13])
            # Гашение при > 24 или 0.0
            if seconds_24 > 24 or (seconds_24 == 0 and tenths_24 == 0):
                send_data("24sec.Text", "")
                _attack_last = None
                continue

            # Формирование отображаемого значения
            shown = f"{seconds_24}.{tenths_24}" if seconds_24 <= 4 else str(seconds_24)
            if shown in ("0", "0.0"):
                shown = ""

            # Анти-рывок: не принимаем «рост» без OFF
            now = time.monotonic()
            new_val = seconds_24 + (tenths_24 / 10.0 if seconds_24 <= 5 else 0.0)
            accept = False

            if _attack_last is None:
                accept = True
            elif (now - _attack_off_t) <= _ATTACK_OFF_GRACE:
                accept = True
            else:
                accept = new_val <= _attack_last + _ATTACK_UP_GLITCH

            if accept:
                send_data("24sec.Text", shown)
                _attack_last = new_val
                _attack_last_t = now
            continue

        # ---------------------- ОСНОВНОЕ ВРЕМЯ ----------------------
        if "7D 4A C0 0A" in i:
            try:
                idx = None
                for k in range(len(arr) - 3):
                    if arr[k:k+4] == ["7D", "4A", "C0", "0A"]:
                        idx = k
                        break
                if idx is None or len(arr) <= idx + 13:
                    continue

                def bcd_to_dec(b):
                    return ((b >> 4) & 0x0F) * 10 + (b & 0x0F)

                # секунды и минуты
                seconds_b = int(arr[idx + 12], 16)
                minutes_b = int(arr[idx + 11], 16)

                # отбросить флаговые старшие биты минут
                minutes_b = minutes_b & 0x0F | ((minutes_b >> 4) & 0x07) << 4

                minutes = bcd_to_dec(minutes_b)
                seconds_raw = bcd_to_dec(seconds_b)

                # нормальный таймер
                if seconds_raw < 60:
                    timer_calc = f"{minutes}:{seconds_raw:02d}"
                else:
                    timer_calc = f"{minutes}.{seconds_raw - 128}"

                # анти-дребезг конца периода
                now = time.monotonic()
                is_zero = (minutes == 0 and seconds_raw == 0)
                is_two0 = (minutes == 2 and seconds_raw == 0)

                if is_zero:
                    _hold_zero_active = True
                    _hold_zero_t0 = now
                    timer_str = "0.0"
                elif _hold_zero_active and is_two0:
                    timer_str = "0.0"
                    if now - _hold_zero_t0 > _HOLD_ZERO_SEC:
                        _hold_zero_active = False
                else:
                    if _hold_zero_active and (now - _hold_zero_t0 > _HOLD_ZERO_SEC):
                        _hold_zero_active = False
                    timer_str = timer_calc

                send_data("TIMER.Text", timer_str)
                _last_timer_text = timer_str

            except Exception:
                continue


        # ---------------------- СЧЁТ ----------------------
        if "7D 4A C0 07" in i:
            try:
                idx = None
                for k in range(len(arr) - 3):
                    if arr[k:k+4] == ["7D", "4A", "C0", "07"]:
                        idx = k
                        break
                if idx is None or len(arr) <= idx + 7:
                    continue

                score1 = int(arr[idx + 6], 16)
                score2 = int(arr[idx + 7], 16)

                send_data("SCORE1.Text", score1)
                send_data("SCORE2.Text", score2)
            except Exception:
                continue
            
            
def _connect_socket():
    s = socket(AF_INET, SOCK_STREAM)
    s.settimeout(_SOCK_RECV_TIMEOUT)
    try:
        s.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
    except OSError:
        pass
    s.connect((HOST, PORT))
    s.send(b"hello")  # как у тебя
    return s


def run_client():
    """Главный цикл: читает данные; при ошибке или простое — переподключается."""
    global _last_rx_mono, _rx_buf

    backoff = _RETRY_BACKOFF
    s = None
    _rx_buf = bytearray()
    _last_rx_mono = time.monotonic()

    while True:
        try:
            if s is None:
                print(f"[MOXA] connecting to {HOST}:{PORT} ...")
                s = _connect_socket()
                print("[MOXA] connected")
                backoff = _RETRY_BACKOFF
                _last_rx_mono = time.monotonic()

            # idle-watchdog: давно нет данных — форсим реконнект
            if time.monotonic() - _last_rx_mono > _IDLE_RECONNECT:
                raise TimeoutError(f"idle {time.monotonic() - _last_rx_mono:.1f}s")

            try:
                chunk = s.recv(1024)
                if not chunk:
                    raise ConnectionResetError("socket closed by peer")
                logging.debug(chunk)
                parse_new(chunk)
            except timeout:
                # socket.timeout: просто следующая итерация; проверим idle
                pass

        except (ConnectionError, OSError, TimeoutError) as e:
            if s is not None:
                try:
                    s.close()
                except Exception:
                    pass
                s = None
            print(f"[MOXA] reconnect in {backoff:.1f}s due to: {e}")
            time.sleep(backoff)
            backoff = min(backoff * 2, _RETRY_BACKOFF_MAX)
            continue


def read_logs():
    path = r"C:\Code\timer\LOGS\timer_Nata_Nport_2025-04-25_14-53-51.log"
    # path = r"C:\Code\timer\LOGS\timer_Nata_Nport_2025-04-25_15-15-42.log"
    with open(path, "r", encoding="utf-8", errors="ignore") as file:
        for line in file:
            parts = line.strip().split(" DEBUG ", 1)
            if len(parts) != 2:
                continue

            payload = parts[1].strip()

            # читаем только байтовые строки
            if payload.startswith("b'") or payload.startswith('b"'):
                try:
                    data = ast.literal_eval(payload)
                    if isinstance(data, (bytes, bytearray)):
                        parse_new(bytes(data))
                except Exception:
                    pass

            time.sleep(0.005)


def main():
    if not os.path.isdir("LOGS"):
        os.mkdir("LOGS")

    LogFileName = "LOGS/timer_SARATOV.log"
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s %(levelname)s %(message)s",
        filename=LogFileName,
        filemode="w",
    )

    try:
        run_client()  # ← онлайн-режим с авто-реконнектом
        # read_logs()  # ← оставь для оффлайн-прогона логов (ручной переключатель)
    except KeyboardInterrupt:
        sys.exit(1)


if __name__ == "__main__":
    main()
