"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectAppValidationWarnings = collectAppValidationWarnings;
exports.runFormatScript = runFormatScript;
exports.validateAppTask = validateAppTask;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const semver_1 = require("semver");
const clientInfo_1 = require("../clientInfo");
const build_1 = require("./build");
const projectValidateCache = new Map();
const projectFormatCache = new Map();
const LEGACY_SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.html']);
const LEGACY_SCAN_IGNORED_DIRS = new Set(['node_modules', '.git', 'dist-test', 'coverage']);
const SDK_SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.astro', '.html']);
const SDK_SCAN_IGNORED_DIRS = new Set([
    'node_modules',
    '.git',
    'dist',
    'build',
    'out',
    '.next',
    '.turbo',
    '.cache',
    '.svelte-kit',
    'coverage',
    'tmp',
    'logs',
    'test',
    'tests',
    '__tests__',
]);
const SDK_ENTRY_HTML_FILENAMES = ['index.html', 'main.html', 'template.html'];
const SDK_ENTRY_SOURCE_BASENAMES = ['main', 'index', 'app', 'bootstrap', 'entry', 'client'];
const SDK_ENTRY_ROOT_DIRS = ['src', 'app', 'pages'];
const SDK_SPECIAL_ENTRY_PATHS = ['app/page', 'app/layout', 'pages/index', 'pages/_app'];
const LEGACY_SDK_SYMBOL_PATTERNS = [
    { symbol: 'selectedAvatarKey', pattern: /\bselectedAvatarKey\b/g },
    { symbol: 'sdk.entities', pattern: /\bsdk\s*\.\s*entities\b/g },
    { symbol: 'sdk.assets.avatar', pattern: /\bsdk\s*\.\s*assets\s*\.\s*avatar\b/g },
    { symbol: 'sdk.assets.block', pattern: /\bsdk\s*\.\s*assets\s*\.\s*block\b/g },
    { symbol: 'loadEntity(', pattern: /\.\s*loadEntity\s*\(/g },
];
const SDK_REFERENCE_PATTERNS = [
    /https:\/\/assets\.playdrop\.ai\/sdk\/playdrop\.js/g,
    /@playdrop\/sdk(?:\/browser)?/g,
];
const SDK_INIT_PATTERNS = [
    /\bwindow\s*\.\s*playdrop\s*\.\s*init\s*\(/g,
    /\bplaydrop\s*\.\s*init\s*\(/g,
    /\bsdk\s*\.\s*initialize\s*\(/g,
];
const SDK_READY_PATTERNS = [
    /\bsdk\s*\??\.\s*host\s*\??\.\s*ready\s*\(/g,
];
const LOCAL_HTML_SCRIPT_PATTERN = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
const LOCAL_IMPORT_PATTERNS = [
    /(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?["']([^"']+)["']/g,
    /import\s*\(\s*["']([^"']+)["']\s*\)/g,
];
const LOCAL_SCRIPT_COMMAND_PATTERN = /(?:^|(?:&&|\|\||;)\s*)(?:node(?:\s+--[^\s"'`;|&]+)*|tsx|ts-node(?:-esm)?|bun|deno\s+run)\s+(["']?)([^"'`\s;&|]+)\1/g;
function ensureWithinProject(task) {
    const relativePath = (0, node_path_1.relative)(task.projectDir, task.filePath);
    const segments = relativePath.split(/[/\\]+/);
    if (!relativePath || relativePath.startsWith('..') || segments.includes('..')) {
        throw new Error(`[apps][validate] Bundle path for ${task.name} must remain inside ${task.projectDir}. Update catalogue entry ${task.cataloguePath}.`);
    }
}
async function ensureValidateScript(task) {
    if (!task.packageJsonPath || !task.hasValidateScript) {
        return;
    }
    if (!projectValidateCache.has(task.projectDir)) {
        const promise = (0, build_1.runNpmScript)(task.projectDir, 'validate');
        projectValidateCache.set(task.projectDir, promise);
    }
    await projectValidateCache.get(task.projectDir);
}
function scanForLegacySdkSymbols(task) {
    const findings = [];
    const scanFile = (filePath) => {
        const extension = (0, node_path_1.extname)(filePath).toLowerCase();
        if (!LEGACY_SCAN_EXTENSIONS.has(extension)) {
            return;
        }
        const source = (0, node_fs_1.readFileSync)(filePath, 'utf8');
        for (const { symbol, pattern } of LEGACY_SDK_SYMBOL_PATTERNS) {
            pattern.lastIndex = 0;
            const hit = pattern.exec(source);
            if (!hit) {
                continue;
            }
            const index = typeof hit.index === 'number' ? hit.index : 0;
            const line = source.slice(0, index).split('\n').length;
            findings.push({
                file: (0, node_path_1.relative)(task.projectDir, filePath),
                line,
                symbol,
            });
        }
    };
    const walk = (directory) => {
        const entries = (0, node_fs_1.readdirSync)(directory, { withFileTypes: true });
        for (const entry of entries) {
            if (LEGACY_SCAN_IGNORED_DIRS.has(entry.name)) {
                continue;
            }
            const nextPath = (0, node_path_1.join)(directory, entry.name);
            if (entry.isDirectory()) {
                walk(nextPath);
            }
            else if (entry.isFile()) {
                scanFile(nextPath);
            }
        }
    };
    walk(task.projectDir);
    if (findings.length > 0) {
        const details = findings
            .slice(0, 10)
            .map((entry) => `${entry.file}:${entry.line} (${entry.symbol})`)
            .join(', ');
        const suffix = findings.length > 10 ? ` (+${findings.length - 10} more)` : '';
        throw new Error(`[apps][validate] legacy SDK symbols detected for ${task.name}: ${details}${suffix}. Remove entity-era APIs before publishing.`);
    }
}
function stripSpecifierDecorators(specifier) {
    return specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
}
function escapeRegexLiteral(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function detectSdkInitInSource(source) {
    if (SDK_INIT_PATTERNS.some((pattern) => {
        pattern.lastIndex = 0;
        return pattern.test(source);
    })) {
        return true;
    }
    const playdropAliases = new Set();
    const aliasAssignmentPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;\n]*\bplaydrop\b[^;\n]*/g;
    let aliasMatch;
    while ((aliasMatch = aliasAssignmentPattern.exec(source)) !== null) {
        const alias = aliasMatch[1]?.trim();
        if (alias) {
            playdropAliases.add(alias);
        }
    }
    for (const alias of playdropAliases) {
        const aliasInitPattern = new RegExp(`\\b${escapeRegexLiteral(alias)}\\s*(?:\\.\\s*|\\?\\.\\s*)init\\s*\\(`, 'g');
        if (aliasInitPattern.test(source)) {
            return true;
        }
    }
    return false;
}
function safeReadFile(filePath) {
    try {
        return (0, node_fs_1.readFileSync)(filePath, 'utf8');
    }
    catch {
        return null;
    }
}
function isIgnoredSdkScanDirectory(name) {
    return SDK_SCAN_IGNORED_DIRS.has(name);
}
function resolveSdkModulePath(basePath) {
    const tryFile = (candidate) => {
        if (!(0, node_fs_1.existsSync)(candidate)) {
            return null;
        }
        try {
            return (0, node_fs_1.statSync)(candidate).isFile() ? candidate : null;
        }
        catch {
            return null;
        }
    };
    const direct = tryFile(basePath);
    if (direct) {
        return direct;
    }
    if (!(0, node_path_1.extname)(basePath)) {
        for (const extension of SDK_SCAN_EXTENSIONS) {
            const withExtension = tryFile(`${basePath}${extension}`);
            if (withExtension) {
                return withExtension;
            }
        }
    }
    if ((0, node_fs_1.existsSync)(basePath)) {
        try {
            if ((0, node_fs_1.statSync)(basePath).isDirectory()) {
                for (const extension of SDK_SCAN_EXTENSIONS) {
                    const indexCandidate = tryFile((0, node_path_1.join)(basePath, `index${extension}`));
                    if (indexCandidate) {
                        return indexCandidate;
                    }
                }
            }
        }
        catch {
            return null;
        }
    }
    return null;
}
function resolveLocalSpecifier(task, sourceFilePath, specifier) {
    const normalized = stripSpecifierDecorators(specifier);
    if (!normalized) {
        return null;
    }
    if (normalized.startsWith('/')) {
        return resolveSdkModulePath((0, node_path_1.resolve)(task.projectDir, `.${normalized}`));
    }
    if (!normalized.startsWith('.')) {
        return null;
    }
    return resolveSdkModulePath((0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), normalized));
}
function extractLocalHtmlScriptFiles(task, filePath, source) {
    const matches = [];
    LOCAL_HTML_SCRIPT_PATTERN.lastIndex = 0;
    let match = null;
    while ((match = LOCAL_HTML_SCRIPT_PATTERN.exec(source)) !== null) {
        const specifier = match[1];
        if (!specifier) {
            continue;
        }
        const resolved = resolveLocalSpecifier(task, filePath, specifier);
        if (resolved) {
            matches.push(resolved);
        }
    }
    return matches;
}
function extractLocalImportedFiles(task, filePath, source) {
    const matches = [];
    for (const pattern of LOCAL_IMPORT_PATTERNS) {
        pattern.lastIndex = 0;
        let match = null;
        while ((match = pattern.exec(source)) !== null) {
            const specifier = match[1];
            if (!specifier) {
                continue;
            }
            const resolved = resolveLocalSpecifier(task, filePath, specifier);
            if (resolved) {
                matches.push(resolved);
            }
        }
    }
    return matches;
}
function addFileIfPresent(target, filePath) {
    if (!(0, node_fs_1.existsSync)(filePath)) {
        return;
    }
    try {
        if ((0, node_fs_1.statSync)(filePath).isFile()) {
            target.add(filePath);
        }
    }
    catch {
        // Ignore unreadable candidates and continue scanning the rest.
    }
}
function extractLocalScriptCommandFiles(task) {
    if (!task.packageJsonPath) {
        return [];
    }
    const rawPackageJson = safeReadFile(task.packageJsonPath);
    if (!rawPackageJson) {
        return [];
    }
    let packageJson;
    try {
        packageJson = JSON.parse(rawPackageJson);
    }
    catch {
        return [];
    }
    const scripts = packageJson?.scripts;
    if (!scripts || typeof scripts !== 'object') {
        return [];
    }
    const matches = new Set();
    Object.values(scripts).forEach((scriptValue) => {
        if (typeof scriptValue !== 'string') {
            return;
        }
        LOCAL_SCRIPT_COMMAND_PATTERN.lastIndex = 0;
        let match = null;
        while ((match = LOCAL_SCRIPT_COMMAND_PATTERN.exec(scriptValue)) !== null) {
            const specifier = stripSpecifierDecorators(match[2] ?? '');
            if (!specifier || specifier.startsWith('-')) {
                continue;
            }
            const resolvedPath = specifier.startsWith('/')
                ? resolveSdkModulePath((0, node_path_1.resolve)(specifier))
                : resolveSdkModulePath((0, node_path_1.resolve)(task.projectDir, specifier));
            if (resolvedPath) {
                matches.add(resolvedPath);
            }
        }
    });
    return [...matches];
}
function collectSourceSeedFiles(task) {
    const seeds = new Set();
    const entryRelativePath = (0, node_path_1.relative)(task.projectDir, task.filePath);
    const entrySegments = entryRelativePath.split(/[/\\]+/);
    const entryCrossesIgnoredSourceDirectory = entrySegments.some((segment) => isIgnoredSdkScanDirectory(segment));
    if (entryRelativePath && !entryRelativePath.startsWith('..') && !entryCrossesIgnoredSourceDirectory) {
        addFileIfPresent(seeds, task.filePath);
    }
    SDK_ENTRY_HTML_FILENAMES.forEach((filename) => {
        addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, filename));
        addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, 'public', filename));
    });
    const sourceExtensions = [...SDK_SCAN_EXTENSIONS].filter((extension) => extension !== '.html');
    const addEntryBasenames = (directory) => {
        SDK_ENTRY_SOURCE_BASENAMES.forEach((basename) => {
            sourceExtensions.forEach((extension) => {
                addFileIfPresent(seeds, (0, node_path_1.join)(directory, `${basename}${extension}`));
            });
        });
    };
    addEntryBasenames(task.projectDir);
    SDK_ENTRY_ROOT_DIRS.forEach((directory) => {
        addEntryBasenames((0, node_path_1.join)(task.projectDir, directory));
    });
    SDK_SPECIAL_ENTRY_PATHS.forEach((relativePath) => {
        sourceExtensions.forEach((extension) => {
            addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, `${relativePath}${extension}`));
        });
    });
    extractLocalScriptCommandFiles(task).forEach((filePath) => {
        const relativePath = (0, node_path_1.relative)(task.projectDir, filePath);
        const segments = relativePath.split(/[/\\]+/);
        const crossesIgnoredSourceDirectory = segments.some((segment) => isIgnoredSdkScanDirectory(segment));
        if (!relativePath || relativePath.startsWith('..') || crossesIgnoredSourceDirectory) {
            return;
        }
        seeds.add(filePath);
    });
    return [...seeds];
}
function collectBundleSeedFiles(task) {
    const seeds = new Set();
    addFileIfPresent(seeds, task.filePath);
    return [...seeds];
}
function collectSdkDetectionFiles(task, mode) {
    const queue = mode === 'bundle' ? collectBundleSeedFiles(task) : collectSourceSeedFiles(task);
    const queued = new Set(queue);
    const visited = new Set();
    if (task.packageJsonPath) {
        visited.add(task.packageJsonPath);
    }
    while (queue.length > 0) {
        const filePath = queue.shift();
        if (!filePath || visited.has(filePath)) {
            continue;
        }
        if (!(0, node_fs_1.existsSync)(filePath)) {
            continue;
        }
        let isFile = false;
        try {
            isFile = (0, node_fs_1.statSync)(filePath).isFile();
        }
        catch {
            isFile = false;
        }
        if (!isFile) {
            continue;
        }
        const extension = (0, node_path_1.extname)(filePath).toLowerCase();
        if (extension && !SDK_SCAN_EXTENSIONS.has(extension)) {
            continue;
        }
        visited.add(filePath);
        const source = safeReadFile(filePath);
        if (!source) {
            continue;
        }
        const localReferences = extension === '.html'
            ? extractLocalHtmlScriptFiles(task, filePath, source)
            : extractLocalImportedFiles(task, filePath, source);
        localReferences.forEach((resolvedPath) => {
            const relativePath = (0, node_path_1.relative)(task.projectDir, resolvedPath);
            const segments = relativePath.split(/[/\\]+/);
            const crossesIgnoredSourceDirectory = mode === 'source'
                && segments.some((segment) => isIgnoredSdkScanDirectory(segment));
            if (!relativePath || relativePath.startsWith('..') || crossesIgnoredSourceDirectory) {
                return;
            }
            if (!queued.has(resolvedPath) && !visited.has(resolvedPath)) {
                queue.push(resolvedPath);
                queued.add(resolvedPath);
            }
        });
    }
    return [...visited];
}
function detectSdkUsage(task, mode) {
    let hasSdkReference = false;
    let hasSdkInit = false;
    let hasSdkReady = false;
    const files = collectSdkDetectionFiles(task, mode);
    for (const filePath of files) {
        if (hasSdkReference && hasSdkInit && hasSdkReady) {
            break;
        }
        const source = safeReadFile(filePath);
        if (!source) {
            continue;
        }
        if (!hasSdkReference) {
            hasSdkReference = SDK_REFERENCE_PATTERNS.some((pattern) => {
                pattern.lastIndex = 0;
                return pattern.test(source);
            });
        }
        if (!hasSdkInit) {
            hasSdkInit = detectSdkInitInSource(source);
        }
        if (!hasSdkReady) {
            hasSdkReady = SDK_READY_PATTERNS.some((pattern) => {
                pattern.lastIndex = 0;
                return pattern.test(source);
            });
        }
    }
    return { hasSdkReference, hasSdkInit, hasSdkReady };
}
function collectOutdatedSdkVersionWarnings(task) {
    if (!task.packageJsonPath) {
        return [];
    }
    const rawPackageJson = safeReadFile(task.packageJsonPath);
    if (!rawPackageJson) {
        return [];
    }
    let packageJson;
    try {
        packageJson = JSON.parse(rawPackageJson);
    }
    catch {
        return [];
    }
    const cliVersion = (0, clientInfo_1.getCliVersion)();
    if (!(0, semver_1.valid)(cliVersion)) {
        return [];
    }
    const sdkTypesDependency = typeof packageJson?.devDependencies?.['playdrop-sdk-types'] === 'string'
        ? packageJson.devDependencies['playdrop-sdk-types']
        : typeof packageJson?.dependencies?.['playdrop-sdk-types'] === 'string'
            ? packageJson.dependencies['playdrop-sdk-types']
            : '';
    const sdkTypesVersion = typeof packageJson?.playdrop?.sdkTypesVersion === 'string'
        ? packageJson.playdrop.sdkTypesVersion.trim()
        : '';
    const usesVendoredSdkTypes = sdkTypesDependency.includes('playdrop-sdk-types')
        && (sdkTypesDependency.startsWith('file:') || sdkTypesDependency.includes('.tgz'));
    if (!usesVendoredSdkTypes
        || !(0, semver_1.valid)(sdkTypesVersion)
        || !(0, semver_1.lt)(sdkTypesVersion, cliVersion)) {
        return [];
    }
    return [
        `[apps][validate] ${task.name} vendors playdrop-sdk-types.tgz at ${sdkTypesVersion}, which is older than this CLI version ${cliVersion}. Refresh vendor/playdrop-sdk-types.tgz and package.json playdrop.sdkTypesVersion before upload.`,
    ];
}
function collectAppValidationWarnings(task, mode = 'source') {
    const isExternal = task.hostingMode === 'EXTERNAL' || !!task.externalUrl;
    if (isExternal) {
        return [];
    }
    const primaryDetection = detectSdkUsage(task, mode);
    const sourceDetection = mode === 'bundle'
        ? detectSdkUsage(task, 'source')
        : primaryDetection;
    const hasSdkReference = primaryDetection.hasSdkReference || sourceDetection.hasSdkReference;
    const hasSdkInit = primaryDetection.hasSdkInit || sourceDetection.hasSdkInit;
    const hasSdkReady = primaryDetection.hasSdkReady || sourceDetection.hasSdkReady;
    const warnings = [];
    if (!hasSdkReference) {
        warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find https://assets.playdrop.ai/sdk/playdrop.js or @playdrop/sdk in the app files, so it might not work once uploaded.`);
    }
    if (!hasSdkInit) {
        warnings.push(`[apps][validate] Could not detect Playdrop SDK initialization for ${task.name}. We could not find playdrop.init() in the app files, so it might not work once uploaded.`);
    }
    if (!hasSdkReady) {
        warnings.push(`[apps][validate] Could not detect Playdrop host readiness for ${task.name}. We could not find sdk.host.ready() in the app files, so the app will fail to start correctly inside Playdrop.`);
    }
    return [...warnings, ...collectOutdatedSdkVersionWarnings(task)];
}
async function runFormatScript(task) {
    if (!task.packageJsonPath || !task.hasFormatScript) {
        return false;
    }
    if (!projectFormatCache.has(task.projectDir)) {
        projectFormatCache.set(task.projectDir, (0, build_1.runNpmScript)(task.projectDir, 'format'));
    }
    await projectFormatCache.get(task.projectDir);
    return true;
}
async function validateAppTask(task) {
    if (!task.name) {
        throw new Error('[apps][validate] App entry is missing a name.');
    }
    // External apps don't need file validation - they're hosted elsewhere
    const isExternal = task.hostingMode === 'EXTERNAL' || !!task.externalUrl;
    if (isExternal) {
        if (!task.externalUrl) {
            throw new Error(`[apps][validate] ${task.name} is marked as EXTERNAL but missing externalUrl.`);
        }
        if (!task.externalUrl.startsWith('https://')) {
            throw new Error(`[apps][validate] ${task.name} externalUrl must be HTTPS.`);
        }
        // External apps pass validation - no local files needed
        return;
    }
    if (!task.relativeFile) {
        throw new Error(`[apps][validate] ${task.name} is missing a "file" field in ${task.cataloguePath}.`);
    }
    if (task.isTypescriptProject && !task.hasValidateScript) {
        throw new Error(`[apps][validate] ${task.name} is missing a required "validate" script in package.json. Define a validate command (for example "validate": "npm run build").`);
    }
    ensureWithinProject(task);
    if (!task.isTypescriptProject) {
        if (!(0, node_fs_1.existsSync)(task.filePath)) {
            throw new Error(`[apps][validate] Bundle not found for ${task.name}: ${task.relativeFile}. Run the app build locally before uploading.`);
        }
        const stats = (0, node_fs_1.statSync)(task.filePath);
        if (!stats.isFile()) {
            throw new Error(`[apps][validate] Expected a file at ${task.relativeFile} for ${task.name}. Update the catalogue entry ${task.cataloguePath}.`);
        }
    }
    if (task.hasValidateScript) {
        try {
            await ensureValidateScript(task);
        }
        catch (error) {
            const message = error?.message || String(error);
            throw new Error(`[apps][validate] npm run validate failed for ${task.name}: ${message}`);
        }
    }
    scanForLegacySdkSymbols(task);
}
