"""Telegram userbot: вход в аккаунт, /к (карта), /ап (балансы), /нс и /кс (смена).

  pip install telethon
  python userbot.py

api_id и api_hash: https://my.telegram.org → API development tools.
Сессия сохраняется в userbot.session — повторный код не нужен.
"""

from __future__ import annotations

import asyncio
import html
import json
import os
import re
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
from pathlib import Path
from getpass import getpass

_ROOT = Path(__file__).resolve().parent.parent
if str(_ROOT) not in sys.path:
    sys.path.insert(0, str(_ROOT))

from telethon import TelegramClient, events
from telethon.errors import FloodWaitError, MessageNotModifiedError, SessionPasswordNeededError
from telethon.tl.types import Channel

from api.db_api import SYSTEM_BY_ARG, get_watch_settings, list_devices, pick_card, set_card_flag
from server.config import BOT_TOKEN, DEVICE_CHAT_MAP

# Можно прописать здесь, либо через env: TG_API_ID / TG_API_HASH / TG_PHONE
_API_ID = 21842840
_API_HASH = "1db0b6e807c90e6364287ad8af7fa655"

ROOT_DIR = _ROOT
API_ID = int(os.getenv("TG_API_ID", str(_API_ID)) or 0)
API_HASH = os.getenv("TG_API_HASH", _API_HASH)
SESSION_NAME = os.getenv("TG_SESSION", "userbot")
SESSION_PATH = str(ROOT_DIR / SESSION_NAME)
PHONE = os.getenv("TG_PHONE", "") or None
PANEL_API = os.getenv("PANEL_API", "https://oz-panel.ru:5001/api")
SEND_BOT = "send"
BANKER_ID = 8929791739
BANKER_USERNAME = "zzzoobank"
TG_ALBUM_MAX = 10
TG_CAPTION_MAX = 1024
BUSY_TEXT = "Уже идёт обновление"

COMMAND_CATCH_RE = re.compile(r"^/к(?:@[\w]+)?(?:\s|$)")
COMMAND_RE = re.compile(
    r"^/к(?:@[\w]+)?(?:\s+([мб]))?(?:\s+(-[ябм]))?\s*$",
)
AP_CATCH_RE = re.compile(r"^/ап(?:@[\w]+)?(?:\s|$)")
AP_RE = re.compile(r"^/ап(?:@[\w]+)?\s*$")
SHIFT_CATCH_RE = re.compile(r"^/(?:нс|кс|нc|кc)(?:@[\w]+)?(?:\s|$)", re.IGNORECASE)
SHIFT_RE = re.compile(r"^/(нс|кс|нc|кc)(?:@[\w]+)?(?:\s+(.*))?$", re.IGNORECASE)
SEND_AVAILABLE_RE = re.compile(r"Доступно:?\s*([\d][\d\s.,]*)\s*USDT", re.IGNORECASE)
SEND_TETHER_RE = re.compile(r"Tether[^\n]*?:\s*([\d][\d\s.,]*)\s*USDT", re.IGNORECASE)

_ap_lock = asyncio.Lock()
_watch_seen: set[tuple[int, int]] = set()


def parse_k(text: str) -> tuple[str | None, str | None] | None:
    match = COMMAND_RE.match(text.strip())
    if not match:
        return None
    first = match.group(1)
    second_raw = match.group(2)
    second = second_raw[1:] if second_raw else None
    return first, second


def parse_shift(text: str) -> tuple[bool, str] | None:
    match = SHIFT_RE.match((text or "").strip())
    if not match:
        return None
    kind = match.group(1).casefold()
    name = (match.group(2) or "").strip()
    if not name:
        return None
    return kind.startswith("н"), name


def format_balance(value) -> str:
    try:
        number = float(value or 0)
    except (TypeError, ValueError):
        return "0"
    if number == int(number):
        return str(int(number))
    return f"{number:.2f}".rstrip("0").rstrip(".")


def parse_money(value) -> float:
    if value is None or value == "":
        return 0.0
    if isinstance(value, (int, float)):
        return float(value)
    text = str(value).replace("\xa0", " ").replace(" ", "").replace(",", ".")
    text = re.sub(r"[^\d.]", "", text)
    try:
        return float(text) if text else 0.0
    except ValueError:
        return 0.0


def _clean_usdt(raw: str) -> str:
    return re.sub(r"\s+", "", str(raw or "")).rstrip(".")


def parse_send_usdt(text: str) -> str | None:
    available = SEND_AVAILABLE_RE.search(text or "")
    if available:
        return _clean_usdt(available.group(1))
    tether = SEND_TETHER_RE.search(text or "")
    if tether:
        return _clean_usdt(tether.group(1))
    return None


def _safe_shot_name(label: str) -> str:
    cleaned = re.sub(r"[^\w\-]+", "_", label or "", flags=re.UNICODE).strip("_")
    return (cleaned or "lk")[:40]


def _device_targets(rows: list[dict]) -> list[dict]:
    return [row for row in rows if row.get("device") and row.get("ip")]


def format_send_line(send_text: str) -> str:
    lowered = send_text.casefold()
    if "ошибка" in lowered or "usdt" in lowered:
        return send_text
    return f"{send_text} USDT"


def format_shift_report(
    started: bool,
    name: str,
    panel_total: str,
    send_text: str,
    lines: list[str],
) -> str:
    verb = "Начал" if started else "Закончил"
    parts = [
        f"{verb} смену: {name}",
        "",
        f"Панель: {panel_total} ₽",
        f"Send: {format_send_line(send_text)}",
    ]
    if lines:
        parts.append("")
        parts.extend(lines)
    return "\n".join(parts)


def code_line(text: str) -> str:
    return f"<code>{html.escape(text)}</code>"


def format_card(card: dict) -> str:
    lines = []
    for key in ("number", "expiry", "cvv"):
        value = str(card.get(key) or "").strip()
        if value:
            lines.append(code_line(value))
    lines.append(f'<tg-emoji emoji-id="5276037216244624892">💼</tg-emoji> <b><i>Баланс:</i></b> {format_balance(card.get("balance"))}<tg-emoji emoji-id="5255806447106679302">💱</tg-emoji>')
    return "\n".join(lines) if lines else "Нет данных карты"


async def edit_message(
    event: events.NewMessage.Event,
    text: str,
    parse_mode: str | None = None,
) -> None:
    kwargs = {}
    if parse_mode:
        kwargs["parse_mode"] = parse_mode
    try:
        await event.edit(text, **kwargs)
    except MessageNotModifiedError:
        await event.edit(text + "\u200b", **kwargs)
    except FloodWaitError as exc:
        print(f"FloodWait: подожди {exc.seconds} сек.")


async def delete_message(event: events.NewMessage.Event) -> None:
    try:
        await event.delete()
    except FloodWaitError as exc:
        print(f"FloodWait: подожди {exc.seconds} сек.")
    except Exception as exc:
        print(f"Не удалось удалить сообщение: {exc}")


async def issue_card(first: str | None, second: str | None) -> str:
    smaller = first == "м"
    system = SYSTEM_BY_ARG.get(second) if second else None
    card = await asyncio.to_thread(pick_card, smaller=smaller, system=system)
    if not card:
        return "Нет подходящих карт"
    if system:
        saved = await asyncio.to_thread(
            set_card_flag,
            card["device"],
            card["number_digits"],
            system,
            True,
        )
        if not saved:
            return "Карта выбрана, но флаг не сохранился"
    return format_card(card)


def _http_error_detail(exc: urllib.error.HTTPError) -> str:
    body = exc.read().decode("utf-8", errors="replace")
    try:
        payload = json.loads(body)
        detail = payload.get("detail") or body
    except (TypeError, ValueError, json.JSONDecodeError):
        detail = body or str(exc)
    if isinstance(detail, list):
        detail = "; ".join(str(item.get("msg") or item) for item in detail)
    return str(detail)[:200]


def _ssl_context(*, insecure: bool = False) -> ssl.SSLContext:
    if insecure:
        return ssl._create_unverified_context()
    ctx = ssl.create_default_context()
    chain = Path("/etc/letsencrypt/live/oz-panel.ru/fullchain.pem")
    if chain.is_file():
        try:
            ctx.load_verify_locations(cafile=str(chain))
        except Exception:
            pass
    return ctx


def _urlopen(request: urllib.request.Request, timeout: int = 180):
    try:
        return urllib.request.urlopen(request, timeout=timeout, context=_ssl_context())
    except urllib.error.HTTPError:
        raise
    except urllib.error.URLError as exc:
        if "CERTIFICATE_VERIFY_FAILED" not in str(exc):
            raise
        return urllib.request.urlopen(
            request,
            timeout=timeout,
            context=_ssl_context(insecure=True),
        )


def post_check_balance(device_id: str) -> dict:
    url = f"{PANEL_API.rstrip('/')}/devices/{urllib.parse.quote(str(device_id), safe='')}/check/balance"
    request = urllib.request.Request(
        url,
        data=b"",
        method="POST",
        headers={"Accept": "application/json"},
    )
    try:
        with _urlopen(request) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        raise RuntimeError(_http_error_detail(exc)) from exc


async def _check_balance(device_id: str) -> dict:
    panel = sys.modules.get("server.panel_server") or sys.modules.get("panel_server")
    check = getattr(panel, "run_device_check", None) if panel else None
    if check is None:
        return await asyncio.to_thread(post_check_balance, device_id)
    try:
        return await check(device_id, "balance")
    except Exception as exc:
        detail = getattr(exc, "detail", None)
        if detail is not None:
            raise RuntimeError(str(detail)) from exc
        raise


def get_device_screen(device_id: str) -> bytes:
    url = f"{PANEL_API.rstrip('/')}/devices/{urllib.parse.quote(str(device_id), safe='')}/screen"
    request = urllib.request.Request(
        url,
        method="GET",
        headers={"Accept": "image/png, application/json"},
    )
    try:
        with _urlopen(request, timeout=60) as response:
            data = response.read()
    except urllib.error.HTTPError as exc:
        raise RuntimeError(_http_error_detail(exc)) from exc
    if not data:
        raise RuntimeError("пустой скрин")
    if data[:8] != b"\x89PNG\r\n\x1a\n":
        raise RuntimeError("скрин не PNG")
    return data


def _local_capture_screen():
    panel = sys.modules.get("server.panel_server") or sys.modules.get("panel_server")
    capture = getattr(panel, "capture_screen", None) if panel else None
    if capture is not None:
        return capture
    try:
        from server.main import capture_screen as capture
    except Exception:
        try:
            from main import capture_screen as capture
        except Exception:
            return None
    return capture


async def _capture_screen(device_id: str) -> bytes:
    capture = _local_capture_screen()
    if capture is not None:
        return await asyncio.to_thread(capture, device_id)
    return await asyncio.to_thread(get_device_screen, device_id)


def _png_file(label: str, png: bytes) -> BytesIO:
    bio = BytesIO(png)
    bio.name = f"{_safe_shot_name(label)}.png"
    return bio


async def _call_with_flood(factory):
    try:
        return await factory()
    except FloodWaitError as exc:
        await asyncio.sleep(int(exc.seconds) + 1)
        return await factory()


async def resolve_banker(client: TelegramClient):
    last_error: Exception | None = None
    for target in (BANKER_USERNAME, BANKER_ID):
        try:
            return await client.get_entity(target)
        except Exception as exc:
            last_error = exc
    raise RuntimeError(f"не нашёл @{BANKER_USERNAME}: {last_error}") from last_error


async def _ask_bot(conv, client: TelegramClient, entity, text: str) -> str:
    sent = await conv.send_message(text)
    try:
        response = await conv.get_response()
    finally:
        try:
            await sent.delete()
        except Exception:
            pass
    body = (getattr(response, "raw_text", None) or "").strip()
    if parse_send_usdt(body) is None:
        await asyncio.sleep(1.5)
        fresh = await client.get_messages(entity, ids=response.id)
        body = (getattr(fresh, "raw_text", None) or body).strip()
    return body


async def fetch_send_usdt(client: TelegramClient) -> str:
    try:
        entity = await client.get_entity(SEND_BOT)
    except Exception as exc:
        raise RuntimeError(f"не нашёл @{SEND_BOT}: {exc}") from exc
    parsed = None
    text = ""
    try:
        async with client.conversation(entity, timeout=45) as conv:
            text = await _ask_bot(conv, client, entity, "/wallet")
            parsed = parse_send_usdt(text)
            if parsed is None:
                await _ask_bot(conv, client, entity, "/start")
                text = await _ask_bot(conv, client, entity, "/wallet")
                parsed = parse_send_usdt(text)
    except asyncio.TimeoutError as exc:
        raise RuntimeError("Send не ответил на /wallet") from exc
    if parsed is None:
        snippet = re.sub(r"\s+", " ", text)[:180] if text else "пустое сообщение"
        raise RuntimeError(f"не разобрал USDT ({snippet})")
    return parsed


async def send_shift_to_banker(
    client: TelegramClient,
    text: str,
    screenshots: list[tuple[str, bytes]],
) -> None:
    entity = await resolve_banker(client)
    files = [_png_file(label, png) for label, png in screenshots if png]
    if not files:
        await _call_with_flood(lambda: client.send_message(entity, text))
        return

    caption = text if len(text) <= TG_CAPTION_MAX else None
    if caption is None:
        await _call_with_flood(lambda: client.send_message(entity, text))

    for index in range(0, len(files), TG_ALBUM_MAX):
        chunk = files[index:index + TG_ALBUM_MAX]
        chunk_caption = caption if index == 0 else None
        await _call_with_flood(
            lambda chunk=chunk, chunk_caption=chunk_caption: client.send_file(
                entity,
                chunk,
                caption=chunk_caption,
            )
        )


async def run_shift_report(event: events.NewMessage.Event, started: bool, name: str) -> None:
    client = event.client
    verb = "Начал" if started else "Закончил"
    await edit_message(event, f"{verb} смену: {name}\nСобираю балансы…")

    rows = await asyncio.to_thread(list_devices)
    targets = _device_targets(rows)
    screenshots: list[tuple[str, bytes]] = []
    lines: list[str] = []
    panel_total = 0.0
    total = len(targets)

    for index, row in enumerate(targets, 1):
        device_id = row["device"]
        label = (row.get("name") or "").strip() or device_id
        await edit_message(event, f"{verb} смену: {name}\nЛК {index}/{total}: {label}…")
        balance_value = row.get("balance")
        try:
            data = await _check_balance(device_id)
            balance_value = (data.get("device") or {}).get("balance")
            lines.append(f"{label}: {format_balance(balance_value)}")
        except Exception as exc:
            lines.append(f"{label}: ошибка ({exc})")
        panel_total += parse_money(balance_value)
        try:
            png = await _capture_screen(device_id)
            screenshots.append((label, png))
        except Exception as exc:
            lines.append(f"{label}: нет скрина ({exc})")

    await edit_message(event, f"{verb} смену: {name}\nПроверяю Send…")
    try:
        send_text = await fetch_send_usdt(client)
    except Exception as exc:
        send_text = f"ошибка ({exc})"

    panel_text = format_balance(panel_total)
    report = format_shift_report(started, name, panel_text, send_text, lines)
    await edit_message(event, f"{verb} смену: {name}\nОтправляю банкиру…")
    try:
        await send_shift_to_banker(client, report, screenshots)
    except Exception as exc:
        await edit_message(event, f"Не отправил банкиру: {exc}\n\n{report}")
        return

    await edit_message(
        event,
        f"{verb} смену: {name}\n"
        f"Панель: {panel_text} ₽\n"
        f"Send: {format_send_line(send_text)}\n"
        f"Скринов: {len(screenshots)}/{total}\n"
        f"Отправлено @{BANKER_USERNAME}",
    )


async def refresh_all_balances(event: events.NewMessage.Event) -> None:
    rows = await asyncio.to_thread(list_devices)
    targets = _device_targets(rows)
    if not targets:
        await edit_message(event, "Нет девайсов")
        return

    total = len(targets)
    await edit_message(event, f"Обновляю {total} ЛК…")

    async def check_one(row: dict) -> tuple[str, bool, str]:
        device_id = row["device"]
        name = (row.get("name") or "").strip() or device_id
        try:
            data = await _check_balance(device_id)
            balance = (data.get("device") or {}).get("balance")
            return name, True, format_balance(balance)
        except Exception as exc:
            return name, False, f"ошибка ({exc})"

    results = await asyncio.gather(*[check_one(row) for row in targets])
    ok = sum(1 for _, success, _ in results if success)
    fail = total - ok
    lines = [f"{name}: {text}" for name, _, text in results]
    summary = f"Готово: {ok} ок"
    if fail:
        summary += f", {fail} ошибок"
    await edit_message(event, summary + "\n" + "\n".join(lines))


def _alert_chat_id(settings: dict) -> int | None:
    raw = settings.get("alert_chat_id")
    if raw not in (None, ""):
        try:
            return int(raw)
        except (TypeError, ValueError):
            pass
    for value in DEVICE_CHAT_MAP.values():
        if value is not None:
            try:
                return int(value)
            except (TypeError, ValueError):
                continue
    return None


def _internal_chat_id(chat, marked_chat_id=None) -> str | None:
    if isinstance(chat, Channel):
        return str(abs(int(chat.id)))
    marked = marked_chat_id if marked_chat_id is not None else getattr(chat, "id", None)
    if marked is None:
        return None
    raw = str(int(marked))
    if raw.startswith("-100"):
        return raw[4:]
    return None


def _topic_id(message) -> int | None:
    reply_to = getattr(message, "reply_to", None)
    if reply_to is None or not getattr(reply_to, "forum_topic", False):
        return None
    topic = getattr(reply_to, "reply_to_top_id", None) or getattr(reply_to, "reply_to_msg_id", None)
    try:
        topic = int(topic)
    except (TypeError, ValueError):
        return None
    return topic if topic > 0 else None


def _message_link(chat, message, marked_chat_id=None) -> str:
    message_id = int(getattr(message, "id", 0) or 0)
    if message_id <= 0:
        return ""
    username = (getattr(chat, "username", None) or "").strip()
    topic = _topic_id(message)
    if username:
        if topic:
            return f"https://t.me/{username}/{topic}/{message_id}"
        return f"https://t.me/{username}/{message_id}"
    internal = _internal_chat_id(chat, marked_chat_id)
    if not internal:
        return ""
    if topic:
        return f"https://t.me/c/{internal}/{topic}/{message_id}"
    return f"https://t.me/c/{internal}/{message_id}"


def _match_keywords(text: str, keywords: list[str]) -> list[str]:
    haystack = text.casefold()
    found = []
    for word in keywords:
        needle = word.casefold()
        if needle and needle in haystack:
            found.append(word)
    return found


def _post_telegram(data: dict) -> None:
    token = (BOT_TOKEN or "").strip()
    request = urllib.request.Request(
        f"https://api.telegram.org/bot{token}/sendMessage",
        data=json.dumps(data).encode("utf-8"),
        method="POST",
        headers={"Content-Type": "application/json", "Accept": "application/json"},
    )
    with _urlopen(request, timeout=30) as response:
        response.read()


def _sender_label(sender) -> str:
    if sender is None:
        return ""
    username = (getattr(sender, "username", None) or "").strip()
    title = (getattr(sender, "title", None) or "").strip()
    name = title or " ".join(
        part for part in (
            getattr(sender, "first_name", None),
            getattr(sender, "last_name", None),
        ) if part
    ).strip()
    if username and name:
        return f"{name} (@{username})"
    if username:
        return f"@{username}"
    if name:
        return name
    sender_id = getattr(sender, "id", None)
    return str(sender_id) if sender_id else ""


def send_watch_alert(payload: dict) -> None:
    token = (BOT_TOKEN or "").strip()
    chat_id = payload.get("alert_chat_id")
    if not token or chat_id is None:
        return
    keyword = html.escape(str(payload.get("keyword") or ""))
    chat_title = html.escape(str(payload.get("chat_title") or payload.get("chat_id") or "чат"))
    who = html.escape(str(payload.get("sender") or "").strip())
    body = html.escape(str(payload.get("text") or ""))[:3500]
    link = str(payload.get("link") or "").strip()
    lines = [
        f"<tg-emoji emoji-id='5276395476646653290'>🔍</tg-emoji> <b>Ключевое слово:</b> <code>{keyword}</code>",
        f"<tg-emoji emoji-id='5278528159837348960'>📢</tg-emoji> <b>Чат:</b> {chat_title}",
    ]
    if who:
        lines.append(f"<tg-emoji emoji-id='5275979556308674886'>👤</tg-emoji> <b>Кто:</b> {who}")
    markup = None
    if link.startswith("https://"):
        safe_link = html.escape(link, quote=True)
        lines.append("")
        lines.append(f'<tg-emoji emoji-id="5278305362703835500">🔗</tg-emoji> <a href="{safe_link}"><b>Перейти к сообщению</b></a>')
        # lines.append(html.escape(link))
        markup = {
            "inline_keyboard": [[{"text": "Перейти к сообщению", "url": link, "style": "primary", "icon_custom_emoji_id": "5206222720416643915"}]],
        }
    lines.append("")
    lines.append("<tg-emoji emoji-id='5206222720416643915'>🔔</tg-emoji> <b>Сообщение:</b>")
    lines.append(body)
    data = {
        "chat_id": chat_id,
        "text": "\n".join(lines),
        "parse_mode": "HTML",
        "disable_web_page_preview": True,
    }

    try:
        payload_data = dict(data)
        if markup:
            payload_data["reply_markup"] = markup
        _post_telegram(payload_data)
    except Exception: 
        if not markup:
            print("watch alert failed")
            return
        try:
            _post_telegram(data)
        except Exception:
            print("watch alert failed")


async def handle_watch_message(event: events.NewMessage.Event) -> None:
    if event.out:
        return
    text = (event.raw_text or "").strip()
    if not text:
        return
    settings = get_watch_settings()
    if not settings.get("enabled"):
        return
    keywords = settings.get("keywords") or []
    chats = settings.get("chats") or []
    if not keywords or not chats:
        return
    watched = {int(item["id"]) for item in chats if item.get("id") is not None}
    chat_id = event.chat_id
    if chat_id not in watched:
        return
    alert_id = _alert_chat_id(settings)
    if alert_id is not None and chat_id == alert_id:
        return
    sender = event.sender
    if sender is not None and getattr(sender, "bot", False):
        return
    matched = _match_keywords(text, keywords)
    if not matched:
        return
    key = (int(chat_id), int(event.id))
    if key in _watch_seen:
        return
    _watch_seen.add(key)
    if len(_watch_seen) > 4000:
        _watch_seen.clear()
        _watch_seen.add(key)
    if sender is None:
        try:
            sender = await event.get_sender()
        except Exception:
            sender = None
    if sender is not None and getattr(sender, "bot", False):
        return
    chat = await event.get_chat()
    title = getattr(chat, "title", None) or " ".join(
        part for part in (
            getattr(chat, "first_name", None),
            getattr(chat, "last_name", None),
        ) if part
    ) or str(chat_id)
    payload = {
        "alert_chat_id": alert_id,
        "keyword": ", ".join(matched),
        "chat_id": chat_id,
        "chat_title": title,
        "sender": _sender_label(sender),
        "text": text,
        "link": _message_link(chat, event.message, chat_id),
    }
    await asyncio.to_thread(send_watch_alert, payload)


def register_handlers(client: TelegramClient) -> None:
    if getattr(client, "_ozpay_handlers", False):
        return
    client._ozpay_handlers = True
    try:
        get_watch_settings()
    except Exception:
        pass
    @client.on(events.NewMessage(outgoing=True, pattern=COMMAND_CATCH_RE))
    async def on_k(event: events.NewMessage.Event) -> None:
        parsed = parse_k(event.raw_text or "")
        if parsed is None:
            await delete_message(event)
            return
        try:
            text = await issue_card(*parsed)
            parse_mode = "html" if text.startswith("<code>") else None
        except Exception as exc:
            text = f"Ошибка: {exc}"
            parse_mode = None
        await edit_message(event, text, parse_mode=parse_mode)

    @client.on(events.NewMessage(outgoing=True, pattern=AP_CATCH_RE))
    async def on_ap(event: events.NewMessage.Event) -> None:
        if not AP_RE.match((event.raw_text or "").strip()):
            await delete_message(event)
            return
        if _ap_lock.locked():
            await edit_message(event, BUSY_TEXT)
            return
        async with _ap_lock:
            try:
                await refresh_all_balances(event)
            except Exception as exc:
                await edit_message(event, f"Ошибка: {exc}")

    @client.on(events.NewMessage(outgoing=True, pattern=SHIFT_CATCH_RE))
    async def on_shift(event: events.NewMessage.Event) -> None:
        parsed = parse_shift(event.raw_text or "")
        if parsed is None:
            await edit_message(event, "Укажи имя: /нс имя  или  /кс имя")
            return
        if _ap_lock.locked():
            await edit_message(event, BUSY_TEXT)
            return
        started, name = parsed
        async with _ap_lock:
            try:
                await run_shift_report(event, started, name)
            except Exception as exc:
                await edit_message(event, f"Ошибка: {exc}")

    @client.on(events.NewMessage(incoming=True))
    async def on_watch(event: events.NewMessage.Event) -> None:
        try:
            await handle_watch_message(event)
        except Exception as exc:
            print(f"watch handler: {type(exc).__name__}: {exc}")


async def login(client: TelegramClient) -> None:
    print("Вход в Telegram…")
    await client.connect()
    if await client.is_user_authorized():
        return

    phone = PHONE or input("Телефон в международном формате (+7…): ").strip()
    await client.send_code_request(phone)
    code = input("Код из Telegram: ").strip()
    try:
        await client.sign_in(phone, code)
    except SessionPasswordNeededError:
        password = getpass("Пароль двухэтапной аутентификации: ")
        await client.sign_in(password=password)


async def main() -> None:
    if not API_ID or not API_HASH:
        raise SystemExit(
            "Задай TG_API_ID и TG_API_HASH (env) или пропиши их в userbot.py.\n"
            "Ключи: https://my.telegram.org"
        )

    client = TelegramClient(SESSION_PATH, API_ID, API_HASH)
    register_handlers(client)
    await login(client)

    me = await client.get_me()
    username = f"@{me.username}" if me.username else "без username"
    print(f"Готово: {me.first_name} ({username}, id={me.id})")
    print("  /к [м|б] [-я|-б|-м]  — карта (без м/б = больший баланс)")
    print("  /ап                  — обновить балансы всех девайсов")
    print("  /нс имя              — начал смену (балансы, Send, банкир)")
    print("  /кс имя              — закончил смену")
    print("Не запускай вместе с панелью: вход и юзербот уже внутри panel_server.")
    await client.run_until_disconnected()


if __name__ == "__main__":
    asyncio.run(main())
