"""
TelePush — standalone desktop app.
Flask + WebView window + Telethon clients.
"""
import asyncio
import logging
import os
import sys
import time
import threading
from pathlib import Path

# ── Logging ──────────────────────────────────────────────────────────────────

from config import LOG_DIR, TELEPUSH_VERSION, DATA_DIR, WEB_PORT, TELEPUSH_SERVER_URL
# Debug: write paths to check
try:
    with open("startup_debug.txt", "w") as _df:
        import config as _cfg
        _df.write(f"DATA_DIR={_cfg.DATA_DIR}\nLOG_DIR={_cfg.LOG_DIR}\nACCOUNTS_DIR={getattr(_cfg, 'ACCOUNTS_DIR', '?')}\ncwd={os.getcwd()}\nsys.executable={sys.executable}\n")
except Exception:
    pass

# Console handler with UTF-8 (safe for frozen exe where stdout may be None)
import io
from logging.handlers import RotatingFileHandler

_LOG_FILE = LOG_DIR / "telepush.log"
# Если у клиента уже разросся лог до диких размеров — обрезаем до пустого,
# чтобы не держать на диске бесполезный polling-спам.
try:
    if _LOG_FILE.exists() and _LOG_FILE.stat().st_size > 10 * 1024 * 1024:
        _LOG_FILE.write_text("", encoding="utf-8")
    for _old in LOG_DIR.glob("telepush.log.*"):
        try:
            if _old.stat().st_size > 10 * 1024 * 1024:
                _old.unlink()
        except Exception:
            pass
except Exception:
    pass

_handlers = [RotatingFileHandler(
    str(_LOG_FILE),
    maxBytes=3 * 1024 * 1024,   # 3 МБ на файл
    backupCount=2,               # +2 бекапа = максимум ~9 МБ
    encoding="utf-8",
)]
try:
    if sys.stdout and hasattr(sys.stdout, 'buffer') and sys.stdout.buffer:
        _handlers.insert(0, logging.StreamHandler(
            stream=io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
        ))
    elif sys.stderr and hasattr(sys.stderr, 'buffer') and sys.stderr.buffer:
        _handlers.insert(0, logging.StreamHandler(
            stream=io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
        ))
except Exception:
    pass  # No console at all (windowed exe) — file log only

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] %(levelname)-8s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=_handlers,
)
logging.getLogger("werkzeug").setLevel(logging.WARNING)
# Telethon при каждом разрыве/реконнекте льёт WARNING на уровне connection.
# Это нормальное поведение на слабых прокси — не ошибка. Раньше попадало и в
# local-логи, и в remote_logs (тысячи дубликатов на клиента с плохим каналом).
logging.getLogger("telethon.network.connection.connection").setLevel(logging.ERROR)
logging.getLogger("telethon.network.mtprotosender").setLevel(logging.ERROR)
# mtprotostate ругается на «very new message id» при пакетных апдейтах сервера —
# anti-replay защита Telethon, функционально безвредно (см. telethon FAQ).
logging.getLogger("telethon.network.mtprotostate").setLevel(logging.ERROR)
logger = logging.getLogger("telepush")


# ── Разделение логов по модулям (структура как у Raptor/Kraken) ────────────
# data/logs/
#   Веб/web.log
#   Парсинг/parser.log
#   Рассылка по чатам/spam.log
#   Рассылка в ЛС/dm.log
#   Подписка/join.log
#   Прогрев/warmup.log
#   Мониторинг/monitor.log
#   Переавторизация/reauth.log
#   Обновления/updater.log
#   Автовозобновление/auto_resume.log
#   telepush.log  (общее — остаётся в корне logs/)
#
# Раньше всё сваливалось в корень logs/ плоско. Теперь каждый модуль в своей
# папке с человекочитаемым названием — клиенту сразу понятно где что искать.
_MODULE_LOG_MAP = {
    "telepush.web":     ("Веб",                                  "web.log"),
    "telepush.parser":  ("Парсинг",         "parser.log"),
    "telepush.spam":    ("Рассылка по чатам", "spam.log"),
    "telepush.dm":      ("Рассылка в ЛС",                          "dm.log"),
    "telepush.join":    ("Подписка",   "join.log"),
    "telepush.warmup":  ("Прогрев",         "warmup.log"),
    "telepush.monitor": ("Мониторинг", "monitor.log"),
    "telepush.reauth":  ("Переавторизация", "reauth.log"),
    "telepush.updater": ("Обновления", "updater.log"),
    "telepush.auto_resume": ("Автовозобновление", "auto_resume.log"),
}

# Миграция: если на предыдущей версии логи писались плоско в data/logs/,
# разбрасываем их по новым подпапкам. Один раз при апдейте.
try:
    for _name, (_subdir, _fname) in _MODULE_LOG_MAP.items():
        _old = LOG_DIR / _fname
        _newdir = LOG_DIR / _subdir
        _new = _newdir / _fname
        if _old.exists() and not _new.exists():
            _newdir.mkdir(parents=True, exist_ok=True)
            try:
                _old.rename(_new)
            except Exception:
                pass
except Exception:
    pass

_module_log_fmt = logging.Formatter(
    "[%(asctime)s] %(levelname)-8s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
for _name, (_subdir, _fname) in _MODULE_LOG_MAP.items():
    try:
        _newdir = LOG_DIR / _subdir
        _newdir.mkdir(parents=True, exist_ok=True)
        _h = RotatingFileHandler(
            str(_newdir / _fname),
            maxBytes=2 * 1024 * 1024,
            backupCount=1,
            encoding="utf-8",
        )
        _h.setFormatter(_module_log_fmt)
        _h.setLevel(logging.INFO)
        _sub = logging.getLogger(_name)
        _sub.addHandler(_h)
    except Exception:
        pass


# ── Lock file ────────────────────────────────────────────────────────────────

_LOCK_PATH = DATA_DIR / "telepush.lock"

def _acquire_lock():
    if _LOCK_PATH.exists():
        try:
            old_pid = int(_LOCK_PATH.read_text().strip())
            try:
                import psutil
                if psutil.pid_exists(old_pid):
                    logger.critical("TelePush already running (PID %d) — exit", old_pid)
                    sys.exit(1)
            except ImportError:
                # psutil недоступен — fallback через os
                try:
                    os.kill(old_pid, 0)  # ping — exception если pid не жив
                    logger.critical("TelePush already running (PID %d, fallback check) — exit", old_pid)
                    sys.exit(1)
                except (OSError, ProcessLookupError):
                    pass  # pid мёртв — продолжаем
        except SystemExit:
            raise
        except Exception as e:
            logger.warning("Lock check error: %s — continuing", e)
    _LOCK_PATH.write_text(str(os.getpid()))

def _release_lock():
    try:
        _LOCK_PATH.unlink(missing_ok=True)
    except Exception:
        pass


# ── Async loop in background thread ─────────────────────────────────────────

def _run_async_loop():
    """Запускает asyncio loop в фоновом потоке для Telethon + scheduler."""
    import state

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    state.event_loop = loop
    state.standalone_mode = True

    # Глушим windows-specific шум от ProactorBasePipeTransport: Telegram
    # сервер закрывает TCP без graceful FIN, asyncio Windows-loop пишет
    # ConnectionResetError [WinError 10054] в default exception handler.
    # Это не баг нашего кода — побочка Windows IOCP. При reconnect Telethon
    # всё равно поднимет соединение заново.
    def _silence_winerror_noise(_loop, ctx):
        exc = ctx.get("exception")
        msg = ctx.get("message", "")
        if exc is not None:
            ename = type(exc).__name__
            estr = str(exc)
            if "ConnectionReset" in ename and ("10054" in estr or "10053" in estr):
                return  # silently swallow
            # Telethon Connection._send_loop при reconnect иногда переиспользует
            # завершённую coroutine → RuntimeError ('cannot reuse already
            # awaited coroutine'). Это баг telethon 1.34, никак не лечится
            # снаружи; в логе спам по 5+ ERROR за коннект. Глушим.
            if isinstance(exc, RuntimeError) and "already awaited" in estr:
                return
        if "ProactorBasePipeTransport" in msg and ("10054" in msg or "10053" in msg):
            return
        # Telethon-internal "Task exception was never retrieved" для
        # завершённых _send_loop корутин — то же самое, шум.
        if "Task exception was never retrieved" in msg and "already awaited" in str(exc or ""):
            return
        # Прочее — стандартный handler.
        _loop.default_exception_handler(ctx)
    loop.set_exception_handler(_silence_winerror_noise)
    # state.bot оставлен как _FakeBot-заглушка. В standalone никто не должен
    # его звать (всё лежит в UI приложения), но если где-то остался legacy —
    # FakeBot молча проглотит вызов.

    # Инициализируем dev и proxy для ensure_connected
    from core.client_builder import load_device, load_proxy
    state.dev = load_device()
    state.proxy_res = load_proxy()

    async def _async_main():
        # Autoload accounts
        from utils.license import LicenseManager
        lm = LicenseManager()
        if lm.is_valid():
            try:
                from core.scheduler import autoload_all_users
                await autoload_all_users()
                logger.info("Accounts autoloaded")
            except Exception as e:
                logger.warning("Autoload: %s", e)

        # Start scheduler
        try:
            from core.scheduler import scheduler_loop
            asyncio.create_task(scheduler_loop())
            logger.info("Scheduler started")
        except Exception as e:
            logger.warning("Scheduler: %s", e)

        # Keep alive
        while not state.shutdown_flag:
            await asyncio.sleep(1)

    try:
        loop.run_until_complete(_async_main())
    except Exception:
        pass
    finally:
        loop.close()


# ── Main ─────────────────────────────────────────────────────────────────────

def _apply_pending_updates():
    """Применяет .new файлы оставшиеся после обновления."""
    if getattr(sys, 'frozen', False):
        app_dir = Path(sys.executable).parent
    else:
        app_dir = Path(__file__).parent
    count = 0
    for new_file in app_dir.rglob("*.new"):
        target = new_file.with_suffix("")  # убираем .new
        try:
            if target.exists():
                target.unlink()
            new_file.rename(target)
            count += 1
        except Exception:
            pass
    if count:
        logger.info("Applied %d pending update files", count)


def _stamp_version_markers():
    """При старте программы пишем текущую версию во все файлы-маркеры, откуда
    её читает лаунчер: .tp_ver (корень), .version (app/), .version (app/_internal/).
    Также чистим установочный корень от файлов-мусора (старый README.txt
    и .version) — лаунчеру они не нужны, клиенту захламляют папку."""
    try:
        from pathlib import Path as _P
        if getattr(sys, 'frozen', False):
            _app_dir = _P(sys.executable).parent        # <app>/
        else:
            _app_dir = _P(__file__).parent              # репо
        _root = _app_dir.parent if _app_dir.name.lower() == "app" else _app_dir
        _targets = [
            _root / ".tp_ver",                # маркер лаунчера (приоритетно)
            _app_dir / ".version",            # папка app (frozen exe)
            _app_dir / "_internal" / ".version",  # _internal (где лаунчер читает real_ver)
        ]
        _written = 0
        for _t in _targets:
            try:
                _t.parent.mkdir(parents=True, exist_ok=True)
                # Снимаем read-only если антивирус/архиватор поставил
                try:
                    if _t.exists():
                        import stat as _st
                        _t.chmod(_t.stat().st_mode | _st.S_IWRITE)
                except Exception:
                    pass
                _t.write_text(TELEPUSH_VERSION, encoding="utf-8")
                _written += 1
            except Exception as _ve:
                logger.debug("[version-stamp] write %s failed: %s", _t, _ve)
        logger.info("[version-stamp] записана версия %s в %d из %d маркеров", TELEPUSH_VERSION, _written, len(_targets))

        # Чистим легаси-файлы из корня установки (создавались до 1.5.43).
        # README.txt теперь только внутри data/. .version в корне — было
        # дублирование с _internal/.version; лаунчер читает только последнее.
        for _legacy in [_root / "README.txt", _root / ".version"]:
            try:
                if _legacy.exists():
                    _legacy.unlink()
                    logger.info("[cleanup] удалён %s", _legacy.name)
            except Exception:
                pass
    except Exception as e:
        logger.debug("[version-stamp] общий фейл: %s", e)


def _write_readme(path, text):
    """Атомарно пишет README если его ещё нет (или размер не совпадает)."""
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        if path.exists():
            try:
                if path.read_text(encoding="utf-8") == text:
                    return
            except Exception:
                pass
        path.write_text(text, encoding="utf-8")
    except Exception:
        pass


def _write_data_readme():
    """Создаёт README в data/ и папках рядом — чтобы клиенту была понятна структура."""
    try:
        readme = DATA_DIR / "README.txt"
        if readme.exists():
            return  # уже есть — не переписываем
        readme.write_text(
            "Папка data/ — внутренние данные TelePush.\n\n"
            "Что где:\n"
            "  db/                       — база данных (статистика, задачи)\n"
            "  logs/                     — логи работы, разбиты по модулям\n"
            "    Веб/                    — Flask и API (web.log)\n"
            "    Парсинг/                — парсинг чатов\n"
            "    Рассылка по чатам/      — массовая рассылка\n"
            "    Рассылка в ЛС/          — DM\n"
            "    Подписка/               — подписка на чаты\n"
            "    Прогрев/                — прогрев аккаунтов\n"
            "    Мониторинг/             — мониторинг чатов\n"
            "    Переавторизация/        — переавторизация + конвертеры tdata\n"
            "    Обновления/             — скачивание/установка апдейтов\n"
            "    Автовозобновление/      — восстановление задач после рестарта\n"
            "    telepush.log            — общее (startup, scheduler)\n"
            "  pending_jobs/             — сохранённое состояние активных задач\n"
            "  staging/                  — временные файлы при добавлении акков\n"
            "  update_tmp/               — временные файлы при обновлении\n"
            "  user_settings.json        — настройки (тайминги, api_id)\n"
            "  license.json              — лицензия\n"
            "  telepush.lock             — маркер что программа запущена\n"
            "\n"
            "Что где ещё (рядом с этой папкой):\n"
            "  ../Аккаунты/              — файлы .session и .json аккаунтов\n"
            "  ../Экспорт/               — результаты задач (рассылка, парсинг и т.д.)\n"
            "\n"
            "При проблемах — нажми в программе 'Упаковать логи' (Настройки), получишь\n"
            "зашифрованный файл .tplog который можно безопасно прислать в поддержку.\n",
            encoding="utf-8",
        )
    except Exception:
        pass

    # README рядом — в логах, аккаунтах, экспорте
    _write_readme(
        LOG_DIR / "README.txt",
        "Папка logs/ — логи работы TelePush по модулям.\n\n"
        "telepush.log       — общий (startup, scheduler, auth)\n"
        "Веб/web.log        — Flask и API endpoints\n"
        "Парсинг/parser.log — парсинг чатов\n"
        "Рассылка по чатам/spam.log — массовая рассылка\n"
        "Рассылка в ЛС/dm.log       — DM рассылка\n"
        "Подписка/join.log          — вступление в чаты\n"
        "Прогрев/warmup.log         — прогрев аккаунтов\n"
        "Мониторинг/monitor.log     — мониторинг ключевых слов\n"
        "Переавторизация/reauth.log — переавторизация + конвертеры tdata\n"
        "Обновления/updater.log     — скачивание апдейтов\n"
        "Автовозобновление/auto_resume.log — восстановление задач\n\n"
        "Размер каждого файла ограничен 2 МБ с 1 бэкапом.\n"
        "Для пересылки в поддержку — используй кнопку «Упаковать логи» в программе,\n"
        "она создаст зашифрованный .tplog (AES-256) безопасно для пересылки.\n",
    )

    try:
        from config import ACCOUNTS_DIR as _AD
        _write_readme(
            _AD / "README.txt",
            "Папка Аккаунты/ — здесь лежат .session и .json файлы аккаунтов Telegram.\n\n"
            "Подпапки — это статусы:\n"
            "  В работе/        — живые рабочие аккаунты\n"
            "  Заморозка/       — аккаунт в 24+ч паузе от Telegram\n"
            "  В спам/          — временный спам-блок\n"
            "  Вечный спам/     — постоянный спам-блок\n"
            "  Мёртвые/         — отозванные сессии\n"
            "  Прогрев/         — аккаунты в режиме warmup\n"
            "  Ферма/           — аккаунты-ботоводы для прогрева\n"
            "  Архив/           — убранные вручную\n"
            "  После переавторизации/ — резервные копии перед переавторизацией\n"
            "  Конвертер tdata → session/ — положи сюда tdata-папки для конвертации\n"
            "  Конвертер session → tdata/ — результаты конвертации в tdata\n\n"
            "ВАЖНО: имя файла должно быть ЧИСЛО.session (без +, пробелов, @).\n"
            "Примеры валидных имён: 79012345678.session, 998773235021.session\n",
        )
    except Exception:
        pass

    try:
        from config import EXPORT_DIR as _ED
        _write_readme(
            _ED / "README.txt",
            "Папка Экспорт/ — результаты работы модулей.\n\n"
            "Внутри раскладка по типам задач — каждый запуск в свою папку с датой:\n"
            "  Парсинг участников/<дата-время>/     — парсинг @юзернеймов из чатов\n"
            "  Парсинг писавших/<дата-время>/       — парсинг авторов сообщений\n"
            "  Сбор ссылок/<дата-время>/            — t.me ссылки из сообщений\n"
            "  Поиск по словам/<дата-время>/        — парсинг по keywords\n"
            "  Глобальный поиск/<дата-время>/       — найденные публичные чаты\n"
            "  Чаты юзера/<дата-время>/             — чаты где состоит пользователь\n"
            "  Парсинг по категориям/<дата-время>/  — категоризированный экспорт\n"
            "  Рассылка/<дата-время>/               — куда ушла рассылка / где не вышло\n"
            "  DM/<дата-время>/                     — результат DM рассылки\n"
            "  Подписка/<дата-время>/               — результат вступления в чаты\n"
            "  Логи/                                — зашифрованные .tplog архивы для саппорта\n",
        )
    except Exception:
        pass


def main():
    _apply_pending_updates()
    _stamp_version_markers()
    _write_data_readme()
    logger.info("TelePush v%s starting...", TELEPUSH_VERSION)

    # Диагностика + одноразовая миграция настроек в стабильное место (~/.telepush/).
    # Начиная с v1.4.43 основное хранилище — ~/.telepush/user_settings.json.
    # Если оно пусто, но в DATA_DIR/user_settings.json есть старые настройки — копируем.
    try:
        from pathlib import Path as _P
        import json as _j

        _primary = _P.home() / ".telepush" / "user_settings.json"  # НОВОЕ основное место
        _legacy  = DATA_DIR / "user_settings.json"                 # СТАРОЕ (до v1.4.43)
        _legacy_bak = DATA_DIR / "user_settings.backup.json"

        logger.info("[settings] primary=%s", _primary)
        logger.info("[settings] legacy=%s", _legacy)

        def _is_valid(path):
            try:
                if not path.exists() or path.stat().st_size == 0:
                    return False
                return bool(_j.loads(path.read_text(encoding="utf-8")))
            except Exception:
                return False

        if _is_valid(_primary):
            _data = _j.loads(_primary.read_text(encoding="utf-8"))
            logger.info("[settings] ✓ primary ok: %d ключей, размер %d байт",
                        len(_data), _primary.stat().st_size)
        else:
            # Primary пусто/отсутствует — автоматически мигрируем с легаси-места
            migrated = False
            for _src in [_legacy, _legacy_bak,
                         DATA_DIR.parent / "data" / "user_settings.json",
                         DATA_DIR.parent.parent / "data" / "user_settings.json"]:
                try:
                    if _is_valid(_src) and _src != _primary:
                        _primary.parent.mkdir(parents=True, exist_ok=True)
                        _primary.write_bytes(_src.read_bytes())
                        logger.warning("[settings] ✓ МИГРИРОВАНО из %s → %s (%d байт)",
                                       _src, _primary, _primary.stat().st_size)
                        migrated = True
                        break
                except Exception as _re:
                    logger.debug("[settings] fallback probe %s: %s", _src, _re)
            if not migrated:
                logger.warning("[settings] настройки отсутствуют — будут дефолты до первого сохранения")
    except Exception as _e:
        logger.warning("[settings] diagnostics failed: %s", _e)

    # 0. Remote logging — ошибки клиентов отправляются на сервер
    try:
        from utils.remote_logger import setup_remote_logging
        setup_remote_logging()
        logger.info("Remote logging enabled")
    except Exception:
        pass

    # 0a. Создаём папку Аккаунты/ + удаляем старую data/sessions/
    try:
        from core.accounts_folder import ensure_accounts_folder
        ensure_accounts_folder()
    except Exception as e:
        logger.error("Failed to create Accounts folder: %s", e)

    # Скрываем служебные файлы лаунчера (.tp_ver, .client_id) — они нужны
    # лаунчеру, но клиенту в папке захламляют. Прячем атрибутом HIDDEN.
    # Также подчищаем install-root от README.txt и старого .version (если
    # вдруг _stamp_version_markers не успел до этой точки).
    try:
        import ctypes
        _root = DATA_DIR.parent
        # legacy-файлы — удаляем
        for _stale in [_root / "README.txt", _root / ".version"]:
            try:
                if _stale.exists():
                    _stale.unlink()
            except Exception:
                pass
        # маркеры лаунчера — прячем
        _HIDDEN = 0x02
        for _hf in [_root / ".tp_ver", _root / ".client_id"]:
            if _hf.exists():
                ctypes.windll.kernel32.SetFileAttributesW(str(_hf), _HIDDEN)
    except Exception:
        pass

    # Удаляем старую data/sessions/ если есть
    try:
        _old_sessions = DATA_DIR / "sessions"
        if _old_sessions.exists():
            # Переносим файлы в Аккаунты/В работе если есть
            from config import USERS_DIR
            for f in list(_old_sessions.rglob("*.session")):
                dst = USERS_DIR / f.name
                if not dst.exists():
                    import shutil
                    shutil.move(str(f), str(dst))
                    jf = f.with_suffix(".json")
                    if jf.exists():
                        shutil.move(str(jf), str(USERS_DIR / jf.name))
            # Удаляем если пустая (+ подпапки)
            import shutil as _sh
            try:
                _sh.rmtree(str(_old_sessions), ignore_errors=True)
            except Exception:
                pass
        # Удаляем staging/ если есть
        _old_staging = DATA_DIR / "staging"
        if _old_staging.exists():
            try:
                _sh.rmtree(str(_old_staging), ignore_errors=True)
            except Exception:
                pass
    except Exception:
        pass

    # 0aa. Авто-обновление лаунчера — кладём exe как launcher_update.exe,
    #      лаунчер сам подхватит на следующем старте (BAT-replace)
    try:
        from config import ACCOUNTS_DIR
        import requests as _req
        _client_root = ACCOUNTS_DIR.parent
        _launcher_path = _client_root / "TelePush.exe"
        # Чистим старый мусор от предыдущих версий (битый auto-update)
        for _junk_name in ["launcher_new.exe"]:
            _jp = _client_root / _junk_name
            if _jp.exists():
                try: _jp.unlink()
                except Exception: pass
        # Прячем служебные файлы (маркер версии и лог лаунчера)
        try:
            import ctypes as _ct
            for _hide_name in [".launcher_v", "launcher.log"]:
                _hp = _client_root / _hide_name
                if _hp.exists():
                    _ct.windll.kernel32.SetFileAttributesW(str(_hp), 0x02)
        except Exception:
            pass
        if _launcher_path.exists():
            resp = _req.get(f"{TELEPUSH_SERVER_URL}/api/version", timeout=5)
            _ver_info = resp.json()
            _rv = _ver_info.get("launcher_version", "")
            if _rv:
                _marker = _client_root / ".launcher_v"
                _current_lv = _marker.read_text().strip() if _marker.exists() else ""
                _pending = _client_root / "launcher_update.exe"
                if _current_lv != _rv and not _pending.exists():
                    logger.info("Downloading launcher v%s...", _rv)
                    _lr = _req.get(f"{TELEPUSH_SERVER_URL}/api/download/launcher", timeout=120, stream=True)
                    _lr.raise_for_status()
                    with open(str(_pending), "wb") as _f:
                        for chunk in _lr.iter_content(8192):
                            _f.write(chunk)
                    _marker.write_text(_rv)
                    # Прячем маркер (точка в начале на Windows не скрывает)
                    try:
                        import ctypes as _ct
                        _ct.windll.kernel32.SetFileAttributesW(str(_marker), 0x02)  # HIDDEN
                    except Exception:
                        pass
                    # BAT-replacer: ждёт 60с (лаунчер точно умрёт), заменяет exe
                    import subprocess as _sp
                    _bat = _client_root / "_lupd.bat"
                    # ping вместо timeout — не требует консоли и не выводит UI
                    _bat.write_text(
                        '@echo off\r\n'
                        'ping 127.0.0.1 -n 61 > nul\r\n'
                        f'move /y "{_pending}" "{_launcher_path}" > nul 2>&1\r\n'
                        'del "%~f0"\r\n',
                        encoding="cp866"
                    )
                    _si = _sp.STARTUPINFO()
                    _si.dwFlags |= _sp.STARTF_USESHOWWINDOW
                    _si.wShowWindow = 0  # SW_HIDE
                    _sp.Popen(['cmd', '/c', str(_bat)],
                              creationflags=0x08000000,  # CREATE_NO_WINDOW
                              startupinfo=_si,
                              stdin=_sp.DEVNULL, stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
                              close_fds=True)
                    logger.info("Launcher v%s — replacer запущен (60с, скрыто)", _rv)
    except Exception as e:
        logger.debug("Launcher update: %s", e)

    # 0b. Stats reporter — отправляет метрики на сервер каждые 5 мин
    try:
        from utils.stats_reporter import start as start_stats
        start_stats()
        logger.info("Stats reporter enabled")
    except Exception as e:
        logger.debug("Stats reporter: %s", e)

    # 1. Lock
    _acquire_lock()

    # 2. License check (non-blocking — UI will handle activation)
    from utils.license import LicenseManager
    lm = LicenseManager()
    if lm.is_valid():
        logger.info("License: %s...", lm.get_key()[:16])
        # Фоновая проверка при старте — шлёт серверу актуальную версию.
        # Без этого админ-панель видит старую версию клиента пока он не
        # подёргает страницы (middleware срабатывает раз в 2 часа).
        def _startup_lic_check():
            try:
                lm.check()
            except Exception as _ce:
                logger.debug("Startup license check failed: %s", _ce)
        threading.Thread(target=_startup_lic_check, daemon=True).start()
    else:
        logger.info("No license — UI will ask for key")

    # 3. Start Flask web server
    from web.app import start_web
    port = start_web(WEB_PORT)
    logger.info("Flask on http://localhost:%d", port)

    # 4. Start asyncio loop in background (Telethon + scheduler)
    async_thread = threading.Thread(target=_run_async_loop, daemon=True)
    async_thread.start()

    # 5. Ensure Edge WebView2 Runtime is installed
    def _ensure_webview2():
        try:
            import winreg
            for _key_path in [
                r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
                r"SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
            ]:
                try:
                    _k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _key_path)
                    winreg.CloseKey(_k)
                    return True
                except FileNotFoundError:
                    continue
        except Exception:
            return True  # Can't check, try anyway
        # Not found — install
        logger.warning("Edge WebView2 Runtime not found, installing...")
        try:
            import subprocess
            import requests as _req
            _wv2_url = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"
            _wv2_path = DATA_DIR / "MicrosoftEdgeWebview2Setup.exe"
            _r = _req.get(_wv2_url, timeout=60)
            _wv2_path.write_bytes(_r.content)
            subprocess.run([str(_wv2_path), "/silent", "/install"], timeout=180)
            logger.info("WebView2 Runtime installed successfully")
            return True
        except Exception as _e:
            logger.error("WebView2 install failed: %s", _e)
            return False

    _ensure_webview2()

    # 6. Open WebView window (MUST be in main thread on Windows)
    try:
        import webview
        logger.info("Opening WebView window...")
        # Иконка для таскбара (Windows)
        _icon_path = None
        for _ip in [Path(__file__).parent / "assets" / "telepush.ico",
                     Path(__file__).parent / "web" / "static" / "favicon.ico"]:
            if _ip.exists():
                _icon_path = str(_ip)
                break
        if _icon_path and sys.platform == "win32":
            try:
                import ctypes
                ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("telepush.app")
            except Exception:
                pass

        window = webview.create_window(
            "TelePush",
            f"http://localhost:{port}",
            width=1280,
            height=820,
            min_size=(960, 640),
            resizable=True,
            text_select=True,
            background_color="#14141e",
            transparent=False,
        )

        def _set_window_icon():
            if not _icon_path or sys.platform != "win32":
                return
            try:
                import ctypes
                from ctypes import wintypes
                user32 = ctypes.windll.user32
                WM_SETICON, ICON_BIG, ICON_SMALL = 0x0080, 1, 0
                hicon = user32.LoadImageW(None, _icon_path, 1, 0, 0, 0x00000010)
                if hicon and window and hasattr(window, 'gui') and hasattr(window.gui, 'BrowserView'):
                    hwnd = user32.FindWindowW(None, "TelePush")
                    if hwnd:
                        user32.SendMessageW(hwnd, WM_SETICON, ICON_BIG, hicon)
                        user32.SendMessageW(hwnd, WM_SETICON, ICON_SMALL, hicon)
                        logger.info("Window icon set from %s", _icon_path)
            except Exception as e:
                logger.debug("Failed to set window icon: %s", e)

        # Ставим иконку после запуска окна
        def _on_shown():
            import time as _t
            _t.sleep(1)
            _set_window_icon()
        threading.Thread(target=_on_shown, daemon=True).start()
        # private_mode=False — localStorage/cookies сохраняются между запусками.
        # Без этого онбординг «Добро пожаловать» показывается каждый запуск,
        # потому что pywebview по умолчанию чистит webview-storage на выход.
        try:
            webview.start(debug=False, private_mode=False)
        except TypeError:
            # старые версии pywebview без private_mode
            webview.start(debug=False)
        logger.info("Window closed, shutting down...")
    except Exception as e:
        logger.error("WebView failed: %s — waiting for manual close", e)
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            pass

    # Cleanup
    import state
    state.shutdown_flag = True
    try:
        from utils.stats_reporter import stop as stop_stats
        stop_stats()
    except Exception:
        pass
    try:
        from core.database import store
        store.close()
    except Exception:
        pass
    _release_lock()
    logger.info("TelePush stopped.")


if __name__ == "__main__":
    import atexit
    atexit.register(_release_lock)
    main()
