/**
 * SEO Tools — Website SEO auditing, performance checks, content analysis,
 * and WordPress security assessment.
 *
 * 5 tools:
 * - seo_audit_url: Fetch URL and analyze SEO elements (title, meta, headings, images, etc.)
 * - check_page_performance: Lightweight performance checks via fetch (response time, compression, caching)
 * - analyze_seo_content: Content analysis for SEO (readability, keyword density, link ratio)
 * - check_wordpress_site: Detect WordPress and assess security posture
 * - scan_wordpress_updates: Check WordPress plugins/themes for known versions and vulnerabilities
 *
 * All tools use HTTP fetch + basic HTML parsing. No browser dependencies.
 * No external npm dependencies — uses Node's built-in fetch (Node 18+).
 */
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
const DEFAULT_TIMEOUT_MS = 10_000;
const MAX_RESPONSE_BYTES = 5_242_880; // 5 MB
// SSRF blocklist — block private/internal network ranges
const SSRF_BLOCKED_PATTERNS = [
    /^https?:\/\/localhost/i,
    /^https?:\/\/127\./,
    /^https?:\/\/0\./,
    /^https?:\/\/10\./,
    /^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
    /^https?:\/\/192\.168\./,
    /^https?:\/\/169\.254\./,
    /^https?:\/\/\[::1\]/,
    /^https?:\/\/\[fd/i,
    /^https?:\/\/\[fe80:/i,
    /^https?:\/\/metadata\./i,
];
function validateSeoUrl(url) {
    try {
        const parsed = new URL(url);
        if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
            return "Only http/https URLs are allowed";
        }
        for (const pattern of SSRF_BLOCKED_PATTERNS) {
            if (pattern.test(url)) {
                return "URL points to a blocked internal/private network address";
            }
        }
        return null;
    }
    catch {
        return "Invalid URL";
    }
}
function createAbortSignal(timeoutMs) {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), timeoutMs);
    return controller.signal;
}
async function readBoundedText(response, maxBytes) {
    const reader = response.body?.getReader();
    if (!reader)
        return await response.text();
    const chunks = [];
    let totalBytes = 0;
    while (true) {
        const { done, value } = await reader.read();
        if (done)
            break;
        totalBytes += value.byteLength;
        if (totalBytes > maxBytes) {
            await reader.cancel();
            break;
        }
        chunks.push(value);
    }
    const merged = new Uint8Array(Math.min(totalBytes, maxBytes));
    let offset = 0;
    for (const chunk of chunks) {
        const toCopy = Math.min(chunk.byteLength, maxBytes - offset);
        merged.set(chunk.subarray(0, toCopy), offset);
        offset += toCopy;
        if (offset >= maxBytes)
            break;
    }
    return new TextDecoder().decode(merged);
}
async function safeFetch(url, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
    // SSRF validation
    const ssrfError = validateSeoUrl(url);
    if (ssrfError) {
        return { ok: false, status: 0, headers: new Headers(), text: "", error: ssrfError };
    }
    try {
        const res = await fetch(url, {
            signal: createAbortSignal(timeoutMs),
            headers: {
                "User-Agent": "NodeBench-SEO-Auditor/1.0",
                Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            },
            redirect: "follow",
            ...options,
        });
        const text = await readBoundedText(res, MAX_RESPONSE_BYTES);
        return { ok: res.ok, status: res.status, headers: res.headers, text };
    }
    catch (e) {
        return {
            ok: false,
            status: 0,
            headers: new Headers(),
            text: "",
            error: e.name === "AbortError" ? `Request timed out after ${timeoutMs}ms` : e.message,
        };
    }
}
// ─── HTML parsing helpers ─────────────────────────────────────────────────────
function extractTag(html, tag) {
    const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
    const match = html.match(regex);
    return match ? match[1].trim() : null;
}
function extractMeta(html, name) {
    // Match both name="..." and property="..."
    const nameRegex = new RegExp(`<meta\\s+(?:[^>]*?)?(?:name|property)\\s*=\\s*["']${escapeRegex(name)}["'][^>]*?content\\s*=\\s*["']([^"']*)["'][^>]*?>`, "i");
    const match = html.match(nameRegex);
    if (match)
        return match[1];
    // Try reversed attribute order: content before name/property
    const reversedRegex = new RegExp(`<meta\\s+(?:[^>]*?)?content\\s*=\\s*["']([^"']*)["'][^>]*?(?:name|property)\\s*=\\s*["']${escapeRegex(name)}["'][^>]*?>`, "i");
    const reversedMatch = html.match(reversedRegex);
    return reversedMatch ? reversedMatch[1] : null;
}
function extractLinkRel(html, rel) {
    const regex = new RegExp(`<link\\s+(?:[^>]*?)?rel\\s*=\\s*["']${escapeRegex(rel)}["'][^>]*?href\\s*=\\s*["']([^"']*)["'][^>]*?>`, "i");
    const match = html.match(regex);
    if (match)
        return match[1];
    // Reversed attribute order
    const reversedRegex = new RegExp(`<link\\s+(?:[^>]*?)?href\\s*=\\s*["']([^"']*)["'][^>]*?rel\\s*=\\s*["']${escapeRegex(rel)}["'][^>]*?>`, "i");
    const reversedMatch = html.match(reversedRegex);
    return reversedMatch ? reversedMatch[1] : null;
}
function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function countHeadings(html) {
    const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
    for (const level of ["h1", "h2", "h3", "h4", "h5", "h6"]) {
        const regex = new RegExp(`<${level}[\\s>]`, "gi");
        const matches = html.match(regex);
        counts[level] = matches ? matches.length : 0;
    }
    return counts;
}
function countImages(html) {
    const imgRegex = /<img\s[^>]*?>/gi;
    const imgs = html.match(imgRegex) || [];
    const total = imgs.length;
    let withoutAlt = 0;
    for (const img of imgs) {
        // Check if alt attribute exists and is non-empty
        const altMatch = img.match(/\salt\s*=\s*["']([^"']*)["']/i);
        if (!altMatch || altMatch[1].trim() === "") {
            withoutAlt++;
        }
    }
    return { total, withoutAlt };
}
// ─── Content analysis helpers ─────────────────────────────────────────────────
function stripHtml(html) {
    return html
        .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
        .replace(/<[^>]+>/g, " ")
        .replace(/&[a-z]+;/gi, " ")
        .replace(/\s+/g, " ")
        .trim();
}
function countSyllables(word) {
    const w = word.toLowerCase().replace(/[^a-z]/g, "");
    if (w.length <= 2)
        return 1;
    // Count vowel groups
    const vowelGroups = w.match(/[aeiouy]+/g);
    let count = vowelGroups ? vowelGroups.length : 1;
    // Silent e at end
    if (w.endsWith("e") && count > 1)
        count--;
    return Math.max(1, count);
}
function fleschKincaidScore(words, sentences, syllables) {
    if (sentences === 0 || words === 0)
        return 0;
    return 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
}
function readabilityLevel(score) {
    if (score >= 90)
        return "Very Easy (5th grade)";
    if (score >= 80)
        return "Easy (6th grade)";
    if (score >= 70)
        return "Fairly Easy (7th grade)";
    if (score >= 60)
        return "Standard (8th-9th grade)";
    if (score >= 50)
        return "Fairly Difficult (10th-12th grade)";
    if (score >= 30)
        return "Difficult (College)";
    return "Very Difficult (College Graduate)";
}
// ─── Tools ────────────────────────────────────────────────────────────────────
export const seoTools = [
    {
        name: "seo_audit_url",
        description: "Fetch a URL and analyze its SEO elements: title tag, meta description, Open Graph tags (og:title, og:description, og:image), heading hierarchy (h1-h6 counts), images without alt text, canonical URL, robots meta, and structured data (JSON-LD). Scores each element 0-100 and returns a total score with actionable recommendations. Uses HTTP fetch with regex-based HTML parsing — no browser needed.",
        inputSchema: {
            type: "object",
            properties: {
                url: { type: "string", description: "The URL to audit" },
                timeout: {
                    type: "number",
                    description: "Request timeout in milliseconds (default: 10000)",
                },
            },
            required: ["url"],
        },
        handler: async (args) => {
            const url = args.url;
            const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;
            const res = await safeFetch(url, timeoutMs);
            if (!res.ok) {
                return {
                    error: true,
                    url,
                    status: res.status,
                    message: res.error || `HTTP ${res.status}`,
                };
            }
            const html = res.text;
            const recommendations = [];
            // ── Title ──
            const title = extractTag(html, "title");
            const titleLength = title ? title.length : 0;
            let titleScore = 0;
            const titleIssues = [];
            if (!title) {
                titleIssues.push("Missing title tag");
                recommendations.push("Add a <title> tag — it is the most important on-page SEO element.");
            }
            else {
                if (titleLength < 30) {
                    titleIssues.push("Title too short (< 30 chars)");
                    recommendations.push("Lengthen the title to at least 30 characters for better CTR.");
                }
                else if (titleLength > 60) {
                    titleIssues.push("Title too long (> 60 chars) — may be truncated in SERPs");
                    recommendations.push("Shorten the title to 60 characters or fewer to avoid truncation.");
                }
                titleScore = titleLength >= 30 && titleLength <= 60 ? 100 : titleLength > 0 ? 60 : 0;
            }
            // ── Meta description ──
            const metaDesc = extractMeta(html, "description");
            const metaDescLength = metaDesc ? metaDesc.length : 0;
            let metaDescScore = 0;
            const metaDescIssues = [];
            if (!metaDesc) {
                metaDescIssues.push("Missing meta description");
                recommendations.push("Add a meta description — it directly affects click-through rates.");
            }
            else {
                if (metaDescLength < 70) {
                    metaDescIssues.push("Meta description too short (< 70 chars)");
                    recommendations.push("Expand the meta description to at least 70 characters.");
                }
                else if (metaDescLength > 160) {
                    metaDescIssues.push("Meta description too long (> 160 chars)");
                    recommendations.push("Trim the meta description to 160 characters or fewer.");
                }
                metaDescScore = metaDescLength >= 70 && metaDescLength <= 160 ? 100 : metaDescLength > 0 ? 60 : 0;
            }
            // ── Open Graph ──
            const ogTitle = extractMeta(html, "og:title");
            const ogDescription = extractMeta(html, "og:description");
            const ogImage = extractMeta(html, "og:image");
            let ogScore = 0;
            const ogIssues = [];
            if (!ogTitle)
                ogIssues.push("Missing og:title");
            if (!ogDescription)
                ogIssues.push("Missing og:description");
            if (!ogImage)
                ogIssues.push("Missing og:image");
            ogScore = Math.round(([ogTitle, ogDescription, ogImage].filter(Boolean).length / 3) * 100);
            if (ogScore < 100) {
                recommendations.push(`Add missing Open Graph tags: ${ogIssues.join(", ")}.`);
            }
            // ── Headings ──
            const headings = countHeadings(html);
            let headingScore = 100;
            const headingIssues = [];
            if (headings.h1 === 0) {
                headingIssues.push("No H1 tag found");
                headingScore -= 40;
                recommendations.push("Add exactly one H1 tag — it signals the main topic to search engines.");
            }
            else if (headings.h1 > 1) {
                headingIssues.push(`Multiple H1 tags (${headings.h1}) — use exactly one`);
                headingScore -= 20;
                recommendations.push("Reduce to a single H1 tag for clearer content hierarchy.");
            }
            if (headings.h2 === 0) {
                headingIssues.push("No H2 tags — add subheadings for structure");
                headingScore -= 20;
                recommendations.push("Add H2 subheadings to improve content structure and scannability.");
            }
            headingScore = Math.max(0, headingScore);
            // ── Images ──
            const images = countImages(html);
            let imageScore = 100;
            const imageIssues = [];
            if (images.total > 0 && images.withoutAlt > 0) {
                const pct = Math.round((images.withoutAlt / images.total) * 100);
                imageIssues.push(`${images.withoutAlt}/${images.total} images missing alt text (${pct}%)`);
                imageScore = Math.max(0, Math.round(100 - pct));
                recommendations.push(`Add alt text to ${images.withoutAlt} image(s) for accessibility and image SEO.`);
            }
            // ── Canonical ──
            const canonical = extractLinkRel(html, "canonical");
            let canonicalScore = canonical ? 100 : 0;
            const canonicalIssues = [];
            if (!canonical) {
                canonicalIssues.push("Missing canonical URL");
                canonicalScore = 0;
                recommendations.push("Add a <link rel=\"canonical\"> tag to prevent duplicate content issues.");
            }
            // ── Robots meta ──
            const robotsMeta = extractMeta(html, "robots");
            let robotsScore = 100;
            const robotsIssues = [];
            if (!robotsMeta) {
                robotsIssues.push("No robots meta tag (defaults to index, follow)");
                robotsScore = 80; // Not strictly required
            }
            else if (robotsMeta.toLowerCase().includes("noindex")) {
                robotsIssues.push("Page is set to noindex — will not appear in search results");
                robotsScore = 20;
                recommendations.push("Remove noindex directive if this page should be indexed.");
            }
            // ── Structured data ──
            const hasJsonLd = /<script\s+type\s*=\s*["']application\/ld\+json["'][^>]*>/i.test(html);
            let structuredDataScore = hasJsonLd ? 100 : 0;
            const structuredDataIssues = [];
            if (!hasJsonLd) {
                structuredDataIssues.push("No JSON-LD structured data found");
                structuredDataScore = 0;
                recommendations.push("Add JSON-LD structured data for rich snippets in search results.");
            }
            // ── Total score ──
            const weights = {
                title: 20,
                metaDescription: 15,
                openGraph: 10,
                headings: 15,
                images: 10,
                canonical: 10,
                robots: 10,
                structuredData: 10,
            };
            const totalScore = Math.round((titleScore * weights.title +
                metaDescScore * weights.metaDescription +
                ogScore * weights.openGraph +
                headingScore * weights.headings +
                imageScore * weights.images +
                canonicalScore * weights.canonical +
                robotsScore * weights.robots +
                structuredDataScore * weights.structuredData) /
                Object.values(weights).reduce((a, b) => a + b, 0));
            return {
                url,
                score: totalScore,
                elements: {
                    title: { content: title, length: titleLength, score: titleScore, issues: titleIssues },
                    metaDescription: { content: metaDesc, length: metaDescLength, score: metaDescScore, issues: metaDescIssues },
                    openGraph: {
                        ogTitle,
                        ogDescription,
                        ogImage,
                        score: ogScore,
                        issues: ogIssues,
                    },
                    headings: { counts: headings, score: headingScore, issues: headingIssues },
                    images: { total: images.total, withoutAlt: images.withoutAlt, score: imageScore, issues: imageIssues },
                    canonical: { url: canonical, score: canonicalScore, issues: canonicalIssues },
                    robots: { content: robotsMeta, score: robotsScore, issues: robotsIssues },
                    structuredData: { hasJsonLd, score: structuredDataScore, issues: structuredDataIssues },
                },
                recommendations,
            };
        },
    },
    {
        name: "check_page_performance",
        description: "Lightweight page performance check via HTTP fetch (no browser). Measures: response time, content size, compression (content-encoding header), cache headers (cache-control, etag, last-modified), and HTTP status. Returns a performance score with recommendations for improvement.",
        inputSchema: {
            type: "object",
            properties: {
                url: { type: "string", description: "The URL to check" },
            },
            required: ["url"],
        },
        handler: async (args) => {
            const url = args.url;
            const recommendations = [];
            const startTime = Date.now();
            const res = await safeFetch(url);
            const responseTimeMs = Date.now() - startTime;
            if (!res.ok) {
                return {
                    error: true,
                    url,
                    status: res.status,
                    responseTimeMs,
                    message: res.error || `HTTP ${res.status}`,
                };
            }
            const contentSizeBytes = new TextEncoder().encode(res.text).length;
            const contentEncoding = res.headers.get("content-encoding");
            const compressed = !!contentEncoding;
            const cacheControl = res.headers.get("cache-control");
            const etag = res.headers.get("etag");
            const lastModified = res.headers.get("last-modified");
            // ── Scoring ──
            let score = 100;
            // Response time scoring
            if (responseTimeMs > 3000) {
                score -= 30;
                recommendations.push(`Response time is ${responseTimeMs}ms — aim for under 1000ms. Consider CDN, caching, or server optimization.`);
            }
            else if (responseTimeMs > 1500) {
                score -= 15;
                recommendations.push(`Response time is ${responseTimeMs}ms — aim for under 1000ms.`);
            }
            else if (responseTimeMs > 1000) {
                score -= 5;
            }
            // Content size scoring
            if (contentSizeBytes > 500_000) {
                score -= 20;
                recommendations.push(`Page size is ${Math.round(contentSizeBytes / 1024)}KB — consider reducing HTML, inlining less CSS/JS, or lazy-loading assets.`);
            }
            else if (contentSizeBytes > 200_000) {
                score -= 10;
                recommendations.push(`Page size is ${Math.round(contentSizeBytes / 1024)}KB — could be smaller.`);
            }
            // Compression scoring
            if (!compressed) {
                score -= 15;
                recommendations.push("Enable gzip or brotli compression — the response has no content-encoding header.");
            }
            // Cache scoring
            if (!cacheControl) {
                score -= 10;
                recommendations.push("Add Cache-Control headers to improve repeat visit performance.");
            }
            else if (cacheControl.includes("no-cache") || cacheControl.includes("no-store")) {
                score -= 5;
                recommendations.push("Cache-Control disables caching. Consider allowing caching for static assets.");
            }
            if (!etag && !lastModified) {
                score -= 5;
                recommendations.push("Add ETag or Last-Modified headers to enable conditional requests (304 responses).");
            }
            score = Math.max(0, score);
            return {
                url,
                responseTimeMs,
                contentSizeBytes,
                contentSizeKB: Math.round(contentSizeBytes / 1024),
                compressed,
                contentEncoding: contentEncoding || "none",
                cacheHeaders: {
                    cacheControl: cacheControl || null,
                    etag: etag || null,
                    lastModified: lastModified || null,
                },
                httpStatus: res.status,
                score,
                recommendations,
            };
        },
    },
    {
        name: "analyze_seo_content",
        description: "Analyze HTML or text content for SEO quality: word count, sentence count, paragraph count, Flesch-Kincaid readability score, heading structure, keyword density (if targetKeyword provided), and internal vs external link ratio. Works on raw HTML or plain text. No network requests — pure content analysis.",
        inputSchema: {
            type: "object",
            properties: {
                content: { type: "string", description: "HTML or plain text content to analyze" },
                targetKeyword: {
                    type: "string",
                    description: "Target keyword/phrase to measure density for (optional)",
                },
            },
            required: ["content"],
        },
        handler: async (args) => {
            const content = args.content;
            const targetKeyword = args.targetKeyword;
            const recommendations = [];
            // Strip HTML for text analysis
            const plainText = stripHtml(content);
            const words = plainText.split(/\s+/).filter((w) => w.length > 0);
            const wordCount = words.length;
            // Sentence count — split on sentence-ending punctuation
            const sentences = plainText.split(/[.!?]+/).filter((s) => s.trim().length > 0);
            const sentenceCount = sentences.length;
            // Paragraph count — split on double newlines or <p> tags
            const isHtml = /<[a-z][\s\S]*>/i.test(content);
            let paragraphCount;
            if (isHtml) {
                const pTags = content.match(/<p[\s>]/gi);
                paragraphCount = pTags ? pTags.length : 1;
            }
            else {
                const paras = content.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
                paragraphCount = paras.length;
            }
            // Syllable count and readability
            const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
            const rawReadability = fleschKincaidScore(wordCount, sentenceCount, totalSyllables);
            const readabilityScore = Math.round(Math.max(0, Math.min(100, rawReadability)));
            const level = readabilityLevel(readabilityScore);
            // Heading structure
            const headingStructure = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
            if (isHtml) {
                const htmlHeadings = countHeadings(content);
                Object.assign(headingStructure, htmlHeadings);
            }
            else {
                // Markdown headings
                const lines = content.split("\n");
                for (const line of lines) {
                    const match = line.match(/^(#{1,6})\s/);
                    if (match) {
                        const level = `h${match[1].length}`;
                        headingStructure[level] = (headingStructure[level] || 0) + 1;
                    }
                }
            }
            // Keyword density
            let keywordDensity;
            if (targetKeyword && wordCount > 0) {
                const kwLower = targetKeyword.toLowerCase();
                const textLower = plainText.toLowerCase();
                let kwCount = 0;
                let searchPos = 0;
                while (true) {
                    const idx = textLower.indexOf(kwLower, searchPos);
                    if (idx === -1)
                        break;
                    kwCount++;
                    searchPos = idx + 1;
                }
                const density = Math.round((kwCount / wordCount) * 10000) / 100; // percentage with 2 decimals
                let kwRec = "";
                if (density === 0) {
                    kwRec = `Target keyword "${targetKeyword}" not found — include it naturally in the content.`;
                    recommendations.push(kwRec);
                }
                else if (density < 0.5) {
                    kwRec = `Keyword density is low (${density}%) — consider adding more natural mentions.`;
                    recommendations.push(kwRec);
                }
                else if (density > 3) {
                    kwRec = `Keyword density is high (${density}%) — risk of keyword stuffing. Reduce mentions.`;
                    recommendations.push(kwRec);
                }
                else {
                    kwRec = `Keyword density of ${density}% is in the optimal range (0.5-3%).`;
                }
                keywordDensity = { keyword: targetKeyword, count: kwCount, density, recommendation: kwRec };
            }
            // Link analysis
            const linkRegex = /<a\s[^>]*href\s*=\s*["']([^"']*)["'][^>]*>/gi;
            let linkMatch;
            let internalLinks = 0;
            let externalLinks = 0;
            while ((linkMatch = linkRegex.exec(content)) !== null) {
                const href = linkMatch[1];
                if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//")) {
                    externalLinks++;
                }
                else if (href.startsWith("#") || href.startsWith("/") || href.startsWith("./") || href.startsWith("../")) {
                    internalLinks++;
                }
                else {
                    // Relative link without prefix — treat as internal
                    internalLinks++;
                }
            }
            const linkRatio = externalLinks > 0 ? Math.round((internalLinks / externalLinks) * 100) / 100 : internalLinks > 0 ? Infinity : 0;
            // Content-level recommendations
            if (wordCount < 300) {
                recommendations.push("Content is thin (< 300 words). Aim for 800+ words for competitive SEO.");
            }
            else if (wordCount < 800) {
                recommendations.push("Content length is moderate. Consider expanding to 1000+ words for long-tail keyword coverage.");
            }
            if (readabilityScore < 50) {
                recommendations.push("Content is difficult to read. Simplify sentences and use shorter words.");
            }
            if (headingStructure.h1 === 0 && isHtml) {
                recommendations.push("Add an H1 heading to establish the main topic.");
            }
            if (sentenceCount > 0 && wordCount / sentenceCount > 25) {
                recommendations.push("Average sentence length is high. Break long sentences for better readability.");
            }
            return {
                wordCount,
                sentenceCount,
                paragraphCount,
                avgWordsPerSentence: sentenceCount > 0 ? Math.round((wordCount / sentenceCount) * 10) / 10 : 0,
                totalSyllables,
                readabilityScore,
                readabilityLevel: level,
                headingStructure,
                keywordDensity,
                linkAnalysis: {
                    internal: internalLinks,
                    external: externalLinks,
                    ratio: linkRatio,
                },
                recommendations,
            };
        },
    },
    {
        name: "check_wordpress_site",
        description: "Detect whether a site runs WordPress and assess its security posture. Checks: WP generator meta tag, wp-content/wp-includes paths, WP REST API (/wp-json/), login page (/wp-login.php), XML-RPC (/xmlrpc.php), active theme, and visible plugins. Returns WordPress detection confidence, theme info, plugin list, and security score with risk assessments.",
        inputSchema: {
            type: "object",
            properties: {
                url: { type: "string", description: "The URL of the site to check" },
                timeout: {
                    type: "number",
                    description: "Request timeout in milliseconds (default: 10000)",
                },
            },
            required: ["url"],
        },
        handler: async (args) => {
            const rawUrl = args.url.replace(/\/$/, "");
            const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;
            const recommendations = [];
            // Fetch main page
            const mainRes = await safeFetch(rawUrl, timeoutMs);
            if (!mainRes.ok) {
                return {
                    error: true,
                    url: rawUrl,
                    status: mainRes.status,
                    message: mainRes.error || `HTTP ${mainRes.status}`,
                };
            }
            const html = mainRes.text;
            // ── WordPress detection signals ──
            const signals = [];
            // Generator meta tag
            const generator = extractMeta(html, "generator");
            let version = null;
            if (generator && /wordpress/i.test(generator)) {
                signals.push("generator meta tag");
                const versionMatch = generator.match(/WordPress\s+([\d.]+)/i);
                if (versionMatch)
                    version = versionMatch[1];
            }
            // wp-content / wp-includes in HTML
            if (/\/wp-content\//i.test(html))
                signals.push("wp-content path in HTML");
            if (/\/wp-includes\//i.test(html))
                signals.push("wp-includes path in HTML");
            const isWordPress = signals.length > 0;
            // ── Theme detection ──
            let theme = null;
            const themeMatch = html.match(/\/wp-content\/themes\/([a-zA-Z0-9_-]+)\//i);
            if (themeMatch)
                theme = themeMatch[1];
            // ── Plugin detection from HTML ──
            const pluginSet = new Set();
            const pluginRegex = /\/wp-content\/plugins\/([a-zA-Z0-9_-]+)\//gi;
            let pluginMatch;
            while ((pluginMatch = pluginRegex.exec(html)) !== null) {
                pluginSet.add(pluginMatch[1]);
            }
            const plugins = Array.from(pluginSet);
            // ── Security checks (only if WordPress detected) ──
            let wpJsonExposed = false;
            let loginExposed = false;
            let xmlrpcExposed = false;
            const risks = [];
            if (isWordPress) {
                // Check /wp-json/
                try {
                    const wpJsonRes = await safeFetch(`${rawUrl}/wp-json/`, timeoutMs);
                    if (wpJsonRes.ok) {
                        wpJsonExposed = true;
                        risks.push("WP REST API is publicly accessible — may expose user data and site structure.");
                        recommendations.push("Restrict WP REST API access to authenticated users or disable unused endpoints.");
                    }
                }
                catch { /* ignore */ }
                // Check /wp-login.php
                try {
                    const loginRes = await safeFetch(`${rawUrl}/wp-login.php`, timeoutMs);
                    if (loginRes.ok || loginRes.status === 200) {
                        loginExposed = true;
                        risks.push("Login page is publicly accessible — susceptible to brute force attacks.");
                        recommendations.push("Move or protect wp-login.php with IP allowlisting, 2FA, or a security plugin.");
                    }
                }
                catch { /* ignore */ }
                // Check /xmlrpc.php
                try {
                    const xmlrpcRes = await safeFetch(`${rawUrl}/xmlrpc.php`, timeoutMs);
                    if (xmlrpcRes.ok || xmlrpcRes.status === 405) {
                        xmlrpcExposed = true;
                        risks.push("XML-RPC is exposed — enables brute force amplification and DDoS pingback attacks.");
                        recommendations.push("Disable XML-RPC if not needed (block via .htaccess or security plugin).");
                    }
                }
                catch { /* ignore */ }
                // Version-based recommendation
                if (version) {
                    recommendations.push(`WordPress version ${version} detected in source. Remove the generator meta tag to hide version info.`);
                }
                if (plugins.length > 10) {
                    recommendations.push(`${plugins.length} plugins detected — each adds attack surface. Audit and remove unused plugins.`);
                }
            }
            // Security score
            let securityScore = 100;
            if (wpJsonExposed)
                securityScore -= 20;
            if (loginExposed)
                securityScore -= 25;
            if (xmlrpcExposed)
                securityScore -= 30;
            if (version)
                securityScore -= 10; // Version leak
            securityScore = Math.max(0, securityScore);
            return {
                url: rawUrl,
                isWordPress,
                detectionSignals: signals,
                version,
                theme,
                plugins,
                pluginCount: plugins.length,
                security: {
                    wpJsonExposed,
                    loginExposed,
                    xmlrpcExposed,
                    score: securityScore,
                    risks,
                },
                recommendations: isWordPress
                    ? recommendations
                    : ["Site does not appear to be WordPress — no WP-specific recommendations."],
            };
        },
    },
    {
        name: "scan_wordpress_updates",
        description: "Scan a WordPress site for plugin and theme versions, and optionally check for known vulnerabilities via the WPScan API. Detects plugins/themes from page source and extracts versions from ?ver= query params. If wpscanApiToken is provided, queries the WPScan vulnerability database for each detected plugin.",
        inputSchema: {
            type: "object",
            properties: {
                url: { type: "string", description: "The WordPress site URL to scan" },
                wpscanApiToken: {
                    type: "string",
                    description: "WPScan API token for vulnerability lookups (optional — get one free at https://wpscan.com)",
                },
            },
            required: ["url"],
        },
        handler: async (args) => {
            const rawUrl = args.url.replace(/\/$/, "");
            const wpscanApiToken = args.wpscanApiToken;
            const recommendations = [];
            // Fetch main page
            const mainRes = await safeFetch(rawUrl);
            if (!mainRes.ok) {
                return {
                    error: true,
                    url: rawUrl,
                    status: mainRes.status,
                    message: mainRes.error || `HTTP ${mainRes.status}`,
                };
            }
            const html = mainRes.text;
            // ── Detect plugins with versions ──
            const pluginMap = new Map();
            const pluginRegex = /\/wp-content\/plugins\/([a-zA-Z0-9_-]+)\/[^"']*?(?:\?ver=([0-9.]+))?/gi;
            let match;
            while ((match = pluginRegex.exec(html)) !== null) {
                const name = match[1];
                const ver = match[2] || null;
                // Keep the version if we found one (don't overwrite version with null)
                if (!pluginMap.has(name) || (ver && !pluginMap.get(name))) {
                    pluginMap.set(name, ver);
                }
            }
            // Also try to extract versions from generic script/style ver params
            const verRegex = /\/wp-content\/plugins\/([a-zA-Z0-9_-]+)\/[^"'\s]*["']/gi;
            while ((match = verRegex.exec(html)) !== null) {
                const name = match[1];
                if (!pluginMap.has(name)) {
                    pluginMap.set(name, null);
                }
            }
            // ── Detect themes with versions ──
            const themeMap = new Map();
            const themeRegex = /\/wp-content\/themes\/([a-zA-Z0-9_-]+)\/[^"']*?(?:\?ver=([0-9.]+))?/gi;
            while ((match = themeRegex.exec(html)) !== null) {
                const name = match[1];
                const ver = match[2] || null;
                if (!themeMap.has(name) || (ver && !themeMap.get(name))) {
                    themeMap.set(name, ver);
                }
            }
            // ── Try WP REST API for plugin info (usually requires auth) ──
            try {
                const wpPluginsRes = await safeFetch(`${rawUrl}/wp-json/wp/v2/plugins`);
                if (wpPluginsRes.ok) {
                    try {
                        const pluginsData = JSON.parse(wpPluginsRes.text);
                        if (Array.isArray(pluginsData)) {
                            for (const p of pluginsData) {
                                const slug = p.plugin?.split("/")?.[0] || p.textdomain;
                                if (slug) {
                                    pluginMap.set(slug, p.version || pluginMap.get(slug) || null);
                                }
                            }
                        }
                    }
                    catch { /* JSON parse failed */ }
                }
            }
            catch { /* ignore */ }
            // ── WPScan vulnerability lookup ──
            const pluginsResult = [];
            let totalVulnerabilities = 0;
            const pluginEntries = Array.from(pluginMap.entries());
            for (let pi = 0; pi < pluginEntries.length; pi++) {
                const name = pluginEntries[pi][0];
                const version = pluginEntries[pi][1];
                const entry = { name, version };
                if (wpscanApiToken) {
                    try {
                        const wpscanRes = await safeFetch(`https://wpscan.com/api/v3/plugins/${name}`, DEFAULT_TIMEOUT_MS, { headers: { Authorization: `Token token=${wpscanApiToken}` } });
                        if (wpscanRes.ok) {
                            try {
                                const data = JSON.parse(wpscanRes.text);
                                const pluginData = data[name];
                                if (pluginData?.vulnerabilities) {
                                    entry.vulnerabilities = pluginData.vulnerabilities.map((v) => ({
                                        title: v.title || "Unknown vulnerability",
                                        fixedIn: v.fixed_in || null,
                                        severity: v.cvss?.severity || null,
                                    }));
                                    // Filter to only show vulns affecting the detected version
                                    if (version && entry.vulnerabilities) {
                                        const relevant = entry.vulnerabilities.filter((v) => !v.fixedIn || compareVersions(version, v.fixedIn) < 0);
                                        totalVulnerabilities += relevant.length;
                                        entry.vulnerabilities = relevant;
                                    }
                                    else {
                                        totalVulnerabilities += entry.vulnerabilities?.length ?? 0;
                                    }
                                }
                            }
                            catch { /* JSON parse failed */ }
                        }
                    }
                    catch { /* ignore */ }
                }
                pluginsResult.push(entry);
            }
            const themesResult = [];
            const themeEntries = Array.from(themeMap.entries());
            for (let ti = 0; ti < themeEntries.length; ti++) {
                themesResult.push({ name: themeEntries[ti][0], version: themeEntries[ti][1] });
            }
            // ── Recommendations ──
            const noVersionPlugins = pluginsResult.filter((p) => !p.version);
            if (noVersionPlugins.length > 0) {
                recommendations.push(`${noVersionPlugins.length} plugin(s) have unknown versions — cannot verify if they are up to date.`);
            }
            if (totalVulnerabilities > 0) {
                recommendations.push(`${totalVulnerabilities} known vulnerabilities found. Update affected plugins immediately.`);
            }
            if (!wpscanApiToken) {
                recommendations.push("Provide a wpscanApiToken to check for known vulnerabilities (free at https://wpscan.com).");
            }
            if (pluginsResult.length === 0 && themeMap.size === 0) {
                recommendations.push("No plugins or themes detected in page source — site may not be WordPress.");
            }
            return {
                url: rawUrl,
                plugins: pluginsResult,
                pluginCount: pluginsResult.length,
                themes: themesResult,
                themeCount: themesResult.length,
                totalVulnerabilities,
                wpscanApiUsed: !!wpscanApiToken,
                recommendations,
            };
        },
    },
];
// ─── Version comparison helper ────────────────────────────────────────────────
function compareVersions(a, b) {
    const partsA = a.split(".").map(Number);
    const partsB = b.split(".").map(Number);
    const len = Math.max(partsA.length, partsB.length);
    for (let i = 0; i < len; i++) {
        const va = partsA[i] || 0;
        const vb = partsB[i] || 0;
        if (va < vb)
            return -1;
        if (va > vb)
            return 1;
    }
    return 0;
}
//# sourceMappingURL=seoTools.js.map