<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Replacements;

/**
 * Maps Pretty Link and WordPress events onto the right `PostUrlCache`
 * invalidation call. Surgical where possible — a link or post change
 * only clears the rows that could actually be affected. Full purges are
 * reserved for settings that change the rules for every row (block list,
 * blanket-mode toggle, etc.).
 *
 * All methods are named `onX` so Bootstrap hook registrations stay
 * self-documenting.
 */
class PostUrlCacheInvalidator
{
    /**
     * Option keys whose change requires a full cache purge — each one
     * affects which hrefs match, which get blocked, or which posts are
     * eligible, so previously-cached decisions may no longer be correct.
     */
    private const PURGE_OPTION_KEYS = [
        'replace_urls_with_pretty_links',
        'replace_urls_with_pretty_links_blacklist',
        'url_replacement_is_on',
        'url_replacement_cpts',
        'url_replace_nofollow',
        'url_replace_sponsored',
        'url_replace_new_window',
    ];

    /**
     * The post URL cache.
     *
     * @var PostUrlCache
     */
    private PostUrlCache $cache;

    /**
     * Constructor.
     *
     * @param PostUrlCache $cache The post URL cache.
     */
    public function __construct(PostUrlCache $cache)
    {
        $this->cache = $cache;
    }

    /**
     * Post saved on the front-end admin side. Drop this post's scan so
     * the next visit re-evaluates it against the current link set.
     * Revisions are skipped because they don't render.
     *
     * @param integer $postId The saved post id.
     *
     * @return void
     */
    public function onSavePost(int $postId): void
    {
        if ($postId <= 0 || wp_is_post_revision($postId)) {
            return;
        }
        $this->cache->invalidatePost($postId);
    }

    /**
     * WP-level permanent post deletion. `save_post` doesn't fire, so
     * without this hook the cache rows would orphan.
     *
     * @param integer $postId The deleted post id.
     *
     * @return void
     */
    public function onDeletePost(int $postId): void
    {
        if ($postId <= 0) {
            return;
        }
        $this->cache->invalidatePost($postId);
    }

    /**
     * Per-link URL replacement list was rewritten. Every post that
     * mentions any URL in the union of the old and new lists is
     * potentially affected; posts that mention neither are untouched.
     *
     * @param integer  $linkId   The link whose replacement list was saved.
     * @param string[] $urls     Newly-saved replacement list.
     * @param string[] $previous Prior list, for diff-aware invalidation.
     *
     * @return void
     */
    public function onUrlReplacementsSaved(int $linkId, array $urls, array $previous): void
    {
        unset($linkId);
        $all        = array_values(array_unique(array_merge($urls, $previous)));
        $normalized = [];
        foreach ($all as $u) {
            $n = UrlReplacer::normalizeUrl((string) $u);
            if ($n === '') {
                continue;
            }
            $normalized[] = $n;
            $normalized[] = $n . '/';
        }
        if (!$normalized) {
            return;
        }
        $this->cache->invalidateByUrls(array_values(array_unique($normalized)));
    }

    /**
     * Link URL or slug changed. Cached rows still point at the right
     * link_id, but the rewritten href (or the match key) may have
     * shifted — flush everything tied to this link.
     *
     * @param integer $linkId The updated link id.
     *
     * @return void
     */
    public function onLinkUpdated(int $linkId): void
    {
        if ($linkId <= 0) {
            return;
        }
        $this->cache->invalidateByLinkId($linkId);
    }

    /**
     * Link deleted (soft or hard). Cached replacements pointing at this
     * link are stale either way.
     *
     * @param integer $linkId The deleted link id.
     *
     * @return void
     */
    public function onLinkDeleted(int $linkId): void
    {
        if ($linkId <= 0) {
            return;
        }
        $this->cache->invalidateByLinkId($linkId);
    }

    /**
     * New link created. Posts that contain this URL may have recorded
     * it as unmatched (is_replaced=0); the new link now provides a
     * match, so those posts need to re-scan. Surgical by URL.
     *
     * @param integer $linkId The id of the newly-created link.
     * @param mixed   $row    Link row as array or object — we only read `url`.
     *
     * @return void
     */
    public function onLinkCreated(int $linkId, $row): void
    {
        unset($linkId);
        $url = '';
        if (is_array($row) && isset($row['url'])) {
            $url = (string) $row['url'];
        } elseif (is_object($row) && isset($row->url)) {
            $url = (string) $row->url;
        }
        $normalized = UrlReplacer::normalizeUrl($url);
        if ($normalized === '') {
            return;
        }
        $this->cache->invalidateByUrls([$normalized, $normalized . '/']);
    }

    /**
     * Pro options saved. Full purge if any of the keys listed in
     * PURGE_OPTION_KEYS actually changed — those affect which hrefs
     * match or get blocked, so cached decisions may be stale.
     *
     * @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::PURGE_OPTION_KEYS as $k) {
            if (($old[$k] ?? null) !== ($new[$k] ?? null)) {
                $this->cache->invalidateAll();
                return;
            }
        }
    }
}
