import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Box, Text, useApp, useInput } from 'ink';
import { StatusBar } from './StatusBar.js';
import { MessageList } from './MessageList.js';
import { InputBar } from './InputBar.js';
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, isToolInventoryQuery } from '../commands.js';
import type { Message, ModelClient, TokenUsage } from '../../../models/types.js';
import type { ModelRouter } from '../../../models/router.js';
import type { ManagedSession } from '../../../session/index.js';
import type { NativeAgent, ToolUseEvent } from '../../../backends/native/agent.js';
import type { HookEngine } from '../../../hooks/index.js';
import type { ModelConfig, ModelProvider } from '../../../config/schema.js';
import { MODEL_PROVIDERS } from '../../../config/schema.js';
import { createClientFromConfig } from '../../../daemon/index.js';
import { estimateMessageTokens, getContextWindow } from '../../../context/tokens.js';
import type { PairingManager } from '../../../channels/pairing.js';
import { loginGitHub, loginOpenAI } from '../../../auth/index.js';
import { OllamaClient, LlamaCppClient } from '../../../models/index.js';
import { getElevationStatusMessage, setElevationFromInput } from '../../../security/elevation.js';

/** Format a tool name like "gmail.list" -> "Gmail: List" */
function formatToolName(name: string): string {
  const parts = name.split('.');
  return parts.map((p, i) => {
    const capitalized = p.charAt(0).toUpperCase() + p.slice(1);
    return i === 0 && parts.length > 1 ? capitalized + ':' : capitalized;
  }).join(' ');
}

/** Format tool args as a compact, readable summary. */
function formatToolArgs(args: unknown): string {
  if (!args || typeof args !== 'object') {return '';}
  const entries = Object.entries(args as Record<string, unknown>);
  if (entries.length === 0) {return '';}
  const parts = entries.map(([key, value]) => {
    if (typeof value === 'string') {
      const display = value.length > 50 ? value.slice(0, 47) + '...' : value;
      return `${key}: "${display}"`;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
      return `${key}: ${value}`;
    }
    return `${key}: ${JSON.stringify(value)}`;
  });
  return parts.join(', ');
}

export interface AppProps {
  session: ManagedSession;
  modelClient: ModelClient;
  modelRouter?: ModelRouter;
  systemPrompt: string;
  model: string;
  agent?: NativeAgent;
  hookEngine?: HookEngine;
  pairingManager?: PairingManager;
  localProviders?: Record<string, ModelConfig>;
  currentLocalProvider?: string;
  modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
  contextThresholdPct?: number;
  onTransfer?: (target: string) => string | void;
  onTools?: () => string;
  onResearch?: (task: string) => Promise<string> | string;
  onCouncil?: (task: string) => Promise<string> | string;
  onRuntimeCommand?: (input?: string) => Promise<string> | string;
  onExit?: () => void;
}

export function App({
  session,
  modelClient,
  modelRouter,
  systemPrompt,
  model,
  agent,
  hookEngine,
  pairingManager,
  localProviders,
  currentLocalProvider,
  modelProviderConfigs,
  contextThresholdPct,
  onTransfer,
  onTools,
  onResearch,
  onCouncil,
  onRuntimeCommand,
  onExit,
}: AppProps): React.ReactElement {
  const ensureTimestamp = useCallback((message: Message): Message => ({
    ...message,
    timestamp: message.timestamp ?? Date.now(),
  }), []);

  const ensureTimestamps = useCallback((history: Message[]): Message[] => (
    history.map(ensureTimestamp)
  ), [ensureTimestamp]);

  const ctrlCExitWindowMs = 1_500;
  const { exit } = useApp();
  const [input, setInput] = useState('');
  const [messages, setMessages] = useState<Message[]>(ensureTimestamps(session.getHistory()));
  const [isStreaming, setIsStreaming] = useState(false);
  const [streamingContent, setStreamingContent] = useState('');
  const [scrollOffset, setScrollOffset] = useState(0);
  const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ inputTokens: 0, outputTokens: 0 });
  const [verbose, setVerbose] = useState(false);
  const [currentModel, setCurrentModel] = useState(() => {
    if (!modelRouter) {return model;}
    return modelRouter.getLabel(modelRouter.getTier());
  });

  const abortRef = useRef(false);
  const lastCtrlCAtRef = useRef(0);
  const toolLinesRef = useRef<string[]>([]);

  // Set up an Ink-compatible onToolUse callback for the agent.
  // This replaces process.stdout writes (which corrupt Ink rendering)
  // with one that updates React state to show tool activity.
  useEffect(() => {
    if (!agent) {return;}

    const handleToolEvent = (event: ToolUseEvent) => {
      if (!verbose) {
        return;
      }
      if (event.type === 'start') {
        const label = formatToolName(event.tool);
        const argsStr = event.args ? ` (${formatToolArgs(event.args)})` : '';
        toolLinesRef.current = [...toolLinesRef.current, `> ${label}${argsStr}`];
        setStreamingContent(toolLinesRef.current.join('\n'));
        return;
      }

      if (event.type === 'end' && event.result) {
        const icon = event.result.success ? 'done' : 'error';
        const detail = event.result.success
          ? `(${event.result.output.split('\n').length} lines)`
          : (event.result.error ?? 'unknown error');
        toolLinesRef.current = [...toolLinesRef.current, `  ${icon} ${detail}`];
        setStreamingContent(toolLinesRef.current.join('\n'));
      }
    };

    agent.setOnToolUse(handleToolEvent);
    return () => {
      agent.setOnToolUse(undefined);
    };
  }, [agent, verbose]);

  // Fullscreen TUI runs in non-interactive confirmation mode:
  // confirmation hooks are auto-approved so flows never block on a y/n prompt.
  useEffect(() => {
    if (!hookEngine) {return;}
    hookEngine.setInteractiveConfirmer(async () => ({ approved: true }));

    return () => {
      hookEngine.setInteractiveConfirmer(undefined);
    };
  }, [hookEngine]);

  useInput((inputChar, key) => {
    if (key.escape) {
      if (isStreaming) {
        abortRef.current = true;
      } else {
        onExit?.();
        exit();
      }
      return;
    }

    if (key.ctrl && inputChar.toLowerCase() === 'c') {
      const now = Date.now();
      const shouldExit = lastCtrlCAtRef.current > 0 && (now - lastCtrlCAtRef.current) <= ctrlCExitWindowMs;
      lastCtrlCAtRef.current = now;
      if (shouldExit) {
        onExit?.();
        exit();
        return;
      }
      if (input.length > 0) {
        setInput('');
      }
      return;
    }

    // Tab completion for slash commands
    if (key.tab && !isStreaming) {
      const completions = getCommandCompletions(input);
      if (completions.length > 0) {
        setInput(completions[0]);
        return;
      }
    }

    // Only handle scroll keys, ignore regular typing
    if (!key.upArrow && !key.downArrow && !key.pageUp && !key.pageDown && !key.tab) {
      return;
    }

    if (key.upArrow && scrollOffset > 0) {
      setScrollOffset(prev => Math.max(0, prev - 1));
    }
    if (key.downArrow) {
      setScrollOffset(prev => Math.min(messages.length - 1, prev + 1));
    }
    if (key.pageUp) {
      setScrollOffset(prev => Math.max(0, prev - 10));
    }
    if (key.pageDown) {
      setScrollOffset(prev => Math.min(messages.length - 1, prev + 10));
    }
  });

  const pushAssistantMessage = useCallback((content: string) => {
    setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
  }, [session]);

  const getAvailableBackends = useCallback((): string[] => {
    const backends: string[] = [];
    if (currentLocalProvider) {
      backends.push(currentLocalProvider);
    }
    if (localProviders) {
      backends.push(...Object.keys(localProviders));
    }
    return [...new Set(backends)];
  }, [currentLocalProvider, localProviders]);

  const createLocalClient = useCallback((cfg: ModelConfig): ModelClient | null => {
    if (cfg.provider === 'ollama') {
      return new OllamaClient({
        model: cfg.model,
        host: cfg.endpoint,
      });
    }
    if (cfg.provider === 'llamacpp') {
      return new LlamaCppClient({
        endpoint: cfg.endpoint ?? 'http://localhost:8080',
        model: cfg.model,
        authToken: cfg.auth_token,
      });
    }
    return null;
  }, []);

  const handleSubmit = useCallback(async (value: string) => {
    const command = parseCommand(value);
    if (!command) {return;}

    setInput('');

    switch (command.type) {
      case 'quit':
        onExit?.();
        exit();
        return;

      case 'reset':
        if (agent) {
          agent.reset();
        } else {
          session.clear();
        }
        setMessages([]);
        setTokenUsage({ inputTokens: 0, outputTokens: 0 });
        setScrollOffset(0);
        return;

      case 'help': {
        const helpMsg: Message = { role: 'assistant', content: getHelpText() };
        setMessages(prev => [...prev, session.addMessage(helpMsg)]);
        return;
      }

      case 'status': {
        const status = `Session: ${session.id}\nMessages: ${messages.length}\nTokens: ${tokenUsage.inputTokens} in / ${tokenUsage.outputTokens} out`;
        const statusMsg: Message = { role: 'assistant', content: status };
        setMessages(prev => [...prev, session.addMessage(statusMsg)]);
        return;
      }

      case 'compact': {
        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Compact command is not available in fullscreen TUI mode.' })]);
        return;
      }

      case 'usage': {
        const text = `Token Usage\n\nTotal: ${tokenUsage.inputTokens} in / ${tokenUsage.outputTokens} out`;
        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: text })]);
        return;
      }

      case 'context': {
        const history = session.getHistory();
        const estimated = estimateMessageTokens(history);
        const tier = modelRouter?.getTier() ?? 'default';
        const modelName = modelRouter?.getLabel(tier) ?? model;
        const window = getContextWindow(modelName);
        const usagePct = window > 0 ? (estimated / window) * 100 : 0;
        const thresholdPct = contextThresholdPct ?? 80;
        const thresholdTokens = Math.floor((thresholdPct / 100) * window);
        const remaining = Math.max(0, window - estimated);
        const text = [
          'Context Usage (estimated)',
          '',
          `Model: ${modelName}`,
          `Used: ${estimated.toLocaleString()} / ${window.toLocaleString()} tokens (${usagePct.toFixed(1)}%)`,
          `Remaining: ${remaining.toLocaleString()} tokens`,
          `Compaction threshold: ${thresholdPct}% (${thresholdTokens.toLocaleString()} tokens)`,
          `Should compact: ${estimated > thresholdTokens ? 'yes' : 'no'}`,
        ].join('\n');
        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: text })]);
        return;
      }

      case 'verbose': {
        const next = !verbose;
        setVerbose(next);
        let content = `Verbose mode: ${next ? 'on' : 'off'}`;
        if (next && agent) {
          const snapshot = agent.getToolInventorySnapshot();
          content += `\n[Agent] tool-inventory session=${snapshot.sessionId} agent=${snapshot.agent} provider=${snapshot.provider} skill=${snapshot.skill} internal=${snapshot.internalCount} exposed=${snapshot.exposedCount} internal_browser=[${snapshot.internalBrowser.join(', ') || 'none'}] exposed_browser=[${snapshot.exposedBrowser.join(', ') || 'none'}]`;
        }
        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
        return;
      }

      case 'model': {
        if (!modelRouter) {
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Model switching not available.' })]);
          return;
        }

        // /model
        if (!command.name) {
          const current = modelRouter.getTier();
          const available = modelRouter.getAvailableTiers();
          const labels = modelRouter.getAllLabels();

          const lines: string[] = [];
          lines.push(`Active tier: ${current}`);
          for (const t of available) {
            const label = labels[t] ?? 'unknown';
            const strict = modelRouter.isTierStrict(t) ? ' (strict)' : '';
            lines.push(`  ${t}: ${label}${strict}${t === current ? ' ←' : ''}`);
          }

          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: lines.join('\n') })]);
          return;
        }

        // /model <tier> <provider/model>
        if (command.providerModel) {
          const tier = resolveModelAlias(command.name);
          const providerModel = command.providerModel;

          const slashIdx = providerModel.indexOf('/');
          if (slashIdx === -1) {
            setMessages(prev => [...prev, session.addMessage({
              role: 'assistant',
              content: 'Invalid format. Use provider/model (e.g. anthropic/claude-sonnet-4)',
            })]);
            return;
          }

          const provider = providerModel.slice(0, slashIdx);
          const modelName = providerModel.slice(slashIdx + 1);

          if (!MODEL_PROVIDERS.includes(provider as ModelProvider)) {
            setMessages(prev => [...prev, session.addMessage({
              role: 'assistant',
              content: `Unknown provider "${provider}". Known providers: ${MODEL_PROVIDERS.join(', ')}`,
            })]);
            return;
          }

          try {
            const providerType = provider as ModelProvider;
            const template = modelProviderConfigs?.[providerType];
            const client = createClientFromConfig({
              ...(template ?? {}),
              provider: providerType,
              model: modelName,
            });

            modelRouter.setClient(tier, client, providerModel);
            modelRouter.setTierStrict(tier, true);

            if (agent && tier === modelRouter.getTier()) {
              agent.setModelTier(tier);
              setCurrentModel(modelRouter.getLabel(tier));
            }

            setMessages(prev => [...prev, session.addMessage({
              role: 'assistant',
              content: `Set ${tier} to: ${providerModel}\nFallbacks for ${tier} disabled (strict tier mode).`,
            })]);
          } catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Failed to create client: ${msg}` })]);
          }

          return;
        }

        // /model <tier>
        const tier = resolveModelAlias(command.name);
        if (modelRouter.setTier(tier)) {
          if (agent) {
            agent.setModelTier(tier);
          }
          setCurrentModel(modelRouter.getLabel(tier));
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Switched to model: ${tier}` })]);
        } else {
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Model not available: ${command.name}` })]);
        }
        return;
      }

      case 'fullscreen':
        return;

      case 'transfer': {
        if (!onTransfer) {
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Transfer target is not available in fullscreen mode.' })]);
          return;
        }
        const result = onTransfer(command.target);
        const content = typeof result === 'string' && result.trim()
          ? result
          : `Transfer requested: ${command.target}`;
        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
        return;
      }

      case 'queue': {
        if (!command.action || command.action === 'show') {
          const mode = session.getConfig('queue.mode') ?? 'collect';
          const cap = session.getConfig('queue.cap') ?? '50';
          const overflow = session.getConfig('queue.overflow') ?? 'drop_old';
          const debounceMs = session.getConfig('queue.debounce_ms') ?? '0';
          const summarizeOverflow = session.getConfig('queue.summarize_overflow') ?? 'true';
          const text = [
            'Queue policy:',
            `mode: ${mode}`,
            `cap: ${cap}`,
            `overflow: ${overflow}`,
            `debounce_ms: ${debounceMs}`,
            `summarize_overflow: ${summarizeOverflow}`,
          ].join('\n');
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: text })]);
          return;
        }

        if (command.action === 'reset') {
          session.deleteConfig('queue.mode');
          session.deleteConfig('queue.cap');
          session.deleteConfig('queue.overflow');
          session.deleteConfig('queue.debounce_ms');
          session.deleteConfig('queue.summarize_overflow');
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Reset session queue overrides.' })]);
          return;
        }

        const raw = (command.args ?? '').trim();
        const [rawKey, ...rest] = raw.split(/\s+/);
        const value = rest.join(' ').trim();
        if (!rawKey || !value) {
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Usage: /queue set <mode|cap|overflow|debounce_ms|summarize_overflow> <value>' })]);
          return;
        }

        const key = rawKey.toLowerCase();
        if (key === 'mode') {
          if (!['collect', 'followup', 'steer', 'steer_backlog', 'interrupt'].includes(value)) {
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Invalid mode. Use: collect, followup, steer, steer_backlog, interrupt' })]);
            return;
          }
          session.setConfig('queue.mode', value);
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Set queue.mode=${value}` })]);
          return;
        }

        if (key === 'cap') {
          const cap = Number.parseInt(value, 10);
          if (!Number.isFinite(cap) || cap < 1 || cap > 1000) {
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Invalid cap. Use an integer between 1 and 1000.' })]);
            return;
          }
          session.setConfig('queue.cap', String(cap));
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Set queue.cap=${cap}` })]);
          return;
        }

        if (key === 'overflow') {
          if (value !== 'drop_old' && value !== 'drop_new') {
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Invalid overflow. Use drop_old or drop_new.' })]);
            return;
          }
          session.setConfig('queue.overflow', value);
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Set queue.overflow=${value}` })]);
          return;
        }

        if (key === 'debounce_ms') {
          const debounceMs = Number.parseInt(value, 10);
          if (!Number.isFinite(debounceMs) || debounceMs < 0 || debounceMs > 60_000) {
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Invalid debounce_ms. Use an integer between 0 and 60000.' })]);
            return;
          }
          session.setConfig('queue.debounce_ms', String(debounceMs));
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Set queue.debounce_ms=${debounceMs}` })]);
          return;
        }

        if (key === 'summarize_overflow') {
          const normalized = value.toLowerCase();
          if (normalized !== 'true' && normalized !== 'false') {
            setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Invalid summarize_overflow. Use true or false.' })]);
            return;
          }
          session.setConfig('queue.summarize_overflow', normalized);
          setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Set queue.summarize_overflow=${normalized}` })]);
          return;
        }

        setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: 'Unknown queue key. Use mode, cap, overflow, debounce_ms, summarize_overflow.' })]);
        return;
      }

      case 'backend': {
        if (!modelRouter) {
          pushAssistantMessage('Backend switching not available.');
          return;
        }

        if (!command.provider) {
          const current = modelRouter.getLocalProviderName() ?? currentLocalProvider ?? 'unknown';
          const available = getAvailableBackends();
          pushAssistantMessage(`Current local backend: ${current}\nAvailable: ${available.join(', ')}`);
          return;
        }

        const providerConfig = localProviders?.[command.provider];
        if (!providerConfig) {
          const available = getAvailableBackends();
          pushAssistantMessage(`Backend '${command.provider}' not configured.\nAvailable: ${available.join(', ')}`);
          return;
        }

        const client = createLocalClient(providerConfig);
        if (!client) {
          pushAssistantMessage(`Unsupported backend provider '${providerConfig.provider}'.`);
          return;
        }

        modelRouter.setLocalClient(client, command.provider);
        modelRouter.setTier('local');
        if (agent) {
          agent.setModelTier('local');
        }
        setCurrentModel(modelRouter.getLabel('local'));
        pushAssistantMessage(`Switched backend to ${command.provider}`);
        return;
      }

      case 'runtime': {
        if (!onRuntimeCommand) {
          pushAssistantMessage(
            'Runtime backend mode command service is unavailable in this TUI session.\n'
              + 'Use `flynn start` and reconnect.',
          );
          return;
        }
        try {
          const response = await onRuntimeCommand(command.input);
          pushAssistantMessage(response);
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          pushAssistantMessage(`Runtime command failed: ${message}`);
        }
        return;
      }

      case 'login': {
        const provider = (command.provider ?? '').trim().toLowerCase();
        if (!provider) {
          pushAssistantMessage('Usage: /login <provider>\nSupported: github, openai, anthropic, zai');
          return;
        }

        if (provider === 'github') {
          pushAssistantMessage('Starting GitHub OAuth device login...');
          try {
            await loginGitHub((userCode, verificationUri) => {
              pushAssistantMessage(`GitHub login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
            });
            pushAssistantMessage('GitHub login complete. Token stored in ~/.config/flynn/auth.json');
          } catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            pushAssistantMessage(`GitHub login failed: ${msg}`);
          }
          return;
        }

        if (provider === 'openai') {
          pushAssistantMessage('Starting OpenAI OAuth device login...');
          try {
            await loginOpenAI((userCode, verificationUri) => {
              pushAssistantMessage(`OpenAI login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
            });
            pushAssistantMessage('OpenAI login complete. Credentials stored in ~/.config/flynn/auth.json');
          } catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            pushAssistantMessage(`OpenAI login failed: ${msg}`);
          }
          return;
        }

        if (provider === 'anthropic' || provider === 'zai' || provider === 'zhipuai') {
          pushAssistantMessage(
            `/${command.type} ${provider} requires key entry, which fullscreen mode does not mask.\nUse minimal mode (pnpm tui) for interactive key setup.`,
          );
          return;
        }

        pushAssistantMessage(`Unknown login provider: ${provider}. Supported: github, openai, anthropic, zai`);
        return;
      }

      case 'tools': {
        if (!onTools) {
          pushAssistantMessage('Tools command is not available in this TUI mode.');
          return;
        }
        pushAssistantMessage(onTools());
        return;
      }

      case 'research': {
        if (!command.task.trim()) {
          pushAssistantMessage('Usage: /research <question or task>');
          return;
        }
        if (!onResearch) {
          pushAssistantMessage('Research command is not available in this TUI mode.');
          return;
        }
        pushAssistantMessage(await onResearch(command.task));
        return;
      }

      case 'council': {
        if (!command.task.trim()) {
          pushAssistantMessage('Usage: /council <question or task> | /council preflight');
          return;
        }
        if (!onCouncil) {
          pushAssistantMessage('Council command is not available in this TUI mode.');
          return;
        }
        pushAssistantMessage(await onCouncil(command.task));
        return;
      }

      case 'pair': {
        if (!pairingManager) {
          pushAssistantMessage('Pairing not enabled. Set pairing.enabled: true in config.');
          return;
        }

        if (command.action === 'generate') {
          const code = pairingManager.generateCode(command.args);
          const pending = pairingManager.listPendingCodes().find(p => p.code === code);
          const expiresIn = pending ? Math.round((pending.expiresAt - Date.now()) / 1000) : '?';
          pushAssistantMessage(`Pairing code: ${code}\nExpires in ${expiresIn}s${command.args ? ` (label: ${command.args})` : ''}`);
          return;
        }

        if (command.action === 'revoke') {
          const args = (command.args ?? '').trim();
          const parts = args.split(/\s+/);
          if (parts.length < 2) {
            pushAssistantMessage('Usage: /pair revoke <channel> <senderId>');
            return;
          }
          const [channel, senderId] = parts;
          const revoked = pairingManager.revokeApproval(channel, senderId);
          pushAssistantMessage(revoked ? `Revoked approval for ${channel}:${senderId}` : `No approval found for ${channel}:${senderId}`);
          return;
        }

        const pending = pairingManager.listPendingCodes();
        const approved = pairingManager.listApproved();
        if (pending.length === 0 && approved.length === 0) {
          pushAssistantMessage('No pending codes or approved senders.');
          return;
        }

        const lines: string[] = [];
        if (pending.length > 0) {
          lines.push('Pending codes:');
          for (const p of pending) {
            const ttl = Math.max(0, Math.round((p.expiresAt - Date.now()) / 1000));
            lines.push(`  ${p.code}  expires in ${ttl}s${p.label ? `  (label: ${p.label})` : ''}`);
          }
        }
        if (approved.length > 0) {
          lines.push('Approved senders:');
          for (const a of approved) {
            const date = new Date(a.approvedAt).toISOString().slice(0, 16).replace('T', ' ');
            lines.push(`  ${a.channel}:${a.senderId}  since ${date}  (code: ${a.codeUsed})`);
          }
        }
        pushAssistantMessage(lines.join('\n'));
        return;
      }

      case 'elevate': {
        const store = {
          get: (key: string) => session.getConfig(key),
          set: (key: string, value: string) => session.setConfig(key, value),
          delete: (key: string) => session.deleteConfig(key),
        };

        const raw = (command.args ?? '').trim();
        if (!raw) {
          pushAssistantMessage(getElevationStatusMessage(store, { reasonSeparator: ' - ' }));
          return;
        }
        pushAssistantMessage(setElevationFromInput(store, raw));
        return;
      }

      case 'multiline':
        pushAssistantMessage('Multiline compose mode is currently available in minimal TUI only. In fullscreen mode, submit as a single message block.');
        return;

      case 'message':
        break;

      default: {
        const exhaustive: never = command;
        throw new Error(`Unhandled command: ${JSON.stringify(exhaustive)}`);
      }
    }

    if (command.type !== 'message' || isStreaming) {
      return;
    }

    // Add user message to UI (and session if no agent — agent adds it internally)
    const userMessage: Message = { role: 'user', content: command.content };
    if (!agent) {
      const messageWithTimestamp = session.addMessage(userMessage);
      setMessages(prev => [...prev, messageWithTimestamp]);
    } else {
      setMessages(prev => [...prev, ensureTimestamp(userMessage)]);
    }
    setScrollOffset(0);

    if (onTools && isToolInventoryQuery(command.content)) {
      pushAssistantMessage(onTools());
      return;
    }

    setIsStreaming(true);
    setStreamingContent('');
    toolLinesRef.current = [];
    abortRef.current = false;

    try {
      if (agent) {
        await agent.process(command.content);
        const usage = agent.getUsage();
        setTokenUsage({ inputTokens: usage.inputTokens, outputTokens: usage.outputTokens });
        setMessages(ensureTimestamps(session.getHistory()));
      } else if (modelClient.chatStream) {
        let fullContent = '';

        for await (const event of modelClient.chatStream({
          messages: session.getHistory(),
          system: systemPrompt,
        })) {
          if (abortRef.current) {
            fullContent += '\n\n[interrupted]';
            break;
          }

          if (event.type === 'content' && event.content) {
            fullContent += event.content;
            setStreamingContent(fullContent);
          }
          if (event.type === 'done' && event.usage) {
            const usage = event.usage;
            setTokenUsage(prev => ({
              inputTokens: prev.inputTokens + usage.inputTokens,
              outputTokens: prev.outputTokens + usage.outputTokens,
            }));
          }
          if (event.type === 'error') {
            throw event.error ?? new Error('Stream error');
          }
        }

        const assistantMessage: Message = { role: 'assistant', content: fullContent };
        setMessages(prev => [...prev, session.addMessage(assistantMessage)]);
      } else {
        const response = await modelClient.chat({
          messages: session.getHistory(),
          system: systemPrompt,
        });

        setTokenUsage(prev => ({
          inputTokens: prev.inputTokens + response.usage.inputTokens,
          outputTokens: prev.outputTokens + response.usage.outputTokens,
        }));

        const assistantMessage: Message = { role: 'assistant', content: response.content };
        setMessages(prev => [...prev, session.addMessage(assistantMessage)]);
      }
    } catch (error) {
      const msg = error instanceof Error ? error.message : 'Unknown error';
      setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Error: ${msg}` })]);
    } finally {
      setIsStreaming(false);
      setStreamingContent('');
    }
  }, [
    session,
    agent,
    modelClient,
    modelRouter,
    systemPrompt,
    exit,
    onExit,
    isStreaming,
    ensureTimestamp,
    ensureTimestamps,
    messages.length,
    tokenUsage.inputTokens,
    tokenUsage.outputTokens,
    pushAssistantMessage,
    getAvailableBackends,
    createLocalClient,
    localProviders,
    currentLocalProvider,
    pairingManager,
    modelProviderConfigs,
    onTransfer,
    onRuntimeCommand,
  ]);

  return (
    <Box flexDirection="column" height="100%">
      <MessageList
        messages={messages}
        scrollOffset={scrollOffset}
        streamingContent={isStreaming ? streamingContent : undefined}
      />

      <InputBar
        value={input}
        onChange={setInput}
        onSubmit={handleSubmit}
        isLoading={isStreaming}
        placeholder={isStreaming ? 'Flynn is typing... (Esc to cancel)' : 'Type a message... (Esc=exit, /help)'}
      />

      <StatusBar
        sessionId={session.id}
        messageCount={messages.length}
        model={currentModel}
        tokenUsage={{ input: tokenUsage.inputTokens, output: tokenUsage.outputTokens }}
        isStreaming={isStreaming}
      />
    </Box>
  );
}
