"""
Remote logger — отправляет логи ERROR/CRITICAL на сервер.
Работает в фоне, не блокирует основной поток.
"""
import logging
import sys
import threading
import time
import traceback
import queue
from typing import List

import requests

from config import TELEPUSH_SERVER_URL, TELEPUSH_VERSION


class RemoteLogHandler(logging.Handler):
    """Logging handler который отправляет логи на сервер пакетами."""

    # Дедуп: одинаковое (level, message) не чаще раза в N секунд.
    _DEDUP_WINDOW = 60
    _DEDUP_MAX = 512

    def __init__(self, hwid: str, min_level=logging.WARNING, batch_size=10, flush_interval=30):
        super().__init__(min_level)
        self.hwid = hwid
        self.batch_size = batch_size
        self.flush_interval = flush_interval
        self._queue: queue.Queue = queue.Queue(maxsize=500)
        self._dedup: dict = {}
        self._dedup_lock = threading.Lock()
        self._thread = threading.Thread(target=self._worker, daemon=True)
        self._thread.start()

    # Чёрный список: эти сообщения не несут actionable-инфы для нас, но Telethon
    # шлёт их сотнями тысяч за сессию (один клиент v1.5.35 нагнал 347k записей).
    # Фильтруем на клиенте — на сервере было ~80% мусора от пары клиентов.
    _NOISE_PATTERNS = (
        "Server sent a very new message",       # mtprotostate время-desync
        "MsgidDecreaseRetry",                   # Telegram-side
        "PersistentTimestampOutdated",          # Telegram-side
        "Could not find a matching Constructor",# TypeNotFoundError, telethon outdated
        "Task was destroyed but it is pending", # GC-warnings, не actionable
        "cannot reuse already awaited coroutine",  # Telethon-internal
        "Error executing high-level request after reconnect",
        "Getting difference for channel updates",
        "ProactorBasePipeTransport",            # Windows IOCP
        "ConnectionResetError",                 # сетевые transient
        "_call_connection_lost",                # Telethon disconnect noise
        "FrozenMethodInvalid",                  # ожидаемая ошибка для frozen-акков
        "Edge WebView2 Runtime not found",      # информационное при первом запуске
    )

    def emit(self, record):
        try:
            msg = self.format(record)
            # Фильтр шума ДО dedup — иначе один client сжирает _DEDUP_MAX
            # уникальными сообщениями (каждый mtprotostate имеет свой ID).
            if any(p in msg for p in self._NOISE_PATTERNS):
                return
            key = (record.levelname, msg)
            now = time.time()
            with self._dedup_lock:
                last = self._dedup.get(key, 0.0)
                if now - last < self._DEDUP_WINDOW:
                    return
                self._dedup[key] = now
                # Чистим устаревшие, если карта разрослась.
                if len(self._dedup) > self._DEDUP_MAX:
                    cutoff = now - self._DEDUP_WINDOW
                    self._dedup = {k: v for k, v in self._dedup.items() if v >= cutoff}
            self._queue.put_nowait({"level": record.levelname, "message": msg})
        except queue.Full:
            pass
        except Exception:
            pass

    def _worker(self):
        """Фоновый поток — отправляет пакеты логов на сервер."""
        buffer: List[dict] = []
        last_flush = time.time()

        while True:
            try:
                entry = self._queue.get(timeout=5)
                buffer.append(entry)
            except queue.Empty:
                pass

            now = time.time()
            if len(buffer) >= self.batch_size or (buffer and now - last_flush >= self.flush_interval):
                self._send(buffer[:self.batch_size])
                buffer = buffer[self.batch_size:]
                last_flush = now

    def _send(self, entries: List[dict]):
        if not entries:
            return
        # Подкладываем license_key — сервер сверяет (key, hwid) и отказывает
        # запросам с чужими hwid → не дать злоумышленнику засрать наши логи.
        key = ""
        try:
            from utils.license import LicenseManager
            key = (LicenseManager()._state.get("key") or "").strip()
        except Exception:
            pass
        try:
            requests.post(
                f"{TELEPUSH_SERVER_URL}/api/log/batch",
                json={
                    "hwid": self.hwid,
                    "key": key,
                    "version": TELEPUSH_VERSION,
                    "logs": entries,
                },
                timeout=10,
            )
        except Exception:
            pass


class _TracebackFormatter(logging.Formatter):
    """Формат который ВКЛЮЧАЕТ traceback (если был exc_info), а не обрезает его.
    Раньше %(message)s терял stacktrace от logger.exception() — теперь нет."""
    def format(self, record):
        s = super().format(record)
        if record.exc_info:
            tb = self.formatException(record.exc_info)
            # Ограничиваем длину — телеграм-алёрты и БД не любят полотен по 50 КБ
            if len(tb) > 4000:
                tb = tb[:4000] + "\n…[truncated]"
            s = s + "\n" + tb
        return s


def setup_remote_logging():
    """Подключает remote logging к корневому логгеру и всем дочерним."""
    try:
        from utils.hwid import generate_hwid
        hwid = generate_hwid()
        handler = RemoteLogHandler(hwid=hwid, min_level=logging.INFO)
        # Формат с traceback если есть exc_info
        handler.setFormatter(_TracebackFormatter("%(name)s: %(message)s"))
        # Ставим на root logger — ловит ВСЕ логгеры (telepush, autojoin, stats, etc)
        logging.getLogger().addHandler(handler)

        # Ловим все необработанные исключения главного потока
        _prev_hook = sys.excepthook
        def _excepthook(exc_type, exc_value, exc_tb):
            try:
                if not issubclass(exc_type, KeyboardInterrupt):
                    tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
                    logging.getLogger("uncaught").critical("Uncaught exception:\n" + tb)
            except Exception:
                pass
            if _prev_hook:
                _prev_hook(exc_type, exc_value, exc_tb)
        sys.excepthook = _excepthook

        # Ловим исключения в тредах (Python 3.8+)
        try:
            _prev_thook = getattr(threading, "excepthook", None)
            def _thread_excepthook(args):
                try:
                    if args.exc_type and not issubclass(args.exc_type, SystemExit):
                        tb = "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback))
                        logging.getLogger("thread").critical("Thread exception in " + str(args.thread.name if args.thread else "?") + ":\n" + tb)
                except Exception:
                    pass
                if _prev_thook:
                    _prev_thook(args)
            threading.excepthook = _thread_excepthook
        except Exception:
            pass
    except Exception:
        pass
