/**
 * Media SEO scoring.
 *
 * Pure, dependency-free functions that derive an SEO score and a list of
 * actionable issues from a media asset's metadata. The score is intentionally
 * *derived* (never persisted) so it can never drift out of sync with the
 * underlying metadata — every read recomputes it from the current fields.
 *
 * The weights below are the contract the admin SEO drawer, the SEO Audit
 * table, and the `validateMediaSEO` MCP tool all share. Changing a weight is a
 * behavioural change to every surface that shows a media score, so keep the
 * unit tests in `__tests__/seo.test.ts` as the source of truth.
 */
const DEFAULT_MAX_IMAGE_BYTES = 2 * 1024 * 1024;
/** SEO weights. Must sum to 100. */
export const MEDIA_SEO_WEIGHTS = {
    altText: 30,
    title: 20,
    description: 20,
    caption: 10,
    descriptiveFileName: 10,
    focusKeyword: 10,
};
/**
 * Filenames that carry no semantic value. These come straight off cameras,
 * screenshot tools, and "save as" defaults, so they neither help users nor
 * search engines. Matched case-insensitively against the name without its
 * extension.
 */
const GENERIC_FILENAME_PATTERNS = [
    /^img[-_ ]?\d+$/i, // IMG_4892
    /^dsc[-_ ]?\d+$/i, // DSC_0001
    /^dscf\d+$/i, // DSCF1234 (Fujifilm)
    /^p\d{6,}$/i, // P1010101
    /^image[-_ ]?\d*$/i, // image, image1
    /^photo[-_ ]?\d*$/i, // photo, photo-2
    /^pic[-_ ]?\d*$/i,
    /^untitled[-_ ]?\d*$/i,
    /^screen[-_ ]?shot.*$/i, // Screen Shot 2024-...
    /^screenshot.*$/i,
    /^download[-_ ]?\d*$/i,
    /^file[-_ ]?\d*$/i,
    /^new[-_ ]?\d+$/i,
    /^\d+$/i, // 12345
    /^[a-f0-9]{8,}$/i, // hex blobs / hashes
];
/** True when `value` is a non-empty string after trimming. */
function present(value) {
    return typeof value === 'string' && value.trim().length > 0;
}
/** Strip a single trailing extension from a filename. */
function stripExtension(name) {
    const dot = name.lastIndexOf('.');
    return dot > 0 ? name.slice(0, dot) : name;
}
/** Map a MIME type to a coarse {@link MediaKind}. */
export function mediaKind(mimeType) {
    const mime = (mimeType ?? '').toLowerCase();
    if (mime === 'image/svg+xml')
        return 'svg';
    if (mime.startsWith('image/'))
        return 'image';
    if (mime.startsWith('video/'))
        return 'video';
    if (mime.startsWith('audio/'))
        return 'audio';
    if (mime === 'application/pdf')
        return 'pdf';
    return 'other';
}
/**
 * Whether a filename is generic/non-descriptive (e.g. `IMG_4892.jpg`). A
 * descriptive name needs at least one alphabetic run of 3+ chars and must not
 * match a known camera/screenshot/default pattern.
 */
export function isGenericFilename(fileName) {
    if (!present(fileName))
        return true;
    const base = stripExtension(String(fileName).trim());
    if (base.length === 0)
        return true;
    if (GENERIC_FILENAME_PATTERNS.some((re) => re.test(base)))
        return true;
    // Require some real word content: at least three consecutive letters.
    if (!/[a-z]{3,}/i.test(base))
        return true;
    return false;
}
/** Map a numeric score to a grade bucket. */
export function gradeForScore(score) {
    if (score >= 80)
        return 'good';
    if (score >= 60)
        return 'needs-work';
    if (score >= 1)
        return 'poor';
    return 'critical';
}
/**
 * Compute the SEO score (0–100) and issue list for a media asset.
 *
 * Scoring (weights in {@link MEDIA_SEO_WEIGHTS}, sum 100):
 *   alt text +30, title +20, description +20, caption +10,
 *   descriptive filename +10, focus keyword +10.
 *
 * Issues are additive context and do not subtract from the score directly —
 * e.g. an oversized image still scores on metadata, but surfaces an
 * `oversized-image` issue operators can act on.
 */
export function calculateMediaSeoScore(input, options = {}) {
    const kind = mediaKind(input.mimeType);
    const maxImageBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
    const hasAlt = present(input.altText);
    const hasTitle = present(input.title);
    const hasDescription = present(input.description);
    const hasCaption = present(input.caption);
    const hasFocusKeyword = present(input.focusKeyword);
    const genericName = isGenericFilename(input.fileName);
    let score = 0;
    if (hasAlt)
        score += MEDIA_SEO_WEIGHTS.altText;
    if (hasTitle)
        score += MEDIA_SEO_WEIGHTS.title;
    if (hasDescription)
        score += MEDIA_SEO_WEIGHTS.description;
    if (hasCaption)
        score += MEDIA_SEO_WEIGHTS.caption;
    if (!genericName)
        score += MEDIA_SEO_WEIGHTS.descriptiveFileName;
    if (hasFocusKeyword)
        score += MEDIA_SEO_WEIGHTS.focusKeyword;
    score = Math.max(0, Math.min(100, Math.round(score)));
    const issues = [];
    // Alt text matters most for images/SVGs (screen readers + image search).
    if (!hasAlt && (kind === 'image' || kind === 'svg')) {
        issues.push({
            type: 'missing-alt',
            severity: 'high',
            message: 'Add alt text describing this image for accessibility and image search.',
            field: 'altText',
            fixableWithAI: true,
        });
    }
    if (!hasTitle) {
        issues.push({
            type: 'missing-title',
            severity: 'medium',
            message: 'Add a title to give this asset a clear, human-readable label.',
            field: 'title',
            fixableWithAI: true,
        });
    }
    if (!hasDescription) {
        issues.push({
            type: 'missing-description',
            severity: 'medium',
            message: 'Add a description to provide richer context for search and reuse.',
            field: 'description',
            fixableWithAI: true,
        });
    }
    if (genericName) {
        issues.push({
            type: 'generic-filename',
            severity: 'low',
            message: 'Rename to a descriptive filename — generic names like IMG_4892 hurt SEO.',
            field: 'fileName',
            fixableWithAI: true,
        });
    }
    if (kind === 'image' && typeof input.sizeBytes === 'number' && input.sizeBytes > maxImageBytes) {
        issues.push({
            type: 'oversized-image',
            severity: 'medium',
            message: 'Image is large — optimise it to improve page load performance.',
            field: 'sizeBytes',
            fixableWithAI: false,
        });
    }
    // Video accessibility: captions/transcript. We track this as caption today.
    if (kind === 'video' && !hasCaption) {
        issues.push({
            type: 'missing-video-caption',
            severity: 'medium',
            message: 'Add a caption or transcript reference for this video.',
            field: 'caption',
            fixableWithAI: false,
        });
    }
    if (kind === 'pdf' && !hasTitle) {
        // Already emitted missing-title above; no duplicate. Kept for clarity that
        // PDFs are intentionally covered by the generic title check.
    }
    // Surface the overall verdict as an issue when the score is weak, so the
    // audit table can show a single "poor score" row chip.
    const grade = gradeForScore(score);
    if (grade === 'critical') {
        issues.push({
            type: 'critical-score',
            severity: 'critical',
            message: 'No SEO metadata set. This asset is invisible to search and assistive tech.',
            fixableWithAI: true,
        });
    }
    else if (grade === 'poor') {
        issues.push({
            type: 'poor-score',
            severity: 'high',
            message: 'Low SEO score — fill in the missing metadata fields below.',
            fixableWithAI: true,
        });
    }
    return { score, grade, issues };
}
//# sourceMappingURL=seo.js.map