import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
import { HiSparkles as Sparkles } from 'react-icons/hi';
import {
  FaCheck as Check,
  FaEyeSlash as EyeOff,
  FaRegTrashAlt as Trash2,
} from 'react-icons/fa';

import type {
  AgentGapCategory,
  AgentGapFixActionKind,
  AgentGapMutation,
  IAgentGap,
  IAgentGapFixAction,
  IAgentGapFixSuggestion,
  IAgentGapKbDraft,
  IAgentGapProductSuggestionResponse,
  IAgentGapsResponse,
} from '../../service/agent-analytics/agent-analytics.interface';
import {
  fetchAgentGaps,
  prepareGapKbDraft,
  prepareProductSuggestionForGap,
  suggestAgentGapFix,
  updateAgentGapState,
} from '../../service/agent-analytics/agent-analytics.service';

import GapKbDraftModal from './GapKbDraftModal';
import GapProductSuggestionModal from './GapProductSuggestionModal';
import JourneyDetailModal from './JourneyDetailModal';

const GAPS_PER_PAGE: number = 20;

interface CategoryMeta {
  id: AgentGapCategory;
  label: string;
  iconBg: string;
  iconColor: string;
  border: string;
}

const CATEGORY_META: ReadonlyArray<CategoryMeta> = [
  {
    id: 'info_missing',
    label: 'Info missing',
    iconBg: 'bg-amber-50',
    iconColor: 'text-amber-700',
    border: 'border-amber-200',
  },
  {
    id: 'feature_missing',
    label: 'Feature / product missing',
    iconBg: 'bg-rose-50',
    iconColor: 'text-rose-700',
    border: 'border-rose-200',
  },
  {
    id: 'cannot_guide',
    label: 'Cannot guide',
    iconBg: 'bg-orange-50',
    iconColor: 'text-orange-700',
    border: 'border-orange-200',
  },
  {
    id: 'out_of_scope',
    label: 'Out of scope',
    iconBg: 'bg-sky-50',
    iconColor: 'text-sky-700',
    border: 'border-sky-200',
  },
];

const categoryMetaFor = (category: AgentGapCategory | string): CategoryMeta =>
  CATEGORY_META.find(entry => entry.id === category) ?? CATEGORY_META[0];

const ACTION_META: Record<
  AgentGapFixActionKind,
  { cta: string; iconBg: string; iconColor: string }
> = {
  kb_article: {
    cta: 'Open knowledge base',
    iconBg: 'bg-sky-50',
    iconColor: 'text-sky-700',
  },
  ai_visibility_article: {
    cta: 'Open AI Visibility',
    iconBg: 'bg-[#ffeef8]',
    iconColor: 'text-[#B7007C]',
  },
  product_rewrite: {
    cta: 'Open Content Generator',
    iconBg: 'bg-amber-50',
    iconColor: 'text-amber-700',
  },
  policy_update: {
    cta: 'Open store policies',
    iconBg: 'bg-emerald-50',
    iconColor: 'text-emerald-700',
  },
  manual: {
    cta: '',
    iconBg: 'bg-orange-50',
    iconColor: 'text-orange-700',
  },
};

/**
 * @param {IAgentGapFixAction} action
 * @return {string | null}
 */
function actionDestination(action: IAgentGapFixAction): string | null {
  switch (action.kind) {
    case 'kb_article':
      return '/knowledge-base';
    case 'ai_visibility_article': {
      const topic: string =
        typeof action.payload?.topic === 'string' ? action.payload.topic : '';
      const keywordsRaw: unknown = action.payload?.keywords;
      const keywords: string = Array.isArray(keywordsRaw)
        ? keywordsRaw
            .filter((kw): kw is string => typeof kw === 'string')
            .join(',')
        : '';
      const search: URLSearchParams = new URLSearchParams();
      search.set('action', 'new-article');
      if (topic) search.set('topic', topic);
      if (keywords) search.set('keywords', keywords);

      return `/ai-visibility?${search.toString()}`;
    }
    case 'product_rewrite': {
      const focusRaw: unknown = action.payload?.focus_attributes;
      const focus: string = Array.isArray(focusRaw)
        ? focusRaw
            .filter((attr): attr is string => typeof attr === 'string')
            .join(',')
        : '';
      const search: URLSearchParams = new URLSearchParams();
      if (focus) search.set('focus', focus);
      const qs: string = search.toString();

      return qs ? `/content-generator?${qs}` : '/content-generator';
    }
    case 'policy_update':
      return '/knowledge-base';
    case 'manual':
      return null;
  }
}

interface GapsTabProps {
  startDate: string;
  endDate: string;
}

const EMPTY_CATEGORY_COUNTS: Record<AgentGapCategory, number> = {
  info_missing: 0,
  feature_missing: 0,
  cannot_guide: 0,
  out_of_scope: 0,
};

const GapsTab = ({ startDate, endDate }: GapsTabProps): JSX.Element => {
  const [activeCategory, setActiveCategory] = useState<AgentGapCategory | null>(
    null
  );
  const [gaps, setGaps] = useState<IAgentGap[]>([]);
  const [nextCursor, setNextCursor] = useState<string | null>(null);
  const [categoryCounts, setCategoryCounts] = useState<
    Record<AgentGapCategory, number>
  >(EMPTY_CATEGORY_COUNTS);
  const [totalAll, setTotalAll] = useState<number>(0);
  const [loading, setLoading] = useState<boolean>(true);
  const [loadingMore, setLoadingMore] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [openFingerprint, setOpenFingerprint] = useState<string | null>(null);

  const loadPage = useCallback(
    async (append: boolean, cursor: string | null): Promise<void> => {
      append ? setLoadingMore(true) : setLoading(true);
      setError(null);
      try {
        const result: IAgentGapsResponse = await fetchAgentGaps(
          startDate,
          endDate,
          GAPS_PER_PAGE,
          cursor,
          activeCategory,
          'active'
        );
        setGaps(previous =>
          append ? [...previous, ...result.gaps] : result.gaps
        );
        setNextCursor(result.next_cursor);
        if (result.category_counts) setCategoryCounts(result.category_counts);
        if (typeof result.total_all_categories === 'number') {
          setTotalAll(result.total_all_categories);
        }
      } catch (loadError) {
        setError(
          loadError instanceof Error
            ? loadError.message
            : 'Failed to load agent gaps'
        );
      } finally {
        append ? setLoadingMore(false) : setLoading(false);
      }
    },
    [startDate, endDate, activeCategory]
  );

  useEffect(() => {
    loadPage(false, null);
  }, [activeCategory, startDate, endDate]);

  const handleLoadMore = useCallback((): void => {
    if (!nextCursor) return;
    loadPage(true, nextCursor);
  }, [nextCursor, loadPage]);

  const handleMutateStatus = useCallback(
    async (gap: IAgentGap, status: AgentGapMutation): Promise<void> => {
      const targetKey: string = `${gap.category}::${gap.target}`;
      setGaps(previous =>
        previous.filter(
          entry => `${entry.category}::${entry.target}` !== targetKey
        )
      );
      try {
        await updateAgentGapState({
          target: gap.target,
          category: gap.category,
          status,
        });
      } catch (mutateError) {
        console.error('[gaps] updateAgentGapState failed', mutateError);
      } finally {
        // Counts come from the backend - skip FE-side maths and let the
        // refetch surface the authoritative totals.
        loadPage(false, null);
      }
    },
    [loadPage]
  );

  const visibleGaps: IAgentGap[] = useMemo(() => gaps, [gaps]);

  return (
    <div className="space-y-6">
      <div className="space-y-3">
        <div>
          <p className="text-sm text-gray-500">
            Detected agent gaps in the selected period
          </p>
          <p className="text-2xl font-bold text-gray-900">
            {totalAll.toLocaleString()} gap{totalAll === 1 ? '' : 's'}
          </p>
          <p className="mt-2 max-w-3xl text-xs text-gray-500">
            Each gap is one assistant turn where the agent said it does not
            know, does not carry the product, cannot guide the customer, or
            routed them to a human.
          </p>
        </div>
        <div className="flex flex-wrap gap-2">
          <CategoryChip
            label="All categories"
            count={totalAll}
            active={activeCategory === null}
            onClick={() => setActiveCategory(null)}
            accent="border-gray-200 text-gray-700"
          />
          {CATEGORY_META.map(meta => (
            <CategoryChip
              key={meta.id}
              label={meta.label}
              count={categoryCounts[meta.id] ?? 0}
              active={activeCategory === meta.id}
              onClick={() =>
                setActiveCategory(activeCategory === meta.id ? null : meta.id)
              }
              accent={`${meta.border} ${meta.iconColor}`}
            />
          ))}
        </div>
      </div>
      {error && (
        <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700">
          {error}
        </div>
      )}
      {loading ? (
        <div className="flex justify-center py-16">
          <div className="w-8 h-8 border-2 border-gray-200 border-t-[#B7007C] rounded-full animate-spin" />
        </div>
      ) : visibleGaps.length === 0 ? (
        <div className="rounded-2xl border border-gray-200 bg-white py-12 text-center">
          <p className="text-sm text-gray-500">
            {activeCategory
              ? 'No gaps in this bucket for the selected period.'
              : 'No agent gaps detected for this period. The assistant handled every conversation it received.'}
          </p>
        </div>
      ) : (
        <div className="space-y-3">
          {visibleGaps.map((gap, index) => (
            <GapRow
              key={`${gap.category}-${gap.target}-${index}`}
              gap={gap}
              onOpenSample={setOpenFingerprint}
              onMutateStatus={handleMutateStatus}
            />
          ))}
        </div>
      )}
      {nextCursor && visibleGaps.length > 0 && (
        <div className="flex items-center justify-center py-2">
          <button
            type="button"
            disabled={loadingMore}
            onClick={handleLoadMore}
            className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-full border border-gray-200 bg-white px-4 text-xs font-medium text-gray-700 transition-colors hover:border-[#B7007C]/30 hover:text-[#B7007C] disabled:cursor-not-allowed disabled:opacity-60"
          >
            {loadingMore ? 'Loading...' : 'Load more'}
          </button>
        </div>
      )}
      <JourneyDetailModal
        fingerprint={openFingerprint}
        onClose={() => setOpenFingerprint(null)}
      />
    </div>
  );
};

interface CategoryChipProps {
  label: string;
  count: number;
  active: boolean;
  onClick: () => void;
  accent: string;
}

function CategoryChip({
  label,
  count,
  active,
  onClick,
  accent,
}: CategoryChipProps): JSX.Element {
  return (
    <button
      type="button"
      onClick={onClick}
      className={[
        'inline-flex cursor-pointer items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
        active
          ? 'border-[#B7007C] bg-[#ffeef8] text-[#B7007C]'
          : `${accent} bg-white hover:border-[#B7007C]/30 hover:text-[#B7007C]`,
      ].join(' ')}
    >
      <span>{label}</span>
      <span
        className={`rounded-full px-1.5 py-0.5 text-[10px] tabular-nums ${active ? 'bg-white text-[#B7007C]' : 'bg-gray-100 text-gray-600'}`}
      >
        {count}
      </span>
    </button>
  );
}

interface GapRowProps {
  gap: IAgentGap;
  onOpenSample: (fingerprint: string) => void;
  onMutateStatus: (gap: IAgentGap, status: AgentGapMutation) => Promise<void>;
}

function GapRow({
  gap,
  onOpenSample,
  onMutateStatus,
}: GapRowProps): JSX.Element {
  const meta: CategoryMeta = categoryMetaFor(gap.category);
  const [fixOpen, setFixOpen] = useState<boolean>(false);
  const [pendingStatus, setPendingStatus] = useState<AgentGapMutation | null>(
    null
  );
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState<boolean>(false);

  const handleStatusFlip = async (status: AgentGapMutation): Promise<void> => {
    if (pendingStatus) return;
    setPendingStatus(status);
    try {
      await onMutateStatus(gap, status);
    } finally {
      setPendingStatus(null);
    }
  };

  const handleConfirmDelete = async (): Promise<void> => {
    setConfirmDeleteOpen(false);
    await handleStatusFlip('deleted');
  };

  const sampleCount: number = gap.samples.length;
  const onlySample = sampleCount > 0 ? gap.samples[0] : null;
  const countLabel: string = `${gap.count} conversation${gap.count === 1 ? '' : 's'}`;

  return (
    <div className="rounded-2xl border border-gray-200 bg-white p-4 space-y-3 transition-all hover:border-[#B7007C]/30 hover:shadow-sm">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="flex items-start gap-3">
          <div
            className={`mt-0.5 inline-flex h-8 w-8 items-center justify-center rounded-lg ${meta.iconBg}`}
          />
          <div>
            <p className="text-sm font-semibold text-gray-900">{gap.target}</p>
            <p className="text-xs text-gray-500">{meta.label}</p>
          </div>
        </div>
        <div className="flex items-center gap-2">
          {onlySample ? (
            <button
              type="button"
              onClick={() => onOpenSample(onlySample.fingerprint)}
              title="Open the conversation"
              className="inline-flex cursor-pointer items-center rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-medium tabular-nums text-gray-700 transition-colors hover:border-[#B7007C]/30 hover:text-[#B7007C]"
            >
              {countLabel}
            </button>
          ) : (
            <span className="inline-flex items-center rounded-full border border-gray-200 bg-white px-3 py-1 text-xs tabular-nums text-gray-600">
              {countLabel}
            </span>
          )}
          <button
            type="button"
            onClick={() => setFixOpen(current => !current)}
            className={[
              'inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors',
              fixOpen
                ? 'border-[#B7007C] bg-[#ffeef8] text-[#B7007C]'
                : 'border-gray-200 bg-white text-gray-700 hover:border-[#B7007C]/30 hover:text-[#B7007C]',
            ].join(' ')}
          >
            Fix it {fixOpen ? '−' : '+'}
          </button>
          <button
            type="button"
            onClick={() => handleStatusFlip('completed')}
            disabled={pendingStatus !== null}
            title="Mark this gap as fixed"
            className="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-emerald-200 bg-white px-3 py-1 text-xs font-medium text-emerald-700 transition-colors hover:bg-emerald-50 disabled:cursor-not-allowed disabled:opacity-60"
          >
            Mark fixed
          </button>
          <button
            type="button"
            onClick={() => handleStatusFlip('dismissed')}
            disabled={pendingStatus !== null}
            title="Dismiss this gap (keeps it in the Dismissed list)"
            className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-full border border-gray-200 bg-white text-gray-500 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-60"
          >
            <EyeOff className="h-3.5 w-3.5" />
          </button>
          <button
            type="button"
            onClick={() => setConfirmDeleteOpen(true)}
            disabled={pendingStatus !== null}
            title="Delete this gap (removes it permanently from every list)"
            className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-full border border-red-200 bg-white text-red-600 transition-colors hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
          >
            <Trash2 className="h-3.5 w-3.5" />
          </button>
        </div>
      </div>
      {fixOpen && <GapFixPanel gap={gap} />}
      <ConfirmDeleteGapModal
        gap={gap}
        open={confirmDeleteOpen}
        pending={pendingStatus === 'deleted'}
        onConfirm={handleConfirmDelete}
        onCancel={() => setConfirmDeleteOpen(false)}
      />
    </div>
  );
}

interface GapFixPanelProps {
  gap: IAgentGap;
}

function GapFixPanel({ gap }: GapFixPanelProps): JSX.Element {
  const [suggestion, setSuggestion] = useState<IAgentGapFixSuggestion | null>(
    null
  );
  const [loading, setLoading] = useState<boolean>(true);
  const [panelError, setPanelError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled: boolean = false;
    setLoading(true);
    setPanelError(null);
    const firstSample = gap.samples[0];
    suggestAgentGapFix({
      target: gap.target,
      category: gap.category,
      sample_fingerprint: firstSample?.fingerprint ?? null,
      sample_message_index: firstSample?.message_index ?? null,
    })
      .then(result => {
        if (!cancelled) setSuggestion(result);
      })
      .catch(suggestError => {
        if (!cancelled) {
          setPanelError(
            suggestError instanceof Error
              ? suggestError.message
              : 'Failed to suggest a fix'
          );
        }
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => {
      cancelled = true;
    };
  }, [gap.target, gap.category]);

  return (
    <div className="rounded-xl border border-gray-200 bg-[#fafafa] p-4 space-y-4">
      <span className="text-xs font-semibold uppercase tracking-wide text-gray-500">
        Recommended fix
      </span>
      {loading && (
        <p className="text-xs text-gray-500">
          Asking the assistant how to close this gap...
        </p>
      )}
      {panelError && <p className="text-xs text-red-600">{panelError}</p>}
      {suggestion && !loading && (
        <>
          {suggestion.explanation && (
            <p className="text-sm leading-relaxed text-gray-800">
              {suggestion.explanation}
            </p>
          )}
          {suggestion.actions.length === 0 ? (
            <p className="text-xs text-gray-500">
              No automated action fits this gap. Review the conversation samples
              above and decide on a manual next step.
            </p>
          ) : (
            <div className="space-y-2">
              {suggestion.actions.map((action, index) => (
                <ActionCard
                  key={`${action.kind}-${index}`}
                  action={action}
                  gap={gap}
                />
              ))}
            </div>
          )}
        </>
      )}
    </div>
  );
}

interface ActionCardProps {
  action: IAgentGapFixAction;
  gap: IAgentGap;
}

function ActionCard({ action, gap }: ActionCardProps): JSX.Element {
  const meta = ACTION_META[action.kind] ?? ACTION_META.manual;

  return (
    <div className="rounded-lg border border-gray-200 bg-white p-3">
      <div className="flex items-start gap-3">
        <div
          className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-lg ${meta.iconBg} ${meta.iconColor}`}
        >
          ✦
        </div>
        <div className="flex-1 space-y-1">
          <p className="text-sm font-semibold text-gray-900">{action.label}</p>
          {action.description && (
            <p className="text-xs leading-relaxed text-gray-600">
              {action.description}
            </p>
          )}
        </div>
        <ActionCta action={action} gap={gap} />
      </div>
    </div>
  );
}

interface ActionCtaProps {
  action: IAgentGapFixAction;
  gap: IAgentGap;
}

/**
 * Picks the right CTA for one action. ``kb_article`` runs the in-place
 * "Prepare fix" -> draft modal -> publish flow; ``product_rewrite``
 * fires the agent's enrich-product job and shows an inline confirmation
 * pill. ``policy_update`` has no CTA - the recommendation text is the
 * call to action and the merchant resolves it inside the Knowledge Base
 * via the sibling ``kb_article`` action. Everything else falls back to
 * the static deep-link the original panel had.
 *
 * @param {ActionCtaProps} props - Component props.
 * @return {JSX.Element | null} CTA element, or null when none fits.
 */
function ActionCta({ action, gap }: ActionCtaProps): JSX.Element | null {
  if (action.kind === 'kb_article') {
    return <PrepareKbDraftButton gap={gap} />;
  }
  if (action.kind === 'product_rewrite') {
    return <EnrichProductButton gap={gap} />;
  }
  if (action.kind === 'policy_update') return null;
  const meta = ACTION_META[action.kind] ?? ACTION_META.manual;
  const destination: string | null = actionDestination(action);
  if (!destination || !meta.cta) return null;
  return (
    <Link
      to={destination}
      className="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:border-[#B7007C]/30 hover:text-[#B7007C]"
    >
      {meta.cta} →
    </Link>
  );
}

interface AiActionButtonProps {
  gap: IAgentGap;
}

/**
 * "Prepare fix" CTA on a kb_article action. Asks the agent for a
 * ready-to-publish KB draft, opens :func:`GapKbDraftModal` so the
 * merchant can edit before publishing, then flips into a "Published"
 * pill once the publish round-trip succeeds.
 *
 * @param {AiActionButtonProps} props - Component props.
 * @return {JSX.Element} CTA + modal wrapper.
 */
function PrepareKbDraftButton({ gap }: AiActionButtonProps): JSX.Element {
  const [draft, setDraft] = useState<IAgentGapKbDraft | null>(null);
  const [preparing, setPreparing] = useState<boolean>(false);
  const [published, setPublished] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [modalOpen, setModalOpen] = useState<boolean>(false);

  const handleClick = async (): Promise<void> => {
    if (preparing) return;
    setError(null);
    setPreparing(true);
    try {
      const firstSample = gap.samples[0];
      const result: IAgentGapKbDraft | null = await prepareGapKbDraft({
        target: gap.target,
        category: gap.category,
        sample_fingerprint: firstSample?.fingerprint ?? null,
        sample_message_index: firstSample?.message_index ?? null,
      });
      if (!result) {
        setError('Failed to prepare a fix.');
        return;
      }
      setDraft(result);
      setModalOpen(true);
    } catch (prepareError) {
      setError(
        prepareError instanceof Error
          ? prepareError.message
          : 'Failed to prepare a fix.'
      );
    } finally {
      setPreparing(false);
    }
  };

  if (published) {
    return (
      <span className="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-emerald-200 bg-emerald-50 px-2.5 py-1.5 text-xs font-semibold text-emerald-700">
        <Check className="h-3 w-3" />
        Published
      </span>
    );
  }

  return (
    <div className="flex flex-col items-end gap-1">
      <button
        type="button"
        onClick={handleClick}
        disabled={preparing}
        className="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-md bg-[#B7007C] px-3 text-xs font-semibold text-white shadow-sm shadow-[#B7007C]/30 transition-colors hover:bg-[#9c006a] disabled:cursor-not-allowed disabled:opacity-60"
      >
        {preparing ? (
          <>
            <span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
            Preparing fix...
          </>
        ) : (
          <>
            <Sparkles className="h-3.5 w-3.5" />
            Prepare fix
          </>
        )}
      </button>
      {error && <p className="text-[11px] text-red-600">{error}</p>}
      {draft && modalOpen && (
        <GapKbDraftModal
          gap={gap}
          draft={draft}
          onClose={() => setModalOpen(false)}
          onPublished={() => {
            setPublished(true);
            setModalOpen(false);
          }}
        />
      )}
    </div>
  );
}

/**
 * "Suggest snippet" CTA on a product_rewrite action. Calls the agent's
 * product-suggestion endpoint, then opens
 * :func:`GapProductSuggestionModal` so the merchant can copy / edit /
 * regenerate the snippet inline. No CSV / no email - the whole loop
 * lives in the modal.
 *
 * @param {AiActionButtonProps} props - Component props.
 * @return {JSX.Element} CTA + modal wrapper.
 */
function EnrichProductButton({ gap }: AiActionButtonProps): JSX.Element {
  const [suggestion, setSuggestion] =
    useState<IAgentGapProductSuggestionResponse | null>(null);
  const [preparing, setPreparing] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [modalOpen, setModalOpen] = useState<boolean>(false);

  const handleClick = async (): Promise<void> => {
    if (preparing) return;
    setError(null);
    setPreparing(true);
    try {
      const firstSample = gap.samples[0];
      const result: IAgentGapProductSuggestionResponse | null =
        await prepareProductSuggestionForGap({
          target: gap.target,
          category: gap.category,
          sample_fingerprint: firstSample?.fingerprint ?? null,
          sample_message_index: firstSample?.message_index ?? null,
        });
      if (!result) {
        setError('Failed to prepare the snippet.');
        return;
      }
      setSuggestion(result);
      setModalOpen(true);
    } catch (prepareError) {
      setError(
        prepareError instanceof Error
          ? prepareError.message
          : 'Failed to prepare the snippet.'
      );
    } finally {
      setPreparing(false);
    }
  };

  return (
    <div className="flex flex-col items-end gap-1">
      <button
        type="button"
        onClick={handleClick}
        disabled={preparing}
        className="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-md bg-[#B7007C] px-3 text-xs font-semibold text-white shadow-sm shadow-[#B7007C]/30 transition-colors hover:bg-[#9c006a] disabled:cursor-not-allowed disabled:opacity-60"
      >
        {preparing ? (
          <>
            <span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
            Preparing...
          </>
        ) : (
          <>
            <Sparkles className="h-3.5 w-3.5" />
            Suggest snippet
          </>
        )}
      </button>
      {error && <p className="text-[11px] text-red-600">{error}</p>}
      {suggestion && modalOpen && (
        <GapProductSuggestionModal
          gap={gap}
          initial={suggestion}
          onClose={() => setModalOpen(false)}
        />
      )}
    </div>
  );
}

interface ConfirmDeleteGapModalProps {
  gap: IAgentGap;
  open: boolean;
  pending: boolean;
  onConfirm: () => void;
  onCancel: () => void;
}

/**
 * Confirmation overlay shown before a gap is hard-deleted. The trash
 * icon on the gap row opens this modal; the actual state flip to
 * ``deleted`` fires only after the merchant clicks the destructive
 * button inside.
 *
 * @param {ConfirmDeleteGapModalProps} props - Component props.
 * @return {JSX.Element | null} Overlay element or null when closed.
 */
function ConfirmDeleteGapModal({
  gap,
  open,
  pending,
  onConfirm,
  onCancel,
}: ConfirmDeleteGapModalProps): JSX.Element | null {
  useEffect(() => {
    if (!open) return undefined;
    const handleEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape' && !pending) onCancel();
    };
    document.addEventListener('keydown', handleEscape);
    return () => {
      document.removeEventListener('keydown', handleEscape);
    };
  }, [open, pending, onCancel]);

  if (!open) return null;
  if (typeof document === 'undefined') return null;

  return createPortal(
    <div className="fixed inset-0 z-[9999] flex items-center justify-center">
      <div
        className="absolute inset-0 bg-black/50"
        onClick={() => !pending && onCancel()}
      />
      <div className="relative z-10 mx-4 w-full max-w-md rounded-2xl border border-gray-200 bg-white shadow-2xl">
        <div className="space-y-2 border-b border-gray-200 px-6 py-4">
          <h2 className="text-base font-semibold text-gray-900">
            Delete this gap?
          </h2>
          <p className="text-xs text-gray-500">
            This permanently removes{' '}
            <span className="font-semibold text-gray-900">{gap.target}</span>{' '}
            from every Gaps tab. The action cannot be undone.
          </p>
        </div>
        <div className="flex items-center justify-end gap-3 bg-gray-50 px-6 py-3">
          <button
            type="button"
            onClick={onCancel}
            disabled={pending}
            className="inline-flex h-9 cursor-pointer items-center rounded-md border border-gray-200 bg-white px-4 text-sm font-medium text-gray-700 transition-colors hover:border-gray-300 disabled:cursor-not-allowed disabled:opacity-50"
          >
            Cancel
          </button>
          <button
            type="button"
            onClick={onConfirm}
            disabled={pending}
            className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md bg-red-600 px-4 text-sm font-semibold text-white shadow-sm shadow-red-600/30 transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60"
          >
            {pending ? (
              <>
                <span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
                Deleting...
              </>
            ) : (
              <>
                <Trash2 className="h-3.5 w-3.5" />
                Delete permanently
              </>
            )}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

export default GapsTab;
