/**
 * SEO audit engine.
 *
 * Pure analysis: given a set of content entities (pages/posts) plus optional
 * cross-cutting signals (broken links, redirect chains/loops, sitemap/robots
 * health), it produces:
 *   - per-page SEO scores (via `calculatePageSeoScore`),
 *   - a flat list of detected issues with stable `issueKey`s,
 *   - an explainable site score (via `calculateSiteSeoScore`),
 *   - issue counts by severity, and
 *   - priority-ranked quick wins.
 *
 * The handler layer fetches the data, calls `runSeoAuditAnalysis`, and
 * snapshots the result into `SeoAuditRun` / `SeoIssue` rows. Keeping detection
 * pure means it is fully unit-testable with no DB.
 */
import { calculatePageSeoScore, calculateSiteSeoScore, gradeForSeoScore, countH1, } from './score.js';
import { stripHtmlTags } from './analysis.js';
function present(v) {
    return typeof v === 'string' && v.trim().length > 0;
}
function normalizeTitle(t) {
    return t.trim().toLowerCase().replace(/\s+/g, ' ');
}
function wordsOf(content, fallback) {
    if (typeof fallback === 'number')
        return fallback;
    if (!content)
        return 0;
    const text = stripHtmlTags(content);
    return text ? text.split(/\s+/).filter((w) => w.length > 0).length : 0;
}
function countMissingAlt(content) {
    if (!content)
        return 0;
    const imgs = content.match(/<img\b[^>]*>/gi) ?? [];
    return imgs.filter((img) => !/\balt\s*=\s*["'][^"']+["']/i.test(img)).length;
}
/** Stable per-issue key so status carries across audit runs. */
export function issueKeyFor(type, identifier) {
    return `${type}:${identifier}`;
}
/**
 * Run the full SEO audit analysis over a set of entities + signals.
 */
export function runSeoAuditAnalysis(entities, signals = {}) {
    const issues = [];
    const pages = [];
    // -- Per-entity content checks -------------------------------------------
    for (const e of entities) {
        const wordCount = wordsOf(e.content, e.wordCount);
        const scoreResult = calculatePageSeoScore({
            title: e.title,
            url: e.url,
            content: e.content,
            metaTitle: e.metaTitle,
            metaDescription: e.metaDescription,
            focusKeyword: e.focusKeyword,
            canonicalUrl: e.canonicalUrl,
            ogTitle: e.ogTitle,
            ogDescription: e.ogDescription,
            ogImage: e.ogImage,
            structuredDataType: e.structuredDataType,
            noindex: e.noindex,
            shouldBeIndexable: e.shouldBeIndexable,
            wordCount,
        });
        pages.push({
            entityType: e.entityType,
            entityId: e.entityId,
            title: e.title,
            url: e.url,
            score: scoreResult.score,
            grade: scoreResult.grade,
            metaTitleSet: present(e.metaTitle),
            metaDescriptionSet: present(e.metaDescription),
        });
        const ctx = {
            entityType: e.entityType,
            entityId: e.entityId,
            entityTitle: e.title,
            url: e.url,
        };
        if (!present(e.metaTitle)) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-meta-title', e.entityId),
                severity: 'warning',
                category: 'content',
                type: 'missing-meta-title',
                title: 'Missing meta title',
                description: 'This page has no meta title. Search engines fall back to the H1 or filename.',
                impact: 'High',
                recommendation: 'Add a 30–60 character meta title containing the target keyword.',
                fixable: true,
                fixActionType: 'edit-metadata',
                relatedEntityIds: [],
            });
        }
        if (!present(e.metaDescription)) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-meta-description', e.entityId),
                severity: 'warning',
                category: 'content',
                type: 'missing-meta-description',
                title: 'Missing meta description',
                description: 'Add a 150–160 character description summarising the page for search engines.',
                impact: 'High',
                recommendation: 'Write a unique meta description that includes the focus keyword.',
                fixable: true,
                fixActionType: 'edit-metadata',
                relatedEntityIds: [],
            });
        }
        if (!present(e.structuredDataType)) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-structured-data', e.entityId),
                severity: 'warning',
                category: 'schema',
                type: 'missing-structured-data',
                title: 'No structured data found',
                description: 'Add Schema.org markup to improve rich-result eligibility.',
                impact: 'Medium',
                recommendation: 'Choose an appropriate schema type (Article, Product, FAQPage, …).',
                fixable: true,
                fixActionType: 'edit-schema',
                relatedEntityIds: [],
            });
        }
        if (!present(e.canonicalUrl)) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-canonical', e.entityId),
                severity: 'info',
                category: 'technical',
                type: 'missing-canonical',
                title: 'Missing canonical tag',
                description: 'Add a canonical tag to prevent duplicate-content issues across URLs.',
                impact: 'Low',
                recommendation: 'Set the canonical URL to this page’s preferred public URL.',
                fixable: true,
                fixActionType: 'edit-canonical',
                relatedEntityIds: [],
            });
        }
        if (!present(e.ogImage)) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-og-image', e.entityId),
                severity: 'info',
                category: 'social',
                type: 'missing-og-image',
                title: 'No OG image set',
                description: 'Pages without an OG image show a generic placeholder when shared.',
                impact: 'Low',
                recommendation: 'Pick or generate an Open Graph image in the social metadata section.',
                fixable: true,
                fixActionType: 'edit-og-image',
                relatedEntityIds: [],
            });
        }
        if ((e.content ?? '').length > 0 && countH1(e.content ?? '') === 0) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-h1', e.entityId),
                severity: 'info',
                category: 'content',
                type: 'missing-h1',
                title: 'Missing H1 heading',
                description: 'Every page should have exactly one H1 that includes the target keyword.',
                impact: 'Low',
                recommendation: 'Add a single descriptive H1 heading to the content.',
                fixable: false,
                fixActionType: 'edit-content',
                relatedEntityIds: [],
            });
        }
        if (wordCount > 0 && wordCount < 300) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('thin-content', e.entityId),
                severity: 'info',
                category: 'content',
                type: 'thin-content',
                title: 'Content below 300 words',
                description: 'Thin pages tend to rank poorly. Consider expanding the copy.',
                impact: 'Low',
                recommendation: 'Add more useful content or an FAQ section.',
                fixable: false,
                fixActionType: 'edit-content',
                relatedEntityIds: [],
            });
        }
        const missingAlt = countMissingAlt(e.content);
        if (missingAlt > 0) {
            issues.push({
                ...ctx,
                issueKey: issueKeyFor('missing-alt-text', e.entityId),
                severity: 'warning',
                category: 'media',
                type: 'missing-alt-text',
                title: 'Images missing alt text',
                description: `${missingAlt} image(s) are missing alt text, hurting accessibility and image search.`,
                impact: 'Medium',
                recommendation: 'Add descriptive alt text to every content image.',
                fixable: true,
                fixActionType: 'edit-content',
                relatedEntityIds: [],
            });
        }
    }
    // -- Duplicate meta titles (cross-entity) --------------------------------
    const titleGroups = new Map();
    for (const e of entities) {
        if (!present(e.metaTitle))
            continue;
        const key = normalizeTitle(e.metaTitle);
        const arr = titleGroups.get(key) ?? [];
        arr.push(e);
        titleGroups.set(key, arr);
    }
    for (const group of titleGroups.values()) {
        if (group.length < 2)
            continue;
        const ids = group.map((g) => g.entityId).sort();
        const urls = group.map((g) => g.url).join(', ');
        issues.push({
            entityType: group[0].entityType,
            entityId: group[0].entityId,
            entityTitle: group[0].title,
            url: urls,
            issueKey: issueKeyFor('duplicate-meta-title', ids.join(',')),
            severity: 'critical',
            category: 'content',
            type: 'duplicate-meta-title',
            title: 'Duplicate meta title detected',
            description: `Each page must have a unique meta title. ${urls} share the same title.`,
            impact: 'High',
            recommendation: 'Differentiate the meta titles so each page targets distinct intent.',
            fixable: true,
            fixActionType: 'edit-metadata',
            relatedEntityIds: ids,
        });
    }
    // -- Broken internal links -----------------------------------------------
    for (const link of signals.brokenLinks ?? []) {
        issues.push({
            entityType: 'page',
            entityId: link.fromEntityId,
            entityTitle: link.fromTitle,
            url: link.fromUrl,
            issueKey: issueKeyFor('broken-internal-link', `${link.fromEntityId}:${link.url}`),
            severity: 'critical',
            category: 'technical',
            type: 'broken-internal-link',
            title: 'Internal link returns 404',
            description: `The link to ${link.url} is broken. Remove or update it to a valid URL.`,
            impact: 'High',
            recommendation: 'Fix the link target or create a redirect for the old URL.',
            fixable: true,
            fixActionType: 'fix-link',
            relatedEntityIds: [],
        });
    }
    // -- Slow pages (external perf source) -----------------------------------
    for (const slow of signals.slowPages ?? []) {
        issues.push({
            entityType: 'page',
            entityId: slow.entityId,
            entityTitle: slow.title,
            url: slow.url,
            issueKey: issueKeyFor('slow-page', slow.entityId),
            severity: 'warning',
            category: 'performance',
            type: 'slow-page',
            title: 'Page load time exceeds 3 seconds',
            description: slow.note ?? 'Optimise images, enable lazy loading, and consider a CDN.',
            impact: 'Medium',
            recommendation: 'Reduce image sizes and JavaScript; see the technical recommendations.',
            fixable: false,
            fixActionType: 'technical-recommendation',
            relatedEntityIds: [],
        });
    }
    // -- Site score (explainable category sub-scores) ------------------------
    const total = entities.length;
    const ratio = (count) => (total === 0 ? 100 : Math.round((count / total) * 100));
    const withMetaTitle = entities.filter((e) => present(e.metaTitle)).length;
    const withMetaDesc = entities.filter((e) => present(e.metaDescription)).length;
    const withH1 = entities.filter((e) => countH1(e.content ?? '') === 1).length;
    const contentScore = Math.round((ratio(withMetaTitle) + ratio(withMetaDesc) + ratio(withH1)) / 3);
    const withCanonical = entities.filter((e) => present(e.canonicalUrl)).length;
    const correctlyIndexed = entities.filter((e) => !(e.shouldBeIndexable !== false && e.noindex === true)).length;
    const crawlBonus = (signals.sitemapEnabled !== false ? 50 : 0) + (signals.robotsEnabled !== false ? 50 : 0);
    const technicalScore = Math.round((ratio(withCanonical) + ratio(correctlyIndexed) + crawlBonus) / 3);
    const brokenCount = (signals.brokenLinks ?? []).length;
    const chainPenalty = ((signals.redirectChains ?? 0) + (signals.redirectLoops ?? 0)) * 8;
    const linkRedirectScore = Math.max(0, 100 - brokenCount * 15 - chainPenalty);
    const withSchema = entities.filter((e) => present(e.structuredDataType)).length;
    const withOgImage = entities.filter((e) => present(e.ogImage)).length;
    const structuredSocialScore = Math.round((ratio(withSchema) + ratio(withOgImage)) / 2);
    const performanceScore = (signals.slowPages ?? []).length > 0
        ? Math.max(0, 70 - (signals.slowPages ?? []).length * 10)
        : 70;
    const siteScore = calculateSiteSeoScore({
        content: contentScore,
        technical: technicalScore,
        linkRedirect: linkRedirectScore,
        structuredSocial: structuredSocialScore,
        performance: performanceScore,
    });
    // -- Counts + quick wins -------------------------------------------------
    const counts = {
        total: issues.length,
        critical: issues.filter((i) => i.severity === 'critical').length,
        warning: issues.filter((i) => i.severity === 'warning').length,
        info: issues.filter((i) => i.severity === 'info').length,
    };
    const quickWins = buildQuickWins(issues);
    return { pages, issues, siteScore, counts, quickWins };
}
const IMPACT_RANK = { High: 3, Medium: 2, Low: 1 };
const SEVERITY_RANK = { critical: 3, warning: 2, info: 1 };
/**
 * Derive priority-ranked quick wins by grouping issues by type and ranking by
 * (impact, severity, count). Never hardcoded — purely a function of detected
 * issues, so the Overview "Quick Wins" card always reflects reality.
 */
export function buildQuickWins(issues) {
    const byType = new Map();
    for (const i of issues) {
        if (!i.fixable)
            continue;
        const arr = byType.get(i.type) ?? [];
        arr.push(i);
        byType.set(i.type, arr);
    }
    const wins = [];
    for (const [type, group] of byType) {
        const first = group[0];
        const count = group.length;
        wins.push({
            id: `qw-${type}`,
            title: quickWinTitle(type, count),
            explanation: quickWinExplanation(type, count),
            impact: first.impact === 'High' ? 'High' : first.impact === 'Medium' ? 'Medium' : 'Low',
            issueType: type,
            fixActionType: first.fixActionType,
            count,
        });
    }
    wins.sort((a, b) => {
        const ai = IMPACT_RANK[a.impact];
        const bi = IMPACT_RANK[b.impact];
        if (ai !== bi)
            return bi - ai;
        return b.count - a.count;
    });
    return wins.slice(0, 5);
}
function quickWinTitle(type, count) {
    switch (type) {
        case 'missing-meta-description':
            return `Add meta descriptions to ${count} page${count === 1 ? '' : 's'}`;
        case 'missing-meta-title':
            return `Add meta titles to ${count} page${count === 1 ? '' : 's'}`;
        case 'duplicate-meta-title':
            return 'Fix duplicate meta title';
        case 'missing-structured-data':
            return `Add structured data to ${count} page${count === 1 ? '' : 's'}`;
        case 'missing-og-image':
            return `Add Open Graph images to ${count} page${count === 1 ? '' : 's'}`;
        case 'missing-canonical':
            return `Add canonical tags to ${count} page${count === 1 ? '' : 's'}`;
        case 'broken-internal-link':
            return `Fix ${count} broken internal link${count === 1 ? '' : 's'}`;
        case 'missing-alt-text':
            return `Add alt text on ${count} page${count === 1 ? '' : 's'}`;
        default:
            return `Resolve ${count} ${type.replace(/-/g, ' ')} issue${count === 1 ? '' : 's'}`;
    }
}
function quickWinExplanation(type, count) {
    switch (type) {
        case 'missing-meta-description':
            return 'Missing meta descriptions reduce click-through rates by up to 30%. This is the single highest-impact fix available.';
        case 'duplicate-meta-title':
            return 'Pages sharing the same meta title may be penalised by Google for duplication. Differentiate them.';
        case 'missing-structured-data':
            return 'Schema markup enables rich results (star ratings, FAQs, breadcrumbs) and improves visibility.';
        case 'missing-og-image':
            return 'Open Graph images significantly improve click-through from social shares.';
        case 'broken-internal-link':
            return 'Broken internal links waste crawl budget and frustrate users. Fix or redirect them.';
        default:
            return `Resolving ${count} ${type.replace(/-/g, ' ')} issue${count === 1 ? '' : 's'} will improve your SEO health.`;
    }
}
export { gradeForSeoScore };
//# sourceMappingURL=audit-engine.js.map