import * as fs from 'fs';
import * as readline from 'readline';
import { join } from 'path';
import { Effect } from 'effect';
import { getClaudeSessionPath, getClaude2SessionPath, findCodexSessionPath, findKimiSessionPath, getOllamaSessionPath, getDeepseekSessionPath } from '@cockpit/shared-utils';
import { handler, ok, parseJsonRaw } from '@cockpit/effect-runtime/server';
import {
  AppError,
  NotFoundError,
  ValidationError,
} from '@cockpit/effect-core';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

interface TokenUsage {
  input_tokens?: number;
  output_tokens?: number;
  cache_creation_input_tokens?: number;
  cache_read_input_tokens?: number;
}

interface TranscriptMessage {
  type: string;
  message?: {
    role?: string;
    content?: Array<{
      type: string;
      text?: string;
      name?: string;
      id?: string;
      input?: Record<string, unknown>;
      tool_use_id?: string;
      content?: string;
      is_error?: boolean;
      source?: {
        type: string;
        media_type: string;
        data: string;
      };
    }>;
    usage?: TokenUsage;
  };
  uuid?: string;
  sessionId?: string;
  timestamp?: string;
  toolUseResult?: {
    stdout?: string;
    stderr?: string;
  };
}

interface MessageImage {
  type: 'base64';
  media_type: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
  data: string;
}

interface ChatMessage {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  images?: MessageImage[];
  timestamp?: string;
  toolCalls?: Array<{
    id: string;
    name: string;
    input: Record<string, unknown>;
    result?: string;
    isLoading: boolean;
  }>;
}


// File fingerprint: mtime + size, lightweight check for file changes
function getFileFingerprint(filePath: string): string {
  const stat = fs.statSync(filePath);
  return `${stat.mtimeMs}-${stat.size}`;
}

interface SessionByPathBody {
  cwd?: string;
  sessionId?: string;
  // When set, return the transcript of the subagent spawned by this Agent/Task
  // tool call instead of the main session (new-format `<sessionId>/subagents/` dir).
  toolUseId?: string;
  // Workflow drill-in. When `workflowId` is set, return the workflow run journal
  // (`<sessionId>/workflows/<workflowId>.json`); when `workflowAgentId` is also
  // set, return that workflow subagent's transcript
  // (`<sessionId>/subagents/workflows/<workflowId>/agent-<workflowAgentId>.jsonl`).
  workflowId?: string;
  workflowAgentId?: string;
  limit?: number;
  beforeTurnIndex?: number;
  ifFingerprint?: string;
}

// Subagent meta sidecar (agent-<id>.meta.json next to agent-<id>.jsonl)
interface SubagentMeta {
  agentType?: string;
  description?: string;
  toolUseId?: string;
}

// One agent's progress entry inside a workflow run journal.
interface WorkflowAgentEntry {
  type?: string;
  index?: number;
  label?: string;
  phaseIndex?: number;
  phaseTitle?: string;
  agentId?: string;
  model?: string;
  state?: string;
  tokens?: number;
  toolCalls?: number;
  durationMs?: number;
  lastToolName?: string;
  lastToolSummary?: string;
  promptPreview?: string;
  resultPreview?: string;
}

// Workflow run journal (`workflows/<runId>.json`). Only the fields the
// drill-in UI needs are typed; the raw file also carries `script`, `logs`,
// full `result`, etc. which we deliberately do NOT forward to the client.
interface WorkflowJournal {
  runId?: string;
  workflowName?: string;
  status?: string;
  durationMs?: number;
  agentCount?: number;
  totalTokens?: number;
  totalToolCalls?: number;
  startTime?: number;
  phases?: Array<{ title?: string; detail?: string }>;
  summary?: string;
  workflowProgress?: WorkflowAgentEntry[];
}

// Locate the subagent transcript spawned by a given tool_use id.
// Subagents live in `<sessionDir>/<sessionId>/subagents/agent-<id>.jsonl`
// with a meta sidecar carrying the spawning toolUseId.
function findSubagentTranscript(
  sessionPath: string,
  toolUseId: string
): { transcriptPath: string; meta: SubagentMeta } | null {
  const subagentsDir = join(sessionPath.replace(/\.jsonl$/, ''), 'subagents');
  if (!fs.existsSync(subagentsDir)) return null;
  for (const file of fs.readdirSync(subagentsDir)) {
    if (!file.endsWith('.meta.json')) continue;
    try {
      const meta = JSON.parse(
        fs.readFileSync(join(subagentsDir, file), 'utf-8')
      ) as SubagentMeta;
      if (meta.toolUseId !== toolUseId) continue;
      const transcriptPath = join(subagentsDir, file.replace(/\.meta\.json$/, '.jsonl'));
      if (fs.existsSync(transcriptPath)) return { transcriptPath, meta };
    } catch {
      // Skip unreadable meta files
    }
  }
  return null;
}

// Path of a workflow run journal: `<sessionDir>/workflows/<runId>.json`.
function workflowJournalPath(sessionPath: string, workflowId: string): string {
  return join(sessionPath.replace(/\.jsonl$/, ''), 'workflows', `${workflowId}.json`);
}

// Path of a single workflow subagent transcript:
// `<sessionDir>/subagents/workflows/<runId>/agent-<agentId>.jsonl`.
function workflowAgentTranscriptPath(
  sessionPath: string,
  workflowId: string,
  agentId: string
): string {
  return join(
    sessionPath.replace(/\.jsonl$/, ''),
    'subagents',
    'workflows',
    workflowId,
    `agent-${agentId}.jsonl`
  );
}

// Trim a raw journal down to the fields the drill-in UI renders. Drops
// `script`, `logs`, and the full `result` blob; keeps bounded previews only.
function trimWorkflowJournal(journal: WorkflowJournal) {
  const agents = (journal.workflowProgress ?? [])
    .filter((e) => e.type === 'workflow_agent')
    .map((e) => ({
      index: e.index,
      label: e.label,
      phaseIndex: e.phaseIndex,
      phaseTitle: e.phaseTitle,
      agentId: e.agentId,
      model: e.model,
      state: e.state,
      tokens: e.tokens,
      toolCalls: e.toolCalls,
      durationMs: e.durationMs,
      lastToolName: e.lastToolName,
      lastToolSummary: e.lastToolSummary,
      promptPreview: e.promptPreview,
      resultPreview: e.resultPreview,
    }));
  return {
    runId: journal.runId,
    workflowName: journal.workflowName,
    status: journal.status,
    durationMs: journal.durationMs,
    agentCount: journal.agentCount,
    totalTokens: journal.totalTokens,
    totalToolCalls: journal.totalToolCalls,
    startTime: journal.startTime,
    phases: journal.phases,
    summary: journal.summary,
    agents,
  };
}

export const POST = handler((req) =>
  Effect.gen(function* () {
    const body = (yield* parseJsonRaw(req)) as SessionByPathBody;
    const { cwd, sessionId, toolUseId, workflowId, workflowAgentId, limit, beforeTurnIndex, ifFingerprint } = body;
    if (!cwd || !sessionId) {
      return yield* Effect.fail(
        new ValidationError({
          field: !cwd ? 'cwd' : 'sessionId',
          reason: 'missing',
        })
      );
    }

    // Resolve session file across 6 engines (claude/claude2/deepseek/codex/kimi/ollama)
    const resolved = yield* Effect.sync(() => resolveSessionPath(cwd, sessionId));
    if (!resolved) {
      return yield* Effect.fail(
        new NotFoundError({ resource: 'session', id: sessionId })
      );
    }
    const { sessionPath, engine } = resolved;

    // Subagent transcript branch: same parser/fingerprint flow on the agent jsonl
    if (toolUseId) {
      if (!/^[A-Za-z0-9_-]+$/.test(toolUseId)) {
        return yield* Effect.fail(
          new ValidationError({ field: 'toolUseId', reason: 'invalid' })
        );
      }
      const sub = yield* Effect.sync(() => findSubagentTranscript(sessionPath, toolUseId));
      if (!sub) {
        return yield* Effect.fail(
          new NotFoundError({ resource: 'subagent', id: toolUseId })
        );
      }
      const subFingerprint = getFileFingerprint(sub.transcriptPath);
      if (ifFingerprint && ifFingerprint === subFingerprint) {
        return ok({ notModified: true, fingerprint: subFingerprint });
      }
      const subResult = yield* Effect.tryPromise({
        try: () => parseTranscriptFile(sub.transcriptPath),
        catch: (cause) =>
          new AppError({ message: 'parseTranscriptFile failed', cause }),
      });
      return ok({
        messages: subResult.messages,
        subagent: { agentType: sub.meta.agentType, description: sub.meta.description },
        fingerprint: subFingerprint,
      });
    }

    // Workflow drill-in branch: run journal, or a single workflow subagent's
    // transcript when workflowAgentId is also supplied. Both ids are
    // whitelisted to keep the file path inside the session dir.
    if (workflowId) {
      if (!/^wf_[A-Za-z0-9_-]+$/.test(workflowId)) {
        return yield* Effect.fail(
          new ValidationError({ field: 'workflowId', reason: 'invalid' })
        );
      }

      if (workflowAgentId) {
        if (!/^[A-Za-z0-9_-]+$/.test(workflowAgentId)) {
          return yield* Effect.fail(
            new ValidationError({ field: 'workflowAgentId', reason: 'invalid' })
          );
        }
        const agentPath = workflowAgentTranscriptPath(sessionPath, workflowId, workflowAgentId);
        if (!fs.existsSync(agentPath)) {
          return yield* Effect.fail(
            new NotFoundError({ resource: 'workflowAgent', id: workflowAgentId })
          );
        }
        const agentFingerprint = getFileFingerprint(agentPath);
        if (ifFingerprint && ifFingerprint === agentFingerprint) {
          return ok({ notModified: true, fingerprint: agentFingerprint });
        }
        const agentResult = yield* Effect.tryPromise({
          try: () => parseTranscriptFile(agentPath),
          catch: (cause) =>
            new AppError({ message: 'parseTranscriptFile failed', cause }),
        });
        return ok({ messages: agentResult.messages, fingerprint: agentFingerprint });
      }

      const journalPath = workflowJournalPath(sessionPath, workflowId);
      if (!fs.existsSync(journalPath)) {
        return yield* Effect.fail(
          new NotFoundError({ resource: 'workflow', id: workflowId })
        );
      }
      const journalFingerprint = getFileFingerprint(journalPath);
      if (ifFingerprint && ifFingerprint === journalFingerprint) {
        return ok({ notModified: true, fingerprint: journalFingerprint });
      }
      const workflow = yield* Effect.try({
        try: () =>
          trimWorkflowJournal(
            JSON.parse(fs.readFileSync(journalPath, 'utf-8')) as WorkflowJournal
          ),
        catch: (cause) =>
          new AppError({ message: 'readWorkflowJournal failed', cause }),
      });
      return ok({ workflow, fingerprint: journalFingerprint });
    }

    const fingerprint = getFileFingerprint(sessionPath);
    if (ifFingerprint && ifFingerprint === fingerprint) {
      return ok({ notModified: true, fingerprint });
    }

    const parseResult = yield* Effect.tryPromise({
      try: async () => {
        if (engine === 'codex') return parseCodexTranscriptFile(sessionPath);
        if (engine === 'kimi') return parseKimiTranscriptFile(sessionPath);
        if (engine === 'ollama') {
          const r = await parseTranscriptFile(sessionPath, limit, beforeTurnIndex);
          if (r.messages.length === 0) return parseOllamaTranscriptFile(sessionPath);
          return r;
        }
        return parseTranscriptFile(sessionPath, limit, beforeTurnIndex);
      },
      catch: (cause) =>
        new AppError({ message: 'parseTranscriptFile failed', cause }),
    });

    const { messages, title, usage } = parseResult;
    const totalTurns = 'totalTurns' in parseResult ? parseResult.totalTurns : 0;
    const hasMore = 'hasMore' in parseResult ? parseResult.hasMore : false;
    return ok({
      messages,
      sessionId,
      title,
      usage,
      totalTurns,
      hasMore,
      fingerprint,
      // Authoritative engine for this session, resolved by file location across
      // all 6 engines. Mobile (/m) uses this to send on the session's native
      // engine — more reliable than the optional global-state engine field.
      engine,
    });
  })
);

function resolveSessionPath(
  cwd: string,
  sessionId: string
): {
  sessionPath: string;
  engine: 'claude' | 'claude2' | 'codex' | 'kimi' | 'ollama' | 'deepseek';
} | null {
  const sessionPath = getClaudeSessionPath(cwd, sessionId);
  if (fs.existsSync(sessionPath)) {
    return { sessionPath, engine: 'claude' };
  }
  const claude2Path = getClaude2SessionPath(cwd, sessionId);
  if (fs.existsSync(claude2Path)) {
    return { sessionPath: claude2Path, engine: 'claude2' };
  }
  const deepseekPath = getDeepseekSessionPath(cwd, sessionId);
  if (fs.existsSync(deepseekPath)) {
    return { sessionPath: deepseekPath, engine: 'deepseek' };
  }
  const codexPath = findCodexSessionPath(sessionId);
  if (codexPath) {
    return { sessionPath: codexPath, engine: 'codex' };
  }
  const kimiPath = findKimiSessionPath(sessionId);
  if (kimiPath) {
    return { sessionPath: kimiPath, engine: 'kimi' };
  }
  const ollamaPath = getOllamaSessionPath(cwd, sessionId);
  if (fs.existsSync(ollamaPath)) {
    return { sessionPath: ollamaPath, engine: 'ollama' };
  }
  return null;
}

// Filter command tags and extract meaningful content
function filterCommandTags(text: string): string {
  // First try to extract command-args
  const argsMatch = text.match(/<command-args>([^<]*)<\/command-args>/);
  if (argsMatch && argsMatch[1].trim()) {
    return argsMatch[1].trim();
  }
  // If no args, try to extract command-name
  const nameMatch = text.match(/<command-name>([^<]*)<\/command-name>/);
  if (nameMatch && nameMatch[1].trim()) {
    return nameMatch[1].trim();
  }
  // Filter all command tags
  let filtered = text.replace(/<command-message>[^<]*<\/command-message>/g, '');
  filtered = filtered.replace(/<command-name>[^<]*<\/command-name>/g, '');
  filtered = filtered.replace(/<command-args>[^<]*<\/command-args>/g, '');
  filtered = filtered.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '');
  filtered = filtered.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '');
  return filtered.trim();
}

// Generate a title (no truncation, preserve full content)
function generateTitle(summary: string, userMessages: string[]): string {
  if (summary) return summary;

  let commandName = '';
  for (const msg of userMessages) {
    const filtered = filterCommandTags(msg);
    if (!filtered) continue;

    // If it's a command (starts with /), save the command name and continue to the next message
    if (filtered.startsWith('/') && !commandName) {
      commandName = filtered;
      continue;
    }

    // If a command name was saved before, combine them
    if (commandName) {
      return `${commandName} ${filtered}`;
    }

    // Regular message used directly as the title
    return filtered;
  }

  // If there is only a command name with no subsequent messages, show the command name
  if (commandName) return commandName;

  return 'Untitled Session';
}

async function parseTranscriptFile(
  filePath: string,
  limit?: number,
  beforeTurnIndex?: number
): Promise<{ messages: ChatMessage[]; title: string; usage?: TokenUsage; totalTurns: number; hasMore: boolean }> {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  const rawMessages: TranscriptMessage[] = [];
  let summary = '';
  const userTextMessages: string[] = [];
  let lastUsage: TokenUsage | undefined;

  for await (const line of rl) {
    try {
      const obj = JSON.parse(line) as TranscriptMessage & { summary?: string; isMeta?: boolean };
      if (obj.type === 'user' || obj.type === 'assistant') {
        // Deduplicate: skip user messages with identical content within 1s of the previous one
        // (SDK resume + prompt may write duplicate user entries)
        if (obj.type === 'user' && rawMessages.length > 0) {
          const prev = rawMessages[rawMessages.length - 1];
          if (
            prev.type === 'user' &&
            prev.timestamp && obj.timestamp &&
            Math.abs(new Date(obj.timestamp).getTime() - new Date(prev.timestamp).getTime()) < 1000 &&
            JSON.stringify(prev.message?.content) === JSON.stringify(obj.message?.content)
          ) {
            continue; // skip duplicate
          }
        }
        rawMessages.push(obj);

        // Collect the usage of the last assistant message
        if (obj.type === 'assistant' && obj.message?.usage) {
          lastUsage = obj.message.usage;
        }

        // Collect user text messages for title generation
        if (obj.type === 'user' && !obj.isMeta && obj.message?.content) {
          const content = obj.message.content;
          if (typeof content === 'string') {
            userTextMessages.push(content);
          } else if (Array.isArray(content)) {
            const textBlocks = content.filter((b) => b.type === 'text');
            for (const block of textBlocks) {
              if (block.text) userTextMessages.push(block.text);
            }
          }
        }
      }
      // Collect summary
      if (obj.type === 'summary' && obj.summary) {
        summary = obj.summary;
      }
    } catch {
      // Ignore lines with parse errors
    }
  }

  // Convert message format (full set)
  const allMessages = convertToChatMessages(rawMessages);
  const title = generateTitle(summary, userTextMessages);

  // Count turns: one turn = one user message + the corresponding assistant message
  // Simplified here: each user message starts a new turn
  const turns: ChatMessage[][] = [];
  let currentTurn: ChatMessage[] = [];

  for (const msg of allMessages) {
    if (msg.role === 'user') {
      if (currentTurn.length > 0) {
        turns.push(currentTurn);
      }
      currentTurn = [msg];
    } else {
      currentTurn.push(msg);
    }
  }
  if (currentTurn.length > 0) {
    turns.push(currentTurn);
  }

  const totalTurns = turns.length;

  // If there are no pagination params, return all messages
  if (limit === undefined) {
    return { messages: allMessages, title, usage: lastUsage, totalTurns, hasMore: false };
  }

  // Pagination logic: take `limit` turns going back from beforeTurnIndex
  const endIndex = beforeTurnIndex !== undefined ? beforeTurnIndex : totalTurns;
  const startIndex = Math.max(0, endIndex - limit);
  const hasMore = startIndex > 0;

  // Extract the specified range of turns and flatten into a message array
  const selectedTurns = turns.slice(startIndex, endIndex);
  const messages = selectedTurns.flat();

  return { messages, title, usage: lastUsage, totalTurns, hasMore };
}

function convertToChatMessages(rawMessages: TranscriptMessage[]): ChatMessage[] {
  const chatMessages: ChatMessage[] = [];
  let currentAssistantMessage: ChatMessage | null = null;
  const toolResults = new Map<string, string>();

  // First pass: collect all tool results
  for (const msg of rawMessages) {
    if (msg.type === 'user' && msg.message?.content && Array.isArray(msg.message.content)) {
      for (const block of msg.message.content) {
        if (block.type === 'tool_result' && block.tool_use_id) {
          toolResults.set(block.tool_use_id, block.content || '');
        }
      }
    }
  }

  // Second pass: build the message list
  for (const msg of rawMessages) {
    // Handle user text messages
    if (msg.type === 'user' && msg.message?.role === 'user' && msg.message?.content) {
      const content = msg.message.content;
      if (typeof content === 'string') {
        if (currentAssistantMessage) {
          chatMessages.push(currentAssistantMessage);
          currentAssistantMessage = null;
        }

        const userMessage: ChatMessage = {
          id: msg.uuid || `user-${Date.now()}`,
          role: 'user',
          content: content,
          timestamp: msg.timestamp,
        };
        chatMessages.push(userMessage);
        continue;
      }

      if (!Array.isArray(content)) continue;

      const textBlocks = content.filter((b) => b.type === 'text');
      const imageBlocks = content.filter((b) => b.type === 'image' && b.source);

      if (textBlocks.length > 0 || imageBlocks.length > 0) {
        if (currentAssistantMessage) {
          chatMessages.push(currentAssistantMessage);
          currentAssistantMessage = null;
        }

        const userMessage: ChatMessage = {
          id: msg.uuid || `user-${Date.now()}`,
          role: 'user',
          content: textBlocks.map((b) => b.text || '').join('\n'),
          timestamp: msg.timestamp,
        };

        if (imageBlocks.length > 0) {
          userMessage.images = imageBlocks.map((b) => ({
            type: 'base64' as const,
            media_type: (b.source?.media_type || 'image/png') as MessageImage['media_type'],
            data: b.source?.data || '',
          }));
        }

        chatMessages.push(userMessage);
      }
    }

    // Handle assistant messages
    if (msg.type === 'assistant' && msg.message?.content) {
      const content = msg.message.content;
      if (!Array.isArray(content)) continue;

      const textBlocks = content.filter((b) => b.type === 'text');
      const toolBlocks = content.filter((b) => b.type === 'tool_use');

      if (textBlocks.length > 0) {
        if (currentAssistantMessage) {
          currentAssistantMessage.content += textBlocks.map((b) => b.text || '').join('\n');
        } else {
          currentAssistantMessage = {
            id: msg.uuid || `assistant-${Date.now()}`,
            role: 'assistant',
            content: textBlocks.map((b) => b.text || '').join('\n'),
            timestamp: msg.timestamp,
            toolCalls: [],
          };
        }
      }

      if (toolBlocks.length > 0) {
        if (!currentAssistantMessage) {
          currentAssistantMessage = {
            id: msg.uuid || `assistant-${Date.now()}`,
            role: 'assistant',
            content: '',
            timestamp: msg.timestamp,
            toolCalls: [],
          };
        }

        for (const tool of toolBlocks) {
          if (tool.name && tool.id) {
            currentAssistantMessage.toolCalls!.push({
              id: tool.id,
              name: tool.name,
              input: tool.input || {},
              result: toolResults.get(tool.id),
              isLoading: false,
            });
          }
        }
      }
    }
  }

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

  return chatMessages;
}

// ============================================
// Codex session transcript parser
// ============================================

interface CodexPayload {
  type?: string;
  role?: string;
  name?: string;
  arguments?: string;
  call_id?: string;
  output?: string;
  content?: Array<{ type?: string; text?: string }>;
}

async function parseCodexTranscriptFile(
  filePath: string
): Promise<{ messages: ChatMessage[]; title: string; usage?: TokenUsage }> {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });

  const messages: ChatMessage[] = [];
  let currentAssistant: ChatMessage | null = null;
  let title = 'Untitled Session';
  let lastUsage: TokenUsage | undefined;
  let msgCounter = 0;

  const flushAssistant = () => {
    if (currentAssistant) {
      messages.push(currentAssistant);
      currentAssistant = null;
    }
  };

  const ensureAssistant = (timestamp?: string): ChatMessage => {
    if (!currentAssistant) {
      currentAssistant = {
        id: `codex-assistant-${msgCounter++}`,
        role: 'assistant',
        content: '',
        toolCalls: [],
        timestamp,
      };
    }
    return currentAssistant;
  };

  for await (const line of rl) {
    if (!line.trim()) continue;
    let entry: { timestamp?: string; type?: string; payload?: CodexPayload };
    try {
      entry = JSON.parse(line);
    } catch { continue; }

    const { type, payload, timestamp } = entry;
    if (!payload) continue;

    if (type === 'response_item') {
      // User message
      if (payload.type === 'message' && payload.role === 'user') {
        const text = payload.content
          ?.filter(c => c.type === 'input_text' && c.text)
          .map(c => c.text!)
          .join('') || '';
        // Skip system/developer messages (permissions, AGENTS.md, env context)
        if (!text || text.startsWith('<') || text.startsWith('#')) continue;

        flushAssistant();
        messages.push({
          id: `codex-user-${msgCounter++}`,
          role: 'user',
          content: text,
          timestamp,
        });
        // First real user message becomes the title
        if (title === 'Untitled Session') {
          title = text.slice(0, 80);
        }
      }

      // Assistant text message
      if (payload.type === 'message' && payload.role === 'assistant') {
        const text = payload.content
          ?.filter(c => c.type === 'output_text' && c.text)
          .map(c => c.text!)
          .join('') || '';
        if (text) {
          const assistant = ensureAssistant(timestamp);
          assistant.content = (assistant.content || '') + text;
        }
      }

      // Reasoning
      if (payload.type === 'reasoning') {
        // Skip reasoning for now (could render as collapsed block later)
      }

      // Tool call (function_call)
      if (payload.type === 'function_call' && payload.name) {
        const assistant = ensureAssistant(timestamp);
        let input: Record<string, unknown> = {};
        try { input = JSON.parse(payload.arguments || '{}'); } catch { /* */ }
        assistant.toolCalls = assistant.toolCalls || [];
        assistant.toolCalls.push({
          id: payload.call_id || `tool-${msgCounter++}`,
          name: payload.name === 'shell_command' ? 'Bash' : payload.name,
          input,
          isLoading: false,
        });
      }

      // Tool result (function_call_output)
      if (payload.type === 'function_call_output' && payload.call_id) {
        const assistant = ensureAssistant(timestamp);
        const tc = assistant.toolCalls?.find(t => t.id === payload.call_id);
        if (tc) {
          tc.result = payload.output || '';
          tc.isLoading = false;
        }
      }
    }

    // Usage from response_completed or event_msg
    if (type === 'response_completed') {
      const usage = (payload as Record<string, unknown>).usage as TokenUsage | undefined;
      if (usage) lastUsage = usage;
      flushAssistant();
    }
  }

  flushAssistant();

  return { messages, title, usage: lastUsage };
}

// ============================================
// Kimi session transcript parser (context.jsonl)
// ============================================
// Format: each line is {"role":"user"|"assistant"|"_system_prompt"|"_checkpoint", "content":[...], ...}

async function parseKimiTranscriptFile(
  filePath: string
): Promise<{ messages: ChatMessage[]; title: string; usage?: TokenUsage }> {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });

  const messages: ChatMessage[] = [];
  let title = 'Untitled Session';
  let msgCounter = 0;

  for await (const line of rl) {
    if (!line.trim()) continue;
    let entry: { role?: string; content?: string | Array<{ type?: string; text?: string; think?: string }>; id?: number };
    try { entry = JSON.parse(line); } catch { continue; }

    // Skip system prompts and checkpoints
    if (!entry.role || entry.role.startsWith('_')) continue;

    if (entry.role === 'user') {
      // content can be a string or an array of blocks
      const text = typeof entry.content === 'string'
        ? entry.content
        : Array.isArray(entry.content)
          ? entry.content.filter(c => (c.type === 'input_text' || c.type === 'text') && c.text).map(c => c.text!).join('')
          : '';
      // Skip system-injected messages
      if (!text || text.startsWith('<system') || text.startsWith('<environment') || text.startsWith('# AGENTS.md') || text.startsWith('<permissions')) continue;
      messages.push({
        id: `kimi-user-${msgCounter++}`,
        role: 'user',
        content: text,
      });
      if (title === 'Untitled Session') {
        title = text.slice(0, 80);
      }
    }

    if (entry.role === 'assistant') {
      let text = '';
      if (typeof entry.content === 'string') {
        text = entry.content;
      } else if (Array.isArray(entry.content)) {
        for (const block of entry.content) {
          if (block.type === 'text' && block.text) {
            text += block.text;
          }
        }
      }

      // Extract tool calls
      const newToolCalls: NonNullable<ChatMessage['toolCalls']> = [];
      const entryToolCalls = (entry as Record<string, unknown>).tool_calls as Array<{ id?: string; function?: { name?: string; arguments?: string } }> | undefined;
      if (entryToolCalls && Array.isArray(entryToolCalls)) {
        for (const tc of entryToolCalls) {
          if (tc.function?.name) {
            let input: Record<string, unknown> = {};
            try { input = JSON.parse(tc.function.arguments || '{}'); } catch { /* */ }
            newToolCalls.push({
              id: tc.id || `tool-${msgCounter++}`,
              name: tc.function.name === 'Shell' ? 'Bash' : tc.function.name,
              input,
              isLoading: false,
            });
          }
        }
      }

      // Merge into the last assistant message if it's part of a tool call chain
      // (consecutive assistant messages without a user message in between)
      const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
      if (lastMsg && lastMsg.role === 'assistant' && lastMsg.toolCalls && lastMsg.toolCalls.length > 0) {
        // Append tool calls and text to existing bubble
        if (newToolCalls.length > 0) {
          lastMsg.toolCalls.push(...newToolCalls);
        }
        if (text) {
          lastMsg.content = (lastMsg.content || '') + text;
        }
      } else if (text || newToolCalls.length > 0) {
        // New assistant bubble
        messages.push({
          id: `kimi-assistant-${msgCounter++}`,
          role: 'assistant',
          content: text,
          ...(newToolCalls.length > 0 ? { toolCalls: newToolCalls } : {}),
        });
      }
    }

    if (entry.role === 'tool') {
      // Match tool result to the last assistant message's tool call
      const toolCallId = (entry as Record<string, unknown>).tool_call_id as string | undefined;
      if (toolCallId && messages.length > 0) {
        const lastMsg = messages[messages.length - 1];
        if (lastMsg.role === 'assistant' && lastMsg.toolCalls) {
          const tc = lastMsg.toolCalls.find(t => t.id === toolCallId);
          if (tc) {
            let result = '';
            if (typeof entry.content === 'string') {
              result = entry.content;
            } else if (Array.isArray(entry.content)) {
              result = entry.content.filter(c => c.type === 'text' && c.text).map(c => c.text!).join('\n');
            }
            tc.result = result;
          }
        }
      }
    }
  }

  return { messages, title };
}

// ============================================
// Ollama session transcript parser
// ============================================
// Format: AI SDK ModelMessage JSONL
// { role: 'user'|'assistant'|'tool', content: string | ContentPart[] }

async function parseOllamaTranscriptFile(
  filePath: string
): Promise<{ messages: ChatMessage[]; title: string; usage?: TokenUsage }> {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });

  let title = 'Untitled Session';
  let msgCounter = 0;

  // First pass: collect all lines and tool results
  const rawLines: string[] = [];
  const toolResults = new Map<string, string>();

  for await (const line of rl) {
    if (!line.trim()) continue;
    rawLines.push(line);
    try {
      const entry = JSON.parse(line) as Record<string, unknown>;
      if (entry.role === 'tool' && Array.isArray(entry.content)) {
        for (const part of entry.content as Array<{ type?: string; toolCallId?: string; result?: unknown }>) {
          if (part.type === 'tool-result' && part.toolCallId) {
            toolResults.set(part.toolCallId, String(part.result ?? ''));
          }
        }
      }
    } catch { /* ignore */ }
  }

  const messages: ChatMessage[] = [];
  let currentAssistant: ChatMessage | null = null;

  const flushAssistant = () => {
    if (currentAssistant) {
      messages.push(currentAssistant);
      currentAssistant = null;
    }
  };

  for (const line of rawLines) {
    try {
      const entry = JSON.parse(line) as Record<string, unknown>;

      if (entry.role === 'user') {
        flushAssistant();
        let text = '';
        if (typeof entry.content === 'string') {
          text = entry.content;
        } else if (Array.isArray(entry.content)) {
          text = (entry.content as Array<{ type?: string; text?: string }>)
            .filter(p => p.type === 'text')
            .map(p => p.text || '')
            .join('\n');
        }
        if (title === 'Untitled Session' && text) {
          title = text.slice(0, 80);
        }
        messages.push({
          id: `ollama-user-${msgCounter++}`,
          role: 'user',
          content: text,
        });
      } else if (entry.role === 'assistant') {
        let text = '';
        const newToolCalls: NonNullable<ChatMessage['toolCalls']> = [];

        if (typeof entry.content === 'string') {
          text = entry.content;
        } else if (Array.isArray(entry.content)) {
          for (const part of entry.content as Array<Record<string, unknown>>) {
            if (part.type === 'text' && typeof part.text === 'string') {
              text += (text ? '\n' : '') + part.text;
            } else if (part.type === 'tool-call') {
              const tcId = String(part.toolCallId || '');
              const tcName = String(part.toolName || '');
              let input: Record<string, unknown> = {};
              try {
                if (typeof part.args === 'string') {
                  input = JSON.parse(part.args);
                } else if (typeof part.args === 'object' && part.args !== null) {
                  input = part.args as Record<string, unknown>;
                }
              } catch { /* ignore */ }
              newToolCalls.push({
                id: tcId,
                name: tcName,
                input,
                result: toolResults.get(tcId),
                isLoading: false,
              });
            }
          }
        }

        if (!currentAssistant) {
          currentAssistant = {
            id: `ollama-assistant-${msgCounter++}`,
            role: 'assistant',
            content: text,
            toolCalls: [],
          };
        } else {
          currentAssistant.content += (currentAssistant.content && text ? '\n' : '') + text;
        }

        if (newToolCalls.length > 0) {
          currentAssistant.toolCalls = currentAssistant.toolCalls || [];
          currentAssistant.toolCalls.push(...newToolCalls);
        }
      }
    } catch { /* ignore parse errors */ }
  }

  flushAssistant();

  return { messages, title };
}
