<?php

/**
 * Schema Module
 *
 * Phase 1 owner of the site-wide Schema Aggregation feature.
 *
 * For Phase 1 the module exists purely to wire cache invalidation hooks for
 * the SchemaMapCache. It intentionally does NOT render any admin UI or
 * duplicate the per-page schema output that SchemaOutputManager already
 * handles — the goal is a single owning module for aggregation plumbing,
 * not a second schema manager.
 *
 * When the aggregation surface grows (admin toggle, NLWeb, partner hooks)
 * this module is the right place to hang it.
 *
 * @package ProRank\SEO\Modules\TechnicalSEO
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\TechnicalSEO;

use ProRank\SEO\Core\Schema\SchemaMapCache;
use ProRank\SEO\Modules\BaseModule;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


class SchemaModule extends BaseModule {

    /**
     * Module slug.
     *
     * @var string
     */
    protected string $slug = 'schema';

    /**
     * Module name.
     *
     * @var string
     */
    protected string $name = 'Schema Aggregation';

    /**
     * Module description.
     *
     * @var string
     */
    protected string $description = 'AI Discovery: expose a site-wide JSON-LD @graph for LLMs and AI crawlers via a unified endpoint.';

    /**
     * Required tier.
     *
     * @var string
     */
    protected string $feature_tier = 'free';

    /**
     * Parent module.
     *
     * @var string|null
     */
    protected ?string $parent_slug = 'technical-seo';

    /**
     * Cache instance used for invalidation.
     *
     * @var SchemaMapCache|null
     */
    private ?SchemaMapCache $cache = null;

    /**
     * Initialize module hooks.
     *
     * Wires invalidation for the schema-map cache on any event that can
     * change aggregated output:
     *
     *   - post saved, trashed, untrashed, deleted
     *   - `_prorank_schema_data` meta written (editor schema tab)
     *   - site basics option updated (org/person details, logo, social)
     *
     * @return void
     */
    public function init_hooks(): void {
        // NOTE: the endpoint enable/disable gate is intentionally NOT
        // registered here — it lives in SchemaMapGate and is evaluated by
        // the controllers directly. If we registered it here, a disabled
        // schema module could leave the endpoint effectively ungated
        // (init_hooks doesn't run for inactive modules).

        // Toggle changes invalidate the map so disabling the endpoint
        // doesn't leave a hot cached payload lying around. These hooks are
        // only attached when the module is active; when it's inactive, the
        // endpoint is already gated off by SchemaMapGate so invalidation
        // is moot.
        add_action('update_option_prorank_schema_map_enabled', [$this, 'invalidate_cache']);
        add_action('add_option_prorank_schema_map_enabled',    [$this, 'invalidate_cache']);
        add_action('delete_option_prorank_schema_map_enabled', [$this, 'invalidate_cache']);

        // Post lifecycle.
        add_action('save_post', [$this, 'invalidate_on_post_event'], 20, 2);
        add_action('deleted_post', [$this, 'invalidate_on_post_event'], 20, 1);
        add_action('trashed_post', [$this, 'invalidate_on_post_event'], 20, 1);
        add_action('untrashed_post', [$this, 'invalidate_on_post_event'], 20, 1);
        add_action('transition_post_status', [$this, 'invalidate_on_status_change'], 20, 3);

        // Per-post schema meta writes.
        add_action('updated_post_meta', [$this, 'invalidate_on_schema_meta'], 20, 4);
        add_action('added_post_meta', [$this, 'invalidate_on_schema_meta'], 20, 4);
        add_action('deleted_post_meta', [$this, 'invalidate_on_schema_meta'], 20, 4);

        // Site-level config (org/person, logo, social profiles).
        add_action('update_option_prorank_seo_site_basics', [$this, 'invalidate_cache']);
        add_action('add_option_prorank_seo_site_basics', [$this, 'invalidate_cache']);

        // Permalink structure changes invalidate every entity @id.
        add_action('permalink_structure_changed', [$this, 'invalidate_cache']);
        add_action('update_option_home', [$this, 'invalidate_cache']);
        add_action('update_option_siteurl', [$this, 'invalidate_cache']);

        /**
         * Allow other modules to trigger a schema-map flush.
         *
         * Example:
         *   do_action('prorank_schema_map_flush');
         */
        add_action('prorank_schema_map_flush', [$this, 'invalidate_cache']);
    }

    /**
     * Invalidate on save_post / trash / untrash / deleted_post.
     *
     * Skips auto-draft and revisions — save_post fires for those too and we
     * don't want to spam cache flushes during a single edit session.
     *
     * @param int          $post_id Post ID.
     * @param \WP_Post|null $post   Post object (save_post provides this; other hooks do not).
     * @return void
     */
    public function invalidate_on_post_event(int $post_id, $post = null): void {
        if ($post_id <= 0) {
            return;
        }
        if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
            return;
        }
        if ($post instanceof \WP_Post && in_array($post->post_status, ['auto-draft'], true)) {
            return;
        }
        $this->invalidate_cache();
    }

    /**
     * Invalidate when a post moves in/out of publish status.
     *
     * @param string   $new_status New status.
     * @param string   $old_status Previous status.
     * @param \WP_Post $post       Post object.
     * @return void
     */
    public function invalidate_on_status_change(string $new_status, string $old_status, $post): void {
        if ($new_status === $old_status) {
            return;
        }
        if ($new_status === 'publish' || $old_status === 'publish') {
            $this->invalidate_cache();
        }
    }

    /**
     * Invalidate when the per-post schema meta key is written.
     *
     * Also invalidates on robots meta changes — flipping a post to noindex
     * removes it from the aggregation.
     *
     * WordPress passes a single int for added_post_meta / updated_post_meta
     * and an array of IDs for deleted_post_meta. The first parameter is
     * deliberately untyped so a single callback handles all three hooks
     * cleanly under strict_types=1.
     *
     * @param mixed  $_meta_ids   Meta ID (int) or IDs (array) — not used.
     * @param int    $object_id   Post ID.
     * @param string $meta_key    Meta key.
     * @param mixed  $_meta_value Meta value — not used.
     * @return void
     */
    public function invalidate_on_schema_meta($_meta_ids, int $object_id, string $meta_key, $_meta_value): void {
        if ($object_id <= 0) {
            return;
        }
        if ($meta_key === '_prorank_schema_data' || $meta_key === '_prorank_meta_robots') {
            $this->invalidate_cache();
        }
    }

    /**
     * Flush the schema-map cache.
     *
     * Records the flush timestamp so operator-facing surfaces (e.g. the
     * admin "Schema Aggregation" panel) can display it regardless of which
     * hook triggered the invalidation.
     *
     * @return void
     */
    public function invalidate_cache(): void {
        if (!class_exists(SchemaMapCache::class)) {
            return;
        }
        if ($this->cache === null) {
            $this->cache = new SchemaMapCache();
        }
        $this->cache->flush();
        update_option('prorank_schema_map_last_flushed_at', time(), false);
    }
}
