<?php

declare(strict_types=1);

namespace ProRank\SEO\Core\Content;

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

/**
 * Removes AI chat surface attributes from post content and Elementor data.
 *
 * Only "high-confidence" attributes (provably unique to AI chat UIs) are
 * stripped. Ambiguous attributes (data-testid, data-cy etc.) are left alone
 * because legitimate tooling uses them.
 *
 * Modes:
 *   - dry_run: count would-be changes without writing
 *   - commit:  write changes to the database
 *
 * Used by the REST endpoint and the WP-CLI command.
 */
class AiAttributeStripper {

    public function strip_from_html(string $html, bool &$mutated = false): string {
        $original = $html;

        foreach (AiContentLeakSignatures::high_confidence_attributes() as $attr) {
            // Match attribute regardless of quoting (double, single, unquoted)
            $pattern = '/\s+' . preg_quote($attr, '/') . '\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i';
            $html = preg_replace($pattern, '', $html) ?? $html;
        }

        $mutated = ($html !== $original);
        return $html;
    }

    public function strip_from_elementor_json(string $json, bool &$mutated = false): string {
        // Elementor stores its tree as JSON; HTML attributes inside string
        // values appear as escaped sequences:  data-start=\"123\"
        // Strip both the escaped and unescaped forms.
        $original = $json;

        foreach (AiContentLeakSignatures::high_confidence_attributes() as $attr) {
            $attr_quoted = preg_quote($attr, '/');

            // Escaped form (most common in _elementor_data): data-start=\"...\"
            $json = preg_replace(
                '/\s+' . $attr_quoted . '\s*=\s*\\\\"[^"\\\\]*\\\\"/i',
                '',
                $json
            ) ?? $json;

            // Single-escaped: data-start=\'...\'
            $json = preg_replace(
                '/\s+' . $attr_quoted . "\s*=\s*\\\\'[^'\\\\]*\\\\'/i",
                '',
                $json
            ) ?? $json;

            // Bare attribute (no quotes): data-start=42
            $json = preg_replace(
                '/\s+' . $attr_quoted . '\s*=\s*[^\s,"\\\\\\]\\}>]+/i',
                '',
                $json
            ) ?? $json;
        }

        $mutated = ($json !== $original);
        return $json;
    }

    /**
     * Strip a single post's content (and excerpt).
     *
     * @return array{ before: int, after: int, removed: int, mutated: bool }
     */
    public function strip_post(int $post_id, bool $dry_run = true): array {
        $post = get_post($post_id);
        if (!$post) {
            return ['before' => 0, 'after' => 0, 'removed' => 0, 'mutated' => false];
        }

        $auditor = new AiContentLeakAuditor();
        $before_breakdown = $auditor->breakdown((string) $post->post_content);
        $before_excerpt = $auditor->breakdown((string) $post->post_excerpt);
        $before_high = $before_breakdown['high_confidence'] + $before_excerpt['high_confidence'];

        $content_mutated = false;
        $excerpt_mutated = false;
        $new_content = $this->strip_from_html((string) $post->post_content, $content_mutated);
        $new_excerpt = $this->strip_from_html((string) $post->post_excerpt, $excerpt_mutated);

        $after_breakdown = $auditor->breakdown($new_content);
        $after_excerpt_breakdown = $auditor->breakdown($new_excerpt);
        $after_high = $after_breakdown['high_confidence'] + $after_excerpt_breakdown['high_confidence'];

        $mutated = $content_mutated || $excerpt_mutated;
        $removed = $before_high - $after_high;

        if (!$dry_run && $mutated) {
            $update = [
                'ID' => $post_id,
                'post_content' => $new_content,
                'post_excerpt' => $new_excerpt,
            ];
            wp_update_post(wp_slash($update));
            clean_post_cache($post_id);
        }

        return [
            'before' => $before_high,
            'after' => $after_high,
            'removed' => $removed,
            'mutated' => $mutated,
        ];
    }

    /**
     * Strip _elementor_data for a single post.
     */
    public function strip_elementor(int $post_id, bool $dry_run = true): array {
        $data = get_post_meta($post_id, '_elementor_data', true);
        if (!is_string($data) || $data === '') {
            return ['before' => 0, 'after' => 0, 'removed' => 0, 'mutated' => false];
        }

        $auditor = new AiContentLeakAuditor();
        $before = $auditor->breakdown_json($data)['high_confidence'];

        $mutated = false;
        $stripped = $this->strip_from_elementor_json($data, $mutated);

        $after = $auditor->breakdown_json($stripped)['high_confidence'];
        $removed = $before - $after;

        if (!$dry_run && $mutated) {
            update_post_meta($post_id, '_elementor_data', wp_slash($stripped));
            // Bust Elementor caches for this post so the change reflects on the front-end.
            delete_post_meta($post_id, '_elementor_css');
            delete_post_meta($post_id, '_elementor_element_cache');
            clean_post_cache($post_id);
        }

        return [
            'before' => $before,
            'after' => $after,
            'removed' => $removed,
            'mutated' => $mutated,
        ];
    }

    /**
     * Strip a single term description (category/tag/custom taxonomy).
     */
    public function strip_term(int $term_id, string $taxonomy, bool $dry_run = true): array {
        $term = get_term($term_id, $taxonomy);
        if (!$term || is_wp_error($term)) {
            return ['before' => 0, 'after' => 0, 'removed' => 0, 'mutated' => false];
        }

        $auditor = new AiContentLeakAuditor();
        $before = $auditor->breakdown((string) $term->description)['high_confidence'];

        $mutated = false;
        $new_description = $this->strip_from_html((string) $term->description, $mutated);

        $after = $auditor->breakdown($new_description)['high_confidence'];
        $removed = $before - $after;

        if (!$dry_run && $mutated) {
            wp_update_term($term_id, $taxonomy, [
                'description' => $new_description,
            ]);
        }

        return [
            'before' => $before,
            'after' => $after,
            'removed' => $removed,
            'mutated' => $mutated,
        ];
    }

    /**
     * Strip all affected posts, Elementor data, and term descriptions in one pass.
     */
    public function strip_all(bool $dry_run = true, ?callable $progress = null): array {
        $auditor = new AiContentLeakAuditor();
        $scan = $auditor->scan();

        $term_items = $scan['terms']['items'] ?? [];
        $touched_post_ids = [];
        $touched_terms = [];

        $totals = [
            'posts_examined' => count($scan['offenders']),
            'posts_mutated' => 0,
            'content_leaks_removed' => 0,
            'elementor_items_examined' => count($scan['elementor']['items']),
            'elementor_items_mutated' => 0,
            'elementor_leaks_removed' => 0,
            'terms_examined' => count($term_items),
            'terms_mutated' => 0,
            'term_leaks_removed' => 0,
        ];

        foreach ($scan['offenders'] as $offender) {
            $result = $this->strip_post((int) $offender['id'], $dry_run);
            if ($result['mutated']) {
                $totals['posts_mutated']++;
                $totals['content_leaks_removed'] += $result['removed'];
                $touched_post_ids[] = (int) $offender['id'];
            }
            if ($progress !== null) {
                $progress('post', $offender, $result);
            }
        }

        foreach ($scan['elementor']['items'] as $item) {
            $result = $this->strip_elementor((int) $item['id'], $dry_run);
            if ($result['mutated']) {
                $totals['elementor_items_mutated']++;
                $totals['elementor_leaks_removed'] += $result['removed'];
                $touched_post_ids[] = (int) $item['id'];
            }
            if ($progress !== null) {
                $progress('elementor', $item, $result);
            }
        }

        foreach ($term_items as $item) {
            $result = $this->strip_term(
                (int) $item['term_id'],
                (string) $item['taxonomy'],
                $dry_run
            );
            if ($result['mutated']) {
                $totals['terms_mutated']++;
                $totals['term_leaks_removed'] += $result['removed'];
                $touched_terms[] = [
                    'term_id' => (int) $item['term_id'],
                    'taxonomy' => (string) $item['taxonomy'],
                ];
            }
            if ($progress !== null) {
                $progress('term', $item, $result);
            }
        }

        if (
            !$dry_run
            && (
                $totals['posts_mutated'] > 0
                || $totals['elementor_items_mutated'] > 0
                || $totals['terms_mutated'] > 0
            )
        ) {
            $this->refresh_runtime_caches($touched_post_ids, $touched_terms);
        }

        return $totals;
    }

    /**
     * Refresh only the caches touched by a cleanup run.
     *
     * Bulk metadata cleanup can touch many posts. A full performance-runtime
     * flush makes every page on the site cache-cold, which is exactly what
     * external crawlers report as temporary "slow page" issues. Keep this
     * targeted: post URLs, related archives/terms, edited term pages, and
     * Elementor runtime files.
     *
     * @param array<int,int> $post_ids
     * @param array<int,array{term_id:int,taxonomy:string}> $terms
     * @return void
     */
    public function refresh_runtime_caches(array $post_ids = [], array $terms = []): void {
        $post_ids = array_values(array_unique(array_filter(array_map('intval', $post_ids), static fn($id) => $id > 0)));
        $terms = array_values(array_filter($terms, static function ($term): bool {
            return is_array($term)
                && !empty($term['term_id'])
                && !empty($term['taxonomy']);
        }));

        if (class_exists('\Elementor\Plugin')) {
            try {
                $elementor = \Elementor\Plugin::$instance ?? null;
                if (is_object($elementor) && isset($elementor->files_manager) && method_exists($elementor->files_manager, 'clear_cache')) {
                    $elementor->files_manager->clear_cache();
                }
            } catch (\Throwable $e) {
                // Cache refresh is best-effort; cleanup has already completed.
            }
        }

        foreach ($post_ids as $post_id) {
            clean_post_cache($post_id);
            wp_cache_delete($post_id, 'posts');
            wp_cache_delete($post_id, 'post_meta');
        }

        foreach ($terms as $term) {
            clean_term_cache([(int) $term['term_id']], (string) $term['taxonomy']);
        }

        $urls = $this->collect_target_urls($post_ids, $terms);
        $this->clear_page_cache_urls($urls);
        $this->schedule_targeted_preload($urls);
    }

    /**
     * @param array<int,int> $post_ids
     * @param array<int,array{term_id:int,taxonomy:string}> $terms
     * @return array<int,string>
     */
    private function collect_target_urls(array $post_ids, array $terms): array {
        $urls = [home_url('/')];

        $posts_page_id = (int) get_option('page_for_posts');
        if ($posts_page_id > 0) {
            $posts_page_url = get_permalink($posts_page_id);
            if (is_string($posts_page_url) && $posts_page_url !== '') {
                $urls[] = $posts_page_url;
            }
        }

        foreach ($post_ids as $post_id) {
            $permalink = get_permalink($post_id);
            if (is_string($permalink) && $permalink !== '') {
                $urls[] = $permalink;
            }

            $post_type = get_post_type($post_id);
            if (is_string($post_type) && $post_type !== 'page') {
                $archive = get_post_type_archive_link($post_type);
                if (is_string($archive) && $archive !== '') {
                    $urls[] = $archive;
                }
            }

            $post = get_post($post_id);
            if ($post && (int) $post->post_author > 0) {
                $author_url = get_author_posts_url((int) $post->post_author);
                if (is_string($author_url) && $author_url !== '') {
                    $urls[] = $author_url;
                }
            }

            foreach (get_object_taxonomies((string) $post_type) as $taxonomy) {
                $post_terms = wp_get_post_terms($post_id, $taxonomy, ['number' => 10]);
                if (is_wp_error($post_terms)) {
                    continue;
                }
                foreach ($post_terms as $term) {
                    $term_link = get_term_link($term);
                    if (!is_wp_error($term_link) && is_string($term_link) && $term_link !== '') {
                        $urls[] = $term_link;
                    }
                }
            }
        }

        foreach ($terms as $term) {
            $term_link = get_term_link((int) $term['term_id'], (string) $term['taxonomy']);
            if (!is_wp_error($term_link) && is_string($term_link) && $term_link !== '') {
                $urls[] = $term_link;
            }
        }

        return array_values(array_unique(array_filter($urls, static fn($url) => is_string($url) && $url !== '')));
    }

    /**
     * @param array<int,string> $urls
     * @return void
     */
    private function clear_page_cache_urls(array $urls): void {
        if (empty($urls) || !class_exists('\ProRank\SEO\Plugin')) {
            return;
        }

        if (class_exists('\ProRank\SEO\Plugin')) {
            try {
                $plugin = \ProRank\SEO\Plugin::get_instance();
                if (is_object($plugin) && method_exists($plugin, 'modules')) {
                    $modules = $plugin->modules();
                    foreach (['modern_cache', 'cache'] as $slug) {
                        $module = $modules->get_module($slug);
                        if (is_object($module) && method_exists($module, 'clear_urls_cache')) {
                            $module->clear_urls_cache($urls);
                        }
                    }
                }
            } catch (\Throwable $e) {
                // Targeted cache refresh is best-effort.
            }
        }
    }

    /**
     * @param array<int,string> $urls
     * @return void
     */
    private function schedule_targeted_preload(array $urls): void {
        if (empty($urls) || !function_exists('as_enqueue_async_action')) {
            return;
        }

        foreach (array_slice($urls, 0, 50) as $url) {
            as_enqueue_async_action('prorank_cache_preload_url', [$url]);
        }
    }

}
