import { join } from "node:path";
import type { Logger } from "pino";
import { SLACK_MENTION } from "@/core/constants.js";
import { JobAbortedError } from "@/core/errors.js";
import { buildSlackMentionPrompt, parsePrCommentReply } from "@/agent/prompts.js";
import type { JiraClientImpl } from "@/jira/client.js";
import { RepoManagerImpl } from "@/repo/manager.js";
import type { RepoRegistry } from "@/repo/registry.js";
import type { SlackNotifier } from "@/slack/client.js";
import type { DatadogLogsClient } from "@/datadog/client.js";
import { formatDatadogLogLines } from "@/datadog/format.js";
import type { Metrics } from "@/metrics/types.js";
import { METRIC } from "@/metrics/names.js";
import type {
  AgentRunner,
  JiraIssue,
  PipelineResult,
  SlackMentionJobPayload,
  ThreadContextEntry,
} from "@/types/index.js";

const JIRA_TICKET_PATTERN = /\b[A-Z][A-Z0-9]+-\d+\b/g;

interface PipelineDeps {
  agentCli: AgentRunner;
  repoRegistry: RepoRegistry;
  getGitHubToken: () => Promise<string>;
  workdirPath: string;
  slackNotifier: SlackNotifier;
  jiraClient: JiraClientImpl;
  datadogLogsClient: DatadogLogsClient | null;
  logger: Logger;
  metrics: Metrics;
}

export class SlackMentionPipeline {
  constructor(private readonly deps: PipelineDeps) {}

  async execute(job: SlackMentionJobPayload, signal?: AbortSignal): Promise<PipelineResult> {
    const logger = this.deps.logger.child({
      channelId: job.channelId,
      messageTs: job.messageTs,
      userId: job.userId,
    });
    const __t = Date.now();

    try {
      // Ensure repos are cloned and at their base branch (read-only, no new branches).
      const repos = this.deps.repoRegistry.getAll();
      await Promise.allSettled(
        repos.map(async (repo) => {
          const localPath = join(this.deps.workdirPath, repo.name);
          const manager = new RepoManagerImpl(
            repo.url,
            localPath,
            logger.child({ repo: repo.name }),
            repo.installCmd,
          );
          await manager.ensureCloned();
          await manager.checkoutBase(repo.defaultBranch ?? "develop");
        }),
      );

      // Load thread context only when the mention is a reply inside an existing thread.
      const threadContext =
        job.messageTs !== job.threadTs ? await this.loadThreadContext(job, logger) : undefined;

      // Fetch any Jira tickets referenced in the message
      const ticketKeys = [...new Set(job.text.match(JIRA_TICKET_PATTERN) ?? [])];
      const jiraIssues: JiraIssue[] = [];
      if (ticketKeys.length > 0) {
        logger.info({ ticketKeys }, "slack_mention_fetching_jira_tickets");
        const results = await Promise.allSettled(
          ticketKeys.map((key) => this.deps.jiraClient.getIssue(key)),
        );
        for (const result of results) {
          if (result.status === "fulfilled") {
            jiraIssues.push(result.value);
          }
        }
      }

      // Fetch Datadog logs for referenced tickets
      const datadogLogsByTicket: Record<string, string[]> = {};
      if (ticketKeys.length > 0 && this.deps.datadogLogsClient) {
        logger.info({ ticketKeys }, "slack_mention_fetching_datadog_logs");
        const now = Date.now();
        const toIso = new Date(now).toISOString();
        const fromIso = new Date(now - 24 * 60 * 60 * 1000).toISOString();

        const logResults = await Promise.allSettled(
          ticketKeys.map(async (key) => {
            const result = await this.deps.datadogLogsClient!.searchLogs({
              query: `service:hu-agent (@issueKey:"${key}" OR "[${key}]")`,
              from: fromIso,
              to: toIso,
              limit: 500,
            });
            return { key, result };
          }),
        );

        for (const r of logResults) {
          if (r.status === "fulfilled") {
            const lines = formatDatadogLogLines(r.value.result.logs, { hideThinking: true });
            if (lines.length > 0) {
              datadogLogsByTicket[r.value.key] = lines;
            }
          }
        }
      }

      const prompt = buildSlackMentionPrompt(
        job.text,
        repos,
        job.language,
        jiraIssues,
        datadogLogsByTicket,
        threadContext,
      );

      logger.info({ textLength: job.text.length }, "slack_mention_agent_start");

      const __t_agent = Date.now();
      const result = await this.deps.agentCli.runAgent(
        prompt,
        this.deps.workdirPath,
        SLACK_MENTION.TIMEOUT_MS,
        { signal },
      );
      this.deps.metrics.increment(METRIC.agentInvocations, { phase: "slack_mention" });
      this.deps.metrics.timing(METRIC.agentDuration, Date.now() - __t_agent, {
        phase: "slack_mention",
      });

      const reply = parsePrCommentReply(result.output);

      await this.deps.slackNotifier.replyInThread(job.channelId, job.threadTs, reply);

      try {
        await this.deps.slackNotifier.addReaction(
          job.channelId,
          job.messageTs,
          SLACK_MENTION.REACTION_DONE,
        );
        await this.deps.slackNotifier.removeReaction(
          job.channelId,
          job.messageTs,
          SLACK_MENTION.REACTION_CLAIMED,
        );
      } catch {
        // reactions are advisory
      }

      this.deps.metrics.increment(METRIC.slackMentionHandled);
      this.deps.metrics.timing(METRIC.slackMentionDuration, Date.now() - __t);
      logger.info("slack_mention_replied");

      return {
        success: true,
        issueKey: `slack:${job.channelId}:${job.messageTs}`,
        outcome: "completed",
      };
    } catch (err) {
      if (err instanceof JobAbortedError || signal?.aborted) {
        // interrupted by shutdown: no reply; poller re-enqueues on next boot
        return {
          success: false,
          issueKey: `slack:${job.channelId}:${job.messageTs}`,
          outcome: "completed",
          abortReason: "aborted_for_shutdown",
        };
      }

      logger.error(
        { error: err instanceof Error ? err.message : String(err) },
        "slack_mention_pipeline_failed",
      );

      try {
        const errorMsg =
          job.language === "es"
            ? "Lo siento, ocurrió un error al intentar responder tu pregunta. Por favor intentá de nuevo más tarde."
            : "Sorry, I ran into an error trying to answer your question. Please try again later.";
        await this.deps.slackNotifier.replyInThread(job.channelId, job.threadTs, errorMsg);
        await this.deps.slackNotifier.addReaction(
          job.channelId,
          job.messageTs,
          SLACK_MENTION.REACTION_FAILED,
        );
      } catch {
        // best-effort error notification + reaction
      }

      return {
        success: false,
        issueKey: `slack:${job.channelId}:${job.messageTs}`,
        outcome: "completed",
        abortReason: err instanceof Error ? err.message : String(err),
      };
    }
  }

  private async loadThreadContext(
    job: SlackMentionJobPayload,
    logger: Logger,
  ): Promise<ThreadContextEntry[] | undefined> {
    try {
      const messages = await this.deps.slackNotifier.fetchThreadReplies(
        job.channelId,
        job.threadTs,
        { includeBots: true },
      );

      // Drop the message that triggered the mention — that text is rendered as ## Question.
      // Keep everything else, oldest first.
      const filtered = messages
        .filter((m) => m.ts !== job.messageTs)
        .sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));

      if (filtered.length === 0) return undefined;

      // Resolve unique non-bot user IDs to display names.
      const userIds = Array.from(
        new Set(filtered.filter((m) => !m.bot_id && m.user).map((m) => m.user as string)),
      );
      const nameMap = await this.deps.slackNotifier.resolveUsers(userIds);

      // Build entries.
      const entries: ThreadContextEntry[] = [];
      for (const m of filtered) {
        const text = (m.text ?? "").trim();
        if (!text) continue;
        const isBot = !!m.bot_id;
        const author = isBot ? "[hu-agent]" : (nameMap.get(m.user ?? "") ?? m.user ?? "unknown");
        entries.push({ author, text, ts: m.ts, isBot });
      }

      return truncateThreadContext(entries);
    } catch (err) {
      logger.warn(
        { error: err instanceof Error ? err.message : String(err) },
        "slack_mention_thread_context_failed",
      );
      return undefined;
    }
  }
}

function truncateThreadContext(entries: ThreadContextEntry[]): ThreadContextEntry[] {
  const totalChars = (xs: ThreadContextEntry[]) =>
    xs.reduce((sum, e) => sum + e.author.length + e.text.length, 0);

  if (entries.length === 0) return entries;
  if (totalChars(entries) <= SLACK_MENTION.THREAD_CONTEXT_MAX_CHARS) return entries;

  // Keep the parent (entries[0]) always; drop the second-oldest first
  // (preserve recent context plus the original topic).
  const parent = entries[0]!;
  const tail = entries.slice(1);

  while (
    tail.length > 0 &&
    totalChars([parent, ...tail]) > SLACK_MENTION.THREAD_CONTEXT_MAX_CHARS
  ) {
    tail.shift(); // remove the oldest non-parent entry
  }

  return [parent, ...tail];
}
