import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../infra/prisma.service';
import { NotificationsService } from '../notifications.service';
import { BotCallbackRegistryService } from './bot-callback-registry.service';
import { formatTokenForLog } from './bot-debug-log.util';
import { BotFatherService } from './bot-father.service';
import { BotRateLimitService } from './bot-rate-limit.service';
import { BotUpdateQueueService } from './bot-update-queue.service';
import { deriveTelegramChatId, hashBotToken } from './bot-token.util';
import { serializeTelegramResponse, telegramError, telegramOk } from './telegram-response.util';
import { TelegramSerializerService } from './telegram-serializer.service';
import { WebAppCryptoService } from './webapp-crypto.service';
import { BOTFATHER_BOT_USERNAME, buildBotManageWebAppUrl } from './bot-father.constants';
import {
  menuButtonToJson,
  parseMenuButtonInput,
  resolveComposerMenuButton,
} from './bot-menu-button.util';
import { assertHumanAccount } from '../system-account.util';

type ValidatedBot = {
  id: string;
  name: string;
  username: string;
  ownerId: string;
  webAppUrl?: string | null;
  menuButton?: unknown;
  webhookUrl?: string | null;
  webhookSecretToken?: string | null;
  isActive: boolean;
  isSystemBot: boolean;
};

type ChatPayload = {
  chat_id?: string | number;
  message_id?: number | string;
};

type SendMessagePayload = ChatPayload & {
  text?: string;
  parse_mode?: string;
  reply_markup?: unknown;
  disable_web_page_preview?: boolean;
};

type EditMessageTextPayload = ChatPayload & {
  text?: string;
  parse_mode?: string;
  reply_markup?: unknown;
};

type EditMessageReplyMarkupPayload = ChatPayload & {
  reply_markup?: unknown;
};

type PinChatMessagePayload = ChatPayload & {
  disable_notification?: boolean;
};

type AnswerCallbackQueryPayload = {
  callback_query_id?: string;
  text?: string;
  show_alert?: boolean;
  url?: string;
};

type MediaPayload = ChatPayload & {
  photo?: string;
  document?: string;
  caption?: string;
  reply_markup?: unknown;
};

type SetWebhookPayload = {
  url?: string;
  secret_token?: string;
  drop_pending_updates?: boolean;
};

type GetUpdatesPayload = {
  offset?: number | string;
  limit?: number | string;
  timeout?: number | string;
};

type SetChatMenuButtonPayload = {
  chat_id?: string | number | null;
  menu_button?: unknown;
};

@Injectable()
export class BotApiService {
  private readonly logger = new Logger(BotApiService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly botFather: BotFatherService,
    private readonly rateLimit: BotRateLimitService,
    private readonly notifications: NotificationsService,
    private readonly webAppCrypto: WebAppCryptoService,
    private readonly updateQueue: BotUpdateQueueService,
    private readonly serializer: TelegramSerializerService,
    private readonly callbackRegistry: BotCallbackRegistryService
  ) {}

  async executeMethod(token: string, method: string, payload: Record<string, unknown>) {
    const bot = (await this.botFather.validateBotToken(token)) as ValidatedBot | null;
    if (!bot) {
      this.logger.warn(`Unauthorized Bot API call\n${formatTokenForLog(token)}`);
      return {
        httpStatus: 401,
        responseJson: serializeTelegramResponse(telegramError(401, 'Unauthorized'))
      };
    }

    const rateCheck = await this.rateLimit.assertBotApiRateLimit(hashBotToken(token));
    if (!rateCheck.allowed) {
      return {
        httpStatus: 429,
        responseJson: serializeTelegramResponse(
          telegramError(429, 'Too Many Requests: retry later', { retry_after: rateCheck.retryAfter })
        )
      };
    }

    const normalizedMethod = method.trim();
    this.logger.debug(`Bot API ${normalizedMethod} bot=@${bot.username}\n${formatTokenForLog(token)}`);

    switch (normalizedMethod) {
      case 'getMe':
        return this.wrapOk(this.buildUserObject(bot));
      case 'getChat':
        return this.getChat(bot, payload as ChatPayload);
      case 'sendMessage':
        return this.sendMessage(bot, payload as SendMessagePayload);
      case 'editMessageText':
        return this.editMessageText(bot, payload as EditMessageTextPayload);
      case 'editMessageReplyMarkup':
        return this.editMessageReplyMarkup(bot, payload as EditMessageReplyMarkupPayload);
      case 'pinChatMessage':
        return this.setMessagePinned(bot, payload as PinChatMessagePayload, true);
      case 'unpinChatMessage':
        return this.setMessagePinned(bot, payload as ChatPayload, false);
      case 'answerCallbackQuery':
        return this.answerCallbackQuery(payload as AnswerCallbackQueryPayload);
      case 'sendPhoto':
        return this.sendPhoto(bot, payload as MediaPayload);
      case 'sendDocument':
        return this.sendDocument(bot, payload as MediaPayload);
      case 'setWebhook':
        return this.setWebhook(bot, payload as SetWebhookPayload);
      case 'deleteWebhook':
        return this.deleteWebhook(bot, payload);
      case 'getWebhookInfo':
        return this.getWebhookInfo(bot);
      case 'getUpdates':
        return this.getUpdates(bot, payload as GetUpdatesPayload);
      case 'setChatMenuButton':
        return this.setChatMenuButton(bot, payload as SetChatMenuButtonPayload);
      default:
        return {
          httpStatus: 404,
          responseJson: serializeTelegramResponse(telegramError(404, `Method '${normalizedMethod}' is not supported`))
        };
    }
  }

  validateWebAppInitData(initData: string, botToken: string) {
    this.logger.debug(`Validate WebApp initData\n${formatTokenForLog(botToken)}`);
    const result = this.webAppCrypto.validateWebAppData(initData, botToken);
    if (!result.valid) {
      return { valid: false, error: result.error };
    }
    return {
      valid: true,
      userJson: result.userJson,
      authDate: result.authDate
    };
  }

  async sendInternalBotMessage(
    botId: string,
    recipientUserId: string,
    text: string,
    replyMarkup?: Record<string, unknown> | null
  ) {
    const bot = await this.prisma.bot.findUnique({
      where: { id: botId },
      select: { id: true, name: true, username: true, isActive: true }
    });
    if (!bot?.isActive) {
      throw new Error('Системный бот недоступен');
    }

    const context = await this.resolveChatContext(bot.id, recipientUserId);
    if (!context) {
      throw new Error('Получатель не найден');
    }

    const parsedMarkup = this.serializer.parseReplyMarkup(replyMarkup ?? null);
    const message = await this.createOutboundMessage(bot.id, context.chat.id, {
      messageType: 'text',
      text,
      payload: this.serializer.buildStoredPayload(parsedMarkup)
    });

    await this.publishBotMessage(context.recipient.id, bot as ValidatedBot, context.chat, message, parsedMarkup.internal);
    return message;
  }

  async listBotChatMessages(userId: string, botRef: string, roomId?: string) {
    const sender = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { isSystemAccount: true, linkedBotId: true, deletedAt: true, status: true }
    });
    if (!sender || sender.deletedAt || sender.status !== 'ACTIVE') {
      return { messages: [], botUsername: botRef, botDisplayName: 'Bot' };
    }
    assertHumanAccount(sender, { message: 'Системные учётные записи не могут просматривать чаты ботов' });

    const bot = await this.prisma.bot.findFirst({
      where: {
        OR: [
          { id: botRef },
          { username: botRef },
          { username: `${botRef.replace(/_bot$/i, '')}_bot` }
        ]
      },
      select: {
        id: true,
        name: true,
        username: true,
        isActive: true,
        isSystemBot: true,
        webAppUrl: true,
        menuButton: true,
        ownerId: true
      }
    });
    if (!bot?.isActive) {
      return {
        messages: [],
        botUsername: botRef,
        botDisplayName: 'Bot',
        composerWebAppUrl: undefined,
        composerMenuButtonJson: undefined
      };
    }

    const chat = roomId
      ? await this.resolveSharedFamilyBotChat(bot.id, userId, roomId)
      : await this.prisma.botChat.findUnique({
          where: { botId_recipientUserId: { botId: bot.id, recipientUserId: userId } },
          include: { menuButton: true }
        });

    const composer = this.resolveComposerForBot(bot, chat?.menuButton?.menuButton ?? null);
    const emptyResponse = {
      messages: [] as Array<Record<string, unknown>>,
      botUsername: bot.username,
      botDisplayName: bot.name,
      botId: bot.id,
      botOwnerId: bot.ownerId,
      composerWebAppUrl: composer.composerWebAppUrl,
      composerMenuButtonJson: composer.composerMenuButtonJson,
      manageWebAppUrl: buildBotManageWebAppUrl(bot.id)
    };

    if (!chat) {
      return emptyResponse;
    }

    const [outbound, inbound] = await Promise.all([
      this.prisma.botMessage.findMany({
        where: { botChatId: chat.id },
        orderBy: { createdAt: 'asc' }
      }),
      this.prisma.botInboundMessage.findMany({
        where: { botChatId: chat.id },
        orderBy: { createdAt: 'asc' }
      })
    ]);

    const messages = [
      ...inbound.map((item) => ({
        id: `in-${item.id}`,
        direction: 'in' as const,
        text: item.text ?? '',
        messageType: 'text',
        messageId: item.telegramMsgId,
        createdAt: item.createdAt.toISOString(),
        replyMarkupJson: null as string | null,
        isPinned: false,
        pinnedAt: null as string | null
      })),
      ...outbound.map((item) => {
        const payload = this.serializer.readPayload(item.payload);
        const replyMarkup = payload.telegramReplyMarkup ?? payload.replyMarkup;
        return {
          id: `out-${item.id}`,
          direction: 'out' as const,
          text: item.text ?? '',
          messageType: item.messageType ?? 'text',
          messageId: item.telegramMsgId,
          createdAt: item.createdAt.toISOString(),
          replyMarkupJson: replyMarkup ? JSON.stringify(replyMarkup) : null,
          isPinned: item.isPinned,
          pinnedAt: item.pinnedAt?.toISOString() ?? null
        };
      })
    ].sort((a, b) => a.createdAt.localeCompare(b.createdAt));

    return {
      messages,
      botUsername: bot.username,
      botDisplayName: bot.name,
      botId: bot.id,
      botOwnerId: bot.ownerId,
      composerWebAppUrl: composer.composerWebAppUrl,
      composerMenuButtonJson: composer.composerMenuButtonJson,
      manageWebAppUrl: buildBotManageWebAppUrl(bot.id)
    };
  }

  private resolveComposerForBot(
    bot: { isSystemBot: boolean; username: string; webAppUrl?: string | null; menuButton?: unknown },
    chatOverride: unknown
  ) {
    const resolved = resolveComposerMenuButton({
      chatOverride,
      globalMenuButton: bot.menuButton,
      bot
    });
    return {
      composerWebAppUrl: resolved.composerWebAppUrl,
      composerMenuButtonJson: resolved.menuButton ? JSON.stringify(resolved.menuButton) : undefined,
      buttonText: resolved.buttonText
    };
  }

  private async resolveSharedFamilyBotChat(botId: string, viewerUserId: string, roomId: string) {
    const room = await this.prisma.chatRoom.findUnique({
      where: { id: roomId },
      include: {
        members: { include: { user: { select: { linkedBotId: true } } } }
      }
    });
    if (!room || room.type !== 'BOT' || !room.members.some((member) => member.userId === viewerUserId)) {
      return null;
    }
    const botMember = room.members.find((member) => member.user.linkedBotId === botId);
    if (!botMember || room.peerUserId !== botMember.userId) {
      return null;
    }
    const ownerUserId =
      room.createdById ??
      room.members.find((member) => !member.user.linkedBotId)?.userId ??
      viewerUserId;
    return this.prisma.botChat.findUnique({
      where: { botId_recipientUserId: { botId, recipientUserId: ownerUserId } },
      include: { menuButton: true }
    });
  }

  private async setChatMenuButton(bot: ValidatedBot, payload: SetChatMenuButtonPayload) {
    const chatIdRaw = payload.chat_id;
    const parsedMenuButton = parseMenuButtonInput(payload.menu_button);
    const isGlobal =
      chatIdRaw === undefined ||
      chatIdRaw === null ||
      (typeof chatIdRaw === 'string' && chatIdRaw.trim() === '');

    if (isGlobal) {
      const stored = menuButtonToJson(parsedMenuButton ?? null);
      await this.prisma.bot.update({
        where: { id: bot.id },
        data: { menuButton: stored as never }
      });
      await this.botFather.invalidateBotCacheById(bot.id);

      const chats = await this.prisma.botChat.findMany({
        where: { botId: bot.id, recipientUserId: { not: null } },
        include: { menuButton: true }
      });

      await Promise.all(
        chats.map(async (chat) => {
          if (!chat.recipientUserId) return;
          const composer = this.resolveComposerForBot(
            { ...bot, menuButton: stored },
            chat.menuButton?.menuButton ?? null
          );
          await this.publishMenuButtonUpdate(chat.recipientUserId, bot, composer);
        })
      );

      return this.wrapOk(true);
    }

    const context = await this.resolveChatContext(bot.id, String(chatIdRaw));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const shouldClear =
      payload.menu_button === undefined ||
      parsedMenuButton === null ||
      parsedMenuButton === undefined ||
      parsedMenuButton.type === 'default';

    if (shouldClear) {
      await this.prisma.chatMenuButton.deleteMany({ where: { botChatId: context.chat.id } });
    } else {
      const stored = menuButtonToJson(parsedMenuButton);
      if (!stored) {
        return this.wrapBadRequest('Bad Request: menu_button is invalid');
      }
      await this.prisma.chatMenuButton.upsert({
        where: { botChatId: context.chat.id },
        create: {
          botId: bot.id,
          botChatId: context.chat.id,
          menuButton: stored as never
        },
        update: {
          menuButton: stored as never
        }
      });
    }

    const freshBot = await this.prisma.bot.findUnique({
      where: { id: bot.id },
      select: { menuButton: true, isSystemBot: true, username: true, webAppUrl: true }
    });
    const override = shouldClear
      ? null
      : (await this.prisma.chatMenuButton.findUnique({ where: { botChatId: context.chat.id } }))?.menuButton ?? null;
    const composer = this.resolveComposerForBot(
      {
        isSystemBot: freshBot?.isSystemBot ?? bot.isSystemBot,
        username: freshBot?.username ?? bot.username,
        webAppUrl: freshBot?.webAppUrl ?? bot.webAppUrl,
        menuButton: freshBot?.menuButton
      },
      override
    );
    await this.publishMenuButtonUpdate(context.recipient.id, bot, composer);

    return this.wrapOk(true);
  }

  private async publishMenuButtonUpdate(
    userId: string,
    bot: ValidatedBot,
    composer: { composerWebAppUrl?: string; composerMenuButtonJson?: string; buttonText?: string }
  ) {
    await this.notifications.publishRealtime(userId, 'bot_menu_button_updated', bot.name, '', {
      botId: bot.id,
      botUsername: bot.username,
      composerWebAppUrl: composer.composerWebAppUrl ?? null,
      composerMenuButtonJson: composer.composerMenuButtonJson ?? null,
      buttonText: composer.buttonText ?? null
    });
  }

  private async sendMessage(bot: ValidatedBot, payload: SendMessagePayload) {
    const chatIdRaw = payload.chat_id;
    const text = payload.text?.trim();
    if (chatIdRaw === undefined || chatIdRaw === null || chatIdRaw === '') {
      return this.wrapBadRequest('Bad Request: chat_id is required');
    }
    if (!text) {
      return this.wrapBadRequest('Bad Request: text is required');
    }

    const context = await this.resolveChatContext(bot.id, String(chatIdRaw));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const parsedMarkup = this.serializer.parseReplyMarkup(payload.reply_markup);
    const message = await this.createOutboundMessage(bot.id, context.chat.id, {
      messageType: 'text',
      text,
      payload: this.serializer.buildStoredPayload(parsedMarkup)
    });

    await this.publishBotMessage(context.recipient.id, bot, context.chat, message, parsedMarkup.internal);

    return this.wrapOk(this.buildTelegramMessage(bot, context, message));
  }

  private async sendPhoto(bot: ValidatedBot, payload: MediaPayload) {
    const photo = payload.photo?.trim();
    if (!payload.chat_id || !photo) {
      return this.wrapBadRequest('Bad Request: chat_id and photo are required');
    }

    const context = await this.resolveChatContext(bot.id, String(payload.chat_id));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const parsedMarkup = this.serializer.parseReplyMarkup(payload.reply_markup);
    const message = await this.createOutboundMessage(bot.id, context.chat.id, {
      messageType: 'photo',
      text: payload.caption?.trim() || null,
      mediaUrl: photo,
      payload: this.serializer.buildStoredPayload(parsedMarkup)
    });

    await this.publishBotMessage(context.recipient.id, bot, context.chat, message, parsedMarkup.internal, 'photo', photo);

    return this.wrapOk(this.buildTelegramMessage(bot, context, message));
  }

  private async sendDocument(bot: ValidatedBot, payload: MediaPayload) {
    const document = payload.document?.trim();
    if (!payload.chat_id || !document) {
      return this.wrapBadRequest('Bad Request: chat_id and document are required');
    }

    const context = await this.resolveChatContext(bot.id, String(payload.chat_id));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const parsedMarkup = this.serializer.parseReplyMarkup(payload.reply_markup);
    const message = await this.createOutboundMessage(bot.id, context.chat.id, {
      messageType: 'document',
      text: payload.caption?.trim() || null,
      mediaUrl: document,
      payload: this.serializer.buildStoredPayload(parsedMarkup)
    });

    await this.publishBotMessage(context.recipient.id, bot, context.chat, message, parsedMarkup.internal, 'document', document);

    return this.wrapOk(this.buildTelegramMessage(bot, context, message));
  }

  private async editMessageText(bot: ValidatedBot, payload: EditMessageTextPayload) {
    const text = payload.text?.trim();
    if (!payload.chat_id || payload.message_id === undefined) {
      return this.wrapBadRequest('Bad Request: chat_id and message_id are required');
    }
    if (!text) {
      return this.wrapBadRequest('Bad Request: text is required');
    }

    const messageId = Number(payload.message_id);
    if (!Number.isFinite(messageId)) {
      return this.wrapBadRequest('Bad Request: message_id is invalid');
    }

    const located = await this.locateMessage(bot.id, String(payload.chat_id), messageId);
    if (!located) {
      return this.wrapBadRequest('Bad Request: message to edit not found');
    }

    const parsedMarkup = payload.reply_markup
      ? this.serializer.parseReplyMarkup(payload.reply_markup)
      : this.serializer.readPayloadAsParsed(located.message.payload);

    const updated = await this.prisma.botMessage.update({
      where: { id: located.message.id },
      data: {
        text,
        editedAt: new Date(),
        payload: this.mergePayload(located.message.payload, parsedMarkup)
      }
    });

    await this.publishBotMessageEdited(located.recipient.id, bot, located.chat, updated, parsedMarkup.internal);

    return this.wrapOk(this.buildTelegramMessage(bot, located, updated));
  }

  private async editMessageReplyMarkup(bot: ValidatedBot, payload: EditMessageReplyMarkupPayload) {
    if (!payload.chat_id || payload.message_id === undefined || payload.reply_markup === undefined) {
      return this.wrapBadRequest('Bad Request: chat_id, message_id and reply_markup are required');
    }

    const messageId = Number(payload.message_id);
    if (!Number.isFinite(messageId)) {
      return this.wrapBadRequest('Bad Request: message_id is invalid');
    }

    const located = await this.locateMessage(bot.id, String(payload.chat_id), messageId);
    if (!located) {
      return this.wrapBadRequest('Bad Request: message to edit not found');
    }

    const parsedMarkup = this.serializer.parseReplyMarkup(payload.reply_markup);
    const updated = await this.prisma.botMessage.update({
      where: { id: located.message.id },
      data: {
        editedAt: new Date(),
        payload: this.mergePayload(located.message.payload, parsedMarkup)
      }
    });

    await this.publishBotMessageEdited(located.recipient.id, bot, located.chat, updated, parsedMarkup.internal);

    return this.wrapOk(this.buildTelegramMessage(bot, located, updated));
  }

  private async getChat(bot: ValidatedBot, payload: ChatPayload) {
    if (!payload.chat_id) {
      return this.wrapBadRequest('Bad Request: chat_id is required');
    }

    const context = await this.resolveChatContext(bot.id, String(payload.chat_id));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const pinned = await this.prisma.botMessage.findFirst({
      where: {
        botId: bot.id,
        botChatId: context.chat.id,
        isPinned: true
      },
      orderBy: { pinnedAt: 'desc' }
    });

    return this.wrapOk({
      id: Number(context.chat.telegramChatId),
      type: context.chat.chatType,
      first_name: context.recipient.displayName,
      ...(context.recipient.username ? { username: context.recipient.username } : {}),
      ...(pinned ? { pinned_message: this.buildTelegramMessage(bot, context, pinned) } : {})
    });
  }

  private async setMessagePinned(bot: ValidatedBot, payload: ChatPayload, pinned: boolean) {
    if (!payload.chat_id) {
      return this.wrapBadRequest('Bad Request: chat_id is required');
    }
    if (pinned && payload.message_id === undefined) {
      return this.wrapBadRequest('Bad Request: message_id is required');
    }

    const messageId = payload.message_id === undefined ? undefined : Number(payload.message_id);
    if (messageId !== undefined && !Number.isFinite(messageId)) {
      return this.wrapBadRequest('Bad Request: message_id is invalid');
    }

    const context = await this.resolveChatContext(bot.id, String(payload.chat_id));
    if (!context) {
      return this.wrapBadRequest('Bad Request: chat not found');
    }

    const message = messageId === undefined
      ? await this.prisma.botMessage.findFirst({
          where: {
            botId: bot.id,
            botChatId: context.chat.id,
            isPinned: true
          },
          orderBy: { pinnedAt: 'desc' }
        })
      : await this.prisma.botMessage.findFirst({
          where: {
            botId: bot.id,
            botChatId: context.chat.id,
            telegramMsgId: messageId
          }
        });

    if (!message) {
      return this.wrapBadRequest(pinned ? 'Bad Request: message to pin not found' : 'Bad Request: message to unpin not found');
    }

    const updated = await this.prisma.botMessage.update({
      where: { id: message.id },
      data: pinned ? { isPinned: true, pinnedAt: new Date() } : { isPinned: false, pinnedAt: null }
    });

    await this.publishBotMessagePinned(context.recipient.id, bot, context.chat, updated);
    return this.wrapOk(true);
  }

  private async answerCallbackQuery(payload: AnswerCallbackQueryPayload) {
    const callbackQueryId = payload.callback_query_id?.trim();
    if (!callbackQueryId) {
      return this.wrapBadRequest('Bad Request: callback_query_id is required');
    }

    const registered = await this.callbackRegistry.resolve(callbackQueryId);
    if (!registered) {
      return this.wrapBadRequest('Bad Request: callback query not found or expired');
    }

    await this.notifications.publishRealtime(registered.userId, 'bot_callback_answer', '', payload.text?.trim() ?? '', {
      callbackQueryId,
      text: payload.text?.trim() ?? null,
      showAlert: Boolean(payload.show_alert),
      url: payload.url?.trim() ?? null,
      botId: registered.botId
    });

    return this.wrapOk(true);
  }

  private async setWebhook(bot: ValidatedBot, payload: SetWebhookPayload) {
    const url = payload.url?.trim();
    if (!url) {
      return this.wrapBadRequest('Bad Request: url is required');
    }
    if (!/^https?:\/\//i.test(url)) {
      return this.wrapBadRequest('Bad Request: invalid webhook url');
    }

    if (payload.drop_pending_updates) {
      await this.updateQueue.clearQueue(bot.id);
      await this.prisma.botUpdate.deleteMany({ where: { botId: bot.id } });
    }

    await this.prisma.bot.update({
      where: { id: bot.id },
      data: {
        webhookUrl: url,
        webhookSecretToken: payload.secret_token?.trim() || null
      }
    });

    await this.botFather.invalidateBotCacheById(bot.id);
    return this.wrapOk(true);
  }

  private async deleteWebhook(bot: ValidatedBot, payload: Record<string, unknown>) {
    if (payload.drop_pending_updates) {
      await this.updateQueue.clearQueue(bot.id);
      await this.prisma.botUpdate.deleteMany({ where: { botId: bot.id } });
    }

    await this.prisma.bot.update({
      where: { id: bot.id },
      data: {
        webhookUrl: null,
        webhookSecretToken: null
      }
    });

    await this.botFather.invalidateBotCacheById(bot.id);
    return this.wrapOk(true);
  }

  private async getWebhookInfo(bot: ValidatedBot) {
    const record = await this.prisma.bot.findUnique({
      where: { id: bot.id },
      select: { webhookUrl: true }
    });

    const pending = record?.webhookUrl ? 0 : await this.updateQueue.pendingCount(bot.id);

    return this.wrapOk({
      url: record?.webhookUrl ?? '',
      has_custom_certificate: false,
      pending_update_count: pending,
      max_connections: 40
    });
  }

  private async getUpdates(bot: ValidatedBot, payload: GetUpdatesPayload) {
    const record = await this.prisma.bot.findUnique({
      where: { id: bot.id },
      select: { webhookUrl: true }
    });
    if (record?.webhookUrl) {
      return this.wrapBadRequest("Bad Request: can't use getUpdates while webhook is active; use deleteWebhook first");
    }

    const offset = payload.offset !== undefined ? Number(payload.offset) : 0;
    const limit = payload.limit !== undefined ? Number(payload.limit) : 100;
    const timeout = payload.timeout !== undefined ? Number(payload.timeout) : 0;

    if (!Number.isFinite(offset) || offset < 0) {
      return this.wrapBadRequest('Bad Request: offset is invalid');
    }
    if (!Number.isFinite(limit) || limit <= 0) {
      return this.wrapBadRequest('Bad Request: limit is invalid');
    }
    if (!Number.isFinite(timeout) || timeout < 0) {
      return this.wrapBadRequest('Bad Request: timeout is invalid');
    }

    const updates = await this.updateQueue.getUpdates(bot.id, offset, limit, timeout);
    return this.wrapOk(updates);
  }

  private async createOutboundMessage(
    botId: string,
    botChatId: string,
    data: {
      messageType: string;
      text?: string | null;
      mediaUrl?: string | null;
      payload?: Record<string, unknown>;
    }
  ) {
    return this.prisma.$transaction(async (tx) => {
      const currentBot = await tx.bot.update({
        where: { id: botId },
        data: { nextMessageId: { increment: 1 } },
        select: { nextMessageId: true }
      });
      const telegramMsgId = currentBot.nextMessageId - 1;

      return tx.botMessage.create({
        data: {
          botId,
          botChatId,
          telegramMsgId,
          messageType: data.messageType,
          text: data.text ?? null,
          mediaUrl: data.mediaUrl ?? null,
          payload: data.payload as never
        }
      });
    });
  }

  private async locateMessage(botId: string, chatIdRaw: string, telegramMsgId: number) {
    const context = await this.resolveChatContext(botId, chatIdRaw);
    if (!context) return null;

    const message = await this.prisma.botMessage.findFirst({
      where: {
        botId,
        botChatId: context.chat.id,
        telegramMsgId
      }
    });
    if (!message) return null;

    return { ...context, message };
  }

  private async resolveChatContext(botId: string, chatIdRaw: string) {
    const recipient = await this.resolveRecipient(chatIdRaw, botId);
    if (!recipient) return null;

    const chat = await this.ensureBotChat(botId, recipient.id, chatIdRaw);
    return { recipient, chat };
  }

  private buildTelegramMessage(
    bot: ValidatedBot,
    context: {
      recipient: { id: string; displayName: string; username: string | null };
      chat: { telegramChatId: bigint; chatType: string };
    },
    message: {
      telegramMsgId: number;
      text?: string | null;
      messageType?: string | null;
      mediaUrl?: string | null;
      createdAt: Date;
      editedAt?: Date | null;
      payload?: unknown;
      isPinned?: boolean;
      pinnedAt?: Date | null;
    }
  ) {
    return this.serializer.buildBotMessageObject({
      message,
      bot,
      chat: {
        telegramChatId: context.chat.telegramChatId,
        chatType: context.chat.chatType,
        recipientDisplayName: context.recipient.displayName,
        recipientUsername: context.recipient.username
      }
    });
  }

  private async publishBotMessage(
    userId: string,
    bot: ValidatedBot,
    chat: { telegramChatId: bigint; chatType: string },
    message: {
      telegramMsgId: number;
      text?: string | null;
      messageType?: string | null;
      mediaUrl?: string | null;
      payload?: unknown;
    },
    replyMarkup: unknown,
    mediaType?: string,
    mediaUrl?: string
  ) {
    const payloadMarkup = this.serializer.readPayload(message.payload);
    const payload = {
      botId: bot.id,
      botUsername: bot.username,
      chatId: chat.telegramChatId.toString(),
      messageId: message.telegramMsgId,
      text: message.text ?? null,
      messageType: message.messageType ?? mediaType ?? 'text',
      mediaUrl: message.mediaUrl ?? mediaUrl ?? null,
      replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
      telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null
    };
    await this.notifications.publishRealtime(userId, 'bot_message', bot.name, message.text ?? '', payload);
    await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message', message.text ?? '', payload);
  }

  private async publishBotMessagePinned(
    userId: string,
    bot: ValidatedBot,
    chat: { telegramChatId: bigint; chatType: string },
    message: {
      telegramMsgId: number;
      isPinned?: boolean;
      pinnedAt?: Date | null;
    }
  ) {
    const payload = {
      botId: bot.id,
      botUsername: bot.username,
      chatId: chat.telegramChatId.toString(),
      messageId: message.telegramMsgId,
      isPinned: Boolean(message.isPinned),
      pinnedAt: message.pinnedAt?.toISOString() ?? null
    };
    await this.notifications.publishRealtime(userId, 'bot_message_pinned', bot.name, '', payload);
    await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message_pinned', '', payload);
  }

  private async publishBotMessageEdited(
    userId: string,
    bot: ValidatedBot,
    chat: { telegramChatId: bigint; chatType: string },
    message: {
      telegramMsgId: number;
      text?: string | null;
      messageType?: string | null;
      mediaUrl?: string | null;
      payload?: unknown;
      editedAt?: Date | null;
    },
    replyMarkup: unknown
  ) {
    const payloadMarkup = this.serializer.readPayload(message.payload);
    const payload = {
      botId: bot.id,
      botUsername: bot.username,
      chatId: chat.telegramChatId.toString(),
      messageId: message.telegramMsgId,
      text: message.text ?? null,
      messageType: message.messageType ?? 'text',
      mediaUrl: message.mediaUrl ?? null,
      replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
      telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null,
      editedAt: message.editedAt?.toISOString() ?? new Date().toISOString()
    };
    await this.notifications.publishRealtime(userId, 'bot_message_edited', bot.name, message.text ?? '', payload);
    await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message_edited', message.text ?? '', payload);
  }

  private async publishToSharedBotRoomMembers(
    ownerUserId: string,
    bot: ValidatedBot,
    eventType: string,
    message: string,
    payload: Record<string, unknown>
  ) {
    const rooms = await this.prisma.chatRoom.findMany({
      where: {
        type: 'BOT',
        botUsername: bot.username,
        createdById: ownerUserId
      },
      include: {
        members: { include: { user: { select: { linkedBotId: true } } } }
      }
    });

    const recipients = new Set<string>();
    for (const room of rooms) {
      for (const member of room.members) {
        if (member.userId !== ownerUserId && !member.user.linkedBotId) {
          recipients.add(member.userId);
        }
      }
    }

    await Promise.all(
      [...recipients].map((recipientId) => this.notifications.publishRealtime(recipientId, eventType, bot.name, message, payload))
    );
  }

  private mergePayload(existing: unknown, parsed: ReturnType<TelegramSerializerService['parseReplyMarkup']>) {
    const current = existing && typeof existing === 'object' ? (existing as Record<string, unknown>) : {};
    const stored = this.serializer.buildStoredPayload(parsed);
    return (stored ? { ...current, ...stored } : current) as never;
  }

  private async resolveRecipient(chatId: string, botId: string) {
    const numericChatId = chatId.match(/^\d+$/) ? BigInt(chatId) : null;
    if (numericChatId) {
      const chat = await this.prisma.botChat.findUnique({
        where: { botId_telegramChatId: { botId, telegramChatId: numericChatId } }
      });
      if (!chat?.recipientUserId) return null;
      return this.prisma.user.findUnique({
        where: { id: chat.recipientUserId },
        select: { id: true, displayName: true, username: true, status: true, deletedAt: true }
      });
    }

    const user = await this.prisma.user.findUnique({
      where: { id: chatId },
      select: { id: true, displayName: true, username: true, status: true, deletedAt: true }
    });
    if (!user || user.deletedAt || user.status !== 'ACTIVE') {
      return null;
    }
    return user;
  }

  private async ensureBotChat(botId: string, recipientUserId: string, chatIdRaw: string) {
    const existing = await this.prisma.botChat.findUnique({
      where: { botId_recipientUserId: { botId, recipientUserId } }
    });
    if (existing) return existing;

    const telegramChatId = chatIdRaw.match(/^\d+$/) ? BigInt(chatIdRaw) : deriveTelegramChatId(`${botId}:${recipientUserId}`);

    try {
      return await this.prisma.botChat.create({
        data: {
          botId,
          recipientUserId,
          telegramChatId,
          chatType: 'private'
        }
      });
    } catch {
      const fallback = await this.prisma.botChat.findUnique({
        where: { botId_recipientUserId: { botId, recipientUserId } }
      });
      if (!fallback) {
        throw new Error('Не удалось создать чат бота');
      }
      return fallback;
    }
  }

  private buildUserObject(bot: ValidatedBot) {
    return {
      id: Number(deriveTelegramChatId(bot.id)),
      is_bot: true,
      first_name: bot.name,
      username: bot.username,
      can_join_groups: true,
      can_read_all_group_messages: false,
      supports_inline_queries: false
    };
  }

  private wrapOk(result: unknown) {
    return {
      httpStatus: 200,
      responseJson: serializeTelegramResponse(telegramOk(result))
    };
  }

  private wrapBadRequest(description: string) {
    return {
      httpStatus: 200,
      responseJson: serializeTelegramResponse(telegramError(400, description))
    };
  }
}
