import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { Box, useApp, useInput } from 'ink';
import type { ParsedCommand } from '@mosaicstack/types';
import { TopBar } from './components/top-bar.js';
import { BottomBar } from './components/bottom-bar.js';
import { MessageList } from './components/message-list.js';
import { InputBar } from './components/input-bar.js';
import { Sidebar } from './components/sidebar.js';
import { SearchBar } from './components/search-bar.js';
import { useSocket } from './hooks/use-socket.js';
import { useGitInfo } from './hooks/use-git-info.js';
import { useViewport } from './hooks/use-viewport.js';
import { useAppMode } from './hooks/use-app-mode.js';
import { useConversations } from './hooks/use-conversations.js';
import { useSearch } from './hooks/use-search.js';
import { executeHelp, executeStatus, executeHistory, commandRegistry } from './commands/index.js';
import { fetchConversationMessages } from './gateway-api.js';
import { expandFileRefs, hasFileRefs, handleAttachCommand } from './file-ref.js';

export interface TuiAppProps {
  gatewayUrl: string;
  conversationId?: string;
  sessionCookie?: string;
  initialModel?: string;
  initialProvider?: string;
  agentId?: string;
  agentName?: string;
  projectId?: string;
  /** CLI package version passed from the entry point (cli.ts). */
  version?: string;
}

export function TuiApp({
  gatewayUrl,
  conversationId,
  sessionCookie,
  initialModel,
  initialProvider,
  agentId,
  agentName,
  projectId: _projectId,
  version = '0.0.0',
}: TuiAppProps) {
  const { exit } = useApp();
  const gitInfo = useGitInfo();
  const appMode = useAppMode();

  const socket = useSocket({
    gatewayUrl,
    sessionCookie,
    initialConversationId: conversationId,
    initialModel,
    initialProvider,
    agentId,
  });

  const conversations = useConversations({ gatewayUrl, sessionCookie });

  const viewport = useViewport({ totalItems: socket.messages.length });

  const search = useSearch(socket.messages);

  // Scroll to current match when it changes
  const currentMatch = search.matches[search.currentMatchIndex];
  useEffect(() => {
    if (currentMatch && appMode.mode === 'search') {
      viewport.scrollTo(currentMatch.messageIndex);
    }
  }, [currentMatch, appMode.mode, viewport]);

  // Compute highlighted message indices for MessageList
  const highlightedMessageIndices = useMemo(() => {
    if (search.matches.length === 0) return undefined;
    return new Set(search.matches.map((m) => m.messageIndex));
  }, [search.matches]);

  const currentHighlightIndex = currentMatch?.messageIndex;

  const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);

  // Controlled input state — held here so Ctrl+C can clear it
  const [tuiInput, setTuiInput] = useState('');
  // Ctrl+C double-press: first press with empty input shows hint; second exits
  const ctrlCPendingExit = useRef(false);
  // Flag to suppress the character that ink-text-input leaks when a Ctrl+key
  // combo is handled by the top-level useInput handler (e.g. Ctrl+T → 't').
  const ctrlJustFired = useRef(false);

  // Wrap sendMessage to expand @file references before sending
  const sendMessageWithFileRefs = useCallback(
    (content: string) => {
      if (!hasFileRefs(content)) {
        socket.sendMessage(content);
        return;
      }
      void expandFileRefs(content)
        .then(({ expandedMessage, filesAttached, errors }) => {
          for (const err of errors) {
            socket.addSystemMessage(err);
          }
          if (filesAttached.length > 0) {
            socket.addSystemMessage(
              `📎 Attached ${filesAttached.length} file(s): ${filesAttached.join(', ')}`,
            );
          }
          socket.sendMessage(expandedMessage);
        })
        .catch((err: unknown) => {
          socket.addSystemMessage(
            `File expansion failed: ${err instanceof Error ? err.message : String(err)}`,
          );
          // Send original message without expansion
          socket.sendMessage(content);
        });
    },
    [socket],
  );

  const handleLocalCommand = useCallback(
    (parsed: ParsedCommand) => {
      switch (parsed.command) {
        case 'help':
        case 'h': {
          const result = executeHelp(parsed);
          socket.addSystemMessage(result);
          break;
        }
        case 'status':
        case 's': {
          const result = executeStatus(parsed, {
            connected: socket.connected,
            model: socket.modelName,
            provider: socket.providerName,
            sessionId: socket.conversationId ?? null,
            tokenCount: socket.tokenUsage.total,
          });
          socket.addSystemMessage(result);
          break;
        }
        case 'clear':
          socket.clearMessages();
          break;
        case 'new':
        case 'n':
          void conversations
            .createConversation()
            .then((conv) => {
              if (conv) {
                socket.switchConversation(conv.id);
                appMode.setMode('chat');
              }
            })
            .catch(() => {
              socket.addSystemMessage('Failed to create new conversation.');
            });
          break;
        case 'attach': {
          if (!parsed.args) {
            socket.addSystemMessage('Usage: /attach <file-path>');
            break;
          }
          void handleAttachCommand(parsed.args)
            .then(({ content, error }) => {
              if (error) {
                socket.addSystemMessage(`Attach error: ${error}`);
              } else if (content) {
                // Send the file content as a user message
                socket.sendMessage(content);
              }
            })
            .catch((err: unknown) => {
              socket.addSystemMessage(
                `Attach failed: ${err instanceof Error ? err.message : String(err)}`,
              );
            });
          break;
        }
        case 'stop':
          if (socket.isStreaming && socket.socketRef.current?.connected && socket.conversationId) {
            socket.socketRef.current.emit('abort', {
              conversationId: socket.conversationId,
            });
            socket.addSystemMessage('Abort signal sent.');
          } else {
            socket.addSystemMessage('No active stream to stop.');
          }
          break;
        case 'cost': {
          const u = socket.tokenUsage;
          socket.addSystemMessage(
            `Tokens — input: ${u.input}, output: ${u.output}, total: ${u.total}\nCost: $${u.cost.toFixed(6)}`,
          );
          break;
        }
        case 'history':
        case 'hist': {
          void executeHistory({
            conversationId: socket.conversationId,
            gatewayUrl,
            sessionCookie,
            fetchMessages: fetchConversationMessages,
          })
            .then((result) => {
              socket.addSystemMessage(result);
            })
            .catch((err: unknown) => {
              const msg = err instanceof Error ? err.message : String(err);
              socket.addSystemMessage(`Failed to fetch history: ${msg}`);
            });
          break;
        }
        default:
          socket.addSystemMessage(`Local command not implemented: /${parsed.command}`);
      }
    },
    [socket],
  );

  const handleGatewayCommand = useCallback(
    (parsed: ParsedCommand) => {
      if (!socket.socketRef.current?.connected) {
        socket.addSystemMessage('Not connected to gateway. Command cannot be executed.');
        return;
      }
      socket.socketRef.current.emit('command:execute', {
        conversationId: socket.conversationId ?? '',
        command: parsed.command,
        args: parsed.args ?? undefined,
      });
    },
    [socket],
  );

  const handleSwitchConversation = useCallback(
    (id: string) => {
      socket.switchConversation(id);
      appMode.setMode('chat');
    },
    [socket, appMode],
  );

  const handleDeleteConversation = useCallback(
    (id: string) => {
      void conversations
        .deleteConversation(id)
        .then((ok) => {
          if (ok && id === socket.conversationId) {
            socket.clearMessages();
          }
        })
        .catch(() => {});
    },
    [conversations, socket],
  );

  useInput((ch, key) => {
    // Ctrl+C: clear input → show hint → second empty press exits
    if (key.ctrl && ch === 'c') {
      if (tuiInput) {
        setTuiInput('');
        ctrlCPendingExit.current = false;
      } else if (ctrlCPendingExit.current) {
        exit();
      } else {
        ctrlCPendingExit.current = true;
        socket.addSystemMessage('Press Ctrl+C again to exit.');
      }
      return;
    }
    // Any other key resets the pending-exit flag
    ctrlCPendingExit.current = false;
    // Ctrl+L: toggle sidebar (refresh on open)
    if (key.ctrl && ch === 'l') {
      ctrlJustFired.current = true;
      queueMicrotask(() => {
        ctrlJustFired.current = false;
      });
      const willOpen = !appMode.sidebarOpen;
      appMode.toggleSidebar();
      if (willOpen) {
        void conversations.refresh();
      }
      return;
    }
    // Ctrl+N: create new conversation and switch to it
    if (key.ctrl && ch === 'n') {
      ctrlJustFired.current = true;
      queueMicrotask(() => {
        ctrlJustFired.current = false;
      });
      void conversations
        .createConversation()
        .then((conv) => {
          if (conv) {
            socket.switchConversation(conv.id);
            appMode.setMode('chat');
          }
        })
        .catch(() => {});
      return;
    }
    // Ctrl+K: toggle search mode
    if (key.ctrl && ch === 'k') {
      ctrlJustFired.current = true;
      queueMicrotask(() => {
        ctrlJustFired.current = false;
      });
      if (appMode.mode === 'search') {
        search.clear();
        appMode.setMode('chat');
      } else {
        appMode.setMode('search');
      }
      return;
    }
    // Page Up / Page Down: scroll message history (only in chat mode)
    if (appMode.mode === 'chat') {
      if (key.pageUp) {
        viewport.scrollBy(-viewport.viewportSize);
      }
      if (key.pageDown) {
        viewport.scrollBy(viewport.viewportSize);
      }
    }
    // Ctrl+T: cycle thinking level
    if (key.ctrl && ch === 't') {
      ctrlJustFired.current = true;
      queueMicrotask(() => {
        ctrlJustFired.current = false;
      });
      const levels = socket.availableThinkingLevels;
      if (levels.length > 0) {
        const currentIdx = levels.indexOf(socket.thinkingLevel);
        const nextIdx = (currentIdx + 1) % levels.length;
        const next = levels[nextIdx];
        if (next) {
          socket.setThinkingLevel(next);
        }
      }
      return;
    }
    // Escape: return to chat from sidebar/search; in chat, scroll to bottom
    if (key.escape) {
      if (appMode.mode === 'search') {
        search.clear();
        appMode.setMode('chat');
      } else if (appMode.mode === 'sidebar') {
        appMode.setMode('chat');
      } else if (appMode.mode === 'chat') {
        viewport.scrollToBottom();
      }
    }
  });

  const inputPlaceholder =
    appMode.mode === 'sidebar'
      ? 'focus is on sidebar… press Esc to return'
      : appMode.mode === 'search'
        ? 'search mode… press Esc to return'
        : undefined;

  const isSearchMode = appMode.mode === 'search';

  const messageArea = (
    <Box flexDirection="column" flexGrow={1}>
      <MessageList
        messages={socket.messages}
        isStreaming={socket.isStreaming}
        currentStreamText={socket.currentStreamText}
        currentThinkingText={socket.currentThinkingText}
        activeToolCalls={socket.activeToolCalls}
        scrollOffset={viewport.scrollOffset}
        viewportSize={viewport.viewportSize}
        isScrolledUp={viewport.isScrolledUp}
        highlightedMessageIndices={highlightedMessageIndices}
        currentHighlightIndex={currentHighlightIndex}
      />

      {isSearchMode && (
        <SearchBar
          query={search.query}
          onQueryChange={search.setQuery}
          totalMatches={search.totalMatches}
          currentMatch={search.currentMatchIndex}
          onNext={search.nextMatch}
          onPrev={search.prevMatch}
          onClose={() => {
            search.clear();
            appMode.setMode('chat');
          }}
          focused={isSearchMode}
        />
      )}

      <InputBar
        value={tuiInput}
        onChange={(val: string) => {
          // Suppress the character that ink-text-input leaks when a Ctrl+key
          // combo fires (e.g. Ctrl+T inserts 't'). The ctrlJustFired ref is
          // set synchronously in the useInput handler and cleared via a
          // microtask, so this callback sees it as still true on the same
          // event-loop tick.
          if (ctrlJustFired.current) {
            ctrlJustFired.current = false;
            return;
          }
          setTuiInput(val);
        }}
        onSubmit={sendMessageWithFileRefs}
        onSystemMessage={socket.addSystemMessage}
        onLocalCommand={handleLocalCommand}
        onGatewayCommand={handleGatewayCommand}
        isStreaming={socket.isStreaming}
        connected={socket.connected}
        focused={appMode.mode === 'chat'}
        placeholder={inputPlaceholder}
        allCommands={commandRegistry.getAll()}
      />
    </Box>
  );

  return (
    <Box flexDirection="column" height="100%">
      <Box marginTop={1} />
      <TopBar
        gatewayUrl={gatewayUrl}
        version={version}
        modelName={socket.modelName}
        thinkingLevel={socket.thinkingLevel}
        contextWindow={socket.tokenUsage.contextWindow}
        agentName={agentName ?? 'default'}
        connected={socket.connected}
        connecting={socket.connecting}
      />

      {appMode.sidebarOpen ? (
        <Box flexDirection="row" flexGrow={1}>
          <Sidebar
            conversations={conversations.conversations}
            activeConversationId={socket.conversationId}
            selectedIndex={sidebarSelectedIndex}
            onSelectIndex={setSidebarSelectedIndex}
            onSwitchConversation={handleSwitchConversation}
            onDeleteConversation={handleDeleteConversation}
            loading={conversations.loading}
            focused={appMode.mode === 'sidebar'}
            width={30}
          />
          {messageArea}
        </Box>
      ) : (
        <Box flexGrow={1}>{messageArea}</Box>
      )}

      <BottomBar
        gitInfo={gitInfo}
        tokenUsage={socket.tokenUsage}
        connected={socket.connected}
        connecting={socket.connecting}
        modelName={socket.modelName}
        providerName={socket.providerName}
        thinkingLevel={socket.thinkingLevel}
        conversationId={socket.conversationId}
        routingDecision={socket.routingDecision}
      />
    </Box>
  );
}
