import { html, useState, useEffect, useCallback, useMemo, useRef } from '../lib/preact.js';
import { API_STATE, API_STREAM, API_DISMISS, API_DISMISS_DEAD, API_RESPOND, API_CONVERSATION, API_HEALTH, POLL_MS, fetchWithTimeout } from '../utils/api.js';
import { groupSessionsByProject } from '../utils/status.js';
import { Sidebar } from './Sidebar.js';
import { SessionCard } from './SessionCard.js';
import { Modal } from './Modal.js';
import { EmptyState } from './EmptyState.js';
import { ToastContainer, showToast, trackError, clearErrorCount } from './Toast.js';
import { SpawnModal } from './SpawnModal.js';

let optimisticMsgId = 0;

export function App() {
  const [sessions, setSessions] = useState([]);
  const [modalSession, setModalSession] = useState(null);
  const [conversations, setConversations] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [selectedProject, setSelectedProject] = useState(null);
  const [sseConnected, setSseConnected] = useState(false);
  const [deadSessionsCollapsed, setDeadSessionsCollapsed] = useState(true);
  const [spawnModalOpen, setSpawnModalOpen] = useState(false);
  const [zellijAvailable, setZellijAvailable] = useState(true);
  const [newlySpawnedIds, setNewlySpawnedIds] = useState(new Set());
  const pendingSpawnIdsRef = useRef(new Set());

  // Background conversation refresh with error tracking
  const refreshConversationSilent = useCallback(async (sessionId, projectDir, agent = 'claude') => {
    try {
      let url = API_CONVERSATION + encodeURIComponent(sessionId);
      const params = new URLSearchParams();
      if (projectDir) params.set('project_dir', projectDir);
      if (agent) params.set('agent', agent);
      if (params.toString()) url += '?' + params.toString();
      const response = await fetch(url);
      if (!response.ok) {
        trackError(`conversation-${sessionId}`, `Failed to fetch conversation (HTTP ${response.status})`);
        return;
      }
      const data = await response.json();
      setConversations(prev => ({
        ...prev,
        [sessionId]: data.messages || []
      }));
      clearErrorCount(`conversation-${sessionId}`); // Clear on success
    } catch (err) {
      trackError(`conversation-${sessionId}`, `Failed to fetch conversation: ${err.message}`);
    }
  }, []);

  // Track last_event_at for each session to detect actual changes
  const lastEventAtRef = useRef({});

  // Refs for stable callback access (avoids recreation on state changes)
  const sessionsRef = useRef(sessions);
  const conversationsRef = useRef(conversations);
  const modalSessionRef = useRef(null);
  sessionsRef.current = sessions;
  conversationsRef.current = conversations;

  // Apply state payload from polling or SSE stream
  const applyStateData = useCallback((data) => {
    const newSessions = data.sessions || [];
    const newSessionIds = new Set(newSessions.map(s => s.session_id));
    setSessions(newSessions);
    setError(null);

    // Update modalSession if it's still open (to get latest pending_questions, etc.)
    const modalId = modalSessionRef.current;
    if (modalId) {
      const updatedSession = newSessions.find(s => s.session_id === modalId);
      if (updatedSession) {
        setModalSession(updatedSession);
      }
    }

    // Check for newly spawned sessions matching pending spawn IDs
    if (pendingSpawnIdsRef.current.size > 0) {
      const matched = new Set();
      for (const session of newSessions) {
        if (session.spawn_id && pendingSpawnIdsRef.current.has(session.spawn_id)) {
          matched.add(session.session_id);
          pendingSpawnIdsRef.current.delete(session.spawn_id);
        }
      }
      if (matched.size > 0) {
        setNewlySpawnedIds(prev => {
          const next = new Set(prev);
          for (const id of matched) next.add(id);
          return next;
        });
        // Auto-clear highlight after animation duration (2.5s)
        for (const id of matched) {
          setTimeout(() => {
            setNewlySpawnedIds(prev => {
              const next = new Set(prev);
              next.delete(id);
              return next;
            });
          }, 2500);
        }
      }
    }

    // Clean up conversation cache for sessions that no longer exist
    setConversations(prev => {
      const activeIds = Object.keys(prev).filter(id => newSessionIds.has(id));
      if (activeIds.length === Object.keys(prev).length) return prev; // No cleanup needed
      const cleaned = {};
      for (const id of activeIds) {
        cleaned[id] = prev[id];
      }
      return cleaned;
    });

    // Refresh conversations for sessions that have actually changed
    // Use conversation_mtime_ns for real-time updates (changes on every file write),
    // falling back to last_event_at for sessions without mtime tracking
    const prevEventMap = lastEventAtRef.current;
    const nextEventMap = {};

    for (const session of newSessions) {
      const id = session.session_id;
      // Prefer mtime (changes on every write) over last_event_at (only on hook events)
      const newKey = session.conversation_mtime_ns || session.last_event_at || '';
      nextEventMap[id] = newKey;

      const oldKey = prevEventMap[id] || '';
      if (newKey !== oldKey) {
        refreshConversationSilent(session.session_id, session.project_dir, session.agent || 'claude');
      }
    }
    lastEventAtRef.current = nextEventMap;

    setLoading(false);
  }, [refreshConversationSilent]);

  // Fetch state from API
  const fetchState = useCallback(async () => {
    try {
      const response = await fetchWithTimeout(API_STATE);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const data = await response.json();
      applyStateData(data);
      clearErrorCount('state-fetch');
    } catch (err) {
      const msg = err.name === 'AbortError' ? 'Request timed out' : err.message;
      trackError('state-fetch', `Failed to fetch state: ${msg}`);
      setError(msg);
      setLoading(false);
    }
  }, [applyStateData]);

  // Fetch conversation for a session (explicit fetch, e.g., on modal open)
  const fetchConversation = useCallback(async (sessionId, projectDir, agent = 'claude', force = false) => {
    // Skip if already fetched and not forcing refresh
    if (!force && conversationsRef.current[sessionId]) return;

    try {
      let url = API_CONVERSATION + encodeURIComponent(sessionId);
      const params = new URLSearchParams();
      if (projectDir) params.set('project_dir', projectDir);
      if (agent) params.set('agent', agent);
      if (params.toString()) url += '?' + params.toString();
      const response = await fetch(url);
      if (!response.ok) {
        trackError(`conversation-${sessionId}`, `Failed to fetch conversation (HTTP ${response.status})`);
        return;
      }
      const data = await response.json();
      setConversations(prev => ({
        ...prev,
        [sessionId]: data.messages || []
      }));
      clearErrorCount(`conversation-${sessionId}`);
    } catch (err) {
      trackError(`conversation-${sessionId}`, `Error fetching conversation: ${err.message}`);
    }
  }, []);

  // Respond to a session's pending question with optimistic update
  const respondToSession = useCallback(async (sessionId, text, isFreeform = false, optionCount = 0) => {
    const payload = { text };
    if (isFreeform) {
      payload.freeform = true;
      payload.optionCount = optionCount;
    }

    // Optimistic update: immediately show user's message
    const optimisticMsg = {
      id: `optimistic-${++optimisticMsgId}`,
      role: 'user',
      content: text,
      timestamp: new Date().toISOString(),
      _optimistic: true, // Flag for identification
    };
    setConversations(prev => ({
      ...prev,
      [sessionId]: [...(prev[sessionId] || []), optimisticMsg]
    }));

    try {
      const res = await fetch(API_RESPOND + encodeURIComponent(sessionId), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
      const data = await res.json();
      if (!data.ok) {
        throw new Error(data.error || 'Failed to send response');
      }
      clearErrorCount(`respond-${sessionId}`);
      // SSE will push state update when Claude processes the message,
      // which triggers conversation refresh via applyStateData
    } catch (err) {
      // Remove optimistic message on failure
      setConversations(prev => ({
        ...prev,
        [sessionId]: (prev[sessionId] || []).filter(m => m !== optimisticMsg)
      }));
      trackError(`respond-${sessionId}`, `Failed to send message: ${err.message}`);
      throw err; // Re-throw so SimpleInput/QuestionBlock can catch and show error
    }
  }, []);

  // Dismiss a session
  const dismissSession = useCallback(async (sessionId) => {
    try {
      const res = await fetch(API_DISMISS + encodeURIComponent(sessionId), {
        method: 'POST'
      });
      const data = await res.json();
      if (data.ok) {
        // Trigger refresh
        fetchState();
      } else {
        trackError(`dismiss-${sessionId}`, `Failed to dismiss session: ${data.error || 'Unknown error'}`);
      }
    } catch (err) {
      trackError(`dismiss-${sessionId}`, `Error dismissing session: ${err.message}`);
    }
  }, [fetchState]);

  // Dismiss all dead sessions
  const dismissDeadSessions = useCallback(async () => {
    try {
      const res = await fetch(API_DISMISS_DEAD, { method: 'POST' });
      const data = await res.json();
      if (data.ok) {
        fetchState();
      } else {
        trackError('dismiss-dead', `Failed to clear completed sessions: ${data.error || 'Unknown error'}`);
      }
    } catch (err) {
      trackError('dismiss-dead', `Error clearing completed sessions: ${err.message}`);
    }
  }, [fetchState]);

  // Subscribe to live state updates via SSE
  useEffect(() => {
    let eventSource = null;
    let reconnectTimer = null;
    let stopped = false;

    const connect = () => {
      if (stopped) return;

      try {
        eventSource = new EventSource(API_STREAM);
      } catch (err) {
        trackError('sse-init', `Failed to initialize EventSource: ${err.message}`);
        setSseConnected(false);
        reconnectTimer = setTimeout(connect, 2000);
        return;
      }

      eventSource.addEventListener('open', () => {
        if (stopped) return;
        setSseConnected(true);
        setError(null);
        // Clear event cache on reconnect to force refresh of all conversations
        // (handles updates missed during disconnect)
        lastEventAtRef.current = {};
      });

      eventSource.addEventListener('state', (event) => {
        if (stopped) return;
        try {
          const data = JSON.parse(event.data);
          applyStateData(data);
          clearErrorCount('sse-parse');
        } catch (err) {
          trackError('sse-parse', `Failed to parse SSE state payload: ${err.message}`);
        }
      });

      eventSource.addEventListener('error', () => {
        if (stopped) return;
        setSseConnected(false);
        if (eventSource) {
          eventSource.close();
          eventSource = null;
        }
        if (!reconnectTimer) {
          reconnectTimer = setTimeout(() => {
            reconnectTimer = null;
            connect();
          }, 2000);
        }
      });
    };

    connect();

    return () => {
      stopped = true;
      if (reconnectTimer) {
        clearTimeout(reconnectTimer);
      }
      if (eventSource) {
        eventSource.close();
      }
    };
  }, [applyStateData]);

  // Poll for updates only when SSE is disconnected (fallback mode)
  useEffect(() => {
    if (sseConnected) return;

    fetchState();
    const interval = setInterval(fetchState, POLL_MS);
    return () => clearInterval(interval);
  }, [fetchState, sseConnected]);

  // Poll Zellij health status
  useEffect(() => {
    const checkHealth = async () => {
      try {
        const response = await fetchWithTimeout(API_HEALTH);
        if (response.ok) {
          const data = await response.json();
          setZellijAvailable(data.zellij_available);
        }
      } catch {
        // Server unreachable - handled by state fetch error
      }
    };

    checkHealth();
    const interval = setInterval(checkHealth, 30000);
    return () => clearInterval(interval);
  }, []);

  // Group sessions by project
  const projectGroups = groupSessionsByProject(sessions);

  // Filter sessions based on selected project
  const filteredGroups = useMemo(() => {
    if (selectedProject === null) {
      return projectGroups;
    }
    return projectGroups.filter(g => g.projectDir === selectedProject);
  }, [projectGroups, selectedProject]);

  // Split sessions into active and dead
  const { activeSessions, deadSessions } = useMemo(() => {
    const active = [];
    const dead = [];
    for (const group of filteredGroups) {
      for (const session of group.sessions) {
        if (session.is_dead) {
          dead.push(session);
        } else {
          active.push(session);
        }
      }
    }
    return { activeSessions: active, deadSessions: dead };
  }, [filteredGroups]);

  // Handle card click - open modal and fetch conversation if not cached
  const handleCardClick = useCallback(async (session) => {
    modalSessionRef.current = session.session_id;
    setModalSession(session);

    // Fetch conversation if not already cached
    if (!conversationsRef.current[session.session_id]) {
      await fetchConversation(session.session_id, session.project_dir, session.agent || 'claude');
    }
  }, [fetchConversation]);

  const handleCloseModal = useCallback(() => {
    modalSessionRef.current = null;
    setModalSession(null);
  }, []);

  const handleSelectProject = useCallback((projectDir) => {
    setSelectedProject(projectDir);
  }, []);

  const handleSpawnResult = useCallback((result) => {
    if (result.success) {
      showToast(`${result.agentType} agent spawned for ${result.project}`, 'success');
      if (result.spawnId) {
        pendingSpawnIdsRef.current.add(result.spawnId);
      }
    } else if (result.error) {
      showToast(result.error, 'error');
    }
  }, []);

  return html`
    <!-- Sidebar -->
    <${Sidebar}
      projectGroups=${projectGroups}
      selectedProject=${selectedProject}
      onSelectProject=${handleSelectProject}
      totalSessions=${sessions.length}
    />

    <!-- Main Content (offset for sidebar) -->
    <div class="ml-80 min-h-screen pb-10">
      <!-- Compact Header -->
      <header class="sticky top-0 z-30 border-b border-selection/50 bg-surface/95 px-6 py-4 backdrop-blur-sm">
        <div class="flex items-center justify-between">
          <div>
            <h2 class="font-display text-lg font-semibold text-bright">
              ${selectedProject === null ? 'All Projects' : filteredGroups[0]?.projectName || 'Project'}
            </h2>
            <p class="mt-0.5 font-mono text-micro text-dim">
              ${filteredGroups.reduce((sum, g) => sum + g.sessions.length, 0)} session${filteredGroups.reduce((sum, g) => sum + g.sessions.length, 0) === 1 ? '' : 's'}
              ${selectedProject !== null && filteredGroups[0]?.projectDir ? html` in <span class="text-dim/80">${filteredGroups[0].projectDir}</span>` : ''}
            </p>
          </div>
          <!-- Status summary chips -->
          <div class="flex items-center gap-2">
            ${(() => {
              const counts = { needs_attention: 0, active: 0, starting: 0, done: 0 };
              for (const g of filteredGroups) {
                for (const s of g.sessions) {
                  counts[s.status] = (counts[s.status] || 0) + 1;
                }
              }
              return html`
                ${counts.needs_attention > 0 && html`
                  <div class="rounded-lg border border-attention/40 bg-attention/12 px-2.5 py-1 text-attention">
                    <span class="font-mono text-sm font-medium tabular-nums">${counts.needs_attention}</span>
                    <span class="ml-1 text-micro uppercase tracking-wider">attention</span>
                  </div>
                `}
                ${counts.active > 0 && html`
                  <div class="rounded-lg border border-active/40 bg-active/12 px-2.5 py-1 text-active">
                    <span class="font-mono text-sm font-medium tabular-nums">${counts.active}</span>
                    <span class="ml-1 text-micro uppercase tracking-wider">active</span>
                  </div>
                `}
                ${counts.starting > 0 && html`
                  <div class="rounded-lg border border-starting/40 bg-starting/12 px-2.5 py-1 text-starting">
                    <span class="font-mono text-sm font-medium tabular-nums">${counts.starting}</span>
                    <span class="ml-1 text-micro uppercase tracking-wider">starting</span>
                  </div>
                `}
                ${counts.done > 0 && html`
                  <div class="rounded-lg border border-done/40 bg-done/12 px-2.5 py-1 text-done">
                    <span class="font-mono text-sm font-medium tabular-nums">${counts.done}</span>
                    <span class="ml-1 text-micro uppercase tracking-wider">done</span>
                  </div>
                `}
              `;
            })()}
          </div>
          <div class="relative">
            <button
              disabled=${!zellijAvailable}
              class="rounded-lg border border-active/40 bg-active/12 px-3 py-2 text-sm font-medium text-active transition-colors hover:bg-active/20 ${!zellijAvailable ? 'opacity-50 cursor-not-allowed' : ''}"
              onClick=${() => setSpawnModalOpen(true)}
            >
              + New Agent
            </button>
            <${SpawnModal}
              isOpen=${spawnModalOpen}
              onClose=${() => setSpawnModalOpen(false)}
              onSpawn=${handleSpawnResult}
              currentProject=${selectedProject}
            />
          </div>
        </div>
      </header>

      ${!zellijAvailable && html`
        <div class="border-b border-attention/50 bg-attention/10 px-6 py-2 text-sm text-attention">
          <span class="font-medium">Zellij session not found.</span>
          ${' '}Agent spawning is unavailable. Start Zellij with: <code class="rounded bg-attention/15 px-1.5 py-0.5 font-mono text-micro">zellij attach infra</code>
        </div>
      `}

      <main class="px-6 pb-6 pt-6">
        ${loading ? html`
          <div class="glass-panel animate-fade-in-up flex items-center justify-center rounded-2xl py-24">
            <div class="font-mono text-dim">Loading sessions...</div>
          </div>
        ` : error ? html`
          <div class="glass-panel animate-fade-in-up flex items-center justify-center rounded-2xl py-24">
            <div class="text-center">
              <p class="mb-2 font-display text-lg text-attention">Failed to connect to API</p>
              <p class="font-mono text-sm text-dim">${error}</p>
            </div>
          </div>
        ` : filteredGroups.length === 0 ? html`
          <${EmptyState} />
        ` : html`
          <!-- Active Sessions Grid -->
          ${activeSessions.length > 0 ? html`
            <div class="flex flex-wrap gap-4">
              ${activeSessions.map(session => html`
                <${SessionCard}
                  key=${session.session_id}
                  session=${session}
                  onClick=${handleCardClick}
                  conversation=${conversations[session.session_id]}
                  onFetchConversation=${fetchConversation}
                  onRespond=${respondToSession}
                  onDismiss=${dismissSession}
                  isNewlySpawned=${newlySpawnedIds.has(session.session_id)}
                />
              `)}
            </div>
          ` : deadSessions.length > 0 ? html`
            <div class="glass-panel flex items-center justify-center rounded-xl py-12 mb-6">
              <div class="text-center">
                <p class="font-display text-lg text-dim">No active sessions</p>
                <p class="mt-1 font-mono text-micro text-dim/70">All sessions have completed</p>
              </div>
            </div>
          ` : ''}

          <!-- Completed Sessions (Dead) - Collapsible -->
          ${deadSessions.length > 0 && html`
            <div class="mt-8">
              <button
                onClick=${() => setDeadSessionsCollapsed(!deadSessionsCollapsed)}
                class="group flex w-full items-center gap-3 rounded-lg border border-selection/50 bg-surface/50 px-4 py-3 text-left transition-colors hover:border-selection hover:bg-surface/80"
              >
                <svg
                  class="h-4 w-4 text-dim transition-transform ${deadSessionsCollapsed ? '' : 'rotate-90'}"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
                </svg>
                <span class="font-display text-sm font-medium text-dim">
                  Completed Sessions
                </span>
                <span class="rounded-full bg-done/15 px-2 py-0.5 font-mono text-micro tabular-nums text-done/70">
                  ${deadSessions.length}
                </span>
                <div class="flex-1"></div>
                <button
                  onClick=${(e) => { e.stopPropagation(); dismissDeadSessions(); }}
                  class="rounded-lg border border-selection/80 bg-bg/40 px-3 py-1.5 font-mono text-micro text-dim transition-colors hover:border-done/40 hover:bg-done/10 hover:text-bright"
                >
                  Clear All
                </button>
              </button>

              ${!deadSessionsCollapsed && html`
                <div class="mt-4 flex flex-wrap gap-4">
                  ${deadSessions.map(session => html`
                    <${SessionCard}
                      key=${session.session_id}
                      session=${session}
                      onClick=${handleCardClick}
                      conversation=${conversations[session.session_id]}
                      onFetchConversation=${fetchConversation}
                      onRespond=${respondToSession}
                      onDismiss=${dismissSession}
                    />
                  `)}
                </div>
              `}
            </div>
          `}
        `}
      </main>
    </div>

    <${Modal}
      session=${modalSession}
      conversations=${conversations}
      onClose=${handleCloseModal}
      onFetchConversation=${fetchConversation}
      onRespond=${respondToSession}
      onDismiss=${dismissSession}
    />

    <${ToastContainer} />
  `;
}
