<?php
if (!defined('ABSPATH')) exit;

class WPTA_Audit_Engine {
    private $issues = array();
    private $checked_link_cache = array();

    public function run($args = array()) {
        $args = wp_parse_args($args, array('limit' => 25, 'mode' => 'manual'));
        $run_id = WPTA_Utils::insert_run($args['mode']);
        $crawler = new WPTA_Crawler((int)$args['limit']);
        $pages = $crawler->crawl();
        $this->issues = array();

        $this->audit_site_level($run_id);
        foreach ($pages as $page) {
            $this->audit_page($page);
        }

        foreach ($this->issues as $issue) {
            WPTA_Utils::insert_issue($run_id, $issue);
        }
        $score = WPTA_Utils::score_from_issues($this->issues);
        $totals = $this->summarize($pages, $this->issues);
        WPTA_Utils::finish_run($run_id, $score, $totals);
        return array('run_id' => $run_id, 'score' => $score, 'totals' => $totals, 'issues' => $this->issues);
    }

    private function add_issue($url, $category, $code, $title, $message, $priority = 'medium', $recommendation = '', $data = array()) {
        $this->issues[] = compact('url','category','code','title','message','priority','recommendation','data');
    }

    private function summarize($pages, $issues) {
        $categories = array();
        $priorities = array('critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0);
        foreach ($issues as $i) {
            $cat = $i['category'] ?? 'General';
            $categories[$cat] = isset($categories[$cat]) ? $categories[$cat] + 1 : 1;
            $p = $i['priority'] ?? 'medium';
            if (!isset($priorities[$p])) $priorities[$p] = 0;
            $priorities[$p]++;
        }
        return array(
            'pages_crawled' => count($pages),
            'issues_found' => count($issues),
            'categories' => $categories,
            'priorities' => $priorities,
            'generated_at' => current_time('mysql')
        );
    }

    private function audit_site_level($run_id) {
        $home = home_url('/');
        $this->audit_robots($home);
        $this->audit_sitemap($home);
        $this->audit_ssl_and_domain($home);
        $this->audit_wp_exposure($home);
        $this->audit_email_records_basic($home);
        $this->audit_plugin_environment($home);
    }

    private function audit_robots($home) {
        $url = home_url('/robots.txt');
        $res = WPTA_Utils::wp_remote_get_safe($url, array('timeout' => 10));
        if (is_wp_error($res) || wp_remote_retrieve_response_code($res) >= 400) {
            $this->add_issue($home, 'Crawlability', 'robots_missing', 'Robots.txt missing or inaccessible', 'The robots.txt file could not be fetched.', 'medium', 'Add a valid robots.txt file and include sitemap location.');
            return;
        }
        $body = wp_remote_retrieve_body($res);
        if (stripos($body, 'Sitemap:') === false) {
            $this->add_issue($url, 'Crawlability', 'robots_no_sitemap', 'Robots.txt does not reference sitemap', 'Search engines can discover sitemap more reliably when robots.txt contains a Sitemap directive.', 'low', 'Add Sitemap: ' . esc_html(home_url('/sitemap.xml')) . ' to robots.txt.');
        }
        if (preg_match('/Disallow:\s*\//i', $body) && !preg_match('/Allow:\s*\//i', $body)) {
            $this->add_issue($url, 'Indexing', 'robots_blocks_all', 'Robots.txt may block the whole site', 'A broad Disallow: / rule was found.', 'critical', 'Review robots.txt and avoid blocking public pages unless intentional.');
        }
    }

    private function audit_sitemap($home) {
        $candidates = array(home_url('/wp-sitemap.xml'), home_url('/sitemap.xml'), home_url('/sitemap_index.xml'));
        $found = false;
        foreach ($candidates as $url) {
            $res = WPTA_Utils::wp_remote_get_safe($url, array('timeout' => 10));
            $code = is_wp_error($res) ? 0 : wp_remote_retrieve_response_code($res);
            $body = is_wp_error($res) ? '' : wp_remote_retrieve_body($res);
            if ($code >= 200 && $code < 400 && (stripos($body, '<url') !== false || stripos($body, '<sitemap') !== false)) {
                $found = true;
                break;
            }
        }
        if (!$found) {
            $this->add_issue($home, 'Crawlability', 'sitemap_missing', 'XML sitemap not found', 'No accessible XML sitemap was found at common WordPress sitemap locations.', 'high', 'Enable WordPress sitemap or use an SEO plugin to generate sitemap.xml.');
        }
    }

    private function audit_ssl_and_domain($home) {
        if (stripos($home, 'https://') !== 0) {
            $this->add_issue($home, 'Security', 'https_missing', 'Website is not using HTTPS', 'The site home URL is not HTTPS.', 'critical', 'Install SSL and force HTTP to HTTPS redirects.');
        }
        $host = WPTA_Utils::get_domain_host();
        if ($host) {
            $www = (strpos($host, 'www.') === 0) ? substr($host, 4) : 'www.' . $host;
            $scheme = is_ssl() ? 'https://' : 'http://';
            $res = WPTA_Utils::wp_remote_get_safe($scheme . $www . '/', array('timeout' => 10, 'redirection' => 0));
            if (!is_wp_error($res)) {
                $code = wp_remote_retrieve_response_code($res);
                if ($code === 200) {
                    $this->add_issue($scheme . $www . '/', 'URL Structure', 'www_nonwww_duplicate_risk', 'WWW/non-WWW duplicate risk', 'Both host variations may be accessible without a redirect.', 'medium', 'Choose one canonical host and 301 redirect the other version.');
                }
            }
        }
    }

    private function audit_wp_exposure($home) {
        $checks = array(
            '/wp-admin/' => 'Admin login path is publicly discoverable. This is normal for WordPress, but should be protected.',
            '/xmlrpc.php' => 'XML-RPC endpoint is accessible. Disable it if not required.',
            '/wp-content/uploads/' => 'Uploads directory listing may be exposed.',
            '/.env' => 'Environment file exposed.',
            '/.git/config' => 'Git config exposed.',
            '/wp-config.php.bak' => 'Backup config file exposed.',
            '/backup.zip' => 'Backup ZIP file exposed.',
            '/database.sql' => 'Database dump exposed.'
        );
        foreach ($checks as $path => $message) {
            $url = home_url($path);
            $res = WPTA_Utils::wp_remote_get_safe($url, array('timeout' => 7, 'redirection' => 0));
            if (is_wp_error($res)) continue;
            $code = wp_remote_retrieve_response_code($res);
            $body = wp_remote_retrieve_body($res);
            if (in_array($path, array('/.env','/.git/config','/wp-config.php.bak','/backup.zip','/database.sql'), true) && $code === 200 && strlen($body) > 20) {
                $this->add_issue($url, 'Security', 'sensitive_file_exposure', 'Sensitive file may be exposed', $message, 'critical', 'Remove the file from public web root and block access at server level.');
            }
            if ($path === '/xmlrpc.php' && in_array($code, array(200,405), true)) {
                $this->add_issue($url, 'Security', 'xmlrpc_enabled', 'XML-RPC endpoint enabled', $message, 'medium', 'Disable XML-RPC if your site does not need Jetpack/mobile app/pingback support.');
            }
            if ($path === '/wp-content/uploads/' && $code === 200 && preg_match('/Index of|Parent Directory/i', $body)) {
                $this->add_issue($url, 'Security', 'directory_listing_enabled', 'Directory listing enabled', $message, 'high', 'Disable directory listing using server config or .htaccess Options -Indexes.');
            }
        }
    }

    private function audit_email_records_basic($home) {
        $host = WPTA_Utils::get_domain_host();
        if (!$host || !function_exists('dns_get_record')) return;
        $txt = @dns_get_record($host, DNS_TXT);
        $spf = false;
        $dmarc = false;
        if (is_array($txt)) {
            foreach ($txt as $record) {
                $value = isset($record['txt']) ? $record['txt'] : '';
                if (stripos($value, 'v=spf1') !== false) $spf = true;
            }
        }
        $dmarc_txt = @dns_get_record('_dmarc.' . $host, DNS_TXT);
        if (is_array($dmarc_txt)) {
            foreach ($dmarc_txt as $record) {
                $value = isset($record['txt']) ? $record['txt'] : '';
                if (stripos($value, 'v=DMARC1') !== false) $dmarc = true;
            }
        }
        if (!$spf) {
            $this->add_issue($home, 'Email Deliverability', 'spf_missing', 'SPF record missing', 'No SPF TXT record was detected for the domain.', 'medium', 'Add an SPF record for your sending services.');
        }
        if (!$dmarc) {
            $this->add_issue($home, 'Email Deliverability', 'dmarc_missing', 'DMARC record missing', 'No DMARC TXT record was detected.', 'medium', 'Add a DMARC policy such as p=none first, then monitor and harden.');
        }
    }

    private function audit_plugin_environment($home) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $this->add_issue($home, 'WordPress', 'wp_debug_enabled', 'WP_DEBUG is enabled', 'Debug mode should not be enabled on production websites.', 'high', 'Set WP_DEBUG to false in wp-config.php for production.');
        }
        if (!get_option('blog_public')) {
            $this->add_issue($home, 'Indexing', 'discourage_search_engines', 'Search engine visibility is disabled', 'WordPress setting “Discourage search engines from indexing this site” is active.', 'critical', 'Disable this setting from Settings > Reading when the site is live.');
        }
    }

    private function audit_page($page) {
        $url = $page['url'];
        $status = (int)$page['status'];
        $html = (string)$page['html'];
        $headers = $page['headers'];
        if ($status === 0) {
            $this->add_issue($url, 'Server', 'request_failed', 'Page request failed', $page['error'], 'high', 'Check DNS, firewall, SSL, and server availability.');
            return;
        }
        if ($status >= 500) {
            $this->add_issue($url, 'Server', 'server_error', '5xx server error', 'The URL returned HTTP ' . $status . '.', 'critical', 'Fix application/server error for this URL.');
            return;
        }
        if ($status === 404) {
            $this->add_issue($url, 'Status Code', 'not_found', '404 page found', 'The URL returned HTTP 404.', 'high', 'Restore the page, remove internal links to it, or redirect to a relevant page.');
            return;
        }
        if ($status >= 300 && $status < 400) {
            $this->add_issue($url, 'Redirect', 'redirect_url', 'Redirect detected', 'The URL returned HTTP ' . $status . '.', 'low', 'Use final destination URLs in sitemap and internal links.');
        }
        if (!$html) return;

        $this->audit_head($url, $html);
        $this->audit_headings($url, $html);
        $this->audit_links($url, $html);
        $this->audit_images($url, $html);
        $this->audit_schema($url, $html);
        $this->audit_mobile_accessibility($url, $html);
        $this->audit_performance_headers($url, $headers, $html);
        $this->audit_security_headers($url, $headers, $html);
        $this->audit_content_quality($url, $html);
        $this->audit_forms_cta($url, $html);
        $this->audit_ecommerce_signals($url, $html);
        $this->audit_social_pwa($url, $html);
        $this->audit_js_signals($url, $html);
    }


    private function wpta_is_expected_noindex_url($url) {
        $path = wp_parse_url($url, PHP_URL_PATH);
        $query = wp_parse_url($url, PHP_URL_QUERY);
        $path = is_string($path) ? strtolower($path) : '';
        $query = is_string($query) ? strtolower($query) : '';
        return (strpos($path, '/cart') !== false || strpos($path, '/checkout') !== false || strpos($path, '/my-account') !== false || strpos($query, 'add-to-cart=') !== false || strpos($path, '/wp-admin') !== false);
    }

    private function audit_head($url, $html) {
        $title = WPTA_Utils::extract_title($html);
        $desc = WPTA_Utils::extract_meta($html, 'description');
        $canonical = WPTA_Utils::extract_link_rel($html, 'canonical');
        $robots = WPTA_Utils::extract_meta($html, 'robots');
        if ($title === '') $this->add_issue($url, 'SEO', 'title_missing', 'Meta title missing', 'The page has no <title> tag.', 'high', 'Add a unique, descriptive title around 45–65 characters.');
        if ($title !== '' && WPTA_Utils::text_length($title) < 30) $this->add_issue($url, 'SEO', 'title_too_short', 'Meta title too short', 'The title is shorter than 30 characters.', 'medium', 'Write a more descriptive title with the target topic.');
        if ($title !== '' && WPTA_Utils::text_length($title) > 70) $this->add_issue($url, 'SEO', 'title_too_long', 'Meta title too long', 'The title is longer than 70 characters.', 'medium', 'Keep title around 45–65 characters.');
        if ($desc === '') $this->add_issue($url, 'SEO', 'description_missing', 'Meta description missing', 'The page has no meta description.', 'high', 'Add a unique 120–160 character meta description.');
        if ($desc !== '' && WPTA_Utils::text_length($desc) < 70) $this->add_issue($url, 'SEO', 'description_too_short', 'Meta description too short', 'The description is shorter than 70 characters.', 'low', 'Expand the meta description with benefit and keyword context.');
        if ($desc !== '' && WPTA_Utils::text_length($desc) > 170) $this->add_issue($url, 'SEO', 'description_too_long', 'Meta description too long', 'The description is longer than 170 characters.', 'medium', 'Shorten meta description to avoid truncation.');
        if ($canonical === '') $this->add_issue($url, 'Canonical', 'canonical_missing', 'Canonical tag missing', 'No canonical URL was found.', 'medium', 'Add a self-referencing canonical URL to avoid duplicate URL issues.');
        if (stripos($robots, 'noindex') !== false && !$this->wpta_is_expected_noindex_url($url)) $this->add_issue($url, 'Indexing', 'noindex_found', 'Noindex tag found', 'This page contains a robots noindex directive.', 'critical', 'Remove noindex from pages that should rank in search.');
        if (substr_count(strtolower($html), 'rel="canonical"') + substr_count(strtolower($html), "rel='canonical'") > 1) $this->add_issue($url, 'Canonical', 'multiple_canonical', 'Multiple canonical tags', 'The page appears to contain more than one canonical tag.', 'high', 'Ensure only one canonical tag is printed.');
        if (WPTA_Utils::extract_meta($html, 'og:title') === '') $this->add_issue($url, 'Social Preview', 'og_title_missing', 'Open Graph title missing', 'Facebook/WhatsApp preview may be weak without og:title.', 'low', 'Add Open Graph tags for share previews.');
        if (WPTA_Utils::extract_meta($html, 'og:image') === '') $this->add_issue($url, 'Social Preview', 'og_image_missing', 'Open Graph image missing', 'Social shares may not show a proper image.', 'low', 'Add a 1200×630 Open Graph image.');
        if (stripos($html, '<html') !== false && !preg_match('/<html[^>]+lang=["\'][a-zA-Z-]+["\']/i', $html)) $this->add_issue($url, 'Accessibility', 'html_lang_missing', 'HTML lang attribute missing', 'Screen readers and search engines use the lang attribute.', 'medium', 'Add lang attribute to the <html> element.');
    }

    private function audit_headings($url, $html) {
        $h1 = WPTA_Utils::count_tag($html, 'h1');
        if ($h1 === 0) $this->add_issue($url, 'Headings', 'h1_missing', 'H1 missing', 'The page does not contain an H1 heading.', 'high', 'Add exactly one clear H1 that describes the page.');
        if ($h1 > 1) $this->add_issue($url, 'Headings', 'multiple_h1', 'Multiple H1 headings', 'The page contains ' . $h1 . ' H1 headings.', 'medium', 'Use one primary H1 and demote other major headings to H2/H3.');
        if (preg_match('/<h([1-6])[^>]*>\s*<\/h\1>/i', $html)) $this->add_issue($url, 'Accessibility', 'empty_heading', 'Empty heading found', 'One or more heading tags are empty.', 'medium', 'Remove empty heading tags or add meaningful heading text.');
        preg_match_all('/<h([1-6])[^>]*>/i', $html, $m);
        $prev = 0;
        foreach ($m[1] as $level) {
            $level = (int)$level;
            if ($prev && $level > $prev + 1) {
                $this->add_issue($url, 'Headings', 'heading_level_skip', 'Heading level skip', 'Heading structure skips from H' . $prev . ' to H' . $level . '.', 'low', 'Use headings in a logical hierarchy.');
                break;
            }
            $prev = $level;
        }
    }

    private function audit_links($url, $html) {
        $links = WPTA_Utils::matches_all('/<a\s([^>]*)>/is', $html);
        $internal_count = 0;
        $external_count = 0;
        $empty_anchor = 0;
        foreach ($links as $match) {
            $attrs = $match[1];
            if (!preg_match('/href=["\']([^"\']*)["\']/i', $attrs, $hm)) continue;
            $href_raw = trim($hm[1]);
            if ($href_raw === '' || $href_raw === '#' || stripos($href_raw, 'javascript:') === 0) {
                $empty_anchor++;
                continue;
            }
            $href = WPTA_Utils::absolute_url($href_raw, $url);
            if (preg_match('#^https?://#i', $href)) {
                if (WPTA_Utils::is_internal_url($href)) $internal_count++; else $external_count++;
                if (stripos($href, 'http://') === 0 && stripos(home_url('/'), 'https://') === 0) {
                    $this->add_issue($url, 'Security', 'http_link_on_https', 'HTTP link on HTTPS page', 'The page links to non-HTTPS URL: ' . esc_html($href), 'medium', 'Use HTTPS URLs where available.');
                }
                $this->check_link_status($url, $href);
            }
            if (!preg_match('/>\s*[^<\s]/', $match[0])) {
                // The full anchor content is not captured here; image-only anchors may be valid. Keep as low only via empty href above.
            }
        }
        if ($empty_anchor > 0) $this->add_issue($url, 'Links', 'empty_or_js_links', 'Empty or JavaScript links found', $empty_anchor . ' empty/#/javascript links were found.', 'low', 'Use real crawlable links for important navigation and content.');
        if ($internal_count < 2) $this->add_issue($url, 'Internal Linking', 'few_internal_links', 'Few internal links', 'The page has very few internal links.', 'medium', 'Add contextual links to relevant pages, categories, products, or articles.');
        if ($external_count > 30) $this->add_issue($url, 'External Links', 'many_external_links', 'Many external links', 'The page has ' . $external_count . ' external links.', 'low', 'Review external links for relevance, trust, and nofollow/sponsored where appropriate.');
    }

    private function check_link_status($source_url, $target_url) {
        $target_url = strtok($target_url, '#');
        if (isset($this->checked_link_cache[$target_url])) return;
        $this->checked_link_cache[$target_url] = true;
        if (count($this->checked_link_cache) > 120) return;
        $res = WPTA_Utils::wp_remote_get_safe($target_url, array('timeout' => 8, 'redirection' => 5));
        if (is_wp_error($res)) return;
        $code = wp_remote_retrieve_response_code($res);
        if ($code >= 400) {
            $this->add_issue($source_url, 'Broken Links', 'broken_link', 'Broken link found', 'Link target returned HTTP ' . $code . ': ' . esc_html($target_url), WPTA_Utils::is_internal_url($target_url) ? 'high' : 'medium', 'Update, remove, or redirect the broken link.', array('target' => $target_url, 'status' => $code));
        }
    }

    private function audit_images($url, $html) {
        $imgs = WPTA_Utils::matches_all('/<img\s([^>]*)>/is', $html);
        $missing_alt = 0;
        $no_lazy = 0;
        $http_images = 0;
        foreach ($imgs as $img) {
            $attrs = $img[1];
            if (!preg_match('/alt=["\']([^"\']*)["\']/i', $attrs, $am) || trim($am[1]) === '') $missing_alt++;
            if (stripos($attrs, 'loading=') === false && count($imgs) > 3) $no_lazy++;
            if (preg_match('/src=["\']http:\/\//i', $attrs)) $http_images++;
        }
        if ($missing_alt) $this->add_issue($url, 'Images', 'image_alt_missing', 'Images missing alt text', $missing_alt . ' images are missing alt text.', 'medium', 'Add descriptive alt text, especially for product and content images.');
        if ($no_lazy > 3) $this->add_issue($url, 'Images', 'lazy_loading_missing', 'Image lazy loading may be missing', $no_lazy . ' images do not declare loading="lazy".', 'low', 'Lazy-load below-the-fold images; do not lazy-load the hero/LCP image.');
        if ($http_images) $this->add_issue($url, 'Security', 'mixed_content_image', 'HTTP image found', $http_images . ' images load over HTTP.', 'high', 'Serve all images over HTTPS.');
        if (count($imgs) > 80) $this->add_issue($url, 'Performance', 'too_many_images', 'Too many images on page', 'The page contains ' . count($imgs) . ' images.', 'medium', 'Paginate, lazy-load, compress, and use responsive images.');
    }

    private function audit_schema($url, $html) {
        $jsonld_count = substr_count(strtolower($html), 'application/ld+json');
        if ($jsonld_count === 0) $this->add_issue($url, 'Schema', 'schema_missing', 'Structured data missing', 'No JSON-LD schema was detected.', 'medium', 'Add relevant schema such as Organization, BreadcrumbList, Product, Article, FAQPage, or LocalBusiness.');
        if (stripos($url, '/product') !== false || stripos($html, 'woocommerce') !== false) {
            if (stripos($html, '"@type":"Product"') === false && stripos($html, '"@type": "Product"') === false) {
                $this->add_issue($url, 'Ecommerce', 'product_schema_missing', 'Product schema may be missing', 'This looks like a product page but Product schema was not detected.', 'high', 'Add Product schema with name, image, brand, offers, price, currency, availability, SKU, and description.');
            }
        }
        if (stripos($html, 'BreadcrumbList') === false) {
            $this->add_issue($url, 'Schema', 'breadcrumb_schema_missing', 'Breadcrumb schema missing', 'Breadcrumb structured data was not detected.', 'low', 'Add BreadcrumbList schema to improve page hierarchy understanding.');
        }
    }

    private function audit_mobile_accessibility($url, $html) {
        if (!preg_match('/<meta[^>]+name=["\']viewport["\']/i', $html)) $this->add_issue($url, 'Mobile', 'viewport_missing', 'Mobile viewport tag missing', 'The page does not contain a viewport meta tag.', 'critical', 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.');
        if (preg_match_all('/<button[^>]*>\s*<\/button>/i', $html, $m) && count($m[0]) > 0) $this->add_issue($url, 'Accessibility', 'empty_button', 'Empty buttons found', count($m[0]) . ' empty buttons were detected.', 'medium', 'Add visible text or aria-label to icon-only buttons.');
        if (preg_match_all('/<input(?![^>]+type=["\']hidden["\'])[^>]*>/i', $html, $inputs)) {
            $labels = substr_count(strtolower($html), '<label');
            if (count($inputs[0]) > $labels + 2) $this->add_issue($url, 'Forms', 'form_labels_missing', 'Form labels may be missing', 'The page has more input fields than labels.', 'medium', 'Add label elements or aria-label attributes to form controls.');
        }
        if (stripos($html, '<main') === false) $this->add_issue($url, 'Semantic HTML', 'main_landmark_missing', 'Main landmark missing', 'The page does not appear to use a <main> landmark.', 'low', 'Wrap primary content in a <main> element.');
        if (stripos($html, '<nav') === false) $this->add_issue($url, 'Semantic HTML', 'nav_landmark_missing', 'Navigation landmark missing', 'The page does not appear to use a <nav> landmark.', 'low', 'Use <nav> for primary navigation areas.');
    }

    private function audit_performance_headers($url, $headers, $html) {
        $header_array = $this->headers_to_array($headers);
        if (empty($header_array['cache-control'])) $this->add_issue($url, 'Performance', 'cache_control_missing', 'Cache-Control header missing', 'The response does not include Cache-Control.', 'medium', 'Set browser caching policies for HTML and static assets.');
        if (empty($header_array['content-encoding']) || !preg_match('/br|gzip/i', $header_array['content-encoding'])) $this->add_issue($url, 'Performance', 'compression_missing', 'Compression may be missing', 'No gzip/br content-encoding header was detected.', 'medium', 'Enable Brotli or Gzip compression.');
        if (strlen($html) > 350000) $this->add_issue($url, 'Performance', 'html_too_large', 'HTML document is large', 'The HTML response is larger than 350KB.', 'medium', 'Reduce HTML, avoid oversized inline scripts/styles, paginate large listings.');
        if (WPTA_Utils::count_tag($html, 'script') > 45) $this->add_issue($url, 'Performance', 'many_scripts', 'Too many script tags', 'The page loads many scripts.', 'medium', 'Remove unused scripts and defer non-critical JavaScript.');
        if (WPTA_Utils::count_tag($html, 'link') > 80) $this->add_issue($url, 'Performance', 'many_link_tags', 'Too many link tags', 'The page contains many link tags.', 'low', 'Review CSS, preloads, fonts, and plugin-generated resources.');
        if (preg_match_all('/<script[^>]+src=["\']([^"\']+)["\']/i', $html, $scripts)) {
            $third_party = 0;
            foreach ($scripts[1] as $src) {
                $abs = WPTA_Utils::absolute_url($src, $url);
                if (preg_match('#^https?://#i', $abs) && !WPTA_Utils::is_internal_url($abs)) $third_party++;
            }
            if ($third_party > 8) $this->add_issue($url, 'Third-Party Scripts', 'many_third_party_scripts', 'Many third-party scripts', $third_party . ' third-party scripts were detected.', 'medium', 'Audit tracking, widgets, pixels, chat scripts, and remove unused tags.');
        }
    }

    private function audit_security_headers($url, $headers, $html) {
        $h = $this->headers_to_array($headers);
        $required = array(
            'strict-transport-security' => array('Security', 'hsts_missing', 'HSTS header missing', 'medium', 'Add Strict-Transport-Security after confirming HTTPS works everywhere.'),
            'x-content-type-options' => array('Security', 'x_content_type_missing', 'X-Content-Type-Options missing', 'low', 'Add X-Content-Type-Options: nosniff.'),
            'x-frame-options' => array('Security', 'x_frame_options_missing', 'X-Frame-Options missing', 'medium', 'Add X-Frame-Options: SAMEORIGIN or use CSP frame-ancestors.'),
            'referrer-policy' => array('Security', 'referrer_policy_missing', 'Referrer-Policy missing', 'low', 'Add a sensible Referrer-Policy header.'),
            'content-security-policy' => array('Security', 'csp_missing', 'Content-Security-Policy missing', 'medium', 'Add CSP carefully after testing third-party scripts.')
        );
        foreach ($required as $key => $meta) {
            if (empty($h[$key])) $this->add_issue($url, $meta[0], $meta[1], $meta[2], 'The response does not include this security header.', $meta[3], $meta[4]);
        }
        if (!empty($h['x-powered-by'])) $this->add_issue($url, 'Security', 'x_powered_by_exposed', 'X-Powered-By exposed', 'Technology header is visible: ' . esc_html($h['x-powered-by']), 'low', 'Remove or hide X-Powered-By headers.');
        if (preg_match('/http:\/\//i', $html) && stripos($url, 'https://') === 0) $this->add_issue($url, 'Security', 'mixed_content_possible', 'Possible mixed content', 'The HTML contains http:// resources or links.', 'high', 'Replace HTTP resources with HTTPS.');
    }

    private function headers_to_array($headers) {
        $out = array();
        if (is_object($headers) && method_exists($headers, 'getAll')) $headers = $headers->getAll();
        if (is_array($headers)) {
            foreach ($headers as $k => $v) $out[strtolower($k)] = is_array($v) ? implode(',', $v) : (string)$v;
        }
        return $out;
    }

    private function audit_content_quality($url, $html) {
        $text = trim(preg_replace('/\s+/', ' ', wp_strip_all_tags(preg_replace('/<script\b[^>]*>.*?<\/script>|<style\b[^>]*>.*?<\/style>/is', ' ', $html))));
        $words = str_word_count($text);
        if ($words < 150) $this->add_issue($url, 'Content Quality', 'thin_content', 'Thin content risk', 'The page appears to have fewer than 150 English-detected words. Bangla content may need manual review.', 'medium', 'Add helpful original content, direct answers, FAQs, specs, or product/category descriptions.');
        if (stripos($text, 'lorem ipsum') !== false) $this->add_issue($url, 'Content Quality', 'placeholder_text', 'Placeholder text found', 'Lorem Ipsum or placeholder text appears on the page.', 'high', 'Replace placeholder text with final content.');
        if (preg_match('/\b202[0-4]\b/', $text) && !preg_match('/\b2026\b/', $text)) $this->add_issue($url, 'Content Freshness', 'possibly_outdated_year', 'Possibly outdated year found', 'Old year references were found in content.', 'low', 'Review content freshness, offers, pricing, and years.');
        if (stripos($html, '<table') === false && (stripos($url, 'price') !== false || stripos($url, 'spec') !== false)) $this->add_issue($url, 'AEO', 'table_missing_for_comparison', 'Comparison/spec table may be missing', 'Price/specification pages often perform better with tables.', 'low', 'Add a clear table for prices, specs, comparisons, or key facts.');
        if (!preg_match('/\?\s*<\/h[2-4]>/i', $html) && stripos($url, 'blog') !== false) $this->add_issue($url, 'AEO', 'question_headings_missing', 'Question-style headings missing', 'No question-based H2/H3 heading was detected.', 'low', 'Add FAQs or question-style headings for AEO/AI search visibility.');
    }

    private function audit_forms_cta($url, $html) {
        if (stripos($html, '<form') !== false) {
            if (stripos($html, 'wp_nonce') === false && stripos($html, '_wpnonce') === false) $this->add_issue($url, 'Form Security', 'nonce_not_detected', 'Form nonce not detected', 'A form was found but WordPress nonce was not detected in HTML.', 'medium', 'Use wp_nonce_field and verify nonce on submission.');
            if (!preg_match('/type=["\']email["\']/i', $html) && stripos($html, 'email') !== false) $this->add_issue($url, 'Forms', 'email_input_type_missing', 'Email input type may be missing', 'Email field text found but type="email" was not detected.', 'low', 'Use proper input types for validation and mobile keyboards.');
        }
        if (stripos($html, 'tel:') === false && preg_match('/(\+?88)?01[3-9][0-9]{8}/', $html)) $this->add_issue($url, 'Conversion UX', 'phone_not_clickable', 'Phone number may not be clickable', 'A phone number was found but no tel: link was detected.', 'low', 'Wrap phone numbers in tel: links.');
        if (stripos($html, 'wa.me') === false && stripos($html, 'whatsapp') !== false) $this->add_issue($url, 'Conversion UX', 'whatsapp_link_check', 'WhatsApp mention without wa.me link', 'WhatsApp is mentioned but a wa.me link was not detected.', 'low', 'Add a working WhatsApp click-to-chat link.');
    }

    private function audit_ecommerce_signals($url, $html) {
        $is_wc = class_exists('WooCommerce') || stripos($html, 'woocommerce') !== false || stripos($url, '/product/') !== false;
        if (!$is_wc) return;
        if (stripos($url, 'cart') !== false || stripos($url, 'checkout') !== false) {
            $robots = WPTA_Utils::extract_meta($html, 'robots');
            if (stripos($robots, 'noindex') === false) $this->add_issue($url, 'Ecommerce', 'cart_checkout_indexable', 'Cart/checkout may be indexable', 'Cart or checkout pages should usually be noindex.', 'medium', 'Add noindex to cart, checkout, and account pages.');
        }
        if (stripos($url, '/product/') !== false || stripos($html, 'single-product') !== false) {
            if (!preg_match('/class=["\'][^"\']*price/i', $html) && stripos($html, '৳') === false) $this->add_issue($url, 'Ecommerce', 'product_price_missing', 'Product price not detected', 'Product price markup was not detected.', 'high', 'Ensure product price is visible in HTML and schema.');
            if (stripos($html, 'add_to_cart') === false && stripos($html, 'add-to-cart') === false && stripos($html, 'Add to cart') === false) $this->add_issue($url, 'Ecommerce', 'add_to_cart_missing', 'Add to cart not detected', 'Add to cart control was not detected.', 'high', 'Check product template and purchase flow.');
            if (stripos($html, 'sku') === false) $this->add_issue($url, 'Ecommerce', 'sku_missing', 'SKU not detected', 'SKU was not detected on the product page.', 'low', 'Add SKU/model information for product clarity and schema.');
            if (stripos($html, 'warranty') === false && stripos($html, 'ওয়ারেন্টি') === false) $this->add_issue($url, 'Ecommerce', 'warranty_missing', 'Warranty info not detected', 'Warranty information was not detected.', 'low', 'Add warranty and service support information.');
        }
    }

    private function audit_social_pwa($url, $html) {
        if (WPTA_Utils::extract_link_rel($html, 'manifest') === '') $this->add_issue($url, 'PWA', 'manifest_missing', 'Web app manifest missing', 'No manifest link was detected.', 'low', 'Add manifest.json if you want app-like/PWA support.');
        if (WPTA_Utils::extract_link_rel($html, 'icon') === '') $this->add_issue($url, 'Branding', 'favicon_missing', 'Favicon link missing', 'No favicon/icon link was detected.', 'low', 'Add favicon and app icons.');
        if (WPTA_Utils::extract_link_rel($html, 'apple-touch-icon') === '') $this->add_issue($url, 'PWA', 'apple_touch_icon_missing', 'Apple touch icon missing', 'No apple-touch-icon link was detected.', 'low', 'Add Apple touch icon for iOS home screen.');
        if (WPTA_Utils::extract_meta($html, 'twitter:card') === '') $this->add_issue($url, 'Social Preview', 'twitter_card_missing', 'Twitter/X card missing', 'No twitter:card meta tag detected.', 'low', 'Add Twitter/X card tags.');
    }

    private function audit_js_signals($url, $html) {
        if (stripos($html, '__NEXT_DATA__') !== false) $this->add_issue($url, 'Technology', 'nextjs_detected', 'Next.js detected', 'This page appears to use Next.js. Consider hydration, bundle size, image optimization and metadata checks.', 'low', 'Run JavaScript rendering audit with a headless browser for deeper validation.');
        if (stripos($html, 'react') !== false && stripos($html, 'hydration') !== false) $this->add_issue($url, 'JavaScript Rendering', 'hydration_signal', 'Hydration signal found', 'React/hydration related text was detected in HTML.', 'low', 'Use a browser-based audit to detect hydration mismatch and client-side rendering issues.');
        if (stripos($html, '.map') !== false) $this->add_issue($url, 'Security', 'sourcemap_reference', 'Source map reference detected', 'The HTML references source map files.', 'medium', 'Avoid exposing production source maps if they reveal source code or API details.');
        if (preg_match('/gtm\.js|googletagmanager/i', $html)) {
            $count = preg_match_all('/googletagmanager/i', $html);
            if ($count > 4) $this->add_issue($url, 'Analytics', 'possible_duplicate_gtm', 'Possible duplicate GTM/Google tags', 'Multiple Google Tag Manager references were detected.', 'medium', 'Check GTM/GA4 implementation to avoid duplicate events.');
        } else {
            $this->add_issue($url, 'Analytics', 'gtm_missing', 'Google Tag Manager not detected', 'GTM was not detected in page HTML.', 'low', 'Install GTM if you need centralized analytics/conversion tracking.');
        }
        if (stripos($html, 'fbq(') === false && stripos($html, 'facebook.com/tr') === false) $this->add_issue($url, 'Analytics', 'meta_pixel_missing', 'Meta Pixel not detected', 'Facebook/Meta Pixel was not detected.', 'low', 'Install Meta Pixel if you run Facebook ads.');
    }
}
