import { useCallback, useEffect, useRef, useState } from 'react';
import {
  Link,
  useLocation,
  useNavigate,
  useParams,
  useSearchParams,
} from 'react-router-dom';
import { ChevronRight, FolderTree, History, Menu, X } from 'lucide-react';
import { api } from '@/api/client';
import type { Project, Session as SessionType } from '@/api/types';
import { sessionEvents } from '@/hooks/sessionEvents';
import { terminalsRegistry } from '@/lib/events';
import { useActivePane } from '@/hooks/useActivePane';
import { useSidebarDrawer } from '@/hooks/useSidebarDrawer';
import { useRightRailDrawer } from '@/hooks/useRightRailDrawer';
import { useViewport } from '@/hooks/useViewport';
import { useWorkspacePanes, MAX_PANES } from '@/hooks/useWorkspacePanes';
import { useSessionChats } from '@/hooks/useSessionChats';
import { useProjectGit } from '@/hooks/useProjectGit';
import { Workspace } from '@/components/Workspace';
import { ModelPicker } from '@/components/ModelPicker';
import { MobileTabSwitcher } from '@/components/MobileTabSwitcher';
import { NewPaneMenu } from '@/components/NewPaneMenu';
import { cn } from '@/lib/utils';

export function Session() {
  const { id } = useParams<{ id: string }>();
  if (!id) return null;
  // v1.8: key on id so route navigation remounts SessionInner — the hoisted
  // useWorkspacePanes + useSessionChats then reinitialize cleanly from the
  // new sessionId instead of carrying stale state across sessions.
  return <SessionInner key={id} sessionId={id} />;
}

function SessionInner({ sessionId }: { sessionId: string }) {
  const navigate = useNavigate();
  const location = useLocation();
  const [searchParams] = useSearchParams();
  const [session, setSession] = useState<SessionType | null>(null);
  const [project, setProject] = useState<Project | null>(null);
  const [name, setName] = useState('');
  const [editingName, setEditingName] = useState(false);
  const active = useActivePane();
  const { setOpen: setDrawerOpen } = useSidebarDrawer();
  const { toggle: toggleRightRail } = useRightRailDrawer();
  const { isMobile } = useViewport();

  // v1.8: pane + chat state hoisted into Session so the mobile header pill
  // (MobileTabSwitcher) shares one source of truth with the pane grid below.
  const panesHook = useWorkspacePanes(sessionId);
  const {
    panes,
    activePaneIdx,
    setActivePaneIdx,
    openChatInPane,
    activePaneIdxRef,
    addSplitPane,
    removePane,
    openSessionHistory,
    removeChatFromPanes,
    initializeFirstChatIfEmpty,
    validatePanes,
  } = panesHook;
  const activePane = panes[activePaneIdx];

  const openChatInActivePane = useCallback(
    (chatId: string) => openChatInPane(activePaneIdxRef.current, chatId),
    [openChatInPane, activePaneIdxRef],
  );
  const chatsHook = useSessionChats(sessionId, {
    removeChatFromPanes,
    openChatInPane,
    openChatInActivePane,
    initializeFirstChatIfEmpty,
    validatePanes,
  });
  const { chats, renameChat } = chatsHook;

  // v2.3: fix hydrate race — if workspace hydrate clobbers the chat-pane
  // promotion (panes[0] is still 'empty' while an open chat exists),
  // re-promote immediately. Guarded by a ref to avoid infinite loops.
  const promotedRef = useRef(false);
  useEffect(() => {
    if (panes.length !== 1 || panes[0]?.kind !== 'empty') return;
    const openChat = chats.find((c) => c.status === 'open');
    if (!openChat) return;
    if (promotedRef.current) return;
    promotedRef.current = true;
    initializeFirstChatIfEmpty(openChat.id);
  }, [panes, chats, initializeFirstChatIfEmpty]);

  // v1.8 Level 1: branch indicator. Polls every 30s; server caches the same
  // span so back-to-back loads are cheap. Returns null until the first fetch
  // resolves or if the project isn't a git repo.
  const git = useProjectGit(project?.id);

  useEffect(() => {
    setSession(null);
    setProject(null);
    let cancelled = false;
    api.sessions
      .get(sessionId)
      .then((s) => {
        if (cancelled) return;
        setSession(s);
        setName(s.name);
        sessionEvents.emit({
          type: 'session_loaded',
          session_id: sessionId,
          project_id: s.project_id,
        });
        api.projects.list().then((projects) => {
          if (cancelled) return;
          const p = projects.find((x) => x.id === s.project_id);
          if (p) setProject(p);
        }).catch((err) => console.warn('Session: failed to load project for breadcrumb', err));
      })
      .catch((err) => console.warn('Session: failed to fetch session', err));
    return () => {
      cancelled = true;
    };
  }, [sessionId]);

  // v2.3: opening the settings pane on mobile must push ?pane= atomically, or
  // the URL-sync effect below snaps activePaneIdx back to the chat pane and the
  // settings pane never shows (same fix as addPaneAndSwitch). toggleSettingsPane
  // returns the new pane id when it opens (null when it closes → drop ?pane= so
  // the effect falls back to pane 0). Desktop has no URL pane state — no-op.
  const toggleSettingsAndSync = useCallback(() => {
    const openedId = panesHook.toggleSettingsPane();
    if (!isMobile) return;
    const params = new URLSearchParams(location.search);
    if (openedId) params.set('pane', openedId);
    else params.delete('pane');
    navigate(`${location.pathname}?${params.toString()}`);
  }, [panesHook, isMobile, navigate, location.pathname, location.search]);

  useEffect(() => {
    return sessionEvents.subscribe((event) => {
      if (event.type === 'session_renamed' && event.session_id === sessionId) {
        setSession((prev) => (prev ? { ...prev, name: event.name } : prev));
        setName((prev) => (editingName ? prev : event.name));
        return;
      }
      if (
        (event.type === 'session_deleted' || event.type === 'session_archived') &&
        event.session_id === sessionId
      ) {
        navigate(`/project/${event.project_id}`);
        return;
      }
      // v1.9: any session_updated for this session triggers a full refetch so
      // SettingsPane (mounted in a workspace pane) picks up system_prompt /
      // web_search_enabled / model edits made from another tab.
      if (event.type === 'session_updated' && event.session_id === sessionId) {
        void api.sessions.get(sessionId).then((s) => {
          setSession(s);
          setName((prev) => (editingName ? prev : s.name));
        }).catch(() => {});
        return;
      }
      // v1.9: project_updated → refetch project so the Project section in
      // SettingsPane reflects the new defaults.
      if (event.type === 'project_updated' && project && event.project_id === project.id) {
        void api.projects.get(project.id).then(setProject).catch(() => {});
        return;
      }
      // Sidebar Settings button broadcasts this when a session is mounted;
      // toggleSettingsPane opens on first click, closes on second.
      if (event.type === 'open_settings_pane') {
        toggleSettingsAndSync();
      }
    });
  }, [sessionId, editingName, navigate, project, toggleSettingsAndSync]);

  // v1.8: URL ?pane= sync (mobile only). Lifted from Workspace.tsx so
  // MobileTabSwitcher's onSwitchPane can push the same URL state and the
  // browser Back button continues to walk pane history on mobile.
  useEffect(() => {
    if (!isMobile || panes.length === 0) return;
    const paneId = searchParams.get('pane');
    if (!paneId) {
      if (activePaneIdx !== 0) setActivePaneIdx(0);
      return;
    }
    const idx = panes.findIndex((p) => p.id === paneId);
    if (idx >= 0 && idx !== activePaneIdx) setActivePaneIdx(idx);
  }, [isMobile, searchParams, panes, activePaneIdx, setActivePaneIdx]);

  const switchActivePane = useCallback(
    (idx: number) => {
      setActivePaneIdx(idx);
      if (isMobile) {
        const pane = panes[idx];
        if (!pane) return;
        const params = new URLSearchParams(location.search);
        params.set('pane', pane.id);
        navigate(`${location.pathname}?${params.toString()}`);
      }
    },
    [setActivePaneIdx, isMobile, panes, navigate, location.pathname, location.search],
  );

  // v1.10.3 fix: addSplitPane sets activePaneIdx, but on mobile the URL-sync
  // effect below sees a stale ?pane= and immediately resets the index. Push
  // the new pane's id to the URL atomically so the effect's next pass sees a
  // matching id and is a no-op. Desktop has no URL pane state — fall through.
  const addPaneAndSwitch = useCallback(
    (kind: 'chat' | 'terminal' | 'coder') => {
      const newPaneId = addSplitPane(kind);
      if (newPaneId === null) return;
      if (isMobile) {
        const params = new URLSearchParams(location.search);
        params.set('pane', newPaneId);
        navigate(`${location.pathname}?${params.toString()}`);
      }
    },
    [addSplitPane, isMobile, navigate, location.pathname, location.search],
  );

  const activePaneKind = panes[activePaneIdx]?.kind;
  const showSessionModelPicker = activePaneKind !== 'coder';

  // v1.10.3 keyboard shortcuts. Window-level keydown so they fire from
  // anywhere in the session view. Only Cmd/Ctrl-Shift-C defers to the xterm
  // (which has its own copy binding for that combo); everything else fires
  // regardless of focus. Cmd-W and Cmd-T are typically reserved by the
  // browser — preventDefault() works in most browsers but not all.
  useEffect(() => {
    function onKey(e: KeyboardEvent): void {
      const mod = e.ctrlKey || e.metaKey;
      if (!mod) return;
      const key = e.key.toLowerCase();
      const target = e.target;
      const inXterm = target instanceof Element && target.closest('.xterm') !== null;

      // Cmd/Ctrl + ` — focus the active terminal or jump to the most recent
      // terminal pane and focus it. No-op if there are no terminal panes.
      if (key === '`') {
        e.preventDefault();
        const activePane = panes[activePaneIdx];
        if (activePane?.kind === 'terminal') {
          terminalsRegistry.get(activePane.id)?.focus();
          return;
        }
        let lastTermIdx = -1;
        for (let i = panes.length - 1; i >= 0; i--) {
          if (panes[i]?.kind === 'terminal') {
            lastTermIdx = i;
            break;
          }
        }
        if (lastTermIdx < 0) return;
        const target = panes[lastTermIdx];
        switchActivePane(lastTermIdx);
        if (target) {
          // The terminal may have just mounted on mobile (it was return-null
          // before the switch). Defer focus until the new render commits.
          setTimeout(() => terminalsRegistry.get(target.id)?.focus(), 80);
        }
        return;
      }

      // Cmd/Ctrl + Shift + T — new terminal pane and switch to it.
      if (key === 't' && e.shiftKey) {
        e.preventDefault();
        addPaneAndSwitch('terminal');
        return;
      }

      // Cmd/Ctrl + Shift + C — new chat pane and switch to it. The xterm's
      // own Shift-C binding is "copy selection" — defer to it when in xterm.
      if (key === 'c' && e.shiftKey) {
        if (inXterm) return;
        e.preventDefault();
        addPaneAndSwitch('chat');
        return;
      }

      // Cmd/Ctrl + W — close the active pane.
      if (key === 'w' && !e.shiftKey) {
        e.preventDefault();
        removePane(activePaneIdx);
        return;
      }

      // v1.10.4: Cmd/Ctrl + F — when the active pane is a terminal, open the
      // scrollback search bar. When it isn't, fall through to the browser's
      // native find (no preventDefault, no early return).
      if (key === 'f' && !e.shiftKey) {
        const activePane = panes[activePaneIdx];
        if (activePane?.kind === 'terminal') {
          e.preventDefault();
          terminalsRegistry.get(activePane.id)?.openSearch();
        }
        return;
      }

      // Cmd/Ctrl + Tab / Shift+Tab — cycle through panes.
      if (key === 'tab') {
        if (panes.length <= 1) return;
        e.preventDefault();
        const dir = e.shiftKey ? -1 : 1;
        const next = (activePaneIdx + dir + panes.length) % panes.length;
        switchActivePane(next);
        return;
      }

      // Cmd/Ctrl + 1..9 — direct jump to pane N.
      if (/^[1-9]$/.test(key)) {
        const idx = parseInt(key, 10) - 1;
        if (idx < panes.length) {
          e.preventDefault();
          switchActivePane(idx);
        }
        return;
      }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [panes, activePaneIdx, switchActivePane, addPaneAndSwitch, removePane]);

  async function saveName() {
    if (!session) return;
    const trimmed = name.trim();
    if (!trimmed || trimmed === session.name) {
      setName(session.name);
      setEditingName(false);
      return;
    }
    const updated = await api.sessions.update(sessionId, { name: trimmed });
    setSession(updated);
    setEditingName(false);
    // Server publishes session_renamed via broker.publishUser; no local emit needed.
  }

  // Workspace only sets activeFile for file-browser panes; checking it alone
  // suffices and is forward-compatible with future pane kinds.
  const showActiveFile = active.sessionId === sessionId && !!active.activeFile;

  return (
    <div className="flex-1 flex flex-col min-h-0">
      <header
        className={cn(
          'border-b shrink-0 text-sm',
          isMobile
            ? 'flex flex-col gap-1 px-3 py-1'
            : 'flex items-center gap-1.5 px-3 sm:px-4 py-2',
        )}
        style={{ paddingTop: 'max(0.5rem, env(safe-area-inset-top))' }}
      >
        {isMobile ? (
          <>
            {/* v1.8 mobile row 1: hamburger | repo+branch | ModelPicker | FolderTree.
                Gear/kebab cluster lands in Batch 7; ModelPicker stays here until
                then so mobile users keep model-switching access. */}
            <div className="flex items-center gap-1.5">
              <button
                type="button"
                onClick={() => setDrawerOpen(true)}
                className="inline-flex items-center justify-center -ml-1 min-w-[44px] min-h-[36px] rounded text-muted-foreground hover:bg-muted hover:text-foreground shrink-0"
                aria-label="Open sidebar"
              >
                <Menu className="size-5" />
              </button>

              <div className="flex-1 min-w-0 flex items-center justify-center gap-1.5">
                {project ? (
                  <span className="text-sm font-medium truncate" title={project.name}>
                    {project.name}
                  </span>
                ) : (
                  <span className="text-muted-foreground/60">…</span>
                )}
                {git?.branch && (
                  <span
                    className="text-muted-foreground/80 text-xs truncate"
                    title={`branch: ${git.branch}${git.is_dirty ? ' (dirty)' : ''}`}
                  >
                    · {git.branch}
                  </span>
                )}
              </div>

              {session && showSessionModelPicker && (
                <ModelPicker
                  value={session.model}
                  onChange={async (model) => {
                    const updated = await api.sessions.update(session.id, { model });
                    setSession(updated);
                  }}
                />
              )}

              <button
                type="button"
                onClick={toggleRightRail}
                className="relative inline-flex items-center justify-center -mr-1 min-w-[44px] min-h-[36px] rounded text-muted-foreground hover:bg-muted hover:text-foreground shrink-0"
                aria-label="Toggle file browser"
              >
                <FolderTree className="size-5" />
                {git?.is_dirty && (
                  <span className="absolute top-2 right-2 w-1.5 h-1.5 rounded-full bg-yellow-400" aria-hidden="true" />
                )}
              </button>
            </div>

            {/* v1.8 mobile row 2: pane-switcher pill + new-pane menu. Pill
                expands; NewPaneMenu is the trailing 44x44 trigger. */}
            <div className="flex items-center gap-1.5">
              <MobileTabSwitcher
                panes={panes}
                activePaneIdx={activePaneIdx}
                chats={chats}
                onSwitchPane={switchActivePane}
                onRemovePane={removePane}
                onRenameChat={renameChat}
              />
              <NewPaneMenu
                onAddPane={addPaneAndSwitch}
                disabled={panes.length >= MAX_PANES}
                projectId={session?.project_id}
              />
              {activePane && panes.length > 1 && (
                <button
                  type="button"
                  onClick={() => removePane(activePaneIdx)}
                  className="inline-flex items-center justify-center min-h-[36px] min-w-[44px] rounded-full bg-muted/40 hover:bg-muted/70 text-foreground shrink-0"
                  aria-label="Close pane"
                >
                  <X size={16} />
                </button>
              )}
              <button
                type="button"
                onClick={() => openSessionHistory(activePaneIdx)}
                className="inline-flex items-center justify-center min-h-[36px] min-w-[44px] rounded-full bg-muted/40 hover:bg-muted/70 text-foreground shrink-0"
                aria-label="Session history"
                title="Session history"
              >
                <History size={16} />
              </button>
            </div>
          </>
        ) : (
          <>
            {/* Desktop: unchanged single-row header. */}
            <div className="hidden sm:flex items-center gap-1.5 min-w-0">
              <Link to="/" className="text-muted-foreground hover:text-foreground shrink-0 text-xs">
                Projects
              </Link>
              <ChevronRight className="size-3 text-muted-foreground/60 shrink-0" />
              {project ? (
                <Link
                  to={`/project/${project.id}`}
                  className="text-muted-foreground hover:text-foreground truncate max-w-[200px]"
                  title={project.name}
                >
                  {project.name}
                </Link>
              ) : (
                <span className="text-muted-foreground/60">…</span>
              )}
              <ChevronRight className="size-3 text-muted-foreground/60 shrink-0" />
            </div>

            {editingName ? (
              <input
                autoFocus
                value={name}
                onChange={(e) => setName(e.target.value)}
                onBlur={() => void saveName()}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') void saveName();
                  if (e.key === 'Escape') {
                    setName(session?.name ?? '');
                    setEditingName(false);
                  }
                }}
                className="bg-transparent border-b border-border px-1 py-0.5 text-sm font-medium outline-none focus:border-ring min-w-0"
              />
            ) : (
              <button
                type="button"
                className="text-sm font-medium hover:underline truncate max-w-[280px] min-w-0"
                onClick={() => setEditingName(true)}
                title={session?.name ?? ''}
              >
                {session?.name ?? '…'}
              </button>
            )}

            {showActiveFile && active.activeFile && (
              <>
                <span className="text-muted-foreground/40 mx-1">·</span>
                <span
                  className="text-xs font-mono text-muted-foreground truncate max-w-[200px]"
                  title={active.activeFile}
                >
                  {active.activeFile}
                </span>
              </>
            )}

            <div className="ml-auto shrink-0">
              {session && showSessionModelPicker && (
                <div className="inline-flex items-center rounded-full bg-muted/40 hover:bg-muted/70 px-1">
                  <ModelPicker
                    value={session.model}
                    onChange={async (model) => {
                      const updated = await api.sessions.update(session.id, { model });
                      setSession(updated);
                    }}
                  />
                </div>
              )}
            </div>
          </>
        )}
      </header>

      {session && (
        <Workspace
          sessionId={sessionId}
          projectId={session.project_id}
          agentId={session.agent_id}
          onAgentChange={async (agent_id) => {
            const updated = await api.sessions.update(session.id, { agent_id });
            setSession(updated);
          }}
          panesHook={panesHook}
          chatsHook={chatsHook}
          session={session}
          project={project}
          onAddPane={addPaneAndSwitch}
        />
      )}
    </div>
  );
}
