// @generated — DO NOT EDIT. Source: packages/server/tour/tour-review.ts
import { join } from "node:path";
import { tmpdir } from "node:os";
import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
import { getPlannotatorDataDir } from "./data-dir";
import type { DiffType } from "./review-core.js";
import type { PRMetadata } from "./pr-provider.js";
import { getLocalDiffInstruction } from "./agent-review-message.js";
import type {
  CodeTourOutput,
  TourDiffAnchor,
  TourKeyTakeaway,
  TourStop,
  TourQAItem,
} from "./tour.js";

export type { CodeTourOutput, TourDiffAnchor, TourKeyTakeaway, TourStop, TourQAItem };

export const TOUR_EMPTY_OUTPUT_ERROR = "Tour generation returned empty or malformed output";

export const TOUR_SCHEMA_JSON = JSON.stringify({
  type: "object",
  properties: {
    title: { type: "string" },
    greeting: { type: "string" },
    intent: { type: "string" },
    before: { type: "string" },
    after: { type: "string" },
    key_takeaways: {
      type: "array",
      items: {
        type: "object",
        properties: {
          text: { type: "string" },
          severity: { type: "string", enum: ["info", "important", "warning"] },
        },
        required: ["text", "severity"],
        additionalProperties: false,
      },
    },
    stops: {
      type: "array",
      items: {
        type: "object",
        properties: {
          title: { type: "string" },
          gist: { type: "string" },
          detail: { type: "string" },
          transition: { type: "string" },
          anchors: {
            type: "array",
            items: {
              type: "object",
              properties: {
                file: { type: "string" },
                line: { type: "integer" },
                end_line: { type: "integer" },
                hunk: { type: "string" },
                label: { type: "string" },
              },
              required: ["file", "line", "end_line", "hunk", "label"],
              additionalProperties: false,
            },
          },
        },
        required: ["title", "gist", "detail", "transition", "anchors"],
        additionalProperties: false,
      },
    },
    qa_checklist: {
      type: "array",
      items: {
        type: "object",
        properties: {
          question: { type: "string" },
          stop_indices: { type: "array", items: { type: "integer" } },
        },
        required: ["question", "stop_indices"],
        additionalProperties: false,
      },
    },
  },
  required: ["title", "greeting", "intent", "before", "after", "key_takeaways", "stops", "qa_checklist"],
  additionalProperties: false,
});

export const TOUR_REVIEW_PROMPT = `# Code Tour Narrator

## Identity
You are a colleague giving a casual, warm tour of work you understand well.
Think of it like sitting down next to someone and saying: "Hey Mike, here's
the PR. Let me walk you through it." The whole voice is conversational, not
documentary. You're telling the story of what changed and why.

The arguments (like "here's why we did it this way" or "we picked X instead
of Y") live INSIDE the stop details, where they belong. The framing (the
greeting, intent, before/after, transitions between stops) stays warm and
human, the way a coworker actually talks over coffee.

You are NOT finding bugs. You are NOT writing a technical report.

## Tone
- Conversational throughout. You're talking to a coworker, not writing docs.
- Use "we" and "you". "Here's what we changed." "You'll notice that..."
- A couple of sentences of context is fine, even for small PRs. If a
  colleague was describing a one-line change, they wouldn't just say "I
  changed a line." They'd say "Oh yeah, I bumped the TTL from 7 days to 24
  hours because the audit flagged it last month." A little color is good.
- Each stop should feel like a colleague pausing to point at something:
  "Okay, look at this part. Here's why it's interesting."
- **Do NOT use em-dashes (—) anywhere.** They're a dead giveaway of
  AI-generated prose. Use commas, colons, semicolons, or separate sentences
  instead. If you want to add an aside, use parentheses or start a new
  sentence. Never an em-dash.
- No emoji anywhere. The UI handles all visual labeling deterministically.

## Output structure

### greeting
2-4 sentences welcoming the reviewer and setting the scene. Not a headline,
more like how you'd actually open a conversation. "Hey, so this PR does X
and Y. Grab a coffee; I'll walk you through it." A bit of warmth and context,
even for small changes.
Example: "Hey, so this PR tightens the auth session lifetime from a week down
to 24 hours. It's small in line count but it's the fix the security team has
been asking for since Q1. Let me walk you through it."

### intent
1-3 sentences explaining WHY this changeset exists. What problem is being
solved? What motivated the work? Keep it conversational; you're giving
context, not writing a ticket.

To determine intent:
- If a PR/MR URL was provided, read the PR description (gh pr view or
  equivalent). Look for motivation, linked issues, and context the author
  provided.
- If the PR body references a GitHub issue (e.g. "Fixes #123", "Closes
  owner/repo#456") or GitLab issue, read that specific issue for deeper
  context.
- If no PR is provided, infer intent from commit messages, branch name, and
  the nature of the changes themselves.
- IMPORTANT: Do NOT search for issues or tickets that are not explicitly
  referenced. Do not browse all open issues. Do not look up Linear/Jira
  tickets unless a link appears in the PR description or commit messages.
  Only follow what is given.

Example: "Closes SEC-412, the overly-permissive session TTL flagged by the
security team during the Q1 audit. It also lays some groundwork for the
offline-first work shipping next sprint."

### before / after
One to two sentences each. Paint the picture of the world before and after
this change. Focus on user or system behavior, not code structure.
Example before: "Sessions lasted 7 days, with no refresh contract, so a
stolen token was dangerous for a full week."
Example after: "Sessions now expire in 24 hours with a clean refresh path,
and mobile clients poll every 15 minutes to stay fresh."

### key_takeaways
3 to 5 bullet points. These are the MOST IMPORTANT things someone needs to
know at a glance about what this changeset DOES. Focus on what changes in
behavior, functionality, or developer experience. Each is ONE sentence. No
emoji, no prefix, just the text.

Severity guide (drives visual styling automatically; pick honestly, don't inflate):
- "info": neutral context, good to know.
- "important": a meaningful change in behavior, capability, or system contract.
- "warning": a behavioral shift worth watching, something that changes how
  the system works in a way someone could miss. NOT code smells or style
  nits. A clean changeset with no warnings is perfectly normal.

### stops
Each stop is the colleague pausing at a specific change to explain it.

#### How to ORDER stops
Order by READING FLOW, the order the colleague would walk you through the
change to make it understandable. NOT by blast radius or criticality.

Lead with the entry point: the file or function that, if understood alone,
unlocks the rest. Then walk outward:
- Definitions before consumers (types/interfaces/schemas before usage).
- Cause before effect (the change that motivated downstream changes comes first).
- Verification last (tests and migrations after the code they exercise).

#### How to CHUNK stops
A stop is a logical change, NOT a file. If three files changed for one reason,
that's ONE stop with three anchors. If one file has two unrelated changes,
that's two stops. Never "one-stop-per-file" by default; let logic decide.

#### Stop fields
- **title**: Short, friendly. "Token refresh flow", not "Changes to auth/refresh.ts".
- **gist**: ONE sentence. The headline. A reviewer who reads nothing else should
  understand this stop from the gist alone.
- **detail**: This is where the colleague pauses to explain. Supports basic markdown.
  - Start with 1-2 sentences describing the situation or problem this stop addresses.
  - Then make the argument: WHY did we change this? WHY does the new code look the
    way it does? If a non-obvious choice was made (data structure, error strategy,
    sync vs async, where the logic lives), surface it. "We did X instead of Y
    because Z" is exactly what the reviewer wants.
  - Use ### headings (e.g. "### Why this shape") to highlight critical sub-sections.
  - Use > [!IMPORTANT], > [!WARNING], or > [!NOTE] callout blocks for context
    that helps the reader understand non-obvious decisions or behavioral shifts
    (e.g., a new default value, a changed error path, a contract that callers
    now depend on). These are not for flagging code smells.
  - Use - bullet points for multi-part changes or parallel considerations.
  - Keep total length reasonable, around 3-6 sentences equivalent. Don't write
    an essay.
- **transition**: A short connective phrase to the next stop, in the colleague's
  voice. Examples: "Building on that...", "On a related note...", "To support
  that change...". Empty string for the last stop.
- **anchors**: The specific diff hunks shown inline below the detail narrative.
  Each anchor MUST have a non-empty "hunk" field containing the actual unified
  diff text extracted from the changeset. The hunk must include the @@ line.

  Valid hunk format (REQUIRED; every anchor needs this):

    @@ -42,7 +42,9 @@
     function processRequest(req) {
    -  const result = await fetch(url);
    -  return result.json();
    +  const result = await fetch(url, { timeout: 5000 });
    +  if (!result.ok) throw new Error("HTTP " + result.status);
    +  return result.json();
     }

  The label should be a substantive 1-sentence explanation of what this code
  section does or why it matters, not a filename paraphrase.
  E.g. "Adds a 5-second timeout and explicit error check to prevent silent hangs",
  not "Changes to request.ts".

### qa_checklist
4 to 8 verification questions a HUMAN can actually answer. Two valid channels:

1. By READING the code (e.g., "Did we update both call sites of \`legacyAuth()\`?",
   "Are all uses of the old token format migrated?", "Does the error handler
   cover the new throw paths?").
2. By manually USING the product (e.g., "Sign in, restart the browser, and
   confirm the session persists.", "Trigger a 503 from the API and confirm the
   retry banner appears.").

NOT machine-runnable test ideas. NOT generic "smoke test" framing. The reviewer
is a person; what would THEY do to gain confidence?

Reference which stops each question relates to via stop_indices. Every question
should reference at least one stop.

## Pipeline

1. Read the full diff (git diff, jj diff, or inlined patch).
2. Read CLAUDE.md and README.md for project context.
3. Read commit messages (git log --oneline) and PR title/body if available.
4. Identify logical groupings of change (cross-file when appropriate). These
   become stops.
5. Determine reading flow order: entry point first, then outward. Definitions
   before consumers, cause before effect.
6. Write the greeting, intent, before/after, takeaways, stops, and checklist
   in the voice of a coworker walking you through the work.
7. Return structured JSON matching the schema.

## Hard constraints
- Every anchor MUST have a non-empty "hunk" field. An anchor with an empty hunk
  is broken; it will show "diff not available" to the reviewer. Extract the
  real unified diff text from the input patch. Do not leave hunk blank.
- Never fabricate line numbers. Extract them from the diff.
- Gist must be ONE sentence. Not two. Not a run-on. One.
- Detail supports markdown. Use it when it makes the explanation clearer, not
  for decoration. Plain prose is fine when the change is simple.
- Anchor labels must explain the code's purpose or the change's impact, not
  just describe the filename.
- key_takeaways: 3 to 5 items, each ONE sentence.
- Stops are LOGICAL units, not files. Cross-file grouping is expected.
- Stop ORDER is reading flow: entry point first, definitions before consumers,
  cause before effect, verification last.
- Combine trivial changes (renames, imports, formatting) into one "Housekeeping"
  stop at the end, or omit entirely.
- QA questions must be answerable by a human, either by reading code or by
  using the product. Never frame them as automated tests.
- NEVER use em-dashes (—) anywhere in the output. Use commas, colons,
  semicolons, parentheses, or separate sentences. This is a hard constraint.

## Calibration: tour, not review
Your job is to EXPLAIN the changeset, not to critique it. If you genuinely
spot a real bug or a meaningful behavioral concern while reading the code,
surface it naturally in the relevant stop detail or as a warning takeaway.
That's the colleague noticing something worth mentioning. But don't hunt for
problems. Most clean changesets should have zero warnings and zero [!WARNING]
callouts. The primary question is "what does this change do and why?" not
"what's wrong with this code?"`;

export function buildTourUserMessage(
  patch: string,
  diffType: DiffType,
  options?: { defaultBranch?: string; hasLocalAccess?: boolean; prDiffScope?: string },
  prMetadata?: PRMetadata,
): string {
  if (prMetadata) {
    if (options?.prDiffScope === "full-stack") {
      return [
        `Full-stack tour of ${prMetadata.url}`,
        "",
        "This is a stacked PR. The diff below shows ALL accumulated changes from the repository default branch through this PR's head (not just this PR's own layer).",
        "Walk the reviewer through the complete changeset as a guided tour.",
        "",
        "```diff",
        patch,
        "```",
      ].join("\n");
    }
    if (options?.hasLocalAccess) {
      return [
        prMetadata.url,
        "",
        "You are in a local worktree checked out at the PR head. The code is available locally.",
        `To see the PR changes, diff against the remote base branch: git diff origin/${prMetadata.baseBranch}...HEAD`,
        "Do NOT diff against the local `main` branch; it may be stale. Always use origin/.",
        "",
        "Walk the reviewer through this changeset as a guided tour.",
      ].join("\n");
    }
    return [prMetadata.url, "", "Walk the reviewer through this PR as a guided tour."].join("\n");
  }

  const instruction = getLocalDiffInstruction(diffType, options?.defaultBranch);
  if (instruction) {
    return `Walk the reviewer through ${instruction.target} as a guided tour. ${instruction.inspect}`;
  }

  return [
    "Walk the reviewer through the following code changes as a guided tour.",
    "",
    "```diff",
    patch,
    "```",
  ].join("\n");
}

export interface TourClaudeCommandResult {
  command: string[];
  stdinPrompt: string;
}

export function buildTourClaudeCommand(prompt: string, model: string = "sonnet", effort?: string): TourClaudeCommandResult {
  const allowedTools = [
    "Agent", "Read", "Glob", "Grep",
    "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
    "Bash(git show:*)", "Bash(git blame:*)", "Bash(git branch:*)",
    "Bash(git grep:*)", "Bash(git ls-remote:*)", "Bash(git ls-tree:*)",
    "Bash(git merge-base:*)", "Bash(git remote:*)", "Bash(git rev-parse:*)",
    "Bash(git show-ref:*)",
    "Bash(jj status:*)", "Bash(jj diff:*)", "Bash(jj log:*)",
    "Bash(jj show:*)", "Bash(jj file show:*)", "Bash(jj cat:*)",
    "Bash(jj bookmark list:*)",
    "Bash(gh pr view:*)", "Bash(gh pr diff:*)", "Bash(gh pr list:*)",
    "Bash(gh api repos/*/*/pulls/*)", "Bash(gh api repos/*/*/pulls/*/files*)",
    // The tour prompt follows linked issues (`Fixes #123`, `Closes owner/repo#456`),
    // so the allowlist has to permit the issue-read commands.
    "Bash(gh issue view:*)", "Bash(gh api repos/*/*/issues/*)",
    "Bash(glab mr view:*)", "Bash(glab mr diff:*)",
    "Bash(glab issue view:*)",
    "Bash(wc:*)",
  ].join(",");

  const disallowedTools = [
    "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch",
    "Bash(python:*)", "Bash(python3:*)", "Bash(node:*)", "Bash(npx:*)",
    "Bash(bun:*)", "Bash(bunx:*)", "Bash(sh:*)", "Bash(bash:*)", "Bash(zsh:*)",
    "Bash(curl:*)", "Bash(wget:*)",
  ].join(",");

  return {
    command: [
      "claude", "-p",
      "--permission-mode", "dontAsk",
      "--output-format", "stream-json",
      "--verbose",
      "--json-schema", TOUR_SCHEMA_JSON,
      "--no-session-persistence",
      "--model", model,
      ...(effort ? ["--effort", effort] : []),
      "--tools", "Agent,Bash,Read,Glob,Grep",
      "--allowedTools", allowedTools,
      "--disallowedTools", disallowedTools,
    ],
    stdinPrompt: prompt,
  };
}

const TOUR_SCHEMA_DIR = getPlannotatorDataDir();
const TOUR_SCHEMA_FILE = join(TOUR_SCHEMA_DIR, "tour-schema.json");
let tourSchemaMaterialized = false;

async function ensureTourSchemaFile(): Promise<string> {
  if (!tourSchemaMaterialized) {
    await mkdir(TOUR_SCHEMA_DIR, { recursive: true });
    await writeFile(TOUR_SCHEMA_FILE, TOUR_SCHEMA_JSON);
    tourSchemaMaterialized = true;
  }
  return TOUR_SCHEMA_FILE;
}

export function generateTourOutputPath(): string {
  return join(tmpdir(), `plannotator-tour-${crypto.randomUUID()}.json`);
}

export async function buildTourCodexCommand(options: {
  cwd: string;
  outputPath: string;
  prompt: string;
  model?: string;
  reasoningEffort?: string;
  fastMode?: boolean;
}): Promise<string[]> {
  const { cwd, outputPath, prompt, model, reasoningEffort, fastMode } = options;
  const schemaPath = await ensureTourSchemaFile();

  const command = [
    "codex",
    // Global flags must precede the "exec" subcommand for the Codex CLI.
    ...(model ? ["-m", model] : []),
    ...(reasoningEffort ? ["-c", `model_reasoning_effort=${reasoningEffort}`] : []),
    ...(fastMode ? ["-c", "service_tier=fast"] : []),
    "exec",
    "--output-schema", schemaPath,
    "-o", outputPath,
    "--full-auto", "--ephemeral",
    "-C", cwd,
    prompt,
  ];

  return command;
}

export function parseTourStreamOutput(stdout: string): CodeTourOutput | null {
  if (!stdout.trim()) return null;

  const lines = stdout.trim().split('\n');
  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i].trim();
    if (!line) continue;

    try {
      const event = JSON.parse(line);
      if (event.type === 'result') {
        if (event.is_error) return null;
        const output = event.structured_output;
        // A tour with no stops isn't a tour — treat as invalid so the UI
        // error state fires instead of rendering an empty walkthrough.
        if (!output || !Array.isArray(output.stops) || output.stops.length === 0) return null;
        return output as CodeTourOutput;
      }
    } catch {
      // Not valid JSON — skip
    }
  }

  return null;
}

export async function parseTourFileOutput(outputPath: string): Promise<CodeTourOutput | null> {
  try {
    const text = await readFile(outputPath, "utf-8");
    try { await unlink(outputPath); } catch { /* ignore */ }
    if (!text.trim()) return null;
    const parsed = JSON.parse(text);
    // A tour with no stops isn't a tour — treat as invalid so the UI
    // error state fires instead of rendering an empty walkthrough.
    if (!parsed || !Array.isArray(parsed.stops) || parsed.stops.length === 0) return null;
    return parsed as CodeTourOutput;
  } catch {
    try { await unlink(outputPath); } catch { /* ignore */ }
    return null;
  }
}

export interface TourSessionBuildCommandOptions {
  cwd: string;
  patch: string;
  diffType: DiffType;
  options?: { defaultBranch?: string; hasLocalAccess?: boolean };
  prMetadata?: PRMetadata;
  config?: Record<string, unknown>;
}

export interface TourSessionBuildCommandResult {
  command: string[];
  outputPath?: string;
  captureStdout?: boolean;
  stdinPrompt?: string;
  cwd?: string;
  label?: string;
  prompt?: string;
  engine: "claude" | "codex";
  model: string;
  effort?: string;
  reasoningEffort?: string;
  fastMode?: boolean;
}

export interface TourSessionJobSummary {
  correctness: string;
  explanation: string;
  confidence: number;
}

export interface TourSessionJobRef {
  id: string;
  engine?: string;
}

export interface TourSessionOnJobCompleteOptions {
  job: TourSessionJobRef;
  meta: { outputPath?: string; stdout?: string };
}

export interface TourSession {
  tourResults: Map<string, CodeTourOutput>;
  tourChecklists: Map<string, boolean[]>;
  buildCommand(opts: TourSessionBuildCommandOptions): Promise<TourSessionBuildCommandResult>;
  onJobComplete(opts: TourSessionOnJobCompleteOptions): Promise<{ summary: TourSessionJobSummary | null }>;
  getTour(jobId: string): (CodeTourOutput & { checklist: boolean[] }) | null;
  saveChecklist(jobId: string, checked: boolean[]): void;
}

export function createTourSession(): TourSession {
  const tourResults = new Map<string, CodeTourOutput>();
  const tourChecklists = new Map<string, boolean[]>();

  return {
    tourResults,
    tourChecklists,

    async buildCommand({ cwd, patch, diffType, options, prMetadata, config }) {
      const engine = (typeof config?.engine === "string" ? config.engine : "claude") as "claude" | "codex";
      const explicitModel = typeof config?.model === "string" && config.model ? config.model : null;
      // "sonnet" is a Claude model, so we must NOT pass it to Codex when no model
      // is explicitly selected. Leave Codex model blank and let its CLI default pick.
      const model = explicitModel ?? (engine === "codex" ? "" : "sonnet");
      const reasoningEffort = typeof config?.reasoningEffort === "string" && config.reasoningEffort ? config.reasoningEffort : undefined;
      const effort = typeof config?.effort === "string" && config.effort ? config.effort : undefined;
      const fastMode = config?.fastMode === true;
      const userMessage = buildTourUserMessage(patch, diffType, options, prMetadata);
      const prompt = TOUR_REVIEW_PROMPT + "\n\n---\n\n" + userMessage;

      if (engine === "codex") {
        const outputPath = generateTourOutputPath();
        const command = await buildTourCodexCommand({ cwd, outputPath, prompt, model: model || undefined, reasoningEffort, fastMode });
        return { command, outputPath, prompt, label: "Code Tour", engine: "codex", model, reasoningEffort, fastMode: fastMode || undefined };
      }

      const { command, stdinPrompt } = buildTourClaudeCommand(prompt, model, effort);
      return { command, stdinPrompt, prompt, cwd, label: "Code Tour", captureStdout: true, engine: "claude", model, effort };
    },

    async onJobComplete({ job, meta }) {
      let output: CodeTourOutput | null = null;
      if (job.engine === "codex" && meta.outputPath) {
        output = await parseTourFileOutput(meta.outputPath);
      } else if (meta.stdout) {
        output = parseTourStreamOutput(meta.stdout);
      }

      if (!output) {
        console.error(`[tour] Failed to parse output for job ${job.id}`);
        return { summary: null };
      }

      tourResults.set(job.id, output);
      const summary: TourSessionJobSummary = {
        correctness: "Tour Generated",
        explanation: `${output.stops.length} stop${output.stops.length !== 1 ? "s" : ""}, ${output.qa_checklist.length} QA item${output.qa_checklist.length !== 1 ? "s" : ""}`,
        confidence: 1.0,
      };
      return { summary };
    },

    getTour(jobId) {
      const tour = tourResults.get(jobId);
      if (!tour) return null;
      return { ...tour, checklist: tourChecklists.get(jobId) ?? [] };
    },

    saveChecklist(jobId, checked) {
      tourChecklists.set(jobId, checked);
    },
  };
}
