<?php

declare(strict_types=1);

namespace Zab\ZabSite\Search;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Doctrine\DBAL\ParameterType;

/**
 * Finds the closest indexed word for a given search term using Levenshtein distance.
 * Used to provide "Did you mean?" suggestions when a search returns no results.
 */
class FuzzyWordMatcher
{
    /**
     * Find the closest word in the index_words table to the given term.
     *
     * @param string $term        The search term that returned no results
     * @param int    $maxDistance Maximum Levenshtein distance to consider a match (default: 2)
     * @return string|null        The closest matching word, or null if none found within distance
     */
    public function findClosestWord(string $term, int $maxDistance = 2): ?string
    {
        $term = mb_strtolower(trim($term));
        $termLength = mb_strlen($term);

        if ($termLength < 3) {
            return null;
        }

        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
        $queryBuilder = $connectionPool->getQueryBuilderForTable('index_words');

        // Only fetch words with a similar length to avoid pointless comparisons.
        // Length window: term ± (maxDistance + 1)
        $minLen = max(2, $termLength - $maxDistance - 1);
        $maxLen = $termLength + $maxDistance + 1;

        $rows = $queryBuilder
            ->select('baseword')
            ->from('index_words')
            ->where(
                $queryBuilder->expr()->eq('is_stopword', $queryBuilder->createNamedParameter(0, ParameterType::INTEGER)),
                'CHAR_LENGTH(baseword) >= ' . $minLen,
                'CHAR_LENGTH(baseword) <= ' . $maxLen
            )
            ->groupBy('baseword')
            ->executeQuery()
            ->fetchAllAssociative();

        $best = null;
        $bestDist = PHP_INT_MAX;

        foreach ($rows as $row) {
            $word = mb_strtolower($row['baseword']);

            // Skip stop words and numbers
            if ($word === $term || is_numeric($word)) {
                continue;
            }

            $dist = levenshtein($term, $word);

            if ($dist < $bestDist && $dist <= $maxDistance) {
                $bestDist = $dist;
                $best = $row['baseword'];
            }
        }

        return $best;
    }
}
