import {
  rankedFuzzySearch,
  substringFuzzySearch,
} from '@learnitive/fuzzysearch';
import {
  $suggestions,
  Fragment,
  parseWikiLinkContent,
  type WikiLinkAttrs,
} from '@learnitive/prosemirror-plugins';
import type { WsFilePath } from '@learnitive/ws-path';
import { useAtomValue } from 'jotai';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useEditorCoreServices } from '../use-editor-core-services';
import {
  FLOATING_INITIAL_STYLE,
  useFloatingPosition,
} from './use-floating-position';
import { useSuggestionUiHandler } from './use-suggestion-ui-handler';

type WikiSearchRecord = {
  searchText: string;
  target: string;
  wsPath: WsFilePath;
};

export type WikiOption = {
  attrs: WikiLinkAttrs;
  path?: WsFilePath;
  query?: string;
};

const MAX_OPTIONS = 12;

export function buildWikiLinkOptions({
  excludeWsPath,
  query,
  searchRecords = [],
}: {
  excludeWsPath?: string;
  query: string;
  searchRecords?: readonly WikiSearchRecord[];
}): WikiOption[] {
  let filtered: readonly WikiSearchRecord[] = excludeWsPath
    ? searchRecords.filter((record) => record.wsPath.wsPath !== excludeWsPath)
    : searchRecords;
  if (query && filtered.length) {
    const bySearchValue = new Map(
      filtered.map((record) => [record.searchText, record]),
    );
    filtered = rankedFuzzySearch(query, [...bySearchValue.keys()], {
      fuzzySearchFunction: substringFuzzySearch,
      limit: MAX_OPTIONS - 1,
    })
      .map((result) => bySearchValue.get(result.item))
      .filter((record): record is WikiSearchRecord => Boolean(record));
  }
  const matchLimit = query ? MAX_OPTIONS - 1 : MAX_OPTIONS;
  const matches = filtered.slice(0, matchLimit).map((record) => ({
    path: record.wsPath,
    attrs: { target: record.target, label: null },
  }));
  const unresolvedAttrs = query ? parseWikiLinkContent(query) : null;
  return unresolvedAttrs
    ? [...matches, { attrs: unresolvedAttrs, query }]
    : matches;
}

export function WikiLinkMenu({ editorName }: { editorName: string }) {
  const suggestions = useAtomValue($suggestions);
  const { editorEngine, workspaceState } = useEditorCoreServices();
  const index = useAtomValue(workspaceState.$wikiLinkIndex);
  const listRef = useRef<HTMLDivElement>(null);
  const editorView = editorEngine.getEditor(editorName);
  const suggestion = editorView ? suggestions.get(editorView) : undefined;
  const active =
    suggestion?.markName === 'wiki_link_suggestion' ? suggestion : undefined;
  const query = active?.text.slice(2) ?? '';
  const editorStatus = editorEngine.getEditorLoadStatus(editorName);
  const excludeWsPath =
    editorStatus.status === 'ready' ? editorStatus.wsPath : undefined;

  const options = useMemo<WikiOption[]>(
    () =>
      buildWikiLinkOptions({
        excludeWsPath,
        query,
        searchRecords: index?.searchRecords,
      }),
    [excludeWsPath, index, query],
  );

  const selectedIndex = Math.max(
    0,
    Math.min(active?.selectedIndex ?? 0, Math.max(0, options.length - 1)),
  );
  const select = useCallback(
    (option: WikiOption | undefined) => {
      if (!editorView || !option) return;
      const node = editorView.state.schema.nodes.wiki_link?.create(
        option.attrs,
      );
      if (!node) return;
      editorEngine.extensions.wikiSuggestions.command.replaceSuggestMarkWith({
        content: Fragment.from(node),
      })(editorView.state, editorView.dispatch, editorView);
    },
    [editorView, editorEngine],
  );

  const selectedOption = options[selectedIndex];
  const wikiSuggestionUiHandler = useMemo(
    () => ({
      optionCount: options.length,
      onSelect: () => select(selectedOption),
    }),
    [options.length, select, selectedOption],
  );

  useSuggestionUiHandler({
    active: Boolean(active),
    editorView,
    handler: wikiSuggestionUiHandler,
    markName: 'wiki_link_suggestion',
  });

  useEffect(() => {
    listRef.current
      ?.querySelector<HTMLElement>(`[data-option-index="${selectedIndex}"]`)
      ?.scrollIntoView({ block: 'nearest' });
  }, [selectedIndex]);

  const floatingRef = useFloatingPosition({
    show: Boolean(active?.show),
    anchorEl: () => active?.anchorEl() ?? null,
    // Constrain to the owning editor: a global selector would pick the
    // first editable ProseMirror on the page and leak across editors.
    boundaryElement: editorView?.dom ?? null,
  });
  if (!active?.show || !editorView) return null;

  return (
    <div ref={floatingRef} style={FLOATING_INITIAL_STYLE}>
      <div
        ref={listRef}
        role="listbox"
        aria-label={t.app.editor.wikiLinkMenu.label}
        className="max-h-72 min-w-64 overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
      >
        {options.length === 0 ? (
          <div className="px-2 py-1.5 text-muted-foreground">
            {t.app.editor.wikiLinkMenu.empty}
          </div>
        ) : (
          options.map((option, index) => (
            <div
              key={
                option.path?.wsPath ??
                `unresolved:${option.query ?? option.attrs.target}`
              }
              role="option"
              tabIndex={-1}
              aria-selected={index === selectedIndex}
              data-option-index={index}
              className={`cursor-default rounded px-2 py-1.5 ${
                index === selectedIndex
                  ? 'bg-accent text-accent-foreground'
                  : ''
              }`}
              onMouseDown={(event) => event.preventDefault()}
              onClick={() => select(option)}
              onKeyDown={(event) => {
                if (event.key === 'Enter') select(option);
              }}
            >
              <div>
                {option.path?.fileNameWithoutExtension ??
                  t.app.editor.wikiLinkMenu.linkTo({
                    query: option.query ?? option.attrs.target,
                  })}
              </div>
              {option.path?.getParent()?.path ? (
                <div className="text-muted-foreground text-xs">
                  {option.path.getParent()?.path}
                </div>
              ) : null}
            </div>
          ))
        )}
      </div>
    </div>
  );
}
