// engine/detectors/hypermedia-detector.ts — SPEC-043
// Detects HTMX, Hotwire/Turbo, Alpine.js, and backend-first patterns.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// CDN URL patterns for HTMX
const HTMX_CDN_PATTERNS = ['unpkg.com/htmx.org', 'cdn.jsdelivr.net/npm/htmx.org'];
// CDN URL patterns for Hotwire/Turbo
const TURBO_CDN_PATTERNS = ['turbo', 'hotwire'];
// CDN URL patterns for Alpine.js
const ALPINE_CDN_PATTERNS = ['alpinejs', 'alpine.js'];
// Template file extensions to scan for CDN patterns
const TEMPLATE_EXTENSIONS = ['html', 'erb', 'jinja2', 'blade.php', 'templ', 'hbs', 'njk'];
// Bundlers that indicate client-side rendering
const BUNDLER_DEPS = ['webpack', 'vite', 'esbuild', 'parcel', '@vitejs/core', 'rollup'];
/**
 * Read and parse package.json from a project path.
 */
async function readPackageJson(projectPath) {
    try {
        const content = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const pkg = JSON.parse(content);
        return {
            deps: pkg.dependencies ?? {},
            devDeps: pkg.devDependencies ?? {},
        };
    }
    catch {
        return null;
    }
}
/**
 * Collect all template files up to depth 4.
 */
async function collectTemplateFiles(projectPath) {
    const pattern = `**/*.{${TEMPLATE_EXTENSIONS.join(',')}}`;
    try {
        const files = await glob(pattern, {
            cwd: projectPath,
            nodir: true,
            maxDepth: 4,
            ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**', 'vendor/**'],
        });
        return files.map((f) => join(projectPath, f));
    }
    catch {
        return [];
    }
}
/**
 * Check if any template file contains a CDN pattern.
 */
async function fileContainsPattern(filePath, patterns) {
    try {
        const content = await readFile(filePath, 'utf-8');
        return patterns.some((p) => content.includes(p));
    }
    catch {
        return false;
    }
}
/**
 * Detect HTMX via package.json dep or CDN in template files.
 * Returns the version if found in package.json.
 */
async function detectHtmx(projectPath, pkgJson) {
    // Check package.json — top-level key must be exactly 'htmx.org'
    const allDeps = { ...(pkgJson?.deps ?? {}), ...(pkgJson?.devDeps ?? {}) };
    if ('htmx.org' in allDeps) {
        return { detected: true, version: allDeps['htmx.org'] };
    }
    // Check CDN usage in template files
    const templateFiles = await collectTemplateFiles(projectPath);
    for (const file of templateFiles) {
        if (await fileContainsPattern(file, HTMX_CDN_PATTERNS)) {
            return { detected: true };
        }
    }
    return { detected: false };
}
/**
 * Infer TurboSubtype by scanning template files for turbo-frame and turbo-stream tags.
 */
async function detectTurboSubtype(templateFiles) {
    let hasFrames = false;
    let hasStreams = false;
    for (const file of templateFiles) {
        try {
            const content = await readFile(file, 'utf-8');
            if (content.includes('<turbo-frame') || content.includes('data-turbo-frame')) {
                hasFrames = true;
            }
            if (content.includes('<turbo-stream') || content.includes('Turbo.renderStreamMessage')) {
                hasStreams = true;
            }
        }
        catch {
            // Skip unreadable files
        }
    }
    if (hasFrames && hasStreams) {
        return 'both';
    }
    else if (hasFrames) {
        return 'frames';
    }
    else if (hasStreams) {
        return 'streams';
    }
    return undefined;
}
/**
 * Detect Hotwire/Turbo via package.json or CDN in template files.
 * Also infers TurboSubtype from tag usage.
 */
async function detectTurbo(projectPath, pkgJson) {
    const allDeps = { ...(pkgJson?.deps ?? {}), ...(pkgJson?.devDeps ?? {}) };
    // Check package.json for @hotwired/turbo
    const hasTurboDep = '@hotwired/turbo' in allDeps;
    // Check Gemfile for turbo-rails
    let hasTurboRails = false;
    try {
        const gemfile = await readFile(join(projectPath, 'Gemfile'), 'utf-8');
        hasTurboRails = gemfile.includes('turbo-rails');
    }
    catch {
        // No Gemfile
    }
    const templateFiles = await collectTemplateFiles(projectPath);
    let hasTurboCDN = false;
    if (!hasTurboDep && !hasTurboRails) {
        for (const file of templateFiles) {
            if (await fileContainsPattern(file, TURBO_CDN_PATTERNS)) {
                hasTurboCDN = true;
                break;
            }
        }
    }
    if (!hasTurboDep && !hasTurboRails && !hasTurboCDN) {
        return { detected: false };
    }
    const subtype = await detectTurboSubtype(templateFiles);
    return { detected: true, subtype };
}
/**
 * Detect Alpine.js via package.json dep or CDN in template files.
 */
async function detectAlpine(projectPath, pkgJson) {
    const allDeps = { ...(pkgJson?.deps ?? {}), ...(pkgJson?.devDeps ?? {}) };
    if ('alpinejs' in allDeps) {
        return true;
    }
    const templateFiles = await collectTemplateFiles(projectPath);
    for (const file of templateFiles) {
        if (await fileContainsPattern(file, ALPINE_CDN_PATTERNS)) {
            return true;
        }
    }
    return false;
}
/**
 * Check if the project uses a JS bundler in devDependencies.
 */
function detectBundler(pkgJson) {
    if (!pkgJson) {
        return false;
    }
    const allDeps = { ...pkgJson.deps, ...pkgJson.devDeps };
    return BUNDLER_DEPS.some((b) => b in allDeps);
}
/**
 * Detect the primary hypermedia framework and rendering mode.
 * Returns a HypermediaDetectionResult with all detected signals.
 */
export async function detectHypermedia(projectPath) {
    const pkgJson = await readPackageJson(projectPath);
    const [htmxResult, turboResult, hasAlpine] = await Promise.all([
        detectHtmx(projectPath, pkgJson),
        detectTurbo(projectPath, pkgJson),
        detectAlpine(projectPath, pkgJson),
    ]);
    const hasBundler = detectBundler(pkgJson);
    // Determine primary framework
    let framework = 'none';
    let version;
    let turboSubtype;
    if (htmxResult.detected) {
        framework = 'htmx';
        version = htmxResult.version;
    }
    else if (turboResult.detected) {
        framework = 'hotwire-turbo';
        turboSubtype = turboResult.subtype;
    }
    // jsComplement: Alpine is only a complement if there is a template engine (server-side context)
    // We leave templateEngine detection to template-engine-detector; here we record the signal
    const jsComplement = hasAlpine ? 'alpine' : 'none';
    // Determine rendering mode
    let renderingMode;
    if (framework !== 'none') {
        renderingMode = hasBundler ? 'hybrid' : 'server-side';
    }
    else if (hasBundler) {
        renderingMode = 'client-side';
    }
    else {
        renderingMode = 'client-side';
    }
    const result = {
        framework,
        renderingMode,
        hasBundler,
        jsComplement,
    };
    if (version !== undefined) {
        result.version = version;
    }
    if (turboSubtype !== undefined) {
        result.turboSubtype = turboSubtype;
    }
    return result;
}
//# sourceMappingURL=hypermedia-detector.js.map