import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type {
  JiraIssue,
  JiraComment,
  TriageResult,
  RepoContext,
  RepoConfig,
  ThreadContextEntry,
} from "@/types/index.js";
import { ATTACHMENTS, CARD_READINESS_PROMPT, TRIAGE_PROMPT } from "@/core/constants.js";
import { stripCodeFenceArtifacts } from "@/utils/readiness-display.js";
import {
  sanitizeJiraAttachmentFilename,
  selectJiraIssueImageAttachments,
} from "@/utils/jira-attachments.js";
import type { ProcessedVideo } from "@/utils/video-frames.js";

let cachedGeneralRules: string | null = null;

function loadGeneralRules(): string {
  if (cachedGeneralRules) return cachedGeneralRules;
  cachedGeneralRules = readFileSync(resolve(process.cwd(), "rules", "general.mdc"), "utf-8").trim();
  return cachedGeneralRules;
}

/**
 * Parent review comment being replied to. Surfaced as primary context so short/vague
 * replies like "fix this" or "follow X feedback" can be resolved to the right thread
 * without the agent guessing.
 */
export interface ParentThreadComment {
  author: string;
  body: string;
  path?: string | null;
  line?: number | null;
  createdAt?: string | null;
}

const PARENT_THREAD_BODY_LIMIT = 2000;

function renderParentThreadBlock(parent: ParentThreadComment): string[] {
  const lines: string[] = [
    "",
    "## Thread you are replying to (primary context)",
    'This is the review-comment thread the user just replied to. Treat it as the **primary target** of the request below — when the request is short, vague, or references the reviewer by name (e.g. "do it", "fix this", "follow X feedback"), act on this comment\'s content.',
    "",
    `**Author:** ${parent.author}`,
  ];
  if (parent.path) {
    lines.push(
      parent.line != null
        ? `**Location:** ${parent.path}:${parent.line}`
        : `**File:** ${parent.path}`,
    );
  }
  const body = parent.body.trim().slice(0, PARENT_THREAD_BODY_LIMIT);
  lines.push("", "> " + body.replace(/\n/g, "\n> "));
  return lines;
}

type TicketContextVariant = "bug" | "readiness";

function buildTicketContext(
  issue: JiraIssue,
  variant: TicketContextVariant = "bug",
  imageDir?: string,
  processedVideos?: ProcessedVideo[],
): string[] {
  const lines: string[] = [];

  lines.push(variant === "bug" ? "## Bug Report" : "## Issue");
  lines.push(`**Key:** ${issue.key}`);
  lines.push(`**Summary:** ${issue.summary}`);
  if (variant === "readiness") {
    lines.push(`**Type:** ${issue.issueType}`);
  }
  lines.push("");
  lines.push("**Description:**");
  lines.push(issue.description || "No description provided.");

  if (issue.parent) {
    lines.push("", "## Parent Ticket");
    lines.push(`**Key:** ${issue.parent.key}`);
    lines.push(`**Summary:** ${issue.parent.summary}`);
    lines.push(`**Type:** ${issue.parent.issueType}`);
    if (issue.parent.description) {
      lines.push("", "**Parent Description:**", issue.parent.description);
    }
  }

  if (issue.flow) {
    lines.push("", `**Flow:** ${issue.flow.charAt(0).toUpperCase() + issue.flow.slice(1)}`);
  }

  if (issue.acceptanceCriteria) {
    lines.push("", "## Acceptance Criteria", issue.acceptanceCriteria);
  }

  if (issue.stacktrace) {
    lines.push("", "## Stacktrace", "```", issue.stacktrace, "```");
  }

  const maxIssueImages =
    variant === "readiness" ? ATTACHMENTS.MAX_JIRA_ISSUE_IMAGES : ATTACHMENTS.MAX_IMAGES;
  const images = selectJiraIssueImageAttachments(
    issue.attachments,
    issue.parent?.attachments ?? [],
    maxIssueImages,
  );

  if (images.length > 0) {
    const assetsDir = imageDir ? `${imageDir}/assets` : ".hu-bot";
    lines.push("", `## Attachments (saved in ${assetsDir}/ directory)`);
    for (const img of images) {
      const safe = sanitizeJiraAttachmentFilename(img.filename);
      lines.push(`- ${assetsDir}/${safe} (${img.mimeType}, ${Math.round(img.size / 1024)}KB)`);
    }
    lines.push("", `You can view these images in the ${assetsDir}/ directory for visual context.`);
    if (variant === "readiness") {
      lines.push(
        "",
        "These paths are relative to the agent working directory (repository root). The agent receives this prompt as **text only** — images are not inlined. You **must** open each file with your read/image tools and use what you see before judging visual detail, screenshot-to-case mapping, or claiming screenshots are missing.",
        "Do not state that screenshots are unavailable locally unless you attempted to read these paths and the reads failed.",
      );
    }
  }

  if (processedVideos && processedVideos.length > 0) {
    lines.push("", "## Video Attachments (frames pre-extracted to disk)");
    lines.push(
      "Each video attachment was sampled at scene changes; frames were saved as JPEGs into the directory listed below.",
      "The agent receives this prompt as **text only** — videos themselves are not included. You must read the JPEG frames from disk to see them.",
      "",
      "Frame filenames encode their source-video timestamp, e.g. `frame_0003_at_3.2s.jpg` is the third selected frame at 3.2s in the original video. Treat the frames as an ordered sequence: events happen *between* adjacent frames, and the gap between timestamps tells you how long the user spent in each state (≈0.5s = animation; several seconds = user reading or stuck).",
    );
    lines.push("", "**Videos for this ticket:**");
    for (const video of processedVideos) {
      const duration = video.durationSec.toFixed(1);
      lines.push(
        `- ${video.relativeDir}/  (originally "${video.originalFilename}", ${duration}s, ${video.frames.length} frames)`,
      );
      const previewCount = Math.min(video.frames.length, 6);
      const preview = video.frames
        .slice(0, previewCount)
        .map((f) => f.filename)
        .join(", ");
      const suffix = video.frames.length > previewCount ? ", …" : "";
      lines.push(`  Frames: ${preview}${suffix}`);
    }
    lines.push(
      "",
      "You **must** open these frames before commenting on what the user did or saw in the recording.",
      "Do not claim the video is unavailable unless your read calls fail.",
    );
  }

  if (issue.comments.length > 0) {
    lines.push("", "## Comments");
    lines.push(
      variant === "bug"
        ? "Previous conversation on this ticket (may include bot assessments from earlier triage attempts and human replies with additional context):"
        : "Discussion on this ticket (humans and bots; use for prior decisions and open questions):",
    );
    lines.push("");
    for (const comment of issue.comments) {
      lines.push(...formatComment(comment));
    }
  }

  return lines;
}

function formatComment(comment: JiraComment): string[] {
  const date = comment.created.split("T")[0] ?? comment.created;
  return [
    `> **${comment.author.displayName}** (${date}):`,
    `> ${comment.body.replace(/\n/g, "\n> ")}`,
    "",
  ];
}

type RepoSectionFlavor = "triage" | "readiness";

function buildRepoSection(repos: RepoContext[], flavor: RepoSectionFlavor = "triage"): string[] {
  if (repos.length === 0) return [];
  const lines: string[] = [];

  lines.push("", "## Available Repositories");
  lines.push("Each repository is a subdirectory in the current working directory.");
  if (flavor === "triage") {
    lines.push("Determine which repository contains the bug and make the fix there.");
    lines.push("You may fix multiple repositories if the bug spans frontend and backend.");
  } else {
    lines.push(
      "Use these repos to judge scope and where work would likely land. Do not modify any files — read-only assessment.",
    );
    lines.push(
      "Identify which repository(ies) are most relevant; the change may span more than one repo.",
    );
  }
  lines.push("");

  for (const repo of repos) {
    lines.push(`### ${repo.name}/`);
    lines.push(repo.description);
    if (repo.keywords.length > 0) {
      lines.push(`**Keywords:** ${repo.keywords.join(", ")}`);
    }
    if (repo.checkScripts.length > 0) {
      lines.push(
        flavor === "triage"
          ? `**Pre-commit checks:** Before finishing changes in this repo, run: ${repo.checkScripts.map((s) => `\`npm run ${s}\``).join(", ")}. Fix any errors before considering your work done.`
          : `**Typical checks in this repo:** ${repo.checkScripts.map((s) => `\`npm run ${s}\``).join(", ")} (for estimating implementation surface).`,
      );
    }
    if (repo.rules) {
      lines.push("", "**Rules:**", repo.rules);
    }
    lines.push("");
  }

  return lines;
}

export function buildFixPrompt(
  issue: JiraIssue,
  repos: RepoContext[],
  triageSummary?: string,
  imageDir?: string,
  processedVideos?: ProcessedVideo[],
): string {
  const generalRules = loadGeneralRules();
  const lines: string[] = [generalRules, ""];

  if (triageSummary) {
    lines.push("## Prior Triage Analysis");
    lines.push(
      "A triage agent already analyzed this ticket and explored the codebase. **You MUST trust this analysis and start implementing immediately.** Do NOT re-explore or re-read files that the triage already summarized — that wastes your limited budget. Open files only when you need to edit them or see details the triage didn't cover.",
    );
    lines.push("");
    lines.push(triageSummary);
    lines.push("");
  }

  lines.push(...buildTicketContext(issue, "bug", imageDir, processedVideos));
  lines.push(...buildRepoSection(repos, "triage"));

  return lines.join("\n");
}

export function buildTriagePrompt(
  issue: JiraIssue,
  repos: RepoContext[],
  imageDir?: string,
  processedVideos?: ProcessedVideo[],
): string {
  const lines: string[] = [
    "# Triage Assessment",
    "",
    "You are a senior engineer performing a **triage assessment** of a bug ticket.",
    "Your goal is to evaluate whether this ticket has enough information to attempt a fix.",
    "",
    "## Your Task",
    "",
    ...TRIAGE_PROMPT.TASK,
    "",
    "## DO NOT modify any files. This is a read-only assessment.",
    "",
    "## Evaluation Criteria",
    "",
    "Answer PROCEED if ALL of the following are true:",
    ...TRIAGE_PROMPT.PROCEED_CRITERIA.map((c) => `- ${c}`),
    "",
    "Answer NEEDS_INFO if ANY of the following are true:",
    ...TRIAGE_PROMPT.NEEDS_INFO_CRITERIA.map((c) => `- ${c}`),
    "",
    "## Required Response Format",
    "",
    "You MUST end your response with exactly this structure:",
    "",
    "```",
    ...TRIAGE_PROMPT.PROCEED_FORMAT,
    "```",
    "",
    "or",
    "",
    "```",
    ...TRIAGE_PROMPT.NEEDS_INFO_FORMAT,
    "```",
  ];

  lines.push(...buildTicketContext(issue, "bug", imageDir, processedVideos));
  lines.push(...buildRepoSection(repos, "triage"));

  return lines.join("\n");
}

/**
 * Implementation readiness for any Jira issue type (features, bugs, chores, etc.).
 * Used by the ticket-review pipeline; same VERDICT/SUMMARY/QUESTIONS shape as triage for parsing.
 */
export function buildCardReadinessPrompt(
  issue: JiraIssue,
  repos: RepoContext[],
  imageDir?: string,
  processedVideos?: ProcessedVideo[],
): string {
  const lines: string[] = [
    "# Implementation readiness",
    "",
    "You are a senior engineer reviewing whether this **Jira card** is specific and complete enough to be implemented fully by a developer or a coding agent (clear scope, testable outcomes, and enough detail on flows and edge cases).",
    "The issue may be a feature, bug, technical task, or any other type — judge readiness to implement, not only “bugginess”.",
    "",
    "## Your Task",
    "",
    ...CARD_READINESS_PROMPT.TASK,
    "",
    "## DO NOT modify any files. This is a read-only assessment.",
    "",
    "## Evaluation Criteria",
    "",
    "Answer PROCEED if ALL of the following are true:",
    ...CARD_READINESS_PROMPT.PROCEED_CRITERIA.map((c) => `- ${c}`),
    "",
    "Answer NEEDS_INFO if ANY of the following are true:",
    ...CARD_READINESS_PROMPT.NEEDS_INFO_CRITERIA.map((c) => `- ${c}`),
    "",
    "## Required Response Format",
    "",
    "You MUST end your response with exactly this structure:",
    "",
    "```",
    ...CARD_READINESS_PROMPT.PROCEED_FORMAT,
    "```",
    "",
    "or",
    "",
    "```",
    ...CARD_READINESS_PROMPT.NEEDS_INFO_FORMAT,
    "```",
  ];

  lines.push(...buildTicketContext(issue, "readiness", imageDir, processedVideos));
  lines.push(...buildRepoSection(repos, "readiness"));

  return lines.join("\n");
}

export function buildFollowupPrompt(repoName: string, buildOutput: string): string {
  return [
    `The previous fix in ${repoName}/ caused a build/test failure.`,
    "",
    "## Error Output",
    "```",
    buildOutput.slice(0, 8000),
    "```",
    "",
    "## Instructions",
    "",
    ...TRIAGE_PROMPT.FOLLOWUP_INSTRUCTIONS,
  ].join("\n");
}

/**
 * Build prompt for responding to a PR comment (or review comment).
 * The agent should analyze the comment, optionally apply code changes, and produce a reply.
 */
export function buildPrCommentPrompt(
  commentBody: string,
  repoName: string,
  branch: string,
  prDiff?: string,
  repoRules?: string | null,
  conversationHistory?: string | null,
  savedImages?: string[],
  parentThreadComment?: ParentThreadComment | null,
): string {
  const generalRules = loadGeneralRules();
  const lines: string[] = [generalRules, ""];

  lines.push(
    "# PR comment response",
    "",
    "You are responding to a comment on a pull request in this repository.",
    `**Repository:** ${repoName}`,
    `**Branch:** ${branch}`,
  );

  if (repoRules) {
    lines.push("", "## Repository Rules", repoRules);
  }

  if (parentThreadComment) {
    lines.push(...renderParentThreadBlock(parentThreadComment));
  }

  if (conversationHistory) {
    lines.push("", "## Previous conversation on this PR", conversationHistory);
  }

  lines.push(
    "",
    "## Comment to respond to",
    "```",
    commentBody.slice(0, 4000),
    "```",
    "",
    "## Your task",
    "1. Understand what the commenter is asking or suggesting.",
    "2. Analyze the codebase if needed (you are in the repo at the PR branch).",
    "3. If the comment requests a code change, make the change.",
    "4. Write a concise reply to the comment (explain what you did or answer their question).",
    "",
    "Put your **reply text** in a block that starts with REPLY_START and ends with REPLY_END, so it can be posted back to the PR. Example:",
    "REPLY_START",
    "Your response here.",
    "REPLY_END",
    "",
    "If you make code changes, they will be committed and pushed after you finish.",
  );

  if (savedImages && savedImages.length > 0) {
    lines.push("", "## Attachments (saved in .hu-bot/ directory)");
    for (const img of savedImages) {
      lines.push(`- ${img}`);
    }
    lines.push(
      "",
      "View these images in the .hu-bot/ directory for visual context from the comment.",
    );
  }

  if (prDiff && prDiff.length > 0) {
    lines.push("", "## PR diff (for context)", "```diff", prDiff.slice(0, 12000), "```");
  }

  return lines.join("\n");
}

/**
 * Build prompt for responding to an @hu-agent mention on someone else's PR.
 * The agent should analyze the request, optionally apply code changes, and produce a reply.
 */
export function buildPrMentionPrompt(
  mentionBody: string,
  repoName: string,
  branch: string,
  mentionAuthor: string,
  prDiff?: string,
  repoRules?: string | null,
  conversationHistory?: string | null,
  savedImages?: string[],
  parentThreadComment?: ParentThreadComment | null,
): string {
  const generalRules = loadGeneralRules();
  const lines: string[] = [generalRules, ""];

  // Strip @hu-agent tag from the comment body
  const cleanBody = mentionBody.replace(/@hu-agent\b/gi, "").trim();

  lines.push(
    "# @hu-agent mention — PR assistance",
    "",
    "You are responding to an @hu-agent mention on someone else's pull request.",
    "You are a **helper**, NOT the PR author. Be respectful of the author's code style and approach.",
    "",
    `**Repository:** ${repoName}`,
    `**Branch:** ${branch}`,
    `**Requested by:** ${mentionAuthor}`,
  );

  if (repoRules) {
    lines.push("", "## Repository Rules", repoRules);
  }

  if (parentThreadComment) {
    lines.push(...renderParentThreadBlock(parentThreadComment));
  }

  if (conversationHistory) {
    lines.push(
      "",
      "## Previous conversation on this PR (CONTEXT ONLY — NOT A TASK LIST)",
      "The messages below are provided so you understand the state of the PR. They are **not** tasks for you to resolve.",
      "Other reviewers' comments, prior bot interactions, and unrelated threads must be treated as read-only context.",
      "Do **not** address, fix, or respond to any of them unless the current request (below) explicitly names them or the request is a reply inside a review thread whose parent comment is shown above.",
      "",
      conversationHistory,
    );
  }

  lines.push(
    "",
    "## Request (the ONLY thing you are asked to do)",
    "```",
    cleanBody.slice(0, 4000),
    "```",
    "",
    "## Your task",
    "1. Do **only** what the request above literally asks. The request is the single source of truth for scope.",
    '2. If the mention is a reply inside a review thread (see "Thread you are replying to" above, when present), treat that thread\'s parent comment as the primary target of the request — even if the request body is short or vague (e.g. "do it", "fix this", "follow X feedback"). Otherwise ignore other reviewer/bot comments visible in the history unless the request explicitly references them (e.g. "fix the locale one", "address the typography comment").',
    '3. Do not infer additional work from the PR diff, from prior reviewer comments, or from your own opinions about code quality. No drive-by fixes, no "while I\'m here" improvements.',
    "4. Analyze the codebase only as much as needed to satisfy the request (you are in the repo at the PR branch).",
    "5. If the request is a question or is too vague/ambiguous to act on safely **and** there is no parent review thread to resolve the ambiguity, do **not** modify any files — reply asking for clarification or answering the question, and stop there.",
    "6. If the request explicitly asks for a code change, make the smallest change that satisfies it. No refactors, no style changes, no extras.",
    "7. Write a concise reply to the comment (explain what you did or answer their question). The reply should describe only what you actually did for the specific request.",
    "",
    "Put your **reply text** in a block that starts with REPLY_START and ends with REPLY_END, so it can be posted back to the PR. Example:",
    "REPLY_START",
    "Your response here.",
    "REPLY_END",
    "",
    "If you make code changes, they will be committed and pushed to the PR branch after you finish.",
  );

  if (savedImages && savedImages.length > 0) {
    lines.push("", "## Attachments (saved in .hu-bot/ directory)");
    for (const img of savedImages) {
      lines.push(`- ${img}`);
    }
    lines.push(
      "",
      "View these images in the .hu-bot/ directory for visual context from the comment.",
    );
  }

  if (prDiff && prDiff.length > 0) {
    lines.push("", "## PR diff (for context)", "```diff", prDiff.slice(0, 12000), "```");
  }

  return lines.join("\n");
}

/**
 * Extract the reply text from agent output between REPLY_START and REPLY_END.
 * Falls back to a truncated version of the full output if markers are missing.
 */
export function parsePrCommentReply(raw: string): string {
  const match = raw.match(/REPLY_START\s*([\s\S]*?)REPLY_END/);
  if (match) return match[1]!.trim();
  return raw.trim().slice(0, 4000);
}

/**
 * Extract the PR title from agent output between PR_TITLE_START and PR_TITLE_END.
 * Returns null if markers are not found (caller should fall back to template).
 */
export function parsePrTitle(raw: string): string | null {
  const match = raw.match(/PR_TITLE_START\s*([\s\S]*?)\s*PR_TITLE_END/);
  if (!match) return null;
  const title = match[1]!.trim();
  return title.length > 0 ? title : null;
}

/**
 * Extract the PR body from agent output between PR_BODY_START and PR_BODY_END.
 * Returns null if markers are not found (caller should fall back to buildPrBody).
 */
export function parsePrBody(raw: string): string | null {
  const match = raw.match(/PR_BODY_START\s*([\s\S]*?)\s*PR_BODY_END/);
  if (!match) return null;
  const body = match[1]!.trim();
  return body.length > 0 ? body : null;
}

export interface ParseVerdictStructuredOptions {
  /**
   * When the `VERDICT:` line is missing or does not match PROCEED/NEEDS_INFO.
   * Bug triage uses PROCEED (optimistic fix path); card readiness uses NEEDS_INFO (do not claim "ready").
   */
  defaultVerdictIfMissing?: "PROCEED" | "NEEDS_INFO";
}

/**
 * Parse VERDICT / SUMMARY / QUESTIONS blocks from triage or card-readiness agent output.
 * Defaults to PROCEED if the verdict line is missing (bug triage optimistic path), unless
 * `defaultVerdictIfMissing` is set (e.g. card readiness may use NEEDS_INFO).
 */
export function parseVerdictStructuredOutput(
  raw: string,
  options?: ParseVerdictStructuredOptions,
): TriageResult {
  const verdictMatch = raw.match(/VERDICT:\s*(PROCEED|NEEDS_INFO)/i);
  const verdict = verdictMatch
    ? (verdictMatch[1]!.toUpperCase() as "PROCEED" | "NEEDS_INFO")
    : (options?.defaultVerdictIfMissing ?? "PROCEED");

  const summaryMatch = raw.match(/SUMMARY:\s*([\s\S]*?)(?=\nQUESTIONS:|$)/i);
  const summary = summaryMatch ? summaryMatch[1]!.trim() : raw.trim();

  const questionsMatch = raw.match(/QUESTIONS:\s*([\s\S]*?)$/i);
  const questions = questionsMatch ? questionsMatch[1]!.trim() || null : null;

  return { verdict, title: null, summary, questions, raw };
}

/** Bug triage pipeline — same as {@link parseVerdictStructuredOutput}. */
export function parseTriageOutput(raw: string): TriageResult {
  return parseVerdictStructuredOutput(raw);
}

/**
 * Card readiness / ticket-review pipeline — same structure as triage, plus fence cleanup for Jira comments.
 * If `VERDICT:` is missing or invalid, defaults to **NEEDS_INFO** (not PROCEED), so we never imply "ready to implement" without a clear agent verdict.
 */
export function parseCardReadinessOutput(raw: string): TriageResult {
  const base = parseVerdictStructuredOutput(raw, { defaultVerdictIfMissing: "NEEDS_INFO" });
  const summary = stripCodeFenceArtifacts(base.summary);
  let questions = base.questions ? stripCodeFenceArtifacts(base.questions) : null;
  if (questions === "") questions = null;
  return { ...base, summary, questions };
}

/**
 * Build a prompt for answering a Slack mention by searching the codebase.
 * The agent should explore repos read-only and produce a concise answer.
 */
export function buildSlackMentionPrompt(
  userMessage: string,
  repos: readonly RepoConfig[],
  language: "es" | "en" = "en",
  jiraIssues?: readonly JiraIssue[],
  datadogLogsByTicket?: Record<string, string[]>,
  threadContext?: readonly ThreadContextEntry[],
): string {
  const generalRules = loadGeneralRules();
  const lines: string[] = [generalRules, ""];

  lines.push(
    "# Slack mention — codebase Q&A",
    "",
    "You are responding to a developer question asked via Slack.",
    "You have access to the following repositories (already cloned in the working directory):",
    "",
  );

  for (const repo of repos) {
    lines.push(`- **${repo.name}**: ${repo.description}`);
  }

  if (jiraIssues && jiraIssues.length > 0) {
    lines.push("", "## Referenced Jira Tickets", "");
    for (const issue of jiraIssues) {
      lines.push(...buildTicketContext(issue), "");
    }
  }

  if (datadogLogsByTicket && Object.keys(datadogLogsByTicket).length > 0) {
    lines.push("", "## Datadog Logs (last 24h)", "");
    lines.push(
      "The following are application logs from Datadog related to the referenced tickets.",
      "Use them to understand what happened at runtime (errors, flow, timing, etc.).",
      "",
    );
    for (const [ticketKey, logLines] of Object.entries(datadogLogsByTicket)) {
      lines.push(`### Logs for ${ticketKey}`, "```");
      // Cap log output to avoid blowing up the prompt
      const MAX_LOG_LINES = 200;
      const truncated = logLines.length > MAX_LOG_LINES;
      lines.push(...logLines.slice(0, MAX_LOG_LINES));
      if (truncated) {
        lines.push(`... (${logLines.length - MAX_LOG_LINES} more lines truncated)`);
      }
      lines.push("```", "");
    }
  }

  if (threadContext && threadContext.length > 0) {
    lines.push(...renderThreadContext(threadContext));
  }

  lines.push(
    "",
    "## Question",
    "```",
    userMessage.slice(0, 4000),
    "```",
    "",
    "## Instructions",
    "1. Navigate the repositories to find the answer (use grep, read files, explore the codebase).",
    "2. Do NOT modify any files — this is a read-only task.",
    "3. Write a clear, concise answer. Include file paths and code snippets when relevant.",
    "4. Keep the answer under 3000 characters (Slack message limit).",
    `5. You MUST write your entire answer in ${language === "es" ? "Spanish" : "English"}.`,
    "6. Put your answer in a block that starts with REPLY_START and ends with REPLY_END.",
    "",
    "## Formatting — Slack mrkdwn (IMPORTANT)",
    "Your answer will be posted directly to Slack, so you MUST use Slack mrkdwn syntax, NOT GitHub Markdown.",
    "- Bold: *bold* (single asterisk, NOT **double**)",
    "- Italic: _italic_ (NOT *single asterisk*)",
    "- Code inline: `code`",
    "- Code block: wrap in triple backticks (```)",
    "- Bullet lists: use • or - at the start of lines",
    "- DO NOT use Markdown tables (| col | col |) — Slack does not render them. Use bullet lists instead.",
    "- DO NOT use Markdown headers (## Header) — use *Bold text* on its own line instead.",
    "- Links: <url|label>",
    "",
    "REPLY_START",
    "Your answer here.",
    "REPLY_END",
  );

  return lines.join("\n");
}

function renderThreadContext(entries: readonly ThreadContextEntry[]): string[] {
  const lines: string[] = [
    "",
    "## Thread Context",
    "",
    "The user mentioned you in a Slack thread. Below is the conversation so far (oldest first). The latest message is what you must answer (see ## Question below).",
    "",
  ];
  for (const e of entries) {
    const time = formatThreadTs(e.ts);
    const cleanText = (e.text ?? "").replace(/\s+/g, " ").trim();
    const safeText = cleanText.length > 800 ? `${cleanText.slice(0, 800)}…` : cleanText;
    const tsSuffix = time ? ` (${time})` : "";
    lines.push(`- **${e.author}**${tsSuffix}: ${safeText}`);
  }
  return lines;
}

function formatThreadTs(ts: string): string | null {
  const seconds = parseInt(ts.split(".")[0] ?? "", 10);
  if (!Number.isFinite(seconds)) return null;
  const d = new Date(seconds * 1000);
  if (Number.isNaN(d.getTime())) return null;
  const hh = String(d.getUTCHours()).padStart(2, "0");
  const mm = String(d.getUTCMinutes()).padStart(2, "0");
  return `${hh}:${mm}`;
}
