import fs from "node:fs";
import path from "node:path";
import TelegramBot from "node-telegram-bot-api";
import OpenAI, { toFile } from "openai";
import { logger } from "./logger";

export type ChildBotConfig = {
  token: string;
  name: string;
  systemPrompt: string;
  username?: string;
  createdAt: string;
  ownerUserId?: number;
};

type PremiumEntry = {
  userId: number;
  expiresAt: number | null;
};

function DATA_DIR_PATH(): string {
  return path.resolve(process.cwd(), "data");
}

const PREMIUM_FILE = path.join(DATA_DIR_PATH(), "premium-users.json");

function ensureDirOnly(): void {
  const dir = DATA_DIR_PATH();
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

export function loadPremiumUsers(): PremiumEntry[] {
  ensureDirOnly();
  if (!fs.existsSync(PREMIUM_FILE)) return [];
  try {
    const raw = JSON.parse(fs.readFileSync(PREMIUM_FILE, "utf8")) as unknown;
    if (!Array.isArray(raw)) return [];
    return raw
      .map((item) => {
        if (typeof item === "number") {
          return { userId: item, expiresAt: null } as PremiumEntry;
        }
        if (
          item &&
          typeof item === "object" &&
          typeof (item as PremiumEntry).userId === "number"
        ) {
          const e = item as PremiumEntry;
          return {
            userId: e.userId,
            expiresAt: typeof e.expiresAt === "number" ? e.expiresAt : null,
          };
        }
        return null;
      })
      .filter((e): e is PremiumEntry => e !== null);
  } catch {
    return [];
  }
}

function savePremiumUsers(entries: PremiumEntry[]): void {
  ensureDirOnly();
  fs.writeFileSync(PREMIUM_FILE, JSON.stringify(entries, null, 2));
}

export function getPremiumEntry(userId: number): PremiumEntry | null {
  const entries = loadPremiumUsers();
  const e = entries.find((x) => x.userId === userId);
  if (!e) return null;
  if (e.expiresAt !== null && e.expiresAt < Date.now()) {
    savePremiumUsers(entries.filter((x) => x.userId !== userId));
    return null;
  }
  return e;
}

export function isPremiumUser(userId: number): boolean {
  return getPremiumEntry(userId) !== null;
}

export function setPremiumUser(
  userId: number,
  premium: boolean,
  durationDays?: number,
): PremiumEntry | null {
  const entries = loadPremiumUsers().filter((e) => e.userId !== userId);
  if (!premium) {
    savePremiumUsers(entries);
    return null;
  }
  const expiresAt =
    durationDays && durationDays > 0
      ? Date.now() + durationDays * 24 * 60 * 60 * 1000
      : null;
  const entry: PremiumEntry = { userId, expiresAt };
  entries.push(entry);
  savePremiumUsers(entries);
  return entry;
}

type VerifiedEntry = {
  userId: number;
  phone: string;
  verifiedAt: number;
};

const VERIFIED_FILE = path.join(DATA_DIR_PATH(), "verified-users.json");

export function loadVerifiedUsers(): VerifiedEntry[] {
  ensureDirOnly();
  if (!fs.existsSync(VERIFIED_FILE)) return [];
  try {
    return JSON.parse(fs.readFileSync(VERIFIED_FILE, "utf8")) as VerifiedEntry[];
  } catch {
    return [];
  }
}

function saveVerifiedUsers(entries: VerifiedEntry[]): void {
  ensureDirOnly();
  fs.writeFileSync(VERIFIED_FILE, JSON.stringify(entries, null, 2));
}

export function getVerifiedUser(userId: number): VerifiedEntry | null {
  return loadVerifiedUsers().find((e) => e.userId === userId) ?? null;
}

export function isVerifiedUser(userId: number): boolean {
  return getVerifiedUser(userId) !== null;
}

export function setVerifiedUser(userId: number, phone: string): VerifiedEntry {
  const entries = loadVerifiedUsers().filter((e) => e.userId !== userId);
  const entry: VerifiedEntry = {
    userId,
    phone,
    verifiedAt: Date.now(),
  };
  entries.push(entry);
  saveVerifiedUsers(entries);
  return entry;
}

export function isIranianPhone(phone: string): boolean {
  const digits = phone.replace(/\D/g, "");
  return digits.startsWith("98") && digits.length >= 11;
}

const ADMINS_FILE = path.join(DATA_DIR_PATH(), "admins.json");
const BANNED_FILE = path.join(DATA_DIR_PATH(), "banned-users.json");
const STATE_FILE = path.join(DATA_DIR_PATH(), "bot-state.json");

function loadJsonArray<T>(file: string): T[] {
  ensureDirOnly();
  if (!fs.existsSync(file)) return [];
  try {
    const data = JSON.parse(fs.readFileSync(file, "utf8"));
    return Array.isArray(data) ? (data as T[]) : [];
  } catch {
    return [];
  }
}

function saveJsonArray<T>(file: string, data: T[]): void {
  ensureDirOnly();
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

export function listAdmins(): number[] {
  return loadJsonArray<number>(ADMINS_FILE);
}

export function isAdmin(userId: number): boolean {
  return listAdmins().includes(userId);
}

export function addAdmin(userId: number): boolean {
  const admins = listAdmins();
  if (admins.includes(userId)) return false;
  admins.push(userId);
  saveJsonArray(ADMINS_FILE, admins);
  return true;
}

export function removeAdmin(userId: number): boolean {
  const admins = listAdmins();
  if (!admins.includes(userId)) return false;
  saveJsonArray(
    ADMINS_FILE,
    admins.filter((a) => a !== userId),
  );
  return true;
}

export function listBanned(): number[] {
  return loadJsonArray<number>(BANNED_FILE);
}

export function isBanned(userId: number): boolean {
  return listBanned().includes(userId);
}

export function banUser(userId: number): boolean {
  const banned = listBanned();
  if (banned.includes(userId)) return false;
  banned.push(userId);
  saveJsonArray(BANNED_FILE, banned);
  return true;
}

export function unbanUser(userId: number): boolean {
  const banned = listBanned();
  if (!banned.includes(userId)) return false;
  saveJsonArray(
    BANNED_FILE,
    banned.filter((b) => b !== userId),
  );
  return true;
}

const LOG_CHANNEL_FILE = path.join(DATA_DIR_PATH(), "log-channel.json");

export function getLogChannelId(): number | null {
  ensureDirOnly();
  if (!fs.existsSync(LOG_CHANNEL_FILE)) return null;
  try {
    const data = JSON.parse(fs.readFileSync(LOG_CHANNEL_FILE, "utf8"));
    return typeof data?.chatId === "number" ? data.chatId : null;
  } catch {
    return null;
  }
}

export function setLogChannelId(chatId: number | null): void {
  ensureDirOnly();
  fs.writeFileSync(LOG_CHANNEL_FILE, JSON.stringify({ chatId }, null, 2));
}

type BotState = { active: boolean };

export function loadBotState(): BotState {
  ensureDirOnly();
  if (!fs.existsSync(STATE_FILE)) return { active: true };
  try {
    const data = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
    if (data && typeof data.active === "boolean") return data;
    return { active: true };
  } catch {
    return { active: true };
  }
}

export function isBotActive(): boolean {
  return loadBotState().active;
}

export function setBotActive(active: boolean): void {
  ensureDirOnly();
  fs.writeFileSync(STATE_FILE, JSON.stringify({ active }, null, 2));
}

export function countBotsForUser(userId: number): number {
  return loadConfigs().filter((c) => c.ownerUserId === userId).length;
}

export function listBotsForUser(userId: number): ChildBotConfig[] {
  return loadConfigs().filter((c) => c.ownerUserId === userId);
}

export function findBotByNameOrUsername(query: string): ChildBotConfig | null {
  const q = query.trim().replace(/^@/, "").toLowerCase();
  if (!q) return null;
  return (
    loadConfigs().find(
      (c) =>
        c.username?.toLowerCase() === q ||
        c.name.toLowerCase() === q,
    ) ?? null
  );
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ChatMessage = any;

const DATA_DIR = path.resolve(process.cwd(), "data");
const DATA_FILE = path.join(DATA_DIR, "child-bots.json");
const CHILD_STATE_DIR = path.join(DATA_DIR, "child-states");
const CHILD_USERS_DIR = path.join(DATA_DIR, "child-users");
const MAX_HISTORY = 60;

function safeFsName(token: string): string {
  return token.replace(/[^a-zA-Z0-9]/g, "_");
}

function loadChildStates(token: string): Record<number, Record<string, string>> {
  ensureDir();
  if (!fs.existsSync(CHILD_STATE_DIR)) fs.mkdirSync(CHILD_STATE_DIR, { recursive: true });
  const file = path.join(CHILD_STATE_DIR, `${safeFsName(token)}.json`);
  if (!fs.existsSync(file)) return {};
  try {
    return JSON.parse(fs.readFileSync(file, "utf8"));
  } catch {
    return {};
  }
}

function saveChildStates(
  token: string,
  states: Record<number, Record<string, string>>,
): void {
  if (!fs.existsSync(CHILD_STATE_DIR)) fs.mkdirSync(CHILD_STATE_DIR, { recursive: true });
  const file = path.join(CHILD_STATE_DIR, `${safeFsName(token)}.json`);
  try {
    fs.writeFileSync(file, JSON.stringify(states, null, 2));
  } catch (err) {
    logger.warn({ err }, "Failed to save child states");
  }
}

function loadChildUsers(token: string): number[] {
  if (!fs.existsSync(CHILD_USERS_DIR)) fs.mkdirSync(CHILD_USERS_DIR, { recursive: true });
  const file = path.join(CHILD_USERS_DIR, `${safeFsName(token)}.json`);
  if (!fs.existsSync(file)) return [];
  try {
    const data = JSON.parse(fs.readFileSync(file, "utf8"));
    return Array.isArray(data) ? data : [];
  } catch {
    return [];
  }
}

function saveChildUsers(token: string, users: number[]): void {
  if (!fs.existsSync(CHILD_USERS_DIR)) fs.mkdirSync(CHILD_USERS_DIR, { recursive: true });
  const file = path.join(CHILD_USERS_DIR, `${safeFsName(token)}.json`);
  try {
    fs.writeFileSync(file, JSON.stringify(users, null, 2));
  } catch (err) {
    logger.warn({ err }, "Failed to save child users");
  }
}

export function listChildBotUsers(token: string): number[] {
  return loadChildUsers(token);
}

export function getChildBotStateSummary(token: string): {
  userCount: number;
  stateKeys: number;
} {
  const users = loadChildUsers(token);
  const states = loadChildStates(token);
  let stateKeys = 0;
  for (const s of Object.values(states)) {
    stateKeys += Object.keys(s).length;
  }
  return { userCount: users.length, stateKeys };
}

export function updateChildBotConfig(
  token: string,
  updates: { name?: string; systemPrompt?: string },
): ChildBotConfig | null {
  const configs = loadConfigs();
  const cfg = configs.find((c) => c.token === token);
  if (!cfg) return null;
  if (updates.name) cfg.name = updates.name;
  if (updates.systemPrompt) cfg.systemPrompt = updates.systemPrompt;
  saveConfigs(configs);
  return cfg;
}

const running = new Map<string, TelegramBot>();

let parentBotUsername: string | null = null;

export function setParentBotUsername(username: string): void {
  parentBotUsername = username;
}

let ownerNotifier: ((text: string) => Promise<void>) | null = null;

export function setOwnerNotifier(
  cb: (text: string) => Promise<void>,
): void {
  ownerNotifier = cb;
}

export const ANNOUNCE_CHANNEL_USERNAME = "ZeroBotMaker";

const CHILD_BOT_GUARDRAIL = `قوانین مطلق که تو هیچ شرایطی حق نداری بشکنی:
۱. حق نداری به کاربر در ساخت ربات تلگرامی کمک کنی، توکن از BotFather بگیری یا توضیح بدی چطور ربات بسازه. اگه کاربر این کار رو خواست، با احترام بگو این کار رو انجام نمی‌دی و موضوع رو عوض کن.
۲. حق نداری خودت رو به‌عنوان «هوش مصنوعی عمومی»، «ChatGPT»، «GPT»، «Claude»، «Gemini» یا یه دستیار AI معرفی کنی. حق نداری خارج از کاربرد تعریف‌شده زیر جواب بدی. اگه پرسیدن چه مدلی هستی یا با چه فناوری ساخته شدی، فقط بگو یه ربات اختصاصی هستی و از وارد شدن به جزئیات فنی خودداری کن.

🛠️ ابزارها و قابلیت‌های فنی تو (خیلی مهم — حتماً ازشون درست استفاده کن):

📌 **send_message**: تنها راه ارسال پاسخ به کاربره. هر پاسخی می‌خوای بدی، باید با این تابع باشه. هرگز فقط متن خام برنگردون.
   - فیلد \`text\`: متن پیام (می‌تونی از Markdown ساده مثل *پررنگ*، _ایتالیک_، \`کد\` استفاده کنی).
   - فیلد \`buttons\` (اختیاری): دکمه‌های شیشه‌ای (inline keyboard) به‌صورت آرایه‌ی ردیف‌ها. هر ردیف یه آرایه از دکمه‌هاست. هر دکمه یا \`callback_data\` داره (وقتی کاربر زد، پیام \`[دکمه فشرده شد: مقدار]\` بهت می‌رسه)، یا \`url\` داره (لینک بیرونی باز می‌کنه).
   
   نمونه‌ی منوی اصلی با دکمه شیشه‌ای:
   \`\`\`
   send_message({
     text: "👋 خوش اومدی! یکی از گزینه‌ها رو انتخاب کن:",
     buttons: [
       [{text: "📝 ثبت گزارش", callback_data: "new_report"}, {text: "📊 گزارش‌های من", callback_data: "my_reports"}],
       [{text: "❓ راهنما", callback_data: "help"}, {text: "📞 تماس با ما", url: "https://t.me/admin"}]
     ]
   })
   \`\`\`

📌 **send_message با کیبورد پایین (reply_keyboard / دکمه کیبوردی)**: 
   - تفاوت با شیشه‌ای: دکمه‌های شیشه‌ای زیر یه پیام خاص ظاهر می‌شن، ولی دکمه‌های کیبوردی پایین صفحه‌ی کاربر همیشه می‌مونن. وقتی کاربر دکمه کیبوردی بزنه، **متن دکمه** به‌عنوان پیام عادی برمی‌گرده (نه callback_data).
   - مثال:
   \`\`\`
   send_message({
     text: "از منوی پایین انتخاب کن:",
     reply_keyboard: [["🏠 خانه", "📊 آمار"], ["⚙️ تنظیمات"]]
   })
   \`\`\`
   - برای حذف کیبورد: \`send_message({text: "...", remove_keyboard: true})\`.

📌 **edit_message**: متن یا دکمه‌های پیام قبلی ربات رو **در جا** ویرایش می‌کنه (به‌جای فرستادن پیام جدید). برای منوهای پویا و رفت‌وبرگشت بین صفحه‌ها فوق‌العاده‌ست.
   - بدون message_id، آخرین پیام ویرایش می‌شه.
   - مثال (تب‌های منو):
   \`\`\`
   edit_message({text: "📊 صفحه آمار:", buttons: [[{text:"🔙 برگشت", callback_data:"home"}]]})
   \`\`\`

📌 **delete_message**: یه پیام رو حذف می‌کنه. \`delete_message({message_id: 123})\`.

📌 **set_state / list_state / clear_state**: حافظه‌ی پایدار هر کاربر (روی دیسک هم ذخیره می‌شه و بعد ری‌استارت باقی می‌مونه).
   - \`set_state({key: "step", value: "q1"})\` — تنظیم.
   - \`list_state()\` — همه‌ی کلید/مقدارها رو می‌گیری (برای نمایش لیست‌هایی مثل «گزارش‌های من»).
   - \`clear_state()\` — همه چیز پاک.
   - برای پاک کردن یه کلید خاص: \`set_state({key: "...", value: ""})\`.
   - **کلید مرکب برای لیست‌ها**: مثلاً \`report_001\`, \`report_002\` تا بعداً با list_state بخونی و نمایش بدی.

📌 **generate_image**: عکس بساز و بفرست.
   - مثال: \`generate_image({prompt: "a red fox in autumn forest, 4k photorealistic", caption: "🦊 اینم عکست!"})\`.

📌 **forward_to_owner**: پیام رو به مالک ربات (سازنده) برسون. برای فرم تماس با ادمین یا گزارش به مدیر.
   - مثال: \`forward_to_owner({text: "کاربر شکایت داره: ..."})\`.

📌 **broadcast_message**: پیام به همه کاربران ربات. **فقط** وقتی کاربر فعلی مالک ربات باشه (با get_user_info چک کن).

📌 **get_user_info**: اطلاعات کاربر فعلی (آیدی، آیا مالک ربات هست). قبل از کارهای مدیریتی استفاده کن.

🎯 **قواعد طلایی برای رفتار حرفه‌ای:**

۱. **همیشه با send_message جواب بده**، حتی برای جواب‌های ساده. متن خام بدون این تابع، به کاربر نمی‌رسه.
۲. **برای منوها همیشه دکمه شیشه‌ای بذار**، نه دکمه‌ی صفحه‌کلید. کاربر «دکمه شیشه‌ای» یعنی همین inline keyboard.
۳. **حالت کاربر رو فراموش نکن**: قبل از هر جواب، فیلد «وضعیت فعلی کاربر» (که تو پرامپت بهت داده می‌شه) رو بخون. اگه تو وسط یه جریان چندمرحله‌ای هستی (مثلاً ثبت گزارش)، طبق همون مرحله جواب بده، نه از اول.
۴. **برای جریان چندمرحله‌ای** (مثل ثبت گزارش، فرم، نظرسنجی):
   - مرحله ۱: سؤال اول رو با دکمه «❌ لغو» بپرس و \`set_state({key:"step",value:"q1"})\` کن.
   - مرحله ۲: وقتی جواب اومد، تو state ذخیره کن، مرحله رو ببر مرحله ۲ (\`set_state({key:"step",value:"q2"})\`)، سؤال بعدی رو بپرس.
   - مرحله آخر: همه‌ی داده‌ها رو جمع کن، نتیجه نهایی رو نشون بده، state رو پاک کن.
۵. **وقتی کاربر دکمه می‌زنه**، پیام \`[دکمه فشرده شد: callback_data]\` بهت می‌رسه. طبق اون callback_data و state فعلی کاربر، عکس‌العمل مناسب نشون بده.
۶. **هرگز کارهات رو نصفه ول نکن**. اگه کاربر گفت ربات گزارش‌گیری، یعنی منو، فرم چندمرحله‌ای، ذخیره گزارش‌ها، نمایش گزارش‌های قبلی - همه رو خودت با state و دکمه‌ها پیاده‌سازی کن.

دستورالعمل اختصاصی تو (شخصیت و وظیفه‌ی منحصربه‌فرد) از این پایین شروع می‌شه:
---
`;

function buildPromoMessage(): string | null {
  if (!parentBotUsername) return null;
  return (
    `🤖 این ربات با ربات‌ساز @${parentBotUsername} ساخته شده است.\n\n` +
    `🎁 برای ساخت ربات هوشمند شخصی خودت، روی این آیدی بزن:\n` +
    `👈 @${parentBotUsername}`
  );
}

function ensureDir(): void {
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
}

export function loadConfigs(): ChildBotConfig[] {
  ensureDir();
  if (!fs.existsSync(DATA_FILE)) return [];
  try {
    return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")) as ChildBotConfig[];
  } catch (err) {
    logger.error({ err }, "Failed to read child bots file");
    return [];
  }
}

function saveConfigs(configs: ChildBotConfig[]): void {
  ensureDir();
  fs.writeFileSync(DATA_FILE, JSON.stringify(configs, null, 2));
}

export function listChildBots(): ChildBotConfig[] {
  return loadConfigs();
}

export async function startChildBot(
  config: ChildBotConfig,
  openai: OpenAI,
): Promise<{ username: string }> {
  if (running.has(config.token)) {
    const existing = running.get(config.token)!;
    const me = await existing.getMe();
    return { username: me.username ?? config.name };
  }

  const bot = new TelegramBot(config.token, { polling: true });
  const histories = new Map<number, ChatMessage[]>();

  // Per-chat state — persisted to disk so multi-step flows survive restart.
  const chatStatesObj: Record<number, Record<string, string>> = loadChildStates(
    config.token,
  );
  const knownUsers = new Set<number>(loadChildUsers(config.token));
  let stateSaveTimer: NodeJS.Timeout | null = null;
  let usersSaveTimer: NodeJS.Timeout | null = null;

  function scheduleStateSave(): void {
    if (stateSaveTimer) return;
    stateSaveTimer = setTimeout(() => {
      stateSaveTimer = null;
      saveChildStates(config.token, chatStatesObj);
    }, 500);
  }

  function scheduleUsersSave(): void {
    if (usersSaveTimer) return;
    usersSaveTimer = setTimeout(() => {
      usersSaveTimer = null;
      saveChildUsers(config.token, [...knownUsers]);
    }, 500);
  }

  function trackUser(userId: number): void {
    if (!knownUsers.has(userId)) {
      knownUsers.add(userId);
      scheduleUsersSave();
    }
  }

  function getChatState(chatId: number): Record<string, string> {
    let s = chatStatesObj[chatId];
    if (!s) {
      s = {};
      chatStatesObj[chatId] = s;
    }
    return s;
  }

  function formatStateForPrompt(chatId: number): string {
    const s = chatStatesObj[chatId];
    if (!s || Object.keys(s).length === 0) {
      return "📋 وضعیت فعلی کاربر: (خالی — جریان جدیدی شروع نشده)";
    }
    const lines = Object.entries(s)
      .filter(([, v]) => v !== "")
      .map(([k, v]) => `  - ${k}: ${v.slice(0, 200)}`);
    if (lines.length === 0) {
      return "📋 وضعیت فعلی کاربر: (خالی)";
    }
    return `📋 وضعیت فعلی کاربر (مهم — ادامه‌ی جریان رو از اینجا بگیر):\n${lines.join("\n")}`;
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const childTools: any[] = [
    {
      type: "function",
      function: {
        name: "send_message",
        description:
          "تنها راه ارسال پیام به کاربر. می‌تونه دکمه‌های شیشه‌ای (inline) یا دکمه‌های کیبوردی (reply) همراه داشته باشه.",
        parameters: {
          type: "object",
          properties: {
            text: { type: "string", description: "متن پیام" },
            parse_mode: {
              type: "string",
              enum: ["Markdown", "HTML", "MarkdownV2"],
              description: "حالت قالب‌بندی متن (اختیاری)",
            },
            buttons: {
              type: "array",
              description:
                "دکمه‌های شیشه‌ای (inline keyboard). آرایه‌ی ردیف‌ها، هر ردیف آرایه از دکمه‌ها.",
              items: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    text: { type: "string" },
                    callback_data: {
                      type: "string",
                      description: "حداکثر 64 کاراکتر",
                    },
                    url: {
                      type: "string",
                      description: "لینک خارجی به جای callback_data",
                    },
                  },
                  required: ["text"],
                },
              },
            },
            reply_keyboard: {
              type: "array",
              description:
                "دکمه‌های کیبوردی پایین صفحه (همیشه می‌مونن). آرایه‌ی ردیف‌ها از متن دکمه‌ها. وقتی کاربر بزنه، متنش به‌عنوان پیام عادی برمی‌گرده.",
              items: { type: "array", items: { type: "string" } },
            },
            remove_keyboard: {
              type: "boolean",
              description: "اگه true باشه، دکمه‌های کیبوردی پایین رو حذف می‌کنه.",
            },
          },
          required: ["text"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "edit_message",
        description:
          "متن یا دکمه‌های یه پیام قبلی ربات رو ویرایش می‌کنه (به‌جای فرستادن پیام جدید). برای به‌روزکردن منوها در جا عالیه.",
        parameters: {
          type: "object",
          properties: {
            message_id: {
              type: "number",
              description:
                "شناسه پیام برای ویرایش. اگه ندی، آخرین پیام ربات ویرایش می‌شه.",
            },
            text: { type: "string" },
            buttons: {
              type: "array",
              items: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    text: { type: "string" },
                    callback_data: { type: "string" },
                    url: { type: "string" },
                  },
                  required: ["text"],
                },
              },
            },
          },
          required: ["text"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "delete_message",
        description: "یه پیام رو حذف می‌کنه (با شناسه message_id).",
        parameters: {
          type: "object",
          properties: { message_id: { type: "number" } },
          required: ["message_id"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "set_state",
        description:
          "ذخیره‌ی حالت برای جریان‌های چندمرحله‌ای. مقدار خالی یعنی پاک کردن کلید. روی دیسک ذخیره می‌شه و بعد ری‌استارت باقی می‌مونه.",
        parameters: {
          type: "object",
          properties: {
            key: { type: "string" },
            value: { type: "string" },
          },
          required: ["key", "value"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "list_state",
        description:
          "همه‌ی کلید/مقدارهای ذخیره‌شده برای کاربر فعلی رو برگردون. برای نمایش لیست‌هایی مثل «گزارش‌های من» مفیده.",
        parameters: { type: "object", properties: {} },
      },
    },
    {
      type: "function",
      function: {
        name: "clear_state",
        description: "همه‌ی state کاربر فعلی رو پاک می‌کنه.",
        parameters: { type: "object", properties: {} },
      },
    },
    {
      type: "function",
      function: {
        name: "generate_image",
        description: "ساخت عکس با هوش مصنوعی و ارسال به کاربر.",
        parameters: {
          type: "object",
          properties: {
            prompt: {
              type: "string",
              description: "توصیف کامل به انگلیسی برای کیفیت بهتر.",
            },
            caption: { type: "string" },
            size: {
              type: "string",
              enum: ["1024x1024", "1536x1024", "1024x1536"],
            },
          },
          required: ["prompt"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "forward_to_owner",
        description:
          "پیام داده‌شده رو برای مالک ربات (که این ربات رو ساخته) فوروارد می‌کنه. برای فرم تماس با ادمین یا گزارش به مدیر مفیده.",
        parameters: {
          type: "object",
          properties: {
            text: {
              type: "string",
              description: "متن پیامی که می‌خوای به مالک برسه.",
            },
          },
          required: ["text"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "broadcast_message",
        description:
          "پیام به همه‌ی کاربران ربات (به جز خود مالک) ارسال می‌کنه. فقط زمانی صدا بزن که کاربر فعلی، مالک ربات باشه.",
        parameters: {
          type: "object",
          properties: { text: { type: "string" } },
          required: ["text"],
        },
      },
    },
    {
      type: "function",
      function: {
        name: "get_user_info",
        description: "اطلاعات کاربر فعلی (آیدی، نقش، آیا مالکه) رو برمی‌گردونه.",
        parameters: { type: "object", properties: {} },
      },
    },
    {
      type: "function",
      function: {
        name: "set_bot_commands",
        description:
          "لیست دستورات (commands) ربات رو ست می‌کنه — همون چیزی که توی منوی آبی تلگرام کنار فیلد تایپ می‌بینی. هر بار که صدا زده شه، لیست قبلی کامل جایگزین می‌شه. برای پاک کردن کامل، یه آرایه‌ی خالی بفرست. این تابع رو وقتی صدا بزن که مالک گفت دستوری مثل /menu یا /start اضافه شه. بعد از این، هندلینگ خود دستور با set_state یا منطق گفتگو انجام می‌شه — این تابع فقط لیست منو رو ست می‌کنه.",
        parameters: {
          type: "object",
          properties: {
            commands: {
              type: "array",
              description:
                "لیست دستورات. هر کدوم باید یه command بدون / و یه description کوتاه داشته باشه.",
              items: {
                type: "object",
                properties: {
                  command: {
                    type: "string",
                    description: "اسم دستور بدون اسلش، مثلاً «menu» یا «help»",
                  },
                  description: {
                    type: "string",
                    description: "توضیح کوتاه فارسی، حداکثر ۲۵۶ کاراکتر",
                  },
                },
                required: ["command", "description"],
              },
            },
          },
          required: ["commands"],
        },
      },
    },
  ];

  // Track last message id sent by bot per chat (for edit_message default).
  const lastBotMessage = new Map<number, number>();

  // Track which user is interacting per chat (for owner checks, forwards).
  const lastUserIdInChat = new Map<number, number>();
  function userIdFromChat(chatId: number): number | undefined {
    return lastUserIdInChat.get(chatId);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function buildReplyKeyboard(rk: any): any {
    if (!Array.isArray(rk) || rk.length === 0) return undefined;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const rows = rk
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .map((row: any) => {
        if (!Array.isArray(row)) return null;
        return row
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          .map((b: any) => (typeof b === "string" ? { text: b } : null))
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          .filter((b: any) => b !== null);
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .filter((row: any) => Array.isArray(row) && row.length > 0);
    if (rows.length === 0) return undefined;
    return { keyboard: rows, resize_keyboard: true };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function buildInlineKeyboard(buttons: any): any {
    if (!Array.isArray(buttons)) return undefined;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const rows = buttons
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .map((row: any) => {
        if (!Array.isArray(row)) return null;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return row
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          .map((b: any) => {
            if (!b || typeof b.text !== "string") return null;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const out: any = { text: b.text };
            if (typeof b.url === "string" && b.url) out.url = b.url;
            else if (typeof b.callback_data === "string")
              out.callback_data = b.callback_data.slice(0, 64);
            else out.callback_data = b.text.slice(0, 64);
            return out;
          })
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          .filter((b: any) => b !== null);
      })
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      .filter((row: any) => Array.isArray(row) && row.length > 0);
    if (rows.length === 0) return undefined;
    return { inline_keyboard: rows };
  }

  async function generateImageBuffer(
    prompt: string,
    size: "1024x1024" | "1536x1024" | "1024x1536" = "1024x1024",
  ): Promise<Buffer> {
    const result = await openai.images.generate({
      model: "gpt-image-1",
      prompt,
      size,
    });
    const item = result.data?.[0];
    const b64 = item?.b64_json;
    if (b64) return Buffer.from(b64, "base64");
    const url = item?.url;
    if (url) {
      const r = await fetch(url);
      return Buffer.from(await r.arrayBuffer());
    }
    throw new Error("no image returned");
  }

  let unauthorizedRemoved = false;
  bot.on("polling_error", (err) => {
    const msg = err.message ?? "";
    if (msg.includes("401") || msg.includes("Unauthorized")) {
      if (unauthorizedRemoved) return;
      unauthorizedRemoved = true;
      logger.warn(
        { bot: config.name },
        "Child bot token invalid (401), removing",
      );
      void stopChildBot(config.token).catch(() => {});
      return;
    }
    logger.error({ err: msg, bot: config.name }, "Child bot polling error");
  });

  const promotedChats = new Set<number>();

  async function sendAndPinPromo(chatId: number): Promise<void> {
    if (promotedChats.has(chatId)) return;
    promotedChats.add(chatId);
    const promo = buildPromoMessage();
    if (!promo) return;
    try {
      const sent = await bot.sendMessage(chatId, promo, {
        disable_web_page_preview: true,
      });
      try {
        await bot.pinChatMessage(chatId, sent.message_id, {
          disable_notification: true,
        });
      } catch {
        /* pin may fail in private chats with restrictions or groups w/o admin */
      }
    } catch (err) {
      logger.warn({ err, bot: config.name, chatId }, "Promo send failed");
    }
  }

  const phoneRequestKeyboard = {
    reply_markup: {
      keyboard: [
        [{ text: "📱 ارسال شماره تلفن", request_contact: true }],
      ],
      resize_keyboard: true,
      one_time_keyboard: true,
    },
  };

  function markActive(userId: number, chatId: number): void {
    lastUserIdInChat.set(chatId, userId);
    trackUser(userId);
  }

  async function ensureChildAccess(
    userId: number,
    chatId: number,
  ): Promise<boolean> {
    markActive(userId, chatId);
    if (isVerifiedUser(userId)) return true;
    try {
      await bot.sendMessage(
        chatId,
        "🔐 برای استفاده از این ربات اول باید شماره تلفنت رو ارسال کنی.\n\nدکمه پایین رو بزن.",
        phoneRequestKeyboard,
      );
    } catch {
      /* ignore */
    }
    return false;
  }

  bot.onText(/^\/start$/, async (msg) => {
    histories.delete(msg.chat.id);
    const userId = msg.from?.id ?? msg.chat.id;
    markActive(userId, msg.chat.id);
    if (isBanned(userId)) {
      try {
        await bot.sendMessage(msg.chat.id, "⛔ تو از این ربات مسدود شدی.");
      } catch {
        /* ignore */
      }
      return;
    }
    if (!isVerifiedUser(userId)) {
      try {
        await bot.sendMessage(
          msg.chat.id,
          `سلام! 👋 به «${config.name}» خوش اومدی.\n\n🔐 برای ادامه، لطفاً شماره تلفن ایرانی خودت رو از طریق دکمه پایین ارسال کن.`,
          phoneRequestKeyboard,
        );
      } catch {
        /* ignore */
      }
      return;
    }
    try {
      await bot.sendMessage(msg.chat.id, `سلام! من «${config.name}» هستم.`);
    } catch {
      /* ignore */
    }
    void sendAndPinPromo(msg.chat.id);
  });

  bot.onText(/^\/reset$/, (msg) => {
    histories.delete(msg.chat.id);
    void bot.sendMessage(msg.chat.id, "حافظه پاک شد. ✨");
  });

  bot.on("contact", async (msg) => {
    const userId = msg.from?.id;
    const contact = msg.contact;
    if (!userId || !contact) return;
    markActive(userId, msg.chat.id);

    if (contact.user_id && contact.user_id !== userId) {
      try {
        await bot.sendMessage(
          msg.chat.id,
          "❌ لطفاً شماره تلفن خودت رو ارسال کن، نه شماره شخص دیگه.",
          phoneRequestKeyboard,
        );
      } catch {
        /* ignore */
      }
      return;
    }

    if (!isIranianPhone(contact.phone_number)) {
      try {
        await bot.sendMessage(
          msg.chat.id,
          "❌ فقط شماره‌های ایرانی (با کد +۹۸) پذیرفته می‌شن. لطفاً شماره ایرانی خودت رو ارسال کن.",
          phoneRequestKeyboard,
        );
      } catch {
        /* ignore */
      }
      return;
    }

    const wasNew = !isVerifiedUser(userId);
    setVerifiedUser(userId, contact.phone_number);
    try {
      await bot.sendMessage(
        msg.chat.id,
        `✅ شماره ${contact.phone_number} ثبت شد. حالا می‌تونی از ربات استفاده کنی.`,
        { reply_markup: { remove_keyboard: true } },
      );
    } catch {
      /* ignore */
    }
    void sendAndPinPromo(msg.chat.id);

    if (wasNew && ownerNotifier) {
      const from = msg.from;
      const fullName =
        [from?.first_name, from?.last_name].filter(Boolean).join(" ") || "—";
      const usernamePart = from?.username ? `@${from.username}` : "—";
      void ownerNotifier(
        `🆕 احراز هویت جدید در ربات «${config.name}»${
          config.username ? ` (@${config.username})` : ""
        }:\n\n` +
          `👤 نام: ${fullName}\n` +
          `🔗 یوزرنیم: ${usernamePart}\n` +
          `🆔 آیدی عددی: ${userId}\n` +
          `📱 شماره: ${contact.phone_number}`,
      ).catch(() => {});
    }
  });

  async function handleChildPhotoLike(
    fileId: string,
    mime: string,
    caption: string | undefined,
    chatId: number,
  ): Promise<void> {
    const question =
      caption?.trim() || "این عکس چیه؟ برام به فارسی توضیح بده.";
    try {
      await bot.sendChatAction(chatId, "typing");
      const fileLink = await bot.getFileLink(fileId);
      const res = await fetch(fileLink);
      const buf = Buffer.from(await res.arrayBuffer());
      const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
      await runChildChatTurn(question, chatId, dataUrl);
    } catch (err) {
      logger.error(
        { err, chatId, bot: config.name },
        "Child bot photo analysis failed",
      );
      try {
        await bot.sendMessage(
          chatId,
          "❌ نتونستم عکس رو تحلیل کنم. دوباره امتحان کن.",
        );
      } catch {
        /* ignore */
      }
    }
  }

  bot.on("photo", async (msg) => {
    const chatId = msg.chat.id;
    const userId = msg.from?.id ?? chatId;
    if (isBanned(userId)) {
      try {
        await bot.sendMessage(chatId, "⛔ تو از این ربات مسدود شدی.");
      } catch {
        /* ignore */
      }
      return;
    }
    if (!(await ensureChildAccess(userId, chatId))) return;

    const photos = msg.photo;
    if (!photos || photos.length === 0) return;
    const largest = photos[photos.length - 1];
    if (!largest) return;

    await handleChildPhotoLike(largest.file_id, "image/jpeg", msg.caption, chatId);
  });

  bot.on("document", async (msg) => {
    const chatId = msg.chat.id;
    const userId = msg.from?.id ?? chatId;
    if (isBanned(userId)) return;
    if (!(await ensureChildAccess(userId, chatId))) return;

    const doc = msg.document;
    if (!doc) return;

    const mime = doc.mime_type ?? "";
    const fileName = doc.file_name ?? "file";
    const lowerName = fileName.toLowerCase();

    try {
      if (mime.startsWith("image/")) {
        await handleChildPhotoLike(doc.file_id, mime, msg.caption, chatId);
        return;
      }

      if (mime.startsWith("audio/")) {
        await bot.sendChatAction(chatId, "typing");
        const ext = lowerName.split(".").pop() || "mp3";
        const text = await transcribeChildAudio(doc.file_id, mime, ext);
        if (!text) {
          await bot.sendMessage(chatId, "❌ نتونستم محتوای صدا رو متوجه بشم.");
          return;
        }
        await bot.sendMessage(chatId, `🎵 متن فایل صدا: «${text}»`);
        await runChildChatTurn(text, chatId);
        return;
      }

      const isText =
        mime.startsWith("text/") ||
        mime === "application/json" ||
        mime === "application/xml" ||
        mime === "" ||
        /\.(txt|md|json|xml|csv|log|js|ts|py|html|css)$/.test(lowerName);

      if (isText) {
        await bot.sendChatAction(chatId, "typing");
        const fileLink = await bot.getFileLink(doc.file_id);
        const res = await fetch(fileLink);
        const buf = Buffer.from(await res.arrayBuffer());
        const content = buf.toString("utf8").slice(0, 50000);
        const caption = msg.caption?.trim();
        const composed = caption
          ? `${caption}\n\n[محتوای فایل «${fileName}»]:\n${content}`
          : `[محتوای فایل «${fileName}»]:\n${content}`;
        await runChildChatTurn(composed, chatId);
        return;
      }

      await bot.sendMessage(
        chatId,
        `❌ نوع فایل «${mime || "نامشخص"}» پشتیبانی نمی‌شه. فعلاً عکس، فایل صدا و فایل‌های متنی پشتیبانی می‌شن.`,
      );
    } catch (err) {
      logger.error(
        { err, chatId, mime, bot: config.name },
        "Child bot document failed",
      );
      try {
        await bot.sendMessage(
          chatId,
          "❌ نتونستم فایل رو پردازش کنم. دوباره امتحان کن.",
        );
      } catch {
        /* ignore */
      }
    }
  });

  async function runChildChatTurn(
    text: string,
    chatId: number,
    imageDataUrl?: string,
  ): Promise<void> {
    const history = histories.get(chatId) ?? [];
    const storedText = imageDataUrl ? `[تصویر ارسال شد] ${text}` : text;

    if (imageDataUrl) {
      history.push({
        role: "user",
        content: [
          { type: "text", text },
          { type: "image_url", image_url: { url: imageDataUrl } },
        ],
      });
    } else {
      history.push({ role: "user", content: storedText });
    }

    try {
      await bot.sendChatAction(chatId, "typing");

      const systemContent =
        CHILD_BOT_GUARDRAIL +
        config.systemPrompt +
        "\n\n---\n" +
        formatStateForPrompt(chatId);

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const messages: any[] = [
        { role: "system", content: systemContent },
        ...history.slice(-MAX_HISTORY),
      ];

      let sentSomething = false;
      const MAX_TURNS = 8;

      for (let turn = 0; turn < MAX_TURNS; turn++) {
        const completion = await openai.chat.completions.create({
          model: "gpt-5.4",
          max_completion_tokens: 8192,
          messages,
          tools: childTools,
        });

        const choice = completion.choices[0];
        const msg = choice?.message;
        if (!msg) break;

        // Persist assistant turn (with any tool_calls) into both history and messages.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const assistantMsg: any = {
          role: "assistant",
          content: msg.content ?? "",
        };
        if (msg.tool_calls && msg.tool_calls.length > 0) {
          assistantMsg.tool_calls = msg.tool_calls
            .filter((tc) => tc.type === "function")
            .map((tc) => ({
              id: tc.id,
              type: "function",
              function: {
                name: tc.function.name,
                arguments: tc.function.arguments,
              },
            }));
        }
        messages.push(assistantMsg);
        history.push(assistantMsg);

        if (!msg.tool_calls || msg.tool_calls.length === 0) {
          // No tool call. If model produced text, send as fallback.
          const fallback = (msg.content ?? "").trim();
          if (fallback && !sentSomething) {
            try {
              await bot.sendMessage(chatId, fallback);
              sentSomething = true;
            } catch {
              /* ignore */
            }
          }
          break;
        }

        for (const tc of msg.tool_calls) {
          if (tc.type !== "function") continue;
          const name = tc.function.name;
          let args: Record<string, unknown> = {};
          try {
            args = JSON.parse(tc.function.arguments || "{}");
          } catch {
            args = {};
          }

          let toolResult = "ok";

          try {
            if (name === "send_message") {
              const t = String(args.text ?? "").trim();
              const inlineKb = buildInlineKeyboard(args.buttons);
              const replyKb = buildReplyKeyboard(args.reply_keyboard);
              const removeKb = args.remove_keyboard === true;
              const parseMode =
                typeof args.parse_mode === "string" &&
                ["Markdown", "HTML", "MarkdownV2"].includes(args.parse_mode)
                  ? (args.parse_mode as "Markdown" | "HTML" | "MarkdownV2")
                  : undefined;

              if (t) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const opts: any = {};
                if (inlineKb) opts.reply_markup = inlineKb;
                else if (replyKb) opts.reply_markup = replyKb;
                else if (removeKb) opts.reply_markup = { remove_keyboard: true };
                if (parseMode) opts.parse_mode = parseMode;

                try {
                  const sent = await bot.sendMessage(chatId, t, opts);
                  if (sent.message_id) lastBotMessage.set(chatId, sent.message_id);
                  sentSomething = true;
                  toolResult = `delivered (msg_id=${sent.message_id})`;
                } catch (sendErr) {
                  // Retry without parse_mode if formatting failed.
                  if (parseMode) {
                    delete opts.parse_mode;
                    const sent = await bot.sendMessage(chatId, t, opts);
                    if (sent.message_id)
                      lastBotMessage.set(chatId, sent.message_id);
                    sentSomething = true;
                    toolResult = `delivered without parse_mode (parse error: ${(sendErr as Error).message})`;
                  } else {
                    throw sendErr;
                  }
                }
              } else {
                toolResult = "empty text, nothing sent";
              }
            } else if (name === "edit_message") {
              const t = String(args.text ?? "").trim();
              const kb = buildInlineKeyboard(args.buttons);
              const msgId =
                typeof args.message_id === "number"
                  ? args.message_id
                  : lastBotMessage.get(chatId);
              if (!msgId) {
                toolResult = "no message to edit";
              } else if (!t) {
                toolResult = "empty text";
              } else {
                try {
                  await bot.editMessageText(t, {
                    chat_id: chatId,
                    message_id: msgId,
                    reply_markup: kb,
                  });
                  toolResult = `edited msg ${msgId}`;
                  sentSomething = true;
                } catch (editErr) {
                  toolResult = `edit failed: ${(editErr as Error).message}`;
                }
              }
            } else if (name === "delete_message") {
              const msgId = Number(args.message_id);
              if (!msgId) {
                toolResult = "missing message_id";
              } else {
                try {
                  await bot.deleteMessage(chatId, msgId);
                  toolResult = "deleted";
                } catch (delErr) {
                  toolResult = `delete failed: ${(delErr as Error).message}`;
                }
              }
            } else if (name === "set_state") {
              const k = String(args.key ?? "").trim();
              const v = String(args.value ?? "");
              if (k) {
                const s = getChatState(chatId);
                if (v === "") delete s[k];
                else s[k] = v;
                scheduleStateSave();
                toolResult = `state.${k} = ${v.slice(0, 60) || "(cleared)"}`;
              } else {
                toolResult = "missing key";
              }
            } else if (name === "list_state") {
              const s = chatStatesObj[chatId] ?? {};
              const entries = Object.entries(s)
                .filter(([, v]) => v !== "")
                .map(([k, v]) => `${k} = ${v.slice(0, 200)}`);
              toolResult =
                entries.length === 0
                  ? "(no state)"
                  : `state:\n${entries.join("\n")}`;
            } else if (name === "clear_state") {
              delete chatStatesObj[chatId];
              scheduleStateSave();
              toolResult = "cleared";
            } else if (name === "generate_image") {
              const prompt = String(args.prompt ?? "").trim();
              const caption =
                typeof args.caption === "string" ? args.caption : undefined;
              const sizeArg =
                typeof args.size === "string" &&
                ["1024x1024", "1536x1024", "1024x1536"].includes(args.size)
                  ? (args.size as "1024x1024" | "1536x1024" | "1024x1536")
                  : "1024x1024";
              if (!prompt) {
                toolResult = "missing prompt";
              } else {
                await bot.sendChatAction(chatId, "upload_photo");
                const buf = await generateImageBuffer(prompt, sizeArg);
                await bot.sendPhoto(chatId, buf, caption ? { caption } : {});
                sentSomething = true;
                toolResult = "image sent";
              }
            } else if (name === "forward_to_owner") {
              const t = String(args.text ?? "").trim();
              if (!t) {
                toolResult = "empty text";
              } else if (!config.ownerUserId) {
                toolResult = "no owner set for this bot";
              } else {
                try {
                  await bot.sendMessage(
                    config.ownerUserId,
                    `📨 از کاربر ${userIdFromChat(chatId) ?? chatId}:\n\n${t}`,
                  );
                  toolResult = "forwarded to owner";
                } catch (fwdErr) {
                  toolResult = `forward failed: ${(fwdErr as Error).message}`;
                }
              }
            } else if (name === "broadcast_message") {
              const t = String(args.text ?? "").trim();
              const isOwner = userIdFromChat(chatId) === config.ownerUserId;
              if (!t) {
                toolResult = "empty text";
              } else if (!isOwner) {
                toolResult = "forbidden: only owner can broadcast";
              } else {
                let sent = 0;
                let failed = 0;
                for (const uid of knownUsers) {
                  if (uid === config.ownerUserId) continue;
                  try {
                    await bot.sendMessage(uid, t);
                    sent++;
                  } catch {
                    failed++;
                  }
                }
                toolResult = `broadcast: ${sent} sent, ${failed} failed`;
              }
            } else if (name === "get_user_info") {
              const uid = userIdFromChat(chatId) ?? chatId;
              const isOwner = uid === config.ownerUserId;
              toolResult = JSON.stringify({
                user_id: uid,
                chat_id: chatId,
                is_bot_owner: isOwner,
                bot_name: config.name,
                bot_username: config.username,
              });
            } else if (name === "set_bot_commands") {
              const uid = userIdFromChat(chatId) ?? chatId;
              if (uid !== config.ownerUserId) {
                toolResult = JSON.stringify({
                  error: "فقط مالک ربات می‌تونه دستورات رو ست کنه.",
                });
              } else {
                const list = Array.isArray(args.commands) ? args.commands : [];
                const cleaned = list
                  // eslint-disable-next-line @typescript-eslint/no-explicit-any
                  .map((c: any) => ({
                    command: String(c?.command ?? "")
                      .trim()
                      .replace(/^\//, "")
                      .toLowerCase(),
                    description: String(c?.description ?? "").trim().slice(0, 256),
                  }))
                  .filter(
                    (c: { command: string; description: string }) =>
                      /^[a-z][a-z0-9_]{0,31}$/.test(c.command) && c.description.length > 0,
                  );
                try {
                  await bot.setMyCommands(cleaned);
                  toolResult = JSON.stringify({
                    ok: true,
                    set_count: cleaned.length,
                    commands: cleaned,
                  });
                } catch (e) {
                  const m = e instanceof Error ? e.message : "error";
                  toolResult = JSON.stringify({ ok: false, error: m });
                }
              }
            } else {
              toolResult = `unknown tool: ${name}`;
            }
          } catch (toolErr) {
            const m = toolErr instanceof Error ? toolErr.message : "error";
            toolResult = `error: ${m}`;
            logger.warn(
              { err: toolErr, chatId, bot: config.name, name },
              "Child bot tool failed",
            );
          }

          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const toolResMsg: any = {
            role: "tool",
            tool_call_id: tc.id,
            content: toolResult,
          };
          messages.push(toolResMsg);
          history.push(toolResMsg);
        }
      }

      if (!sentSomething) {
        try {
          await bot.sendMessage(chatId, "متأسفم، نتونستم جواب بدم.");
        } catch {
          /* ignore */
        }
      }

      histories.set(chatId, history.slice(-MAX_HISTORY));
    } catch (err) {
      logger.error(
        { err, chatId, bot: config.name },
        "Child bot reply failed",
      );
      try {
        await bot.sendMessage(chatId, "مشکلی پیش اومد. لطفاً دوباره تلاش کن.");
      } catch {
        /* ignore */
      }
    }
  }

  async function transcribeChildAudio(
    fileId: string,
    mime: string,
    fileExt: string,
  ): Promise<string | null> {
    const fileLink = await bot.getFileLink(fileId);
    const res = await fetch(fileLink);
    const buf = Buffer.from(await res.arrayBuffer());
    const file = await toFile(buf, `voice.${fileExt}`, { type: mime });
    const tx = await openai.audio.transcriptions.create({
      file,
      model: "whisper-1",
    });
    return tx.text?.trim() || null;
  }

  bot.on("voice", async (msg) => {
    const chatId = msg.chat.id;
    const userId = msg.from?.id ?? chatId;
    if (isBanned(userId)) return;
    if (!(await ensureChildAccess(userId, chatId))) return;

    const voice = msg.voice;
    if (!voice) return;

    try {
      await bot.sendChatAction(chatId, "typing");
      const text = await transcribeChildAudio(
        voice.file_id,
        voice.mime_type ?? "audio/ogg",
        "ogg",
      );
      if (!text) {
        await bot.sendMessage(chatId, "❌ نتونستم صدات رو متوجه بشم.");
        return;
      }
      await bot.sendMessage(chatId, `🎤 شنیدم: «${text}»`);
      await runChildChatTurn(text, chatId);
    } catch (err) {
      logger.error(
        { err, chatId, bot: config.name },
        "Child bot voice failed",
      );
      try {
        await bot.sendMessage(chatId, "❌ نتونستم صدات رو پردازش کنم.");
      } catch {
        /* ignore */
      }
    }
  });

  bot.on("audio", async (msg) => {
    const chatId = msg.chat.id;
    const userId = msg.from?.id ?? chatId;
    if (isBanned(userId)) return;
    if (!(await ensureChildAccess(userId, chatId))) return;

    const audio = msg.audio;
    if (!audio) return;

    try {
      await bot.sendChatAction(chatId, "typing");
      const text = await transcribeChildAudio(
        audio.file_id,
        audio.mime_type ?? "audio/mpeg",
        "mp3",
      );
      if (!text) {
        await bot.sendMessage(chatId, "❌ نتونستم صدا رو متوجه بشم.");
        return;
      }
      await bot.sendMessage(chatId, `🎵 متن صدا: «${text}»`);
      await runChildChatTurn(text, chatId);
    } catch (err) {
      logger.error(
        { err, chatId, bot: config.name },
        "Child bot audio failed",
      );
      try {
        await bot.sendMessage(chatId, "❌ نتونستم صدا رو پردازش کنم.");
      } catch {
        /* ignore */
      }
    }
  });

  bot.on("message", async (msg) => {
    const text = msg.text;
    if (!text || text.startsWith("/")) return;

    const chatId = msg.chat.id;
    const userId = msg.from?.id ?? chatId;
    if (isBanned(userId)) {
      try {
        await bot.sendMessage(chatId, "⛔ تو از این ربات مسدود شدی.");
      } catch {
        /* ignore */
      }
      return;
    }
    if (!(await ensureChildAccess(userId, chatId))) return;

    void sendAndPinPromo(chatId);
    await runChildChatTurn(text, chatId);
  });

  // When user taps an inline (glass) button, deliver a synthetic user message
  // to the AI so it can react based on the callback_data and current state.
  bot.on("callback_query", async (cb) => {
    const chatId = cb.message?.chat.id;
    const userId = cb.from?.id;
    const data = cb.data;
    if (!chatId || !userId || !data) {
      try {
        await bot.answerCallbackQuery(cb.id);
      } catch {
        /* ignore */
      }
      return;
    }
    markActive(userId, chatId);
    if (isBanned(userId)) {
      try {
        await bot.answerCallbackQuery(cb.id, { text: "⛔ مسدود شدی" });
      } catch {
        /* ignore */
      }
      return;
    }
    if (!isVerifiedUser(userId)) {
      try {
        await bot.answerCallbackQuery(cb.id, {
          text: "اول شماره تلفنت رو بفرست",
        });
      } catch {
        /* ignore */
      }
      return;
    }

    try {
      await bot.answerCallbackQuery(cb.id);
    } catch {
      /* ignore */
    }

    const synthetic = `[دکمه فشرده شد: ${data}]`;
    await runChildChatTurn(synthetic, chatId);
  });

  running.set(config.token, bot);
  const me = await bot.getMe();
  const username = me.username ?? config.name;
  logger.info({ name: config.name, username }, "Child bot started");
  return { username };
}

export async function createChildBot(
  params: {
    token: string;
    name: string;
    systemPrompt: string;
    ownerUserId?: number;
  },
  openai: OpenAI,
): Promise<{ username: string }> {
  const configs = loadConfigs();
  const existing = configs.find((c) => c.token === params.token);
  if (existing) {
    const result = await startChildBot(existing, openai);
    existing.systemPrompt = params.systemPrompt;
    existing.name = params.name;
    existing.username = result.username;
    if (params.ownerUserId && !existing.ownerUserId) {
      existing.ownerUserId = params.ownerUserId;
    }
    saveConfigs(configs);
    return result;
  }

  const config: ChildBotConfig = {
    token: params.token,
    name: params.name,
    systemPrompt: params.systemPrompt,
    createdAt: new Date().toISOString(),
    ownerUserId: params.ownerUserId,
  };

  const result = await startChildBot(config, openai);
  config.username = result.username;
  configs.push(config);
  saveConfigs(configs);
  return result;
}

export async function stopChildBot(identifier: string): Promise<boolean> {
  const configs = loadConfigs();
  const idx = configs.findIndex(
    (c) =>
      c.username?.toLowerCase() === identifier.toLowerCase() ||
      c.name.toLowerCase() === identifier.toLowerCase() ||
      c.token === identifier,
  );
  if (idx === -1) return false;
  const config = configs[idx]!;
  const bot = running.get(config.token);
  if (bot) {
    try {
      await bot.stopPolling();
    } catch (err) {
      logger.error({ err }, "Failed to stop child bot polling");
    }
    running.delete(config.token);
  }
  configs.splice(idx, 1);
  saveConfigs(configs);
  return true;
}

export async function startAllChildBots(openai: OpenAI): Promise<void> {
  const configs = loadConfigs();
  for (const config of configs) {
    try {
      await startChildBot(config, openai);
    } catch (err) {
      logger.error(
        { err, name: config.name },
        "Failed to start child bot on boot",
      );
    }
  }
}
