<?php

namespace App\Services;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Lvl0\AnagramFinder\Core\Matcher;

/**
 * Caches anagram lookups. A search scans the whole dictionary — 466k words —
 * and the result is fixed for a given (letters, minLength, dictionary), so it
 * only ever needs computing once.
 */
class AnagramSearch
{
    private const DICTIONARY = __DIR__ . '/../../vendor/anagram-finder/core/assets/words.txt';

    public function find(string $anagram, int $minLength = 3): Collection
    {
        $key = $this->key($anagram, $minLength);

        // Cache a plain array: a serialised Collection comes back as
        // __PHP_Incomplete_Class through the database store.
        $words = Cache::rememberForever(
            $key,
            fn () => Matcher::findWords($this->normalise($anagram), $minLength)
                ->values()
                ->all(),
        );

        return new Collection($words);
    }

    /**
     * Entries live forever; the dictionary hash in the key is the invalidation
     * mechanism. Replacing words.txt makes every old key unreachable rather
     * than stale, so nothing has to be flushed by hand.
     */
    private function key(string $anagram, int $minLength): string
    {
        return sprintf(
            'anagram:%s:%d:%s',
            $this->dictionaryHash(),
            $minLength,
            $this->normalise($anagram),
        );
    }

    /**
     * Sorted letters, so "eamstoxil" and "latoximse" share one entry — the same
     * bag of letters is the same query.
     */
    private function normalise(string $anagram): string
    {
        $letters = str_split(strtolower($anagram));
        sort($letters);

        return implode('', $letters);
    }

    /**
     * Content-addressed, not mtime: the dictionary ships inside the image, so
     * its mtime changes on every rebuild even when the words do not.
     *
     * Hashing 4.6MB per request would undo the saving, so the hash is itself
     * cached under a key made of the cheap stat values that change whenever the
     * file is replaced.
     */
    private function dictionaryHash(): string
    {
        $path = self::DICTIONARY;

        if (! is_file($path)) {
            return 'nodict';
        }

        $stat = filemtime($path) . ':' . filesize($path);

        return Cache::rememberForever(
            "anagram:dict:{$stat}",
            fn () => hash_file('xxh128', $path),
        );
    }
}
