/**
 * SDK Session Parser - Parses Claude Agent SDK native session files.
 *
 * The SDK stores sessions in ~/.claude/projects/{vault-path-encoded}/{sessionId}.jsonl
 * Each line is a JSON object with message data.
 *
 * This utility converts SDK native messages to Claudian's ChatMessage format
 * for displaying conversation history from native sessions.
 */

import { existsSync } from 'fs';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';

import { extractToolResultContent } from '../core/sdk/toolResultContent';
import { extractResolvedAnswers, extractResolvedAnswersFromResultText } from '../core/tools';
import { isSubagentToolName, TOOL_ASK_USER_QUESTION } from '../core/tools/toolNames';
import type {
  AsyncSubagentStatus,
  ChatMessage,
  ContentBlock,
  ImageAttachment,
  ImageMediaType,
  SubagentInfo,
  ToolCallInfo,
} from '../core/types';
import { extractContentBeforeXmlContext } from './context';
import { extractDiffData } from './diff';
import { isCompactionCanceledStderr, isInterruptSignalText } from './interrupt';
import { extractFinalResultFromSubagentJsonl } from './subagentJsonl';

export interface SDKSessionReadResult {
  messages: SDKNativeMessage[];
  skippedLines: number;
  error?: string;
}

/** Stored in session JSONL files. Based on Claude Agent SDK internal format. */
export interface SDKNativeMessage {
  type: 'user' | 'assistant' | 'system' | 'result' | 'file-history-snapshot' | 'queue-operation';
  parentUuid?: string | null;
  sessionId?: string;
  uuid?: string;
  timestamp?: string;
  /** Request ID groups assistant messages from the same API call. */
  requestId?: string;
  message?: {
    role?: string;
    content?: string | SDKNativeContentBlock[];
    model?: string;
  };
  // Result message fields
  subtype?: string;
  duration_ms?: number;
  duration_api_ms?: number;
  /** Present on tool result user messages - contains the tool execution result. */
  toolUseResult?: unknown;
  /** UUID of the assistant message that initiated this tool call. */
  sourceToolAssistantUUID?: string;
  /** Tool use ID for injected content (e.g., skill prompt expansion). */
  sourceToolUseID?: string;
  /** Meta messages are system-injected, not actual user input. */
  isMeta?: boolean;
  /** Queue operation type (enqueue/dequeue) — present on queue-operation messages. */
  operation?: string;
  /** Content string for queue-operation enqueue entries (e.g., task-notification XML). */
  content?: string;
}

export interface SDKNativeContentBlock {
  type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'image';
  text?: string;
  thinking?: string;
  id?: string;
  name?: string;
  input?: Record<string, unknown>;
  tool_use_id?: string;
  content?: string | unknown;
  is_error?: boolean;
  // Image block fields
  source?: {
    type: 'base64';
    media_type: string;
    data: string;
  };
}

/**
 * Encodes a vault path for the SDK project directory name.
 * The SDK replaces ALL non-alphanumeric characters with `-`.
 * This handles Unicode characters (Chinese, Japanese, etc.) and special chars (brackets, etc.).
 */
export function encodeVaultPathForSDK(vaultPath: string): string {
  const absolutePath = path.resolve(vaultPath);
  return absolutePath.replace(/[^a-zA-Z0-9]/g, '-');
}

export function getSDKProjectsPath(): string {
  return path.join(os.homedir(), '.claude', 'projects');
}

/** Validates a subagent agent ID to prevent path traversal attacks. */
function isValidAgentId(agentId: string): boolean {
  if (!agentId || agentId.length > 128) {
    return false;
  }
  if (agentId.includes('..') || agentId.includes('/') || agentId.includes('\\')) {
    return false;
  }
  return /^[a-zA-Z0-9_-]+$/.test(agentId);
}

type SubagentToolEvent =
  | {
      type: 'tool_use';
      toolUseId: string;
      toolName: string;
      toolInput: Record<string, unknown>;
      timestamp: number;
    }
  | {
      type: 'tool_result';
      toolUseId: string;
      content: string;
      isError: boolean;
      timestamp: number;
    };

function parseTimestampMs(raw: unknown): number {
  if (typeof raw !== 'string') return Date.now();
  const parsed = Date.parse(raw);
  return Number.isNaN(parsed) ? Date.now() : parsed;
}

function parseSubagentEvents(entry: unknown): SubagentToolEvent[] {
  if (!entry || typeof entry !== 'object') return [];

  const record = entry as {
    timestamp?: unknown;
    message?: { content?: unknown };
  };
  const content = record.message?.content;
  if (!Array.isArray(content)) return [];

  const timestamp = parseTimestampMs(record.timestamp);
  const events: SubagentToolEvent[] = [];

  for (const blockRaw of content) {
    if (!blockRaw || typeof blockRaw !== 'object') continue;

    const block = blockRaw as {
      type?: unknown;
      id?: unknown;
      name?: unknown;
      input?: unknown;
      tool_use_id?: unknown;
      content?: unknown;
      is_error?: unknown;
    };

    if (block.type === 'tool_use') {
      if (typeof block.id !== 'string' || typeof block.name !== 'string') continue;
      events.push({
        type: 'tool_use',
        toolUseId: block.id,
        toolName: block.name,
        toolInput:
          block.input && typeof block.input === 'object'
            ? (block.input as Record<string, unknown>)
            : {},
        timestamp,
      });
      continue;
    }

    if (block.type === 'tool_result') {
      if (typeof block.tool_use_id !== 'string') continue;
      const contentText = extractToolResultContent(block.content);
      events.push({
        type: 'tool_result',
        toolUseId: block.tool_use_id,
        content: contentText,
        isError: block.is_error === true,
        timestamp,
      });
    }
  }

  return events;
}

function buildToolCallsFromSubagentEvents(events: SubagentToolEvent[]): ToolCallInfo[] {
  const toolsById = new Map<
    string,
    {
      toolCall: ToolCallInfo;
      hasToolUse: boolean;
      hasToolResult: boolean;
      timestamp: number;
    }
  >();

  for (const event of events) {
    const existing = toolsById.get(event.toolUseId);

    if (event.type === 'tool_use') {
      if (!existing) {
        toolsById.set(event.toolUseId, {
          toolCall: {
            id: event.toolUseId,
            name: event.toolName,
            input: { ...event.toolInput },
            status: 'running',
            isExpanded: false,
          },
          hasToolUse: true,
          hasToolResult: false,
          timestamp: event.timestamp,
        });
      } else {
        existing.toolCall.name = event.toolName;
        existing.toolCall.input = { ...event.toolInput };
        existing.hasToolUse = true;
        existing.timestamp = event.timestamp;
      }
      continue;
    }

    if (!existing) {
      toolsById.set(event.toolUseId, {
        toolCall: {
          id: event.toolUseId,
          name: 'Unknown',
          input: {},
          status: event.isError ? 'error' : 'completed',
          result: event.content,
          isExpanded: false,
        },
        hasToolUse: false,
        hasToolResult: true,
        timestamp: event.timestamp,
      });
      continue;
    }

    existing.toolCall.status = event.isError ? 'error' : 'completed';
    existing.toolCall.result = event.content;
    existing.hasToolResult = true;
  }

  return Array.from(toolsById.values())
    .filter(entry => entry.hasToolUse)
    .sort((a, b) => a.timestamp - b.timestamp)
    .map(entry => entry.toolCall);
}

function getSubagentSidecarPath(
  vaultPath: string,
  sessionId: string,
  agentId: string
): string | null {
  if (!isValidSessionId(sessionId) || !isValidAgentId(agentId)) {
    return null;
  }

  const encodedVault = encodeVaultPathForSDK(vaultPath);
  return path.join(
    getSDKProjectsPath(),
    encodedVault,
    sessionId,
    'subagents',
    `agent-${agentId}.jsonl`
  );
}

/**
 * Loads tool calls executed inside a subagent from SDK sidechain logs.
 *
 * File location:
 * ~/.claude/projects/{encoded-vault}/{sessionId}/subagents/agent-{agentId}.jsonl
 */
export async function loadSubagentToolCalls(
  vaultPath: string,
  sessionId: string,
  agentId: string
): Promise<ToolCallInfo[]> {
  const subagentFilePath = getSubagentSidecarPath(vaultPath, sessionId, agentId);
  if (!subagentFilePath) return [];

  try {
    if (!existsSync(subagentFilePath)) return [];

    const content = await fs.readFile(subagentFilePath, 'utf-8');
    const lines = content.split('\n').filter(line => line.trim());
    const events: SubagentToolEvent[] = [];
    const seen = new Set<string>();

    for (const line of lines) {
      let raw: unknown;
      try {
        raw = JSON.parse(line);
      } catch {
        continue;
      }

      for (const event of parseSubagentEvents(raw)) {
        const key = `${event.type}:${event.toolUseId}`;
        if (seen.has(key)) continue;
        seen.add(key);
        events.push(event);
      }
    }

    if (events.length === 0) return [];
    return buildToolCallsFromSubagentEvents(events);
  } catch {
    return [];
  }
}

/**
 * Loads the final textual result produced by a subagent from its sidecar JSONL.
 * Prefers the latest assistant text block; falls back to a top-level result field.
 */
export async function loadSubagentFinalResult(
  vaultPath: string,
  sessionId: string,
  agentId: string
): Promise<string | null> {
  const subagentFilePath = getSubagentSidecarPath(vaultPath, sessionId, agentId);
  if (!subagentFilePath) return null;

  try {
    if (!existsSync(subagentFilePath)) return null;
    const content = await fs.readFile(subagentFilePath, 'utf-8');
    return extractFinalResultFromSubagentJsonl(content);
  } catch {
    return null;
  }
}

/**
 * Validates a session ID to prevent path traversal attacks.
 * Accepts alphanumeric strings with hyphens and underscores (max 128 chars).
 * Common formats: SDK UUIDs, Claudian IDs (conv-TIMESTAMP-RANDOM).
 */
export function isValidSessionId(sessionId: string): boolean {
  if (!sessionId || sessionId.length === 0 || sessionId.length > 128) {
    return false;
  }
  // Reject path traversal attempts and path separators
  if (sessionId.includes('..') || sessionId.includes('/') || sessionId.includes('\\')) {
    return false;
  }
  // Allow only alphanumeric characters, hyphens, and underscores
  return /^[a-zA-Z0-9_-]+$/.test(sessionId);
}

/**
 * Gets the full path to an SDK session file.
 *
 * @param vaultPath - The vault's absolute path
 * @param sessionId - The SDK session ID (may equal conversation ID for new native sessions)
 * @returns Full path to the session JSONL file
 * @throws Error if sessionId is invalid (path traversal protection)
 */
export function getSDKSessionPath(vaultPath: string, sessionId: string): string {
  if (!isValidSessionId(sessionId)) {
    throw new Error(`Invalid session ID: ${sessionId}`);
  }
  const projectsPath = getSDKProjectsPath();
  const encodedVault = encodeVaultPathForSDK(vaultPath);
  return path.join(projectsPath, encodedVault, `${sessionId}.jsonl`);
}

export function sdkSessionExists(vaultPath: string, sessionId: string): boolean {
  try {
    const sessionPath = getSDKSessionPath(vaultPath, sessionId);
    return existsSync(sessionPath);
  } catch {
    return false;
  }
}

export async function deleteSDKSession(vaultPath: string, sessionId: string): Promise<void> {
  try {
    const sessionPath = getSDKSessionPath(vaultPath, sessionId);
    if (!existsSync(sessionPath)) return;
    await fs.unlink(sessionPath);
  } catch {
    // Best-effort deletion
  }
}

export async function readSDKSession(vaultPath: string, sessionId: string): Promise<SDKSessionReadResult> {
  try {
    const sessionPath = getSDKSessionPath(vaultPath, sessionId);
    if (!existsSync(sessionPath)) {
      return { messages: [], skippedLines: 0 };
    }

    const content = await fs.readFile(sessionPath, 'utf-8');
    const lines = content.split('\n').filter(line => line.trim());
    const messages: SDKNativeMessage[] = [];
    let skippedLines = 0;

    for (const line of lines) {
      try {
        const msg = JSON.parse(line) as SDKNativeMessage;
        messages.push(msg);
      } catch {
        skippedLines++;
      }
    }

    return { messages, skippedLines };
  } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    return { messages: [], skippedLines: 0, error: errorMsg };
  }
}

function extractTextContent(content: string | SDKNativeContentBlock[] | undefined): string {
  if (!content) return '';
  if (typeof content === 'string') return content;

  return content
    .filter((block): block is SDKNativeContentBlock & { type: 'text'; text: string } =>
      block.type === 'text' && typeof block.text === 'string' &&
      block.text.trim() !== '(no content)'
    )
    .map(block => block.text)
    .join('\n');
}

/**
 * Checks if user message content represents rebuilt context (history sent to SDK when session reset).
 * These start with a conversation role prefix and contain conversation history markers.
 * Handles both normal history (starting with User:) and truncated/malformed history (starting with Assistant:).
 */
function isRebuiltContextContent(textContent: string): boolean {
  // Must start with a conversation role prefix
  if (!/^(User|Assistant):\s/.test(textContent)) return false;
  // Must contain conversation continuation markers
  return textContent.includes('\n\nUser:') ||
         textContent.includes('\n\nAssistant:') ||
         textContent.includes('\n\nA:');
}

function extractDisplayContent(textContent: string): string | undefined {
  return extractContentBeforeXmlContext(textContent);
}

function extractImages(content: string | SDKNativeContentBlock[] | undefined): ImageAttachment[] | undefined {
  if (!content || typeof content === 'string') return undefined;

  const imageBlocks = content.filter(
    (block): block is SDKNativeContentBlock & {
      type: 'image';
      source: { type: 'base64'; media_type: string; data: string };
    } => block.type === 'image' && !!block.source?.data
  );

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

  return imageBlocks.map((block, index) => ({
    id: `sdk-img-${Date.now()}-${index}`,
    name: `image-${index + 1}`,
    mediaType: block.source.media_type as ImageMediaType,
    data: block.source.data,
    size: Math.ceil(block.source.data.length * 0.75), // Approximate original size from base64
    source: 'paste' as const,
  }));
}

/**
 * Extracts tool calls from SDK content blocks.
 *
 * @param content - The content blocks from the assistant message
 * @param toolResults - Pre-collected tool results from all messages (for cross-message matching)
 */
function extractToolCalls(
  content: string | SDKNativeContentBlock[] | undefined,
  toolResults?: Map<string, { content: string; isError: boolean }>
): ToolCallInfo[] | undefined {
  if (!content || typeof content === 'string') return undefined;

  const toolUses = content.filter(
    (block): block is SDKNativeContentBlock & { type: 'tool_use'; id: string; name: string } =>
      block.type === 'tool_use' && !!block.id && !!block.name
  );

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

  // Use provided results map, or build one from same-message results (fallback)
  const results = toolResults ?? new Map<string, { content: string; isError: boolean }>();
  if (!toolResults) {
    for (const block of content) {
      if (block.type === 'tool_result' && block.tool_use_id) {
        results.set(block.tool_use_id, {
          content: extractToolResultContent(block.content),
          isError: block.is_error ?? false,
        });
      }
    }
  }

  return toolUses.map(block => {
    const result = results.get(block.id);
    return {
      id: block.id,
      name: block.name,
      input: block.input ?? {},
      status: result ? (result.isError ? 'error' : 'completed') : 'running',
      result: result?.content,
      isExpanded: false,
    };
  });
}

function mapContentBlocks(content: string | SDKNativeContentBlock[] | undefined): ContentBlock[] | undefined {
  if (!content || typeof content === 'string') return undefined;

  const blocks: ContentBlock[] = [];

  for (const block of content) {
    switch (block.type) {
      case 'text': {
        // Skip "(no content)" placeholder the SDK writes as the first assistant entry
        const text = block.text;
        const trimmed = text?.trim();
        if (text && trimmed && trimmed !== '(no content)') {
          blocks.push({ type: 'text', content: text });
        }
        break;
      }

      case 'thinking':
        if (block.thinking) {
          blocks.push({ type: 'thinking', content: block.thinking });
        }
        break;

      case 'tool_use':
        if (block.id) {
          blocks.push({ type: 'tool_use', toolId: block.id });
        }
        break;

      // tool_result blocks are part of tool calls, not content blocks
    }
  }

  return blocks.length > 0 ? blocks : undefined;
}

/**
 * Converts an SDK native message to a ChatMessage.
 *
 * @param sdkMsg - The SDK native message
 * @param toolResults - Optional pre-collected tool results for cross-message matching.
 *   If not provided, only matches tool_result in the same message as tool_use.
 *   For full cross-message matching, use loadSDKSessionMessages() which performs three-pass parsing.
 * @returns ChatMessage or null if the message should be skipped
 */
export function parseSDKMessageToChat(
  sdkMsg: SDKNativeMessage,
  toolResults?: Map<string, { content: string; isError: boolean }>
): ChatMessage | null {
  if (sdkMsg.type === 'file-history-snapshot') return null;
  if (sdkMsg.type === 'system') {
    if (sdkMsg.subtype === 'compact_boundary') {
      const timestamp = sdkMsg.timestamp
        ? new Date(sdkMsg.timestamp).getTime()
        : Date.now();
      return {
        id: sdkMsg.uuid || `compact-${timestamp}-${Math.random().toString(36).slice(2)}`,
        role: 'assistant',
        content: '',
        timestamp,
        contentBlocks: [{ type: 'compact_boundary' }],
      };
    }
    return null;
  }
  if (sdkMsg.type === 'result') return null;
  if (sdkMsg.type !== 'user' && sdkMsg.type !== 'assistant') return null;

  const content = sdkMsg.message?.content;
  const textContent = extractTextContent(content);
  const images = sdkMsg.type === 'user' ? extractImages(content) : undefined;

  const hasToolUse = Array.isArray(content) && content.some(b => b.type === 'tool_use');
  const hasImages = images && images.length > 0;
  if (!textContent && !hasToolUse && !hasImages && (!content || typeof content === 'string')) return null;

  const timestamp = sdkMsg.timestamp
    ? new Date(sdkMsg.timestamp).getTime()
    : Date.now();

  // SDK wraps slash commands in XML tags — restore clean display (e.g., /compact, /md2docx)
  const commandNameMatch = sdkMsg.type === 'user'
    ? textContent.match(/<command-name>(\/[^<]+)<\/command-name>/)
    : null;

  let displayContent: string | undefined;
  if (sdkMsg.type === 'user') {
    displayContent = commandNameMatch ? commandNameMatch[1] : extractDisplayContent(textContent);
  }

  const isInterrupt = sdkMsg.type === 'user' && isInterruptSignalText(textContent);

  const isRebuiltContext = sdkMsg.type === 'user' && isRebuiltContextContent(textContent);

  return {
    id: sdkMsg.uuid || `sdk-${timestamp}-${Math.random().toString(36).slice(2)}`,
    role: sdkMsg.type,
    content: textContent,
    displayContent,
    timestamp,
    toolCalls: sdkMsg.type === 'assistant' ? extractToolCalls(content, toolResults) : undefined,
    contentBlocks: sdkMsg.type === 'assistant' ? mapContentBlocks(content) : undefined,
    images,
    ...(sdkMsg.type === 'user' && sdkMsg.uuid && { sdkUserUuid: sdkMsg.uuid }),
    ...(sdkMsg.type === 'assistant' && sdkMsg.uuid && { sdkAssistantUuid: sdkMsg.uuid }),
    ...(isInterrupt && { isInterrupt: true }),
    ...(isRebuiltContext && { isRebuiltContext: true }),
  };
}

/** tool_result often appears in user message following assistant's tool_use. */
function collectToolResults(sdkMessages: SDKNativeMessage[]): Map<string, { content: string; isError: boolean }> {
  const results = new Map<string, { content: string; isError: boolean }>();

  for (const sdkMsg of sdkMessages) {
    const content = sdkMsg.message?.content;
    if (!content || typeof content === 'string') continue;

    for (const block of content) {
      if (block.type === 'tool_result' && block.tool_use_id) {
        results.set(block.tool_use_id, {
          content: extractToolResultContent(block.content),
          isError: block.is_error ?? false,
        });
      }
    }
  }

  return results;
}

/** Contains structuredPatch data for Write/Edit diff rendering. */
function collectStructuredPatchResults(sdkMessages: SDKNativeMessage[]): Map<string, unknown> {
  const results = new Map<string, unknown>();

  for (const sdkMsg of sdkMessages) {
    if (sdkMsg.type !== 'user' || !sdkMsg.toolUseResult) continue;

    const content = sdkMsg.message?.content;
    if (!content || typeof content === 'string') continue;

    for (const block of content) {
      if (block.type === 'tool_result' && block.tool_use_id) {
        results.set(block.tool_use_id, sdkMsg.toolUseResult);
      }
    }
  }

  return results;
}

interface AsyncSubagentResult {
  result: string;
  status: string;
}

/**
 * Collects full async subagent results from queue-operation enqueue entries.
 *
 * The SDK stores a `queue-operation` entry with `operation: 'enqueue'` and a `content`
 * field containing `<task-notification>` XML when a background agent completes.
 * The XML includes `<task-id>`, `<status>`, and `<result>` tags.
 *
 * @returns Map keyed by task-id (agentId) → full result + status
 */
export function collectAsyncSubagentResults(
  sdkMessages: SDKNativeMessage[]
): Map<string, AsyncSubagentResult> {
  const results = new Map<string, AsyncSubagentResult>();

  for (const sdkMsg of sdkMessages) {
    if (sdkMsg.type !== 'queue-operation') continue;
    if (sdkMsg.operation !== 'enqueue') continue;
    if (typeof sdkMsg.content !== 'string') continue;
    if (!sdkMsg.content.includes('<task-notification>')) continue;

    const taskId = extractXmlTag(sdkMsg.content, 'task-id');
    const status = extractXmlTag(sdkMsg.content, 'status');
    const result = extractXmlTag(sdkMsg.content, 'result');
    if (!taskId || !result) continue;

    results.set(taskId, {
      result,
      status: status ?? 'completed',
    });
  }

  return results;
}

export function extractXmlTag(content: string, tagName: string): string | null {
  const regex = new RegExp(`<${tagName}>\\s*([\\s\\S]*?)\\s*</${tagName}>`, 'i');
  const match = content.match(regex);
  if (!match || !match[1]) return null;
  const trimmed = match[1].trim();
  return trimmed.length > 0 ? trimmed : null;
}

/**
 * Checks if a user message is system-injected (not actual user input).
 * These include:
 * - Tool result messages (`toolUseResult` field)
 * - Skill prompt injections (`sourceToolUseID` field)
 * - Meta messages (`isMeta` field)
 * - Compact summary messages (SDK-generated context after /compact)
 * - Slash command invocations (`<command-name>`)
 * - Command stdout (`<local-command-stdout>`)
 * Such messages should be skipped as they're internal SDK communication.
 */
function isSystemInjectedMessage(sdkMsg: SDKNativeMessage): boolean {
  if (sdkMsg.type !== 'user') return false;
  if ('toolUseResult' in sdkMsg ||
      'sourceToolUseID' in sdkMsg ||
      !!sdkMsg.isMeta) {
    return true;
  }

  const text = extractTextContent(sdkMsg.message?.content);
  if (!text) return false;

  // Preserve user-invoked slash commands (have both <command-name> and <command-message>)
  if (text.includes('<command-name>') && text.includes('<command-message>')) return false;
  if (isCompactionCanceledStderr(text)) return false;

  // Filter system-injected messages
  if (text.startsWith('This session is being continued from a previous conversation')) return true;
  if (text.includes('<command-name>')) return true;
  if (text.includes('<local-command-stdout>') || text.includes('<local-command-stderr>')) return true;
  if (text.includes('<task-notification>')) return true;

  return false;
}

/**
 * After rewind + follow-up, the JSONL forms a tree via parentUuid. Walks backward
 * from the newest branch leaf to collect only active entries. Without branching,
 * resumeSessionAt truncates the linear chain at that UUID.
 */
export function filterActiveBranch(
  entries: SDKNativeMessage[],
  resumeSessionAt?: string
): SDKNativeMessage[] {
  if (entries.length === 0) return [];

  function isRealUserBranchChild(entry: SDKNativeMessage | undefined): boolean {
    return (
      !!entry &&
      entry.type === 'user' &&
      !('toolUseResult' in entry) &&
      !entry.isMeta &&
      !('sourceToolUseID' in entry)
    );
  }

  function isDirectRealUserBranchChild(
    parentUuid: string,
    entry: SDKNativeMessage | undefined
  ): boolean {
    return !!entry && entry.parentUuid === parentUuid && isRealUserBranchChild(entry);
  }

  // SDK may write duplicates around compaction, which inflates child counts
  const seen = new Set<string>();
  const deduped: SDKNativeMessage[] = [];
  for (const entry of entries) {
    if (entry.uuid) {
      if (seen.has(entry.uuid)) continue;
      seen.add(entry.uuid);
    }
    deduped.push(entry);
  }

  // Strip progress entries (subagent execution logs) from the tree.
  // They have uuid/parentUuid chains that create false branching.
  // Entries whose parentUuid points into a progress chain get reparented
  // to the chain's conversation-level ancestor.
  const progressUuids = new Set<string>();
  const progressParentOf = new Map<string, string | null>();
  for (const entry of deduped) {
    if ((entry.type as string) === 'progress' && entry.uuid) {
      progressUuids.add(entry.uuid);
      progressParentOf.set(entry.uuid, entry.parentUuid ?? null);
    }
  }

  function resolveParent(parentUuid: string | null | undefined): string | null | undefined {
    if (!parentUuid) return parentUuid;
    let cur: string | null = parentUuid;
    let guard = progressUuids.size + 1;
    while (cur && progressUuids.has(cur)) {
      if (--guard < 0) break;
      cur = progressParentOf.get(cur) ?? null;
    }
    return cur;
  }

  // Build maps from conversation entries only (excluding progress)
  const convEntries: SDKNativeMessage[] = [];
  for (const entry of deduped) {
    if ((entry.type as string) === 'progress') continue;
    convEntries.push(entry);
  }

  const byUuid = new Map<string, SDKNativeMessage>();
  const childrenOf = new Map<string, Set<string>>();

  for (const entry of convEntries) {
    if (entry.uuid) {
      byUuid.set(entry.uuid, entry);
    }
    const effectiveParent = resolveParent(entry.parentUuid) ?? null;
    if (effectiveParent && entry.uuid) {
      let children = childrenOf.get(effectiveParent);
      if (!children) {
        children = new Set();
        childrenOf.set(effectiveParent, children);
      }
      children.add(entry.uuid);
    }
  }

  function findLatestLeaf(): SDKNativeMessage | undefined {
    for (let i = convEntries.length - 1; i >= 0; i--) {
      const uuid = convEntries[i].uuid;
      if (uuid && !childrenOf.has(uuid)) {
        return convEntries[i];
      }
    }
    return undefined;
  }

  const latestLeaf = findLatestLeaf();
  const latestBranchUuids = new Set<string>();
  const activeChildOf = new Map<string, string>();

  let currentLatest: SDKNativeMessage | undefined = latestLeaf;
  while (currentLatest?.uuid) {
    latestBranchUuids.add(currentLatest.uuid);
    const ep = resolveParent(currentLatest.parentUuid);
    if (ep) {
      activeChildOf.set(ep, currentLatest.uuid);
    }
    currentLatest = ep ? byUuid.get(ep) : undefined;
  }

  const conversationContentCache = new Map<string, boolean>();
  function hasConversationContent(uuid: string): boolean {
    const cached = conversationContentCache.get(uuid);
    if (cached !== undefined) return cached;

    const entry = byUuid.get(uuid);
    let result = false;

    if (entry?.type === 'assistant') {
      result = true;
    } else if (entry?.type === 'user' && !entry.isMeta && !('sourceToolUseID' in entry)) {
      result = true;
    } else {
      const children = childrenOf.get(uuid);
      if (children) {
        for (const childUuid of children) {
          if (hasConversationContent(childUuid)) {
            result = true;
            break;
          }
        }
      }
    }

    conversationContentCache.set(uuid, result);
    return result;
  }

  // A real rewind shows up along the latest branch as:
  // 1. at least one genuine user child from a parent on that branch, and
  // 2. another sibling subtree with conversation content that the latest branch did not take.
  // This catches rewinds where the abandoned path continues through assistant/tool nodes,
  // while still ignoring parallel tool calls that never create a user branch.
  const hasBranching = [...latestBranchUuids].some(uuid => {
    const children = childrenOf.get(uuid);
    if (!children || children.size <= 1) return false;

    const activeChildUuid = activeChildOf.get(uuid);
    let sawRealUserChild = false;
    let sawAlternateConversationChild = false;

    for (const childUuid of children) {
      const child = byUuid.get(childUuid);
      if (isDirectRealUserBranchChild(uuid, child)) {
        sawRealUserChild = true;
      }
      if (childUuid !== activeChildUuid && hasConversationContent(childUuid)) {
        sawAlternateConversationChild = true;
      }
      if (sawRealUserChild && sawAlternateConversationChild) {
        return true;
      }
    }

    return false;
  });

  let leaf: SDKNativeMessage | undefined;

  if (hasBranching) {
    leaf = latestLeaf;

    // When resumeSessionAt is also set (rewind on the latest branch without follow-up),
    // truncate at that point instead of using the full branch leaf
    if (resumeSessionAt && leaf?.uuid && byUuid.has(resumeSessionAt)) {
      // Check if resumeSessionAt is an ancestor of the leaf — if so, truncate there
      let current: SDKNativeMessage | undefined = leaf;
      while (current?.uuid) {
        if (current.uuid === resumeSessionAt) {
          leaf = current;
          break;
        }
        const ep = resolveParent(current.parentUuid);
        current = ep ? byUuid.get(ep) : undefined;
      }
    }
  } else if (resumeSessionAt) {
    leaf = byUuid.get(resumeSessionAt);
  } else {
    return convEntries;
  }

  if (!leaf || !leaf.uuid) return convEntries;

  const activeUuids = new Set<string>();
  let current: SDKNativeMessage | undefined = leaf;
  while (current?.uuid) {
    activeUuids.add(current.uuid);
    const ep = resolveParent(current.parentUuid);
    current = ep ? byUuid.get(ep) : undefined;
  }

  // When no real branching was detected but resumeSessionAt truncated,
  // the active set only has the chain up to the leaf. For no-branching
  // with truncation, this is correct. For branching, we also need to
  // include sibling entries that are part of the same turn (parallel tool
  // calls, tool results from the same parent) and their non-branching
  // descendants.
  if (hasBranching) {
    // Seed: collect non-branch siblings of active nodes (parallel tool calls,
    // tool results) that the main ancestor walk didn't pick up.
    const ancestorUuids = [...activeUuids];
    const pending: string[] = [];

    for (const uuid of ancestorUuids) {
      const children = childrenOf.get(uuid);
      if (!children || children.size <= 1) continue;
      const activeChildUuid = activeChildOf.get(uuid);
      if (activeChildUuid && isDirectRealUserBranchChild(uuid, byUuid.get(activeChildUuid))) {
        continue;
      }
      for (const childUuid of children) {
        if (activeUuids.has(childUuid)) continue;
        const child = byUuid.get(childUuid);
        if (!child || isRealUserBranchChild(child)) continue;
        activeUuids.add(childUuid);
        pending.push(childUuid);
      }
    }

    // BFS: include non-branching descendants of seeded siblings
    while (pending.length > 0) {
      const parentUuid = pending.pop()!;
      const children = childrenOf.get(parentUuid);
      if (!children) continue;

      for (const childUuid of children) {
        if (activeUuids.has(childUuid)) continue;
        const child = byUuid.get(childUuid);
        if (!child || isRealUserBranchChild(child)) continue;
        activeUuids.add(childUuid);
        pending.push(childUuid);
      }
    }
  }

  // O(n) sweep: include no-uuid entries only if both nearest uuid neighbors are active
  const n = convEntries.length;
  const prevIsActive = new Array<boolean>(n);
  const nextIsActive = new Array<boolean>(n);

  let lastPrevActive = false;
  for (let i = 0; i < n; i++) {
    if (convEntries[i].uuid) {
      lastPrevActive = activeUuids.has(convEntries[i].uuid!);
    }
    prevIsActive[i] = lastPrevActive;
  }

  let lastNextActive = false;
  for (let i = n - 1; i >= 0; i--) {
    if (convEntries[i].uuid) {
      lastNextActive = activeUuids.has(convEntries[i].uuid!);
    }
    nextIsActive[i] = lastNextActive;
  }

  return convEntries.filter((entry, idx) => {
    if (entry.uuid) return activeUuids.has(entry.uuid);
    return prevIsActive[idx] && nextIsActive[idx];
  });
}

export interface SDKSessionLoadResult {
  messages: ChatMessage[];
  skippedLines: number;
  error?: string;
}

/**
 * Merges content from a source assistant message into a target message.
 * Used to combine multiple SDK messages from the same API turn (same requestId).
 */
function mergeAssistantMessage(target: ChatMessage, source: ChatMessage): void {
  // Merge text content (with separator if both have content)
  if (source.content) {
    if (target.content) {
      target.content = target.content + '\n\n' + source.content;
    } else {
      target.content = source.content;
    }
  }

  // Merge tool calls
  if (source.toolCalls) {
    target.toolCalls = [...(target.toolCalls || []), ...source.toolCalls];
  }

  // Merge content blocks
  if (source.contentBlocks) {
    target.contentBlocks = [...(target.contentBlocks || []), ...source.contentBlocks];
  }

  if (source.sdkAssistantUuid) {
    target.sdkAssistantUuid = source.sdkAssistantUuid;
  }
}

/**
 * Loads and converts all messages from an SDK native session.
 *
 * Uses three-pass approach:
 * 1. First pass: collect all tool_result and toolUseResult from all messages
 * 2. Second pass: convert messages and attach results to tool calls
 * 3. Third pass: attach diff data from toolUseResults to tool calls
 *
 * Consecutive assistant messages with the same requestId are merged into one,
 * as the SDK stores multiple JSONL entries for a single API turn (text, then tool_use, etc).
 *
 * @param vaultPath - The vault's absolute path
 * @param sessionId - The session ID
 * @returns Result object with messages, skipped line count, and any error
 */
/**
 * Extracts the agentId from an Agent tool's toolUseResult (async launch shape).
 * The SDK stores `{ isAsync: true, agentId: '...' }` on the tool result.
 */
export function extractAgentIdFromToolUseResult(toolUseResult: unknown): string | null {
  if (!toolUseResult || typeof toolUseResult !== 'object') return null;
  const record = toolUseResult as Record<string, unknown>;

  const directAgentId = record.agentId ?? record.agent_id;
  if (typeof directAgentId === 'string' && directAgentId.length > 0) {
    return directAgentId;
  }

  const data = record.data;
  if (data && typeof data === 'object') {
    const nested = data as Record<string, unknown>;
    const nestedAgentId = nested.agent_id ?? nested.agentId;
    if (typeof nestedAgentId === 'string' && nestedAgentId.length > 0) {
      return nestedAgentId;
    }
  }

  return null;
}

export type ResolvedAsyncStatus = Exclude<AsyncSubagentStatus, 'pending'>;

/**
 * Both the streaming layer (SubagentManager) and the session-load layer (buildAsyncSubagentInfo)
 * need to interpret the same SDK response shapes — this centralizes that logic.
 */
export function resolveToolUseResultStatus(
  toolUseResult: unknown,
  fallbackStatus: ResolvedAsyncStatus
): ResolvedAsyncStatus {
  if (!toolUseResult || typeof toolUseResult !== 'object') return fallbackStatus;

  const record = toolUseResult as Record<string, unknown>;
  const rawStatus = record.retrieval_status ?? record.status;
  const status = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : '';

  if (status === 'error') return 'error';
  if (status === 'completed' || status === 'success') return 'completed';
  if (record.isAsync === true || status === 'async_launched') return 'running';

  return fallbackStatus;
}

/**
 * Builds a SubagentInfo for an async Agent tool call from stored data.
 * Uses the toolUseResult (launch shape → agentId) and queue-operation results (full result).
 */
function buildAsyncSubagentInfo(
  toolCall: ToolCallInfo,
  toolUseResult: unknown,
  asyncResults: Map<string, AsyncSubagentResult>
): SubagentInfo | null {
  const agentId = extractAgentIdFromToolUseResult(toolUseResult);
  if (!agentId) return null;

  const queueResult = asyncResults.get(agentId);
  const description = (toolCall.input?.description as string) || 'Background task';
  const prompt = (toolCall.input?.prompt as string) || '';

  // Determine final result: prefer queue-operation result (full), fall back to tool_result content
  const finalResult = queueResult?.result ?? toolCall.result;
  let toolCallFallback: ResolvedAsyncStatus = 'running';
  if (toolCall.status === 'error') toolCallFallback = 'error';
  else if (toolCall.status === 'completed') toolCallFallback = 'completed';

  // Queue-operation status reflects the actual async task outcome and must win over
  // the Task tool_result block, whose status only describes launch success.
  const status = queueResult
    ? resolveToolUseResultStatus({ status: queueResult.status }, 'completed')
    : resolveToolUseResultStatus(toolUseResult, toolCallFallback);

  const taskStatus = status === 'orphaned' ? 'error' : status;

  return {
    id: toolCall.id,
    description,
    prompt,
    mode: 'async',
    isExpanded: false,
    status: taskStatus,
    toolCalls: [],
    asyncStatus: status,
    agentId,
    result: finalResult,
  };
}

export async function loadSDKSessionMessages(
  vaultPath: string,
  sessionId: string,
  resumeSessionAt?: string
): Promise<SDKSessionLoadResult> {
  const result = await readSDKSession(vaultPath, sessionId);

  if (result.error) {
    return { messages: [], skippedLines: result.skippedLines, error: result.error };
  }

  const filteredEntries = filterActiveBranch(result.messages, resumeSessionAt);

  const toolResults = collectToolResults(filteredEntries);
  const toolUseResults = collectStructuredPatchResults(filteredEntries);
  const asyncSubagentResults = collectAsyncSubagentResults(filteredEntries);

  const chatMessages: ChatMessage[] = [];
  let pendingAssistant: ChatMessage | null = null;

  // Merge consecutive assistant messages until an actual user message appears
  for (const sdkMsg of filteredEntries) {
    if (isSystemInjectedMessage(sdkMsg)) continue;

    // Skip synthetic assistant messages (e.g., "No response requested." after /compact)
    if (sdkMsg.type === 'assistant' && sdkMsg.message?.model === '<synthetic>') continue;

    const chatMsg = parseSDKMessageToChat(sdkMsg, toolResults);
    if (!chatMsg) continue;

    if (chatMsg.role === 'assistant') {
      // compact_boundary must not merge with previous assistant (it's a standalone separator)
      const isCompactBoundary = chatMsg.contentBlocks?.some(b => b.type === 'compact_boundary');
      if (isCompactBoundary) {
        if (pendingAssistant) {
          chatMessages.push(pendingAssistant);
        }
        chatMessages.push(chatMsg);
        pendingAssistant = null;
      } else if (pendingAssistant) {
        mergeAssistantMessage(pendingAssistant, chatMsg);
      } else {
        pendingAssistant = chatMsg;
      }
    } else {
      if (pendingAssistant) {
        chatMessages.push(pendingAssistant);
        pendingAssistant = null;
      }
      chatMessages.push(chatMsg);
    }
  }

  if (pendingAssistant) {
    chatMessages.push(pendingAssistant);
  }

  if (toolUseResults.size > 0) {
    for (const msg of chatMessages) {
      if (msg.role !== 'assistant' || !msg.toolCalls) continue;
      for (const toolCall of msg.toolCalls) {
        const toolUseResult = toolUseResults.get(toolCall.id);
        if (!toolUseResult) continue;
        if (!toolCall.diffData) {
          toolCall.diffData = extractDiffData(toolUseResult, toolCall);
        }
        if (toolCall.name === TOOL_ASK_USER_QUESTION) {
          const answers =
            extractResolvedAnswers(toolUseResult) ??
            extractResolvedAnswersFromResultText(toolCall.result);
          if (answers) toolCall.resolvedAnswers = answers;
        }
      }
    }
  }

  for (const msg of chatMessages) {
    if (msg.role !== 'assistant' || !msg.toolCalls) continue;
    for (const toolCall of msg.toolCalls) {
      if (toolCall.name !== TOOL_ASK_USER_QUESTION || toolCall.resolvedAnswers) continue;
      const answers = extractResolvedAnswersFromResultText(toolCall.result);
      if (answers) toolCall.resolvedAnswers = answers;
    }
  }

  // Build SubagentInfo for async Agent tool calls from toolUseResult + queue-operation data
  if (toolUseResults.size > 0 || asyncSubagentResults.size > 0) {
    const sidecarLoads: Array<{ subagent: SubagentInfo; promise: Promise<ToolCallInfo[]> }> = [];

    for (const msg of chatMessages) {
      if (msg.role !== 'assistant' || !msg.toolCalls) continue;
      for (const toolCall of msg.toolCalls) {
        if (!isSubagentToolName(toolCall.name)) continue;
        if (toolCall.subagent) continue;
        if (toolCall.input?.run_in_background !== true) continue;

        const toolUseResult = toolUseResults.get(toolCall.id);
        const subagent = buildAsyncSubagentInfo(
          toolCall,
          toolUseResult,
          asyncSubagentResults
        );
        if (subagent) {
          toolCall.subagent = subagent;
          if (subagent.result !== undefined) {
            toolCall.result = subagent.result;
          }
          toolCall.status = subagent.status;

          // Load tool calls from subagent sidecar JSONL in parallel
          if (subagent.agentId && isValidAgentId(subagent.agentId)) {
            sidecarLoads.push({
              subagent,
              promise: loadSubagentToolCalls(vaultPath, sessionId, subagent.agentId),
            });
          }
        }
      }
    }

    // Hydrate subagent tool calls from sidecar files
    if (sidecarLoads.length > 0) {
      const results = await Promise.all(sidecarLoads.map(s => s.promise));
      for (let i = 0; i < sidecarLoads.length; i++) {
        const toolCalls = results[i];
        if (toolCalls.length > 0) {
          sidecarLoads[i].subagent.toolCalls = toolCalls;
        }
      }
    }
  }

  chatMessages.sort((a, b) => a.timestamp - b.timestamp);

  return { messages: chatMessages, skippedLines: result.skippedLines };
}
