import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
  OnModuleInit
} from '@nestjs/common';
import { PrismaService } from '../../infra/prisma.service';
import { AccessService } from '../access.service';
import { NotificationsService } from '../notifications.service';
import { SettingsService } from '../settings.service';
import { RbacService } from '../rbac.service';
import { BotRateLimitService } from './bot-rate-limit.service';
import { assertValidBotUsername, generateTelegramStyleBotToken, hashBotToken, normalizeBotUsername } from './bot-token.util';
import { menuButtonToJson, parseMenuButtonInput, resolveComposerMenuButton } from './bot-menu-button.util';
import { assertNotReservedUsername, isProtectedSystemAccount } from '../system-account.util';

type BotRecord = {
  id: string;
  name: string;
  username: string;
  tokenHash: string;
  tokenPrefix: string;
  ownerId: string;
  description?: string | null;
  aboutText?: string | null;
  botPicUrl?: string | null;
  menuButton?: unknown;
  webAppUrl: string | null;
  isActive: boolean;
  isSystemBot: boolean;
  createdAt: Date;
  updatedAt: Date;
  owner?: { id: string; displayName: string; username: string | null };
  _count?: { messages: number; chats: number };
};

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly access: AccessService,
    private readonly settings: SettingsService,
    private readonly rateLimit: BotRateLimitService,
    private readonly notifications: NotificationsService,
    private readonly rbac: RbacService
  ) {}

  async onModuleInit() {
    await this.backfillBotSystemUsers();
  }

  async ensureBotSystemUser(botId: string) {
    const bot = await this.prisma.bot.findUnique({
      where: { id: botId },
      include: { linkedSystemUser: { select: { id: true } } }
    });
    if (!bot) {
      throw new NotFoundException('Бот не найден');
    }

    if (bot.linkedSystemUser) {
      return { userId: bot.linkedSystemUser.id, botId: bot.id };
    }

    const existing = await this.prisma.user.findFirst({
      where: { linkedBotId: bot.id },
      select: { id: true }
    });
    if (existing) {
      return { userId: existing.id, botId: bot.id };
    }

    const systemUser = await this.prisma.user.create({
      data: {
        displayName: bot.name,
        isSystemAccount: true,
        isVerified: true,
        verificationIcon: 'bot',
        linkedBotId: bot.id
      },
      select: { id: true }
    });

    await this.rbac.ensureBotUserRole(systemUser.id);

    this.logger.log(`Создан системный пользователь для бота @${bot.username}`);
    return { userId: systemUser.id, botId: bot.id };
  }

  private async backfillBotSystemUsers() {
    const bots = await this.prisma.bot.findMany({
      where: { linkedSystemUser: null },
      select: { id: true, username: true }
    });

    for (const bot of bots) {
      try {
        await this.ensureBotSystemUser(bot.id);
      } catch (error) {
        this.logger.warn(`Не удалось создать системного пользователя для @${bot.username}: ${String(error)}`);
      }
    }
  }

  async createBot(ownerId: string, name: string, username: string) {
    const trimmedName = name.trim();
    if (!trimmedName) {
      throw new BadRequestException('Укажите название бота');
    }

    let normalizedUsername: string;
    try {
      normalizedUsername = assertValidBotUsername(username);
    } catch {
      throw new BadRequestException('Username бота должен содержать 5–32 символа: латиница, цифры и _, без суффикса _bot');
    }
    assertNotReservedUsername(normalizedUsername.replace(/_bot$/, ''));
    const owner = await this.prisma.user.findUnique({ where: { id: ownerId } });
    if (!owner || owner.deletedAt || owner.status !== 'ACTIVE') {
      throw new NotFoundException('Владелец бота не найден');
    }
    if (isProtectedSystemAccount(owner)) {
      throw new BadRequestException('Системные учётные записи не могут владеть ботами');
    }

    const maxBots = await this.settings.getNumber('BOT_MAX_BOTS_PER_USER', 5);
    const ownedCount = await this.prisma.bot.count({ where: { ownerId } });
    if (ownedCount >= maxBots) {
      throw new BadRequestException(`Достигнут лимит ботов на пользователя (${maxBots})`);
    }

    const limitCheck = await this.rateLimit.assertCanCreateBot(ownerId);
    if (!limitCheck.allowed) {
      throw new BadRequestException(limitCheck.reason);
    }

    const { token, tokenPrefix } = generateTelegramStyleBotToken();
    const tokenHash = hashBotToken(token);

    try {
      const bot = await this.prisma.bot.create({
        data: {
          name: trimmedName,
          username: normalizedUsername,
          tokenHash,
          tokenPrefix,
          ownerId
        },
        include: {
          owner: { select: { id: true, displayName: true, username: true } },
          _count: { select: { messages: true, chats: true } }
        }
      });

      await this.ensureBotSystemUser(bot.id);
      await this.rateLimit.syncOwnerBotCount(ownerId, ownedCount + 1);
      return { bot: this.toBotResponse(bot), token };
    } catch (error) {
      if (this.isUniqueViolation(error)) {
        throw new ConflictException('Бот с таким username уже существует');
      }
      throw error;
    }
  }

  async revokeToken(requesterId: string, botId: string, isSuperAdmin: boolean) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin);
    const { token, tokenPrefix } = generateTelegramStyleBotToken();
    const tokenHash = hashBotToken(token);

    const updated = await this.prisma.bot.update({
      where: { id: bot.id },
      data: { tokenHash, tokenPrefix },
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        _count: { select: { messages: true, chats: true } }
      }
    });

    await this.rateLimit.invalidateTokenCache(bot.tokenHash);
    return { bot: this.toBotResponse(updated), token };
  }

  async setWebApp(requesterId: string, botId: string, webAppUrl: string | null | undefined, isSuperAdmin: boolean) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin);
    const normalizedUrl = webAppUrl?.trim() || null;
    if (normalizedUrl && !/^https?:\/\//i.test(normalizedUrl)) {
      throw new BadRequestException('Mini App URL должен начинаться с http:// или https://');
    }

    const updated = await this.prisma.bot.update({
      where: { id: bot.id },
      data: { webAppUrl: normalizedUrl },
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        _count: { select: { messages: true, chats: true } }
      }
    });

    return this.toBotResponse(updated);
  }

  async listMyBots(ownerId: string) {
    const bots = await this.prisma.bot.findMany({
      where: { ownerId },
      orderBy: { createdAt: 'desc' },
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        _count: { select: { messages: true, chats: true } }
      }
    });
    return { bots: bots.map((bot) => this.toBotResponse(bot)), total: bots.length };
  }

  async getBot(requesterId: string, botId: string, isSuperAdmin: boolean) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin, true);
    return this.toBotResponse(bot);
  }

  async updateBot(
    requesterId: string,
    botId: string,
    payload: { name?: string; username?: string },
    isSuperAdmin: boolean
  ) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin);
    const data: { name?: string; username?: string } = {};

    if (payload.name !== undefined) {
      const trimmedName = payload.name.trim();
      if (!trimmedName) {
        throw new BadRequestException('Укажите название бота');
      }
      data.name = trimmedName;
    }

    if (payload.username !== undefined) {
      try {
        data.username = assertValidBotUsername(payload.username);
      } catch {
        throw new BadRequestException('Username бота должен содержать 5–32 символа: латиница, цифры и _, без суффикса _bot');
      }
      if (normalizeBotUsername(payload.username) === normalizeBotUsername(bot.username.replace(/_bot$/, ''))) {
        delete data.username;
      }
    }

    try {
      const updated = await this.prisma.bot.update({
        where: { id: bot.id },
        data,
        include: {
          owner: { select: { id: true, displayName: true, username: true } },
          _count: { select: { messages: true, chats: true } }
        }
      });
      return this.toBotResponse(updated);
    } catch (error) {
      if (this.isUniqueViolation(error)) {
        throw new ConflictException('Бот с таким username уже существует');
      }
      throw error;
    }
  }

  async deleteBot(requesterId: string, botId: string, isSuperAdmin: boolean) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin);
    await this.prisma.bot.delete({ where: { id: bot.id } });
    await this.rateLimit.invalidateTokenCache(bot.tokenHash);
    const ownedCount = await this.prisma.bot.count({ where: { ownerId: bot.ownerId } });
    await this.rateLimit.syncOwnerBotCount(bot.ownerId, ownedCount);
    return { count: 1 };
  }

  async listAllBots(requesterId: string, isSuperAdmin: boolean, search?: string, page = 1, limit = 20) {
    await this.assertManageAllBots(requesterId, isSuperAdmin);
    const safePage = Math.max(page, 1);
    const safeLimit = Math.min(Math.max(limit, 1), 100);
    const where = search?.trim()
      ? {
          OR: [
            { name: { contains: search.trim(), mode: 'insensitive' as const } },
            { username: { contains: normalizeBotUsername(search.trim()), mode: 'insensitive' as const } }
          ]
        }
      : {};

    const [bots, total] = await Promise.all([
      this.prisma.bot.findMany({
        where,
        skip: (safePage - 1) * safeLimit,
        take: safeLimit,
        orderBy: { createdAt: 'desc' },
        include: {
          owner: { select: { id: true, displayName: true, username: true } },
          _count: { select: { messages: true, chats: true } }
        }
      }),
      this.prisma.bot.count({ where })
    ]);

    return { bots: bots.map((bot) => this.toBotResponse(bot)), total };
  }

  async updateBotProfileByOwner(
    ownerId: string,
    botUsernameRaw: string,
    patch: {
      description?: string | null;
      aboutText?: string | null;
      botPicUrl?: string | null;
      menuButton?: unknown;
    }
  ) {
    const bot = await this.findOwnedBotByUsername(ownerId, botUsernameRaw);
    return this.applyBotProfilePatch(bot.id, bot.tokenHash, bot.ownerId, patch);
  }

  async updateBotProfile(
    requesterId: string,
    botId: string,
    patch: {
      description?: string | null;
      aboutText?: string | null;
      botPicUrl?: string | null;
      menuButton?: unknown;
    },
    isSuperAdmin: boolean
  ) {
    const bot = await this.getManagedBot(requesterId, botId, isSuperAdmin);
    return this.applyBotProfilePatch(bot.id, bot.tokenHash, bot.ownerId, patch);
  }

  private async applyBotProfilePatch(
    botId: string,
    tokenHash: string,
    ownerId: string,
    patch: {
      description?: string | null;
      aboutText?: string | null;
      botPicUrl?: string | null;
      menuButton?: unknown;
    }
  ) {
    const data: {
      description?: string | null;
      aboutText?: string | null;
      botPicUrl?: string | null;
      menuButton?: unknown;
    } = {};

    if (patch.description !== undefined) {
      const value = patch.description?.trim() || null;
      if (value && value.length > 512) {
        throw new BadRequestException('Описание бота не может быть длиннее 512 символов');
      }
      data.description = value;
    }

    if (patch.aboutText !== undefined) {
      const value = patch.aboutText?.trim() || null;
      if (value && value.length > 120) {
        throw new BadRequestException('Краткое описание не может быть длиннее 120 символов');
      }
      data.aboutText = value;
    }

    if (patch.botPicUrl !== undefined) {
      const value = patch.botPicUrl?.trim() || null;
      if (value && !/^https?:\/\//i.test(value)) {
        throw new BadRequestException('URL аватара должен начинаться с http:// или https://');
      }
      data.botPicUrl = value;
    }

    if (patch.menuButton !== undefined) {
      const parsed = parseMenuButtonInput(patch.menuButton);
      data.menuButton = menuButtonToJson(parsed ?? null);
    }

    const updated = await this.prisma.bot.update({
      where: { id: botId },
      data: data as never,
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        _count: { select: { messages: true, chats: true } }
      }
    });

    await this.rateLimit.invalidateTokenCache(tokenHash);
    await this.publishBotProfileUpdated(ownerId, updated as BotRecord);

    if (patch.menuButton !== undefined) {
      await this.broadcastGlobalMenuButtonChange(updated as BotRecord);
    }

    return this.toBotResponse(updated as BotRecord);
  }

  async setBotActive(requesterId: string, botId: string, isActive: boolean, isSuperAdmin: boolean) {
    await this.assertManageAllBots(requesterId, isSuperAdmin);
    const bot = await this.prisma.bot.findUnique({ where: { id: botId } });
    if (!bot) {
      throw new NotFoundException('Бот не найден');
    }

    const updated = await this.prisma.bot.update({
      where: { id: botId },
      data: { isActive },
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        _count: { select: { messages: true, chats: true } }
      }
    });

    await this.rateLimit.invalidateTokenCache(updated.tokenHash);
    return this.toBotResponse(updated);
  }

  async getBotMetrics(requesterId: string, isSuperAdmin: boolean) {
    await this.assertManageAllBots(requesterId, isSuperAdmin);
    const [totalBots, activeBots, blockedBots, totalMessages, totalChats] = await Promise.all([
      this.prisma.bot.count(),
      this.prisma.bot.count({ where: { isActive: true } }),
      this.prisma.bot.count({ where: { isActive: false } }),
      this.prisma.botMessage.count(),
      this.prisma.botChat.count()
    ]);

    return { totalBots, activeBots, blockedBots, totalMessages, totalChats };
  }

  async validateBotToken(token: string) {
    const tokenHash = hashBotToken(token);
    const cached = await this.rateLimit.getCachedValidatedBot(tokenHash);
    if (cached) {
      return cached;
    }

    const bot = await this.prisma.bot.findUnique({
      where: { tokenHash },
      select: {
        id: true,
        name: true,
        username: true,
        ownerId: true,
        webAppUrl: true,
        menuButton: true,
        webhookUrl: true,
        webhookSecretToken: true,
        isActive: true,
        isSystemBot: true
      }
    });

    if (!bot || !bot.isActive) {
      return null;
    }

    const payload = {
      id: bot.id,
      name: bot.name,
      username: bot.username,
      ownerId: bot.ownerId,
      webAppUrl: bot.webAppUrl,
      menuButton: bot.menuButton,
      webhookUrl: bot.webhookUrl,
      webhookSecretToken: bot.webhookSecretToken,
      isActive: bot.isActive,
      isSystemBot: bot.isSystemBot
    };
    await this.rateLimit.cacheValidatedBot(tokenHash, payload);
    return payload;
  }

  async invalidateBotCacheById(botId: string) {
    const bot = await this.prisma.bot.findUnique({ where: { id: botId }, select: { tokenHash: true } });
    if (bot) {
      await this.rateLimit.invalidateTokenCache(bot.tokenHash);
    }
  }

  private async getManagedBot(requesterId: string, botId: string, isSuperAdmin: boolean, includeCounts = false) {
    const bot = await this.prisma.bot.findUnique({
      where: { id: botId },
      include: {
        owner: { select: { id: true, displayName: true, username: true } },
        ...(includeCounts ? { _count: { select: { messages: true, chats: true } } } : {})
      }
    });
    if (!bot) {
      throw new NotFoundException('Бот не найден');
    }
    if (!isSuperAdmin && bot.ownerId !== requesterId) {
      throw new ForbiddenException('Недостаточно прав для управления этим ботом');
    }
    return bot as BotRecord;
  }

  private async assertManageAllBots(requesterId: string, isSuperAdmin: boolean) {
    if (isSuperAdmin) return;
    try {
      await this.access.assertPermission(requesterId, false, 'bots.manage.all');
    } catch {
      throw new ForbiddenException('Недостаточно прав для управления всеми ботами');
    }
  }

  private toBotResponse(bot: BotRecord) {
    return {
      id: bot.id,
      name: bot.name,
      username: bot.username,
      tokenPrefix: bot.tokenPrefix,
      ownerId: bot.ownerId,
      description: bot.description ?? undefined,
      aboutText: bot.aboutText ?? undefined,
      botPicUrl: bot.botPicUrl ?? undefined,
      menuButtonJson: bot.menuButton ? JSON.stringify(bot.menuButton) : undefined,
      webAppUrl: bot.webAppUrl ?? undefined,
      isActive: bot.isActive,
      isSystemBot: bot.isSystemBot,
      createdAt: bot.createdAt.toISOString(),
      updatedAt: bot.updatedAt.toISOString(),
      owner: bot.owner
        ? {
            id: bot.owner.id,
            displayName: bot.owner.displayName,
            username: bot.owner.username ?? undefined
          }
        : undefined,
      messageCount: bot._count?.messages ?? 0,
      chatCount: bot._count?.chats ?? 0
    };
  }

  private async findOwnedBotByUsername(ownerId: string, botUsernameRaw: string) {
    const normalized = normalizeBotUsername(botUsernameRaw.replace(/^@/, ''));
    const bot = await this.prisma.bot.findFirst({
      where: {
        ownerId,
        OR: [{ username: normalized }, { username: `${normalized.replace(/_bot$/i, '')}_bot` }]
      }
    });
    if (!bot) {
      throw new NotFoundException('Бот не найден или не принадлежит вам');
    }
    return bot;
  }

  private async publishBotProfileUpdated(ownerId: string, bot: BotRecord) {
    await this.notifications.publishRealtime(ownerId, 'bot_profile_updated', bot.name, '', {
      botId: bot.id,
      botUsername: bot.username,
      description: bot.description ?? null,
      aboutText: bot.aboutText ?? null,
      botPicUrl: bot.botPicUrl ?? null,
      menuButtonJson: bot.menuButton ? JSON.stringify(bot.menuButton) : null
    });
  }

  private async broadcastGlobalMenuButtonChange(bot: BotRecord) {
    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 resolved = resolveComposerMenuButton({
          chatOverride: chat.menuButton?.menuButton ?? null,
          globalMenuButton: bot.menuButton,
          bot
        });
        await this.notifications.publishRealtime(chat.recipientUserId, 'bot_menu_button_updated', bot.name, '', {
          botId: bot.id,
          botUsername: bot.username,
          composerWebAppUrl: resolved.composerWebAppUrl ?? null,
          composerMenuButtonJson: resolved.menuButton ? JSON.stringify(resolved.menuButton) : null,
          buttonText: resolved.buttonText ?? null
        });
      })
    );
  }

  private isUniqueViolation(error: unknown) {
    return typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'P2002';
  }
}
