/**
 * Search Store
 *
 * Node and edge search with fuzzy multi-token matching.
 * Provides text search across node labels/types/IDs and edge labels/types
 * with result navigation and visual filter mode.
 */

import { atom } from 'jotai';
import { uiNodesAtom } from './graph-derived';
import { graphAtom, graphUpdateVersionAtom } from './graph-store';
import { centerOnNodeAtom } from './viewport-store';
import { selectSingleNodeAtom } from './selection-store';

// =============================================================================
// Search Query
// =============================================================================

/**
 * Current search query string.
 * Empty string = no active search.
 */
export const searchQueryAtom = atom<string>('');

/**
 * Set search query
 */
export const setSearchQueryAtom = atom(null, (_get, set, query: string) => {
	set(searchQueryAtom, query);
	set(highlightedSearchIndexAtom, 0);
});

/**
 * Clear search
 */
export const clearSearchAtom = atom(null, (_get, set) => {
	set(searchQueryAtom, '');
	set(highlightedSearchIndexAtom, 0);
});

// =============================================================================
// Fuzzy Matching
// =============================================================================

/**
 * Check if ALL tokens in the query appear (in any order) within the haystack.
 * Tokens are split by whitespace. Each token must match as a substring.
 *
 * @example
 *   fuzzyMatch("inp typ", "Input Type Node") → true
 *   fuzzyMatch("xyz", "Input Type Node")     → false
 */
export function fuzzyMatch(query: string, ...haystacks: string[]): boolean {
	const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
	if (tokens.length === 0) return false;

	const combined = haystacks.join(' ').toLowerCase();

	return tokens.every((token) => combined.includes(token));
}

// =============================================================================
// Node Search Results
// =============================================================================

/**
 * Derived: set of node IDs matching the current search query.
 * Uses fuzzy multi-token matching on label, node_type, and id.
 */
export const searchResultsAtom = atom<Set<string>>((get) => {
	const query = get(searchQueryAtom).trim();
	if (!query) return new Set<string>();

	const nodes = get(uiNodesAtom);
	const matches = new Set<string>();

	for (const node of nodes) {
		if (fuzzyMatch(query, node.label || '', node.dbData.node_type || '', node.id)) {
			matches.add(node.id);
		}
	}

	return matches;
});

/**
 * Derived: ordered array of matching node IDs (for navigation)
 */
export const searchResultsArrayAtom = atom<string[]>((get) => {
	return Array.from(get(searchResultsAtom));
});

/**
 * Derived: count of search results
 */
export const searchResultCountAtom = atom<number>((get) => {
	return get(searchResultsAtom).size;
});

// =============================================================================
// Edge Search Results
// =============================================================================

/**
 * Derived: set of edge keys matching the current search query.
 * Matches on edge label, edge_type, and edge id.
 */
export const searchEdgeResultsAtom = atom<Set<string>>((get) => {
	const query = get(searchQueryAtom).trim();
	if (!query) return new Set<string>();

	// Depend on graph version for reactivity
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);
	const matches = new Set<string>();

	graph.forEachEdge((edgeKey, attrs) => {
		const label = (attrs.label as string) || '';
		const edgeType = (attrs.dbData?.edge_type as string) || '';
		if (fuzzyMatch(query, label, edgeType, edgeKey)) {
			matches.add(edgeKey);
		}
	});

	return matches;
});

/**
 * Derived: count of matching edges
 */
export const searchEdgeResultCountAtom = atom<number>((get) => {
	return get(searchEdgeResultsAtom).size;
});

// =============================================================================
// Combined
// =============================================================================

/**
 * Whether a search filter is currently active
 */
export const isFilterActiveAtom = atom<boolean>((get) => {
	return get(searchQueryAtom).trim().length > 0;
});

/**
 * Total count of matching nodes + edges
 */
export const searchTotalResultCountAtom = atom<number>((get) => {
	return get(searchResultCountAtom) + get(searchEdgeResultCountAtom);
});

// =============================================================================
// Result Navigation
// =============================================================================

/**
 * Index of the currently highlighted search result
 */
export const highlightedSearchIndexAtom = atom<number>(0);

/**
 * Navigate to the next search result
 */
export const nextSearchResultAtom = atom(null, (get, set) => {
	const results = get(searchResultsArrayAtom);
	if (results.length === 0) return;

	const currentIndex = get(highlightedSearchIndexAtom);
	const nextIndex = (currentIndex + 1) % results.length;
	set(highlightedSearchIndexAtom, nextIndex);

	const nodeId = results[nextIndex];
	set(centerOnNodeAtom, nodeId);
	set(selectSingleNodeAtom, nodeId);
});

/**
 * Navigate to the previous search result
 */
export const prevSearchResultAtom = atom(null, (get, set) => {
	const results = get(searchResultsArrayAtom);
	if (results.length === 0) return;

	const currentIndex = get(highlightedSearchIndexAtom);
	const prevIndex = (currentIndex - 1 + results.length) % results.length;
	set(highlightedSearchIndexAtom, prevIndex);

	const nodeId = results[prevIndex];
	set(centerOnNodeAtom, nodeId);
	set(selectSingleNodeAtom, nodeId);
});

/**
 * Get the currently highlighted node ID
 */
export const highlightedSearchNodeIdAtom = atom<string | null>((get) => {
	const results = get(searchResultsArrayAtom);
	if (results.length === 0) return null;
	const index = get(highlightedSearchIndexAtom);
	return results[index] ?? null;
});
