import type { Logger } from "pino";
import { OBSERVABILITY, POLLER, SLACK_MENTION } from "@/core/constants.js";
import { METRIC } from "@/metrics/names.js";
import type { Metrics } from "@/metrics/types.js";
import type { SlackNotifier } from "@/slack/client.js";
import type { IJobQueue, SlackMentionJobPayload } from "@/types/index.js";

interface PollerDeps {
  slackNotifier: SlackNotifier;
  queue: IJobQueue;
  slackBotUserId: string;
  logger: Logger;
  metrics: Metrics;
}

export class SlackMentionPoller {
  private timer: ReturnType<typeof setTimeout> | null = null;
  private polling = false;
  private consecutiveFailures = 0;

  /** Cached channel list + timestamp of last refresh. */
  private cachedChannels: Array<{ id: string; name: string }> = [];
  private channelsFetchedAt = 0;

  constructor(private readonly deps: PollerDeps) {}

  start(): void {
    if (this.timer) return;

    this.deps.logger.info(
      { botUserId: this.deps.slackBotUserId, intervalMs: POLLER.SLACK_MENTION_INTERVAL_MS },
      "slack_mention_poller_started",
    );

    this.scheduleNext(0);
  }

  private scheduleNext(delayMs: number): void {
    if (this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(() => {
      void this.poll().finally(() => {
        if (!this.timer) return;
        const backoff =
          this.consecutiveFailures > 0
            ? Math.min(POLLER.SLACK_MENTION_INTERVAL_MS * 2 ** this.consecutiveFailures, 5 * 60_000)
            : POLLER.SLACK_MENTION_INTERVAL_MS;
        this.scheduleNext(backoff);
      });
    }, delayMs);
  }

  stop(): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
      this.deps.logger.info("slack_mention_poller_stopped");
    }
  }

  private async getChannels(): Promise<Array<{ id: string; name: string }>> {
    const now = Date.now();
    if (
      this.cachedChannels.length > 0 &&
      now - this.channelsFetchedAt < SLACK_MENTION.CHANNEL_REFRESH_MS
    ) {
      return this.cachedChannels;
    }

    this.cachedChannels = await this.deps.slackNotifier.listBotChannels();
    this.channelsFetchedAt = now;
    this.deps.logger.debug(
      { channelCount: this.cachedChannels.length },
      "slack_mention_channels_refreshed",
    );
    return this.cachedChannels;
  }

  /**
   * Enqueue a mention unless it's already resolved (✅ done or ❌ failed). A
   * mention left with only 👀 (claimed) is re-enqueued — it was interrupted
   * before answering. After enqueueing, claim it with 👀 (unless already
   * claimed). A reactions-API failure skips this mention safely without
   * crashing the whole poll cycle.
   * @param threadTs - parent message ts (used as thread_ts for replies)
   * @param messageTs - ts of the specific message containing the mention
   */
  private async tryEnqueue(
    channelId: string,
    threadTs: string,
    messageTs: string,
    userId: string,
    rawText: string,
    mentionTag: string,
  ): Promise<void> {
    const text = rawText.replace(new RegExp(mentionTag, "g"), "").trim();
    if (!text) return;

    let reactions: { claimed: boolean; done: boolean; failed: boolean };
    try {
      reactions = await this.deps.slackNotifier.getBotReactions(
        channelId,
        messageTs,
        this.deps.slackBotUserId,
      );
    } catch (err) {
      this.deps.logger.warn(
        { channelId, messageTs, error: err instanceof Error ? err.message : String(err) },
        "slack_mention_reactions_read_failed",
      );
      return;
    }

    if (reactions.done || reactions.failed) {
      this.deps.logger.debug({ channelId, messageTs }, "slack_mention_skipped_already_handled");
      return;
    }

    // Authoritative dedup: reactions are advisory (the ✅ write can fail after a
    // successful reply), so before enqueueing confirm the bot hasn't already
    // replied in-thread. Without this, a dropped ✅ would re-enqueue and double-reply.
    try {
      if (await this.deps.slackNotifier.hasBotRepliedAfter(channelId, threadTs, messageTs)) {
        this.deps.logger.debug({ channelId, messageTs }, "slack_mention_skipped_already_replied");
        return;
      }
    } catch (err) {
      this.deps.logger.warn(
        { channelId, messageTs, error: err instanceof Error ? err.message : String(err) },
        "slack_mention_reply_check_failed",
      );
      return;
    }

    const job: SlackMentionJobPayload = {
      jobKind: "slack_mention",
      channelId,
      threadTs,
      messageTs,
      userId,
      text,
      language: "es",
    };

    await this.deps.queue.enqueue(job);

    if (!reactions.claimed) {
      try {
        await this.deps.slackNotifier.addReaction(
          channelId,
          messageTs,
          SLACK_MENTION.REACTION_CLAIMED,
        );
      } catch (err) {
        this.deps.logger.debug(
          { channelId, messageTs, error: err instanceof Error ? err.message : String(err) },
          "slack_mention_claim_reaction_failed",
        );
      }
    }
  }

  async poll(): Promise<void> {
    if (this.polling) return;
    this.polling = true;

    try {
      const channels = await this.getChannels();
      const mentionTag = `<@${this.deps.slackBotUserId}>`;

      for (const channel of channels) {
        const messages = await this.deps.slackNotifier.fetchChannelHistory(channel.id);

        for (const msg of messages) {
          // Check the main (top-level) message for a mention (skip bot messages)
          if (!msg.bot_id && msg.text?.includes(mentionTag) && msg.user) {
            await this.tryEnqueue(channel.id, msg.ts, msg.ts, msg.user, msg.text, mentionTag);
          }

          // Scan thread replies for new mentions
          if (msg.reply_count && msg.reply_count > 0) {
            const replies = await this.deps.slackNotifier.fetchThreadReplies(channel.id, msg.ts);
            for (const reply of replies) {
              if (reply.ts === msg.ts) continue; // skip parent (already handled above)
              if (!reply.text?.includes(mentionTag) || !reply.user) continue;
              await this.tryEnqueue(
                channel.id,
                msg.ts,
                reply.ts,
                reply.user,
                reply.text,
                mentionTag,
              );
            }
          }
        }
      }

      this.consecutiveFailures = 0;
      this.deps.metrics.increment(METRIC.pollExecuted, { poller: "slack_mention" });
    } catch (err) {
      this.consecutiveFailures++;
      const message = err instanceof Error ? err.message : String(err);
      this.deps.metrics.increment(METRIC.pollError, { poller: "slack_mention" });
      if (/account_inactive|invalid_auth|not_authed|token_revoked/.test(message)) {
        this.deps.metrics.increment(METRIC.pollAuthFailure, {
          poller: "slack_mention",
          integration: "slack",
        });
      }
      const level =
        this.consecutiveFailures >= OBSERVABILITY.FAILURE_ESCALATION_THRESHOLD ? "error" : "warn";
      this.deps.logger[level](
        { error: message, failures: this.consecutiveFailures },
        "slack_mention_poll_failed",
      );
    } finally {
      this.polling = false;
    }
  }
}
