import React, {useEffect, useRef, useState} from 'react';
import {Outlet, useLocation, useNavigate} from 'react-router-dom';
import Header from './components/layout/Header/Header';
import { InlineCopilotProvider, useInlineCopilot } from './hooks/useInlineCopilot';
import {BreadcrumbProvider} from './components/layout/Header/BreadcrumbContext';
import {ThemeProvider} from './design-system/ThemeProvider';
import BootSplashController from './components/BootSplashController';
import {useSceneManager} from './hooks/useSceneManager';
import {useRefetchOnRevive} from './hooks/useRefetchOnRevive';
import {fetchScenes} from './store/sceneSlice';
import {toast} from 'sonner';
import { useTranslation } from 'react-i18next';
import {useDispatch, useSelector} from 'react-redux';
import {AppDispatch, RootState} from './store/stores';
import { setChatPanelVisible, showChatPanel, hideChatPanel } from './store/chatPanelSlice';
import { useUiSurface } from './store/uiSurface/registerSurface';
import { selectEntity } from './store/entitySlice';
import './styles/sonner.css';
import EventNotificationChannel from './services/EventNotificationChannel';
import { watchManager } from './services/WatchManager';
import { WebSocketManager } from './utils/WebSocketManager';
import { WATCH_PATH } from './services/WatchChannel';
import { useWsStatusStore } from './services/wsStatusStore';
import { notifyWarning, notifyError } from './utils/notify';
import { setNavigator, clearNavigator } from './services/navigation';
import { handleUrlIntent } from './services/intent';
import { warmChunksOnIdle } from './utils/idlePrefetch';
import EditionGate from './components/auth/EditionGate';
import {usePermissions} from './contexts/PermissionContext.utils';

// Lazy-loaded ambient store — the 729-line zustand module + backoff controller
// stays out of the entry chunk entirely for scenes that haven't opted into
// ambient intelligence. See the `ambientEnabled` useEffect below.
type AmbientStoreModule = typeof import('./store/copilot/ambientStore');

// Lazy-loaded panels and dialogs
const ModelPanelSideDock = React.lazy(() => import('./components/layout/SideDock/ModelPanelSideDock'));
// Shared import thunks so the React.lazy declaration and the idle-prefetch warm
// list (see the warmChunksOnIdle effect) reference the exact same chunk — no
// duplicated path that could drift.
const loadEntityPanel = () => import('./components/info/EntityPanel');
const loadModelPanel = () => import('./components/model/ModelPanel');
const loadCopilotPanel = () => import('./components/copilot/CopilotPanel');
const EntityPanel = React.lazy(loadEntityPanel);
const ModelPanel = React.lazy(loadModelPanel);
const CopilotPanel = React.lazy(loadCopilotPanel);
const InlineCopilot = React.lazy(() => import('./components/copilot/InlineCopilot'));
const TraceDetailsPanel = React.lazy(() => import('./components/tracing/TraceDetailsPanel'));
const MissionTimeline = React.lazy(() => import('./components/observability/MissionTimeline'));
const InteractionDialogHost = React.lazy(() => import('./components/interaction/InteractionDialogHost'));
const SessionSpotlight = React.lazy(() =>
  import('./components/session/SessionSpotlight').then((m) => ({ default: m.SessionSpotlight })),
);
const SessionPresentation = React.lazy(() =>
  import('./components/session/SessionPresentation').then((m) => ({ default: m.SessionPresentation })),
);
const SessionDockHost = React.lazy(() =>
  import('./components/session/SessionDockHost').then((m) => ({ default: m.SessionDockHost })),
);
const DebugPanel = React.lazy(() => import('./components/debug/DebugPanel'));
const WorkflowActivationDialogHost = React.lazy(() =>
  import('./components/workflow/activation').then(m => ({ default: m.WorkflowActivationDialogHost }))
);
const AmbientNotificationStack = React.lazy(() => import('./components/ambient/AmbientNotificationStack'));
const AmbientTimelineBar = React.lazy(() => import('./components/ambient/AmbientTimelineBar'));
// Genie-button quick-access surface mounted globally on every authenticated
// route. Internally splits into an entry chunk (this lazy import) and a
// second lazy chunk for the expanded panel content (loaded on first click).
const AgenticFamilyHub = React.lazy(() => import('./components/agentic/AgenticFamilyHub'));
const DiagramPanel = React.lazy(() => import('./components/diagram/DiagramPanel'));
// Toasts are fire-and-forget and only render on demand — deferring the
// sonner library from the entry chunk avoids shipping it before first paint.
const Toaster = React.lazy(() => import('sonner').then(m => ({ default: m.Toaster })));

/**
 * True when ambient intelligence is explicitly enabled on the current scene
 * (`configurations.ambient.sensitivity === 'LOW' | 'HIGH'`). Every other
 * state — `'OFF'`, `null`, missing ambient key, missing configurations,
 * missing scene — reads as disabled.
 *
 * This matches the Settings UI, which maps both `null` and `'OFF'` to the
 * same "Off" slider position (see SettingsModal.getSliderPosition), so in
 * practice most scenes have `configurations.ambient === undefined` and
 * should be treated as disabled.
 *
 * Used to gate both the AmbientTimelineBar render and the ambient SSE
 * connection (`/api/crew/sse/ambient/stream`) — neither should run on
 * scenes where the user hasn't opted in.
 */
const selectAmbientEnabled = (state: RootState): boolean => {
  const ambient = state.scene.currentScene?.configurations?.ambient;
  if (ambient && typeof ambient === 'object' && 'sensitivity' in ambient) {
    const sensitivity = (ambient as { sensitivity?: string | null }).sensitivity;
    return sensitivity === 'LOW' || sensitivity === 'HIGH';
  }
  return false;
};

const InlineCopilotHost: React.FC = () => {
  const { state, onOpenChange, openDefault } = useInlineCopilot();

  useEffect(() => {
    const isEditableTarget = (target: EventTarget | null) => {
      if (!(target instanceof HTMLElement)) return false;
      const tag = target.tagName.toLowerCase();
      return tag === 'input' || tag === 'textarea' || target.isContentEditable;
    };

    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.defaultPrevented) return;
      if (isEditableTarget(e.target)) return;
      if (!e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
      if (e.key.toLowerCase() !== 'i') return;

      e.preventDefault();
      openDefault();
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [openDefault]);

  return (
    <React.Suspense fallback={null}>
      <InlineCopilot
        open={state.open}
        onOpenChange={onOpenChange}
        context={state.context}
        agentId={state.agentId}
        quickActions={state.quickActions}
        placeholder={state.placeholder}
        namespace={state.namespace}
      />
    </React.Suspense>
  );
};

const App: React.FC = () => {
  const { t } = useTranslation();
  const dispatch = useDispatch<AppDispatch>();
  const location = useLocation();
  const navigate = useNavigate();
  const {edition} = usePermissions();
  const isSpectator = edition === 'preview';
  const isAgenticPage = location.pathname.startsWith('/agentic');
  // Routes that query the observability/agentic substrate directly and
  // don't depend on a current scene. We don't gate them behind the
  // scene-loaded flag — the global app-loader covering them while the
  // scene resolves is purely cosmetic delay (or, when engine is down,
  // the full 5 s fallback timeout). Engineer / agentic surfaces should
  // become interactive as soon as their own data arrives. Per
  // quikbot/adastra-bridge#605.
  const sceneIndependentRoute =
    location.pathname.startsWith('/observability') ||
    location.pathname.startsWith('/agentic') ||
    location.pathname.startsWith('/knowledge');

  // SessionDock mounts globally via <SessionDockHost> below — the host
  // reads `useSessionRunStore.running` and renders only while a session
  // is active. Survives in-session NAVIGATE because the host is mounted
  // outside <Routes>.

  // Redux selectors for conditional mounting
  const infoPanelVisible = useSelector((state: RootState) => state.infoPanel.isVisible);
  const modelPanelVisible = useSelector((state: RootState) => state.modelPanel.isVisible);
  const modelPanelTab = useSelector((state: RootState) => state.modelPanel.selectedTab);
  const chatPanelVisible = useSelector((state: RootState) => state.chatPanel.isVisible);
  const tracePanelVisible = useSelector((state: RootState) => state.tracePanel.isVisible);
  const missionTimelineVisible = useSelector((state: RootState) => state.missionTimeline.isVisible);
  const debugPanelVisible = useSelector((state: RootState) => state.debugPanel.isVisible);
  const selectedEntity = useSelector((state: RootState) => state.entity.selectedEntity);

  // Initialize comprehensive scene management
  const {currentScene, error: sceneError} = useSceneManager();

  // Self-heal the initial-load "Failed to fetch scenes" class of failure:
  // when the tab comes back from background/offline, refresh silently so
  // a stale error from a transient blip clears without a full reload.
  useRefetchOnRevive();

  // Scene-error UX: persistent toast + Retry action — same shape as the
  // WS dead-state toast below. Auto-recovery is delegated to
  // `useRefetchOnRevive` (tab focus / online events); manual recovery
  // is the toast button. We deliberately do NOT loop our own retry
  // here — see the rationale on the WS dead-state toast.
  // Edge-only emission: a ref guards the body so we only fire on the
  // null→truthy transition and dismiss on truthy→null. Sonner's id
  // dedupes anyway, but without the guard a language switch or lazy
  // namespace load while the toast is up would re-run the effect,
  // re-play the entrance animation, and read as a flicker.
  const sceneErrorActiveRef = useRef(false);
  useEffect(() => {
    if (sceneError && !sceneErrorActiveRef.current) {
      notifyError(
        t('app.scene.unavailable', 'Platform runtime unavailable.'),
        {
          id: 'scene-error',
          duration: Infinity,
          action: {
            label: t('app.scene.retry', 'Retry'),
            onClick: () => { dispatch(fetchScenes({})); },
          },
        },
      );
      sceneErrorActiveRef.current = true;
    } else if (!sceneError && sceneErrorActiveRef.current) {
      toast.dismiss('scene-error');
      sceneErrorActiveRef.current = false;
    }
  }, [sceneError, dispatch, t]);
  const [sceneGateReady, setSceneGateReady] = useState(false);
  const sceneGateTimerRef = useRef<number | null>(null);

  useEffect(() => {
    if (sceneGateReady) return;
    // Scene-independent routes (engineer/agentic surfaces) become ready
    // immediately — they don't render any scene-bound widgets, so the
    // global loader covering them is pure delay.
    if (sceneIndependentRoute || currentScene?.id || sceneError) {
      // eslint-disable-next-line react-hooks/set-state-in-effect -- one-shot gate flip in response to scene/route resolution; falls through to a 5 s safety-net timeout below
      setSceneGateReady(true);
      return;
    }
    if (sceneGateTimerRef.current == null) {
      sceneGateTimerRef.current = window.setTimeout(() => {
        setSceneGateReady(true);
      }, 5000);
    }
    return () => {
      if (sceneGateTimerRef.current != null) {
        clearTimeout(sceneGateTimerRef.current);
        sceneGateTimerRef.current = null;
      }
    };
  }, [currentScene?.id, sceneError, sceneGateReady, sceneIndependentRoute]);

  useEffect(() => {
    if (sceneGateReady) {
      // WatchManager owns the /ws/watch subscribe lifecycle for EntityPanel,
      // UnitMonitor, and future surfaces. Register the connect listener
      // BEFORE unblocking the WebSocket so the initial connect transition
      // doesn't fire before we're listening. Idempotent against Vite HMR +
      // React StrictMode double-mount.
      watchManager.start();
      // Status surface for /ws/watch — drives the ConnectionStatusPip in
      // the topbar and a one-shot "live updates paused" toast on permanent
      // failure. Re-key per HMR by using a stable wiring key; the manager
      // dedups by key so re-registration is a no-op.
      WebSocketManager.getInstance().addStatusListener(
        WATCH_PATH,
        'app-status-wiring',
        (status) => {
          const store = useWsStatusStore.getState();
          store.setStatus(WATCH_PATH, status);
          if (status === 'dead' && !store.notifiedDeadPaths.includes(WATCH_PATH)) {
            store.markNotifiedDead(WATCH_PATH);
            notifyWarning('Live updates paused — try reloading.', {
              duration: Infinity,
              action: {
                label: 'Reload',
                onClick: () => window.location.reload(),
              },
            });
          }
        },
      );
      WebSocketManager.setAppReady();
    }
  }, [sceneGateReady]);

  // bridge#605: scene-independent routes (engineer / agentic surfaces) must not
  // wait for a scene before revealing. Handled by the scene gate —
  // `sceneGateReady` flips immediately for these routes (see the effect above) —
  // so BootSplashController reveals them as soon as the theme is applied. It
  // does also wait for theme-applied (unlike the old force-fade) to avoid
  // flashing an un-themed shell; the controller's watchdog covers a stalled
  // theme. No separate force-fade path needed.

  useEffect(() => {
    if (!sceneGateReady && !sceneError) return;
    if (sceneGateTimerRef.current != null) {
      clearTimeout(sceneGateTimerRef.current);
      sceneGateTimerRef.current = null;
    }
  }, [sceneGateReady, sceneError]);

  // Initialize server-push pipe and Event Notifications on app mount.
  // Server-push opens the `/ws/tools` socket and dispatches workflow-initiated
  // interactions and notifications — it does NOT pull in the copilot tool
  // executors. Those load lazily when the copilot surface first mounts.
  useEffect(() => {
    const notificationChannel = EventNotificationChannel.getInstance();
    notificationChannel.start();

    import('./lib/server-push/connection').then(({ initServerPush }) => {
      initServerPush();
    }).catch(() => {
      // Module may not be available in all environments
    });

    return () => {
      notificationChannel.stop();
    };
  }, []);

  // Warm the lazy chunks a user almost always reaches on an authenticated
  // session, once the browser is idle (after the shell has painted). Primes
  // the module cache so the first panel-open is instant instead of paying a
  // cold dynamic-import fetch. requestIdleCallback naturally defers this past
  // first paint + critical work; the helper skips on data-saver / slow links.
  // Heavier, rarer route chunks are deliberately left to load on navigation.
  useEffect(() => {
    warmChunksOnIdle([loadEntityPanel, loadModelPanel, loadCopilotPanel]);
  }, []);

  // Pack install used to live here as `ensureTutorialSessions()` — bridge
  // upserted the bundled platform-tour packs to the engine on every boot.
  // That path was retired alongside the engine-side scenario-pack
  // installer (see modules/engine/docs/pack_install_design.md). Pack
  // install is now driven by `POST /v1/pack/{id}/install` and surfaced
  // through `/extension/pack` in the bridge UI; existing engine packs
  // remain functional without bridge re-asserting them on boot.

  // Register the Copilot panel as a UI-surface so the platform tour (and
  // any other CONTROL_UI invocation) can open / close it via the surface
  // store. The CopilotPanel itself only mounts when `chatPanel.isVisible`
  // is true — registering here in App keeps the surface adapter alive
  // regardless of the panel's mount state, so an `open` request kicked
  // before the panel exists still wins.
  useUiSurface('copilot', {
    open: () => { dispatch(showChatPanel()); },
    close: () => { dispatch(hideChatPanel()); },
  });

  // Connect Ambient SSE only when (a) a scene is confirmed available and
  // (b) ambient intelligence is enabled on that scene. The SSE stream is
  // cheap per-message but opens a long-lived connection plus keepalive
  // traffic on the backend, so we skip it entirely for scenes that opted
  // out. Flipping sensitivity OFF at runtime tears the connection down.
  //
  // The store module itself is lazy-imported on first use, so disabled
  // scenes never pay for the 729-line zustand module in their entry chunk.
  const ambientEnabled = useSelector(selectAmbientEnabled);
  const ambientConnectedRef = useRef(false);
  const ambientStoreRef = useRef<AmbientStoreModule | null>(null);
  useEffect(() => {
    if (!currentScene?.id) return;

    if (!ambientEnabled) {
      if (ambientConnectedRef.current && ambientStoreRef.current) {
        ambientConnectedRef.current = false;
        ambientStoreRef.current.useAmbientStore.getState().disconnect();
      }
      return;
    }

    let cancelled = false;
    (async () => {
      const mod = ambientStoreRef.current
        ?? await import('./store/copilot/ambientStore');
      if (cancelled) return;
      ambientStoreRef.current = mod;
      const ambient = mod.useAmbientStore.getState();
      if (!ambientConnectedRef.current) {
        ambientConnectedRef.current = true;
        ambient.connect(currentScene.name);
      } else {
        ambient.setNamespace(currentScene.name);
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [currentScene?.id, currentScene?.name, ambientEnabled]);

  // Expose navigate function globally for client tools
  useEffect(() => {
    (window as unknown as { __routerNavigate?: (path: string) => void }).__routerNavigate = navigate;
    return () => {
      delete (window as unknown as { __routerNavigate?: (path: string) => void }).__routerNavigate;
    };
  }, [navigate]);

  // ModelPanel state is restored by its slice initialState (reads localStorage on creation).
  // No dispatch-based restoration needed here — that caused a bug where setModelPanelTab
  // forced isVisible=true as a side effect.

  // Restore chat panel visibility and InfoPanel selected entity on first mount
  useEffect(() => {
    try {
      const rawChat = window.localStorage.getItem('ChatPanel:uiState');
      if (rawChat) {
        const parsed = JSON.parse(rawChat) as { isVisible?: boolean } | null;
        if (parsed && typeof parsed.isVisible === 'boolean') {
          dispatch(setChatPanelVisible(parsed.isVisible));
        }
      }
    } catch { /* ignore */ }

    // Rehydrate the selected entity from a persisted {type, id} pointer.
    // Historically we stored the full entity snapshot, which (a) blocked the
    // main thread on every selection change to stringify+write large
    // blueprints and (b) restored stale data. Now we persist only the
    // pointer and let `selectEntity` fetch fresh via REST.
    try {
      const rawEntity = window.localStorage.getItem('InfoPanel:selectedEntity');
      if (rawEntity) {
        const parsed = JSON.parse(rawEntity) as { type?: string; id?: string } | null;
        if (parsed?.type && parsed?.id) {
          dispatch(selectEntity({ type: parsed.type, id: parsed.id, showPanel: false }));
        }
      }
    } catch { /* ignore */ }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Provide global navigator for out-of-React navigation (WebSocket actions)
  useEffect(() => {
    setNavigator((path: string) => navigate(path));
    return () => clearNavigator();
  }, [navigate]);

  // Process URL intent on first load (direct open / refresh / external link)
  useEffect(() => {
    try {
      handleUrlIntent(new URL(window.location.href));
    } catch {
      // no-op
    }
  }, []);

  // Also process URL intents when the route or query string changes
  useEffect(() => {
    try {
      handleUrlIntent(new URL(window.location.href));
    } catch {
      // no-op
    }
  }, [location.pathname, location.search]);

  // Save chat panel visibility and InfoPanel selected entity on changes
  useEffect(() => {
    try {
      window.localStorage.setItem('ChatPanel:uiState', JSON.stringify({ isVisible: chatPanelVisible }));
    } catch { /* ignore */ }
  }, [chatPanelVisible]);

  useEffect(() => {
    try {
      window.localStorage.setItem('InfoPanel:uiState', JSON.stringify({ isVisible: infoPanelVisible }));
    } catch { /* ignore */ }
  }, [infoPanelVisible]);

  useEffect(() => {
    try {
      window.localStorage.setItem('ModelPanel:uiState', JSON.stringify({ isVisible: modelPanelVisible, selectedTab: modelPanelTab }));
    } catch { /* ignore */ }
  }, [modelPanelVisible, modelPanelTab]);

  // Persist only the entity pointer ({type, id}) — serialising the full
  // entity on every selection blew the main thread on fat blueprints and
  // restored stale data on the next load.
  useEffect(() => {
    try {
      if (selectedEntity) {
        const type = selectedEntity.effectiveType || selectedEntity.type;
        const id = selectedEntity.id;
        if (type && id) {
          window.localStorage.setItem(
            'InfoPanel:selectedEntity',
            JSON.stringify({ type, id }),
          );
        }
      } else {
        window.localStorage.removeItem('InfoPanel:selectedEntity');
      }
    } catch { /* ignore */ }
  }, [selectedEntity]);

  return (
    <ThemeProvider>
      {/* Stage manager for the pre-React boot splash: reveals the app once the
          theme is applied and the scene gate is ready. */}
      <BootSplashController ready={sceneGateReady} />
      <InlineCopilotProvider>
        <BreadcrumbProvider>
        <Header/>
        {!isAgenticPage && !isSpectator && (
          <React.Suspense fallback={null}>
            <ModelPanelSideDock />
          </React.Suspense>
        )}
        {/* Unified page-level progress under the header */}
        <React.Suspense fallback={null}>
          <Toaster position="top-center" expand visibleToasts={3} richColors duration={5000} closeButton offset={50} style={{ '--width': 'var(--toast-width)' } as React.CSSProperties} />
        </React.Suspense>
        {/* Only mount EntityPanel and ModelPanel if visible. EntityPanel
            sits in its own Suspense so trigger-side useTransition (e.g.
            UnitMonitor's pinned chip click) can keep `isPending` true
            across the lazy chunk fetch on cold open — without this
            boundary the lazy throw bubbles to React Router's implicit
            outer Suspense and the transition signal is unreliable.
            Per quikbot/adastra-bridge#826. */}
        {!isAgenticPage && infoPanelVisible && (
          <React.Suspense fallback={null}>
            <EntityPanel />
          </React.Suspense>
        )}
        {!isAgenticPage && modelPanelVisible && <ModelPanel/>}
        {tracePanelVisible && <TraceDetailsPanel/>}
        {missionTimelineVisible && (
          <React.Suspense fallback={null}>
            <MissionTimeline/>
          </React.Suspense>
        )}
        <DiagramPanel />
        {debugPanelVisible && (
          <React.Suspense fallback={null}>
            <DebugPanel/>
          </React.Suspense>
        )}
        {chatPanelVisible && <CopilotPanel />}
        <InlineCopilotHost />

        {/* Global Agentic Hub — bottom-right anchored orb on every
            authenticated route. Owns the ephemeral copilot session; the
            heavy expanded panel content is lazily loaded on first click
            through an inner Suspense + useTransition boundary so the
            click feels responsive on cold load. */}
        {!isSpectator && (
          <React.Suspense fallback={null}>
            <AgenticFamilyHub />
          </React.Suspense>
        )}

        {/* Scene-error surface: persistent toast effect above + the
            SceneStatusPip in UserProfileDropdown. Auto-recovery is
            useRefetchOnRevive only; manual = the Retry action.
            The legacy position:fixed banner and "no scene selected"
            warning were removed — empty-state UI conveys the latter. */}

        <main>
          <EditionGate>
            <Outlet/>
          </EditionGate>
        </main>
        {/* Global Workflow Activation Dialog Host - single instance for the whole app */}
        <React.Suspense fallback={null}>
          <WorkflowActivationDialogHost />
        </React.Suspense>
        {/* Global Interaction Dialog - mounted by a lightweight host that only
            fetches the RJSF chunk when a workflow actually requests one. */}
        <React.Suspense fallback={null}>
          <InteractionDialogHost />
        </React.Suspense>
        {/* Session runtime overlays: spotlight halo, rendered only when
            a session is driving it. */}
        <React.Suspense fallback={null}>
          <SessionSpotlight />
        </React.Suspense>
        {/* Modal slides for session intros / concept explainers. Single-active;
            mounts only when a `present` interaction is in flight. */}
        <React.Suspense fallback={null}>
          <SessionPresentation />
        </React.Suspense>
        {/* Global SessionDock — mounted unconditionally so a session
            started on one route survives an in-session NAVIGATE. The
            host renders only when `useSessionRunStore.running` is true
            (set by `activateWorkflowById` on workflows tagged with
            `metadata.presentation`). */}
        <React.Suspense fallback={null}>
          <SessionDockHost />
        </React.Suspense>
        {/* Ambient surfaces are gated on scene ambient sensitivity. When
            disabled (the common case), neither chunk is fetched and the
            ambient store module is never imported. */}
        {ambientEnabled && (
          <>
            <React.Suspense fallback={null}>
              <AmbientNotificationStack />
            </React.Suspense>
            <React.Suspense fallback={null}>
              <AmbientTimelineBar />
            </React.Suspense>
          </>
        )}
        </BreadcrumbProvider>
      </InlineCopilotProvider>
    </ThemeProvider>
  );
};

export default App;
