'use client';

import {
  type ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { useLocationProperty } from '@djangocfg/ui-core/hooks';

import {
  installChatBridge,
  setBridgeResolver,
} from '../../browser-bridge';
import { useChatSettings } from '../../../tools/chat/settings';

import type { CaptureEngineOptions, CaptureResult } from '../engine';
import { PageSnapshotEngine } from '../engine';
import {
  PageSnapshotContext,
  type PageSnapshotContextValue,
} from './use-page-snapshot';

/** Props for `PageSnapshotProvider`. */
export interface PageSnapshotProviderProps {
  /** Engine configuration. */
  config?: CaptureEngineOptions;
  /**
   * Opt-in value used when nothing is stored yet. Default false — the
   * user must explicitly enable sharing page context. Once the user
   * toggles it, the choice is persisted and this is ignored.
   */
  defaultLinked?: boolean;
  /**
   * localStorage key for the centralized chat-settings object that holds
   * the opt-in (along with the rest of the chat's persisted settings).
   * Override to scope the chat settings per app / per chat.
   */
  storageKey?: string;
  children: ReactNode;
}

/**
 * Provides the page-snapshot capability to the chat.
 *
 * - Constructs the engine lazily, browser-only (hydration-safe).
 * - Holds the opt-in state.
 * - `getChatMetadata` is the contributor handed to
 *   `ChatProvider.getDynamicMetadata`: it captures a fresh snapshot at
 *   send time (capture-on-submit) and returns `{ pageContext }`.
 * - Auto-captures on route change; tracks staleness via content hash.
 */
export function PageSnapshotProvider({
  config,
  defaultLinked = false,
  storageKey,
  children,
}: PageSnapshotProviderProps) {
  // The opt-in choice persists across sessions — the user enables screen
  // sharing once and it is remembered. It now lives in the centralized
  // chat-settings object (`ChatSettings.pageContext.linked`) instead of a
  // standalone key, so the chat owns all its persisted state in one place.
  const settingsDefaults = useMemo(
    () => ({ pageContext: { linked: defaultLinked } }),
    [defaultLinked],
  );
  const { settings, setPageContextLinked } = useChatSettings({
    storageKey,
    defaults: settingsDefaults,
  });
  const isLinked = settings.pageContext.linked;
  const setIsLinked = setPageContextLinked;
  const [isStale, setIsStale] = useState(false);
  const [lastSnapshot, setLastSnapshot] = useState<CaptureResult | null>(null);

  // The snapshot that actually rode the most recent chat message.
  //
  // CST ref ids (`@eN`) are positional — assigned by DOM-order traversal
  // at capture time — so they are NOT stable across captures: `@e22` of
  // one snapshot need not be `@e22` of another. A `point` directive the
  // assistant returns cites refs from the snapshot it was given. To
  // resolve that directive correctly the overlay must use the registry
  // of *that* snapshot, not a newer one.
  //
  // `lastSnapshot` tracks the live page and is overwritten by every
  // route-change auto-capture, so it cannot be trusted for directive
  // resolution. This state holds the sent snapshot and is only ever
  // replaced on the next send — it survives route changes that happen
  // while a reply (and its directives) is still streaming in.
  const [sentSnapshot, setSentSnapshot] = useState<CaptureResult | null>(null);

  // Engine is browser-only. Construct it once, lazily, after mount —
  // a ref (not state) since it never needs to trigger a re-render.
  const engineRef = useRef<PageSnapshotEngine | null>(null);
  const getEngine = useCallback((): PageSnapshotEngine | null => {
    if (typeof window === 'undefined') return null;
    if (!engineRef.current) {
      engineRef.current = new PageSnapshotEngine(config);
    }
    return engineRef.current;
  }, [config]);

  // The latest content hash — used to decide if the page changed.
  const lastHashRef = useRef<string | null>(null);

  /**
   * Run a capture; on success update lastSnapshot + hash + staleness.
   *
   * Capture is synchronous (a DOM walk, single-digit ms) so there is no
   * `isProcessing` flag to toggle around it — that would only add two
   * needless re-renders.
   */
  const runCapture = useCallback(
    (track: boolean): CaptureResult | null => {
      const engine = getEngine();
      if (!engine) return null;
      try {
        const result = engine.capture();
        if (track) {
          setLastSnapshot(result);
          lastHashRef.current = result.payload.metadata.contentHash;
          setIsStale(false);
        }
        return result;
      } catch {
        // Capture must never break the chat — fail soft.
        return null;
      }
    },
    [getEngine],
  );

  const capture = useCallback((): CaptureResult | null => {
    if (!isLinked) return null;
    return runCapture(true);
  }, [isLinked, runCapture]);

  const generatePreview = useCallback(
    (): CaptureResult | null => runCapture(false),
    [runCapture],
  );

  const refresh = useCallback(() => {
    runCapture(true);
  }, [runCapture]);

  /**
   * The chat metadata contributor. Captures fresh at call time — the
   * chat invokes this synchronously at send (capture-on-submit).
   */
  const getChatMetadata = useCallback((): Record<string, unknown> | undefined => {
    if (!isLinked) return undefined;
    const result = capture();
    if (!result) return undefined;
    // This is the snapshot the assistant will see — pin it so any
    // `point` directive in the reply resolves against the exact ref
    // registry that produced the refs the assistant cited.
    setSentSnapshot(result);
    return { pageContext: result.payload };
  }, [isLinked, capture]);

  // Reactive pathname from ui-core's router abstraction. Framework-
  // agnostic (History API under the hood) — no next/navigation
  // dependency, works in Next / Vite / CRA / Electron alike.
  const pathname = useLocationProperty(
    () => window.location.pathname,
    () => '/',
  );

  // Route change → auto-capture a fresh snapshot (capture, not send).
  // Also covers the initial opt-in: when `isLinked` flips true this
  // effect runs and populates `lastSnapshot`.
  useEffect(() => {
    if (!isLinked) return;
    runCapture(true);
    // `pathname` in deps: a new route re-captures automatically.
  }, [isLinked, pathname, runCapture]);

  // Install the dev-only `window.__chatBridge` once — manual testing of
  // highlight / focus commands without an AI round-trip.
  useEffect(() => {
    installChatBridge();
  }, []);

  // Keep the bridge's ref resolver pointed at the latest snapshot, so
  // console-driven `window.__chatBridge.highlight()` commands resolve
  // refs against the current page. This is intentionally the *latest*
  // snapshot, not `sentSnapshot`: manual testing highlights what is on
  // screen now. AI-directive resolution is a separate path — it uses
  // `sentSnapshot` (see `DirectiveOverlay`) so it stays pinned to the
  // registry the assistant actually saw.
  useEffect(() => {
    setBridgeResolver(lastSnapshot?.refs ?? null);
    return () => setBridgeResolver(null);
  }, [lastSnapshot]);

  const value = useMemo<PageSnapshotContextValue>(
    () => ({
      isLinked,
      setIsLinked,
      capture,
      generatePreview,
      getChatMetadata,
      lastSnapshot,
      sentSnapshot,
      isStale,
      refresh,
    }),
    [
      isLinked,
      capture,
      generatePreview,
      getChatMetadata,
      lastSnapshot,
      sentSnapshot,
      isStale,
      refresh,
    ],
  );

  return (
    <PageSnapshotContext.Provider value={value}>
      {children}
    </PageSnapshotContext.Provider>
  );
}
