const dayjs = require("dayjs");

class TaskRunner {
  constructor(store, telegram, task) {
    this.store = store;
    this.telegram = telegram;
    this.task = task;
    this.running = false;
    this.timer = null;
    this.nextRunAt = "";
    this.nextInviteAccountId = 0;
    this.lastInviteAccountId = 0;
  }

  isRunning() {
    return this.running;
  }

  getNextRunAt() {
    return this.nextRunAt || "";
  }

  getNextInviteAccountId() {
    return this.nextInviteAccountId || 0;
  }

  getLastInviteAccountId() {
    return this.lastInviteAccountId || 0;
  }

  _formatAccountLabel(account, fallback) {
    if (account) {
      const base = account.phone || account.user_id || String(account.id);
      const username = account.username ? `@${account.username}` : "";
      return username ? `${base} (${username})` : base;
    }
    if (fallback) return fallback;
    return "—";
  }

  async start() {
    if (this.running) return;
    this.running = true;
    await this._initMonitoring();
    this._scheduleNext();
  }

  stop() {
    this.running = false;
    if (this.timer) clearTimeout(this.timer);
    this.timer = null;
    this.nextRunAt = "";
    this.nextInviteAccountId = 0;
    this.telegram.stopTaskMonitor(this.task.id);
  }

  async _initMonitoring() {
    const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link);
    const accountRows = this.store.listTaskAccounts(this.task.id);
    const accounts = accountRows.map((row) => row.account_id);
    const monitorIds = accountRows.filter((row) => row.role_monitor).map((row) => row.account_id);
    const inviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
    const confirmIds = accountRows.filter((row) => row.role_confirm).map((row) => row.account_id);
    await this.telegram.joinGroupsForTask(this.task, competitors, accounts, {
      monitorIds,
      inviteIds,
      confirmIds
    });
    await this.telegram.startTaskMonitor(this.task, competitors, accounts, monitorIds);
  }

  _scheduleNext() {
    if (!this.running) return;
    const minMs = Number(this.task.min_interval_minutes || 5) * 60 * 1000;
    const maxMs = Number(this.task.max_interval_minutes || 10) * 60 * 1000;
    const jitter = Math.max(minMs, Math.min(maxMs, minMs + Math.random() * (maxMs - minMs)));
    this.nextRunAt = new Date(Date.now() + jitter).toISOString();
    this.timer = setTimeout(() => this._runBatch(), jitter);
  }

  async _runBatch() {
    const startedAt = dayjs().toISOString();
    const errors = [];
    const successIds = [];
    let invitedCount = 0;
    let unconfirmedCount = 0;
    this.nextRunAt = "";
    this.nextInviteAccountId = 0;
    const accountMap = new Map(
      this.store.listAccounts().map((account) => [account.id, account])
    );
    const perCycleLimit = Math.max(1, Number(this.task.max_invites_per_cycle || this.task.maxInvitesPerCycle || 20));
    this.cycleMeta = { cycleLimit: perCycleLimit, queueCount: 0, batchSize: 0 };

    try {
      await this._processConfirmQueue();
      const settings = this.store.getSettings();
      const ttlHours = Number(settings.queueTtlHours || 0);
      if (ttlHours > 0) {
        this.store.clearQueueOlderThan(this.task.id, ttlHours);
      }
      const accountRows = this.store.listTaskAccounts(this.task.id);
      const accounts = accountRows.map((row) => row.account_id);
      const explicitInviteIds = accountRows.filter((row) => row.role_invite).map((row) => row.account_id);
      const inviteLimitRows = accountRows
        .filter((row) => row.role_invite && Number(row.invite_limit || 0) > 0)
        .map((row) => ({ accountId: row.account_id, limit: Number(row.invite_limit || 0) }));
      let inviteAccounts = accounts;
      let inviteOrder = [];
      const roles = this.telegram.getTaskRoleAssignments(this.task.id);
      const hasExplicitRoles = roles && ((roles.ourIds || []).length || (roles.competitorIds || []).length);
      if (explicitInviteIds.length) {
        inviteAccounts = inviteLimitRows.map((row) => row.accountId);
        if (!inviteAccounts.length) {
          errors.push("No invite accounts (invite limit = 0)");
        }
      } else if (hasExplicitRoles) {
        inviteAccounts = (roles.ourIds || []).length ? roles.ourIds : [];
        if (!inviteAccounts.length) {
          errors.push("No invite accounts (role-based)");
        }
      } else if (this.task.separate_bot_roles || this.task.require_same_bot_in_both) {
        inviteAccounts = this.task.require_same_bot_in_both
          ? (roles.competitorIds || [])
          : (roles.ourIds || []);
        if (!inviteAccounts.length) {
          errors.push(this.task.require_same_bot_in_both ? "No invite accounts (same bot required)" : "No invite accounts (separated roles)");
        }
      } else {
        const limit = Math.max(1, Number(this.task.max_our_bots || accounts.length || 1));
        if (inviteAccounts.length > limit) {
          inviteAccounts = inviteAccounts.slice(0, limit);
        }
      }
      if (!accounts.length) {
        errors.push("No accounts assigned");
      }
      if (!this.task.multi_accounts_per_run && !inviteLimitRows.length) {
        const entry = this.telegram.pickInviteAccount(inviteAccounts, Boolean(this.task.random_accounts));
        inviteAccounts = entry ? [entry.account.id] : [];
        this.nextInviteAccountId = entry ? entry.account.id : 0;
        if (entry && entry.account) {
          const label = entry.account.phone || entry.account.username || entry.account.id;
          const poolLabel = inviteAccounts.length ? inviteAccounts.join(",") : "—";
          this.store.addAccountEvent(
            entry.account.id,
            entry.account.phone || "",
            "invite_pick",
            `выбран: ${label}; режим: ${this.task.random_accounts ? "случайный" : "по очереди"}`
          );
        }
      } else if (inviteAccounts.length) {
        this.nextInviteAccountId = inviteAccounts[0];
      }
      const totalAccounts = accounts.length;
      if (this.task.stop_on_blocked) {
        const all = this.store.listAccounts().filter((acc) => accounts.includes(acc.id));
        const blocked = all.filter((acc) => acc.status !== "ok").length;
        const percent = totalAccounts ? Math.round((blocked / totalAccounts) * 100) : 0;
        if (percent >= Number(this.task.stop_blocked_percent || 25)) {
          errors.push(`Stopped: blocked ${percent}% >= ${this.task.stop_blocked_percent}%`);
          this.store.setTaskStopReason(this.task.id, `Блокировки ${percent}% >= ${this.task.stop_blocked_percent}%`);
          this.stop();
        }
      }

      const dailyLimit = this.store.getEffectiveDailyLimit(this.task);
      const alreadyInvited = this.store.countInvitesToday(this.task.id);
      if (alreadyInvited >= dailyLimit) {
        errors.push("Daily limit reached");
        this.store.addAccountEvent(
          0,
          "",
          "invite_skipped",
          `задача ${this.task.id}: дневной лимит ${alreadyInvited}/${dailyLimit}`
        );
      } else {
        const remaining = dailyLimit - alreadyInvited;
        const perAccountSum = inviteLimitRows.reduce((acc, row) => acc + row.limit, 0);
        const perCycle = inviteLimitRows.length ? Math.min(perCycleLimit, perAccountSum) : perCycleLimit;
        const batchSize = Math.min(perCycle, remaining);
        const queueCount = this.store.getPendingCount(this.task.id);
        const pending = this.store.getPendingInvites(this.task.id, batchSize);
        this.cycleMeta = { cycleLimit: perCycle, queueCount, batchSize };
        if (inviteAccounts.length) {
          const eventAccountId = inviteAccounts[0] || 0;
          const eventAccount = accountMap.get(eventAccountId);
          const phone = eventAccount ? eventAccount.phone : "";
          this.store.addAccountEvent(
            eventAccountId,
            phone,
            "cycle_batch",
            `лимит цикла: ${perCycle}, очередь: ${queueCount}, взято: ${pending.length}`
          );
        }
        if (!inviteAccounts.length && pending.length) {
          errors.push("No available accounts under limits");
          this.store.addAccountEvent(
            0,
            "",
            "invite_skipped",
            `задача ${this.task.id}: нет аккаунтов с ролью инвайта`
          );
        }
        if (inviteLimitRows.length) {
          const slots = [];
          inviteLimitRows.forEach((row) => {
            for (let i = 0; i < row.limit; i += 1) {
              slots.push(row.accountId);
            }
          });
          for (let i = slots.length - 1; i > 0; i -= 1) {
            const j = Math.floor(Math.random() * (i + 1));
            [slots[i], slots[j]] = [slots[j], slots[i]];
          }
          inviteOrder = slots;
        }
        if (!pending.length) {
          this.store.addAccountEvent(
            0,
            "",
            "invite_skipped",
            `задача ${this.task.id}: очередь пуста`
          );
        }

        const fallbackRoute = (error, confirmed) => {
          if (confirmed === false) return "link";
          switch (error) {
            case "USER_NOT_MUTUAL_CONTACT":
              return "link";
            case "USER_PRIVACY_RESTRICTED":
              return "stories";
            case "USER_ID_INVALID":
              return "exclude";
            case "USER_NOT_PARTICIPANT":
              return "retry";
            case "USER_BANNED_IN_CHANNEL":
            case "USER_KICKED":
              return "exclude";
            case "CHAT_ADMIN_REQUIRED":
            case "PEER_FLOOD":
            case "FLOOD":
              return "retry";
            default:
              return "retry";
          }
        };
        for (const item of pending) {
          if (item.attempts >= 2 && this.task.retry_on_fail) {
            this.store.markInviteStatus(item.id, "failed");
            const usernameSuffix = item.username ? ` (@${item.username})` : "";
            this.store.addAccountEvent(
              0,
              "",
              "invite_skipped",
              `задача ${this.task.id}: превышен лимит повторов для ${item.user_id}${usernameSuffix}`
            );
            continue;
          }
          let accountsForInvite = inviteAccounts;
          let fixedInviteAccountId = 0;
          if (inviteOrder.length) {
            fixedInviteAccountId = inviteOrder.shift() || 0;
            if (fixedInviteAccountId) {
              accountsForInvite = [fixedInviteAccountId];
              const pickedAccount = accountMap.get(fixedInviteAccountId);
              const label = this._formatAccountLabel(pickedAccount, String(fixedInviteAccountId));
              this.store.addAccountEvent(
                fixedInviteAccountId,
                pickedAccount ? pickedAccount.phone || "" : "",
                "invite_pick",
                `выбран: ${label}; лимит на цикл: ${inviteLimitRows.find((row) => row.accountId === fixedInviteAccountId)?.limit || 0}`
              );
            }
          }
          if (!item.username && this.task.use_watcher_invite_no_username && item.watcher_account_id) {
            accountsForInvite = [item.watcher_account_id];
          }
          const watcherAccount = accountMap.get(item.watcher_account_id || 0);
          const result = await this.telegram.inviteUserForTask(this.task, item.user_id, accountsForInvite, {
            randomize: Boolean(this.task.random_accounts),
            userAccessHash: item.user_access_hash,
            username: item.username,
            sourceChat: item.source_chat,
            watcherAccountId: watcherAccount ? watcherAccount.id : 0,
            watcherPhone: watcherAccount ? watcherAccount.phone : ""
          });
          if (result.ok) {
            const isConfirmed = result.confirmed === true;
            if (isConfirmed) {
              invitedCount += 1;
              successIds.push(item.user_id);
            } else {
              unconfirmedCount += 1;
            }
            this.store.markInviteStatus(item.id, isConfirmed ? "invited" : "unconfirmed");
            this.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
            const inviteStatus = isConfirmed ? "success" : "unconfirmed";
            this.store.recordInvite(
              this.task.id,
              item.user_id,
              item.username,
              result.accountId,
              result.accountPhone,
              item.source_chat,
              inviteStatus,
              "",
              "",
              "invite",
              item.user_access_hash,
              watcherAccount ? watcherAccount.id : 0,
              watcherAccount ? watcherAccount.phone : "",
              result.strategy,
              result.strategyMeta,
              this.task.our_group,
              result.targetType,
              result.confirmed === true,
              result.confirmError || ""
            );
            if (result.confirmed === false) {
              this.store.addFallback(
                this.task.id,
                item.user_id,
                item.username,
                item.source_chat,
                this.task.our_group,
                "NOT_CONFIRMED",
                fallbackRoute("", false)
              );
            }
          } else if (result.error === "USER_ALREADY_PARTICIPANT") {
            this.store.markInviteStatus(item.id, "skipped");
            this.store.recordInvite(
              this.task.id,
              item.user_id,
              item.username,
              result.accountId,
              result.accountPhone,
              item.source_chat,
              "skipped",
              "",
              "USER_ALREADY_PARTICIPANT",
              "invite",
              item.user_access_hash,
              watcherAccount ? watcherAccount.id : 0,
              watcherAccount ? watcherAccount.phone : "",
              result.strategy,
              result.strategyMeta,
              this.task.our_group,
              result.targetType,
              false,
              result.error || ""
            );
          } else {
            errors.push(`${item.user_id}: ${result.error}`);
            if (this.task.retry_on_fail) {
              this.store.incrementInviteAttempt(item.id);
              this.store.markInviteStatus(item.id, "pending");
            } else {
              this.store.markInviteStatus(item.id, "failed");
            }
            this.store.addFallback(
              this.task.id,
              item.user_id,
              item.username,
              item.source_chat,
              this.task.our_group,
              result.error || "unknown",
              fallbackRoute(result.error, true)
            );
            this.store.recordInvite(
              this.task.id,
              item.user_id,
              item.username,
              result.accountId,
              result.accountPhone,
              item.source_chat,
              "failed",
              result.error || "",
              result.error || "",
              "invite",
              item.user_access_hash,
              watcherAccount ? watcherAccount.id : 0,
              watcherAccount ? watcherAccount.phone : "",
              result.strategy,
              result.strategyMeta,
              this.task.our_group,
              result.targetType,
              false,
              result.error || ""
            );
            let strategyLine = result.strategy || "—";
            let strategyDetails = "";
            if (result.strategyMeta) {
              try {
                const parsed = JSON.parse(result.strategyMeta);
                if (Array.isArray(parsed) && parsed.length) {
                  const steps = parsed
                    .map((step) => `${step.strategy}:${step.ok ? "ok" : "fail"}`)
                    .join(", ");
                  strategyLine = `${strategyLine} (${steps})`;
                  const details = parsed
                    .map((step) => `${step.strategy}: ${step.ok ? "ok" : "fail"}${step.detail ? ` (${step.detail})` : ""}`)
                    .join("\n");
                  strategyDetails = details ? `\nСтратегии (подробно):\n${details}` : "";
                }
              } catch (error) {
                // ignore parse errors
              }
            }
            const inviteAccount = accountMap.get(result.accountId);
            const accountLabel = this._formatAccountLabel(
              inviteAccount,
              result.accountPhone || (result.accountId ? String(result.accountId) : "")
            );
            const watcherLabel = this._formatAccountLabel(
              watcherAccount,
              watcherAccount ? watcherAccount.phone : ""
            );
            const detailed = [
              `Пользователь: ${item.user_id || "—"}${item.username ? ` (@${item.username})` : ""}`,
              `Ошибка: ${result.error || "unknown"}`,
              `Стратегия: ${strategyLine}`,
              `Источник: ${item.source_chat || "—"}`,
              `Цель: ${this.task.our_group || "—"}`,
              `Тип цели: ${result.targetType || "—"}`,
              `Инвайт: ${accountLabel}`,
              `Наблюдатель: ${watcherLabel || "—"}`
            ].join("\n") + (strategyDetails || "");
            this.store.addAccountEvent(
              watcherAccount ? watcherAccount.id : 0,
              watcherAccount ? watcherAccount.phone : "",
              "invite_failed",
              detailed
            );
          }
        }
        if (!pending.length) {
          errors.push("queue empty");
          if (this.task.cycle_competitors) {
            const competitors = this.store.listTaskCompetitors(this.task.id).map((row) => row.link).filter(Boolean);
            if (competitors.length > 1) {
              const nextCursor = (Number(this.task.competitor_cursor || 0) + 1) % competitors.length;
              this.store.setTaskCompetitorCursor(this.task.id, nextCursor);
              this.task.competitor_cursor = nextCursor;
              await this.telegram.stopTaskMonitor(this.task.id);
              await this.telegram.startTaskMonitor(this.task, competitors, this.store.listTaskAccounts(this.task.id).map((row) => row.account_id));
            }
          }
        }
      }
    } catch (error) {
      errors.push(error.message || String(error));
    }

    const finishedAt = dayjs().toISOString();
    this.store.addLog({
      taskId: this.task.id,
      startedAt,
      finishedAt,
      invitedCount,
      successIds,
      errors,
      meta: { cycleLimit: perCycleLimit, unconfirmedCount, ...(this.cycleMeta || {}) }
    });

    this._scheduleNext();
  }

  async _processConfirmQueue() {
    const nowIso = dayjs().toISOString();
    const dueItems = this.store.listDueConfirmQueue(this.task.id, nowIso, 50);
    if (!dueItems.length) return;
    for (const item of dueItems) {
      const result = await this.telegram.confirmUserInGroup(this.task, item.user_id, item.account_id);
      if (result && result.ok && result.confirmed === true) {
        this.store.deleteConfirmQueue(item.id);
        this.store.updateInviteConfirmation(this.task.id, item.user_id, true, "");
        this.store.addAccountEvent(
          item.account_id || 0,
          "",
          "confirm_retry_ok",
          `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`
        );
        continue;
      }
      const attempts = Number(item.attempts || 0) + 1;
      if (attempts >= Number(item.max_attempts || 2)) {
        this.store.deleteConfirmQueue(item.id);
        this.store.addAccountEvent(
          item.account_id || 0,
          "",
          "confirm_retry_failed",
          `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""} • лимит попыток`
        );
        continue;
      }
      const nextCheckAt = dayjs().add(5, "minute").toISOString();
      const errorLabel = result && result.detail ? result.detail : (result && result.error ? result.error : "USER_NOT_PARTICIPANT");
      this.store.updateConfirmQueue(item.id, {
        attempts,
        nextCheckAt,
        lastError: errorLabel
      });
      this.store.addAccountEvent(
        item.account_id || 0,
        "",
        "confirm_retry_scheduled",
        [
          `Пользователь: ${item.user_id}${item.username ? ` (@${item.username})` : ""}`,
          "Повторная проверка через 5 минут",
          `Попыток: ${attempts}/${item.max_attempts || 2}`
        ].join("\n")
      );
    }
  }
}

module.exports = { TaskRunner };
