<?php

declare(strict_types=1);

namespace ProRank\SEO\Core\Content;

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

/**
 * Repairs missing H1 by upgrading the first heading widget (Elementor) or
 * heading block (Gutenberg) on a post from its current level (h2/h3/...) to h1.
 *
 * Source-level fix. Falls back to no-op when the page already has an H1 or has
 * no candidate heading to promote.
 */
class H1Repairer {

    public function repair_post(int $post_id, bool $dry_run = true, bool $skip_rendered_check = false): array {
        $auditor = new H1Auditor();
        $before = $auditor->diagnose_post($post_id);

        if ($before['has_h1']) {
            if (($before['source'] ?? '') === 'elementor' && !$skip_rendered_check) {
                $rendered = $auditor->verify_rendered($post_id);
                if (!empty($rendered['ok']) && !$rendered['has_rendered_h1']) {
                    $sync = $this->prepend_title_h1_to_post_content($post_id, $before, $dry_run);
                    if ($sync['mutated']) {
                        $sync['reason'] = 'synced_post_content_h1';
                        $sync['rendered_check'] = $rendered;
                        return $sync;
                    }
                }
            }

            return [
                'post_id' => $post_id,
                'mutated' => false,
                'before' => $before,
                'after' => $before,
                'reason' => 'already_has_h1',
            ];
        }
        if (!$before['fixable']) {
            return [
                'post_id' => $post_id,
                'mutated' => false,
                'before' => $before,
                'after' => $before,
                'reason' => $before['reason'],
            ];
        }

        // Rendered-HTML safety check — stored content may lack H1 but a theme
        // template might still inject one. The fetch is cache-safe (no
        // cache-busting header). If the live page already serves an H1, skip
        // to avoid a double-H1. If the check is inconclusive (request failed
        // or non-success status) we must NOT guess — refuse the commit unless
        // the operator explicitly opted out with skip_rendered_check.
        if (!$skip_rendered_check) {
            $rendered = $auditor->verify_rendered($post_id);
            if (empty($rendered['ok'])) {
                return [
                    'post_id' => $post_id,
                    'mutated' => false,
                    'before' => $before,
                    'after' => $before,
                    'reason' => 'rendered_check_inconclusive',
                    'rendered_check' => $rendered,
                ];
            }
            if ($rendered['has_rendered_h1']) {
                return [
                    'post_id' => $post_id,
                    'mutated' => false,
                    'before' => $before,
                    'after' => array_merge($before, ['has_h1' => true]),
                    'reason' => 'theme_renders_h1',
                    'rendered_check' => $rendered,
                ];
            }
        }

        if ($before['source'] === 'elementor') {
            return $this->repair_elementor_post($post_id, $before, $dry_run);
        }

        if (($before['first_heading']['strategy'] ?? '') === 'prepend_title') {
            return $this->prepend_title_h1_to_post_content($post_id, $before, $dry_run);
        }

        return $this->repair_gutenberg_post($post_id, $before, $dry_run);
    }

    private function repair_elementor_post(int $post_id, array $before, bool $dry_run): array {
        $raw = get_post_meta($post_id, '_elementor_data', true);
        if (!is_string($raw) || $raw === '') {
            return $this->no_op_result($post_id, $before, 'elementor_data_empty');
        }

        $tree = json_decode($raw, true);
        if (!is_array($tree)) {
            return $this->no_op_result($post_id, $before, 'elementor_data_invalid_json');
        }

        $strategy = (string) ($before['first_heading']['strategy'] ?? 'upgrade_widget');
        $target_widget_id = (string) ($before['first_heading']['widget_id'] ?? '');
        $title = trim(wp_strip_all_tags((string) get_the_title($post_id)));

        if ($target_widget_id === '' && $strategy === 'insert_title_widget') {
            $widget_id = $this->generate_elementor_id();
            $inserted = $this->insert_title_widget_in_tree($tree, $widget_id, $title);
            if (!$inserted) {
                return $this->no_op_result($post_id, $before, 'title_widget_insert_failed');
            }
            $demote_widget_id = (string) ($before['first_heading']['demote_widget_id'] ?? '');
            if ($demote_widget_id !== '') {
                $this->set_widget_header_size_in_tree($tree, $demote_widget_id, 'h2');
            }

            $new_raw = wp_json_encode($tree, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if (!is_string($new_raw)) {
                return $this->no_op_result($post_id, $before, 'json_encode_failed');
            }

            if (!$dry_run) {
                $this->backup_before_repair($post_id);
                update_post_meta($post_id, '_elementor_data', wp_slash($new_raw));
                delete_post_meta($post_id, '_elementor_css');
                $this->prepend_title_h1_to_post_content($post_id, $before, false);
                $this->refresh_runtime_caches($post_id, true);
            }

            $auditor = new H1Auditor();
            $after = $dry_run ? array_merge($before, ['has_h1' => true]) : $auditor->diagnose_post($post_id);

            return [
                'post_id' => $post_id,
                'mutated' => true,
                'source' => 'elementor',
                'widget_id' => $widget_id,
                'from_level' => 'none',
                'to_level' => 'h1',
                'before' => $before,
                'after' => $after,
                'reason' => 'inserted_title_h1',
            ];
        }

        if ($target_widget_id === '') {
            return $this->no_op_result($post_id, $before, 'no_target_widget');
        }

        $original_size = null;
        $title_updated = false;
        $updated = $this->upgrade_widget_in_tree($tree, $target_widget_id, $original_size, $title, $title_updated);
        if (!$updated) {
            return $this->no_op_result($post_id, $before, 'widget_not_found');
        }

        // Re-encode preserving slash escapes (Elementor stores escaped forward slashes).
        $new_raw = wp_json_encode($tree, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if (!is_string($new_raw)) {
            return $this->no_op_result($post_id, $before, 'json_encode_failed');
        }

        if (!$dry_run) {
            $this->backup_before_repair($post_id);
            update_post_meta($post_id, '_elementor_data', wp_slash($new_raw));
            // Bust Elementor CSS cache so the change reflects on the front end.
            delete_post_meta($post_id, '_elementor_css');
            $this->prepend_title_h1_to_post_content($post_id, $before, false);
            $this->refresh_runtime_caches($post_id, true);
        }

        $auditor = new H1Auditor();
        $after = $dry_run ? array_merge($before, ['has_h1' => true]) : $auditor->diagnose_post($post_id);

        return [
            'post_id' => $post_id,
            'mutated' => true,
            'source' => 'elementor',
            'widget_id' => $target_widget_id,
            'from_level' => $original_size,
            'to_level' => 'h1',
            'title_updated' => $title_updated,
            'before' => $before,
            'after' => $after,
            'reason' => 'fixed',
        ];
    }

    private function upgrade_widget_in_tree(array &$tree, string $target_widget_id, ?string &$original_size, string $post_title = '', bool &$title_updated = false): bool {
        foreach ($tree as &$node) {
            if (!is_array($node)) {
                continue;
            }
            if (
                ($node['elType'] ?? '') === 'widget'
                && in_array((string) ($node['widgetType'] ?? ''), ['heading', 'theme-post-title', 'post-title', 'page-title', 'archive-title'], true)
                && (string) ($node['id'] ?? '') === $target_widget_id
            ) {
                if (!isset($node['settings']) || !is_array($node['settings'])) {
                    $node['settings'] = [];
                }
                $original_size = isset($node['settings']['header_size'])
                    ? (string) $node['settings']['header_size']
                    : 'h2';
                $node['settings']['header_size'] = 'h1';
                if (
                    $post_title !== ''
                    && empty($node['settings']['__dynamic__']['title'])
                    && $this->is_placeholder_title((string) ($node['settings']['title'] ?? ''))
                ) {
                    $node['settings']['title'] = $post_title;
                    $title_updated = true;
                }
                return true;
            }

            if (!empty($node['elements']) && is_array($node['elements'])) {
                if ($this->upgrade_widget_in_tree($node['elements'], $target_widget_id, $original_size, $post_title, $title_updated)) {
                    return true;
                }
            }
        }
        unset($node);

        return false;
    }

    private function set_widget_header_size_in_tree(array &$tree, string $target_widget_id, string $level): bool {
        foreach ($tree as &$node) {
            if (!is_array($node)) {
                continue;
            }

            if (
                ($node['elType'] ?? '') === 'widget'
                && (string) ($node['id'] ?? '') === $target_widget_id
            ) {
                if (!isset($node['settings']) || !is_array($node['settings'])) {
                    $node['settings'] = [];
                }
                $node['settings']['header_size'] = $level;
                return true;
            }

            if (!empty($node['elements']) && is_array($node['elements'])) {
                if ($this->set_widget_header_size_in_tree($node['elements'], $target_widget_id, $level)) {
                    return true;
                }
            }
        }
        unset($node);

        return false;
    }

    private function insert_title_widget_in_tree(array &$tree, string $widget_id, string $title): bool {
        if ($title === '') {
            return false;
        }

        $widget = [
            'id' => $widget_id,
            'elType' => 'widget',
            'settings' => [
                'title' => $title,
                'header_size' => 'h1',
            ],
            'elements' => [],
            'widgetType' => 'heading',
        ];

        foreach ($tree as &$node) {
            if (!is_array($node)) {
                continue;
            }

            $el_type = (string) ($node['elType'] ?? '');
            if (in_array($el_type, ['container', 'column'], true)) {
                if (!isset($node['elements']) || !is_array($node['elements'])) {
                    $node['elements'] = [];
                }
                array_unshift($node['elements'], $widget);
                return true;
            }

            if ($el_type === 'section' && !empty($node['elements']) && is_array($node['elements'])) {
                foreach ($node['elements'] as &$child) {
                    if (is_array($child) && (string) ($child['elType'] ?? '') === 'column') {
                        if (!isset($child['elements']) || !is_array($child['elements'])) {
                            $child['elements'] = [];
                        }
                        array_unshift($child['elements'], $widget);
                        return true;
                    }
                }
                unset($child);
            }
        }
        unset($node);

        array_unshift($tree, $widget);
        return true;
    }

    private function generate_elementor_id(): string {
        try {
            return substr(bin2hex(random_bytes(4)), 0, 7);
        } catch (\Throwable $e) {
            return substr(md5((string) microtime(true)), 0, 7);
        }
    }

    private function is_placeholder_title(string $title): bool {
        $normalized = strtolower(trim(wp_strip_all_tags($title)));
        return $normalized === '' || in_array($normalized, [
            'add your heading text here',
            'heading',
            'title',
        ], true);
    }

    private function prepend_title_h1_to_post_content(int $post_id, array $before, bool $dry_run): array {
        $post = get_post($post_id);
        if (!$post) {
            return $this->no_op_result($post_id, $before, 'post_not_found');
        }

        $title = trim(wp_strip_all_tags((string) get_the_title($post_id)));
        if ($title === '') {
            return $this->no_op_result($post_id, $before, 'empty_post_title');
        }

        $content = (string) $post->post_content;
        if (stripos($content, '<h1') !== false) {
            return $this->no_op_result($post_id, $before, 'content_already_has_h1');
        }

        $h1 = '<h1>' . esc_html($title) . '</h1>';
        $new_content = $h1 . "\n\n" . ltrim($content);

        if (!$dry_run) {
            $this->backup_before_repair($post_id);
            wp_update_post([
                'ID' => $post_id,
                'post_content' => $new_content,
            ]);
            $this->refresh_runtime_caches($post_id, false);
        }

        $auditor = new H1Auditor();
        $after = $dry_run ? array_merge($before, ['has_h1' => true]) : $auditor->diagnose_post($post_id);

        return [
            'post_id' => $post_id,
            'mutated' => true,
            'source' => $before['source'] ?? 'classic_html',
            'from_level' => 'none',
            'to_level' => 'h1',
            'before' => $before,
            'after' => $after,
            'reason' => 'prepended_title_h1',
        ];
    }

    private function repair_gutenberg_post(int $post_id, array $before, bool $dry_run): array {
        $post = get_post($post_id);
        if (!$post) {
            return $this->no_op_result($post_id, $before, 'post_not_found');
        }

        $content = (string) $post->post_content;
        $original_level = (int) ($before['first_heading']['level'] ?? 2);
        $target_index = (int) ($before['first_heading']['index'] ?? -1);
        if ($target_index < 0) {
            return $this->no_op_result($post_id, $before, 'no_target_block');
        }

        if (!function_exists('parse_blocks') || !function_exists('serialize_blocks')) {
            return $this->no_op_result($post_id, $before, 'block_api_unavailable');
        }

        $blocks = parse_blocks($content);
        $mutated = $this->upgrade_first_heading_block($blocks);
        if (!$mutated) {
            return $this->no_op_result($post_id, $before, 'heading_block_not_found');
        }

        $new_content = serialize_blocks($blocks);
        if ($new_content === $content) {
            return $this->no_op_result($post_id, $before, 'serialized_unchanged');
        }

        if (!$dry_run) {
            $this->backup_before_repair($post_id);
            wp_update_post([
                'ID' => $post_id,
                'post_content' => $new_content,
            ]);
            $this->refresh_runtime_caches($post_id, false);
        }

        $auditor = new H1Auditor();
        $after = $dry_run ? array_merge($before, ['has_h1' => true]) : $auditor->diagnose_post($post_id);

        return [
            'post_id' => $post_id,
            'mutated' => true,
            'source' => 'gutenberg',
            'from_level' => 'h' . $original_level,
            'to_level' => 'h1',
            'before' => $before,
            'after' => $after,
            'reason' => 'fixed',
        ];
    }

    private function upgrade_first_heading_block(array &$blocks): bool {
        foreach ($blocks as &$block) {
            if (!is_array($block)) {
                continue;
            }
            if (($block['blockName'] ?? '') === 'core/heading') {
                if (!isset($block['attrs']) || !is_array($block['attrs'])) {
                    $block['attrs'] = [];
                }
                $block['attrs']['level'] = 1;

                // Rewrite the inner HTML <hN> → <h1>.
                if (isset($block['innerHTML']) && is_string($block['innerHTML'])) {
                    $block['innerHTML'] = preg_replace(
                        '/<h[1-6](\b[^>]*)>(.*?)<\/h[1-6]>/is',
                        '<h1$1>$2</h1>',
                        $block['innerHTML']
                    );
                }
                if (isset($block['innerContent']) && is_array($block['innerContent'])) {
                    foreach ($block['innerContent'] as $i => $chunk) {
                        if (is_string($chunk)) {
                            $block['innerContent'][$i] = preg_replace(
                                '/<h[1-6](\b[^>]*)>(.*?)<\/h[1-6]>/is',
                                '<h1$1>$2</h1>',
                                $chunk
                            );
                        }
                    }
                }
                return true;
            }

            if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
                if ($this->upgrade_first_heading_block($block['innerBlocks'])) {
                    return true;
                }
            }
        }
        unset($block);

        return false;
    }

    /**
     * Targeted, per-post cache refresh ONLY. A single H1 repair must never
     * trigger a full-site page-cache purge — in bulk that would repeatedly
     * cold-start the whole site (the "slow page" problem we are fixing). No
     * clear_all_cache fallback and no global Elementor CSS regeneration; the
     * repaired post's own `_elementor_css` meta is deleted by the caller.
     */
    private function refresh_runtime_caches(int $post_id, bool $elementor = false): void {
        clean_post_cache($post_id);
        wp_cache_delete($post_id, 'posts');
        wp_cache_delete($post_id, 'post_meta');

        if (class_exists('\\ProRank\\SEO\\Plugin')) {
            try {
                $plugin = \ProRank\SEO\Plugin::get_instance();
                if ($plugin && method_exists($plugin, 'modules')) {
                    $url = get_permalink($post_id) ?: '';
                    foreach (['modern_cache', 'cache'] as $slug) {
                        $module = $plugin->modules()->get_module($slug);
                        if (!$module) {
                            continue;
                        }
                        if (method_exists($module, 'smart_cache_invalidation')) {
                            $module->smart_cache_invalidation($post_id);
                        } elseif ($url !== '' && method_exists($module, 'purge_url')) {
                            $module->purge_url($url);
                        } elseif ($url !== '' && method_exists($module, 'invalidate_url')) {
                            $module->invalidate_url($url);
                        }
                        // Intentionally NO clear_all_cache() fallback.
                    }
                }
            } catch (\Throwable $e) {
                // Content repair is the primary operation; cache refresh is best-effort.
            }
        }
    }

    /** Meta key holding the most-recent restorable pre-repair snapshot. */
    public const BACKUP_META_KEY = '_prorank_h1_backup';

    /**
     * Write a restorable snapshot of the post BEFORE any source mutation.
     *
     * For Elementor repairs, update_post_meta('_elementor_data', ...) does NOT
     * create a WordPress revision, so an explicit backup record is required
     * (reviewer hard rule). Content-path repairs also get a snapshot so a
     * restore works even when post revisions are disabled. Only the latest
     * snapshot is kept; restoring is a manual/explicit operation.
     */
    /** @var array<int,bool> Post IDs already snapshotted this request. */
    private $backed_up = [];

    private function backup_before_repair(int $post_id): void {
        // Idempotent per request: a single repair_post() can chain an
        // Elementor write + a post_content sync; only the FIRST (pre-mutation)
        // state must be captured, never overwritten by an intermediate one.
        if (!empty($this->backed_up[$post_id])) {
            return;
        }
        $this->backed_up[$post_id] = true;

        $post = get_post($post_id);
        if (!$post) {
            return;
        }
        $snapshot = [
            'backed_up_at'   => current_time('mysql'),
            'plugin_version' => defined('PRORANK_PREMIUM_VERSION') ? PRORANK_PREMIUM_VERSION : (defined('PRORANK_VERSION') ? PRORANK_VERSION : ''),
            'post_content'   => (string) $post->post_content,
            'elementor_data' => (string) get_post_meta($post_id, '_elementor_data', true),
        ];
        update_post_meta($post_id, self::BACKUP_META_KEY, $snapshot);
    }

    /**
     * Restore the most-recent pre-repair snapshot for a post. Makes the
     * backup meta genuinely "restorable" (reviewer requirement). Returns a
     * structured result; never throws.
     */
    public function restore_post(int $post_id, bool $dry_run = true): array {
        $snapshot = get_post_meta($post_id, self::BACKUP_META_KEY, true);
        if (!is_array($snapshot) || !isset($snapshot['backed_up_at'])) {
            return ['post_id' => $post_id, 'restored' => false, 'reason' => 'no_backup'];
        }

        if (!$dry_run) {
            if (array_key_exists('elementor_data', $snapshot)) {
                $ed = (string) $snapshot['elementor_data'];
                if ($ed === '') {
                    delete_post_meta($post_id, '_elementor_data');
                } else {
                    update_post_meta($post_id, '_elementor_data', wp_slash($ed));
                }
                delete_post_meta($post_id, '_elementor_css');
            }
            wp_update_post([
                'ID' => $post_id,
                'post_content' => (string) ($snapshot['post_content'] ?? ''),
            ]);
            $this->refresh_runtime_caches($post_id, array_key_exists('elementor_data', $snapshot));
            delete_post_meta($post_id, self::BACKUP_META_KEY);
        }

        return [
            'post_id'      => $post_id,
            'restored'     => !$dry_run,
            'backed_up_at' => (string) $snapshot['backed_up_at'],
            'reason'       => $dry_run ? 'dry_run' : 'restored',
        ];
    }

    private function no_op_result(int $post_id, array $before, string $reason): array {
        return [
            'post_id' => $post_id,
            'mutated' => false,
            'before' => $before,
            'after' => $before,
            'reason' => $reason,
        ];
    }

    /**
     * Repair every fixable post in one pass.
     *
     * @param bool          $dry_run          When true, no DB writes happen.
     * @param bool          $include_low_conf When false (default) only high-confidence
     *                                        sources (e.g. Elementor) are touched. Low-
     *                                        confidence sources (Gutenberg posts where
     *                                        the theme may render an H1) are skipped to
     *                                        avoid double-H1.
     * @param callable|null $progress         Optional per-item callback($item, $result).
     */
    public function repair_all(bool $dry_run = true, bool $include_low_conf = false, ?callable $progress = null): array {
        $auditor = new H1Auditor();
        $scan = $auditor->scan();

        $totals = [
            'examined' => $scan['missing_count'],
            'mutated' => 0,
            'skipped' => 0,
            'skipped_low_confidence' => 0,
            'skipped_theme_h1' => 0,
            'by_source' => [],
        ];

        foreach ($scan['missing'] as $item) {
            if (!$item['fixable']) {
                $totals['skipped']++;
                continue;
            }
            if (!$include_low_conf && ($item['confidence'] ?? 'low') !== 'high') {
                $totals['skipped_low_confidence']++;
                continue;
            }

            $result = $this->repair_post((int) $item['id'], $dry_run);
            if ($result['mutated']) {
                $totals['mutated']++;
                $src = $result['source'] ?? 'unknown';
                $totals['by_source'][$src] = ($totals['by_source'][$src] ?? 0) + 1;
            } elseif (($result['reason'] ?? '') === 'theme_renders_h1') {
                $totals['skipped_theme_h1']++;
            } else {
                $totals['skipped']++;
            }
            if ($progress !== null) {
                $progress($item, $result);
            }
        }

        return $totals;
    }
}
