import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  LuChevronDown,
  LuChevronRight,
  LuPlusCircle,
  LuSparkles,
} from 'react-icons/lu';

import {
  ExtractAssistant,
  ExtractModal,
} from '../components/company-id/extract-modal';
import {
  dismissProposal as persistDismissedProposal,
  stripDismissedFromField,
  stripDismissedProposals,
} from '../components/company-id/dismissed-proposals';
import { FieldRow, isEmptyValue } from '../components/company-id/field-row';
import {
  CompanyIdRings,
  RingSegmentState,
} from '../components/company-id/rings';
import { ScrollCue } from '../components/company-id/scroll-cue';
import {
  SourceMaterialRail,
  UploadPhase,
} from '../components/company-id/source-material-rail';
import {
  parseDraft,
  serializeValue,
} from '../components/company-id/value-parsing';
import { Spinner } from '../components/ui/Spinner';
import { useAppStateContext } from '../context/user.data.context';
import {
  CompanyIdExtractImportResult,
  CompanyIdFieldEnvelope,
  CompanyIdPayload,
  CompanyIdRing,
  CompanyIdVisibilityPush,
  POPULATE_MODE,
  PopulateMode,
} from '../service/company-id/company-id.interface';
import {
  actOnCompanyIdSuggestion,
  dispatchCompanyIdPopulate,
  getCompanyId,
  getCompanyIdCheatSheetPdf,
  getCompanyIdPdf,
  pushCompanyIdToVisibility,
  updateCompanyIdField,
} from '../service/company-id/company-id.service';
import { fetchCompanyIdReadState } from '../service/copilot/copilot.service';
import {
  createTextEntry,
  getBatchStatus,
  listDocuments,
  pushToRag,
  uploadBatch,
} from '../service/documentation/documentation.service';
import { getMedia, saveLogo } from '../service/popup/popup.service';
import {
  getSetupProgress,
  setSetupProgressStep,
} from '../service/setup-progress/setup-progress.service';
import { WORDPRESS_STEP_KEYS } from '../service/setup-progress/setup-progress.constants';
import { DocumentFile } from '../types/documentation';

const PageLoading = (): JSX.Element => (
  <div className="flex min-h-[60vh] items-center justify-center">
    <Spinner className="h-6 w-6 text-gray-400" />
  </div>
);

const MONO = "'IBM Plex Mono', ui-monospace, monospace";
const POPULATE_POLL_INTERVAL_MS = 5000;
const POPULATE_POLL_MAX_TICKS = 24;
/**
 * How often to check whether a read this page did not start has landed. Slower
 * than the rail's own poll: nothing is spinning on screen, this only has to
 * catch up within a few seconds of a run finishing elsewhere.
 */
const EXTERNAL_READ_POLL_INTERVAL_MS = 8000;
const KB_POLL_INTERVAL_MS = 2500;
const KB_POLL_MAX_TICKS = 60;
const KB_MAX_FILE_BYTES = 20 * 1024 * 1024;
const CORE_RING_INDEX = 0;

const unwrapXml = (value?: string[] | string): string =>
  (Array.isArray(value) ? value[0] : value) || '';

/**
 * One read/push stamp as a comparable instant. A stamp that carries no zone is
 * already UTC on the wire, but Date would read it as local time, so tag it
 * before parsing.
 *
 * @param value - An ISO stamp, or null/undefined when that step never ran.
 * @return The epoch milliseconds as a string, or '' when there is no stamp.
 */
const toInstantKey = (value?: string | null): string => {
  if (!value) return '';
  const zoned: string = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value)
    ? value
    : `${value}Z`;
  const parsed: number = Date.parse(zoned);

  return Number.isNaN(parsed) ? value : String(parsed);
};

/**
 * The three read/push stamps as one comparable key. The full record serializes
 * them tz-naive ("2026-08-05T10:00:00") while the lean read-status endpoint
 * serializes them UTC-aware ("2026-08-05T10:00:00Z"), so the same instant
 * arrives as two different strings; compare the parsed instants, never the raw
 * text, or the watcher below sees a moved record on every single tick.
 *
 * @param source - Anything carrying the three stamps (record or read state).
 * @return The stamps as epoch milliseconds, joined by "|".
 */
const stampsKey = (source: {
  populated_at?: string | null;
  engines_read_at?: string | null;
  pushed_to_visibility_at?: string | null;
}): string =>
  [
    toInstantKey(source.populated_at),
    toInstantKey(source.engines_read_at),
    toInstantKey(source.pushed_to_visibility_at),
  ].join('|');

function computeStats(rings: CompanyIdRing[]): {
  filled: number;
  total: number;
  pct: number;
} {
  let filled = 0;
  let total = 0;
  rings.forEach(ring =>
    ring.fields.forEach(field => {
      total += 1;
      // Only owner-accepted ("yours") fields count as done; an AI proposal the
      // merchant hasn't claimed yet is not complete.
      if (field.status === 'yours' && !isEmptyValue(field.value)) filled += 1;
    })
  );

  return { filled, total, pct: total ? Math.round((filled / total) * 100) : 0 };
}

function ringHasData(ring: CompanyIdRing): boolean {
  return ring.fields.some(
    field => field.status !== 'empty' && !isEmptyValue(field.value)
  );
}

const Page = (): JSX.Element => {
  const { token, clientId, shopData, user } = useAppStateContext();

  const [payload, setPayload] = useState<CompanyIdPayload | null>(null);
  const [documents, setDocuments] = useState<DocumentFile[]>([]);
  const [initialLoading, setInitialLoading] = useState(true);
  const [hover, setHover] = useState(-1);
  const [revealed, setRevealed] = useState(false);
  const [openMap, setOpenMap] = useState<Record<number, boolean>>({});
  const [shownMap, setShownMap] = useState<Record<number, boolean>>({});
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editSeed, setEditSeed] = useState<string | null>(null);
  const [hotId, setHotId] = useState<string | null>(null);
  const [toast, setToast] = useState<string | null>(null);
  const [busyMode, setBusyMode] = useState<PopulateMode | null>(null);
  const [glow, setGlow] = useState(false);
  const [uploadPhase, setUploadPhase] = useState<UploadPhase>('idle');
  const [pasteBusy, setPasteBusy] = useState(false);
  const [exportBusy, setExportBusy] = useState(false);
  const [pushBusy, setPushBusy] = useState(false);
  const [cheatSheetBusy, setCheatSheetBusy] = useState(false);
  const [logoBusy, setLogoBusy] = useState(false);
  const [extractAssistant, setExtractAssistant] =
    useState<ExtractAssistant | null>(null);
  const [pasteSubmittedCount, setPasteSubmittedCount] = useState(0);

  const reducedMotion = useRef(false);
  const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const kbPollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const pollBatchRef = useRef<
    (jobId: string, batchId: string, ticks: number) => void
  >(() => {});

  const hasCreds = Boolean(token && clientId);

  const showToast = useCallback((message: string) => {
    setToast(message);
    if (toastTimer.current) clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(null), 4200);
  }, []);

  const reloadDocuments = useCallback(async () => {
    if (!clientId || !token) return;
    try {
      const result = await listDocuments(clientId, token);
      if (!result.errors && result.data) {
        setDocuments(result.data.documents ?? []);
      }
    } catch {
      return;
    }
  }, [clientId, token]);

  useEffect(() => {
    reducedMotion.current =
      typeof window !== 'undefined' &&
      !!window.matchMedia &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reducedMotion.current) {
      setRevealed(true);
    } else {
      requestAnimationFrame(() => setTimeout(() => setRevealed(true), 60));
    }

    return () => {
      if (toastTimer.current) clearTimeout(toastTimer.current);
      if (pollTimer.current) clearTimeout(pollTimer.current);
      if (kbPollTimer.current) clearTimeout(kbPollTimer.current);
    };
  }, []);

  useEffect(() => {
    if (!clientId || !token) return;
    let cancelled = false;
    (async () => {
      try {
        const fresh = await getCompanyId(clientId, token);
        if (cancelled) return;
        if (!fresh) {
          console.warn('getCompanyId resolved without a payload', { clientId });

          return;
        }
        setPayload(stripDismissedProposals(fresh));
        const open: Record<number, boolean> = {};
        fresh.rings.forEach(ring => {
          open[ring.index] = !(
            ring.collapsed_by_default || ring.hidden_by_default
          );
        });
        setOpenMap(open);
      } catch (error) {
        console.error('getCompanyId initial load', error);
      } finally {
        if (!cancelled) setInitialLoading(false);
      }
    })();
    void reloadDocuments();

    return () => {
      cancelled = true;
    };
  }, [clientId, token, reloadDocuments]);

  const statsComplete = payload
    ? computeStats(payload.rings).pct === 100
    : false;
  useEffect(() => {
    if (!statsComplete || !revealed || reducedMotion.current) return;
    const glowTimer = setTimeout(() => setGlow(true), 1050);

    return () => clearTimeout(glowTimer);
  }, [statsComplete, revealed]);

  const refreshSilently = useCallback(async () => {
    if (!clientId || !token) return null;
    try {
      const fresh = await getCompanyId(clientId, token, true, true);
      setPayload(stripDismissedProposals(fresh));

      return fresh;
    } catch (error) {
      console.error('getCompanyId refreshSilently', error);

      return null;
    }
  }, [clientId, token]);

  const firstReadPending = !!payload && !payload.populated_at;
  useEffect(() => {
    if (!firstReadPending) return;
    let ticks = 0;
    let timer: ReturnType<typeof setTimeout> | null = null;
    const poll = async () => {
      ticks += 1;
      const fresh = await refreshSilently();
      if (fresh?.populated_at || ticks >= POPULATE_POLL_MAX_TICKS) return;
      timer = setTimeout(poll, POPULATE_POLL_INTERVAL_MS);
    };
    timer = setTimeout(poll, POPULATE_POLL_INTERVAL_MS);

    return () => {
      if (timer) clearTimeout(timer);
    };
  }, [firstReadPending, refreshSilently]);

  const replaceField = useCallback((envelope: CompanyIdFieldEnvelope) => {
    const cleaned = stripDismissedFromField(envelope);
    setPayload(current => {
      if (!current) return current;

      return {
        ...current,
        rings: current.rings.map(ring => ({
          ...ring,
          fields: ring.fields.map(field =>
            field.id === cleaned.id ? cleaned : field
          ),
        })),
      };
    });
  }, []);

  const dismissAiProposal = useCallback(
    (field: CompanyIdFieldEnvelope, engine: string) => {
      const proposal = field.ai_proposals?.find(
        entry => entry.engine === engine
      );
      persistDismissedProposal(field.id, engine, proposal?.value);
      setPayload(current => {
        if (!current) return current;

        return {
          ...current,
          rings: current.rings.map(ring => ({
            ...ring,
            fields: ring.fields.map(entry =>
              entry.id === field.id
                ? {
                    ...entry,
                    ai_proposals: (entry.ai_proposals ?? []).filter(
                      candidate => candidate.engine !== engine
                    ),
                  }
                : entry
            ),
          })),
        };
      });
    },
    []
  );

  const commitEdit = useCallback(
    async (field: CompanyIdFieldEnvelope, draft: string) => {
      setEditingId(null);
      setEditSeed(null);
      if (!clientId || !token) return;
      const value = parseDraft(field.kind, draft);
      if (value == null && isEmptyValue(field.value)) return;
      if (
        value != null &&
        field.status === 'yours' &&
        draft.trim() === serializeValue(field.kind, field.value).trim()
      ) {
        return;
      }
      try {
        const envelope = await updateCompanyIdField(
          clientId,
          token,
          field.id,
          value
        );
        // A merchant save is unambiguous "this is Yours" — drop the pending
        // proposal + AI candidates locally even if the backend echoes the
        // envelope with stale status, so the "Needs your decision" chip
        // clears immediately.
        replaceField({
          ...envelope,
          status: 'yours',
          suggested: null,
          ai_proposals: [],
        });
        if (payload?.nudge?.field_id === field.id) void refreshSilently();
      } catch (saveError) {
        showToast(
          saveError instanceof Error ? saveError.message : 'Save failed'
        );
      }
    },
    [clientId, token, replaceField, showToast, payload, refreshSilently]
  );

  const removeTag = useCallback(
    async (field: CompanyIdFieldEnvelope, index: number) => {
      if (!clientId || !token) return;
      const existing = Array.isArray(field.value)
        ? (field.value as string[])
        : [];
      const remaining = existing.filter(
        (_, entryIndex) => entryIndex !== index
      );
      try {
        const envelope = await updateCompanyIdField(
          clientId,
          token,
          field.id,
          remaining.length ? remaining : null
        );
        replaceField(envelope);
      } catch (saveError) {
        showToast(
          saveError instanceof Error ? saveError.message : 'Delete failed'
        );
      }
    },
    [clientId, token, replaceField, showToast]
  );

  const addTag = useCallback(
    async (field: CompanyIdFieldEnvelope, tag: string) => {
      if (!clientId || !token) return;
      const cleaned = tag.trim();
      if (!cleaned) return;
      const existing = Array.isArray(field.value)
        ? (field.value as string[])
        : [];
      if (
        existing.some(entry => entry.toLowerCase() === cleaned.toLowerCase())
      ) {
        showToast(`"${cleaned}" is already there`);

        return;
      }
      try {
        const envelope = await updateCompanyIdField(clientId, token, field.id, [
          ...existing,
          cleaned,
        ]);
        replaceField(envelope);
        if (payload?.nudge?.field_id === field.id) void refreshSilently();
      } catch (saveError) {
        showToast(
          saveError instanceof Error ? saveError.message : 'Save failed'
        );
      }
    },
    [clientId, token, replaceField, showToast, payload, refreshSilently]
  );

  const resolveSuggestion = useCallback(
    async (fieldId: string, action: 'accept' | 'decline') => {
      if (!clientId || !token) return;
      try {
        const envelope = await actOnCompanyIdSuggestion(
          clientId,
          token,
          fieldId,
          action
        );
        replaceField(envelope);
      } catch (suggestionError) {
        showToast(
          suggestionError instanceof Error
            ? suggestionError.message
            : 'Action failed'
        );
      }
    },
    [clientId, token, replaceField, showToast]
  );

  const claimProposal = useCallback(
    async (field: CompanyIdFieldEnvelope, value: unknown) => {
      if (!clientId || !token) return;
      try {
        const envelope = await updateCompanyIdField(
          clientId,
          token,
          field.id,
          value
        );
        replaceField(envelope);
      } catch (claimError) {
        showToast(
          claimError instanceof Error ? claimError.message : 'Claim failed'
        );
      }
    },
    [clientId, token, replaceField, showToast]
  );

  const onUploadLogo = useCallback(
    async (file: File) => {
      if (!clientId || !token || !user) return;
      setLogoBusy(true);
      try {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('file_type', 'LOGO');
        formData.append('model_type', 'COMPANY_ID_LOGO');
        formData.append('model_id', user.id);
        await saveLogo(formData);
        const mediaRes = await getMedia(user.id, 'COMPANY_ID_LOGO');
        const logoUrl = mediaRes?.media?.file_path ?? null;
        if (!logoUrl) {
          showToast('Logo upload failed. Please try again.');

          return;
        }
        const envelope = await updateCompanyIdField(
          clientId,
          token,
          'brand_assets',
          logoUrl
        );
        replaceField(envelope);
        showToast('Logo saved to your Company ID.');
      } catch {
        showToast('Logo upload failed. Please try again.');
      } finally {
        setLogoBusy(false);
      }
    },
    [clientId, token, user, replaceField, showToast]
  );

  const scrollToSection = useCallback(
    (ringIndex: number, after?: () => void) => {
      setShownMap(current => ({ ...current, [ringIndex]: true }));
      setOpenMap(current => ({ ...current, [ringIndex]: true }));
      setTimeout(() => {
        const el = document.getElementById(`cid-sec-${ringIndex}`);
        if (el) {
          el.scrollIntoView({
            behavior: reducedMotion.current ? 'auto' : 'smooth',
            block: 'start',
          });
        }
        if (after) after();
      }, 40);
    },
    []
  );

  /**
   * Open a field's ring, bring the field to the middle of the screen and
   * flash it, so a pointer from elsewhere on the page lands on the row
   * itself instead of the top of a long section.
   *
   * @param {number} ringIndex - Ring holding the field.
   * @param {string} fieldId - Canonical field id to land on.
   * @param {boolean} openEditor - Also open the inline editor once landed.
   *   Off for pointers at fields that may render an uploader or a proposal
   *   card, where an editor is the wrong first thing to see.
   * @returns {void}
   */
  const focusField = useCallback(
    (ringIndex: number, fieldId: string, openEditor: boolean) => {
      scrollToSection(ringIndex, () => {
        setTimeout(() => {
          const el = document.getElementById(`cid-f-${fieldId}`);
          if (el) {
            el.scrollIntoView({
              behavior: reducedMotion.current ? 'auto' : 'smooth',
              block: 'center',
            });
          }
          setHotId(fieldId);
          if (openEditor) setTimeout(() => setEditingId(fieldId), 500);
          setTimeout(
            () => setHotId(current => (current === fieldId ? null : current)),
            2800
          );
        }, 80);
      });
    },
    [scrollToSection]
  );

  const onNudge = useCallback(() => {
    if (!payload?.nudge) return;
    const nudgeFieldId = payload.nudge.field_id;
    const ring = payload.rings.find(candidate =>
      candidate.fields.some(field => field.id === nudgeFieldId)
    );
    if (!ring) return;
    focusField(ring.index, nudgeFieldId, true);
  }, [payload, focusField]);

  /**
   * The read/push stamps this page is currently rendering, as one comparable
   * string. Kept in a ref so the watcher below can tell "the record moved"
   * from "we already pulled that in" without restarting on every render.
   */
  const renderedStampsRef = useRef<string | null>(null);
  useEffect(() => {
    if (!payload) return;
    renderedStampsRef.current = stampsKey(payload);
  }, [payload]);

  const runPopulate = useCallback(
    async (mode: PopulateMode) => {
      if (busyMode !== null || !payload || !clientId || !token) return;
      setBusyMode(mode);
      const populatedBefore = payload.populated_at;
      // The AI research (WEB) run stamps engines_read_at, not populated_at -
      // only the site read moves that one. Each mode watches its own stamp,
      // otherwise the WEB spinner outlives the finished run.
      const enginesReadBefore = payload.engines_read_at;
      try {
        await dispatchCompanyIdPopulate(
          clientId,
          token,
          mode,
          mode === POPULATE_MODE.SITE
        );
      } catch (dispatchError) {
        setBusyMode(null);
        showToast(
          dispatchError instanceof Error
            ? dispatchError.message
            : 'Could not start the read'
        );

        return;
      }
      let ticks = 0;
      const poll = async () => {
        ticks += 1;
        const fresh = await refreshSilently();
        const runDone =
          !!fresh &&
          (mode === POPULATE_MODE.WEB
            ? fresh.engines_read_at !== enginesReadBefore ||
              fresh.populated_at !== populatedBefore
            : fresh.populated_at !== populatedBefore);
        if (runDone) {
          setBusyMode(null);
          showToast(
            mode === POPULATE_MODE.WEB
              ? 'AI research done. Review the new proposals.'
              : 'Your record was re-read from the site'
          );

          return;
        }
        if (ticks >= POPULATE_POLL_MAX_TICKS) {
          setBusyMode(null);
          showToast('Still reading in the background. Check back shortly.');

          return;
        }
        pollTimer.current = setTimeout(poll, POPULATE_POLL_INTERVAL_MS);
      };
      pollTimer.current = setTimeout(poll, POPULATE_POLL_INTERVAL_MS);
    },
    [busyMode, payload, clientId, token, refreshSilently, showToast]
  );

  // A read started from the copilot lands server-side with nothing on this page
  // watching for it: runPopulate only polls for runs the rail's own buttons
  // dispatched. The merchant was left reading a "Complete" card in the chat
  // over a record that still showed the old fields until they refreshed by
  // hand. Watch the lean stamps instead - they also cover a run started in
  // another tab, or one the signup worker finished while they sat here.
  useEffect(() => {
    if (!token || !clientId) return;
    let cancelled = false;
    let timer: ReturnType<typeof setTimeout> | null = null;
    const tick = async () => {
      if (cancelled) return;
      // While the rail is running its own read, that poll owns the refresh;
      // and a hidden tab has nothing to bring up to date.
      const shouldCheck: boolean =
        busyMode === null &&
        renderedStampsRef.current !== null &&
        (typeof document === 'undefined' ||
          document.visibilityState === 'visible');
      if (shouldCheck) {
        const stamps = await fetchCompanyIdReadState(token, clientId);
        if (stamps) {
          const latest: string = stampsKey(stamps);
          if (!cancelled && latest !== renderedStampsRef.current) {
            renderedStampsRef.current = latest;
            await refreshSilently();
            // The page can unmount across that read; a toast fired now would
            // leave its own dismiss timer running past the cleanup above.
            if (!cancelled) {
              showToast('Your record was updated by a read that just finished');
            }
          }
        }
      }
      if (!cancelled) timer = setTimeout(tick, EXTERNAL_READ_POLL_INTERVAL_MS);
    };
    timer = setTimeout(tick, EXTERNAL_READ_POLL_INTERVAL_MS);

    return () => {
      cancelled = true;
      if (timer) clearTimeout(timer);
    };
  }, [busyMode, refreshSilently, showToast, token, clientId]);

  pollBatchRef.current = (jobId, batchId, ticks) => {
    if (ticks > KB_POLL_MAX_TICKS) {
      setUploadPhase('idle');
      showToast('Still processing in the background. Check back shortly.');

      return;
    }
    kbPollTimer.current = setTimeout(async () => {
      if (!clientId || !token) return;
      try {
        const result = await getBatchStatus(jobId, clientId, token);
        const status = result.data;
        if (status && status.state === 'SUCCESS') {
          setUploadPhase('pushing');
          try {
            await pushToRag(batchId, clientId, token);
            setUploadPhase('idle');
            showToast('Added to your knowledge base');
            void reloadDocuments();
          } catch {
            setUploadPhase('idle');
            showToast('Publishing to the knowledge base failed.');
          }
        } else if (status && status.state === 'FAILURE') {
          setUploadPhase('idle');
          showToast('Upload failed while processing.');
        } else {
          pollBatchRef.current(jobId, batchId, ticks + 1);
        }
      } catch {
        pollBatchRef.current(jobId, batchId, ticks + 1);
      }
    }, KB_POLL_INTERVAL_MS);
  };

  const startUpload = useCallback(
    (files: FileList) => {
      if (!clientId || !token) return;
      const oversized = Array.from(files).find(
        file => file.size > KB_MAX_FILE_BYTES
      );
      if (oversized) {
        showToast(`${oversized.name} is over 20 MB`);

        return;
      }
      const uploadFd = new FormData();
      Array.from(files).forEach(file => uploadFd.append('files', file));
      setUploadPhase('uploading');
      (async () => {
        try {
          const result = await uploadBatch(uploadFd, clientId, token);
          if (result.errors || !result.data) {
            setUploadPhase('idle');
            showToast('Upload failed. Please try again.');

            return;
          }
          setUploadPhase('processing');
          pollBatchRef.current(result.data.job_id, result.data.batch_id, 0);
        } catch {
          setUploadPhase('idle');
          showToast('Upload failed. Please try again.');
        }
      })();
    },
    [clientId, token, showToast]
  );

  const submitPasteText = useCallback(
    (title: string, text: string) => {
      if (!clientId || !token) return;
      setPasteBusy(true);
      (async () => {
        try {
          const result = await createTextEntry(
            { title, description: text },
            clientId,
            token
          );
          if (result.errors || !result.data) {
            showToast('Failed to add the text entry.');

            return;
          }
          setPasteSubmittedCount(count => count + 1);
          showToast('Text added to your knowledge base');
          void reloadDocuments();
        } catch {
          showToast('Failed to add the text entry.');
        } finally {
          setPasteBusy(false);
        }
      })();
    },
    [clientId, token, showToast, reloadDocuments]
  );

  const onExportPdf = useCallback(async () => {
    if (exportBusy || !clientId || !token) return;
    setExportBusy(true);
    try {
      const blob = await getCompanyIdPdf(clientId, token);
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = `company-id-${(payload?.domain || 'record').replace(/\./g, '-')}.pdf`;
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      URL.revokeObjectURL(url);
    } catch (exportError) {
      showToast(
        exportError instanceof Error ? exportError.message : 'Export failed'
      );
    } finally {
      setExportBusy(false);
    }
  }, [clientId, token, payload, exportBusy, showToast]);

  const onPushToVisibility =
    useCallback(async (): Promise<CompanyIdVisibilityPush | null> => {
      if (pushBusy || !clientId || !token) return null;
      setPushBusy(true);
      try {
        const result = await pushCompanyIdToVisibility(clientId, token);
        if (result.synced) {
          // Sequential, never parallel: the API rewrites the whole
          // setup_progress blob per call, so two writes in flight lose one of
          // the two steps. The push also queues a forced Visibility scan, so
          // it satisfies the run step as well.
          setSetupProgressStep(WORDPRESS_STEP_KEYS.COMPANY_ID, true)
            .then(() =>
              setSetupProgressStep(WORDPRESS_STEP_KEYS.RUN_AI_VISIBILITY, true)
            )
            .catch(() => {});
        }
        // A synced push is confirmed by the rail's modal; only empty and
        // failure cases fall back to a toast.
        if (!result.synced) {
          if (result.reason === 'no_brand') {
            showToast(
              'Could not set up your AI Visibility brand. Please try again.'
            );
          } else {
            // "Nothing to sync" with a filled-looking page means the record
            // shown here is stale (e.g. reset elsewhere); re-read it so the
            // fields reflect reality before the merchant retries.
            await refreshSilently();
            showToast(
              'Nothing to push yet. Fill what you sell and your offerings first.'
            );
          }
        }
        return result;
      } catch (pushError) {
        showToast(
          pushError instanceof Error ? pushError.message : 'Push failed'
        );
        return null;
      } finally {
        setPushBusy(false);
      }
    }, [clientId, token, pushBusy, showToast, refreshSilently]);

  // Retro-flip the Company ID setup step for records pushed to Visibility
  // before the step existed; the push handler stamps it going forward.
  useEffect(() => {
    if (!payload?.pushed_to_visibility_at) return;
    getSetupProgress()
      .then(async progress => {
        if (!progress) return;
        // The push satisfies both steps. Test each key on its own so a
        // half-written pair still heals, and write them sequentially.
        if (!progress.steps[WORDPRESS_STEP_KEYS.COMPANY_ID]?.completed) {
          await setSetupProgressStep(WORDPRESS_STEP_KEYS.COMPANY_ID, true);
        }
        if (!progress.steps[WORDPRESS_STEP_KEYS.RUN_AI_VISIBILITY]?.completed) {
          await setSetupProgressStep(
            WORDPRESS_STEP_KEYS.RUN_AI_VISIBILITY,
            true
          );
        }
      })
      .catch(() => {});
  }, [payload?.pushed_to_visibility_at]);

  const onDownloadCheatSheet = useCallback(async () => {
    if (cheatSheetBusy || !clientId || !token) return;
    setCheatSheetBusy(true);
    try {
      const blob = await getCompanyIdCheatSheetPdf(clientId, token);
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = 'company-id-cheat-sheet.pdf';
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      URL.revokeObjectURL(url);
    } catch (cheatSheetError) {
      showToast(
        cheatSheetError instanceof Error
          ? cheatSheetError.message
          : 'Download failed'
      );
    } finally {
      setCheatSheetBusy(false);
    }
  }, [clientId, token, cheatSheetBusy, showToast]);

  const onExtractImported = useCallback(
    (result: CompanyIdExtractImportResult) => {
      setExtractAssistant(null);
      const landed = result.proposed.length + result.suggested.length;
      showToast(
        landed > 0
          ? `Imported ${landed} field${landed === 1 ? '' : 's'} as proposals. Review them below.`
          : 'Nothing usable found in the pasted export.'
      );
      void refreshSilently();
    },
    [showToast, refreshSilently]
  );

  if (!hasCreds || initialLoading) {
    return <PageLoading />;
  }

  if (!payload) {
    return (
      <div
        style={{
          minHeight: '60vh',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: '#6d7175',
          fontFamily: "'DM Sans', system-ui, sans-serif",
        }}
      >
        Company ID could not be loaded. Refresh to retry.
      </div>
    );
  }

  const stats = computeStats(payload.rings);
  const isComplete = stats.pct === 100;
  const hasAiProposals = payload.rings.some(ring =>
    ring.fields.some(field => (field.ai_proposals ?? []).length > 0)
  );
  // Step 2 done: the record's engines-read stamp survives claimed/dismissed
  // proposals; waiting proposals count too (pre-stamp records, mid-run).
  const enginesReadDone = !!payload.engines_read_at || hasAiProposals;
  const coreRing = payload.rings.find(ring => ring.index === CORE_RING_INDEX);
  // The Core fields still standing between the merchant and step 3. Kept as
  // the fields themselves, not a count: the rail shows how many are left and
  // the step jumps straight at the first one, because "6 of 7" on its own
  // never told anyone WHICH answer was missing.
  const coreGaps = (coreRing?.fields ?? []).filter(
    field => field.status !== 'yours' || isEmptyValue(field.value)
  );
  const coreComplete = !!coreRing && coreGaps.length === 0;
  const pushedOnce = !!payload.pushed_to_visibility_at;
  const canDownloadCheatSheet =
    payload.entitlements?.can_download_cheat_sheet ?? false;
  const canExtract = payload.entitlements?.can_extract ?? false;
  // Tri-state segments: claimed fields at full accent, unclaimed proposals as
  // a pale accent (there IS something here, but it's not done), empty as grey.
  // Keeps the rings agreeing with computeStats, which counts only "yours".
  const fills = payload.rings.map(ring =>
    ring.fields.map(
      (field): RingSegmentState =>
        field.status === 'empty' || isEmptyValue(field.value)
          ? 'empty'
          : field.status === 'yours'
            ? 'yours'
            : 'proposed'
    )
  );
  const canonicalField = payload.rings[0]?.fields.find(
    field => field.id === 'canonical_name'
  );
  const canonicalName =
    canonicalField && !isEmptyValue(canonicalField.value)
      ? String(canonicalField.value)
      : '';
  const shopName = unwrapXml(shopData?.data?.store?.name);
  const shopDomain = unwrapXml(shopData?.data?.store?.domain);
  const displayName = canonicalName || shopName;
  const domainLabel = payload.domain || shopDomain || 'your store';
  const headerSub = displayName
    ? `${displayName} · ${domainLabel}`
    : `${domainLabel} · just installed`;
  const centerLine1 = displayName || 'Core ID';
  const centerLine2 = displayName ? 'Company ID' : 'add your name';
  const animate = revealed && !reducedMotion.current;

  return (
    <div
      style={{
        // 100% (not 100vh) so the page fills the pinned app shell without
        // adding a viewport-worth of scroll inside it.
        minHeight: '100%',
        background: '#f6f6f7',
        color: '#202223',
        fontFamily: "'DM Sans', system-ui, sans-serif",
        fontSize: 14,
        lineHeight: 1.55,
        WebkitFontSmoothing: 'antialiased',
        borderRadius: 12,
        overflowX: 'hidden',
        maxWidth: '100%',
        // Lets the @container rules below react to THIS column's width, which
        // shrinks when the copilot opens — a viewport media query can't see
        // that.
        containerType: 'inline-size',
      }}
    >
      <style>{`
        @keyframes cidToastIn { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } }
        @keyframes cidGlow { 0% { opacity: 0; } 30% { opacity: .5; } 100% { opacity: 0; } }
        @keyframes cidSpin { to { transform: rotate(360deg); } }
        @keyframes cidGhostPulse { 0%, 100% { opacity: .45; } 50% { opacity: .85; } }
        @keyframes cidRingPulse { 0%, 100% { stroke-opacity: 1; } 50% { stroke-opacity: .3; } }
        @keyframes cidRingDriftCw { to { transform: rotate(360deg); } }
        @keyframes cidRingDriftCcw { to { transform: rotate(-360deg); } }
        @media (max-width: 1280px) {
          .cid-hero-grid { grid-template-columns: 1fr !important; }
          .cid-lower-grid { grid-template-columns: 1fr !important; }
          .cid-rings-wrap { margin: 0 auto !important; }
          .cid-hero-side { margin: 0 auto !important; }
        }
        @container (max-width: 1100px) {
          .cid-hero-grid { grid-template-columns: 1fr !important; }
          .cid-lower-grid { grid-template-columns: 1fr !important; }
          .cid-rings-wrap { margin: 0 auto !important; }
          .cid-hero-side { margin: 0 auto !important; }
        }
        /* The field rows keep their 3-column layout on a viewport-based xl
           breakpoint, which stays true even when the copilot narrows this
           column - so the status chip ("Yours") slid out of sight. Collapse to
           one column on the container's own width instead. */
        @container (max-width: 900px) {
          .cid-row { grid-template-columns: 1fr !important; }
        }
        /* The side padding lives here rather than on the elements' style
           attribute: an inline value outranks any rule, so the narrow-screen
           override below could never fire. */
        .cid-header-wrap { padding: 30px 32px 0; }
        .cid-hero-grid { padding: 36px 32px 40px; }
        .cid-lower-grid { padding: 0 32px; }
        /* Narrow content area: 32px of side padding is a fifth of a phone
           screen, so the record loses a word per line for nothing. */
        @container (max-width: 640px) {
          .cid-header-wrap { padding: 18px 16px 0; }
          /* gap stays inline on the grid, so it needs !important to give way. */
          .cid-hero-grid { padding: 24px 16px 32px; gap: 24px !important; }
          .cid-lower-grid { padding: 0 16px; }
        }
      `}</style>

      <div
        className="cid-header-wrap"
        style={{ maxWidth: 1400, margin: '0 auto' }}
      >
        <div
          style={{
            display: 'flex',
            flexWrap: 'wrap',
            alignItems: 'flex-end',
            justifyContent: 'space-between',
            gap: 24,
          }}
        >
          <div>
            <div
              style={{
                fontFamily: MONO,
                fontSize: 10.5,
                letterSpacing: '.08em',
                textTransform: 'uppercase',
                color: '#6d7175',
                fontWeight: 500,
              }}
            >
              Settings · Company ID
            </div>
            <h1
              style={{
                margin: '10px 0 0',
                fontSize: 28,
                fontWeight: 600,
                letterSpacing: '-0.03em',
                lineHeight: 1.05,
                color: '#202223',
              }}
            >
              Company ID
            </h1>
            <div style={{ marginTop: 8, fontSize: 13.5, color: '#6d7175' }}>
              {headerSub}
            </div>
          </div>
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: 8,
              paddingBottom: 2,
            }}
          >
            <span
              style={{
                width: 7,
                height: 7,
                borderRadius: '50%',
                background: payload.populated_at ? '#22c55e' : '#babec3',
                display: 'inline-block',
              }}
            />
            <span
              style={{
                fontFamily: MONO,
                fontSize: 10.5,
                letterSpacing: '.08em',
                textTransform: 'uppercase',
                color: '#6d7175',
              }}
            >
              {payload.populated_at
                ? 'Synced · agent, scan and content read from here'
                : 'First read queued'}
            </span>
          </div>
        </div>
      </div>

      <div
        className="cid-hero-grid"
        style={{
          maxWidth: 1400,
          margin: '0 auto',
          display: 'grid',
          // minmax(0,1fr): a bare 1fr floors at the column's min-content, so
          // long field rows would push the grid wider than the (copilot-
          // narrowed) main column and clip under the chat panel.
          gridTemplateColumns: '480px minmax(0, 1fr)',
          gap: 56,
          alignItems: 'start',
        }}
      >
        <div
          className="cid-rings-wrap"
          style={{
            position: 'relative',
            width: '100%',
            maxWidth: 460,
            margin: '0 auto',
          }}
        >
          {glow && (
            <div
              style={{
                position: 'absolute',
                inset: -40,
                borderRadius: '50%',
                background:
                  'radial-gradient(circle, rgba(200,23,93,.28) 0%, rgba(123,63,228,.14) 45%, rgba(9,9,11,0) 70%)',
                animation: 'cidGlow 1.8s cubic-bezier(.4,0,.2,1) 1 both',
                pointerEvents: 'none',
              }}
            />
          )}
          <CompanyIdRings
            fills={fills}
            hover={hover}
            revealed={reducedMotion.current ? true : revealed}
            animate={animate}
            loading={!payload.populated_at || busyMode !== null}
            ambient={!reducedMotion.current}
            ariaLabel={`Company ID ${stats.pct} percent complete, ${stats.filled} of ${stats.total} fields`}
            onRingEnter={setHover}
            onRingLeave={() => setHover(-1)}
            onRingClick={ringIndex => scrollToSection(ringIndex)}
          />
          <div
            style={{
              position: 'absolute',
              inset: 0,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              pointerEvents: 'none',
            }}
          >
            <div
              style={{
                width: 148,
                textAlign: 'center',
                overflowWrap: 'break-word',
              }}
            >
              <div
                style={
                  isComplete
                    ? {
                        fontSize: 17,
                        fontWeight: 600,
                        letterSpacing: '-0.02em',
                        lineHeight: 1.25,
                        background:
                          'linear-gradient(135deg, #C8175D 0%, #7B3FE4 100%)',
                        WebkitBackgroundClip: 'text',
                        backgroundClip: 'text',
                        WebkitTextFillColor: 'transparent',
                      }
                    : {
                        fontSize: 17,
                        fontWeight: 600,
                        letterSpacing: '-0.02em',
                        lineHeight: 1.25,
                        color: '#202223',
                      }
                }
              >
                {centerLine1}
              </div>
              <div
                style={{
                  marginTop: 7,
                  fontFamily: MONO,
                  fontSize: 9.5,
                  letterSpacing: '.1em',
                  textTransform: 'uppercase',
                  color: '#6d7175',
                }}
              >
                {centerLine2}
              </div>
            </div>
          </div>
        </div>

        <div
          className="cid-hero-side"
          style={{ maxWidth: 460, width: '100%', marginLeft: 'auto' }}
        >
          <div
            style={{
              background: '#ffffff',
              border: '1px solid #e1e3e5',
              borderRadius: 14,
              padding: '18px 20px',
            }}
          >
            <div
              style={{
                fontFamily: MONO,
                fontSize: 10.5,
                letterSpacing: '.08em',
                textTransform: 'uppercase',
                color: '#6d7175',
                fontWeight: 500,
              }}
            >
              Company ID completeness
            </div>
            <div
              style={{
                display: 'flex',
                alignItems: 'baseline',
                justifyContent: 'space-between',
                marginTop: 10,
              }}
            >
              <div
                style={{
                  fontFamily: MONO,
                  fontSize: 40,
                  lineHeight: 1,
                  color: '#202223',
                  letterSpacing: '-0.01em',
                }}
              >
                {stats.pct}
                <span style={{ fontSize: 20, color: '#6d7175' }}>%</span>
              </div>
              <div style={{ fontFamily: MONO, fontSize: 12, color: '#6d7175' }}>
                {stats.filled} of {stats.total} fields
              </div>
            </div>
          </div>

          {payload.nudge && (
            <button
              type="button"
              onClick={onNudge}
              style={{
                marginTop: 12,
                width: '100%',
                display: 'flex',
                alignItems: 'center',
                gap: 10,
                background: '#ffffff',
                border: '1px solid #e1e3e5',
                borderRadius: 12,
                padding: '12px 16px',
                cursor: 'pointer',
                font: 'inherit',
                color: '#202223',
                textAlign: 'left',
              }}
            >
              <LuPlusCircle size={16} color="#C8175D" />
              <span style={{ fontSize: 13.5 }}>Next: {payload.nudge.text}</span>
              <LuChevronRight
                size={15}
                color="#6d7175"
                style={{ marginLeft: 'auto' }}
              />
            </button>
          )}

          <div
            style={{ marginTop: 18, display: 'flex', flexDirection: 'column' }}
          >
            {payload.rings.map(ring => {
              const total = ring.fields.length;
              const filledCount = ring.fields.filter(
                field => field.status === 'yours' && !isEmptyValue(field.value)
              ).length;
              const hovered = hover === ring.index;

              return (
                <button
                  type="button"
                  key={ring.index}
                  onClick={() => scrollToSection(ring.index)}
                  onMouseEnter={() => setHover(ring.index)}
                  onMouseLeave={() => setHover(-1)}
                  style={{
                    display: 'grid',
                    gridTemplateColumns: '10px 18px 1fr auto',
                    gap: 10,
                    alignItems: 'center',
                    padding: '7px 10px',
                    borderRadius: 8,
                    background: hovered ? '#ffffff' : 'transparent',
                    border: 'none',
                    cursor: 'pointer',
                    font: 'inherit',
                    textAlign: 'left',
                    transition: 'background 150ms cubic-bezier(.4,0,.2,1)',
                  }}
                >
                  <span
                    style={{
                      width: 8,
                      height: 8,
                      borderRadius: '50%',
                      background: '#C8175D',
                      opacity:
                        filledCount === total
                          ? 1
                          : filledCount > 0
                            ? 0.6
                            : 0.35,
                    }}
                  />
                  <span
                    style={{ fontFamily: MONO, fontSize: 11, color: '#8c9196' }}
                  >
                    {ring.index}
                  </span>
                  <span
                    style={{
                      fontSize: 13,
                      color: hovered ? '#202223' : '#6d7175',
                      transition: 'color 150ms cubic-bezier(.4,0,.2,1)',
                    }}
                  >
                    {ring.name}
                  </span>
                  <span
                    style={{
                      fontFamily: MONO,
                      fontSize: 11.5,
                      color: '#6d7175',
                    }}
                  >
                    {filledCount} of {total}
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      </div>

      <div
        className="cid-lower-grid"
        style={{
          maxWidth: 1400,
          margin: '0 auto',
          display: 'grid',
          gridTemplateColumns: 'minmax(0, 1fr) 300px',
          gap: 48,
          alignItems: 'start',
        }}
      >
        <div>
          {payload.rings.map(ring => {
            const total = ring.fields.length;
            const filledCount = ring.fields.filter(
              field => field.status === 'yours' && !isEmptyValue(field.value)
            ).length;
            const isStub =
              ring.hidden_by_default &&
              !shownMap[ring.index] &&
              !ringHasData(ring);
            const isOpen = !!openMap[ring.index] && !isStub;

            return (
              <div
                key={ring.index}
                id={`cid-sec-${ring.index}`}
                style={{ borderTop: '1px solid #e1e3e5', scrollMarginTop: 16 }}
              >
                {isStub ? (
                  <div
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 14,
                      padding: '15px 0',
                    }}
                  >
                    <span
                      style={{
                        fontFamily: MONO,
                        fontSize: 12,
                        color: '#8c9196',
                        width: 18,
                      }}
                    >
                      {ring.index}
                    </span>
                    <span
                      style={{
                        fontSize: 15,
                        fontWeight: 500,
                        color: '#6d7175',
                      }}
                    >
                      {ring.name}
                    </span>
                    <span
                      style={{
                        fontFamily: MONO,
                        fontSize: 11.5,
                        color: '#8c9196',
                      }}
                    >
                      {filledCount} of {total}
                    </span>
                    <span
                      style={{
                        fontSize: 13,
                        color: '#8c9196',
                        flex: 1,
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      {ring.hidden_note}
                    </span>
                    <button
                      type="button"
                      onClick={() => {
                        setShownMap(current => ({
                          ...current,
                          [ring.index]: true,
                        }));
                        setOpenMap(current => ({
                          ...current,
                          [ring.index]: true,
                        }));
                      }}
                      style={{
                        height: 28,
                        padding: '0 12px',
                        borderRadius: 8,
                        border: '1px solid #e1e3e5',
                        background: 'transparent',
                        color: '#6d7175',
                        font: 'inherit',
                        fontSize: 12.5,
                        cursor: 'pointer',
                      }}
                    >
                      Show
                    </button>
                  </div>
                ) : (
                  <>
                    <button
                      type="button"
                      onClick={() =>
                        setOpenMap(current => ({
                          ...current,
                          [ring.index]: !current[ring.index],
                        }))
                      }
                      style={{
                        width: '100%',
                        display: 'flex',
                        alignItems: 'center',
                        gap: 14,
                        padding: '17px 0',
                        background: 'none',
                        border: 'none',
                        cursor: 'pointer',
                        font: 'inherit',
                        textAlign: 'left',
                      }}
                    >
                      <span
                        style={{
                          fontFamily: MONO,
                          fontSize: 12,
                          color: '#8c9196',
                          width: 18,
                        }}
                      >
                        {ring.index}
                      </span>
                      <span
                        style={{
                          fontSize: 16,
                          fontWeight: 600,
                          letterSpacing: '-0.02em',
                          color: '#202223',
                        }}
                      >
                        {ring.long_name}
                      </span>
                      <span
                        style={{
                          fontFamily: MONO,
                          fontSize: 11.5,
                          color: '#6d7175',
                        }}
                      >
                        {filledCount} of {total}
                      </span>
                      {ring.read_only && (
                        <span
                          style={{
                            fontFamily: MONO,
                            fontSize: 9.5,
                            letterSpacing: '.08em',
                            color: '#6d7175',
                            border: '1px solid #e1e3e5',
                            borderRadius: 5,
                            padding: '2px 7px',
                          }}
                        >
                          READ-ONLY
                        </span>
                      )}
                      <span
                        style={{
                          fontSize: 13,
                          color: '#6d7175',
                          flex: 1,
                          overflow: 'hidden',
                          textOverflow: 'ellipsis',
                          whiteSpace: 'nowrap',
                        }}
                      >
                        {ring.description}
                      </span>
                      <LuChevronDown
                        size={16}
                        color="#6d7175"
                        style={{
                          transform: `rotate(${isOpen ? 0 : -90}deg)`,
                          transition: 'transform 160ms cubic-bezier(.4,0,.2,1)',
                          flexShrink: 0,
                        }}
                      />
                    </button>
                    {isOpen && (
                      <div style={{ padding: '0 0 26px' }}>
                        {ring.fields.map(field => (
                          <FieldRow
                            key={field.id}
                            field={field}
                            ringReadOnly={ring.read_only}
                            domain={payload.domain}
                            isEditing={editingId === field.id}
                            isHot={hotId === field.id}
                            ghostPreview={
                              !payload.populated_at || busyMode !== null
                            }
                            seedValue={
                              editingId === field.id
                                ? (editSeed ?? undefined)
                                : undefined
                            }
                            onStartEdit={(fieldId, seed) => {
                              setEditingId(fieldId);
                              setEditSeed(seed ?? null);
                            }}
                            onCancelEdit={() => {
                              setEditingId(null);
                              setEditSeed(null);
                            }}
                            onCommit={commitEdit}
                            onAddTag={addTag}
                            onRemoveTag={removeTag}
                            onSuggestion={resolveSuggestion}
                            onClaimProposal={claimProposal}
                            onDismissProposal={dismissAiProposal}
                            onUploadLogo={onUploadLogo}
                            logoBusy={logoBusy}
                            searching={busyMode === POPULATE_MODE.WEB}
                            onToast={showToast}
                          />
                        ))}
                      </div>
                    )}
                  </>
                )}
              </div>
            );
          })}
        </div>

        <div>
          <SourceMaterialRail
            domain={payload.domain}
            populatedAt={payload.populated_at}
            documents={documents}
            uploadPhase={uploadPhase}
            onUploadFiles={startUpload}
            onPasteText={submitPasteText}
            pasteSubmittedCount={pasteSubmittedCount}
            kbBusy={pasteBusy || uploadPhase !== 'idle'}
            exportBusy={exportBusy}
            onExportPdf={onExportPdf}
            busyMode={busyMode}
            onPopulate={runPopulate}
            pushBusy={pushBusy}
            onPushToVisibility={onPushToVisibility}
            enginesReadDone={enginesReadDone}
            coreComplete={coreComplete}
            coreRemaining={coreGaps.length}
            pushedOnce={pushedOnce}
            onGoToCore={() => {
              const gap = coreGaps[0];
              if (gap) focusField(CORE_RING_INDEX, gap.id, false);
              else scrollToSection(CORE_RING_INDEX);
            }}
            canDownloadCheatSheet={canDownloadCheatSheet}
            canExtract={canExtract}
            cheatSheetBusy={cheatSheetBusy}
            onDownloadCheatSheet={onDownloadCheatSheet}
            onOpenExtract={setExtractAssistant}
          />
        </div>
      </div>

      <ScrollCue label="Scroll to your record" />

      {toast && (
        <div
          style={{
            position: 'fixed',
            bottom: 28,
            left: '50%',
            transform: 'translateX(-50%)',
            background: '#ffffff',
            border: '1px solid #e1e3e5',
            boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
            borderRadius: 12,
            padding: '11px 18px',
            color: '#202223',
            fontSize: 13.5,
            display: 'flex',
            alignItems: 'center',
            gap: 10,
            animation: 'cidToastIn 240ms cubic-bezier(.4,0,.2,1) both',
            zIndex: 50,
          }}
        >
          <LuSparkles size={14} color="#C8175D" />
          {toast}
        </div>
      )}

      {extractAssistant && clientId && token && (
        <ExtractModal
          assistant={extractAssistant}
          clientId={clientId}
          token={token}
          onClose={() => setExtractAssistant(null)}
          onImported={onExtractImported}
          onToast={showToast}
        />
      )}
    </div>
  );
};

export default Page;
