import type { Logger } from "pino";
import { OBSERVABILITY, POLLER } from "@/core/constants.js";
import { GitHubClientImpl } from "@/github/client.js";
import { shouldIgnoreComment } from "@/poller/comment-filter.js";
import type { Metrics } from "@/metrics/types.js";
import type { IJobQueue, PrMentionJobPayload } from "@/types/index.js";
import type { RepoRegistry } from "@/repo/registry.js";
import { PollRunner } from "@/poller/poll-runner.js";

interface PollerDeps {
  repoRegistry: RepoRegistry;
  getGitHubToken: () => Promise<string>;
  queue: IJobQueue;
  metrics: Metrics;
  githubBotUser: string;
  ignoredCommentLogins: string[];
  logger: Logger;
}

type CommentItem = {
  id: number;
  body: string;
  userLogin: string | null;
  createdAt: string;
  isReviewComment: boolean;
  inReplyToId: number | null;
};

const MAX_ENQUEUE_ATTEMPTS = 3;

export class PrMentionPoller {
  private timer: ReturnType<typeof setInterval> | null = null;
  private readonly ignoredLogins: Set<string>;
  private readonly mentionTag: string;
  private readonly processedMentions = new Set<string>();
  /**
   * Count of times each comment id has been enqueued without a visible bot reply following.
   * Caps re-enqueues when the pipeline keeps failing (e.g. persistent 401 on the reply POST)
   * so we don't loop forever — after MAX_ENQUEUE_ATTEMPTS the mention is added to
   * processedMentions and logged.
   */
  private readonly enqueueAttempts = new Map<string, number>();
  private consecutiveFailures = 0;
  private readonly runner: PollRunner;

  constructor(private readonly deps: PollerDeps) {
    this.ignoredLogins = new Set(deps.ignoredCommentLogins);
    // GitHub App bots are "hu-agent[bot]" but users type "@hu-agent"
    this.mentionTag = `@${deps.githubBotUser.replace(/\[bot\]$/, "")}`;
    this.runner = new PollRunner({
      poller: "pr_mention",
      queue: deps.queue,
      metrics: deps.metrics,
      logger: deps.logger,
      contendingKinds: ["jira"],
      skipLogThrottleMs: OBSERVABILITY.SKIP_LOG_THROTTLE_MS,
    });
  }

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

    this.deps.logger.info(
      {
        githubBotUser: this.deps.githubBotUser,
        mentionTag: this.mentionTag,
        intervalMs: POLLER.PR_MENTION_INTERVAL_MS,
      },
      "pr_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.PR_MENTION_INTERVAL_MS * 2 ** this.consecutiveFailures, 5 * 60_000)
            : POLLER.PR_MENTION_INTERVAL_MS;
        this.scheduleNext(backoff);
      });
    }, delayMs);
  }

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

  async poll(): Promise<number> {
    return this.runner.run(async () => {
      let enqueued = 0;
      let listPrFailures = 0;

      try {
        const token = await this.deps.getGitHubToken();
        const repos = this.deps.repoRegistry.getAll();

        for (const repo of repos) {
          const repoLogger = this.deps.logger.child({ repo: repo.name });
          const client = new GitHubClientImpl(token, repo.url, "", repoLogger);

          let prs;
          try {
            prs = await client.listOpenPrs();
          } catch (err) {
            listPrFailures++;
            repoLogger.warn(
              { error: err instanceof Error ? err.message : String(err) },
              "pr_mention_poll_list_prs_failed",
            );
            if (listPrFailures >= 3) {
              this.deps.logger.warn(
                { listPrFailures },
                "pr_mention_poll_aborting_cycle_too_many_failures",
              );
              break;
            }
            continue;
          }

          // Track open PR numbers for memory cleanup
          const openPrKeys = new Set(prs.map((pr) => `${repo.name}#${pr.number}`));
          this.cleanProcessedMentions(repo.name, openPrKeys);

          // Skip bot-authored PRs — those are handled by PrCommentPoller
          const nonBotPrs = prs.filter((pr) => pr.author !== this.deps.githubBotUser);

          for (const pr of nonBotPrs) {
            const comments: CommentItem[] = [];

            try {
              const issueComments = await client.listIssueComments(pr.number);
              for (const c of issueComments) {
                comments.push({
                  id: c.id,
                  body: c.body ?? "",
                  userLogin: c.user?.login ?? null,
                  createdAt: c.created_at,
                  isReviewComment: false,
                  inReplyToId: null,
                });
              }
            } catch (err) {
              repoLogger.warn(
                { prNumber: pr.number, error: err instanceof Error ? err.message : String(err) },
                "pr_mention_poll_issue_comments_failed",
              );
            }

            try {
              const reviewComments = await client.listPullReviewComments(pr.number);
              for (const c of reviewComments) {
                comments.push({
                  id: c.id,
                  body: c.body,
                  userLogin: c.user?.login ?? null,
                  createdAt: c.created_at,
                  isReviewComment: true,
                  inReplyToId: c.in_reply_to_id,
                });
              }
            } catch (err) {
              repoLogger.warn(
                { prNumber: pr.number, error: err instanceof Error ? err.message : String(err) },
                "pr_mention_poll_review_comments_failed",
              );
            }

            try {
              const reviews = await client.listPullReviews(pr.number);
              for (const r of reviews) {
                if (!r.body?.trim()) continue;
                comments.push({
                  id: r.id,
                  body: r.body,
                  userLogin: r.user?.login ?? null,
                  createdAt: r.submitted_at ?? "",
                  isReviewComment: false,
                  inReplyToId: null,
                });
              }
            } catch (err) {
              repoLogger.warn(
                { prNumber: pr.number, error: err instanceof Error ? err.message : String(err) },
                "pr_mention_poll_reviews_failed",
              );
            }

            if (comments.length === 0) continue;

            // Find comments that mention the bot and haven't been processed yet
            const mentionComments = comments.filter((c) => {
              // Must contain @hu-agent
              if (!c.body.includes(this.mentionTag)) return false;

              // Skip bot's own comments and ignored logins
              if (
                shouldIgnoreComment({
                  login: c.userLogin,
                  botUser: this.deps.githubBotUser,
                  ignoredLogins: this.ignoredLogins,
                })
              )
                return false;

              // Skip already processed
              const key = `${repo.name}#${pr.number}#${c.id}`;
              if (this.processedMentions.has(key)) return false;

              return true;
            });

            // Decide if a mention was already replied to using per-thread (review)
            // or per-stream (issue / review-body) state. A global "any later bot
            // comment" check would mark mentions on file A as resolved as soon as
            // the bot replied to a different mention on file B.
            const reviewByRoot = new Map<number, CommentItem[]>();
            for (const c of comments) {
              if (!c.isReviewComment) continue;
              const root = c.inReplyToId ?? c.id;
              const arr = reviewByRoot.get(root);
              if (arr) arr.push(c);
              else reviewByRoot.set(root, [c]);
            }
            for (const arr of reviewByRoot.values()) {
              arr.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
            }

            let latestBotFlatTs = -Infinity;
            for (const c of comments) {
              if (c.isReviewComment) continue;
              if (c.userLogin !== this.deps.githubBotUser) continue;
              const ts = new Date(c.createdAt).getTime();
              if (ts > latestBotFlatTs) latestBotFlatTs = ts;
            }

            const pendingMentions = mentionComments.filter((mention) => {
              const mentionTs = new Date(mention.createdAt).getTime();
              let botRepliedAfter = false;
              if (mention.isReviewComment) {
                const root = mention.inReplyToId ?? mention.id;
                const thread = reviewByRoot.get(root) ?? [];
                botRepliedAfter = thread.some(
                  (c) =>
                    c.userLogin === this.deps.githubBotUser &&
                    new Date(c.createdAt).getTime() > mentionTs,
                );
              } else {
                botRepliedAfter = mentionTs < latestBotFlatTs;
              }
              if (botRepliedAfter) {
                const key = `${repo.name}#${pr.number}#${mention.id}`;
                this.processedMentions.add(key);
                this.enqueueAttempts.delete(key);
                return false;
              }
              return true;
            });

            if (pendingMentions.length === 0) continue;

            repoLogger.info(
              {
                prNumber: pr.number,
                pendingCount: pendingMentions.length,
                commentIds: pendingMentions.map((c) => c.id),
              },
              "pr_mention_pending_found",
            );

            for (const mention of pendingMentions) {
              const key = `${repo.name}#${pr.number}#${mention.id}`;
              const nextAttempts = (this.enqueueAttempts.get(key) ?? 0) + 1;
              if (nextAttempts > MAX_ENQUEUE_ATTEMPTS) {
                this.processedMentions.add(key);
                this.enqueueAttempts.delete(key);
                repoLogger.warn(
                  {
                    prNumber: pr.number,
                    commentId: mention.id,
                    attempts: nextAttempts - 1,
                  },
                  "pr_mention_enqueue_attempts_exhausted",
                );
                continue;
              }
              this.enqueueAttempts.set(key, nextAttempts);

              const job: PrMentionJobPayload = {
                jobKind: "pr_mention",
                repoName: repo.name,
                prNumber: pr.number,
                branch: pr.headRef,
                commentId: mention.id,
                commentBody: mention.body,
                isReviewComment: mention.isReviewComment,
                prUrl: pr.url,
                mentionAuthor: mention.userLogin ?? "unknown",
                inReplyToId: mention.inReplyToId,
              };

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

              repoLogger.info(
                {
                  prNumber: pr.number,
                  commentId: mention.id,
                  isReviewComment: mention.isReviewComment,
                  author: mention.userLogin,
                  attempts: nextAttempts,
                },
                "pr_mention_enqueued",
              );
            }
          }
        }
      } catch (err) {
        this.deps.logger.error(
          { error: err instanceof Error ? err.message : String(err) },
          "pr_mention_poll_error",
        );
      }

      if (listPrFailures >= 3) {
        this.consecutiveFailures++;
        const level =
          this.consecutiveFailures >= OBSERVABILITY.FAILURE_ESCALATION_THRESHOLD ? "error" : "warn";
        this.deps.logger[level](
          { consecutiveFailures: this.consecutiveFailures },
          "pr_mention_poll_backoff_active",
        );
      } else {
        if (this.consecutiveFailures > 0) {
          this.deps.logger.info("pr_mention_poll_backoff_cleared");
        }
        this.consecutiveFailures = 0;
      }

      return enqueued;
    });
  }

  /** Remove processed mention + attempt entries for PRs that are no longer open. */
  private cleanProcessedMentions(repoName: string, openPrKeys: Set<string>): void {
    for (const key of this.processedMentions) {
      // Key format: "repoName#prNumber#commentId"
      const prKey = key.split("#").slice(0, 2).join("#");
      if (prKey.startsWith(`${repoName}#`) && !openPrKeys.has(prKey)) {
        this.processedMentions.delete(key);
      }
    }
    for (const key of this.enqueueAttempts.keys()) {
      const prKey = key.split("#").slice(0, 2).join("#");
      if (prKey.startsWith(`${repoName}#`) && !openPrKeys.has(prKey)) {
        this.enqueueAttempts.delete(key);
      }
    }
  }
}
