import type { Logger } from "pino";
import { JIRA_RECOVERY, OBSERVABILITY, POLLER } from "@/core/constants.js";
import { METRIC } from "@/metrics/names.js";
import type { Metrics } from "@/metrics/types.js";
import type { JiraClientImpl } from "@/jira/client.js";
import type { GitHubClientImpl } from "@/github/client.js";
import type { RepoRegistry } from "@/repo/registry.js";
import type { IJobQueue } from "@/types/index.js";
import { buildBranchPrefix } from "@/utils/slug.js";
import { PollRunner } from "@/poller/poll-runner.js";

interface PollerDeps {
  jiraClient: JiraClientImpl;
  queue: IJobQueue;
  metrics: Metrics;
  botEmail: string;
  /** Project keys to restrict polling to (e.g. ["SQDP", "SQZB"]). Empty = all projects. */
  squadScope: string[];
  getGitHubToken: () => Promise<string>;
  repoRegistry: RepoRegistry;
  buildGitHubClient: (token: string, repoUrl: string, logger: Logger) => GitHubClientImpl;
  logger: Logger;
}

export class JiraPoller {
  private timer: ReturnType<typeof setInterval> | null = null;
  private botAccountId: string | null = null;
  private readonly runner: PollRunner;

  constructor(private readonly deps: PollerDeps) {
    this.runner = new PollRunner({
      poller: "jira",
      queue: deps.queue,
      metrics: deps.metrics,
      logger: deps.logger,
      contendingKinds: ["pr_comment", "jira_ticket_review"],
      skipLogThrottleMs: OBSERVABILITY.SKIP_LOG_THROTTLE_MS,
    });
  }

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

    this.deps.logger.info(
      { botEmail: this.deps.botEmail, intervalMs: POLLER.INTERVAL_MS },
      "poller_started",
    );

    this.poll();
    this.timer = setInterval(() => this.poll(), POLLER.INTERVAL_MS);
  }

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

  async poll(): Promise<number> {
    return this.runner.run(async () => {
      if (!this.botAccountId) {
        const accountId = await this.deps.jiraClient.resolveAccountIdForBotEmail(
          this.deps.botEmail,
        );
        if (!accountId) {
          this.deps.metrics.increment(METRIC.pollAuthFailure, {
            poller: "jira",
            integration: "jira",
          });
          this.deps.logger.error(
            { botEmail: this.deps.botEmail },
            "Could not resolve bot email to Jira accountId, skipping poll",
          );
          return 0;
        }
        this.botAccountId = accountId;
        this.deps.logger.info(
          { botEmail: this.deps.botEmail, accountId: this.botAccountId },
          "bot_account_resolved_from_email",
        );
      }

      const projectFilter =
        this.deps.squadScope.length > 0
          ? `project in (${this.deps.squadScope.join(", ")}) AND `
          : "";
      const jql = `${projectFilter}assignee = "${this.botAccountId}" AND status IN ("To Do", "In Progress") ORDER BY created DESC`;

      let result: Awaited<ReturnType<JiraClientImpl["searchIssues"]>>;
      try {
        result = await this.deps.jiraClient.searchIssues(jql);
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (/\b401\b|\b403\b/.test(msg)) {
          this.botAccountId = null;
          this.deps.metrics.increment(METRIC.pollAuthFailure, {
            poller: "jira",
            integration: "jira",
          });
        }
        throw err;
      }

      let enqueued = 0;
      for (const issue of result.issues) {
        const hasChildren = issue.children.length > 0 || issue.subtasks.length > 0;
        if (hasChildren) {
          this.deps.logger.debug(
            {
              issueKey: issue.key,
              subtasks: issue.subtasks.length,
              children: issue.children.length,
            },
            "poll_skipped_has_children",
          );
          continue;
        }

        if (issue.status === "In Progress") {
          const updatedMs = Date.parse(issue.updated ?? issue.created ?? "");
          if (
            Number.isFinite(updatedMs) &&
            Date.now() - updatedMs < JIRA_RECOVERY.ORPHAN_MIN_AGE_MS
          ) {
            this.deps.logger.debug({ issueKey: issue.key }, "poll_skipped_in_progress_too_recent");
            continue;
          }
          if (await this.hasOpenPrForIssue(issue.key)) {
            this.deps.logger.debug({ issueKey: issue.key }, "poll_skipped_in_progress_has_pr");
            continue;
          }
          this.deps.logger.info({ issueKey: issue.key }, "reenqueue_orphaned_in_progress");
        }

        await this.deps.queue.enqueue({
          jobKind: "jira",
          issueId: issue.id,
          issueKey: issue.key,
          summary: issue.summary,
          description: issue.description ?? "",
        });
        enqueued++;
      }

      return enqueued;
    });
  }

  /**
   * Fail-safe open-PR check for a deterministic fix branch. Returns true (= "has PR",
   * so the orphaned issue is NOT re-enqueued) whenever we cannot confirm the absence of a
   * PR — token fetch failure or any GitHub error — so we never create a duplicate.
   */
  private async hasOpenPrForIssue(issueKey: string): Promise<boolean> {
    // Match by the issue-key branch PREFIX, not a summary-derived exact branch:
    // a multi-repo fix pushes a subtask's PR on `buildBranchName(subtaskKey, parentSummary)`,
    // whereas the subtask's own Jira summary carries a repo prefix — so reconstructing the
    // full branch from `issue.summary` would diverge and miss the PR. Every fix branch for
    // this issue starts with `fix/<key>-`, so a prefix match is summary-independent.
    const branchPrefix = buildBranchPrefix(issueKey);
    let token: string;
    try {
      token = await this.deps.getGitHubToken();
    } catch (err) {
      this.deps.logger.warn(
        { issueKey, error: err instanceof Error ? err.message : String(err) },
        "orphan_pr_check_token_fetch_failed",
      );
      return true;
    }
    for (const repo of this.deps.repoRegistry.getAll()) {
      try {
        const client = this.deps.buildGitHubClient(token, repo.url, this.deps.logger);
        const openPrs = await client.listOpenPrs();
        if (openPrs.some((pr) => pr.headRef.startsWith(branchPrefix))) return true;
      } catch (err) {
        this.deps.logger.warn(
          { repo: repo.name, error: err instanceof Error ? err.message : String(err) },
          "orphan_pr_check_failed",
        );
        return true;
      }
    }
    return false;
  }
}
