"""Сборка MCP-сервера tgclient (FastMCP, streamable HTTP, stateless).

Эндпоинты: /mcp (основной) и /mcp-protocol/ (канон handbook mcp.md — слэш в
конце значим для реверс-прокси). Транспорт stateless + json_response; процесс
тот же, что API деплоя. Авторизация — гард require_mcp на APIRoute
(app/security.py); личность в request.state.mcp_user, здесь переносится в
ContextVar (app/mcp/context.py), тулы читают current_mcp_user().

Инструкции агенту (AGENT_INSTRUCTIONS) отдаются и как instructions, и как
prompt agent_guide (паттерн hard-panel / gnexus-creds).
"""

from fastapi import Request
from mcp.server.fastmcp import FastMCP
from starlette.responses import Response

from app.mcp.context import set_mcp_user, reset_mcp_user
from app.mcp.tools import register_tools

AGENT_INSTRUCTIONS = """\
# tgclient — инструкция для ИИ-агента

## Назначение
Этот сервер — MCP-интерфейс мульти-аккаунтного Telegram-клиента (MTProto
юзер-аккаунты, не боты). Ты действуешь от лица владельца MCP-ключа: у тебя
доступ только к **его** Telegram-аккаунтам (админ-ключ видит все, но писать
обязан всё равно в чужой чат только по явной просьбе этого человека).

## Данные и аккаунты
- **Аккаунты** (`accounts_list`): каждый имеет `id`, phone, статус
  (`pending|active|logged_out|error`) и отметку живости клиента.
- Почти все тулы принимают `account_id`; если у владельца один активный
  аккаунт, указывать его не обязательно (иначе — 422 с просьбой уточнить).
- `dialog_id` — маркированный int из тулов (юзер >0, группа -id,
  канал -100id). Не угадывай id: бери из `dialogs_list` / `messages_*`.
- Имя канала/юзера в запросе («@username») — сначала реши через
  `chat_info`/`dialogs_list`, чтобы получить id.

## Карта инструментов — что вызывать
| Вопрос пользователя | Инструмент |
|---|---|
| «какие у меня акки?» | `accounts_list` |
| «кто я в этом акке?» | `me_get` |
| «список чатов / диалогов» | `dialogs_list` |
| «что в этом чате» (последние сообщения) | `messages_history` |
| «что за сообщение N» | `messages_read` |
| «найди сообщения про X» | `messages_search` |
| «напиши в этот чат …» | `message_send` (или `message_reply`) |
| «поправь сообщение N» | `message_edit` (только свои) |
| «удали сообщения …» | `message_delete` |
| «сходи по контактам / кто в группе» | `contacts_list` / `chat_participants` |
| «что за файл тут?» | `messages_read` (media-метаданные) |
| «скачай/файл есть?» | `download_media` (возвращает base64) |
| «отправь файл/фото» | `upload_file` (base64 или `file_path`) |
| «отправь голосовое» | `upload_voice` (base64 или `file_path`) |
| «отправь кружок» | `upload_round` (base64 или `file_path`) |
| «позвони ему» | `call_start` (сигналинг: ринг у абонента, без аудио) |
| «добавь мой акк / залогинь» | `account_login_start` → `code` → [`password`] |

## Добавление аккаунта (текст-режим — интерактив с человеком)
Телевижн-код и пароль 2FA знает только человек; ты не догадываешься и не
выдумываешь их. Сценарий:
1. Попроси номер телефона и подтверди вслух («логиним +7…123, верно?»).
2. `account_login_start(phone)` → попроси у пользователя код из Telegram.
3. `account_login_code(login_id, code)` — промах не фатален: вернётся
   `attempts_left` — вежливо попроси код ещё раз.
4. Если сервер спросит пароль (`step: "awaiting_password"`) — это
   Cloud-пароль (2FA): попроси его у пользователя, `account_login_password`.
5. Статус/отмена — `account_login_status` / `account_login_cancel`.
Лимиты: ≤3 параллельных логинов, ограничен поток кодов; код живёт ~10 минут.

## Правила
- **Мутации (send/edit/delete/upload/call_start и т.п.) — только по явной
  просьбе пользователя.** Удаление и логаут аккаунта — необратимы.
- **Звонки (`call_start`) — сигналинг без звука** (Telethon не умеет
  аудио-движок tgcalls): абонент увидит настоящий входящий вызов; примет —
  вызов тут же корректно завершится (hangup_after_answer). Предупреждай
  пользователя об этом заранее; статусы — `call_status`/`call_discard`.
- Ошибки — данные: `{"error": 403|404|409|413|422|429, "detail": "…"}` —
  прочти detail и скажи человеку нормально (или исправь вызов сам).
- 403 по аккаунту = не твой аккаунт: не перебирай чужие id.
- 413 — файл больше лимита; 429 — rate-limit/флуд, скажи сколько ждать
  (деталь содержит секунды).
- Голосовой файл (voice) и кружок (round) в медиа-метаданных видны сразу
  (`is_voice`/`is_round`, duration и waveform) — не нужен download для
  понимания, что это голосовое. Достать голосовое: `download_media` вернёт
  ogg-opus в `data_base64`; duration/waveform уже в метаданных.
- Отправка файла с диска сервера: `file_path` — **только имя файла** внутри
  каталога `TGCLIENT_UPLOAD_DIR` (пути запрещены). Каталог не настроен — 503;
  тогда передавайте содержимое `data_base64` как раньше.
- Личные данные (peer-ids, тексты) — конфиденциальны: не выкладывай наружу.

## Подключение
```
claude mcp add --transport http tgclient https://<host>/mcp-protocol/ \\
  --header "Authorization: Bearer mcp_<персональный ключ>"
```
`/mcp` — historический alias того же сервера. Ключи выпускаются на странице
«MCP-ключи» сервиса (≤10 активных на пользователя, отзыв — там же); роль
ключа — снейпшот роли владельца на момент выпуска.
"""


def build_mcp_server() -> FastMCP:
    mcp = FastMCP(
        "tgclient",
        instructions=AGENT_INSTRUCTIONS,
        stateless_http=True,
        json_response=True,
        streamable_http_path="/",
    )
    register_tools(mcp)

    @mcp.prompt()
    def agent_guide() -> str:
        """📘 Гайд: как работать с Telegram через tgclient (аккаунты, чаты, медиа)."""
        return AGENT_INSTRUCTIONS

    return mcp


mcp = build_mcp_server()
mcp_app = mcp.streamable_http_app()

# ASGI-приложениеstreamable-http: FastAPI валидирует сигнатуру endpoint-а,
# поэтому тело в receive() вручную и ответ собираем из send() (stateless +
# json_response — ответ всегда один JSON, SSE-моста нет).
mcp_asgi = mcp_app.router.routes[0].app


async def mcp_endpoint(request: Request) -> "Response":
    """Мост FastAPI → ASGI streamable-http-app.

    Личность из require_mcp (request.state.mcp_user) переносится в ContextVar
    на время выполнения запроса — тулы видят current_mcp_user().
    """
    user = getattr(request.state, "mcp_user", None)
    body = await request.body()
    received = False
    start: dict = {}
    chunks: list[bytes] = []

    async def receive():
        nonlocal received
        if received:
            return {"type": "http.disconnect"}
        received = True
        return {"type": "http.request", "body": body, "more_body": False}

    async def send(message) -> None:
        if message["type"] == "http.response.start":
            start.update(message)
        else:
            chunks.append(message.get("body", b""))

    ctx_token = set_mcp_user(user) if user else None
    try:
        await mcp_asgi(request.scope, receive, send)
    finally:
        if ctx_token is not None:
            reset_mcp_user(ctx_token)
    response = Response(
        content=b"".join(chunks) if chunks else b"", status_code=start["status"]
    )
    response.raw_headers.extend(start.get("headers") or [])
    return response