const { TelegramClient, Api } = require("telegram");
const { StringSession } = require("telegram/sessions");
const { NewMessage } = require("telegram/events");

class TelegramManager {
  constructor(store) {
    this.store = store;
    this.clients = new Map();
    this.pendingLogins = new Map();
    this.monitorHandler = null;
    this.monitorClientId = null;
    this.monitorGroups = [];
    this.monitorTimer = null;
    this.monitorState = new Map();
    this.lastMonitorMessageAt = "";
    this.lastMonitorSource = "";
    this.taskMonitors = new Map();
    this.taskRoleAssignments = new Map();
    this.inviteIndex = 0;
    this.desktopApiId = 2040;
    this.desktopApiHash = "b18441a1ff607e10a989891a5462e627";
    this.participantCache = new Map();
  }

  async init() {
    const accounts = this.store.listAccounts();
    for (const account of accounts) {
      try {
        await this._connectAccount(account);
      } catch (error) {
        const errorText = error && (error.errorMessage || error.message) ? (error.errorMessage || error.message) : String(error);
        this.store.updateAccountStatus(account.id, "error", errorText);
        this.store.addAccountEvent(account.id, account.phone || "", "connect_failed", errorText);
      }
    }
  }

  async _connectAccount(account) {
    const session = new StringSession(account.session);
    const client = new TelegramClient(session, Number(account.api_id || account.apiId), account.api_hash || account.apiHash, {
      connectionRetries: 3
    });
    await client.connect();
    try {
      const me = await client.getMe();
      if (me && me.id) {
        this.store.updateAccountIdentity(
          account.id,
          me.id.toString(),
          me.phone || account.phone || "",
          me.username || ""
        );
        account.user_id = me.id.toString();
        if (me.phone) account.phone = me.phone;
        account.username = me.username || account.username || "";
      }
    } catch (error) {
      // ignore identity fetch errors
    }
    this.clients.set(account.id, { client, account });
  }

  async startLogin({ apiId, apiHash, phone }) {
    const session = new StringSession("");
    const client = new TelegramClient(session, Number(apiId), apiHash, {
      connectionRetries: 3
    });

    await client.connect();
    const sendResult = await client.invoke(
      new Api.auth.SendCode({
        phoneNumber: phone,
        apiId: Number(apiId),
        apiHash,
        settings: new Api.CodeSettings({})
      })
    );

    const loginId = `${phone}-${Date.now()}`;
    this.pendingLogins.set(loginId, {
      client,
      apiId: Number(apiId),
      apiHash,
      phone,
      phoneCodeHash: sendResult.phoneCodeHash
    });

    return { loginId };
  }

  async completeLogin({ loginId, code, password }) {
    const pending = this.pendingLogins.get(loginId);
    if (!pending) {
      return { ok: false, error: "Login session not found" };
    }

    const { client, apiId, apiHash, phone, phoneCodeHash } = pending;
    try {
      await client.invoke(
        new Api.auth.SignIn({
          phoneNumber: phone,
          phoneCodeHash,
          phoneCode: code
        })
      );
    } catch (error) {
      if (error.errorMessage === "SESSION_PASSWORD_NEEDED") {
        if (!password) {
          return { ok: false, error: "PASSWORD_REQUIRED" };
        }
        await client.checkPassword(password);
      } else {
        return { ok: false, error: error.message || String(error) };
      }
    }

    const sessionString = client.session.save();
    const settings = this.store.getSettings();
    const me = await client.getMe();
    const userId = me && me.id ? me.id.toString() : "";
    const username = me && me.username ? me.username : "";
    const actualPhone = me && me.phone ? me.phone : phone;
    const existing = this.store.findAccountByIdentity({
      userId,
      phone: actualPhone,
      session: sessionString
    });
    if (existing) {
      this.pendingLogins.delete(loginId);
      try {
        await client.disconnect();
      } catch (error) {
        // ignore disconnect errors
      }
      return { ok: false, error: "DUPLICATE_ACCOUNT", accountId: existing.id };
    }
    const accountId = this.store.addAccount({
      phone: actualPhone,
      apiId,
      apiHash,
      session: sessionString,
      userId,
      username,
      maxGroups: settings.accountMaxGroups,
      dailyLimit: settings.accountDailyLimit,
      status: "ok",
      lastError: ""
    });

    this.clients.set(accountId, {
      client,
      account: {
        id: accountId,
        phone: actualPhone,
        api_id: apiId,
        api_hash: apiHash,
        user_id: userId,
        username,
        status: "ok",
        last_error: ""
      }
    });
    this.pendingLogins.delete(loginId);

    return { ok: true, accountId };
  }

  async importTdataSession({ sessionString, apiId, apiHash }) {
    const usedApiId = Number(apiId || this.desktopApiId);
    const usedApiHash = apiHash || this.desktopApiHash;
    const session = new StringSession(sessionString);
    const client = new TelegramClient(session, usedApiId, usedApiHash, {
      connectionRetries: 3
    });
    await client.connect();
    const me = await client.getMe();
    const phone = me && me.phone ? me.phone : "unknown";
    const userId = me && me.id ? me.id.toString() : "";
    const username = me && me.username ? me.username : "";
    const existing = this.store.findAccountByIdentity({
      userId,
      phone,
      session: sessionString
    });
    if (existing) {
      try {
        await client.disconnect();
      } catch (error) {
        // ignore disconnect errors
      }
      return { ok: false, error: "DUPLICATE_ACCOUNT", accountId: existing.id };
    }

    const savedSession = client.session.save();
    const settings = this.store.getSettings();
    const accountId = this.store.addAccount({
      phone,
      apiId: usedApiId,
      apiHash: usedApiHash,
      session: savedSession,
      userId,
      username,
      maxGroups: settings.accountMaxGroups,
      dailyLimit: settings.accountDailyLimit,
      status: "ok",
      lastError: ""
    });

    this.clients.set(accountId, {
      client,
      account: {
        id: accountId,
        phone,
        api_id: usedApiId,
        api_hash: usedApiHash,
        user_id: userId,
        username,
        max_groups: settings.accountMaxGroups,
        daily_limit: settings.accountDailyLimit,
        status: "ok",
        last_error: ""
      }
    });

    return { ok: true, accountId };
  }

  async startMonitoring(competitorGroup) {
    const groups = Array.isArray(competitorGroup) ? competitorGroup.filter(Boolean) : [competitorGroup].filter(Boolean);
    if (!groups.length) return;
    if (this.monitorHandler) return;

    const accountEntry = this._pickClient();
    if (!accountEntry) return;

    this.monitorClientId = accountEntry.account.id;
    this.monitorGroups = groups;
    const client = accountEntry.client;

    const settings = this.store.getSettings();
    await this._autoJoinGroups(client, groups, settings.autoJoinCompetitors, accountEntry.account);

    const resolved = [];
    const errors = [];
    for (const group of groups) {
      const result = await this._resolveGroupEntity(client, group, settings.autoJoinCompetitors, accountEntry.account);
      if (result.ok) {
        resolved.push({ entity: result.entity, source: group });
      } else {
        errors.push(`${group}: ${result.error}`);
      }
    }
    if (!resolved.length) {
      return { ok: false, errors: errors.length ? errors : ["No groups to monitor"] };
    }

    this.monitorHandler = async (event) => {
      const sender = event.message.senderId;
      if (!sender) return;
      const userId = sender.toString();
      if (this._isOwnAccount(userId)) return;
      let senderEntity = null;
      try {
        senderEntity = await event.getSender();
      } catch (error) {
        senderEntity = null;
      }
      if (senderEntity && senderEntity.bot) return;
      const username = senderEntity && senderEntity.username ? senderEntity.username : "";
      const accessHash = senderEntity && senderEntity.accessHash ? senderEntity.accessHash.toString() : "";
      const sourceChat = event.message.chatId ? event.message.chatId.toString() : "unknown";
      if (!this.monitorState.has(sourceChat)) return;
      this.store.enqueueInvite(0, userId, username, sourceChat, accessHash, this.monitorClientId || 0);
      this.lastMonitorMessageAt = new Date().toISOString();
      this.lastMonitorSource = sourceChat;
    };

    this.monitorState.clear();
    resolved.forEach((item) => {
      const id = item.entity && item.entity.id != null ? item.entity.id.toString() : item.source;
      this.monitorState.set(id, { entity: item.entity, source: item.source, lastId: 0 });
    });
    client.addEventHandler(this.monitorHandler, new NewMessage({}));
    this._startMonitorPolling(client);
    return { ok: true, errors };
  }

  async stopMonitoring() {
    if (!this.monitorHandler || this.monitorClientId == null) return;
    const entry = this.clients.get(this.monitorClientId);
    if (entry) {
      entry.client.removeEventHandler(this.monitorHandler);
    }
    this.monitorHandler = null;
    this.monitorClientId = null;
    this.monitorGroups = [];
    this.monitorState.clear();
    if (this.monitorTimer) clearInterval(this.monitorTimer);
    this.monitorTimer = null;
  }

  async removeAccount(accountId) {
    const entry = this.clients.get(accountId);
    if (!entry) return;
    if (this.monitorClientId === accountId) {
      await this.stopMonitoring();
    }
    try {
      await entry.client.disconnect();
    } catch (error) {
      // ignore disconnect errors
    }
    this.clients.delete(accountId);
  }

  async refreshAccountIdentity(accountId) {
    const entry = this.clients.get(accountId);
    if (!entry) return;
    try {
      const me = await entry.client.getMe();
      if (me && me.id) {
        this.store.updateAccountIdentity(
          accountId,
          me.id.toString(),
          me.phone || entry.account.phone || "",
          me.username || ""
        );
        entry.account.user_id = me.id.toString();
        if (me.phone) entry.account.phone = me.phone;
        entry.account.username = me.username || entry.account.username || "";
      }
    } catch (error) {
      // ignore identity refresh errors
    }
  }

  isMonitoring() {
    return Boolean(this.monitorHandler);
  }

  getMonitorInfo() {
    return {
      monitoring: this.isMonitoring(),
      accountId: this.monitorClientId || 0,
      groups: this.monitorGroups || [],
      lastMessageAt: this.lastMonitorMessageAt,
      lastSource: this.lastMonitorSource
    };
  }

  _pickClient() {
    const entries = Array.from(this.clients.values());
    if (!entries.length) return null;
    const ordered = entries.filter((entry) => entry.account.status === "ok" && !this._isInCooldown(entry.account));
    if (!ordered.length) return null;
    const entry = ordered[this.inviteIndex % ordered.length];
    this.inviteIndex += 1;
    return entry;
  }

  async inviteUser(targetGroup, userId) {
    const entry = this._pickClientForInvite();
    if (!entry) {
      return { ok: false, error: "No available accounts under limits", accountId: 0, accountPhone: "" };
    }

    const { client, account } = entry;
    try {
      const allowJoin = this.store.getSettings().autoJoinOurGroup;
      await this._autoJoinGroups(client, [targetGroup], allowJoin, account);
      const resolved = await this._resolveGroupEntity(client, targetGroup, allowJoin, account);
      if (!resolved.ok) {
        throw new Error(resolved.error);
      }
      const targetEntity = resolved.entity;
      const user = await client.getEntity(userId);

      if (targetEntity.className === "Channel") {
        await client.invoke(
          new Api.channels.InviteToChannel({
            channel: targetEntity,
            users: [user]
          })
        );
      } else if (targetEntity.className === "Chat") {
        await client.invoke(
          new Api.messages.AddChatUser({
            chatId: targetEntity.id,
            userId: user,
            fwdLimit: 0
          })
        );
      } else {
        throw new Error("Unsupported target chat type");
      }

      this.store.updateAccountStatus(account.id, "ok", "");
      return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
    } catch (error) {
      const errorText = error.errorMessage || error.message || String(error);
      if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
        this._applyFloodCooldown(account, errorText);
      } else {
        this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
      }
      return { ok: false, error: errorText, accountId: account.id, accountPhone: account.phone || "" };
    }
  }

  async inviteUserForTask(task, userId, allowedAccountIds, options = {}) {
    const entry = this._pickClientForInvite(allowedAccountIds, options.randomize);
    if (!entry) {
      return { ok: false, error: "No available accounts under limits", accountId: 0, accountPhone: "" };
    }

    const { client, account } = entry;
    let targetEntity = null;
    let targetType = "";
    const attemptInvite = async (user) => {
      if (!targetEntity) {
        throw new Error("Target group not resolved");
      }
      if (targetEntity.className === "Channel") {
        await client.invoke(
          new Api.channels.InviteToChannel({
            channel: targetEntity,
            users: [user]
          })
        );
      } else if (targetEntity.className === "Chat") {
        await client.invoke(
          new Api.messages.AddChatUser({
            chatId: targetEntity.id,
            userId: user,
            fwdLimit: 0
          })
        );
      } else {
        throw new Error("Unsupported target chat type");
      }
    };

    const resolveInputUser = async () => {
      const accessHash = options.userAccessHash || "";
      const providedUsername = options.username || "";
      const sourceChat = options.sourceChat || "";
      const attempts = [];
      let user = null;
      if (accessHash) {
        try {
          user = new Api.InputUser({
            userId: BigInt(userId),
            accessHash: BigInt(accessHash)
          });
          attempts.push({ strategy: "access_hash", ok: true, detail: "from message" });
        } catch (error) {
          user = null;
          attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" });
        }
      }
      if (!user && sourceChat) {
        const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
        if (resolved && resolved.accessHash) {
          try {
            user = new Api.InputUser({
              userId: BigInt(userId),
              accessHash: BigInt(resolved.accessHash)
            });
            attempts.push({ strategy: "participants", ok: true, detail: resolved.detail || "from participants" });
          } catch (error) {
            user = null;
            attempts.push({ strategy: "participants", ok: false, detail: "invalid access_hash" });
          }
        } else {
          attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" });
        }
      }
      if (!user && providedUsername) {
        const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
        try {
          user = await client.getEntity(username);
          attempts.push({ strategy: "username", ok: true, detail: username });
        } catch (error) {
          user = null;
          attempts.push({ strategy: "username", ok: false, detail: "resolve failed" });
        }
      }
      if (!user) {
        const resolvedUser = await client.getEntity(userId);
        user = await client.getInputEntity(resolvedUser);
        attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" });
      }
      return { user, attempts };
    };

    let lastAttempts = [];
    try {
      const accessHash = options.userAccessHash || "";
      const providedUsername = options.username || "";
      const allowJoin = Boolean(task.auto_join_our_group);
      await this._autoJoinGroups(client, [task.our_group], allowJoin, account);
      const resolvedTarget = await this._resolveGroupEntity(client, task.our_group, allowJoin, account);
      if (!resolvedTarget.ok) throw new Error(resolvedTarget.error);
      targetEntity = resolvedTarget.entity;
      if (targetEntity && targetEntity.className === "Channel") {
        targetType = targetEntity.megagroup ? "megagroup" : "channel";
      } else if (targetEntity && targetEntity.className === "Chat") {
        targetType = "group";
      } else {
        targetType = targetEntity && targetEntity.className ? targetEntity.className : "";
      }
      const resolved = await resolveInputUser();
      lastAttempts = resolved.attempts || [];
      const user = resolved.user;
      await attemptInvite(user);

      this.store.updateAccountStatus(account.id, "ok", "");
      const last = lastAttempts.filter((item) => item.ok).slice(-1)[0];
      return {
        ok: true,
        accountId: account.id,
        accountPhone: account.phone || "",
        strategy: last ? last.strategy : "",
        strategyMeta: JSON.stringify(lastAttempts),
        targetType
      };
    } catch (error) {
      const errorText = error.errorMessage || error.message || String(error);
      let fallbackMeta = lastAttempts.length ? JSON.stringify(lastAttempts) : "";
      if (errorText === "USER_ID_INVALID") {
        const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
      try {
        let retryUser = null;
        if (!retryUser && options.sourceChat) {
          const resolved = await this._resolveUserFromSource(client, options.sourceChat, userId);
          if (resolved && resolved.accessHash) {
            retryUser = new Api.InputUser({
              userId: BigInt(userId),
              accessHash: BigInt(resolved.accessHash)
            });
          }
        }
        if (!retryUser && username) {
          try {
            retryUser = await client.getEntity(username);
            } catch (resolveError) {
              retryUser = null;
            }
          }
          if (!retryUser) {
            const resolvedUser = await client.getEntity(userId);
            retryUser = await client.getInputEntity(resolvedUser);
          }
          await attemptInvite(retryUser);
          this.store.updateAccountStatus(account.id, "ok", "");
          return { ok: true, accountId: account.id, accountPhone: account.phone || "" };
        } catch (retryError) {
          const retryText = retryError.errorMessage || retryError.message || String(retryError);
          this.store.addAccountEvent(
            account.id,
            account.phone || "",
            "invite_user_invalid",
            `USER_ID_INVALID -> retry failed (${retryText}); user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}`
          );
          fallbackMeta = JSON.stringify([
            { strategy: "retry", ok: false, detail: retryText }
          ]);
        }
        this.store.addAccountEvent(
          account.id,
          account.phone || "",
          "invite_user_invalid",
          `USER_ID_INVALID; user=${userId}; username=${options.username || "—"}; hash=${options.userAccessHash || "—"}`
        );
      }
      if (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD")) {
        this._applyFloodCooldown(account, errorText);
      } else {
        this.store.updateAccountStatus(account.id, account.status || "ok", errorText);
      }
      return {
        ok: false,
        error: errorText,
        accountId: account.id,
        accountPhone: account.phone || "",
        strategy: "",
        strategyMeta: fallbackMeta,
        targetType
      };
    }
  }

  _pickClientForInvite(allowedAccountIds, randomize) {
    const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
    if (!entries.length) return null;

    const settings = this.store.getSettings();
    const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
      ? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
      : entries;
    if (!allowed.length) return null;
    const eligible = allowed.filter((entry) => {
      if (this._isInCooldown(entry.account)) return false;
      const limit = Number(entry.account.daily_limit || settings.accountDailyLimit || 0);
      if (limit > 0) {
        const used = this.store.countInvitesTodayByAccount(entry.account.id);
        if (used >= limit) return false;
      }
      return true;
    });
    if (!eligible.length) return null;
    if (randomize) {
      const entry = eligible[Math.floor(Math.random() * eligible.length)];
      this.inviteIndex += 1;
      return entry;
    }
    for (let i = 0; i < allowed.length; i += 1) {
      const entry = allowed[(this.inviteIndex + i) % allowed.length];
      if (!eligible.includes(entry)) continue;
      this.inviteIndex += 1;
      return entry;
    }
    return eligible[0] || null;
  }

  _pickClientFromAllowed(allowedAccountIds) {
    const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
    if (!entries.length) return null;
    const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
      ? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
      : entries;
    if (!allowed.length) return null;
    const available = allowed.filter((entry) => !this._isInCooldown(entry.account));
    return available[0] || null;
  }

  _listClientsFromAllowed(allowedAccountIds) {
    const entries = Array.from(this.clients.values()).filter((entry) => entry.account.status === "ok");
    if (!entries.length) return [];
    const allowed = Array.isArray(allowedAccountIds) && allowedAccountIds.length
      ? entries.filter((entry) => allowedAccountIds.includes(entry.account.id))
      : entries;
    return allowed.filter((entry) => !this._isInCooldown(entry.account));
  }

  pickInviteAccount(allowedAccountIds, randomize) {
    return this._pickClientForInvite(allowedAccountIds, randomize);
  }

  async parseHistory(competitorGroups, limit) {
    const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [competitorGroups].filter(Boolean);
    if (!groups.length) return { ok: false, error: "No competitor groups" };

    const entry = this._pickClient();
    if (!entry) return { ok: false, error: "No available accounts" };

    const { client } = entry;
    const perGroupLimit = Math.max(1, Number(limit) || 200);
    await this._autoJoinGroups(client, groups, this.store.getSettings().autoJoinCompetitors, entry.account);
    const errors = [];
    for (const group of groups) {
      const resolved = await this._resolveGroupEntity(client, group, this.store.getSettings().autoJoinCompetitors, entry.account);
      if (!resolved.ok) {
        errors.push(`${group}: ${resolved.error}`);
        continue;
      }
      const messages = await client.getMessages(resolved.entity, { limit: perGroupLimit });
      for (const message of messages) {
        const senderId = message.senderId;
        if (!senderId) continue;
        let username = "";
        let accessHash = "";
        try {
          const sender = await message.getSender();
          if (sender && sender.bot) continue;
          username = sender && sender.username ? sender.username : "";
          accessHash = sender && sender.accessHash ? sender.accessHash.toString() : "";
        } catch (error) {
          username = "";
          accessHash = "";
        }
        this.store.enqueueInvite(0, senderId.toString(), username, group, accessHash, this.monitorClientId || 0);
      }
    }

    return { ok: true, errors };
  }

  async getMembershipStatus(competitorGroups, ourGroup) {
    const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [];
    const results = [];
    const titleClientEntry = Array.from(this.clients.values())[0];
    const titleClient = titleClientEntry ? titleClientEntry.client : null;
    const titleMap = new Map();
    if (titleClient) {
      for (const group of groups) {
        const title = await this._getGroupTitle(titleClient, group);
        titleMap.set(group, title);
      }
      if (ourGroup) {
        const title = await this._getGroupTitle(titleClient, ourGroup);
        titleMap.set(ourGroup, title);
      }
    }

    for (const entry of this.clients.values()) {
      const { client, account } = entry;

      let competitorCount = 0;
      const competitorGroupsInfo = [];
      for (const group of groups) {
        const isMember = await this._isParticipant(client, group);
        if (isMember) competitorCount += 1;
        competitorGroupsInfo.push({
          link: group,
          title: titleMap.get(group) || "",
          isMember
        });
      }

      let ourGroupMember = false;
      let ourGroupInfo = null;
      if (ourGroup) {
        ourGroupMember = await this._isParticipant(client, ourGroup);
        ourGroupInfo = {
          link: ourGroup,
          title: titleMap.get(ourGroup) || "",
          isMember: ourGroupMember
        };
      }

      results.push({
        accountId: account.id,
        competitorCount,
        competitorTotal: groups.length,
        ourGroupMember,
        competitorGroups: competitorGroupsInfo,
        ourGroup: ourGroupInfo
      });
    }

    return results;
  }

  async _getGroupTitle(client, group) {
    try {
      if (this._isInviteLink(group)) {
        const hash = this._extractInviteHash(group);
        if (!hash) return "";
        const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
        if (check && check.chat && check.chat.title) return check.chat.title;
        if (check && check.title) return check.title;
        return "";
      }
      const entity = await client.getEntity(group);
      return entity && entity.title ? entity.title : "";
    } catch (error) {
      return "";
    }
  }

  async _loadParticipantCache(client, entity, limit) {
    const map = new Map();
    if (!client || !entity) return map;
    try {
      const participants = await client.getParticipants(entity, { limit: limit || 200 });
      (participants || []).forEach((user) => {
        if (!user || user.className !== "User") return;
        const userId = user.id != null ? user.id.toString() : "";
        if (!userId) return;
        const accessHash = user.accessHash != null ? user.accessHash.toString() : "";
        const username = user.username ? user.username : "";
        if (!accessHash && !username) return;
        map.set(userId, { accessHash, username });
      });
    } catch (error) {
      // ignore cache errors
    }
    return map;
  }

  async getGroupVisibility(task, competitorGroups) {
    const groups = (competitorGroups || []).filter(Boolean);
    if (!groups.length) return [];
    const entry = this._pickClientFromAllowed([]);
    if (!entry) return [];
    const results = [];
    for (const group of groups) {
      const resolved = await this._resolveGroupEntity(entry.client, group, false, entry.account);
      if (!resolved || !resolved.ok) {
        results.push({
          source: group,
          ok: false,
          title: "",
          visibleCount: 0,
          hidden: true,
          error: resolved ? resolved.error : "resolve_failed"
        });
        continue;
      }
      const title = resolved.entity && resolved.entity.title ? resolved.entity.title : "";
      const cache = await this._loadParticipantCache(entry.client, resolved.entity, 200);
      const visibleCount = cache.size;
      results.push({
        source: group,
        ok: true,
        title,
        visibleCount,
        hidden: visibleCount === 0,
        error: ""
      });
    }
    return results;
  }

  async _resolveUserFromSource(client, sourceChat, userId) {
    if (!client || !userId) {
      return { accessHash: "", detail: "no client" };
    }
    if (!sourceChat) {
      return { accessHash: "", detail: "no source chat" };
    }
    const cacheEntry = this.participantCache.get(sourceChat);
    const now = Date.now();
    if (cacheEntry && now - cacheEntry.at < 5 * 60 * 1000) {
      const cached = cacheEntry.map.get(userId.toString());
      if (cached && cached.accessHash) {
        return {
          accessHash: cached.accessHash,
          detail: `cache hit (${cacheEntry.map.size})`
        };
      }
      return { accessHash: "", detail: `cache miss (${cacheEntry.map.size})` };
    }
    const resolved = await this._resolveGroupEntity(client, sourceChat, false, null);
    if (!resolved || !resolved.ok) {
      return { accessHash: "", detail: `resolve failed (${resolved ? resolved.error : "unknown"})` };
    }
    const map = await this._loadParticipantCache(client, resolved.entity, 400);
    this.participantCache.set(sourceChat, { at: now, map });
    if (!map.size) {
      return { accessHash: "", detail: "participants hidden" };
    }
    const cached = map.get(userId.toString());
    if (cached && cached.accessHash) {
      return { accessHash: cached.accessHash, detail: `participants (${map.size})` };
    }
    return { accessHash: "", detail: `not in participants (${map.size})` };
  }

  async _resolveQueueIdentity(client, sourceChat, userId) {
    const attempts = [];
    if (!client || !userId) {
      return { accessHash: "", strategy: "", detail: "no client or userId", attempts };
    }
    const participantResult = await this._resolveUserFromSource(client, sourceChat, userId);
    attempts.push({
      strategy: "participants",
      ok: Boolean(participantResult && participantResult.accessHash),
      detail: participantResult ? participantResult.detail : "unknown"
    });
    if (participantResult && participantResult.accessHash) {
      return {
        accessHash: participantResult.accessHash.toString(),
        strategy: "participants",
        detail: participantResult.detail,
        attempts
      };
    }
    try {
      const resolvedUser = await client.getEntity(userId);
      const input = await client.getInputEntity(resolvedUser);
      if (input && input.accessHash != null) {
        attempts.push({ strategy: "entity", ok: true, detail: "getEntity(userId)" });
        return {
          accessHash: input.accessHash.toString(),
          strategy: "entity",
          detail: "getEntity(userId)",
          attempts
        };
      }
      attempts.push({ strategy: "entity", ok: false, detail: "no access_hash" });
    } catch (error) {
      attempts.push({ strategy: "entity", ok: false, detail: error.message || String(error) });
    }
    return { accessHash: "", strategy: "", detail: "", attempts };
  }

  async ensureJoinOurGroup(ourGroup) {
    if (!ourGroup) return { ok: false, error: "No target group" };
    const entry = this._pickClient();
    if (!entry) return { ok: false, error: "No available accounts" };
    await this._autoJoinGroups(entry.client, [ourGroup], true, entry.account);
    return { ok: true };
  }

  async checkGroupAccess(competitorGroups, ourGroup) {
    const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [];
    const result = [];
    const entry = this._pickClient();
    if (!entry) return { ok: false, error: "No available accounts" };

    const { client } = entry;
    const allGroups = [
      ...groups.map((value) => ({ type: "competitor", value })),
      ...(ourGroup ? [{ type: "our", value: ourGroup }] : [])
    ];

    for (const item of allGroups) {
      const value = item.value;
      try {
        if (this._isInviteLink(value)) {
          const hash = this._extractInviteHash(value);
          if (!hash) throw new Error("Invalid invite link");
          const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
          const title = check && check.chat ? check.chat.title : "";
          result.push({
            type: item.type,
            value,
            ok: true,
            title,
            details: check.className || ""
          });
        } else {
          const entity = await client.getEntity(value);
          const title = entity && entity.title ? entity.title : "";
          result.push({
            type: item.type,
            value,
            ok: true,
            title,
            details: "entity"
          });
        }
      } catch (error) {
        result.push({
          type: item.type,
          value,
          ok: false,
          title: "",
          details: error.errorMessage || error.message || String(error)
        });
      }
    }

    return { ok: true, result };
  }

  async checkInvitePermissions(task, accountIds) {
    if (!task || !task.our_group) {
      return { ok: false, error: "No target group" };
    }
    const ids = Array.isArray(accountIds) ? accountIds.filter(Boolean) : [];
    if (!ids.length) {
      return { ok: false, error: "No invite accounts" };
    }
    const accounts = this.store.listAccounts();
    const accountMap = new Map(accounts.map((account) => [account.id, account]));
    const results = [];
    for (const accountId of ids) {
      const entry = this.clients.get(accountId);
      const accountRecord = accountMap.get(accountId);
      if (!entry) {
        results.push({
          accountId,
          accountPhone: accountRecord ? (accountRecord.phone || "") : "",
          ok: false,
          canInvite: false,
          member: false,
          reason: "Сессия не подключена",
          targetType: "",
          title: "",
          targetChat: task.our_group
        });
        continue;
      }
      const { client, account } = entry;
      const resolved = await this._resolveGroupEntity(client, task.our_group, Boolean(task.auto_join_our_group), account);
      if (!resolved.ok) {
        results.push({
          accountId,
          accountPhone: account.phone || "",
          ok: false,
          canInvite: false,
          member: false,
          reason: resolved.error || "Не удалось получить группу",
          targetType: "",
          title: "",
          targetChat: task.our_group
        });
        continue;
      }
      const entity = resolved.entity;
      const title = entity && entity.title ? entity.title : "";
      const className = entity && entity.className ? entity.className : "";
      let targetType = className;
      if (className === "Channel") {
        targetType = entity && entity.megagroup ? "megagroup" : "channel";
      } else if (className === "Chat") {
        targetType = "group";
      }
      let canInvite = false;
      let member = true;
      let reason = "";

      try {
        if (className === "Channel") {
          const me = await client.getMe();
          const participant = await client.invoke(new Api.channels.GetParticipant({
            channel: entity,
            participant: me
          }));
          const part = participant && participant.participant ? participant.participant : participant;
          const className = part && part.className ? part.className : "";
          const isAdmin = className.includes("Admin") || className.includes("Creator");
          const addUsers = part && part.adminRights ? Boolean(part.adminRights.addUsers) : isAdmin;
          canInvite = Boolean(isAdmin && addUsers);
          if (!canInvite) {
            reason = "Нужны права администратора на добавление участников";
          }
        } else if (className === "Chat") {
          let fullChat = null;
          try {
            fullChat = await client.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
          } catch (error) {
            fullChat = null;
          }
          const full = fullChat && fullChat.fullChat ? fullChat.fullChat : null;
          const restricted = Boolean(full && full.defaultBannedRights && full.defaultBannedRights.inviteUsers);
          if (restricted) {
            canInvite = false;
            reason = "Добавление пользователей запрещено для участников";
          } else {
            canInvite = true;
          }
        } else {
          canInvite = false;
          reason = "Не удалось распознать тип цели (не группа/канал)";
        }
      } catch (error) {
        const errorText = error.errorMessage || error.message || String(error);
        if (errorText.includes("USER_NOT_PARTICIPANT")) {
          member = false;
          reason = "Аккаунт не состоит в группе";
        } else if (errorText.includes("CHAT_ADMIN_REQUIRED")) {
          reason = "Нужны права администратора";
        } else {
          reason = errorText;
        }
      }

      results.push({
        accountId,
        accountPhone: account.phone || "",
        ok: true,
        canInvite,
        member,
        reason,
        targetType,
        title,
        targetChat: task.our_group
      });
    }
    return { ok: true, result: results };
  }

  async _autoJoinGroups(client, groups, enabled, account) {
    if (!enabled) return;
    const settings = this.store.getSettings();
    let maxGroups = Number(account && account.max_groups != null ? account.max_groups : settings.accountMaxGroups);
    if (!Number.isFinite(maxGroups) || maxGroups <= 0) {
      maxGroups = Number.POSITIVE_INFINITY;
    }
    const existing = [];
    const toJoin = [];
    for (const group of groups) {
      if (!group) continue;
      try {
        const alreadyMember = await this._isParticipant(client, group);
        if (alreadyMember) {
          existing.push(group);
        } else {
          toJoin.push(group);
        }
      } catch (error) {
        toJoin.push(group);
      }
    }
    const alreadyCount = existing.length;
    const requiredCount = toJoin.length;
    let allowedCount = Number.isFinite(maxGroups) ? Math.max(0, maxGroups - alreadyCount) : requiredCount;
    if (!Number.isFinite(maxGroups)) {
      allowedCount = requiredCount;
    }
    if (Number.isFinite(maxGroups) && account && requiredCount > allowedCount) {
      const message = `Лимит групп: ${maxGroups}. Уже в группах задачи: ${alreadyCount}. Нужно добавить: ${requiredCount}. Доступно слотов: ${allowedCount}.`;
      this.store.addAccountEvent(account.id, account.phone || "", "auto_join_limit", message);
    }
    if (allowedCount <= 0) return;
    const joinList = toJoin.slice(0, allowedCount);
    for (const group of joinList) {
      try {
        if (this._isInviteLink(group)) {
          const hash = this._extractInviteHash(group);
          if (hash) {
            await client.invoke(new Api.messages.ImportChatInvite({ hash }));
          }
        } else {
          const entity = await client.getEntity(group);
          await client.invoke(new Api.channels.JoinChannel({ channel: entity }));
        }
      } catch (error) {
        const errorText = error.errorMessage || error.message || String(error);
        if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
          this._applyFloodCooldown(account, errorText);
        }
      }
    }
  }

  async _isParticipant(client, group) {
    try {
      if (this._isInviteLink(group)) {
        const hash = this._extractInviteHash(group);
        if (!hash) return false;
        const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
        if (check && typeof check.className === "string" && check.className.includes("ChatInviteAlready")) {
          return true;
        }
        return false;
      }
      const entity = await client.getEntity(group);
      const me = await client.getMe();
      await client.invoke(new Api.channels.GetParticipant({ channel: entity, participant: me }));
      return true;
    } catch (error) {
      return false;
    }
  }

  async _resolveGroupEntity(client, group, allowJoin, account) {
    try {
      if (this._isInviteLink(group)) {
        const hash = this._extractInviteHash(group);
        if (!hash) return { ok: false, error: "Invalid invite link" };
        try {
          const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
          if (check && check.chat) {
            return { ok: true, entity: await this._normalizeEntity(client, check.chat) };
          }
        } catch (error) {
          // ignore and try import
        }
        if (!allowJoin) {
          return { ok: false, error: "Invite link requires auto-join" };
        }
        try {
          const imported = await client.invoke(new Api.messages.ImportChatInvite({ hash }));
          if (imported && imported.chats && imported.chats.length) {
            return { ok: true, entity: await this._normalizeEntity(client, imported.chats[0]) };
          }
        } catch (error) {
          const errorText = error.errorMessage || error.message || String(error);
          if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
            this._applyFloodCooldown(account, errorText);
          }
          if (errorText.includes("USER_ALREADY_PARTICIPANT")) {
            try {
              const check = await client.invoke(new Api.messages.CheckChatInvite({ hash }));
              if (check && check.chat) {
                return { ok: true, entity: check.chat };
              }
            } catch (checkError) {
              // fall through
            }
            return { ok: false, error: "USER_ALREADY_PARTICIPANT" };
          }
          return { ok: false, error: errorText };
        }
        return { ok: false, error: "Unable to resolve invite link" };
      }

      const entity = await client.getEntity(group);
      return { ok: true, entity: await this._normalizeEntity(client, entity) };
    } catch (error) {
      const errorText = error.errorMessage || error.message || String(error);
      if (account && (errorText.includes("FLOOD") || errorText.includes("PEER_FLOOD"))) {
        this._applyFloodCooldown(account, errorText);
      }
      return { ok: false, error: errorText };
    }
  }

  _startMonitorPolling(client) {
    if (this.monitorTimer) clearInterval(this.monitorTimer);
    this.monitorTimer = setInterval(async () => {
      if (!this.isMonitoring()) return;
      for (const [key, state] of this.monitorState.entries()) {
        try {
          const messages = await client.getMessages(state.entity, { limit: 10 });
          for (const message of messages.reverse()) {
            if (state.lastId && message.id <= state.lastId) continue;
            state.lastId = Math.max(state.lastId || 0, message.id || 0);
            if (!message.senderId) continue;
            const senderId = message.senderId.toString();
            if (this._isOwnAccount(senderId)) continue;
            let username = "";
            let accessHash = "";
            try {
              const sender = await message.getSender();
              if (sender && sender.bot) continue;
              username = sender && sender.username ? sender.username : "";
              accessHash = sender && sender.accessHash ? sender.accessHash.toString() : "";
            } catch (error) {
              username = "";
              accessHash = "";
            }
            this.store.enqueueInvite(0, senderId, username, state.source, accessHash, this.monitorClientId || 0);
            this.lastMonitorMessageAt = new Date().toISOString();
            this.lastMonitorSource = state.source;
          }
        } catch (error) {
          // ignore polling errors
        }
      }
    }, 10000);
  }

  async _normalizeEntity(client, entity) {
    if (!entity) return entity;
    if (entity.className === "Chat" || entity.className === "Channel") {
      return entity;
    }
    if (entity.id != null) {
      return await client.getEntity(entity.id);
    }
    return entity;
  }

  _isOwnAccount(userId) {
    const accounts = Array.from(this.clients.values()).map((entry) => entry.account);
    return accounts.some((account) => account.user_id && account.user_id.toString() === userId);
  }

  async joinGroupsForAllAccounts(competitorGroups, ourGroup, settings) {
    const groups = Array.isArray(competitorGroups) ? competitorGroups.filter(Boolean) : [];
    for (const entry of this.clients.values()) {
      if (settings.autoJoinCompetitors) {
        await this._autoJoinGroups(entry.client, groups, true, entry.account);
      }
      if (settings.autoJoinOurGroup && ourGroup) {
        await this._autoJoinGroups(entry.client, [ourGroup], true, entry.account);
      }
    }
  }

  async joinGroupsForTask(task, competitorGroups, accountIds, roleIds = {}) {
    const accounts = Array.from(this.clients.values()).filter((entry) => accountIds.includes(entry.account.id));
    const competitorBots = Math.max(1, Number(task.max_competitor_bots || 1));
    const ourBots = Math.max(1, Number(task.max_our_bots || 1));

    const explicitMonitorIds = Array.isArray(roleIds.monitorIds) ? roleIds.monitorIds : [];
    const explicitInviteIds = Array.isArray(roleIds.inviteIds) ? roleIds.inviteIds : [];
    const hasExplicitRoles = explicitMonitorIds.length || explicitInviteIds.length;

    const competitors = competitorGroups || [];
    let cursor = 0;
    const usedForCompetitors = new Set();
    if (hasExplicitRoles) {
      for (const group of competitors) {
        const pool = accounts.filter((entry) => explicitMonitorIds.includes(entry.account.id));
        for (const entry of pool) {
          usedForCompetitors.add(entry.account.id);
          if (task.auto_join_competitors) {
            await this._autoJoinGroups(entry.client, [group], true, entry.account);
          }
        }
      }
    } else {
      for (const group of competitors) {
        for (let i = 0; i < competitorBots; i += 1) {
          if (!accounts.length) break;
          const entry = accounts[cursor % accounts.length];
          cursor += 1;
          usedForCompetitors.add(entry.account.id);
          if (task.auto_join_competitors) {
            await this._autoJoinGroups(entry.client, [group], true, entry.account);
          }
        }
      }
    }

    const usedForOur = new Set();
    if (task.our_group) {
      if (hasExplicitRoles) {
        const pool = accounts.filter((entry) => explicitInviteIds.includes(entry.account.id));
        for (const entry of pool) {
          usedForOur.add(entry.account.id);
          if (task.auto_join_our_group) {
            await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
          }
        }
      } else if (task.require_same_bot_in_both) {
        const pool = accounts.filter((entry) => usedForCompetitors.has(entry.account.id));
        const finalPool = pool.length ? pool : accounts;
        const targetCount = Math.max(1, Number(task.max_competitor_bots || 1));
        const limitedPool = finalPool.slice(0, Math.min(targetCount, finalPool.length));
        for (const entry of limitedPool) {
          usedForOur.add(entry.account.id);
          if (task.auto_join_our_group) {
            await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
          }
        }
      } else {
        const available = accounts.filter((entry) => !usedForCompetitors.has(entry.account.id));
        const pool = task.separate_bot_roles ? available : (available.length ? available : accounts);
        for (let i = 0; i < Math.min(ourBots, pool.length); i += 1) {
          const entry = pool[i];
          usedForOur.add(entry.account.id);
          if (task.auto_join_our_group) {
            await this._autoJoinGroups(entry.client, [task.our_group], true, entry.account);
          }
        }
      }
    }
    this.taskRoleAssignments.set(task.id, {
      competitorIds: hasExplicitRoles ? explicitMonitorIds : Array.from(usedForCompetitors),
      ourIds: hasExplicitRoles ? explicitInviteIds : Array.from(usedForOur)
    });
  }

  async _startMonitorEntry(task, monitorAccount, groups) {
    const resolved = [];
    const errors = [];
    for (const group of groups) {
      const result = await this._resolveGroupEntity(
        monitorAccount.client,
        group,
        Boolean(task.auto_join_competitors),
        monitorAccount.account
      );
      if (result.ok) {
        resolved.push({ entity: result.entity, source: group });
      } else {
        errors.push(`${group}: ${result.error}`);
      }
    }
    if (!resolved.length) return { ok: false, error: "No groups to monitor", errors };
    if (errors.length) {
      this.store.addAccountEvent(
        monitorAccount.account.id,
        monitorAccount.account.phone,
        "monitor_error",
        errors.join(" | ")
      );
    }

    const state = new Map();
    resolved.forEach((item) => {
      const id = item.entity && item.entity.id != null ? item.entity.id.toString() : item.source;
      const title = item.entity && item.entity.title ? item.entity.title : "";
      state.set(id, {
        entity: item.entity,
        source: item.source,
        title,
        lastId: 0,
        participantCache: new Map(),
        participantCacheAt: 0
      });
    });

    const monitorEntry = {
      timer: null,
      handler: null,
      accountId: monitorAccount.account.id,
      groups,
      lastMessageAt: "",
      lastSource: "",
      lastErrorAt: new Map(),
      lastSkipAt: new Map(),
      lastMessageEventAt: new Map(),
      lastEventAt: new Map(),
      state
    };
    this.store.addAccountEvent(
      monitorAccount.account.id,
      monitorAccount.account.phone,
      "monitor_started",
      `Групп: ${resolved.length}`
    );

    const shouldLogEvent = (key, intervalMs) => {
      const now = Date.now();
      const last = monitorEntry.lastEventAt.get(key) || 0;
      if (now - last < intervalMs) return false;
      monitorEntry.lastEventAt.set(key, now);
      return true;
    };

    const formatGroupLabel = (st) => (st.title ? `${st.title} (${st.source})` : st.source);

    const ensureParticipantCache = async (st) => {
      const now = Date.now();
      if (st.participantCache && st.participantCache.size && now - st.participantCacheAt < 5 * 60 * 1000) {
        return;
      }
      st.participantCache = await this._loadParticipantCache(monitorAccount.client, st.entity, 200);
      st.participantCacheAt = now;
    };

    const handler = async (event) => {
      const message = event.message;
      const chatId = message && message.chatId != null
        ? message.chatId.toString()
        : (message && message.peerId && (message.peerId.channelId || message.peerId.chatId || message.peerId.userId))
          ? (message.peerId.channelId || message.peerId.chatId || message.peerId.userId).toString()
          : "";
      const st = state.get(chatId);
      if (!st) return;
      if (st.lastId && message.id <= st.lastId) return;
      if (shouldLogEvent(`${chatId}:raw`, 20000)) {
        this.store.addAccountEvent(
          monitorAccount.account.id,
          monitorAccount.account.phone,
          "new_message_raw",
          `${formatGroupLabel(st)}: ${this._describeSender(message)}`
        );
      }
      st.lastId = Math.max(st.lastId || 0, message.id || 0);
      const senderInfo = await this._getUserInfoFromMessage(monitorAccount.client, message);
      const rawSenderId = message && message.senderId != null ? message.senderId.toString() : "";
      if (!senderInfo || !senderInfo.info) {
        const resolved = rawSenderId
          ? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
          : { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
        if (!resolved || !resolved.accessHash) {
          if (shouldLogEvent(`${chatId}:skip`, 30000)) {
            const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
            const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
            const strategyBlock = strategyLines.length
              ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
              : "";
            this.store.addAccountEvent(
              monitorAccount.account.id,
              monitorAccount.account.phone,
              "new_message_skipped",
              `${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}`
            );
          }
          return;
        }
        const senderId = rawSenderId;
        const username = "";
        const accessHash = resolved.accessHash;
        if (shouldLogEvent(`${chatId}:queue`, 30000)) {
          const detail = resolved.detail ? `, ${resolved.detail}` : "";
          this.store.addAccountEvent(
            monitorAccount.account.id,
            monitorAccount.account.phone,
            "queue_strategy",
            `${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
          );
        }
        monitorEntry.lastMessageAt = new Date().toISOString();
        monitorEntry.lastSource = st.source;
        this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id);
        return;
      }
      const senderPayload = { ...senderInfo.info };
      if (!senderPayload.accessHash && !senderPayload.username) {
        await ensureParticipantCache(st);
        const cached = st.participantCache.get(senderPayload.userId);
        if (cached && cached.accessHash) {
          senderPayload.accessHash = cached.accessHash;
          senderPayload.username = cached.username || senderPayload.username;
        }
      }
      if (!senderPayload.accessHash && !senderPayload.username) {
        const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
        if (resolved && resolved.accessHash) {
          senderPayload.accessHash = resolved.accessHash;
          if (shouldLogEvent(`${chatId}:queue`, 30000)) {
            const detail = resolved.detail ? `, ${resolved.detail}` : "";
            this.store.addAccountEvent(
              monitorAccount.account.id,
              monitorAccount.account.phone,
              "queue_strategy",
              `${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
            );
          }
        }
        if (!senderPayload.accessHash && !senderPayload.username) {
          if (shouldLogEvent(`${chatId}:skip`, 30000)) {
            const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
            const strategyBlock = strategyLines.length
              ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
              : "";
            this.store.addAccountEvent(
              monitorAccount.account.id,
              monitorAccount.account.phone,
              "new_message_skipped",
              `${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}`
            );
          }
          return;
        }
      }
      const { userId: senderId, username, accessHash } = senderPayload;
      if (this._isOwnAccount(senderId)) return;
      let messageDate = new Date();
      if (message.date instanceof Date) {
        messageDate = message.date;
      } else if (typeof message.date === "number") {
        messageDate = new Date(message.date * 1000);
      }
      monitorEntry.lastMessageAt = messageDate.toISOString();
      monitorEntry.lastSource = st.source;
      const enqueued = this.store.enqueueInvite(
        task.id,
        senderId,
        username,
        st.source,
        accessHash,
        monitorAccount.account.id
      );
      if (enqueued) {
        const now = Date.now();
        const lastEvent = monitorEntry.lastMessageEventAt.get(chatId) || 0;
        if (now - lastEvent > 15000) {
          monitorEntry.lastMessageEventAt.set(chatId, now);
          this.store.addAccountEvent(
            monitorAccount.account.id,
            monitorAccount.account.phone,
            "new_message",
            `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}`
          );
        }
      } else if (shouldLogEvent(`${chatId}:dup`, 30000)) {
        this.store.addAccountEvent(
          monitorAccount.account.id,
          monitorAccount.account.phone,
          "new_message_duplicate",
          `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId} уже в очереди`
        );
      }
    };

    try {
      monitorAccount.client.addEventHandler(handler, new NewMessage({ chats: resolved.map((item) => item.entity) }));
      monitorEntry.handler = handler;
    } catch (error) {
      this.store.addAccountEvent(
        monitorAccount.account.id,
        monitorAccount.account.phone,
        "monitor_handler_error",
        error.message || String(error)
      );
    }

    const timer = setInterval(async () => {
      for (const [key, st] of state.entries()) {
        try {
          const messages = await monitorAccount.client.getMessages(st.entity, { limit: 10 });
          let totalMessages = 0;
          let enqueued = 0;
          let skipped = 0;
          for (const message of messages.reverse()) {
            totalMessages += 1;
            if (st.lastId && message.id <= st.lastId) continue;
            if (shouldLogEvent(`${key}:raw`, 20000)) {
              this.store.addAccountEvent(
                monitorAccount.account.id,
                monitorAccount.account.phone,
                "new_message_raw",
                `${formatGroupLabel(st)}: ${this._describeSender(message)}`
              );
            }
            st.lastId = Math.max(st.lastId || 0, message.id || 0);
            const senderInfo = await this._getUserInfoFromMessage(monitorAccount.client, message);
            const rawSenderId = message && message.senderId != null ? message.senderId.toString() : "";
            if (!senderInfo || !senderInfo.info) {
              const resolved = rawSenderId
                ? await this._resolveQueueIdentity(monitorAccount.client, st.source, rawSenderId)
                : { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
              if (!resolved || !resolved.accessHash) {
                skipped += 1;
                if (shouldLogEvent(`${key}:skip`, 30000)) {
                  const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
                  const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
                  const strategyBlock = strategyLines.length
                    ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
                    : "";
                  this.store.addAccountEvent(
                    monitorAccount.account.id,
                    monitorAccount.account.phone,
                    "new_message_skipped",
                    `${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}`
                  );
                }
                continue;
              }
              const senderId = rawSenderId;
              const username = "";
              const accessHash = resolved.accessHash;
              if (shouldLogEvent(`${key}:queue`, 30000)) {
                const detail = resolved.detail ? `, ${resolved.detail}` : "";
                this.store.addAccountEvent(
                  monitorAccount.account.id,
                  monitorAccount.account.phone,
                  "queue_strategy",
                  `${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
                );
              }
              monitorEntry.lastMessageAt = new Date().toISOString();
              monitorEntry.lastSource = st.source;
              if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
                enqueued += 1;
              }
              continue;
            }
            const senderPayload = { ...senderInfo.info };
            if (!senderPayload.accessHash && !senderPayload.username) {
              await ensureParticipantCache(st);
              const cached = st.participantCache.get(senderPayload.userId);
              if (cached && cached.accessHash) {
                senderPayload.accessHash = cached.accessHash;
                senderPayload.username = cached.username || senderPayload.username;
              }
            }
            if (!senderPayload.accessHash && !senderPayload.username) {
              const resolved = await this._resolveQueueIdentity(monitorAccount.client, st.source, senderPayload.userId);
              if (resolved && resolved.accessHash) {
                senderPayload.accessHash = resolved.accessHash;
                if (shouldLogEvent(`${key}:queue`, 30000)) {
                  const detail = resolved.detail ? `, ${resolved.detail}` : "";
                  this.store.addAccountEvent(
                    monitorAccount.account.id,
                    monitorAccount.account.phone,
                    "queue_strategy",
                    `${formatGroupLabel(st)}: найден access_hash (${resolved.strategy})${detail}`
                  );
                }
              }
              if (!senderPayload.accessHash && !senderPayload.username) {
                skipped += 1;
                if (shouldLogEvent(`${key}:skip`, 30000)) {
                  const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
                  const strategyBlock = strategyLines.length
                    ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
                    : "";
                  this.store.addAccountEvent(
                    monitorAccount.account.id,
                    monitorAccount.account.phone,
                    "new_message_skipped",
                    `${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}`
                  );
                }
                continue;
              }
            }
            const { userId: senderId, username, accessHash } = senderPayload;
            if (this._isOwnAccount(senderId)) continue;
            let messageDate = new Date();
            if (message.date instanceof Date) {
              messageDate = message.date;
            } else if (typeof message.date === "number") {
              messageDate = new Date(message.date * 1000);
            }
            monitorEntry.lastMessageAt = messageDate.toISOString();
            monitorEntry.lastSource = st.source;
            if (this.store.enqueueInvite(task.id, senderId, username, st.source, accessHash, monitorAccount.account.id)) {
              enqueued += 1;
              const now = Date.now();
              const lastEvent = monitorEntry.lastMessageEventAt.get(key) || 0;
              if (now - lastEvent > 15000) {
                monitorEntry.lastMessageEventAt.set(key, now);
                this.store.addAccountEvent(
                  monitorAccount.account.id,
                  monitorAccount.account.phone,
                  "new_message",
                  `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId}`
                );
              }
            } else if (shouldLogEvent(`${key}:dup`, 30000)) {
              this.store.addAccountEvent(
                monitorAccount.account.id,
                monitorAccount.account.phone,
                "new_message_duplicate",
                `${formatGroupLabel(st)}: ${username ? `@${username}` : senderId} уже в очереди`
              );
            }
          }
          if (totalMessages > 0 && enqueued === 0 && skipped > 0) {
            const now = Date.now();
            const lastSkip = monitorEntry.lastSkipAt.get(key) || 0;
            if (now - lastSkip > 60000) {
              monitorEntry.lastSkipAt.set(key, now);
              this.store.addAccountEvent(
                monitorAccount.account.id,
                monitorAccount.account.phone,
                "monitor_skip",
                `${formatGroupLabel(st)}\nПричина: сообщения есть, но авторов нельзя определить (анонимные админы/каналы, скрытые участники, нет access_hash)\nПропущено: ${skipped}`
              );
            }
          }
        } catch (error) {
          const now = Date.now();
          const lastError = monitorEntry.lastErrorAt.get(key) || 0;
          if (now - lastError > 60000) {
            monitorEntry.lastErrorAt.set(key, now);
            this.store.addAccountEvent(
              monitorAccount.account.id,
              monitorAccount.account.phone,
              "monitor_poll_error",
              `${st.source}: ${error.message || String(error)}`
            );
          }
        }
      }
    }, 20000);
    monitorEntry.timer = timer;
    this.store.addAccountEvent(
      monitorAccount.account.id,
      monitorAccount.account.phone,
      "monitor_polling_started",
      `Интервал: 20 сек`
    );

    return { ok: true, entry: monitorEntry, errors };
  }

  async startTaskMonitor(task, competitorGroups, accountIds, monitorIds = []) {
    const role = this.taskRoleAssignments.get(task.id);
    const explicitMonitorIds = Array.isArray(monitorIds) ? monitorIds.filter(Boolean) : [];
    const allowed = (task.separate_bot_roles || task.require_same_bot_in_both) && role && role.competitorIds.length
      ? role.competitorIds
      : (explicitMonitorIds.length ? explicitMonitorIds : accountIds);
    const monitorAccounts = this._listClientsFromAllowed(allowed);
    if (!monitorAccounts.length) return { ok: false, error: "No accounts for task" };
    const groups = (competitorGroups || []).filter(Boolean);
    if (!groups.length) return { ok: false, error: "No groups to monitor" };
    const targetCount = Math.max(1, Number(task.max_competitor_bots || monitorAccounts.length || 1));
    const monitorPool = monitorAccounts.slice(0, Math.min(targetCount, monitorAccounts.length));
    const chunks = monitorPool.map(() => []);
    groups.forEach((group, index) => {
      const bucket = index % chunks.length;
      chunks[bucket].push(group);
    });

    const entries = [];
    const errors = [];
    for (let i = 0; i < monitorPool.length; i += 1) {
      const accountEntry = monitorPool[i];
      const chunk = chunks[i] || [];
      if (!chunk.length) continue;
      const result = await this._startMonitorEntry(task, accountEntry, chunk);
      if (result.ok && result.entry) {
        entries.push(result.entry);
      }
      if (result.errors && result.errors.length) {
        errors.push(...result.errors);
      }
    }
    if (!entries.length) return { ok: false, error: "No groups to monitor", errors };
    this.taskMonitors.set(task.id, { entries });
    return { ok: errors.length === 0, errors };
  }

  async parseHistoryForTask(task, competitorGroups, accountIds) {
    const groups = (competitorGroups || []).filter(Boolean);
    if (!groups.length) return { ok: false, error: "No competitor groups" };
    const entry = this._pickClientFromAllowed(accountIds);
    if (!entry) return { ok: false, error: "No available accounts" };
    const perGroupLimit = Math.max(1, Number(task.history_limit || 200));

    if (task.auto_join_competitors) {
      await this._autoJoinGroups(entry.client, groups, true, entry.account);
    }
    const summaryLines = [];
    let totalEnqueued = 0;
    const errors = [];
    for (const group of groups) {
      const resolved = await this._resolveGroupEntity(entry.client, group, Boolean(task.auto_join_competitors), entry.account);
      if (!resolved.ok) {
        errors.push(`${group}: ${resolved.error}`);
        continue;
      }
      const participantCache = await this._loadParticipantCache(entry.client, resolved.entity, Math.max(200, perGroupLimit));
      const messages = await entry.client.getMessages(resolved.entity, { limit: perGroupLimit });
      let total = 0;
      let enqueued = 0;
      let skipped = 0;
      const skipReasons = {};
      let strategySkipSample = "";
      for (const message of messages) {
        total += 1;
        const senderInfo = await this._getUserInfoFromMessage(entry.client, message);
        const rawSenderId = message && message.senderId != null ? message.senderId.toString() : "";
        if (!senderInfo || !senderInfo.info) {
          const resolved = rawSenderId
            ? await this._resolveQueueIdentity(entry.client, group, rawSenderId)
            : { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
          if (!resolved || !resolved.accessHash) {
            skipped += 1;
            const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
            skipReasons[reason] = (skipReasons[reason] || 0) + 1;
            if (!strategySkipSample) {
              const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
              if (strategyLines.length) {
                strategySkipSample = `${group}\nПричина: ${reason}\nШаги попыток:\n- ${strategyLines.join("\n- ")}`;
              }
            }
            continue;
          }
          if (this.store.enqueueInvite(task.id, rawSenderId, "", group, resolved.accessHash, entry.account.id)) {
            enqueued += 1;
            totalEnqueued += 1;
          }
          continue;
        }
        const senderPayload = { ...senderInfo.info };
        if (!senderPayload.accessHash && !senderPayload.username) {
          const cached = participantCache.get(senderPayload.userId);
          if (cached && cached.accessHash) {
            senderPayload.accessHash = cached.accessHash;
            senderPayload.username = cached.username || senderPayload.username;
          }
        }
        if (!senderPayload.accessHash && !senderPayload.username) {
          const resolved = await this._resolveQueueIdentity(entry.client, group, senderPayload.userId);
          if (resolved && resolved.accessHash) {
            senderPayload.accessHash = resolved.accessHash;
          }
          if (!senderPayload.accessHash && !senderPayload.username && !strategySkipSample) {
            const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
            if (strategyLines.length) {
              strategySkipSample = `${group}\nПричина: нет access_hash (нет в списке участников)\nШаги попыток:\n- ${strategyLines.join("\n- ")}`;
            }
          }
        }
        if (!senderPayload.accessHash && !senderPayload.username) {
          skipped += 1;
          const reason = "нет access_hash (нет в списке участников)";
          skipReasons[reason] = (skipReasons[reason] || 0) + 1;
          continue;
        }
        const { userId: senderId, username, accessHash } = senderPayload;
        if (this._isOwnAccount(senderId)) continue;
        if (this.store.enqueueInvite(task.id, senderId, username, group, accessHash, entry.account.id)) {
          enqueued += 1;
          totalEnqueued += 1;
        }
      }
      const skipSummary = Object.entries(skipReasons)
        .map(([reason, count]) => `${reason}: ${count}`)
        .slice(0, 4)
        .join(", ");
      summaryLines.push(
        `${group}: сообщений ${total}, добавлено ${enqueued}, пропущено ${skipped}${skipSummary ? ` (${skipSummary})` : ""}`
      );
      if (strategySkipSample) {
        this.store.addAccountEvent(
          entry.account.id,
          entry.account.phone,
          "history_skip_detail",
          strategySkipSample
        );
      }
    }
    if (summaryLines.length) {
      this.store.addAccountEvent(
        entry.account.id,
        entry.account.phone,
        "history_summary",
        summaryLines.join(" | ")
      );
    }
    if (totalEnqueued === 0 && errors.length === 0) {
      this.store.addAccountEvent(
        entry.account.id,
        entry.account.phone,
        "history_empty",
        "История собрана, но пользователей для очереди нет"
      );
    }
    return { ok: true, errors };
  }

  async _getUserInfoFromMessage(client, message) {
    try {
      const sender = await message.getSender();
      if (!sender) return { info: null, reason: "автор не найден" };
      if (sender.className !== "User") return { info: null, reason: "отправитель не пользователь" };
      if (sender.bot) return { info: null, reason: "бот" };
      let resolvedSender = sender;
      if (client && sender.min) {
        try {
          resolvedSender = await client.getEntity(sender);
        } catch (error) {
          resolvedSender = sender;
        }
      }
      const userId = resolvedSender.id != null ? resolvedSender.id.toString() : "";
      if (!userId) return { info: null, reason: "нет user_id" };
      const username = resolvedSender.username ? resolvedSender.username : "";
      let accessHash = resolvedSender.accessHash ? resolvedSender.accessHash.toString() : "";
      let inputEntity = null;
      if (client) {
        try {
          inputEntity = await client.getInputEntity(resolvedSender);
          if (inputEntity && inputEntity.accessHash != null) {
            accessHash = inputEntity.accessHash.toString();
          }
        } catch (error) {
          inputEntity = null;
        }
      }
      if (!inputEntity && !username) {
        return { info: { userId, username: "", accessHash: "" }, reason: "нет сущности пользователя (username скрыт/нет доступа)" };
      }
      if (!accessHash && !username) {
        return { info: { userId, username: "", accessHash: "" }, reason: "нет access_hash и username" };
      }
      return { info: { userId, username, accessHash }, reason: "" };
    } catch (error) {
      return { info: null, reason: "ошибка чтения автора" };
    }
  }

  _describeSender(message) {
    try {
      const peer = message && message.peerId ? message.peerId : null;
      const sender = message && message.sender ? message.sender : null;
      const senderId = message && message.senderId != null ? message.senderId.toString() : "";
      if (sender && sender.className) {
        if (sender.className === "User") {
          const name = sender.username ? `@${sender.username}` : (senderId || "user");
          return `user ${name}`;
        }
        if (sender.className === "Channel") {
          const title = sender.title || "channel";
          return `channel ${title}`;
        }
      }
      if (peer && peer.channelId) return "channel";
      if (peer && peer.chatId) return "chat";
      if (peer && peer.userId) return "user";
      return "unknown";
    } catch (error) {
      return "unknown";
    }
  }

  _formatStrategyAttemptLines(attempts) {
    if (!Array.isArray(attempts) || attempts.length === 0) return [];
    return attempts.map((item, index) => {
      const label = this._strategyLabelRu(item.strategy);
      const status = item.ok ? "успех" : "не удалось";
      const detail = this._translateStrategyDetail(item.detail);
      const detailText = detail ? ` — ${detail}` : "";
      return `${index + 1}) ${label} — ${status}${detailText}`;
    });
  }

  _strategyLabelRu(strategy) {
    switch (strategy) {
      case "access_hash":
        return "access_hash из сообщения";
      case "participants":
        return "участники группы";
      case "username":
        return "поиск по username";
      case "entity":
        return "getEntity по userId";
      case "retry":
        return "повторная попытка";
      default:
        return strategy || "стратегия";
    }
  }

  _translateStrategyDetail(detail) {
    if (!detail) return "";
    const raw = String(detail).trim();
    if (!raw) return "";
    const normalized = raw.toLowerCase();
    if (normalized === "from message") return "access_hash найден в сообщении";
    if (normalized === "invalid access_hash") return "access_hash невалиден";
    if (normalized === "from participants") return "найден в списке участников";
    if (normalized === "no result") return "нет результата";
    if (normalized === "resolve failed") return "не удалось найти пользователя по username";
    if (normalized === "getentity(userid)") return "получен через getEntity";
    if (normalized === "no access_hash") return "нет access_hash";
    if (normalized === "no sender_id") return "в сообщении нет sender_id";
    if (normalized.includes("could not find the input entity")) {
      return "не удалось получить сущность пользователя (entity)";
    }
    if (normalized.startsWith("not in participants")) {
      const match = raw.match(/\((\d+)\)/);
      const checked = match ? ` (проверено: ${match[1]})` : "";
      return `не найден в списке участников${checked}`;
    }
    if (normalized === "invalid") return "ошибка в данных пользователя";
    return `деталь: ${raw.replace(/\s+/g, " ").slice(0, 120)}`;
  }

  stopTaskMonitor(taskId) {
    const entry = this.taskMonitors.get(taskId);
    if (!entry) return;
    const entries = Array.isArray(entry.entries) ? entry.entries : [entry];
    entries.forEach((monitorEntry) => {
      if (monitorEntry.timer) clearInterval(monitorEntry.timer);
      const clientEntry = this.clients.get(monitorEntry.accountId);
      if (clientEntry && monitorEntry.handler) {
        try {
          clientEntry.client.removeEventHandler(monitorEntry.handler);
        } catch (error) {
          // ignore handler removal errors
        }
      }
    });
    this.taskMonitors.delete(taskId);
    this.taskRoleAssignments.delete(taskId);
  }

  getTaskRoleAssignments(taskId) {
    return this.taskRoleAssignments.get(taskId) || { competitorIds: [], ourIds: [] };
  }

  getTaskMonitorInfo(taskId) {
    const entry = this.taskMonitors.get(taskId);
    if (!entry) {
      return { monitoring: false, accountId: 0, accountIds: [], groups: [], lastMessageAt: "", lastSource: "" };
    }
    const entries = Array.isArray(entry.entries) ? entry.entries : [entry];
    const accountIds = entries.map((item) => item.accountId).filter(Boolean);
    let lastMessageAt = "";
    let lastSource = "";
    entries.forEach((item) => {
      if (item.lastMessageAt && (!lastMessageAt || item.lastMessageAt > lastMessageAt)) {
        lastMessageAt = item.lastMessageAt;
        lastSource = item.lastSource || "";
      }
    });
    return {
      monitoring: true,
      accountId: accountIds[0] || 0,
      accountIds,
      groups: entries.flatMap((item) => item.groups || []),
      lastMessageAt,
      lastSource
    };
  }

  _isInCooldown(account) {
    if (!account || !account.cooldown_until) return false;
    try {
      return new Date(account.cooldown_until).getTime() > Date.now();
    } catch (error) {
      return false;
    }
  }

  _applyFloodCooldown(account, reason) {
    const settings = this.store.getSettings();
    const minutes = Number(settings.floodCooldownMinutes || 1440);
    this.store.setAccountCooldown(account.id, minutes, reason);
    this.store.addAccountEvent(account.id, account.phone || "", "flood", `FLOOD cooldown: ${minutes} min. ${reason || ""}`);
    account.status = "limited";
    account.last_error = reason || "";
    account.cooldown_until = minutes > 0 ? new Date(Date.now() + minutes * 60000).toISOString() : "";
    account.cooldown_reason = reason || "";
  }

  _isInviteLink(value) {
    return value.includes("joinchat/") || value.includes("t.me/+");
  }

  _extractInviteHash(value) {
    if (value.includes("joinchat/")) {
      return value.split("joinchat/")[1].split(/[?#]/)[0];
    }
    if (value.includes("t.me/+")) {
      return value.split("t.me/+")[1].split(/[?#]/)[0];
    }
    return "";
  }

  getConnectedCount() {
    return this.clients.size;
  }
}

module.exports = { TelegramManager };
