<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\SEO;

use Help4BuilderSuite\Support\AdminRequest;
use WP_Post;

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

final class SeoMigration
{
    private const PREVIEW_OPTION = 'h4bs_seo_migration_preview';
    private const RESULT_OPTION = 'h4bs_seo_migration_result';
    private const MAX_CONTENT_ITEMS = 500;
    private const MAX_SAMPLE_ITEMS = 30;
    private ?bool $aioseoTableAvailable = null;

    public function boot(): void
    {
        add_action('admin_post_h4bs_seo_migration_preview', [$this, 'handlePreview']);
        add_action('admin_post_h4bs_seo_migration_import', [$this, 'handleImport']);
    }

    /**
     * @return array<string, array{label: string, post_types: array<int, string>}>
     */
    public function providerCatalog(): array
    {
        $providers = [
            'yoast' => [
                'label' => __('Yoast SEO', 'help4-builder-suite'),
                'post_types' => ['wpseo_locations'],
            ],
            'aioseo' => [
                'label' => __('All in One SEO', 'help4-builder-suite'),
                'post_types' => ['aioseo-location', 'aioseo_locations'],
            ],
            'rank_math' => [
                'label' => __('Rank Math', 'help4-builder-suite'),
                'post_types' => [],
            ],
            'seopress' => [
                'label' => __('SEOPress', 'help4-builder-suite'),
                'post_types' => [],
            ],
            'the_seo_framework' => [
                'label' => __('The SEO Framework', 'help4-builder-suite'),
                'post_types' => [],
            ],
        ];

        /**
         * Filters the SEO migration provider catalog.
         *
         * @param array<string, array{label: string, post_types: array<int, string>}> $providers Providers.
         */
        $filtered = apply_filters('h4bs_seo_migration_providers', $providers);
        return is_array($filtered) ? $filtered : $providers;
    }

    public function handlePreview(): void
    {
        $post = $this->assertAdminRequest('h4bs_seo_migration_preview', 'h4bs_seo_migration_nonce');
        $provider = $this->submittedProvider($post);
        $report = $this->preview($provider);
        update_option(self::PREVIEW_OPTION, $report, false);
        $this->redirect(['h4bs_migration_previewed' => 1]);
    }

    public function handleImport(): void
    {
        $post = $this->assertAdminRequest('h4bs_seo_migration_import', 'h4bs_seo_migration_nonce');
        $provider = $this->submittedProvider($post);
        $overwrite = isset($post['h4bs_seo_migration_overwrite'])
            && rest_sanitize_boolean($post['h4bs_seo_migration_overwrite']);
        $includeLocations = ! isset($post['h4bs_seo_migration_locations'])
            || rest_sanitize_boolean($post['h4bs_seo_migration_locations']);
        $report = $this->import($provider, $overwrite, $includeLocations);
        update_option(self::RESULT_OPTION, $report, false);
        $this->redirect([
            'h4bs_migration_imported' => absint($report['content_updated'] ?? 0),
            'h4bs_locations_imported' => absint($report['locations_imported'] ?? 0),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    public function preview(string $provider = 'all'): array
    {
        $provider = $this->validProvider($provider);
        $posts = $this->contentPosts();
        $report = [
            'mode' => 'preview',
            'provider' => $provider,
            'generated_at' => gmdate('c'),
            'scanned' => count($posts),
            'detected' => 0,
            'candidate_fields' => 0,
            'conflicts' => 0,
            'field_counts' => [],
            'provider_counts' => [],
            'locations_detected' => count($this->sourceLocationPosts($provider)),
            'sample' => [],
        ];

        foreach ($posts as $post) {
            if (! $post instanceof WP_Post) {
                continue;
            }
            $source = $this->sourceDataForPost($post->ID, $provider);
            if ($source === []) {
                continue;
            }
            $report['detected']++;
            $fields = array_keys($source['fields']);
            $conflicts = [];
            foreach ($fields as $targetKey) {
                $report['candidate_fields']++;
                $report['field_counts'][$targetKey] = absint($report['field_counts'][$targetKey] ?? 0) + 1;
                if ($this->targetHasValue($post->ID, $targetKey)) {
                    $report['conflicts']++;
                    $conflicts[] = $targetKey;
                }
            }
            foreach ($source['providers'] as $sourceProvider) {
                $report['provider_counts'][$sourceProvider] = absint($report['provider_counts'][$sourceProvider] ?? 0) + 1;
            }
            if (count($report['sample']) < self::MAX_SAMPLE_ITEMS) {
                $report['sample'][] = [
                    'post_id' => $post->ID,
                    'title' => get_the_title($post),
                    'post_type' => $post->post_type,
                    'providers' => $source['providers'],
                    'fields' => $fields,
                    'conflicts' => $conflicts,
                ];
            }
        }

        ksort($report['field_counts']);
        ksort($report['provider_counts']);
        return $report;
    }

    /**
     * @return array<string, mixed>
     */
    public function import(string $provider = 'all', bool $overwrite = false, bool $includeLocations = true): array
    {
        $provider = $this->validProvider($provider);
        $report = [
            'mode' => 'import',
            'provider' => $provider,
            'overwrite' => $overwrite,
            'generated_at' => gmdate('c'),
            'scanned' => 0,
            'content_updated' => 0,
            'fields_imported' => 0,
            'fields_preserved' => 0,
            'locations_imported' => 0,
            'locations_preserved' => 0,
            'errors' => [],
        ];

        foreach ($this->contentPosts() as $post) {
            if (! $post instanceof WP_Post) {
                continue;
            }
            $report['scanned']++;
            $source = $this->sourceDataForPost($post->ID, $provider);
            if ($source === []) {
                continue;
            }
            $updated = false;
            foreach ($source['fields'] as $targetKey => $value) {
                if (! $overwrite && $this->targetHasValue($post->ID, $targetKey)) {
                    $report['fields_preserved']++;
                    continue;
                }
                update_post_meta($post->ID, $targetKey, $value);
                $report['fields_imported']++;
                $updated = true;
            }
            if ($updated) {
                $report['content_updated']++;
            }
        }

        if ($includeLocations) {
            $locationResult = $this->importLocations($provider, $overwrite);
            $report['locations_imported'] = $locationResult['imported'];
            $report['locations_preserved'] = $locationResult['preserved'];
            $report['errors'] = $locationResult['errors'];
        }

        /**
         * Filters the completed SEO migration report.
         *
         * @param array<string, mixed> $report Migration report.
         * @param string               $provider Provider key or all.
         * @param bool                 $overwrite Whether existing Help4 values could be replaced.
         */
        $filtered = apply_filters('h4bs_seo_migration_result', $report, $provider, $overwrite);
        $report = is_array($filtered) ? $filtered : $report;
        do_action('h4bs_seo_migration_complete', $report, $provider, $overwrite);
        return $report;
    }

    /**
     * @return array{fields: array<string, mixed>, providers: array<int, string>}|array{}
     */
    public function sourceDataForPost(int $postId, string $provider = 'all'): array
    {
        $provider = $this->validProvider($provider);
        $fields = [];
        $providers = [];
        $keys = $provider === 'all' ? array_keys($this->providerCatalog()) : [$provider];
        foreach ($keys as $sourceProvider) {
            $candidate = $this->providerData($postId, $sourceProvider);
            if ($candidate === []) {
                continue;
            }
            $providers[] = $sourceProvider;
            foreach ($candidate as $targetKey => $value) {
                if (! array_key_exists($targetKey, $fields) && $this->hasValue($value)) {
                    $fields[$targetKey] = $this->sanitizeTargetValue($postId, $targetKey, $value);
                }
            }
        }
        $fields = array_filter($fields, fn ($value): bool => $this->hasValue($value));

        /**
         * Filters normalized source metadata before preview or import.
         *
         * @param array<string, mixed> $fields Normalized Help4 metadata fields.
         * @param int                  $postId Post ID.
         * @param string               $provider Selected provider.
         * @param array<int, string>   $providers Providers detected on the post.
         */
        $filtered = apply_filters('h4bs_seo_migration_source_data', $fields, $postId, $provider, $providers);
        $fields = is_array($filtered) ? $filtered : $fields;
        if ($fields === []) {
            return [];
        }
        return ['fields' => $fields, 'providers' => array_values(array_unique($providers))];
    }

    public function renderPanel(): void
    {
        $preview = get_option(self::PREVIEW_OPTION, []);
        $preview = is_array($preview) ? $preview : [];
        $result = get_option(self::RESULT_OPTION, []);
        $result = is_array($result) ? $result : [];
        $previewed = AdminRequest::queryAbsInt('h4bs_migration_previewed');
        $imported = AdminRequest::queryAbsInt('h4bs_migration_imported');
        $locationsImported = AdminRequest::queryAbsInt('h4bs_locations_imported');
        ?>
        <hr style="margin:2rem 0;">
        <h2><?php esc_html_e('SEO Migration', 'help4-builder-suite'); ?></h2>
        <p><?php esc_html_e('Preview metadata from another SEO plugin, then fill empty Help4 fields without removing the source data. Existing Help4 values stay authoritative unless overwrite is explicitly selected.', 'help4-builder-suite'); ?></p>
        <?php if ($previewed > 0) : ?>
            <div class="notice notice-info inline"><p><?php esc_html_e('Migration preview refreshed. No content was changed.', 'help4-builder-suite'); ?></p></div>
        <?php endif; ?>
        <?php if ($imported > 0 || $locationsImported > 0) : ?>
            <div class="notice notice-success inline"><p><?php echo esc_html(sprintf(
                /* translators: 1: Number of content items updated. 2: Number of locations imported. */
                __('Updated %1$d content item(s) and imported %2$d location(s).', 'help4-builder-suite'),
                $imported,
                $locationsImported
            )); ?></p></div>
        <?php endif; ?>
        <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;align-items:start;">
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="border:1px solid #dcdcde;background:#fff;padding:1rem;border-radius:8px;">
                <?php wp_nonce_field('h4bs_seo_migration_preview', 'h4bs_seo_migration_nonce'); ?>
                <input type="hidden" name="action" value="h4bs_seo_migration_preview">
                <label for="h4bs-seo-migration-preview-provider"><strong><?php esc_html_e('Source', 'help4-builder-suite'); ?></strong></label>
                <?php $this->renderProviderSelect('h4bs-seo-migration-preview-provider'); ?>
                <p><button type="submit" class="button button-primary"><?php esc_html_e('Preview Migration', 'help4-builder-suite'); ?></button></p>
                <p class="description"><?php esc_html_e('Scans at most 500 public content items and writes nothing.', 'help4-builder-suite'); ?></p>
            </form>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="border:1px solid #dcdcde;background:#fff;padding:1rem;border-radius:8px;">
                <?php wp_nonce_field('h4bs_seo_migration_import', 'h4bs_seo_migration_nonce'); ?>
                <input type="hidden" name="action" value="h4bs_seo_migration_import">
                <label for="h4bs-seo-migration-import-provider"><strong><?php esc_html_e('Source', 'help4-builder-suite'); ?></strong></label>
                <?php $this->renderProviderSelect('h4bs-seo-migration-import-provider'); ?>
                <p><label><input type="checkbox" name="h4bs_seo_migration_locations" value="1" checked> <?php esc_html_e('Import supported business locations', 'help4-builder-suite'); ?></label></p>
                <p><label><input type="checkbox" name="h4bs_seo_migration_overwrite" value="1"> <?php esc_html_e('Replace existing Help4 SEO values', 'help4-builder-suite'); ?></label></p>
                <p><button type="submit" class="button"><?php esc_html_e('Run Import', 'help4-builder-suite'); ?></button></p>
                <p class="description"><?php esc_html_e('Source metadata is retained. Run a backup through your normal hosting workflow before choosing overwrite.', 'help4-builder-suite'); ?></p>
            </form>
        </div>
        <?php if ($preview !== []) : ?>
            <h3><?php esc_html_e('Latest Preview', 'help4-builder-suite'); ?></h3>
            <p><?php echo esc_html(sprintf(
                /* translators: 1: Detected content count. 2: Candidate field count. 3: Conflict count. 4: Location count. */
                __('Detected %1$d content item(s), %2$d candidate field(s), %3$d existing-value conflict(s), and %4$d supported location(s).', 'help4-builder-suite'),
                absint($preview['detected'] ?? 0),
                absint($preview['candidate_fields'] ?? 0),
                absint($preview['conflicts'] ?? 0),
                absint($preview['locations_detected'] ?? 0)
            )); ?></p>
            <?php $this->renderSampleTable(is_array($preview['sample'] ?? null) ? $preview['sample'] : []); ?>
        <?php endif; ?>
        <?php if ($result !== []) : ?>
            <p class="description"><?php echo esc_html(sprintf(
                /* translators: 1: Imported field count. 2: Preserved field count. */
                __('Last import: %1$d field(s) imported; %2$d existing Help4 field(s) preserved.', 'help4-builder-suite'),
                absint($result['fields_imported'] ?? 0),
                absint($result['fields_preserved'] ?? 0)
            )); ?></p>
        <?php endif;
    }

    private function renderProviderSelect(string $id): void
    {
        ?>
        <select id="<?php echo esc_attr($id); ?>" name="h4bs_seo_migration_provider" style="display:block;width:100%;margin:.5rem 0 1rem;">
            <option value="all"><?php esc_html_e('Auto-detect all supported sources', 'help4-builder-suite'); ?></option>
            <?php foreach ($this->providerCatalog() as $key => $provider) : ?>
                <option value="<?php echo esc_attr($key); ?>"><?php echo esc_html((string) ($provider['label'] ?? $key)); ?></option>
            <?php endforeach; ?>
        </select>
        <?php
    }

    /** @param array<int, array<string, mixed>> $rows */
    private function renderSampleTable(array $rows): void
    {
        if ($rows === []) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No supported source metadata was found in the bounded scan.', 'help4-builder-suite') . '</p></div>';
            return;
        }
        ?>
        <table class="widefat striped" style="margin-bottom:1.5rem;">
            <thead><tr><th><?php esc_html_e('Content', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Sources', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Fields', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Existing Help4 values', 'help4-builder-suite'); ?></th></tr></thead>
            <tbody>
            <?php foreach ($rows as $row) : ?>
                <tr>
                    <td><a href="<?php echo esc_url(get_edit_post_link(absint($row['post_id'] ?? 0), '')); ?>"><?php echo esc_html((string) ($row['title'] ?? '')); ?></a><br><span class="description"><?php echo esc_html((string) ($row['post_type'] ?? '')); ?></span></td>
                    <td><?php echo esc_html(implode(', ', array_map('sanitize_key', (array) ($row['providers'] ?? [])))); ?></td>
                    <td><?php echo esc_html(implode(', ', array_map('sanitize_key', (array) ($row['fields'] ?? [])))); ?></td>
                    <td><?php echo esc_html(implode(', ', array_map('sanitize_key', (array) ($row['conflicts'] ?? [])))); ?></td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
        <?php
    }

    /**
     * @return array<int, WP_Post>
     */
    private function contentPosts(): array
    {
        $postTypes = get_post_types(['public' => true], 'names');
        unset($postTypes['attachment'], $postTypes['h4_location']);
        $posts = get_posts([
            'post_type' => array_values($postTypes),
            'post_status' => ['publish', 'future', 'draft', 'pending', 'private'],
            'posts_per_page' => self::MAX_CONTENT_ITEMS,
            'orderby' => 'ID',
            'order' => 'ASC',
            'no_found_rows' => true,
        ]);
        return array_values(array_filter($posts, static fn ($post): bool => $post instanceof WP_Post));
    }

    /**
     * @return array<string, mixed>
     */
    private function providerData(int $postId, string $provider): array
    {
        $data = match ($provider) {
            'yoast' => $this->mappedMeta($postId, [
                '_h4_seo_title' => ['_yoast_wpseo_title'],
                '_h4_seo_description' => ['_yoast_wpseo_metadesc'],
                '_h4_seo_canonical' => ['_yoast_wpseo_canonical'],
                '_h4_seo_focus_keyword' => ['_yoast_wpseo_focuskw'],
                '_h4_seo_social_title' => ['_yoast_wpseo_opengraph-title', '_yoast_wpseo_twitter-title'],
                '_h4_seo_social_description' => ['_yoast_wpseo_opengraph-description', '_yoast_wpseo_twitter-description'],
                '_h4_seo_social_image_url' => ['_yoast_wpseo_opengraph-image', '_yoast_wpseo_twitter-image'],
                '_h4_seo_og_image_id' => ['_yoast_wpseo_opengraph-image-id'],
                '_h4_seo_schema_type' => ['_yoast_wpseo_schema_page_type'],
            ]) + $this->robotFields(
                get_post_meta($postId, '_yoast_wpseo_meta-robots-noindex', true),
                get_post_meta($postId, '_yoast_wpseo_meta-robots-nofollow', true)
            ),
            'rank_math' => $this->mappedMeta($postId, [
                '_h4_seo_title' => ['rank_math_title'],
                '_h4_seo_description' => ['rank_math_description'],
                '_h4_seo_canonical' => ['rank_math_canonical_url'],
                '_h4_seo_focus_keyword' => ['rank_math_focus_keyword'],
                '_h4_seo_social_title' => ['rank_math_facebook_title', 'rank_math_twitter_title'],
                '_h4_seo_social_description' => ['rank_math_facebook_description', 'rank_math_twitter_description'],
                '_h4_seo_social_image_url' => ['rank_math_facebook_image', 'rank_math_twitter_image'],
                '_h4_seo_og_image_id' => ['rank_math_facebook_image_id'],
                '_h4_seo_schema_type' => ['rank_math_rich_snippet'],
            ]) + $this->rankMathRobots(get_post_meta($postId, 'rank_math_robots', true)),
            'aioseo' => $this->aioseoData($postId),
            'seopress' => $this->mappedMeta($postId, [
                '_h4_seo_title' => ['_seopress_titles_title'],
                '_h4_seo_description' => ['_seopress_titles_desc'],
                '_h4_seo_canonical' => ['_seopress_robots_canonical'],
                '_h4_seo_focus_keyword' => ['_seopress_analysis_target_kw'],
                '_h4_seo_social_title' => ['_seopress_social_fb_title', '_seopress_social_twitter_title'],
                '_h4_seo_social_description' => ['_seopress_social_fb_desc', '_seopress_social_twitter_desc'],
                '_h4_seo_social_image_url' => ['_seopress_social_fb_img', '_seopress_social_twitter_img'],
                '_h4_seo_og_image_id' => ['_seopress_social_fb_img_attachment_id'],
                '_h4_seo_schema_type' => ['_seopress_pro_rich_snippets_type'],
            ]) + $this->robotFields(
                get_post_meta($postId, '_seopress_robots_index', true),
                get_post_meta($postId, '_seopress_robots_follow', true)
            ),
            'the_seo_framework' => $this->mappedMeta($postId, [
                '_h4_seo_title' => ['_genesis_title'],
                '_h4_seo_description' => ['_genesis_description'],
                '_h4_seo_canonical' => ['_genesis_canonical_uri'],
                '_h4_seo_social_image_url' => ['_social_image_url'],
                '_h4_seo_og_image_id' => ['_social_image_id'],
            ]) + $this->robotFields(
                get_post_meta($postId, '_genesis_noindex', true),
                get_post_meta($postId, '_genesis_nofollow', true)
            ),
            default => [],
        };
        return array_filter($data, fn ($value): bool => $this->hasValue($value));
    }

    /**
     * @param array<string, array<int, string>> $mapping
     * @return array<string, mixed>
     */
    private function mappedMeta(int $postId, array $mapping): array
    {
        $data = [];
        foreach ($mapping as $target => $sourceKeys) {
            foreach ($sourceKeys as $sourceKey) {
                $value = get_post_meta($postId, $sourceKey, true);
                if ($this->hasValue($value)) {
                    $data[$target] = $value;
                    break;
                }
            }
        }
        return $data;
    }

    /** @return array<string, mixed> */
    private function aioseoData(int $postId): array
    {
        $legacy = $this->mappedMeta($postId, [
            '_h4_seo_title' => ['_aioseop_title'],
            '_h4_seo_description' => ['_aioseop_description'],
            '_h4_seo_canonical' => ['_aioseop_custom_link'],
            '_h4_seo_focus_keyword' => ['_aioseop_keywords'],
        ]) + $this->robotFields(
            get_post_meta($postId, '_aioseop_noindex', true),
            get_post_meta($postId, '_aioseop_nofollow', true)
        );

        global $wpdb;
        $table = $wpdb->prefix . 'aioseo_posts';
        if (! $this->aioseoTableExists($table)) {
            return $legacy;
        }
        $wpdb->suppress_errors(true);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read-only compatibility bridge for AIOSEO's public post metadata table.
        $row = $wpdb->get_row(
            $wpdb->prepare(
                'SELECT title, description, canonical_url, robots_noindex, robots_nofollow, og_title, og_description, og_image_url, twitter_title, twitter_description, twitter_image_url, keyphrases, schema_type FROM %i WHERE post_id = %d LIMIT 1',
                $table,
                $postId
            ),
            ARRAY_A
        );
        $wpdb->suppress_errors(false);
        if (! is_array($row)) {
            return $legacy;
        }
        $modern = [
            '_h4_seo_title' => $row['title'] ?? '',
            '_h4_seo_description' => $row['description'] ?? '',
            '_h4_seo_canonical' => $row['canonical_url'] ?? '',
            '_h4_seo_focus_keyword' => $this->aioseoFocusKeyword($row['keyphrases'] ?? ''),
            '_h4_seo_social_title' => $row['og_title'] ?: ($row['twitter_title'] ?? ''),
            '_h4_seo_social_description' => $row['og_description'] ?: ($row['twitter_description'] ?? ''),
            '_h4_seo_social_image_url' => $row['og_image_url'] ?: ($row['twitter_image_url'] ?? ''),
            '_h4_seo_schema_type' => $row['schema_type'] ?? '',
        ] + $this->robotFields($row['robots_noindex'] ?? '', $row['robots_nofollow'] ?? '');
        return array_filter($modern, fn ($value): bool => $this->hasValue($value)) + $legacy;
    }

    private function aioseoTableExists(string $table): bool
    {
        if ($this->aioseoTableAvailable !== null) {
            return $this->aioseoTableAvailable;
        }
        global $wpdb;
        $wpdb->suppress_errors(true);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One bounded compatibility probe avoids hundreds of failed AIOSEO table reads.
        $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table)));
        $wpdb->suppress_errors(false);
        $this->aioseoTableAvailable = is_string($found) && hash_equals($table, $found);
        return $this->aioseoTableAvailable;
    }

    private function aioseoFocusKeyword(mixed $value): string
    {
        if (! is_string($value) || trim($value) === '') {
            return '';
        }
        $decoded = json_decode($value, true);
        if (! is_array($decoded)) {
            return sanitize_text_field($value);
        }
        foreach (['focus', 'additional'] as $group) {
            foreach ((array) ($decoded[$group] ?? []) as $keyphrase) {
                if (is_array($keyphrase) && trim((string) ($keyphrase['keyphrase'] ?? '')) !== '') {
                    return sanitize_text_field((string) $keyphrase['keyphrase']);
                }
            }
        }
        return '';
    }

    /** @return array<string, string> */
    private function robotFields(mixed $noindex, mixed $nofollow): array
    {
        $fields = [];
        if ($this->truthyRobotValue($noindex, ['noindex'])) {
            $fields['_h4_seo_noindex'] = '1';
        }
        if ($this->truthyRobotValue($nofollow, ['nofollow'])) {
            $fields['_h4_seo_nofollow'] = '1';
        }
        return $fields;
    }

    /** @return array<string, string> */
    private function rankMathRobots(mixed $robots): array
    {
        $robots = is_array($robots) ? array_map('sanitize_key', $robots) : [sanitize_key((string) $robots)];
        return $this->robotFields(in_array('noindex', $robots, true), in_array('nofollow', $robots, true));
    }

    /** @param array<int, string> $needles */
    private function truthyRobotValue(mixed $value, array $needles): bool
    {
        if (is_bool($value) || is_int($value)) {
            return (bool) $value;
        }
        $normalized = strtolower(trim((string) $value));
        return in_array($normalized, ['1', 'true', 'yes', 'on'], true)
            || count(array_intersect($needles, preg_split('/[^a-z]+/', $normalized) ?: [])) > 0;
    }

    private function sanitizeTargetValue(int $postId, string $targetKey, mixed $value): mixed
    {
        if ($targetKey === '_h4_seo_og_image_id') {
            return absint($value);
        }
        if (in_array($targetKey, ['_h4_seo_noindex', '_h4_seo_nofollow', '_h4_seo_noarchive', '_h4_seo_nosnippet'], true)) {
            return rest_sanitize_boolean($value) ? '1' : '0';
        }
        if (in_array($targetKey, ['_h4_seo_canonical', '_h4_seo_social_image_url'], true)) {
            return esc_url_raw((string) $value, ['http', 'https']);
        }
        if ($targetKey === '_h4_seo_schema_type') {
            return $this->normalizeSchemaType((string) $value);
        }
        $value = is_array($value) ? implode(', ', array_map('strval', $value)) : (string) $value;
        $value = $this->replaceTemplateTokens($postId, $value);
        return sanitize_text_field(wp_strip_all_tags($value));
    }

    private function replaceTemplateTokens(int $postId, string $value): string
    {
        $post = get_post($postId);
        $postTitle = $post instanceof WP_Post ? get_the_title($post) : '';
        $excerpt = $post instanceof WP_Post
            ? trim((string) ($post->post_excerpt ?: wp_trim_words(wp_strip_all_tags($post->post_content), 30)))
            : '';
        return strtr($value, [
            '%%title%%' => $postTitle,
            '%title%' => $postTitle,
            '#post_title' => $postTitle,
            '%%sitename%%' => (string) get_bloginfo('name'),
            '%sitename%' => (string) get_bloginfo('name'),
            '#site_title' => (string) get_bloginfo('name'),
            '%%sep%%' => '-',
            '%sep%' => '-',
            '#separator_sa' => '-',
            '%%excerpt%%' => $excerpt,
            '%excerpt%' => $excerpt,
            '#post_excerpt' => $excerpt,
        ]);
    }

    private function normalizeSchemaType(string $value): string
    {
        $value = sanitize_key($value);
        $map = [
            'article' => 'article',
            'blogposting' => 'article',
            'blog_posting' => 'article',
            'webpage' => 'webpage',
            'web_page' => 'webpage',
            'aboutpage' => 'about_page',
            'about_page' => 'about_page',
            'contactpage' => 'contact_page',
            'contact_page' => 'contact_page',
            'faqpage' => 'faq',
            'faq' => 'faq',
            'product' => 'product',
            'service' => 'service',
            'localbusiness' => 'local_business',
            'local_business' => 'local_business',
            'person' => 'person',
            'course' => 'course',
            'event' => 'event',
            'softwareapplication' => 'software_application',
            'software_application' => 'software_application',
            'realestatelisting' => 'real_estate_listing',
            'real_estate_listing' => 'real_estate_listing',
        ];
        return $map[$value] ?? 'webpage';
    }

    /** @return array<int, WP_Post> */
    private function sourceLocationPosts(string $provider): array
    {
        $catalog = $this->providerCatalog();
        $providers = $provider === 'all' ? array_keys($catalog) : [$provider];
        $types = [];
        foreach ($providers as $sourceProvider) {
            $types = array_merge($types, (array) ($catalog[$sourceProvider]['post_types'] ?? []));
        }
        $types = array_values(array_unique(array_filter(array_map('sanitize_key', $types))));
        if ($types === []) {
            return [];
        }
        $posts = get_posts([
            'post_type' => $types,
            'post_status' => ['publish', 'draft', 'pending', 'private'],
            'posts_per_page' => 300,
            'orderby' => 'ID',
            'order' => 'ASC',
            'no_found_rows' => true,
        ]);
        $posts = array_values(array_filter($posts, static fn ($post): bool => $post instanceof WP_Post));
        /**
         * Filters source business locations selected for migration.
         *
         * @param array<int, WP_Post> $posts Source location posts.
         * @param string              $provider Selected provider or all.
         */
        $filtered = apply_filters('h4bs_seo_migration_location_posts', $posts, $provider);
        return is_array($filtered)
            ? array_slice(array_values(array_filter($filtered, static fn ($post): bool => $post instanceof WP_Post)), 0, 300)
            : $posts;
    }

    /** @return array{imported: int, preserved: int, errors: array<int, string>} */
    private function importLocations(string $provider, bool $overwrite): array
    {
        $result = ['imported' => 0, 'preserved' => 0, 'errors' => []];
        $existingLocations = $this->migratedLocationMap();
        foreach ($this->sourceLocationPosts($provider) as $source) {
            $sourceProvider = str_starts_with($source->post_type, 'wpseo_') ? 'yoast' : 'aioseo';
            $sourceKey = $sourceProvider . ':' . $source->ID;
            $locationId = absint($existingLocations[$sourceKey] ?? 0);
            if ($locationId > 0 && ! $overwrite) {
                $result['preserved']++;
                continue;
            }
            $postData = [
                'ID' => $locationId,
                'post_type' => 'h4_location',
                'post_status' => $source->post_status === 'publish' ? 'publish' : 'draft',
                'post_title' => $source->post_title,
                'post_content' => $source->post_content,
                'post_excerpt' => $source->post_excerpt,
            ];
            $locationId = wp_insert_post($postData, true);
            if (is_wp_error($locationId)) {
                $result['errors'][] = sanitize_text_field($locationId->get_error_message());
                continue;
            }
            $locationId = absint($locationId);
            update_post_meta($locationId, '_h4_seo_migration_source_provider', $sourceProvider);
            update_post_meta($locationId, '_h4_seo_migration_source_id', (string) $source->ID);
            $existingLocations[$sourceKey] = $locationId;
            if ((string) get_post_meta($locationId, '_h4_local_location_key', true) === '') {
                update_post_meta($locationId, '_h4_local_location_key', wp_generate_uuid4());
            }
            update_post_meta($locationId, '_h4_local_data', $this->sourceLocationData($source->ID));
            $thumbnailId = get_post_thumbnail_id($source->ID);
            if ($thumbnailId > 0) {
                set_post_thumbnail($locationId, $thumbnailId);
            }
            $result['imported']++;
        }
        return $result;
    }

    /** @return array<string, int> */
    private function migratedLocationMap(): array
    {
        $locationIds = get_posts([
            'post_type' => 'h4_location',
            'post_status' => 'any',
            'posts_per_page' => 500,
            'fields' => 'ids',
            'orderby' => 'ID',
            'order' => 'ASC',
            'no_found_rows' => true,
            'update_post_meta_cache' => true,
        ]);
        $map = [];
        foreach ($locationIds as $locationId) {
            $locationId = absint($locationId);
            $provider = sanitize_key((string) get_post_meta($locationId, '_h4_seo_migration_source_provider', true));
            $sourceId = absint(get_post_meta($locationId, '_h4_seo_migration_source_id', true));
            if ($locationId > 0 && $provider !== '' && $sourceId > 0) {
                $map[$provider . ':' . $sourceId] = $locationId;
            }
        }
        return $map;
    }

    /** @return array<string, mixed> */
    private function sourceLocationData(int $postId): array
    {
        $value = fn (array $keys): string => sanitize_text_field((string) $this->firstMetaValue($postId, $keys));
        $url = fn (array $keys): string => esc_url_raw((string) $this->firstMetaValue($postId, $keys), ['http', 'https']);
        return [
            'business_type' => '',
            'phone' => $value(['_wpseo_business_phone', '_aioseo_local_business_phone', 'phone']),
            'email' => sanitize_email((string) $this->firstMetaValue($postId, ['_wpseo_business_email', '_aioseo_local_business_email', 'email'])),
            'url' => $url(['_wpseo_business_url', '_aioseo_local_business_url', 'url']),
            'address_line_1' => $value(['_wpseo_business_address', '_aioseo_local_business_address', 'street_address', 'address']),
            'address_line_2' => $value(['_wpseo_business_address_2', 'address_2']),
            'city' => $value(['_wpseo_business_city', '_aioseo_local_business_city', 'city']),
            'region' => $value(['_wpseo_business_state', '_aioseo_local_business_state', 'state', 'region']),
            'postal_code' => $value(['_wpseo_business_zipcode', '_aioseo_local_business_zip', 'zip', 'postal_code']),
            'country_code' => strtoupper(substr($value(['_wpseo_business_country', '_aioseo_local_business_country', 'country']), 0, 2)),
            'latitude' => $value(['_wpseo_coordinates_lat', '_aioseo_local_business_latitude', 'latitude']),
            'longitude' => $value(['_wpseo_coordinates_long', '_aioseo_local_business_longitude', 'longitude']),
            'price_range' => $value(['_wpseo_business_price_range', 'price_range']),
            'currencies_accepted' => '',
            'payment_methods' => '',
            'place_id' => $value(['_wpseo_business_place_id', 'place_id']),
            'map_embed_url' => $url(['_wpseo_business_map_url', 'map_embed_url']),
            'service_areas' => [],
            'opening_hours' => [],
            'special_hours' => [],
        ];
    }

    private function firstMetaValue(int $postId, array $keys): mixed
    {
        foreach ($keys as $key) {
            $value = get_post_meta($postId, (string) $key, true);
            if ($this->hasValue($value)) {
                return $value;
            }
        }
        return '';
    }

    private function hasValue(mixed $value): bool
    {
        if (is_array($value)) {
            return $value !== [];
        }
        return trim((string) $value) !== '';
    }

    private function targetHasValue(int $postId, string $targetKey): bool
    {
        if (! metadata_exists('post', $postId, $targetKey)) {
            return false;
        }
        $value = get_post_meta($postId, $targetKey, true);
        if (in_array($targetKey, ['_h4_seo_noindex', '_h4_seo_nofollow', '_h4_seo_noarchive', '_h4_seo_nosnippet'], true)) {
            return true;
        }
        if ($targetKey === '_h4_seo_og_image_id') {
            return absint($value) > 0;
        }
        return $this->hasValue($value);
    }

    private function validProvider(string $provider): string
    {
        $provider = sanitize_key($provider);
        return $provider === 'all' || isset($this->providerCatalog()[$provider]) ? $provider : 'all';
    }

    /** @param array<string, mixed> $post */
    private function submittedProvider(array $post): string
    {
        $provider = isset($post['h4bs_seo_migration_provider'])
            ? sanitize_key((string) $post['h4bs_seo_migration_provider'])
            : 'all';
        return $this->validProvider($provider);
    }

    /** @return array<string, mixed> */
    private function assertAdminRequest(string $action, string $nonceField): array
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('Unauthorized request.', 'help4-builder-suite'));
        }
        check_admin_referer($action, $nonceField);
        return is_array($_POST) ? wp_unslash($_POST) : [];
    }

    /** @param array<string, int> $query */
    private function redirect(array $query): void
    {
        wp_safe_redirect(add_query_arg($query, admin_url('admin.php?page=h4bs-seo-control')));
        exit;
    }
}
