import type { Logger } from "pino";
import { OBSERVABILITY, POLLER } from "@/core/constants.js";
import { GitHubClientImpl } from "@/github/client.js";
import { clearClosedPrMemories } from "@/pr-memory/store.js";
import { shouldIgnoreComment } from "@/poller/comment-filter.js";
import type { Metrics } from "@/metrics/types.js";
import type { IJobQueue, PrCommentJobPayload } 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[];
  squadScope: string[];
  logger: Logger;
}

type CommentItem = {
  id: number;
  body: string;
  userLogin: string | null;
  createdAt: string;
  isReviewComment: boolean;
  /** For review comments only: null means thread root, a number means reply to that root. */
  inReplyToId: number | null;
};

export class PrCommentPoller {
  private timer: ReturnType<typeof setInterval> | null = null;
  private readonly ignoredLogins: Set<string>;
  private consecutiveFailures = 0;
  private readonly runner: PollRunner;

  constructor(private readonly deps: PollerDeps) {
    this.ignoredLogins = new Set(deps.ignoredCommentLogins);
    this.runner = new PollRunner({
      poller: "pr_comment",
      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, intervalMs: POLLER.PR_COMMENT_INTERVAL_MS },
      "pr_comment_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_COMMENT_INTERVAL_MS * 2 ** this.consecutiveFailures, 5 * 60_000)
            : POLLER.PR_COMMENT_INTERVAL_MS;
        this.scheduleNext(backoff);
      });
    }, delayMs);
  }

  stop(): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
      this.deps.logger.info("pr_comment_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_comment_poll_list_prs_failed",
            );
            if (listPrFailures >= 3) {
              this.deps.logger.warn(
                { listPrFailures },
                "pr_comment_poll_aborting_cycle_too_many_failures",
              );
              break;
            }
            continue;
          }

          const cleared = clearClosedPrMemories(
            repo.name,
            prs.map((pr) => pr.number),
          );
          if (cleared > 0) {
            repoLogger.info({ cleared }, "pr_comment_memory_cleaned");
          }

          const botPrs = prs.filter(
            (pr) =>
              pr.author === this.deps.githubBotUser &&
              (this.deps.squadScope.length === 0 ||
                this.deps.squadScope.some((key) =>
                  pr.headRef.toUpperCase().includes(key.toUpperCase()),
                )),
          );

          for (const pr of botPrs) {
            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_comment_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_comment_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_comment_poll_reviews_failed",
              );
            }

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

            const isBotOrIgnored = (login: string | null): boolean =>
              shouldIgnoreComment({
                login,
                botUser: this.deps.githubBotUser,
                ignoredLogins: this.ignoredLogins,
              });

            const pendingComments: CommentItem[] = [];

            // --- Review comment threads: check per-thread (multi-turn) ---
            // A thread is pending when its most recent message is from a human.
            // This supports follow-up replies: after the bot responds, a new human
            // message in the same thread makes it pending again.
            const reviewComments = comments.filter((c) => c.isReviewComment);
            const threads = new Map<number, CommentItem[]>();
            for (const c of reviewComments) {
              const rootId = c.inReplyToId ?? c.id;
              const thread = threads.get(rootId);
              if (thread) thread.push(c);
              else threads.set(rootId, [c]);
            }

            for (const thread of threads.values()) {
              thread.sort(
                (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
              );
              const last = thread[thread.length - 1]!;
              if (isBotOrIgnored(last.userLogin)) continue;
              pendingComments.push(last);
            }

            // --- Issue comments & review bodies: timestamp-based ---
            const flatComments = comments.filter((c) => !c.isReviewComment);
            const latestBotFlat = flatComments
              .filter((c) => isBotOrIgnored(c.userLogin))
              .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
            const latestBotFlatTs = latestBotFlat
              ? new Date(latestBotFlat.createdAt).getTime()
              : -Infinity;

            for (const c of flatComments) {
              if (isBotOrIgnored(c.userLogin)) continue;
              if (new Date(c.createdAt).getTime() <= latestBotFlatTs) continue;
              pendingComments.push(c);
            }

            pendingComments.sort(
              (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
            );

            if (pendingComments.length === 0) {
              repoLogger.debug({ prNumber: pr.number }, "pr_comment_already_replied_skipping");
              continue;
            }

            repoLogger.info(
              {
                prNumber: pr.number,
                pendingCount: pendingComments.length,
                commentIds: pendingComments.map((c) => c.id),
                pendingReviewThreads: pendingComments.filter((c) => c.isReviewComment).length,
                pendingIssueComments: pendingComments.filter((c) => !c.isReviewComment).length,
              },
              "pr_comment_pending_found",
            );

            for (const pending of pendingComments) {
              const job: PrCommentJobPayload = {
                jobKind: "pr_comment",
                repoName: repo.name,
                prNumber: pr.number,
                branch: pr.headRef,
                commentId: pending.id,
                commentBody: pending.body,
                isReviewComment: pending.isReviewComment,
                prUrl: pr.url,
                inReplyToId: pending.inReplyToId,
              };

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

              repoLogger.info(
                {
                  prNumber: pr.number,
                  commentId: pending.id,
                  isReviewComment: pending.isReviewComment,
                  author: pending.userLogin,
                },
                "pr_comment_enqueued",
              );
            }
          }
        }
      } catch (err) {
        this.deps.logger.error(
          { error: err instanceof Error ? err.message : String(err) },
          "pr_comment_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_comment_poll_backoff_active",
        );
      } else {
        if (this.consecutiveFailures > 0) {
          this.deps.logger.info("pr_comment_poll_backoff_cleared");
        }
        this.consecutiveFailures = 0;
      }

      return enqueued;
    });
  }
}
