/**
 * SSE event renderer for terminal output.
 *
 * Features:
 * - Animated spinner while tools execute
 * - Special UI for spawning sub-agents
 * - Clean, minimal output
 */

import type {
  RenderResult,
  SessionStartData,
  ToolStartData,
  ToolResultData,
  SubagentStartData,
  SubagentEndData,
  ThinkingData,
  AssistantTextData,
  ProgressData,
  PartialResponseData,
  ResponseData,
  ClarificationData,
  PlanModeRequestedData,
  PlanSubmittedData,
  ErrorData,
  WarningData,
  SessionEndData,
  ContextFrameData,
  TodoItem,
} from './types.js';

import {
  ANSI,
  STATUS_ACTIVE,
  STATUS_INACTIVE,
  STATUS_ERROR,
  STATUS_INFO,
  DOT_ACTIVE,
  DOT_BULLET,
  NEST_LINE,
  ARROW_RIGHT,
  EM_DASH,
  header,
  footer,
  progressBar,
  style,
} from './design.js';

interface PendingTool {
  name: string;
  args: Record<string, unknown>;
  startTime: number;
  toolId?: string;
}

interface CompletedTool {
  name: string;
  success: boolean;
  summary?: string;
}

export class SSERenderer {
  private verbose: boolean;
  private partialResponse = '';
  private sessionId?: string;
  private spec?: string;
  private specSubmitted = false;
  private planSubmitted?: PlanSubmittedData;
  private planModeRequested?: PlanModeRequestedData;
  private pendingClarification?: ClarificationData;

  // Live display state
  private currentTool?: PendingTool;
  private toolCount = 0;
  private completedTools: CompletedTool[] = [];
  private pendingTools: Map<string, PendingTool> = new Map();

  // Sub-agent state
  private subagentToolCount = 0;
  private inSubagentMode = false;

  // Spinner animation
  private spinnerInterval?: ReturnType<typeof setInterval>;
  private spinnerRunning = false;
  private spinnerMessage = 'thinking';

  // Extended thinking storage
  private lastThinking?: string;

  // Context frame storage
  private lastContextFrame?: ContextFrameData;

  // In-place tool update tracking
  private toolLineActive = false;

  // Floating todo list state
  private floatingTodos?: TodoItem[];
  private todoPanelHeight = 0;

  constructor(verbose: boolean = true) {
    this.verbose = verbose;
  }

  /**
   * Render SSE stream to terminal.
   */
  async renderStream(
    lines: AsyncIterable<string>,
    interruptSignal?: AbortSignal
  ): Promise<RenderResult> {
    let currentEvent: string | undefined;
    let finalResponse = '';
    let interrupted = false;
    this.lastThinking = undefined;
    this.pendingClarification = undefined;
    this.planSubmitted = undefined;
    this.planModeRequested = undefined;

    // Start spinner while waiting for first event
    if (this.verbose) {
      this.startSpinner('thinking');
    }

    try {
      for await (const line of lines) {
        // Check for interrupt signal
        if (interruptSignal?.aborted) {
          this.stopSpinner();
          console.log('\n\x1b[33mInterrupted\x1b[0m');
          interrupted = true;
          break;
        }

        const trimmedLine = line.trim();

        if (trimmedLine.startsWith('event: ')) {
          currentEvent = trimmedLine.slice(7);
        } else if (trimmedLine.startsWith('data: ')) {
          try {
            let data = JSON.parse(trimmedLine.slice(6)) as Record<string, unknown>;
            // Ensure data is an object
            if (data === null) {
              data = {};
            }
            if (currentEvent) {
              const result = this.handleEvent(currentEvent, data);
              if (result) {
                finalResponse = result;
              }
            }
          } catch {
            // Ignore JSON parse errors
          }
        } else if (trimmedLine === ': ping') {
          // SSE keep-alive
          if (this.verbose && !this.spinnerRunning) {
            this.startSpinner('waiting');
          }
        }
      }
    } finally {
      this.stopSpinner();
      this.finalizeToolLine();
      this.clearFloatingTodos();
    }

    // Render context frame at the end
    if (this.verbose) {
      this.renderFinalContextFrame();
    }

    return {
      content: finalResponse,
      session_id: this.sessionId,
      spec: this.spec,
      spec_submitted: this.specSubmitted,
      plan_submitted: this.planSubmitted,
      plan_mode_requested: this.planModeRequested,
      clarification: this.pendingClarification,
      interrupted,
      thinking: this.lastThinking,
    };
  }

  private startSpinner(message: string = 'thinking'): void {
    if (this.spinnerRunning) return;

    this.spinnerMessage = message;
    this.spinnerRunning = true;

    const waveFrames = [
      '●○○○○',
      '○●○○○',
      '○○●○○',
      '○○○●○',
      '○○○○●',
      '○○○●○',
      '○○●○○',
      '○●○○○',
    ];
    let frameIdx = 0;

    this.spinnerInterval = setInterval(() => {
      if (this.toolLineActive) return;

      frameIdx = (frameIdx + 1) % waveFrames.length;
      const dots = waveFrames[frameIdx];
      process.stdout.write(
        `\r  ${ANSI.PRIMARY}─${ANSI.RESET} ${ANSI.WARNING}${dots}${ANSI.RESET} ${ANSI.MUTED}${this.spinnerMessage}${ANSI.RESET}      `
      );
    }, 100);
  }

  private stopSpinner(): void {
    if (!this.spinnerRunning) return;

    this.spinnerRunning = false;
    if (this.spinnerInterval) {
      clearInterval(this.spinnerInterval);
      this.spinnerInterval = undefined;
    }

    // Clear the spinner line (only if no tool line is active)
    if (!this.toolLineActive) {
      process.stdout.write('\r' + ' '.repeat(60) + '\r');
    }
  }

  private handleEvent(eventType: string, data: Record<string, unknown>): string | undefined {
    // Clear waiting indicator when new event arrives
    const subagentId = data['subagent_id'];
    if (!subagentId && !this.inSubagentMode) {
      this.stopSpinner();
    }

    switch (eventType) {
      case 'session_start':
        this.renderSessionStart(data as unknown as SessionStartData);
        break;
      case 'tool_start':
        this.renderToolStart(data as unknown as ToolStartData);
        break;
      case 'tool_result':
        this.renderToolResult(data as unknown as ToolResultData);
        if (this.verbose) {
          this.startSpinner('thinking');
        }
        break;
      case 'subagent_start':
        this.renderSubagentStart(data as unknown as SubagentStartData);
        break;
      case 'subagent_end':
        this.renderSubagentEnd(data as unknown as SubagentEndData);
        break;
      case 'thinking':
        this.renderThinking(data as unknown as ThinkingData);
        break;
      case 'assistant_text':
        this.renderAssistantText(data as unknown as AssistantTextData);
        break;
      case 'progress':
        this.renderProgress(data as unknown as ProgressData);
        break;
      case 'partial_response':
        this.renderPartial(data as unknown as PartialResponseData);
        break;
      case 'response':
        return this.renderResponse(data as unknown as ResponseData);
      case 'clarification':
        this.renderClarification(data as unknown as ClarificationData);
        break;
      case 'plan_mode_requested':
        this.renderPlanModeRequested(data as unknown as PlanModeRequestedData);
        break;
      case 'plan_submitted':
        this.renderPlanSubmitted(data as unknown as PlanSubmittedData);
        break;
      case 'error':
        this.renderError(data as unknown as ErrorData);
        break;
      case 'warning':
        this.renderWarning(data as unknown as WarningData);
        break;
      case 'session_end':
        this.renderSessionEnd(data as unknown as SessionEndData);
        break;
      case 'context_frame':
        this.lastContextFrame = data as unknown as ContextFrameData;
        break;
    }

    return undefined;
  }

  private renderSessionStart(data: SessionStartData): void {
    if (data.session_id) {
      this.sessionId = data.session_id;
    }

    if (!this.verbose) return;

    const agent = data.agent_name ?? 'Agent';

    console.log();
    console.log(`  ${style.primary(style.bold(agent))}`);

    this.toolCount = 0;
    this.completedTools = [];

    this.startSpinner('thinking');
  }

  private renderToolStart(data: ToolStartData): void {
    if (!this.verbose) return;

    const { name, args, tool_id: toolId, subagent_id: subagentId, subagent_type: subagentType } = data;

    this.toolCount++;

    // Store tool info
    const key = toolId ?? name;
    const toolInfo: PendingTool = { name, args, startTime: Date.now(), toolId };
    this.pendingTools.set(key, toolInfo);
    this.currentTool = toolInfo;

    this.stopSpinner();

    // Special handling for task tool (spawning sub-agents)
    if (name === 'task') {
      this.renderAgentSpawnStart(args);
      return;
    }

    // Sub-agent events: update inline
    if (subagentId) {
      this.subagentToolCount++;
      this.renderSubagentProgress(subagentType ?? 'Agent', name, args);
      return;
    }

    // Show tool start line in-place
    const argsDisplay = this.formatToolArgs(name, args);
    const toolLine = `${ANSI.BOLD}${name}${ANSI.RESET}(${argsDisplay}) ${ANSI.MUTED}...${ANSI.RESET}`;
    this.showToolLine(toolLine, true);
  }

  private renderSubagentProgress(_agentType: string, toolName: string, args: Record<string, unknown>): void {
    let summary = '';
    if (args['path']) {
      const path = String(args['path']);
      summary = path.length > 60 ? '...' + path.slice(-57) : path;
    } else if (args['pattern']) {
      summary = String(args['pattern']).slice(0, 60);
    } else if (args['query']) {
      summary = String(args['query']).slice(0, 60);
    }

    if (summary) {
      console.log(`    ${style.dim(NEST_LINE)} ${style.muted(DOT_ACTIVE)} ${toolName} ${style.dim(summary)}`);
    } else {
      console.log(`    ${style.dim(NEST_LINE)} ${style.muted(DOT_ACTIVE)} ${toolName}`);
    }
  }

  private renderAgentSpawnStart(_args: Record<string, unknown>): void {
    this.stopSpinner();
    this.inSubagentMode = true;
    this.subagentToolCount = 0;
  }

  private renderToolResult(data: ToolResultData): void {
    const { name, success, summary, tool_id: toolId, subagent_id: subagentId } = data;

    // Detect spec submission
    if (name === 'submit_spec' && success) {
      this.specSubmitted = true;
      const specData = data['data'] as Record<string, unknown> | undefined;
      if (specData) {
        this.spec = specData['content'] as string | undefined;
      }
    }

    // Handle todo_write tool
    if (name === 'todo_write' && success) {
      const toolData = data['data'] as Record<string, unknown> | undefined;
      const todos = toolData?.['todos'] as TodoItem[] | undefined;
      if (todos && this.verbose) {
        this.updateFloatingTodos(todos);
      }
    }

    if (!this.verbose) return;

    // Special handling for task tool result
    if (name === 'task') {
      this.renderAgentSpawnResult(data);
      return;
    }

    // Sub-agent events: don't print result lines
    if (subagentId) return;

    // Get tool info
    const key = toolId ?? name;
    const toolInfo = this.pendingTools.get(key) ?? this.currentTool;
    this.pendingTools.delete(key);

    const args = toolInfo?.args ?? {};

    // Calculate duration
    let duration = '';
    if (toolInfo?.startTime) {
      const elapsed = (Date.now() - toolInfo.startTime) / 1000;
      if (elapsed >= 0.5) {
        duration = `${elapsed.toFixed(1)}s`;
      }
    }

    // Format args
    const argsDisplay = this.formatToolArgs(name, args);
    const resultText = summary ? ` ${ANSI.MUTED}${summary}${ANSI.RESET}` : '';
    const durationText = duration ? ` ${ANSI.MUTED}${duration}${ANSI.RESET}` : '';

    const toolLine = `${ANSI.BOLD}${name}${ANSI.RESET}(${argsDisplay})${resultText}${durationText}`;
    this.updateToolLine(toolLine, success);

    this.completedTools.push({ name, success, summary });
    this.currentTool = undefined;
  }

  private renderAgentSpawnResult(data: ToolResultData): void {
    const { success } = data;
    const resultData = (data['data'] as Record<string, unknown>) ?? {};

    this.inSubagentMode = false;

    let duration = '';
    if (this.currentTool?.startTime) {
      const elapsed = (Date.now() - this.currentTool.startTime) / 1000;
      duration = ` ${style.dim(`(${elapsed.toFixed(1)}s)`)}`;
    }

    if (success) {
      const agentType = (resultData['agent_type'] as string) ?? 'Agent';
      const iterations = (resultData['iterations'] as number) ?? 0;
      const filesCount = ((resultData['files_explored'] as string[]) ?? []).length;

      console.log(`    ${style.success(STATUS_ACTIVE)} ${agentType} completed${duration}`);

      const stats: string[] = [];
      if (iterations > 0) stats.push(`${iterations} turns`);
      if (filesCount > 0) stats.push(`${filesCount} files`);
      if (this.subagentToolCount > 0) stats.push(`${this.subagentToolCount} tools`);
      if (stats.length > 0) {
        console.log(`    ${style.dim(`${DOT_BULLET} ${stats.join(' · ')}`)}`);
      }
    } else {
      const error = (resultData['error'] as string) ?? data['summary'] ?? 'failed';
      console.log(`    ${style.error(STATUS_ERROR)} Agent failed: ${error}`);
    }

    console.log();
    this.currentTool = undefined;
    this.subagentToolCount = 0;
    // Agent type cleared
  }

  private renderSubagentStart(data: SubagentStartData): void {
    const { agent_type: agentType, prompt, description } = data;

    this.stopSpinner();

    const promptDisplay = prompt.length > 120 ? prompt.slice(0, 120) + '...' : prompt;

    console.log();

    // Animated header
    const headerText = header(`${agentType} Agent`, 42);
    process.stdout.write(`  ${ANSI.MUTED}${headerText}${ANSI.RESET}\n`);

    // Agent icon based on type
    let icon: string;
    let iconStyle: (s: string) => string;
    if (agentType === 'Plan') {
      icon = '◇';
      iconStyle = style.warning;
    } else if (agentType === 'Explore') {
      icon = '◈';
      iconStyle = style.accent;
    } else {
      icon = '◆';
      iconStyle = style.primary;
    }

    console.log(`    ${iconStyle(icon)} ${iconStyle(agentType)}`);

    if (description) {
      console.log(`    ${style.dim(description)}`);
    }

    console.log();
    console.log(`    ${style.primary(ARROW_RIGHT)} ${promptDisplay}`);
    console.log();

    this.inSubagentMode = true;
    // Agent type tracked
    this.subagentToolCount = 0;
  }

  private renderSubagentEnd(data: SubagentEndData): void {
    const { agent_type: agentType, success, iterations = 0, files_explored: filesExplored = 0, execution_time: executionTime = 0 } = data;

    this.inSubagentMode = false;

    if (success) {
      const completionMsg = `${agentType} completed`;
      console.log(`    ${style.success(STATUS_ACTIVE)} ${style.success(completionMsg)} ${style.dim(`(${executionTime.toFixed(1)}s)`)}`);

      const stats: string[] = [];
      if (iterations > 0) stats.push(`${iterations} turns`);
      if (filesExplored > 0) stats.push(`${filesExplored} files`);
      if (this.subagentToolCount > 0) stats.push(`${this.subagentToolCount} tools`);
      if (stats.length > 0) {
        console.log(`    ${style.dim(`${DOT_BULLET} ${stats.join(' · ')}`)}`);
      }
    } else {
      console.log(`    ${style.error(STATUS_ERROR)} ${style.error(`${agentType} failed`)}`);
    }

    console.log();
    console.log(`  ${style.muted(footer(42))}`);
    console.log();
    // Agent type cleared
    this.subagentToolCount = 0;
  }

  private formatToolArgs(toolName: string, args: Record<string, unknown>): string {
    if (!args || Object.keys(args).length === 0) return '';

    // Tool-specific formatting
    if (['glob', 'grep', 'semantic_search'].includes(toolName)) {
      const pattern = (args['pattern'] ?? args['query']) as string | undefined;
      if (pattern) {
        return pattern.length < 50
          ? `${ANSI.MUTED}pattern:${ANSI.RESET} "${pattern}"`
          : `${ANSI.MUTED}pattern:${ANSI.RESET} "${pattern.slice(0, 47)}..."`;
      }
    } else if (['read_file', 'write_to_file', 'list_files'].includes(toolName)) {
      const path = args['path'] as string | undefined;
      if (path) return `${ANSI.MUTED}${path}${ANSI.RESET}`;
    } else if (toolName === 'bash') {
      const cmd = args['command'] as string | undefined;
      if (cmd) {
        return `${ANSI.MUTED}${cmd.slice(0, 60)}${cmd.length > 60 ? '...' : ''}${ANSI.RESET}`;
      }
    } else if (toolName === 'edit_file') {
      const path = args['path'] as string | undefined;
      if (path) return `${ANSI.MUTED}${path}${ANSI.RESET}`;
    }

    // Default: show first arg value
    const values = Object.values(args);
    if (values.length > 0) {
      let firstVal = String(values[0]);
      if (firstVal.length > 50) {
        firstVal = firstVal.slice(0, 47) + '...';
      }
      return `${ANSI.MUTED}${firstVal}${ANSI.RESET}`;
    }

    return '';
  }

  private renderThinking(data: ThinkingData): void {
    if (!this.verbose) return;

    const message = data.message ?? data.content ?? '';
    const { subagent_id: subagentId } = data;

    // Subagent thinking
    if (subagentId && this.inSubagentMode) {
      this.stopSpinner();
      process.stdout.write('\r\x1b[K');

      const displayMsg = message.length > 300 ? message.slice(0, 300) + '...' : message;
      const lines = displayMsg.trim().split('\n');
      const firstLine = lines[0] ?? '';

      console.log(`    ${style.dim(NEST_LINE)} ${style.dim(style.italic(`${DOT_BULLET} ${firstLine}`))}`);

      for (const line of lines.slice(1, 5)) {
        if (line.trim()) {
          console.log(`    ${style.dim(NEST_LINE)} ${style.dim(`   ${line}`)}`);
        }
      }
      if (lines.length > 5) {
        console.log(`    ${style.dim(NEST_LINE)} ${style.dim('   ...')}`);
      }
      return;
    }

    // Extended thinking (long content)
    if (message.length > 200) {
      this.stopSpinner();
      const lines = message.trim().split('\n');
      const lineCount = lines.length;
      const charCount = message.length;

      console.log(`  ${style.dim(NEST_LINE)} ${style.dim(style.italic(`${DOT_BULLET} thinking (${charCount.toLocaleString()} chars, ${lineCount} lines)`))}`);
      for (const line of lines) {
        console.log(`  ${style.dim(NEST_LINE)} ${style.dim(`   ${line}`)}`);
      }

      this.lastThinking = message;
    } else {
      // Short progress message
      console.log(`  ${style.dim(NEST_LINE)} ${style.dim(style.italic(`${DOT_BULLET} ${message}`))}`);
    }
  }

  private renderAssistantText(data: AssistantTextData): void {
    if (!this.verbose) return;

    const content = data.content?.trim();
    if (!content) return;

    this.stopSpinner();

    const display = content.length > 200 ? content.slice(0, 197) + '...' : content;
    console.log(`  ${style.primary(DOT_BULLET)} ${style.muted(style.italic(display))}`);
  }

  private renderProgress(data: ProgressData): void {
    if (!this.verbose) return;

    const { message, percent } = data;

    if (percent !== undefined) {
      const bar = progressBar(percent, 20);
      console.log(`  ${style.dim(NEST_LINE)} ${style.muted(`${bar} ${message}`)}`);
    } else {
      console.log(`  ${style.dim(NEST_LINE)} ${style.muted(`${DOT_BULLET} ${message}`)}`);
    }
  }

  private renderPartial(data: PartialResponseData): void {
    this.partialResponse += data.content ?? '';
  }

  private renderResponse(data: ResponseData): string {
    const content = data.content ?? '';

    console.log();
    console.log(content);

    return content;
  }

  private renderClarification(data: ClarificationData): void {
    const { question, context, options } = data;

    let optionsList = options ?? [];
    if (typeof optionsList === 'string') {
      optionsList = optionsList ? [optionsList] : [];
    }

    console.log();
    console.log(`  ${style.muted('─── Question ───────────────────────────')}`);
    console.log();
    console.log(`  ${question}`);

    if (optionsList.length > 0) {
      console.log();
      for (let i = 0; i < optionsList.length; i++) {
        console.log(`  ${style.warning(STATUS_INACTIVE)} ${style.muted(`${i + 1}.`)} ${optionsList[i]}`);
      }
    }

    console.log();
    console.log(`  ${style.muted('─────────────────────────────────────────')}`);

    this.pendingClarification = {
      question,
      context,
      options: optionsList,
    };
  }

  private renderPlanModeRequested(data: PlanModeRequestedData): void {
    const { reason } = data;

    this.planModeRequested = data;

    console.log();
    console.log(`  ${style.muted('─── Plan Mode ──────────────────────────')}`);
    console.log();
    console.log(`  ${style.warning(STATUS_INFO)} Request to enter plan mode`);
    if (reason) {
      console.log(`  ${style.dim(reason)}`);
    }
    console.log();
    console.log(`  ${style.muted('─────────────────────────────────────────')}`);
  }

  private renderPlanSubmitted(data: PlanSubmittedData): void {
    const { plan } = data;

    this.planSubmitted = data;

    console.log();
    console.log(`  ${style.muted('─── Plan ───────────────────────────────')}`);
    console.log();
    console.log(plan);
    console.log();
    console.log(`  ${style.muted('─────────────────────────────────────────')}`);
  }

  private renderError(data: ErrorData): void {
    const { message, details } = data;

    console.log();
    console.log(`  ${style.error(STATUS_ERROR)} ${style.error(style.bold('Error'))} ${message}`);

    if (details) {
      console.log(`    ${style.dim(details)}`);
    }
  }

  private renderWarning(data: WarningData): void {
    const { message } = data;
    console.log(`  ${style.warning(STATUS_INFO)} ${message}`);
  }

  private renderSessionEnd(data: SessionEndData): void {
    if (!this.verbose) return;

    if (!data.success) {
      const error = data.error ?? 'Unknown error';
      console.log();
      console.log(`  ${style.error(STATUS_ERROR)} Session ended with error: ${error}`);
    }
  }

  private renderFinalContextFrame(): void {
    const injectEnabled = ['1', 'true', 'yes'].includes(
      (process.env['EMDASH_INJECT_CONTEXT_FRAME'] ?? '').toLowerCase()
    );
    if (!injectEnabled || !this.lastContextFrame) return;

    const adding = this.lastContextFrame.adding ?? {};
    const reading = this.lastContextFrame.reading ?? {};

    const stepCount = adding.step_count ?? 0;
    const entitiesFound = adding.entities_found ?? 0;
    const contextTokens = adding.context_tokens ?? 0;
    const contextBreakdown = adding.context_breakdown ?? {};
    const itemCount = reading.item_count ?? 0;

    if (stepCount === 0 && itemCount === 0 && contextTokens === 0) return;

    console.log();
    console.log(`${style.muted(header('Context Frame', 30))}`);

    if (contextTokens > 0) {
      console.log(`  ${style.bold(`Total: ${contextTokens.toLocaleString()} tokens`)}`);
    }

    if (Object.keys(contextBreakdown).length > 0) {
      const parts = Object.entries(contextBreakdown)
        .filter(([_, tokens]) => tokens > 0)
        .map(([key, tokens]) => `${key}: ${tokens.toLocaleString()}`);
      if (parts.length > 0) {
        console.log(`  ${style.dim(`${DOT_BULLET} ${parts.join(' | ')}`)}`);
      }
    }

    const stats: string[] = [];
    if (stepCount > 0) stats.push(`${stepCount} steps`);
    if (entitiesFound > 0) stats.push(`${entitiesFound} entities`);
    if (itemCount > 0) stats.push(`${itemCount} context items`);

    if (stats.length > 0) {
      console.log(`  ${style.dim(`${DOT_BULLET} ${stats.join(' · ')}`)}`);
    }

    const items = reading.items ?? [];
    if (items.length > 0) {
      console.log();
      console.log(`  ${style.bold(`Reranked Items (${items.length}):`)}`);
      for (const item of items.slice(0, 10)) {
        const scoreStr = item.score !== undefined ? ` ${style.primary(`(${item.score.toFixed(3)})`)}` : '';
        console.log(`    ${style.dim(item.type)} ${style.bold(item.name)}${scoreStr}`);
        if (item.file) {
          console.log(`      ${style.dim(item.file)}`);
        }
      }
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // In-place tool update methods
  // ─────────────────────────────────────────────────────────────────────────────

  private showToolLine(text: string, isActive: boolean = true): void {
    process.stdout.write('\r\x1b[K');

    if (isActive) {
      process.stdout.write(`  ${ANSI.WARNING}○${ANSI.RESET} ${text}`);
    } else {
      process.stdout.write(`  ${text}`);
    }

    this.toolLineActive = true;
  }

  private updateToolLine(text: string, success: boolean = true): void {
    process.stdout.write('\r\x1b[K');

    const icon = success
      ? `${ANSI.SUCCESS}${STATUS_ACTIVE}${ANSI.RESET}`
      : `${ANSI.ERROR}${STATUS_ERROR}${ANSI.RESET}`;
    process.stdout.write(`  ${icon} ${text}\n`);

    this.toolLineActive = false;
  }

  private finalizeToolLine(): void {
    if (this.toolLineActive) {
      process.stdout.write('\n');
      this.toolLineActive = false;
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Floating todo panel methods
  // ─────────────────────────────────────────────────────────────────────────────

  private showFloatingTodos(todos: TodoItem[]): void {
    if (todos.length === 0) return;

    this.floatingTodos = todos;

    const completed = todos.filter((t) => t.status === 'completed').length;
    const inProgress = todos.filter((t) => t.status === 'in_progress').length;
    const pending = todos.filter((t) => t.status === 'pending').length;
    const total = todos.length;

    const lines: string[] = [];
    lines.push(`${ANSI.MUTED}${EM_DASH.repeat(3)} Todo List ${EM_DASH.repeat(32)}${ANSI.RESET}`);

    for (const todo of todos) {
      const content = todo.content;
      const activeForm = todo.activeForm ?? content;

      if (todo.status === 'completed') {
        lines.push(`  ${ANSI.SUCCESS}●${ANSI.RESET} ${ANSI.MUTED}\x1b[9m${content}\x1b[0m${ANSI.RESET}`);
      } else if (todo.status === 'in_progress') {
        lines.push(`  ${ANSI.WARNING}◐${ANSI.RESET} \x1b[1m${activeForm}...\x1b[0m`);
      } else {
        lines.push(`  ${ANSI.MUTED}○${ANSI.RESET} ${content}`);
      }
    }

    lines.push(`${ANSI.MUTED}${EM_DASH.repeat(45)}${ANSI.RESET}`);
    lines.push(`  ${ANSI.MUTED}○ ${pending}${ANSI.RESET}  ${ANSI.WARNING}◐ ${inProgress}${ANSI.RESET}  ${ANSI.SUCCESS}● ${completed}${ANSI.RESET}  ${ANSI.MUTED}total ${total}${ANSI.RESET}`);
    lines.push('');

    this.todoPanelHeight = lines.length;

    process.stdout.write('\n');
    for (const line of lines) {
      process.stdout.write(line + '\n');
    }
  }

  private updateFloatingTodos(todos: TodoItem[]): void {
    if (todos.length === 0) {
      this.clearFloatingTodos();
      return;
    }

    if (!this.floatingTodos) {
      this.showFloatingTodos(todos);
      return;
    }

    this.floatingTodos = todos;

    // Move cursor up and redraw
    if (this.todoPanelHeight > 0) {
      process.stdout.write(`\x1b[${this.todoPanelHeight + 1}A`);
    }

    for (let i = 0; i < this.todoPanelHeight + 1; i++) {
      process.stdout.write('\x1b[K\n');
    }

    process.stdout.write(`\x1b[${this.todoPanelHeight + 1}A`);

    this.showFloatingTodos(todos);
  }

  private clearFloatingTodos(): void {
    if (!this.floatingTodos) return;

    this.floatingTodos = undefined;
    this.todoPanelHeight = 0;
  }
}
