<?php
/**
 * Where a page's meta title and description actually live.
 *
 * If the site runs an SEO plugin that already owns these two fields, that
 * plugin's post meta IS the storage — Schemafy reads and writes the very same
 * rows instead of keeping a parallel copy. One field, one value, whichever
 * screen you edit it from, and the other plugin's snippet preview and content
 * score stay correct because they are looking at the value you actually typed.
 *
 * It also means Schemafy has nothing to override on the frontend: the owner
 * outputs its own storage, which now holds the user's value. That is the whole
 * point — no filter fighting, no divergence, no "did it save?".
 *
 * Only plugins whose post meta is genuinely authoritative are listed. All in
 * One SEO (v4) keeps titles in its own database table and Slim SEO packs them
 * into a single array, so writing post meta there would silently do nothing;
 * for those Schemafy keeps its own storage and overrides output as before.
 *
 * @package Schemafy
 */

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

class SCHEMAFY_Meta_Store {

    /**
     * Schemafy's own post meta, used only when no SEO plugin owns the fields.
     */
    const OWN_TITLE_KEY = '_schemafy_custom_meta_title';
    const OWN_DESC_KEY  = '_schemafy_custom_meta_description';

    /**
     * SEO plugins whose post meta is the real source of truth.
     *
     * Order is priority: the first active one owns the fields.
     *
     * @return array[] slug => [ detect (callable), title, description ]
     */
    private static function owners() {
        return array(
            'yoast' => array(
                'detect' => function () { return defined( 'WPSEO_VERSION' ); },
                'name'         => 'Yoast SEO',
                'title'        => '_yoast_wpseo_title',
                'desc'         => '_yoast_wpseo_metadesc',
                'title_filter' => 'wpseo_title',
                'desc_filter'  => 'wpseo_metadesc',
            ),
            'rank-math' => array(
                'detect' => function () { return defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath' ); },
                'name'         => 'Rank Math',
                'title'        => 'rank_math_title',
                'desc'         => 'rank_math_description',
                'title_filter' => 'rank_math/frontend/title',
                'desc_filter'  => 'rank_math/frontend/description',
            ),
            'seopress' => array(
                'detect' => function () { return defined( 'SEOPRESS_VERSION' ) || function_exists( 'seopress_titles_the_title' ); },
                'name'         => 'SEOPress',
                'title'        => '_seopress_titles_title',
                'desc'         => '_seopress_titles_desc',
                'title_filter' => 'seopress_titles_the_title',
                'desc_filter'  => 'seopress_titles_desc',
            ),
            'the-seo-framework' => array(
                'detect' => function () { return function_exists( 'the_seo_framework' ); },
                'name'         => 'The SEO Framework',
                'title'        => '_genesis_title',
                'desc'         => '_genesis_description',
                'title_filter' => 'the_seo_framework_title_from_custom_field',
                'desc_filter'  => 'the_seo_framework_description_from_custom_field',
            ),
        );
    }

    /**
     * SEO plugins that own the fields but keep them where Schemafy cannot
     * safely write: AIOSEO v4 uses its own database table, Slim SEO packs
     * everything into one array. Their output is overridden with a filter
     * instead, so they have filter names but no meta keys.
     *
     * @return array[]
     */
    private static function others() {
        return array(
            'aioseo' => array(
                'detect'       => function () { return function_exists( 'aioseo' ) || defined( 'AIOSEO_VERSION' ); },
                'name'         => 'All in One SEO',
                'title_filter' => 'aioseo_title',
                'desc_filter'  => 'aioseo_description',
            ),
            'slim-seo' => array(
                'detect'       => function () { return function_exists( 'slim_seo' ) || defined( 'SLIM_SEO_VER' ) || class_exists( 'SlimSEO\\Plugin' ); },
                'name'         => 'Slim SEO',
                'title_filter' => 'slim_seo_meta_title',
                'desc_filter'  => 'slim_seo_meta_description',
            ),
        );
    }

    /**
     * Every SEO plugin active on this site, owners and non-writable alike.
     *
     * One detection list for the whole plugin. When these checks disagreed —
     * and they used to, per call site — a plugin could be considered active for
     * "do not output our own tag" but inactive for "register the override
     * filter", and the meta description silently vanished.
     *
     * @return array[] slug => definition, with 'owns_storage' set.
     */
    public static function active_plugins() {
        static $active = null;

        if ( null !== $active ) {
            return $active;
        }

        $active = array();

        foreach ( self::owners() as $slug => $def ) {
            if ( call_user_func( $def['detect'] ) ) {
                $def['owns_storage'] = true;
                $active[ $slug ]     = $def;
            }
        }

        foreach ( self::others() as $slug => $def ) {
            if ( call_user_func( $def['detect'] ) ) {
                $def['owns_storage'] = false;
                $active[ $slug ]     = $def;
            }
        }

        return $active;
    }

    /**
     * Whether one specific SEO plugin is active, by slug.
     *
     * @param string $slug One of: yoast, rank-math, seopress, the-seo-framework,
     *                     aioseo, slim-seo.
     * @return bool
     */
    public static function is_active( $slug ) {
        return isset( self::active_plugins()[ $slug ] );
    }

    /**
     * Whether any SEO plugin manages meta tags on this site.
     *
     * @return bool
     */
    public static function any_seo_plugin_active() {
        return array() !== self::active_plugins();
    }

    /**
     * The SEO plugin that owns meta fields on this site, if any.
     *
     * @return array|null Owner definition with 'slug' added, or null.
     */
    public static function owner() {
        static $resolved = false;
        static $owner    = null;

        if ( $resolved ) {
            return $owner;
        }

        foreach ( self::owners() as $slug => $def ) {
            if ( call_user_func( $def['detect'] ) ) {
                $def['slug'] = $slug;
                $owner       = $def;
                break;
            }
        }

        $resolved = true;

        return $owner;
    }

    /**
     * Human-readable name of the owning plugin, or '' when Schemafy owns the
     * fields itself. Used to tell the user, in the UI, where the value lives.
     *
     * @return string
     */
    public static function owner_name() {
        $owner = self::owner();

        return $owner ? $owner['name'] : '';
    }

    /**
     * Read the meta title and description for a post.
     *
     * Reads the owner's rows without checking that the plugin is still active:
     * the values belong to the site, so deactivating the SEO plugin must not
     * make the user's text vanish from Schemafy's screens.
     *
     * @param int $post_id Post ID.
     * @return array{0:string,1:string} [ title, description ]
     */
    public static function get( $post_id ) {
        $post_id = (int) $post_id;
        if ( $post_id <= 0 ) {
            return array( '', '' );
        }

        $owner = self::owner();
        if ( $owner ) {
            $title = (string) get_post_meta( $post_id, $owner['title'], true );
            $desc  = (string) get_post_meta( $post_id, $owner['desc'], true );

            // Fall back to any value written before the SEO plugin was
            // installed, so upgrading a site never looks like data loss.
            if ( '' === $title ) {
                $title = (string) get_post_meta( $post_id, self::OWN_TITLE_KEY, true );
            }
            if ( '' === $desc ) {
                $desc = (string) get_post_meta( $post_id, self::OWN_DESC_KEY, true );
            }

            return array( $title, $desc );
        }

        return array(
            (string) get_post_meta( $post_id, self::OWN_TITLE_KEY, true ),
            (string) get_post_meta( $post_id, self::OWN_DESC_KEY, true ),
        );
    }

    /**
     * Write the meta title and description for a post.
     *
     * When an SEO plugin owns the fields the value goes into its rows and
     * Schemafy's own rows are removed, so a stale shadow copy can never win a
     * later read or get output alongside the real one.
     *
     * @param int    $post_id     Post ID.
     * @param string $title       Meta title ('' clears it).
     * @param string $description Meta description ('' clears it).
     * @return void
     */
    public static function set( $post_id, $title, $description ) {
        $post_id = (int) $post_id;
        if ( $post_id <= 0 ) {
            return;
        }

        $owner = self::owner();

        if ( ! $owner ) {
            update_post_meta( $post_id, self::OWN_TITLE_KEY, $title );
            update_post_meta( $post_id, self::OWN_DESC_KEY, $description );
            return;
        }

        update_post_meta( $post_id, $owner['title'], $title );
        update_post_meta( $post_id, $owner['desc'], $description );

        delete_post_meta( $post_id, self::OWN_TITLE_KEY );
        delete_post_meta( $post_id, self::OWN_DESC_KEY );
    }

    /**
     * Clear the meta title and description for a post, wherever they live.
     *
     * @param int $post_id Post ID.
     * @return void
     */
    public static function delete( $post_id ) {
        $post_id = (int) $post_id;
        if ( $post_id <= 0 ) {
            return;
        }

        $owner = self::owner();
        if ( $owner ) {
            delete_post_meta( $post_id, $owner['title'] );
            delete_post_meta( $post_id, $owner['desc'] );
        }

        delete_post_meta( $post_id, self::OWN_TITLE_KEY );
        delete_post_meta( $post_id, self::OWN_DESC_KEY );
    }

    /**
     * Whether Schemafy must output these tags itself.
     *
     * False when an SEO plugin owns the storage: it renders its own rows, which
     * now hold the user's value, so Schemafy adds nothing and overrides nothing.
     *
     * @return bool
     */
    public static function schemafy_outputs_meta() {
        return null === self::owner();
    }
}
