<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Replacements;

/**
 * Invalidation surface for the keyword caches.
 *
 * Three caches exist:
 *   A. `prli_post_keywords` index table (post → keyword mapping).
 *   B. `_plp_post_keywords_updated_at` postmeta stamps (per-post sync
 *      marker compared against the add-log's `occurred_at`).
 *   C. `prli_kw_all` transient (serialized full keyword list joined to links).
 *
 * Most keyword-level changes no longer touch A or B. Adds go through the
 * log and get replayed lazily per-post; removes apply surgically in the
 * repository; link edits only affect the joined columns in C.
 *
 * `invalidateAll()` remains as a big-hammer safety net for third-party
 * REST callers who perform text-edit updates (something the Pretty Links
 * UI can't do).
 */
class CacheInvalidator
{
    /**
     * The keyword index manager.
     *
     * @var KeywordIndexer
     */
    private KeywordIndexer $indexer;

    /**
     * The append-only keyword-add log.
     *
     * @var KeywordAddLog
     */
    private KeywordAddLog $addLog;

    /**
     * The keyword repository.
     *
     * @var KeywordRepository
     */
    private KeywordRepository $keywords;

    /**
     * Constructor.
     *
     * @param KeywordIndexer    $indexer  The keyword index manager.
     * @param KeywordAddLog     $addLog   The append-only keyword-add log.
     * @param KeywordRepository $keywords The keyword repository.
     */
    public function __construct(
        KeywordIndexer $indexer,
        KeywordAddLog $addLog,
        KeywordRepository $keywords
    ) {
        $this->indexer  = $indexer;
        $this->addLog   = $addLog;
        $this->keywords = $keywords;
    }

    /**
     * A new keyword came into existence — log the add so already-indexed
     * posts pick it up on their next render, and clear the `all()` cache
     * since the joined result now includes this row.
     *
     * @param integer $keywordId The id of the newly-added keyword.
     * @param string  $text      The keyword text that was added.
     *
     * @return void
     */
    public function onKeywordAdded(int $keywordId, string $text): void
    {
        $this->addLog->write($keywordId, $text);
        $this->clearListCache();
    }

    /**
     * A keyword (or set of them) was removed. Callers are responsible for
     * the surgical `prli_post_keywords` delete — this method only clears
     * the list-level cache. No stamp invalidation is needed because
     * removals leave the post/keyword mapping correct by construction.
     */
    public function onKeywordRemoved(): void
    {
        $this->clearListCache();
    }

    /**
     * A link row mutated (edit, soft-delete, restore). The index mapping
     * is unaffected; only the joined columns in `all()` change.
     */
    public function onLinkMutated(): void
    {
        $this->clearListCache();
    }

    /**
     * Sledgehammer: wipe the index, clear all stamps, clear the list cache.
     * Reserved for cases where the underlying match semantics could have
     * changed in ways the log can't express — notably third-party REST
     * callers editing a keyword's text directly.
     */
    public function invalidateAll(): void
    {
        $this->indexer->clearAll();
        $this->clearListCache();
    }

    /**
     * Option keys whose change flips what counts as a keyword match —
     * any of these change semantics the persisted index can't represent,
     * so we wipe and re-index lazily. Kept small; KeywordIndexer reads
     * the remaining options fresh each request.
     */
    private const MATCH_AFFECTING_OPTIONS = [
        'keyword_case_sensitive',
        'keyword_ignore',
        'keyword_replacement_cpts',
    ];

    /**
     * `update_option_prlipro_options` handler. Wipes the keyword index
     * when a match-affecting option changes value so the next render
     * re-indexes under the new rules. Other option changes are no-ops.
     *
     * @param mixed $oldValue The previous option value.
     * @param mixed $newValue The new option value.
     *
     * @return void
     */
    public function onOptionsChanged($oldValue, $newValue): void
    {
        $old = is_array($oldValue) ? $oldValue : [];
        $new = is_array($newValue) ? $newValue : [];
        foreach (self::MATCH_AFFECTING_OPTIONS as $key) {
            if (($old[$key] ?? null) !== ($new[$key] ?? null)) {
                $this->invalidateAll();
                return;
            }
        }
    }

    /**
     * Clear the cached full keyword list.
     *
     * @return void
     */
    private function clearListCache(): void
    {
        delete_transient(KeywordRepository::ALL_TRANSIENT);
        $this->keywords->forgetAllMemo();
    }
}
