<?php
/**
 * TamRank Yoast SEO Migrator
 *
 * Maps Yoast SEO meta keys to TamRank equivalents
 * and transforms Yoast template variables.
 */

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

class TamRank_Yoast_Migrator {

    /**
     * Yoast → TamRank meta key mapping.
     *
     * Keys are grouped by migration category.
     */
    private $key_map = array(
        'meta_titles' => array(
            '_yoast_wpseo_title' => '_tam_rank_meta_title',
        ),
        'meta_descriptions' => array(
            '_yoast_wpseo_metadesc' => '_tam_rank_meta_description',
        ),
        'focus_keywords' => array(
            '_yoast_wpseo_focuskw' => '_tam_rank_focus_keyword',
        ),
        'canonical' => array(
            '_yoast_wpseo_canonical' => '_tam_rank_canonical',
        ),
        'social' => array(
            '_yoast_wpseo_opengraph-title'       => '_tam_rank_social_title',
            '_yoast_wpseo_opengraph-description'  => '_tam_rank_social_description',
            '_yoast_wpseo_opengraph-image'        => '_tam_rank_social_image',
        ),
    );

    /**
     * Yoast template variable → TamRank runtime variable transform map.
     *
     * TamRank runtime (see tamrank_replace_dynamic_variables in helpers.php)
     * only resolves these 6 placeholders: %title%, %sitename%, %excerpt%,
     * %date%, %category%, %tag%. Everything else must either map to one of
     * those six or be stripped — otherwise the raw token leaks into page
     * titles on the frontend (indexed by Google).
     */
    private $variable_map = array(
        '%%title%%'               => '%title%',
        '%%sitename%%'            => '%sitename%',
        '%%sep%%'                 => '|',
        '%%primary_category%%'    => '%category%',
        '%%category%%'            => '%category%',
        '%%tag%%'                 => '%tag%',
        '%%excerpt%%'             => '%excerpt%',
        '%%date%%'                => '%date%',
        '%%modified%%'            => '%date%',       // no separate modified var — fall back to date
        '%%archive_title%%'       => '%title%',      // closest fallback
        '%%term_title%%'          => '%title%',
        '%%term_description%%'    => '%excerpt%',
        // Unsupported at runtime — strip to prevent raw token leakage
        '%%name%%'                => '',
        '%%userid%%'              => '',
        '%%searchphrase%%'        => '',
        '%%page%%'                => '',
        '%%pagetotal%%'           => '',
        '%%pagenumber%%'          => '',
        '%%pt_single%%'           => '',
        '%%pt_plural%%'           => '',
        '%%parent_title%%'        => '',
        '%%currentyear%%'         => '',
        '%%currentmonth%%'        => '',
        '%%currentday%%'          => '',
        '%%focuskw%%'             => '',
        '%%cf_([a-zA-Z0-9_]+)%%'  => '',
    );

    /**
     * Yoast schema type → TamRank schema type mapping.
     *
     * TamRank's detector splits schema into primair (1 per page) and extras
     * (0-N per page). FAQPage is always an extra in TamRank's design, never
     * a primary type — so migration writes it to _tamrank_schema_auto_extras
     * and derives the primary type from post_type.
     *
     * Primary types (class-detector.php):
     *   WebPage, AboutPage, ContactPage, CollectionPage, ItemPage,
     *   BlogPosting, NewsArticle, Product, Event, Recipe, LocalBusiness,
     *   Course, Service, JobPosting, SoftwareApplication
     * Extras:
     *   FAQPage, VideoObject, HowTo, ImageObject
     */
    private $schema_page_map = array(
        'WebPage'            => 'WebPage',
        'ItemPage'           => 'ItemPage',
        'AboutPage'          => 'AboutPage',
        'ContactPage'        => 'ContactPage',
        'CollectionPage'     => 'CollectionPage',
        'ProfilePage'        => 'WebPage',
        'CheckoutPage'       => 'WebPage',
        'MedicalWebPage'     => 'WebPage',
        'RealEstateListing'  => 'WebPage',
        'SearchResultsPage'  => 'WebPage',
    );

    /**
     * Yoast page types that map to TamRank extras (never primary).
     * Primary is derived from post_type in migrate_schema().
     */
    private $schema_page_extras = array(
        'FAQPage' => 'FAQPage',
        'QAPage'  => 'FAQPage',
    );

    private $schema_article_map = array(
        'Article'                    => 'Article',
        'BlogPosting'                => 'BlogPosting',
        'NewsArticle'                => 'NewsArticle',
        'ScholarlyArticle'           => 'BlogPosting',
        'TechArticle'                => 'BlogPosting',
        'Report'                     => 'BlogPosting',
        'SatiricalArticle'           => 'BlogPosting',
        'SocialMediaPosting'         => 'BlogPosting',
        'AdvertiserContentArticle'   => 'BlogPosting',
    );

    /**
     * Get source meta keys for a given category.
     *
     * @param string $category
     * @return array Yoast meta keys
     */
    public function get_source_keys( $category ) {
        if ( $category === 'robots' ) {
            return array( '_yoast_wpseo_meta-robots-noindex', '_yoast_wpseo_meta-robots-nofollow' );
        }

        if ( $category === 'schema' ) {
            return array( '_yoast_wpseo_schema_page_type', '_yoast_wpseo_schema_article_type' );
        }

        if ( ! isset( $this->key_map[ $category ] ) ) {
            return array();
        }

        return array_keys( $this->key_map[ $category ] );
    }

    /**
     * Get all source meta keys across all categories.
     *
     * @param array $categories
     * @return array
     */
    public function get_all_source_keys( $categories ) {
        $keys = array();
        foreach ( $categories as $cat ) {
            $keys = array_merge( $keys, $this->get_source_keys( $cat ) );
        }
        return array_unique( $keys );
    }

    /**
     * Migrate meta data for a single post.
     *
     * @param int   $post_id
     * @param array $categories  Categories to migrate
     * @return array {
     *     @type bool   $success
     *     @type array  $migrated  List of migrated keys
     *     @type string $error     Error message (if any)
     * }
     */
    public function migrate_post( $post_id, $categories ) {
        $migrated = array();
        $backup   = array();

        foreach ( $categories as $category ) {
            if ( $category === 'robots' ) {
                $result = $this->migrate_robots( $post_id );
                if ( $result ) {
                    $migrated = array_merge( $migrated, $result['migrated'] );
                    $backup   = array_merge( $backup, $result['backup'] );
                }
                continue;
            }

            if ( $category === 'schema' ) {
                $result = $this->migrate_schema( $post_id );
                if ( $result ) {
                    $migrated = array_merge( $migrated, $result['migrated'] );
                    $backup   = array_merge( $backup, $result['backup'] );
                }
                continue;
            }

            if ( ! isset( $this->key_map[ $category ] ) ) {
                continue;
            }

            foreach ( $this->key_map[ $category ] as $source_key => $target_key ) {
                $source_value = get_post_meta( $post_id, $source_key, true );
                if ( empty( $source_value ) ) {
                    continue;
                }

                // Don't overwrite existing TamRank data
                $existing = get_post_meta( $post_id, $target_key, true );
                if ( ! empty( $existing ) ) {
                    continue;
                }

                // Transform variables in title/description
                if ( in_array( $category, array( 'meta_titles', 'meta_descriptions' ), true ) ) {
                    $source_value = $this->transform_variables( $source_value );
                }

                // Backup original
                $backup[ $target_key ] = $existing;

                update_post_meta( $post_id, $target_key, $source_value );
                $migrated[] = $target_key;
            }
        }

        // Save backup
        if ( ! empty( $migrated ) ) {
            // Merge into any backup from a prior run so earlier-recorded keys are
            // not lost on a second migration (preserves rollback fidelity).
            $existing_backup = get_post_meta( $post_id, '_tamrank_migration_backup', true );
            if ( is_array( $existing_backup ) ) {
                $backup = array_merge( $backup, $existing_backup );
            }
            update_post_meta( $post_id, '_tamrank_migration_backup', $backup );
        }

        return array(
            'success'  => true,
            'migrated' => $migrated,
        );
    }

    /**
     * Migrate Yoast robots meta (noindex/nofollow).
     *
     * Yoast uses: '1' = noindex, '2' = default (use global), empty = default.
     */
    private function migrate_robots( $post_id ) {
        $migrated = array();
        $backup   = array();

        // Noindex
        $noindex = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true );
        if ( $noindex === '1' ) {
            $existing = get_post_meta( $post_id, '_tam_rank_noindex', true );
            if ( empty( $existing ) ) {
                $backup['_tam_rank_noindex'] = $existing;
                update_post_meta( $post_id, '_tam_rank_noindex', 1 );
                $migrated[] = '_tam_rank_noindex';
            }
        }

        // Nofollow
        $nofollow = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-nofollow', true );
        if ( $nofollow === '1' ) {
            $existing = get_post_meta( $post_id, '_tam_rank_nofollow', true );
            if ( empty( $existing ) ) {
                $backup['_tam_rank_nofollow'] = $existing;
                update_post_meta( $post_id, '_tam_rank_nofollow', 1 );
                $migrated[] = '_tam_rank_nofollow';
            }
        }

        if ( empty( $migrated ) ) {
            return null;
        }

        return array(
            'migrated' => $migrated,
            'backup'   => $backup,
        );
    }

    /**
     * Adopt Yoast's per-post-type schema default as TamRank's.
     *
     * Yoast keeps two site-wide keys per post type in wpseo_titles:
     * schema-article-type-<pt> (Article, BlogPosting, NewsArticle, None) and
     * schema-page-type-<pt> (WebPage, ItemPage, AboutPage, ...). The article one
     * wins when it is set to anything but None, exactly as it does in Yoast's own
     * output.
     *
     * Never overwrites: a post type TamRank already has a default for is left
     * alone, so a second migration run and a value the user picked himself both
     * survive. TamRank_Option_Backup covers the option, so a rollback removes it.
     *
     * @since 1.3.8.2
     * @param string $post_type Post type slug.
     * @return string The adopted type, or '' when there was nothing to adopt.
     */
    private function maybe_adopt_global_schema_default( $post_type ) {
        $post_type = (string) $post_type;
        if ( '' === $post_type ) {
            return '';
        }

        $option   = function_exists( 'tamrank_schema_default_types_option' ) ? tamrank_schema_default_types_option() : 'tamrank_schema_default_types';
        $defaults = get_option( $option, array() );
        if ( ! is_array( $defaults ) ) {
            $defaults = array();
        }

        if ( ! empty( $defaults[ $post_type ] ) ) {
            return '';
        }

        $titles = get_option( 'wpseo_titles', array() );
        if ( ! is_array( $titles ) ) {
            return '';
        }

        $article = isset( $titles[ 'schema-article-type-' . $post_type ] ) ? (string) $titles[ 'schema-article-type-' . $post_type ] : '';
        $page    = isset( $titles[ 'schema-page-type-' . $post_type ] ) ? (string) $titles[ 'schema-page-type-' . $post_type ] : '';

        $type = '';

        if ( '' !== $article && 'None' !== $article ) {
            $type = isset( $this->schema_article_map[ $article ] ) ? $this->schema_article_map[ $article ] : 'BlogPosting';
        } elseif ( '' !== $page && ! isset( $this->schema_page_extras[ $page ] ) ) {
            $type = isset( $this->schema_page_map[ $page ] ) ? $this->schema_page_map[ $page ] : 'WebPage';
        }

        if ( '' === $type ) {
            return '';
        }

        $defaults[ $post_type ] = $type;
        update_option( $option, $defaults );

        return $type;
    }

    /**
     * Migrate Yoast schema page/article type → TamRank schema meta.
     *
     * Yoast stores:
     *   - _yoast_wpseo_schema_page_type    (WebPage, ItemPage, AboutPage, FAQPage, …)
     *   - _yoast_wpseo_schema_article_type (Article, BlogPosting, NewsArticle, …)
     *
     * Primary type resolution (one per post):
     *   - Posts → article_type (all Yoast article types are primary in TamRank)
     *   - Pages → page_type via schema_page_map
     *   - FAQPage / QAPage are NEVER primary in TamRank — they become an extra
     *     and the primary is derived from post_type.
     *
     * Target meta:
     *   - tamrank_schema_type          : mapped primary type
     *   - tamrank_schema_enabled       : '1'
     *   - tamrank_schema_bron          : 'automatisch'
     *   - _tamrank_schema_auto_extras  : array of extras (only when applicable)
     */
    private function migrate_schema( $post_id ) {
        $migrated = array();
        $backup   = array();

        $post = get_post( $post_id );
        if ( ! $post ) {
            return null;
        }

        // Yoast's site-wide answer for this whole post type, before the per-post
        // one. On the sites this matters for it is the only answer there is:
        // schema-article-type-page = Article turns every page into an Article
        // without writing a single row of post meta, so a migration that only
        // reads post meta silently drops it and every page comes out a WebPage.
        // Written once per post type, never over a value TamRank already holds.
        $this->maybe_adopt_global_schema_default( $post->post_type );

        $article_type = get_post_meta( $post_id, '_yoast_wpseo_schema_article_type', true );
        $page_type    = get_post_meta( $post_id, '_yoast_wpseo_schema_page_type', true );

        if ( empty( $article_type ) && empty( $page_type ) ) {
            return null;
        }

        $tamrank_type = '';
        $extras       = array();

        // Posts: article_type is primary.
        if ( $post->post_type === 'post' && ! empty( $article_type ) ) {
            $tamrank_type = isset( $this->schema_article_map[ $article_type ] )
                ? $this->schema_article_map[ $article_type ]
                : 'BlogPosting';
        }

        // Page_type: either primary (page_map) or extra (page_extras).
        if ( ! empty( $page_type ) ) {
            if ( isset( $this->schema_page_extras[ $page_type ] ) ) {
                $extras[] = $this->schema_page_extras[ $page_type ];
                if ( empty( $tamrank_type ) ) {
                    $tamrank_type = $this->default_primary_for_post( $post );
                }
            } elseif ( empty( $tamrank_type ) ) {
                $tamrank_type = isset( $this->schema_page_map[ $page_type ] )
                    ? $this->schema_page_map[ $page_type ]
                    : 'WebPage';
            }
        }

        // Non-post with only article_type set.
        if ( empty( $tamrank_type ) && ! empty( $article_type ) ) {
            $tamrank_type = isset( $this->schema_article_map[ $article_type ] )
                ? $this->schema_article_map[ $article_type ]
                : 'BlogPosting';
        }

        if ( empty( $tamrank_type ) ) {
            return null;
        }

        // Don't overwrite existing TamRank schema config.
        $existing_type    = get_post_meta( $post_id, 'tamrank_schema_type', true );
        $existing_enabled = get_post_meta( $post_id, 'tamrank_schema_enabled', true );

        if ( ! empty( $existing_type ) || $existing_enabled === '1' ) {
            return null;
        }

        $backup['tamrank_schema_type']         = $existing_type;
        $backup['tamrank_schema_enabled']      = $existing_enabled;
        $backup['tamrank_schema_bron']         = get_post_meta( $post_id, 'tamrank_schema_bron', true );
        $backup['_tamrank_schema_auto_extras'] = get_post_meta( $post_id, '_tamrank_schema_auto_extras', true );

        update_post_meta( $post_id, 'tamrank_schema_type', $tamrank_type );
        update_post_meta( $post_id, 'tamrank_schema_enabled', '1' );
        update_post_meta( $post_id, 'tamrank_schema_bron', 'automatisch' );

        $migrated[] = 'tamrank_schema_type';
        $migrated[] = 'tamrank_schema_enabled';
        $migrated[] = 'tamrank_schema_bron';

        if ( ! empty( $extras ) ) {
            update_post_meta( $post_id, '_tamrank_schema_auto_extras', $extras );
            $migrated[] = '_tamrank_schema_auto_extras';
        }

        return array(
            'migrated' => $migrated,
            'backup'   => $backup,
        );
    }

    /**
     * Derive TamRank primary schema type from WordPress post_type.
     */
    private function default_primary_for_post( $post ) {
        if ( $post->post_type === 'post' ) {
            return 'BlogPosting';
        }
        if ( $post->post_type === 'product' ) {
            return 'Product';
        }
        return 'WebPage';
    }

    /**
     * Transform Yoast template variables to TamRank format.
     *
     * @param string $value
     * @return string
     */
    public function transform_variables( $value ) {
        foreach ( $this->variable_map as $yoast_var => $tamrank_var ) {
            // Check if it's a regex pattern (contains parentheses)
            if ( strpos( $yoast_var, '(' ) !== false ) {
                $value = preg_replace( '/' . $yoast_var . '/', $tamrank_var, $value );
            } else {
                $value = str_replace( $yoast_var, $tamrank_var, $value );
            }
        }

        // Clean up double spaces and trim
        $value = preg_replace( '/\s{2,}/', ' ', $value );
        return trim( $value );
    }

    /* ------------------------------------------------------------------
     * Redirects (Yoast Premium)
     * ----------------------------------------------------------------*/

    /**
     * Collect importable redirects from Yoast Premium.
     *
     * Yoast Premium stores redirects in the 'wpseo-premium-redirects-base'
     * option: key = origin URL, value = array( 'url' => target,
     * 'type' => int, 'format' => 'plain'|'regex' ). Yoast Free has no
     * redirects at all. Parsed defensively: anything that does not match
     * the expected shape is counted as skipped, never imported.
     *
     * Yoast offers the same non-redirection statuses TamRank does, and its
     * own form hides the target field for them — so a 410 or a 451 arrives
     * here with an empty 'url'. Demanding a target for those would drop
     * exactly the rules a Yoast Premium user cannot rebuild from memory.
     * The emptiness guard therefore runs per status, not up front.
     *
     * @return array {
     *     @type array $import  List of array( 'source' => string, 'target' => string, 'type' => string ).
     *     @type int   $skipped Number of entries not importable (regex, unsupported status, malformed, target missing where one is required).
     * }
     */
    public function get_redirects_to_import() {
        $import  = array();
        $skipped = 0;

        $raw = get_option( 'wpseo-premium-redirects-base', array() );
        if ( ! is_array( $raw ) ) {
            return array( 'import' => array(), 'skipped' => 0 );
        }

        // Same list create_redirect() validates against, filter included, so a
        // status an add-on allows migrates instead of being silently dropped.
        $allowed_types = apply_filters( 'tamrank_redirect_types', array( '301', '302', '307', '410', '451' ) );

        foreach ( $raw as $origin => $entry ) {
            // PHP coerces all-numeric string array keys to int, so cast before
            // the emptiness guard — otherwise a redirect whose origin path is a
            // bare number (e.g. '2019') would be silently dropped.
            $origin = (string) $origin;
            if ( ! is_array( $entry ) || $origin === '' ) {
                $skipped++;
                continue;
            }

            $format = isset( $entry['format'] ) ? $entry['format'] : 'plain';
            $type   = isset( $entry['type'] ) ? (string) (int) $entry['type'] : '301';
            $target = isset( $entry['url'] ) ? (string) $entry['url'] : '';

            if ( $format !== 'plain' || ! in_array( $type, $allowed_types, true ) ) {
                $skipped++;
                continue;
            }

            if ( self::status_needs_target( $type ) ) {
                if ( $target === '' ) {
                    $skipped++;
                    continue;
                }
            } else {
                // The server never reads the target of a 410 or a 451. Anything
                // Yoast left behind in that field would only show up in the
                // redirect table as an arrow to a page nobody is sent to.
                $target = '';
            }

            $import[] = array(
                'source' => $origin,
                'target' => $target,
                'type'   => $type,
            );
        }

        return array( 'import' => $import, 'skipped' => $skipped );
    }

    /**
     * Does a rule with this status code need a target URL?
     *
     * Asks TamRank_Redirect_Manager so the import cannot drift apart from the
     * form, the AJAX handler and the CSV import. The inline fallback mirrors
     * that method exactly and only exists so a missing class is never a fatal
     * — same defensive seam the scanner uses around count_creatable_redirects().
     *
     * @param string|int $status Status code.
     * @return bool True when a target URL is mandatory.
     */
    private static function status_needs_target( $status ) {
        if ( class_exists( 'TamRank_Redirect_Manager' ) ) {
            return TamRank_Redirect_Manager::status_needs_target( $status );
        }

        $code = (int) $status;

        return ( $code >= 300 && $code <= 399 );
    }
}
