import { useCoreServices } from '@learnitive/context';
import type {
  EmbeddingModelState,
  ProjectMeaningIndexProgress,
  ProjectMeaningSearchResult,
  ProjectMeaningStatus,
  ProjectResearchReviewReport,
  ProjectTextSearchMatch,
  ProjectTextSearchOptions,
} from '@learnitive/service-core';
import type {
  ProjectDoctorReport,
  ProjectSemanticSearchResult,
} from '@learnitive/project-semantic-index';
import {
  projectFilePath,
  type ProjectFilePath,
} from '@learnitive/project-storage';
import {
  Button,
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  Input,
  Label,
  toast,
} from '@learnitive/ui-components';
import { type ProjectCapabilities } from '@learnitive/project-runtime';
import { WsPath } from '@learnitive/ws-path';
import {
  AlertCircle,
  CheckSquare,
  FileSearch,
  BookOpenCheck,
  BrainCircuit,
  Download,
  HeartPulse,
  RefreshCw,
  Replace,
  Search,
  Shapes,
  Trash2,
  Square,
} from 'lucide-react';
import React from 'react';

type Tab = 'text' | 'symbols' | 'meaning' | 'doctor' | 'research' | 'refactor';
type RefactorKind = 'label' | 'citation' | 'file';
type TextScope = 'project' | 'folder' | 'file';

function parentFolder(file?: ProjectFilePath): string {
  if (!file) return '';
  const slash = file.lastIndexOf('/');
  return slash < 0 ? '' : file.slice(0, slash);
}

function splitGlobs(value: string): string[] | undefined {
  const globs = value
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
  return globs.length ? globs : undefined;
}

function groupTextResults(results: readonly ProjectTextSearchMatch[]) {
  const groups = new Map<ProjectFilePath, ProjectTextSearchMatch[]>();
  for (const result of results) {
    const list = groups.get(result.file) ?? [];
    list.push(result);
    groups.set(result.file, list);
  }
  return [...groups];
}

export function ProjectToolsDialog({
  wsName,
  activeFile,
  open,
  onOpenChange,
  initialTab = 'text',
  initialTextScope = 'project',
  capabilities,
}: {
  wsName: string;
  activeFile?: ProjectFilePath;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  initialTab?: Tab;
  initialTextScope?: TextScope;
  capabilities?: ProjectCapabilities;
}) {
  const { latexSource, navigation, projectTools } = useCoreServices();
  const latexTools = capabilities?.latexTools ?? true;
  const [tab, setTab] = React.useState<Tab>('text');
  const [busy, setBusy] = React.useState(false);

  const [textQuery, setTextQuery] = React.useState('');
  const [replacement, setReplacement] = React.useState('');
  const [textScope, setTextScope] = React.useState<TextScope>('project');
  const [caseSensitive, setCaseSensitive] = React.useState(false);
  const [wholeWord, setWholeWord] = React.useState(false);
  const [regex, setRegex] = React.useState(false);
  const [includeGlobs, setIncludeGlobs] = React.useState(() => latexTools ? '**/*.tex, **/*.ltx, **/*.bib' : '');
  const [excludeGlobs, setExcludeGlobs] = React.useState(() => latexTools ? '' : '**/node_modules/**, **/.git/**, **/dist/**, **/build/**, **/target/**, **/vendor/**, **/.venv/**, **/coverage/**, **/.cache/**');
  const [textResults, setTextResults] = React.useState<ProjectTextSearchMatch[]>([]);
  const [selectedMatches, setSelectedMatches] = React.useState<Set<string>>(new Set());

  const [symbolQuery, setSymbolQuery] = React.useState('');
  const [symbolResults, setSymbolResults] = React.useState<ProjectSemanticSearchResult[]>([]);
  const [meaningQuery, setMeaningQuery] = React.useState('');
  const [meaningResults, setMeaningResults] = React.useState<ProjectMeaningSearchResult[]>([]);
  const [meaningStatus, setMeaningStatus] = React.useState<ProjectMeaningStatus>();
  const [meaningProgress, setMeaningProgress] = React.useState<ProjectMeaningIndexProgress>();
  const [meaningModelProgress, setMeaningModelProgress] = React.useState<EmbeddingModelState>();
  const meaningAbortRef = React.useRef<AbortController | undefined>(undefined);
  const [doctor, setDoctor] = React.useState<ProjectDoctorReport>();
  const [researchReview, setResearchReview] = React.useState<ProjectResearchReviewReport>();
  const [refactorKind, setRefactorKind] = React.useState<RefactorKind>(() => latexTools ? 'label' : 'file');
  const [fromValue, setFromValue] = React.useState('');
  const [toValue, setToValue] = React.useState('');

  React.useEffect(() => {
    if (!open) return;
    const requestedTab = !latexTools && (initialTab === 'symbols' || initialTab === 'meaning' || initialTab === 'research') ? 'text' : initialTab;
    setTab(requestedTab);
    setTextScope(initialTextScope === 'file' && !activeFile ? 'project' : initialTextScope);
  }, [activeFile, initialTab, initialTextScope, latexTools, open]);

  React.useEffect(() => {
    setIncludeGlobs(latexTools ? '**/*.tex, **/*.ltx, **/*.bib' : '');
    setExcludeGlobs(latexTools ? '' : '**/node_modules/**, **/.git/**, **/dist/**, **/build/**, **/target/**, **/vendor/**, **/.venv/**, **/coverage/**, **/.cache/**');
    setRefactorKind(latexTools ? 'label' : 'file');
  }, [latexTools, wsName]);

  const navigateTo = (file: ProjectFilePath, line = 1, column?: number) => {
    const wsPath = WsPath.fromParts(wsName, file).wsPath;
    latexSource.requestSelect(wsPath, line, column);
    navigation.goWsFile(wsPath);
    onOpenChange(false);
  };

  const makeTextOptions = React.useCallback((): ProjectTextSearchOptions => {
    const folder = parentFolder(activeFile);
    return {
      query: textQuery,
      scope:
        textScope === 'file' && activeFile
          ? { kind: 'file', file: activeFile }
          : textScope === 'folder'
            ? { kind: 'folder', folder }
            : { kind: 'project' },
      caseSensitive,
      wholeWord,
      regex,
      includeGlobs: splitGlobs(includeGlobs),
      excludeGlobs: splitGlobs(excludeGlobs),
    };
  }, [activeFile, caseSensitive, excludeGlobs, includeGlobs, regex, textQuery, textScope, wholeWord]);

  const runTextSearch = React.useCallback(async () => {
    if (!textQuery) {
      setTextResults([]);
      setSelectedMatches(new Set());
      return;
    }
    setBusy(true);
    try {
      const results = await projectTools.searchText(wsName, makeTextOptions());
      setTextResults(results);
      setSelectedMatches(new Set(results.map((result) => result.id)));
    } catch (error) {
      console.error('Unable to search Learnitive project text', error);
      toast.error(error instanceof Error ? error.message : t.app.projectTools.searchFailed);
    } finally {
      setBusy(false);
    }
  }, [makeTextOptions, projectTools, textQuery, wsName]);

  const replaceMatches = async (ids?: readonly string[]) => {
    if (!textQuery) return;
    setBusy(true);
    try {
      const result = await projectTools.replaceText(
        wsName,
        makeTextOptions(),
        replacement,
        ids,
      );
      toast.success(t.app.projectTools.replaceComplete({ count: result.replaced, files: result.files }));
      await runTextSearch();
    } catch (error) {
      console.error('Unable to replace Learnitive project text', error);
      toast.error(error instanceof Error ? error.message : t.app.projectTools.replaceFailed);
    } finally {
      setBusy(false);
    }
  };

  const runSymbolSearch = React.useCallback(async () => {
    if (!symbolQuery.trim()) {
      setSymbolResults([]);
      return;
    }
    setBusy(true);
    try {
      setSymbolResults(await projectTools.search(wsName, symbolQuery));
    } catch (error) {
      console.error('Unable to search Learnitive project symbols', error);
      toast.error(t.app.projectTools.searchFailed);
    } finally {
      setBusy(false);
    }
  }, [projectTools, symbolQuery, wsName]);

  const refreshMeaningStatus = React.useCallback(async () => {
    try {
      setMeaningStatus(await projectTools.meaningStatus(wsName));
    } catch (error) {
      console.error('Unable to inspect Learnitive project meaning', error);
      toast.error(t.app.projectTools.meaningFailed);
    }
  }, [projectTools, wsName]);

  const prepareMeaningModel = React.useCallback(async () => {
    meaningAbortRef.current?.abort();
    const controller = new AbortController();
    meaningAbortRef.current = controller;
    setBusy(true);
    try {
      const model = await projectTools.prepareMeaningModel(setMeaningModelProgress, controller.signal);
      setMeaningModelProgress(model);
      await refreshMeaningStatus();
      toast.success(t.app.projectTools.meaningModelReady);
    } catch (error) {
      if (!controller.signal.aborted) {
        console.error('Unable to prepare EmbeddingGemma', error);
        toast.error(error instanceof Error ? error.message : t.app.projectTools.meaningFailed);
      }
    } finally {
      if (meaningAbortRef.current === controller) meaningAbortRef.current = undefined;
      setBusy(false);
    }
  }, [projectTools, refreshMeaningStatus]);

  const indexMeaning = React.useCallback(async () => {
    meaningAbortRef.current?.abort();
    const controller = new AbortController();
    meaningAbortRef.current = controller;
    setBusy(true);
    setMeaningProgress(undefined);
    try {
      await projectTools.indexMeaning(wsName, { signal: controller.signal, onProgress: setMeaningProgress });
      await refreshMeaningStatus();
      toast.success(t.app.projectTools.meaningIndexed);
    } catch (error) {
      if (!controller.signal.aborted) {
        console.error('Unable to index project meaning', error);
        toast.error(error instanceof Error ? error.message : t.app.projectTools.meaningFailed);
      }
    } finally {
      if (meaningAbortRef.current === controller) meaningAbortRef.current = undefined;
      setBusy(false);
    }
  }, [projectTools, refreshMeaningStatus, wsName]);

  const runMeaningSearch = React.useCallback(async () => {
    if (!meaningQuery.trim()) {
      setMeaningResults([]);
      return;
    }
    meaningAbortRef.current?.abort();
    const controller = new AbortController();
    meaningAbortRef.current = controller;
    setBusy(true);
    try {
      const results = await projectTools.searchMeaning(wsName, meaningQuery, {
        limit: 30,
        signal: controller.signal,
        onProgress: setMeaningProgress,
      });
      setMeaningResults(results);
      await refreshMeaningStatus();
    } catch (error) {
      if (!controller.signal.aborted) {
        console.error('Unable to search project meaning', error);
        toast.error(error instanceof Error ? error.message : t.app.projectTools.meaningFailed);
      }
    } finally {
      if (meaningAbortRef.current === controller) meaningAbortRef.current = undefined;
      setBusy(false);
    }
  }, [meaningQuery, projectTools, refreshMeaningStatus, wsName]);

  const removeMeaningModel = React.useCallback(async () => {
    setBusy(true);
    try {
      await projectTools.removeMeaningModel();
      setMeaningResults([]);
      setMeaningModelProgress(undefined);
      await refreshMeaningStatus();
      toast.success(t.app.projectTools.meaningModelRemoved);
    } catch (error) {
      console.error('Unable to remove EmbeddingGemma', error);
      toast.error(t.app.projectTools.meaningFailed);
    } finally {
      setBusy(false);
    }
  }, [projectTools, refreshMeaningStatus]);

  const clearMeaningIndex = React.useCallback(async () => {
    setBusy(true);
    try {
      await projectTools.clearMeaningIndex(wsName);
      setMeaningResults([]);
      setMeaningProgress(undefined);
      await refreshMeaningStatus();
      toast.success(t.app.projectTools.meaningIndexCleared);
    } catch (error) {
      console.error('Unable to clear project meaning', error);
      toast.error(t.app.projectTools.meaningFailed);
    } finally {
      setBusy(false);
    }
  }, [projectTools, refreshMeaningStatus, wsName]);

  const runDoctor = React.useCallback(async () => {
    setBusy(true);
    try {
      setDoctor(await projectTools.doctor(wsName));
    } catch (error) {
      console.error('Unable to run Learnitive Doctor', error);
      toast.error(t.app.projectTools.doctorFailed);
    } finally {
      setBusy(false);
    }
  }, [projectTools, wsName]);

  const runResearchReview = React.useCallback(async () => {
    setBusy(true);
    try {
      setResearchReview(await projectTools.researchReview(wsName));
    } catch (error) {
      console.error('Unable to run Learnitive research review', error);
      toast.error(t.app.projectTools.researchReviewFailed);
    } finally {
      setBusy(false);
    }
  }, [projectTools, wsName]);

  React.useEffect(() => {
    if (!open) return;
    if (tab === 'meaning') void refreshMeaningStatus();
    if (tab === 'doctor') void runDoctor();
    if (tab === 'research') void runResearchReview();
    return () => { if (tab === 'meaning') meaningAbortRef.current?.abort(); };
  }, [open, refreshMeaningStatus, runDoctor, runResearchReview, tab]);

  const applyRefactor = async () => {
    if (!fromValue.trim() || !toValue.trim()) return;
    setBusy(true);
    try {
      if (refactorKind === 'label') {
        await projectTools.renameLabel(wsName, fromValue.trim(), toValue.trim());
      } else if (refactorKind === 'citation') {
        await projectTools.renameCitationKey(wsName, fromValue.trim(), toValue.trim());
      } else {
        await projectTools.moveProjectFile(
          wsName,
          projectFilePath(fromValue.trim()),
          projectFilePath(toValue.trim()),
        );
      }
      toast.success(t.app.projectTools.refactorComplete);
      setFromValue('');
      setToValue('');
    } catch (error) {
      console.error('Unable to refactor Learnitive project', error);
      toast.error(error instanceof Error ? error.message : t.app.projectTools.refactorFailed);
    } finally {
      setBusy(false);
    }
  };

  const selectedCount = selectedMatches.size;
  const groupedResults = groupTextResults(textResults);
  const meaningModelInstalled = meaningStatus?.model.status === 'installed' || meaningStatus?.model.status === 'ready';

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="flex max-h-[86vh] flex-col sm:max-w-4xl" data-testid="interactive-project-tools-dialog">
        <DialogHeader>
          <DialogTitle>{t.app.projectTools.title}</DialogTitle>
          <DialogDescription>{t.app.projectTools.description}</DialogDescription>
        </DialogHeader>
        <div className="flex flex-wrap gap-1 border-b pb-2" role="tablist" aria-label={t.app.projectTools.title}>
          <Button size="sm" variant={tab === 'text' ? 'secondary' : 'ghost'} onClick={() => setTab('text')}>
            <Search className="mr-1 size-4" />{t.app.projectTools.text}
          </Button>
          {latexTools && <Button size="sm" variant={tab === 'symbols' ? 'secondary' : 'ghost'} onClick={() => setTab('symbols')}>
            <Shapes className="mr-1 size-4" />{t.app.projectTools.symbols}
          </Button>}
          {latexTools && <Button size="sm" variant={tab === 'meaning' ? 'secondary' : 'ghost'} onClick={() => setTab('meaning')}>
            <BrainCircuit className="mr-1 size-4" />{t.app.projectTools.meaning}
          </Button>}
          <Button size="sm" variant={tab === 'doctor' ? 'secondary' : 'ghost'} onClick={() => setTab('doctor')}>
            <HeartPulse className="mr-1 size-4" />{latexTools ? t.app.projectTools.doctor : 'Diagnostics'}
          </Button>
          {latexTools && <Button size="sm" variant={tab === 'research' ? 'secondary' : 'ghost'} onClick={() => setTab('research')}>
            <BookOpenCheck className="mr-1 size-4" />{t.app.projectTools.researchReview}
          </Button>}
          <Button size="sm" variant={tab === 'refactor' ? 'secondary' : 'ghost'} onClick={() => setTab('refactor')}>
            <Replace className="mr-1 size-4" />{t.app.projectTools.refactor}
          </Button>
        </div>

        <div className="min-h-0 flex-1 overflow-auto py-2">
          {tab === 'text' && (
            <div className="space-y-3" data-testid="interactive-project-text-search">
              <div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
                <Input
                  aria-label={t.app.projectTools.textSearchPlaceholder}
                  placeholder={t.app.projectTools.textSearchPlaceholder}
                  value={textQuery}
                  onChange={(event) => setTextQuery(event.currentTarget.value)}
                  onKeyDown={(event) => {
                    if (event.key === 'Enter') void runTextSearch();
                  }}
                />
                <Button disabled={busy || !textQuery} onClick={() => void runTextSearch()}>
                  <FileSearch className="mr-1 size-4" />{t.app.projectTools.search}
                </Button>
              </div>

              <div className="grid gap-2 rounded-md border p-2 text-xs sm:grid-cols-3">
                <label className="flex flex-col gap-1">
                  <span className="text-muted-foreground">{t.app.projectTools.scope}</span>
                  <select
                    className="h-8 rounded-md border bg-background px-2 text-sm"
                    value={textScope}
                    onChange={(event) => setTextScope(event.currentTarget.value as TextScope)}
                  >
                    <option value="project">{t.app.projectTools.wholeProject}</option>
                    <option value="folder" disabled={!parentFolder(activeFile)}>{t.app.projectTools.currentFolder}</option>
                    <option value="file" disabled={!activeFile}>{t.app.projectTools.currentFile}</option>
                  </select>
                </label>
                <label className="flex flex-col gap-1 sm:col-span-2" htmlFor="project-tools-include-globs">
                  <span className="text-muted-foreground">{t.app.projectTools.includeGlobs}</span>
                  <Input id="project-tools-include-globs" className="h-8" value={includeGlobs} onChange={(event) => setIncludeGlobs(event.currentTarget.value)} />
                </label>
                <label className="flex flex-col gap-1 sm:col-span-3" htmlFor="project-tools-exclude-globs">
                  <span className="text-muted-foreground">{t.app.projectTools.excludeGlobs}</span>
                  <Input id="project-tools-exclude-globs" className="h-8" placeholder="build/**, vendor/**" value={excludeGlobs} onChange={(event) => setExcludeGlobs(event.currentTarget.value)} />
                </label>
                <label className="flex items-center gap-2"><input checked={caseSensitive} type="checkbox" onChange={(event) => setCaseSensitive(event.currentTarget.checked)} />{t.app.projectTools.caseSensitive}</label>
                <label className="flex items-center gap-2"><input checked={wholeWord} type="checkbox" onChange={(event) => setWholeWord(event.currentTarget.checked)} />{t.app.projectTools.wholeWord}</label>
                <label className="flex items-center gap-2"><input checked={regex} type="checkbox" onChange={(event) => setRegex(event.currentTarget.checked)} />{t.app.projectTools.regex}</label>
              </div>

              <div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto_auto]">
                <Input aria-label={t.app.projectTools.replaceWith} placeholder={t.app.projectTools.replaceWith} value={replacement} onChange={(event) => setReplacement(event.currentTarget.value)} />
                <Button variant="outline" disabled={busy || selectedCount === 0} onClick={() => void replaceMatches([...selectedMatches])}>
                  {t.app.projectTools.replaceSelected} ({selectedCount})
                </Button>
                <Button variant="outline" disabled={busy || textResults.length === 0} onClick={() => void replaceMatches()}>
                  {t.app.projectTools.replaceAll} ({textResults.length})
                </Button>
              </div>

              {textResults.length > 0 && (
                <div className="flex items-center gap-2 text-xs text-muted-foreground">
                  <button className="inline-flex items-center gap-1 hover:text-foreground" type="button" onClick={() => setSelectedMatches(new Set(textResults.map((result) => result.id)))}>
                    <CheckSquare className="size-3.5" />{t.app.projectTools.selectAll}
                  </button>
                  <button className="inline-flex items-center gap-1 hover:text-foreground" type="button" onClick={() => setSelectedMatches(new Set())}>
                    <Square className="size-3.5" />{t.app.projectTools.selectNone}
                  </button>
                  <span>{t.app.projectTools.affectedSummary({ matches: textResults.length, files: groupedResults.length })}</span>
                </div>
              )}

              {textResults.length === 0 && textQuery && !busy && (
                <p className="text-sm text-muted-foreground">{t.app.projectTools.noTextResults}</p>
              )}
              <div className="space-y-3">
                {groupedResults.map(([file, matches]) => (
                  <section key={file} className="rounded-md border" data-search-file={file}>
                    <div className="border-b bg-muted/30 px-3 py-1.5 text-xs font-medium">{file} · {matches.length}</div>
                    <div className="divide-y">
                      {matches.map((result) => {
                        const selected = selectedMatches.has(result.id);
                        return (
                          <div key={result.id} className="flex items-start gap-2 px-2 py-2">
                            <input
                              aria-label={`${selected ? 'Deselect' : 'Select'} ${file}:${result.line}:${result.column}`}
                              checked={selected}
                              className="mt-1"
                              type="checkbox"
                              onChange={() => {
                                setSelectedMatches((current) => {
                                  const next = new Set(current);
                                  if (next.has(result.id)) next.delete(result.id);
                                  else next.add(result.id);
                                  return next;
                                });
                              }}
                            />
                            <button className="min-w-0 flex-1 text-left" type="button" onClick={() => navigateTo(result.file, result.line, result.column)}>
                              <span className="block font-mono text-[11px] text-muted-foreground">{result.line}:{result.column}</span>
                              <span className="block truncate font-mono text-xs">{result.preview}</span>
                            </button>
                            <Button size="sm" variant="ghost" disabled={busy} onClick={() => void replaceMatches([result.id])}>{t.app.projectTools.replaceOne}</Button>
                          </div>
                        );
                      })}
                    </div>
                  </section>
                ))}
              </div>
            </div>
          )}

          {tab === 'symbols' && (
            <div className="space-y-3" data-testid="interactive-project-symbol-search">
              <div className="flex gap-2">
                <Input
                  aria-label={t.app.projectTools.searchPlaceholder}
                  placeholder={t.app.projectTools.searchPlaceholder}
                  value={symbolQuery}
                  onChange={(event) => setSymbolQuery(event.currentTarget.value)}
                  onKeyDown={(event) => { if (event.key === 'Enter') void runSymbolSearch(); }}
                />
                <Button disabled={busy || !symbolQuery.trim()} onClick={() => void runSymbolSearch()}>{t.app.projectTools.search}</Button>
              </div>
              {symbolResults.length === 0 && symbolQuery.trim() && !busy && <p className="text-sm text-muted-foreground">{t.app.projectTools.noResults}</p>}
              <div className="space-y-1">
                {symbolResults.map((result) => (
                  <button
                    key={`${result.kind}:${result.file}:${result.line}:${result.label}`}
                    className="flex w-full items-start gap-3 rounded border px-3 py-2 text-left hover:bg-muted/50"
                    type="button"
                    onClick={() => navigateTo(result.file, result.line)}
                  >
                    <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">{result.kind}</span>
                    <span className="min-w-0 flex-1">
                      <span className="block truncate text-sm font-medium">{result.label}</span>
                      <span className="block truncate text-xs text-muted-foreground">{result.file}:{result.line}{result.detail ? ` · ${result.detail}` : ''}</span>
                    </span>
                  </button>
                ))}
              </div>
            </div>
          )}

          {tab === 'meaning' && (
            <div className="space-y-3" data-testid="interactive-project-meaning-search">
              <div className="rounded-md border p-3">
                <div className="flex flex-wrap items-start gap-3">
                  <div className="min-w-0 flex-1">
                    <h3 className="text-sm font-semibold">{t.app.projectTools.meaningTitle}</h3>
                    <p className="mt-1 text-xs text-muted-foreground">{t.app.projectTools.meaningDescription}</p>
                    <p className="mt-2 text-[11px] text-muted-foreground">{t.app.projectTools.meaningPrivacy}</p>
                  </div>
                  {!meaningModelInstalled ? (
                    <Button disabled={busy || !meaningStatus || meaningStatus.capabilities.supported === false} size="sm" onClick={() => void prepareMeaningModel()}>
                      <Download className="mr-1 size-4" />{t.app.projectTools.downloadEmbedding}
                    </Button>
                  ) : (
                    <div className="flex gap-1">
                      <Button disabled={busy} size="sm" variant="outline" onClick={() => void indexMeaning()}>{t.app.projectTools.indexMeaning}</Button>
                      <Button aria-label={t.app.projectTools.removeEmbedding} disabled={busy} size="icon" variant="ghost" onClick={() => void removeMeaningModel()}><Trash2 className="size-4" /></Button>
                    </div>
                  )}
                </div>
                {meaningStatus && !meaningStatus.capabilities.supported && (
                  <p className="mt-2 rounded border border-amber-500/30 bg-amber-500/10 p-2 text-xs">{meaningStatus.capabilities.reason ?? t.app.projectTools.meaningUnsupported}</p>
                )}
                {meaningModelProgress?.status === 'downloading' && (
                  <div className="mt-3 space-y-1" aria-live="polite">
                    <div className="h-1.5 overflow-hidden rounded bg-muted"><div className="h-full bg-primary transition-[width]" style={{ width: `${Math.round((meaningModelProgress.progress ?? 0) * 100)}%` }} /></div>
                    <div className="flex items-center justify-between text-[11px] text-muted-foreground">
                      <span>{t.app.projectTools.meaningDownloading({ percent: Math.round((meaningModelProgress.progress ?? 0) * 100) })}</span>
                      <button className="hover:text-foreground" type="button" onClick={() => meaningAbortRef.current?.abort()}>{t.app.projectTools.cancel}</button>
                    </div>
                  </div>
                )}
                {meaningProgress && (
                  <p className="mt-2 text-[11px] text-muted-foreground" aria-live="polite">{t.app.projectTools.meaningProgress({ phase: meaningProgress.phase, completed: meaningProgress.completed, total: meaningProgress.total, reused: meaningProgress.reused })}</p>
                )}
                {meaningStatus?.index && (
                  <div className="mt-2 flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground">
                    <span>{t.app.projectTools.meaningIndexedPassages({ count: meaningStatus.index.passages })}</span>
                    <button className="hover:text-foreground" disabled={busy} type="button" onClick={() => void clearMeaningIndex()}>{t.app.projectTools.clearMeaning}</button>
                  </div>
                )}
              </div>

              <div className="flex gap-2">
                <Input
                  aria-label={t.app.projectTools.meaningSearchPlaceholder}
                  placeholder={t.app.projectTools.meaningSearchPlaceholder}
                  value={meaningQuery}
                  onChange={(event) => setMeaningQuery(event.currentTarget.value)}
                  onKeyDown={(event) => { if (event.key === 'Enter') void runMeaningSearch(); }}
                />
                <Button disabled={busy || !meaningQuery.trim() || !meaningModelInstalled} onClick={() => void runMeaningSearch()}>{t.app.projectTools.search}</Button>
              </div>
              {meaningResults.length === 0 && meaningQuery.trim() && !busy && <p className="text-sm text-muted-foreground">{t.app.projectTools.meaningNoResults}</p>}
              <div className="space-y-1">
                {meaningResults.map((result) => (
                  <button
                    key={result.chunk.id}
                    className="flex w-full items-start gap-3 rounded border px-3 py-2 text-left hover:bg-muted/50"
                    type="button"
                    onClick={() => navigateTo(result.chunk.file, result.chunk.line)}
                  >
                    <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">{result.chunk.kind}</span>
                    <span className="min-w-0 flex-1">
                      <span className="block truncate text-sm font-medium">{result.chunk.title ?? result.chunk.text}</span>
                      {result.chunk.title && <span className="mt-0.5 block line-clamp-2 text-xs text-muted-foreground">{result.chunk.text}</span>}
                      <span className="mt-1 block font-mono text-[10px] text-muted-foreground">{result.chunk.file}:{result.chunk.line} · {Math.max(0, result.score).toFixed(3)}</span>
                    </span>
                  </button>
                ))}
              </div>
            </div>
          )}

          {tab === 'doctor' && (
            <div className="space-y-3">
              <div className="flex items-center gap-3">
                <div className="rounded-lg border px-4 py-3">
                  <div className="text-2xl font-semibold tabular-nums">{doctor?.score ?? '—'}</div>
                  <div className="text-xs text-muted-foreground">{t.app.projectTools.healthScore}</div>
                </div>
                <div className="text-sm text-muted-foreground">
                  {doctor ? `${doctor.errors} ${t.app.projectTools.errors} · ${doctor.warnings} ${t.app.projectTools.warnings}` : t.app.projectTools.doctorRunning}
                </div>
                <Button className="ml-auto" size="sm" variant="outline" disabled={busy} onClick={() => void runDoctor()}>
                  <RefreshCw className="mr-1 size-4" />{t.app.projectTools.refresh}
                </Button>
              </div>
              {doctor?.issues.length === 0 && <p className="rounded border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm">{t.app.projectTools.healthy}</p>}
              <div className="space-y-2">
                {doctor?.issues.map((issue) => (
                  <button
                    key={issue.id}
                    type="button"
                    disabled={!issue.file}
                    className="flex w-full gap-2 rounded border p-3 text-left disabled:cursor-default"
                    onClick={() => issue.file && navigateTo(issue.file, issue.line ?? 1)}
                  >
                    <AlertCircle className={`mt-0.5 size-4 shrink-0 ${issue.severity === 'error' ? 'text-destructive' : issue.severity === 'warning' ? 'text-amber-600' : 'text-muted-foreground'}`} />
                    <span>
                      <span className="block text-sm">{issue.message}</span>
                      <span className="text-xs text-muted-foreground">{issue.category}{issue.file ? ` · ${issue.file}${issue.line ? `:${issue.line}` : ''}` : ''}</span>
                    </span>
                  </button>
                ))}
              </div>
            </div>
          )}

          {tab === 'research' && (
            <div className="space-y-3" data-testid="interactive-research-review">
              <div className="flex flex-wrap items-start gap-3 rounded-md border p-3">
                <div className="min-w-0 flex-1">
                  <h3 className="text-sm font-semibold">{t.app.projectTools.researchReview}</h3>
                  <p className="mt-1 text-xs text-muted-foreground">{t.app.projectTools.researchReviewDescription}</p>
                </div>
                <Button size="sm" variant="outline" disabled={busy} onClick={() => void runResearchReview()}>
                  <RefreshCw className="mr-1 size-4" />{t.app.projectTools.refresh}
                </Button>
              </div>
              {researchReview && (
                <p className="text-xs text-muted-foreground">{t.app.projectTools.researchReviewSummary({ findings: researchReview.findings.length, files: researchReview.filesReviewed })}</p>
              )}
              {researchReview?.findings.length === 0 && (
                <p className="rounded border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm">{t.app.projectTools.researchReviewClear}</p>
              )}
              <div className="space-y-2">
                {researchReview?.findings.map((finding) => (
                  <button
                    key={finding.id}
                    type="button"
                    className="flex w-full items-start gap-2 rounded border p-3 text-left hover:bg-muted/40"
                    onClick={() => navigateTo(finding.file, finding.line)}
                  >
                    <span className={`mt-0.5 rounded px-1.5 py-0.5 text-[10px] uppercase ${finding.severity === 'warning' ? 'bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'bg-muted text-muted-foreground'}`}>{finding.severity}</span>
                    <span className="min-w-0 flex-1">
                      <span className="block text-sm font-medium">{finding.title}</span>
                      <span className="mt-0.5 block text-xs text-muted-foreground">{finding.message}</span>
                      <span className="mt-1 block font-mono text-[10px] text-muted-foreground">{finding.file}:{finding.line}</span>
                    </span>
                  </button>
                ))}
              </div>
              <p className="text-[11px] text-muted-foreground">{t.app.projectTools.researchReviewNoRewrite}</p>
            </div>
          )}

          {tab === 'refactor' && (
            <div className="space-y-4">
              <div className="flex gap-1">
                {(latexTools ? (['label', 'citation', 'file'] as const) : (['file'] as const)).map((kind) => (
                  <Button key={kind} size="sm" variant={refactorKind === kind ? 'secondary' : 'outline'} onClick={() => setRefactorKind(kind)}>
                    {kind === 'label' ? t.app.projectTools.renameLabel : kind === 'citation' ? t.app.projectTools.renameCitation : t.app.projectTools.moveFile}
                  </Button>
                ))}
              </div>
              <div className="space-y-2">
                <Label htmlFor="interactive-refactor-from">{refactorKind === 'file' ? t.app.projectTools.currentPath : t.app.projectTools.currentName}</Label>
                <Input id="interactive-refactor-from" value={fromValue} onChange={(event) => setFromValue(event.currentTarget.value)} />
                <Label htmlFor="interactive-refactor-to">{refactorKind === 'file' ? t.app.projectTools.newPath : t.app.projectTools.newName}</Label>
                <Input id="interactive-refactor-to" value={toValue} onChange={(event) => setToValue(event.currentTarget.value)} />
              </div>
              <p className="text-xs text-muted-foreground">{refactorKind === 'file' ? t.app.projectTools.fileRefactorNotice : t.app.projectTools.symbolRefactorNotice}</p>
              <Button disabled={busy || !fromValue.trim() || !toValue.trim()} onClick={() => void applyRefactor()}>{t.app.projectTools.applyRefactor}</Button>
            </div>
          )}
        </div>
      </DialogContent>
    </Dialog>
  );
}
