<?php

namespace auctionforge\inc\controllers;

/**
 * Cache-purge orchestrator.
 *
 * Single entry point for invalidating a page across every active cache layer
 * (Cloudflare edge, Hummingbird origin file-cache, LiteSpeed). AuctionForge
 * calls this whenever a lot's data changes — from the BidSpirit sync (only for
 * lots the change-detector flagged as updated) and from manual wp-admin edits —
 * and whenever an AUCTION changes (manual_auction post or auction_number term),
 * to keep the auction-listing pages (Ended auctions, home, upcoming) fresh.
 *
 * During a sync run the importer sets $_SERVER['AUCTIONFORGE_IMPORT_LOT']. While
 * that is set, lots are QUEUED BY ID and their URLs are resolved + purged once
 * on `shutdown` — by which point each lot's auction_number term is associated
 * and its meta written, so get_permalink() is correct and one Cloudflare call
 * covers the whole run. Manual edits resolve + purge immediately.
 *
 * @package auctionforge\inc\controllers
 */
class CachePurge
{
    /** @var array<int,int> queued lots during a sync: postId => auction term id (0 if none). */
    private static $pending_lots = array();

    /** @var bool whether the shutdown flush has been registered this request. */
    private static $shutdown_hooked = false;

    /** @var bool auction-listing pages need purging (queued during a sync). */
    private static $pending_listings = false;

    /** @var array<int,int> manual auctions changed during a sync (postId => 1) for own-page purge. */
    private static $pending_auctions = array();

    /** @var bool whether the auction-change hooks have been registered. */
    private static $auction_hooks_registered = false;

    /**
     * Register hooks that purge the auction-LISTING pages whenever an auction
     * changes — manual auctions (the `manual_auction` CPT) and synced auctions
     * (`auction_number` terms). Idempotent; called once at plugin load (bottom
     * of this file).
     */
    public static function registerAuctionHooks()
    {
        if (self::$auction_hooks_registered || !function_exists('add_action')) {
            return;
        }
        self::$auction_hooks_registered = true;

        // Manual auctions (custom post type).
        add_action('save_post_' . ManualAuctions::POST_TYPE, array(__CLASS__, 'onAuctionSave'), 20, 1);
        add_action('before_delete_post', array(__CLASS__, 'onAuctionDelete'), 20, 1);
        add_action('wp_trash_post', array(__CLASS__, 'onAuctionDelete'), 20, 1);
        add_action('untrashed_post', array(__CLASS__, 'onAuctionDelete'), 20, 1);

        // Synced auctions (taxonomy terms).
        $tax = BidspiritLots::TAX_AUCTION_TYPE;
        add_action('created_' . $tax, array(__CLASS__, 'onAuctionTermChange'), 20, 0);
        add_action('edited_' . $tax, array(__CLASS__, 'onAuctionTermChange'), 20, 0);
        add_action('delete_' . $tax, array(__CLASS__, 'onAuctionTermChange'), 20, 0);
    }

    /** save_post handler for a manual auction (skips autosaves/revisions). */
    public static function onAuctionSave($postId)
    {
        if (wp_is_post_revision($postId) || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)) {
            return;
        }
        self::purgeAuctionListings((int) $postId);
    }

    /** delete/trash/untrash handler — only acts on manual_auction posts. */
    public static function onAuctionDelete($postId)
    {
        if (get_post_type($postId) === ManualAuctions::POST_TYPE) {
            self::purgeAuctionListings((int) $postId);
        }
    }

    /** auction_number term create/edit/delete handler (synced auctions). */
    public static function onAuctionTermChange()
    {
        self::purgeAuctionListings();
    }

    /**
     * Purge a lot and its parent auction/catalog page. Queues during a sync
     * run (URLs resolved + flushed on shutdown); purges immediately otherwise.
     *
     * @param int      $postId        Lot post ID.
     * @param int|null $auctionTermId auction_number term ID (parent catalog).
     */
    public static function purgeLot($postId, $auctionTermId = null)
    {
        /**
         * Allow disabling cache purging entirely (e.g. per site). Defaults on.
         * @param bool $enabled
         */
        if (!apply_filters('auctionforge_cache_purge_enabled', true)) {
            return;
        }

        $postId = (int) $postId;
        if ($postId <= 0) {
            return;
        }

        if (!empty($_SERVER['AUCTIONFORGE_IMPORT_LOT'])) {
            // Defer: the term/meta may not be written yet. Resolve on shutdown.
            self::$pending_lots[$postId] = $auctionTermId ? (int) $auctionTermId : 0;
            self::register_shutdown();
        } else {
            self::purgeNow(self::lotUrls($postId, $auctionTermId));
        }
    }

    /**
     * Purge the pages that LIST auctions (front page + any page holding an
     * auction-listing shortcode + the changed auction's own page) so a
     * deleted/changed auction stops showing. Queues during a sync (one flush
     * on shutdown); purges immediately for manual wp-admin changes.
     *
     * @param int|null $auctionPostId changed manual-auction post (also purge its page).
     */
    public static function purgeAuctionListings($auctionPostId = null)
    {
        if (!apply_filters('auctionforge_cache_purge_enabled', true)) {
            return;
        }

        if (!empty($_SERVER['AUCTIONFORGE_IMPORT_LOT'])) {
            self::$pending_listings = true;
            if ($auctionPostId) {
                self::$pending_auctions[(int) $auctionPostId] = 1;
            }
            self::register_shutdown();
        } else {
            self::purgeNow(self::listingUrls($auctionPostId));
        }
    }

    /**
     * Resolve a lot's own URL + its parent catalog URL.
     *
     * @param  int      $postId
     * @param  int|null $auctionTermId
     * @return string[]
     */
    private static function lotUrls($postId, $auctionTermId)
    {
        $urls = array();

        $lotUrl = get_permalink($postId);
        if ($lotUrl) {
            $urls[] = $lotUrl;
        }

        // Resolve the parent catalog term from the lot if it wasn't passed
        // (e.g. from a save_post hook), so callers don't need to know it.
        if (!$auctionTermId) {
            $terms = get_the_terms($postId, BidspiritLots::TAX_AUCTION_TYPE);
            if (!is_wp_error($terms) && !empty($terms) && isset($terms[0]->term_id)) {
                $auctionTermId = $terms[0]->term_id;
            }
        }
        if ($auctionTermId) {
            $termUrl = get_term_link((int) $auctionTermId, BidspiritLots::TAX_AUCTION_TYPE);
            if (!is_wp_error($termUrl) && $termUrl) {
                $urls[] = $termUrl;
            }
        }

        return $urls;
    }

    /**
     * URLs of the pages that LIST auctions: the front page, every published
     * page whose content holds an auction/past-sales/upcoming shortcode, plus
     * the given auction's own permalink.
     *
     * @param  int|null $auctionPostId
     * @return string[]
     */
    private static function listingUrls($auctionPostId = null)
    {
        $urls = array(home_url('/'));

        if ($auctionPostId) {
            $pl = get_permalink((int) $auctionPostId);
            if ($pl) {
                $urls[] = $pl;
            }
        }

        $shortcodes = array(
            'bp_auction_list', 'bp_past_sales', 'bp_upcoming_wide',
            'bp_lot_list', 'bp_trending_lots', 'bp_popular_lots', 'bp_most_favorited',
        );
        $pages = get_posts(array(
            'post_type'   => 'page',
            'post_status' => 'publish',
            'numberposts' => 300,
            'fields'      => 'ids',
        ));
        foreach ((array) $pages as $pid) {
            $content = get_post_field('post_content', $pid);
            if (!$content) {
                continue;
            }
            foreach ($shortcodes as $sc) {
                if (has_shortcode($content, $sc)) {
                    $u = get_permalink($pid);
                    if ($u) {
                        $urls[] = $u;
                    }
                    break;
                }
            }
        }

        /**
         * Adjust which listing URLs get purged on an auction change.
         * @param string[] $urls
         * @param int|null $auctionPostId
         */
        return apply_filters('auctionforge_auction_listing_urls', $urls, $auctionPostId);
    }

    /** Register the one-time shutdown flush (fires after the whole request). */
    private static function register_shutdown()
    {
        if (!self::$shutdown_hooked && function_exists('add_action')) {
            add_action('shutdown', array(__CLASS__, 'flush'), 99);
            self::$shutdown_hooked = true;
        }
    }

    /** Resolve every queued lot + any queued listing purge, and purge (on shutdown). */
    public static function flush()
    {
        $urls = array();

        if (!empty(self::$pending_lots)) {
            $lots = self::$pending_lots;
            self::$pending_lots = array();
            foreach ($lots as $postId => $termId) {
                foreach (self::lotUrls($postId, $termId ?: null) as $u) {
                    $urls[] = $u;
                }
            }
        }

        if (self::$pending_listings) {
            self::$pending_listings = false;
            $auctions = self::$pending_auctions;
            self::$pending_auctions = array();
            foreach (self::listingUrls(null) as $u) {
                $urls[] = $u;
            }
            foreach (array_keys($auctions) as $pid) {
                $pl = get_permalink((int) $pid);
                if ($pl) {
                    $urls[] = $pl;
                }
            }
        }

        if (!empty($urls)) {
            self::purgeNow($urls);
        }
    }

    /**
     * Purge a set of absolute URLs across every active cache layer, now.
     *
     * @param string[] $urls
     */
    public static function purgeNow($urls)
    {
        $urls = array_values(array_unique(array_filter(array_map('strval', (array) $urls))));
        if (empty($urls)) {
            return;
        }

        // 1) Cloudflare edge (the caching layer for CF-proxied sites).
        if (CloudflareSupport::isConfigured()) {
            CloudflareSupport::purgeUrls($urls);
        }

        // 2) Hummingbird origin file-cache, if that plugin is active.
        if (HummingbirdSupport::isHummingbirdActive()) {
            foreach ($urls as $u) {
                HummingbirdSupport::clearCacheByUrl($u);
            }
        }

        // 3) LiteSpeed, if running on a LiteSpeed server with the plugin.
        if (defined('LSCWP_V') || defined('LSWCP_TAG_PREFIX')) {
            foreach ($urls as $u) {
                do_action('litespeed_purge_url', $u);
            }
        }

        /**
         * Let other integrations hook additional cache layers.
         * @param string[] $urls
         */
        do_action('auctionforge_purge_urls', $urls);
    }
}

// Register auction-change purge hooks on `init` — deferred so the referenced controller
// classes (ManualAuctions, BidspiritLots) are all loaded first (this file is required
// before ManualAuctions in coreIncludes).
if (function_exists('add_action')) {
    add_action('init', array(CachePurge::class, 'registerAuctionHooks'), 5);
}
