"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAssetSpecProject = createAssetSpecProject;
exports.create = create;
const promises_1 = __importDefault(require("node:readline/promises"));
const node_process_1 = require("node:process");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_child_process_1 = require("node:child_process");
const fflate_1 = require("fflate");
const types_1 = require("@playdrop/types");
const commandContext_1 = require("../commandContext");
const http_1 = require("../http");
const messages_1 = require("../messages");
const init_1 = require("./init");
const app_1 = require("@playdrop/types/app");
const catalogue_utils_1 = require("../catalogue-utils");
const catalogue_1 = require("../catalogue");
const clientInfo_1 = require("../clientInfo");
const CATALOGUE_FILENAME = 'catalogue.json';
const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
const ALLOWED_CATALOGUE_TOP_LEVEL_KEYS = new Set(['apps', 'assetSpecs', 'assets', 'assetPacks']);
function buildArchiveDownloadHeaders(url, apiBase, token) {
    const targetUrl = new URL(url, apiBase);
    const apiOrigin = new URL(apiBase).origin;
    if (targetUrl.origin !== apiOrigin) {
        return undefined;
    }
    const headers = new Headers((0, clientInfo_1.createClientHeaders)());
    if (token.trim().length > 0) {
        headers.set('authorization', `Bearer ${token}`);
    }
    return headers;
}
async function downloadArchive(url, apiBase, token) {
    const headers = buildArchiveDownloadHeaders(url, apiBase, token);
    const response = await fetch(url, headers ? { headers } : undefined);
    if (!response.ok) {
        throw new Error(`Failed to download project archive (${response.status})`);
    }
    const buffer = await response.arrayBuffer();
    return new Uint8Array(buffer);
}
function resolveArchiveDownloadUrl(url, apiBase) {
    return new URL(url, apiBase).toString();
}
function sanitizeArchivePath(targetDir, relativePath) {
    const normalized = relativePath.replace(/\\/g, '/');
    if (!normalized || normalized.includes('..')) {
        return null;
    }
    const absolute = (0, node_path_1.resolve)(targetDir, normalized);
    if (!absolute.startsWith((0, node_path_1.resolve)(targetDir))) {
        return null;
    }
    return absolute;
}
function trimArchivePrefix(entries) {
    const filtered = entries
        .map((name) => name.replace(/\\/g, '/'))
        .filter((name) => name.length > 0 && !name.startsWith('__MACOSX/'));
    if (filtered.length === 0) {
        return '';
    }
    const firstSegments = new Set(filtered.map((name) => name.split('/')[0]));
    if (firstSegments.size === 1) {
        const [segment] = Array.from(firstSegments);
        if (segment && !segment.includes('.')) {
            return `${segment}/`;
        }
    }
    return '';
}
function extractProjectArchive(buffer, targetDir) {
    const files = (0, fflate_1.unzipSync)(buffer);
    const names = Object.keys(files);
    const prefix = trimArchivePrefix(names);
    let manifest = null;
    for (const originalName of names) {
        let name = originalName.replace(/\\/g, '/');
        if (!name || name.startsWith('__MACOSX/')) {
            continue;
        }
        if (prefix && name.startsWith(prefix)) {
            name = name.slice(prefix.length);
        }
        if (!name) {
            continue;
        }
        const isDirectory = name.endsWith('/');
        const destination = sanitizeArchivePath(targetDir, name);
        if (!destination) {
            continue;
        }
        if (isDirectory) {
            (0, node_fs_1.mkdirSync)(destination, { recursive: true });
            continue;
        }
        (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true });
        const data = files[originalName];
        const bufferData = Buffer.from(data);
        (0, node_fs_1.writeFileSync)(destination, bufferData);
        if (name === 'playdrop.project.json') {
            try {
                manifest = JSON.parse(Buffer.from(data).toString('utf8'));
            }
            catch {
                manifest = null;
            }
        }
    }
    const entryPoint = manifest?.entryPoint ? manifest.entryPoint.trim() : undefined;
    return { manifest, entryPoint: entryPoint || null };
}
function findFirstHtmlFile(projectDir) {
    const stack = [projectDir];
    const visited = new Set();
    while (stack.length > 0) {
        const current = stack.pop();
        if (visited.has(current)) {
            continue;
        }
        visited.add(current);
        let entries = [];
        try {
            entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            const absolute = (0, node_path_1.join)(current, entry.name);
            if (entry.isDirectory()) {
                if (entry.name === 'node_modules' || entry.name === '.git') {
                    continue;
                }
                stack.push(absolute);
                continue;
            }
            if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
                return absolute;
            }
        }
    }
    return null;
}
async function promptInstallDependencies(projectDir) {
    if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
        return false;
    }
    const label = (0, node_path_1.relative)(process.cwd(), projectDir) || projectDir;
    const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
    const answer = await rl.question(`Run "npm install" inside ${label}? (y/N) `);
    rl.close();
    return /^y(es)?$/i.test(answer.trim());
}
function runNpmInstall(projectDir) {
    const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
    const result = (0, node_child_process_1.spawnSync)(npmCommand, ['install'], {
        cwd: projectDir,
        stdio: 'inherit',
    });
    return result.status === 0;
}
function findUnsupportedCatalogueTopLevelKey(parsed) {
    for (const key of Object.keys(parsed)) {
        if (!ALLOWED_CATALOGUE_TOP_LEVEL_KEYS.has(key) && key !== 'games') {
            return key;
        }
    }
    return null;
}
function extractEntryPointFromMetadata(metadata) {
    if (!metadata) {
        return null;
    }
    const candidate = metadata.entryPoint;
    if (typeof candidate === 'string' && candidate.trim().length > 0) {
        return candidate.trim();
    }
    if (typeof metadata.file === 'string' && metadata.file.trim().length > 0) {
        return metadata.file.trim();
    }
    return null;
}
const DEFAULT_SURFACE_TARGETS = {
    desktop: app_1.DEFAULT_APP_SURFACE_TARGETS.includes('DESKTOP'),
    mobileLandscape: app_1.DEFAULT_APP_SURFACE_TARGETS.includes('MOBILE_LANDSCAPE'),
    mobilePortrait: app_1.DEFAULT_APP_SURFACE_TARGETS.includes('MOBILE_PORTRAIT'),
};
const DEFAULT_APP_VERSION = '1.0.0';
const DEFAULT_ASSET_SPEC_VERSION = '1.0.0';
function makeDefaultAppEntry(name, filePath) {
    return {
        name,
        file: filePath,
        version: DEFAULT_APP_VERSION,
        displayName: name,
        description: '',
        type: app_1.DEFAULT_APP_TYPE,
        surfaceTargets: { ...DEFAULT_SURFACE_TARGETS },
    };
}
function makeDefaultAssetSpecEntry(name) {
    const basePath = `asset-specs/${name}`;
    return {
        name,
        version: DEFAULT_ASSET_SPEC_VERSION,
        displayName: name,
        description: '',
        symbol: `${basePath}/symbol.svg`,
        contract: `${basePath}/contract.json`,
        documentation: `${basePath}/README.md`,
        visibility: 'PUBLIC',
    };
}
function buildCatalogueEntry(name, metadata, sourceInfo, relativeFilePath) {
    const entry = makeDefaultAppEntry(name, relativeFilePath);
    if (!metadata) {
        if (sourceInfo.templateSlug) {
            entry.templateSource = sourceInfo.templateSlug;
        }
        if (sourceInfo.remixRef) {
            entry.remix = sourceInfo.remixRef;
        }
        return entry;
    }
    if (typeof metadata.displayName === 'string' && metadata.displayName.trim().length > 0) {
        entry.displayName = metadata.displayName.trim();
    }
    if (typeof metadata.description === 'string') {
        entry.description = metadata.description;
    }
    if (typeof metadata.type === 'string' && metadata.type.trim().length > 0) {
        const parsedType = (0, app_1.parseAppType)(metadata.type.trim());
        if (parsedType) {
            entry.type = parsedType;
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'emoji')) {
        const emoji = metadata.emoji;
        if (emoji === null || (typeof emoji === 'string' && emoji.trim().length === 0)) {
            entry.emoji = null;
        }
        else if (typeof emoji === 'string') {
            entry.emoji = emoji;
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'color')) {
        const color = metadata.color;
        if (color === null || (typeof color === 'string' && color.trim().length === 0)) {
            entry.color = null;
        }
        else if (typeof color === 'string') {
            entry.color = color;
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'file')) {
        const providedFile = metadata.file;
        if (typeof providedFile === 'string' && providedFile.trim().length > 0) {
            entry.file = providedFile.trim();
        }
        else if (providedFile === null) {
            delete entry.file;
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'version')) {
        const providedVersion = metadata.version;
        if (typeof providedVersion === 'string' && providedVersion.trim().length > 0) {
            entry.version = providedVersion.trim();
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'surfaceTargets')) {
        try {
            const parsed = (0, catalogue_utils_1.parseSurfaceTargets)(metadata.surfaceTargets);
            entry.surfaceTargets = { ...parsed.map };
        }
        catch (error) {
            throw new Error(`Invalid surfaceTargets for ${name}: ${error?.message || 'unknown error'}`);
        }
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'templateSource')) {
        const templateSource = metadata.templateSource;
        if (templateSource === null || typeof templateSource === 'string') {
            entry.templateSource = templateSource;
        }
    }
    else if (sourceInfo.templateSlug) {
        entry.templateSource = sourceInfo.templateSlug;
    }
    if (Object.prototype.hasOwnProperty.call(metadata, 'remix')) {
        const remix = metadata.remix;
        if (remix === null || typeof remix === 'string') {
            entry.remix = remix;
        }
    }
    else if (sourceInfo.remixRef) {
        entry.remix = sourceInfo.remixRef;
    }
    return entry;
}
function readCatalogue(path) {
    try {
        const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
            return null;
        }
        return parsed;
    }
    catch {
        return null;
    }
}
function hasNestedCatalogueFiles(rootDir) {
    const rootCataloguePath = (0, node_path_1.join)(rootDir, CATALOGUE_FILENAME);
    const stack = [rootDir];
    while (stack.length > 0) {
        const current = stack.pop();
        const entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
        for (const entry of entries) {
            if (entry.name.startsWith('.') || entry.name === 'node_modules') {
                continue;
            }
            const absolutePath = (0, node_path_1.join)(current, entry.name);
            if (entry.isDirectory()) {
                stack.push(absolutePath);
                continue;
            }
            if (entry.isFile() && entry.name === CATALOGUE_FILENAME && absolutePath !== rootCataloguePath) {
                return true;
            }
        }
    }
    return false;
}
function ensureCatalogueEntry(name, metadata, cataloguePath, sourceInfo, htmlFilePath) {
    const path = (0, node_path_1.resolve)(cataloguePath);
    const catalogueDir = (0, node_path_1.dirname)(path);
    if (hasNestedCatalogueFiles(catalogueDir)) {
        return { path, createdFile: false, addedEntry: false, error: 'workspace_root_marker_required' };
    }
    const relativeFilePath = (0, node_path_1.relative)(catalogueDir, (0, node_path_1.resolve)(htmlFilePath)) || `${name}.html`;
    const normalizedFilePath = relativeFilePath.replace(/\\/g, '/');
    const entry = buildCatalogueEntry(name, metadata, sourceInfo, normalizedFilePath);
    if (!(0, node_fs_1.existsSync)(path)) {
        const catalogue = { apps: [entry] };
        (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(catalogue, null, 2)}\n`);
        return { path, createdFile: true, addedEntry: true };
    }
    const parsed = readCatalogue(path);
    if (!parsed) {
        return { path, createdFile: false, addedEntry: false, error: 'invalid_json' };
    }
    if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
        return { path, createdFile: false, addedEntry: false, error: 'invalid_schema_version' };
    }
    const unsupportedKey = findUnsupportedCatalogueTopLevelKey(parsed);
    if (unsupportedKey) {
        return { path, createdFile: false, addedEntry: false, error: 'unsupported_top_level_key', unsupportedKey };
    }
    if (!Array.isArray(parsed.apps) && Array.isArray(parsed.games)) {
        return { path, createdFile: false, addedEntry: false, error: 'legacy_games_property' };
    }
    const apps = Array.isArray(parsed.apps) ? [...parsed.apps] : [];
    if (apps.some((app) => typeof app?.name === 'string' && app.name === name)) {
        return { path, createdFile: false, addedEntry: false, alreadyPresent: true };
    }
    apps.push(entry);
    const updated = { ...parsed, apps };
    (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(updated, null, 2)}\n`);
    return { path, createdFile: false, addedEntry: true };
}
function logCatalogueOutcome(result, name) {
    const label = (0, node_path_1.relative)(process.cwd(), result.path) || CATALOGUE_FILENAME;
    if (result.error === 'invalid_json') {
        (0, messages_1.printErrorWithHelp)(`Could not update ${label} because it contains invalid JSON.`, [
            `Fix the JSON in ${label} and add ${name} to the apps list, then rerun "playdrop project create app ${name}" if needed.`
        ], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'legacy_games_property') {
        (0, messages_1.printErrorWithHelp)(`${label} still uses the legacy "games" catalogue field.`, [
            'Rename the top-level "games" array to "apps" and rerun `playdrop project create app`. This rename removes the old terminology.'
        ], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'invalid_schema_version') {
        (0, messages_1.printErrorWithHelp)(`${label} must not define the legacy top-level version field.`, ['Remove the legacy top-level version field from the file and rerun `playdrop project create app`.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'unsupported_top_level_key') {
        (0, messages_1.printErrorWithHelp)(`${label} contains unsupported top-level key "${result.unsupportedKey || 'unknown'}".`, ['Keep catalogue.json limited to apps, assetSpecs, assets, and assetPacks, then rerun `playdrop project create app`.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'workspace_root_marker_required') {
        (0, messages_1.printErrorWithHelp)(`${label} must stay {} because nested catalogue.json files already exist below this directory.`, ['Create the app inside its own project folder instead of writing entries into the workspace root catalogue.json.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.createdFile) {
        console.log(`Initialized ${label} and added ${name}.`);
        return true;
    }
    if (result.addedEntry) {
        console.log(`Added ${name} to ${label}.`);
        return true;
    }
    if (result.alreadyPresent) {
        console.log(`${label} already contains ${name}; keeping existing entry.`);
    }
    return true;
}
function ensureAssetSpecCatalogueEntry(name, cataloguePath) {
    const path = (0, node_path_1.resolve)(cataloguePath);
    const catalogueDir = (0, node_path_1.dirname)(path);
    if (hasNestedCatalogueFiles(catalogueDir)) {
        return { path, createdFile: false, addedEntry: false, error: 'workspace_root_marker_required' };
    }
    const entry = makeDefaultAssetSpecEntry(name);
    if (!(0, node_fs_1.existsSync)(path)) {
        const catalogue = { assetSpecs: [entry] };
        (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(catalogue, null, 2)}\n`);
        return { path, createdFile: true, addedEntry: true };
    }
    const parsed = readCatalogue(path);
    if (!parsed) {
        return { path, createdFile: false, addedEntry: false, error: 'invalid_json' };
    }
    if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
        return { path, createdFile: false, addedEntry: false, error: 'invalid_schema_version' };
    }
    const unsupportedKey = findUnsupportedCatalogueTopLevelKey(parsed);
    if (unsupportedKey) {
        return { path, createdFile: false, addedEntry: false, error: 'unsupported_top_level_key', unsupportedKey };
    }
    if (!Array.isArray(parsed.apps) && Array.isArray(parsed.games)) {
        return { path, createdFile: false, addedEntry: false, error: 'legacy_games_property' };
    }
    const assetSpecs = Array.isArray(parsed.assetSpecs) ? [...parsed.assetSpecs] : [];
    if (assetSpecs.some((assetSpec) => typeof assetSpec?.name === 'string' && assetSpec.name === name)) {
        return { path, createdFile: false, addedEntry: false, alreadyPresent: true };
    }
    assetSpecs.push(entry);
    const updated = { ...parsed, assetSpecs };
    (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(updated, null, 2)}\n`);
    return { path, createdFile: false, addedEntry: true };
}
function logAssetSpecCatalogueOutcome(result, name) {
    const label = (0, node_path_1.relative)(process.cwd(), result.path) || CATALOGUE_FILENAME;
    if (result.error === 'invalid_json') {
        (0, messages_1.printErrorWithHelp)(`Could not update ${label} because it contains invalid JSON.`, [
            `Fix the JSON in ${label} and add ${name} to the assetSpecs list, then rerun "playdrop project create asset-spec ${name}" if needed.`,
        ], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'legacy_games_property') {
        (0, messages_1.printErrorWithHelp)(`${label} still uses the legacy "games" catalogue field.`, [
            'Rename the top-level "games" array to "apps" and rerun `playdrop project create asset-spec`.',
        ], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'invalid_schema_version') {
        (0, messages_1.printErrorWithHelp)(`${label} must not define the legacy top-level version field.`, ['Remove the legacy top-level version field from the file and rerun `playdrop project create asset-spec`.'], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'unsupported_top_level_key') {
        (0, messages_1.printErrorWithHelp)(`${label} contains unsupported top-level key "${result.unsupportedKey || 'unknown'}".`, ['Keep catalogue.json limited to apps, assetSpecs, assets, and assetPacks, then rerun `playdrop project create asset-spec`.'], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'workspace_root_marker_required') {
        (0, messages_1.printErrorWithHelp)(`${label} must stay {} because nested catalogue.json files already exist below this directory.`, ['Create the asset spec inside its own project folder instead of writing entries into the workspace root catalogue.json.'], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.createdFile) {
        console.log(`Initialized ${label} and added asset spec ${name}.`);
        return true;
    }
    if (result.addedEntry) {
        console.log(`Added asset spec ${name} to ${label}.`);
        return true;
    }
    if (result.alreadyPresent) {
        console.log(`${label} already contains asset spec ${name}; keeping existing entry.`);
    }
    return true;
}
function updateExtractedProjectCatalogue(name, metadata, projectCataloguePath, sourceInfo, htmlFilePath) {
    const path = (0, node_path_1.resolve)(projectCataloguePath);
    if (hasNestedCatalogueFiles((0, node_path_1.dirname)(path))) {
        return { path, updated: false, error: 'workspace_root_marker_required' };
    }
    const parsed = readCatalogue(path);
    if (!parsed) {
        return { path, updated: false, error: 'invalid_json' };
    }
    if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
        return { path, updated: false, error: 'invalid_schema_version' };
    }
    const unsupportedKey = findUnsupportedCatalogueTopLevelKey(parsed);
    if (unsupportedKey) {
        return { path, updated: false, error: 'unsupported_top_level_key', unsupportedKey };
    }
    if (!Array.isArray(parsed.apps) && Array.isArray(parsed.games)) {
        return { path, updated: false, error: 'legacy_games_property' };
    }
    const apps = Array.isArray(parsed.apps) ? [...parsed.apps] : [];
    const relativeFilePath = (0, node_path_1.relative)((0, node_path_1.dirname)(path), (0, node_path_1.resolve)(htmlFilePath)) || `${name}.html`;
    const normalizedFilePath = relativeFilePath.replace(/\\/g, '/');
    const metadataName = metadata && typeof metadata.name === 'string' && metadata.name.trim().length > 0
        ? metadata.name.trim()
        : null;
    const metadataFile = metadata && typeof metadata.file === 'string' && metadata.file.trim().length > 0
        ? metadata.file.trim()
        : null;
    const nextType = app_1.DEFAULT_APP_TYPE;
    let targetIndex = -1;
    for (let i = 0; i < apps.length; i += 1) {
        const entry = apps[i];
        if (!entry) {
            continue;
        }
        if (metadataName && typeof entry.name === 'string' && entry.name === metadataName) {
            targetIndex = i;
            break;
        }
        if (typeof entry.name === 'string' && entry.name === name) {
            targetIndex = i;
            break;
        }
        if (metadataFile && typeof entry.file === 'string' && entry.file === metadataFile) {
            targetIndex = i;
            break;
        }
        if (typeof entry.file === 'string' && entry.file === normalizedFilePath) {
            targetIndex = i;
            break;
        }
    }
    if (targetIndex === -1) {
        if (apps.length === 0) {
            const newEntry = buildCatalogueEntry(name, metadata, sourceInfo, normalizedFilePath);
            newEntry.type = nextType;
            apps.push(newEntry);
        }
        else {
            targetIndex = 0;
        }
    }
    if (targetIndex >= 0 && targetIndex < apps.length) {
        const existing = { ...(apps[targetIndex] || {}) };
        existing.name = name;
        existing.type = nextType;
        if (!existing.file) {
            existing.file = normalizedFilePath;
        }
        if (typeof existing.version !== 'string' || existing.version.trim().length === 0) {
            existing.version = DEFAULT_APP_VERSION;
        }
        if (sourceInfo.templateSlug) {
            existing.templateSource = sourceInfo.templateSlug;
        }
        if (sourceInfo.remixRef) {
            existing.remix = sourceInfo.remixRef;
        }
        apps[targetIndex] = existing;
    }
    const updated = { ...parsed, apps };
    (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(updated, null, 2)}\n`);
    return { path, updated: true };
}
function logProjectCatalogueOutcome(result, name) {
    const label = (0, node_path_1.relative)(process.cwd(), result.path) || CATALOGUE_FILENAME;
    if (result.error === 'invalid_json') {
        (0, messages_1.printErrorWithHelp)(`Could not update ${label} because it contains invalid JSON.`, [
            `Fix the JSON in ${label} and add ${name} to the apps list, then rerun "playdrop project create app ${name}" if needed.`
        ], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'legacy_games_property') {
        (0, messages_1.printErrorWithHelp)(`${label} still uses the legacy "games" catalogue field.`, [
            'Rename the top-level "games" array to "apps" and rerun `playdrop project create app`. This rename removes the old terminology.'
        ], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'invalid_schema_version') {
        (0, messages_1.printErrorWithHelp)(`${label} must not define the legacy top-level version field.`, ['Remove the legacy top-level version field from the file and rerun `playdrop project create app`.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'unsupported_top_level_key') {
        (0, messages_1.printErrorWithHelp)(`${label} contains unsupported top-level key "${result.unsupportedKey || 'unknown'}".`, ['Keep catalogue.json limited to apps, assetSpecs, assets, and assetPacks, then rerun `playdrop project create app`.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.error === 'workspace_root_marker_required') {
        (0, messages_1.printErrorWithHelp)(`${label} must stay {} because nested catalogue.json files already exist below this directory.`, ['Create the app inside its own project folder instead of writing entries into the workspace root catalogue.json.'], { command: 'project create app' });
        process.exitCode = process.exitCode || 1;
        return false;
    }
    if (result.updated) {
        console.log(`Updated ${label} for ${name}.`);
    }
    return true;
}
function buildPendingExternalRemixTask(name, metadata, sourceInfo, cataloguePath, htmlFilePath) {
    const absoluteCataloguePath = (0, node_path_1.resolve)(cataloguePath);
    const catalogueDir = (0, node_path_1.dirname)(absoluteCataloguePath);
    const absoluteHtmlPath = (0, node_path_1.resolve)(htmlFilePath);
    const relativeFilePath = (0, node_path_1.relative)(catalogueDir, absoluteHtmlPath) || `${name}.html`;
    const entry = buildCatalogueEntry(name, metadata, sourceInfo, relativeFilePath.replace(/\\/g, '/'));
    const surfaceTargets = (0, catalogue_utils_1.parseSurfaceTargets)(entry.surfaceTargets ?? DEFAULT_SURFACE_TARGETS).list;
    return {
        kind: 'app',
        name,
        cataloguePath: (0, node_path_1.relative)(process.cwd(), absoluteCataloguePath) || CATALOGUE_FILENAME,
        catalogueAbsolutePath: absoluteCataloguePath,
        catalogueDir,
        filePath: absoluteHtmlPath,
        relativeFile: entry.file ?? relativeFilePath,
        projectDir: (0, node_path_1.dirname)(absoluteHtmlPath),
        packageJsonPath: null,
        isTypescriptProject: false,
        hasValidateScript: false,
        hasFormatScript: false,
        displayName: entry.displayName,
        description: entry.description,
        type: typeof entry.type === 'string' ? ((0, app_1.parseAppType)(entry.type) ?? undefined) : undefined,
        emoji: entry.emoji,
        color: entry.color,
        surfaceTargets,
        assetSpecSupport: [],
        tags: [],
        missingMetadata: [],
        version: entry.version,
        remix: typeof entry.remix === 'string' ? entry.remix : undefined,
        achievements: [],
        leaderboards: [],
        ownedAssets: [],
        license: types_1.DEFAULT_CONTENT_LICENSE,
        uses: { assets: [], packs: [] },
        graph: { relations: [] },
    };
}
function logNextSteps(name, options) {
    console.log('\nNext steps:');
    if (options.hasCatalogue && options.catalogueLabel) {
        console.log(` - Review ${options.catalogueLabel} to set displayName, description, emoji, color, type, and surfaceTargets as needed.`);
    }
    else {
        console.log(' - Run "playdrop project init ." when you are ready to add catalogue metadata and project docs.');
    }
    if (options.projectDir) {
        console.log(` - Open ${options.projectDir} to continue building your project.`);
        if (options.hasPackageJson) {
            if (options.dependenciesInstalled) {
                console.log(` - Dependencies installed. Use "npm run dev" inside ${options.projectDir} for live reload.`);
            }
            else {
                console.log(` - Run "npm install" inside ${options.projectDir} before your first build.`);
            }
        }
        console.log(` - The bundled HTML entry point is ${options.entryPointLabel}.`);
    }
    else if (options.externalListingUrl) {
        console.log(` - Open the external listing at ${options.externalListingUrl} and fetch the real source yourself.`);
        console.log(` - Keep ${options.entryPointLabel} as the app entry point, or update catalogue.json if your source uses a different file.`);
    }
    else {
        console.log(` - Build your app in ${options.entryPointLabel}.`);
    }
    if (options.externalListingUrl) {
        console.log(` - After you add the real source for ${options.entryPointLabel}, test locally with "playdrop project dev ${name}".`);
    }
    else {
        console.log(` - Test locally with "playdrop project dev ${name}".`);
    }
    if (options.hasCatalogue) {
        console.log(` - Publish a single app with "playdrop project publish ${name}" or everything in the catalogue with "playdrop project publish .".`);
    }
    else {
        console.log(' - After running "playdrop project init", publish with "playdrop project publish ." to share your project.');
    }
}
async function promptInit() {
    if (!node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY) {
        return false;
    }
    const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
    const answer = await rl.question('No catalogue.json found. Run "playdrop project init ." first? (y/N) ');
    rl.close();
    return /^y(es)?$/i.test(answer.trim());
}
async function ensureLocalCatalogue(cataloguePath) {
    if ((0, node_fs_1.existsSync)(cataloguePath)) {
        return { path: cataloguePath, initSummary: null, created: false };
    }
    const shouldInit = node_process_1.stdin.isTTY && node_process_1.stdout.isTTY ? await promptInit() : true;
    if (!shouldInit) {
        (0, messages_1.printErrorWithHelp)('A local catalogue.json is required to register app metadata next to the app file.', [
            'Run "playdrop project init ." in this directory to create catalogue.json and starter docs before running "playdrop project create app".'
        ], { command: 'project create app', includeGeneralHelp: false });
        process.exitCode = process.exitCode || 1;
        return null;
    }
    const summary = await (0, init_1.init)('.', { quiet: true });
    if (!summary) {
        process.exitCode = process.exitCode || 1;
        return null;
    }
    return { path: summary.cataloguePath, initSummary: summary, created: true };
}
function findWorkspaceRootDirectory(cataloguePath) {
    let currentDir = (0, node_path_1.dirname)((0, node_path_1.resolve)(cataloguePath));
    while (true) {
        const parentDir = (0, node_path_1.dirname)(currentDir);
        if (parentDir === currentDir) {
            return currentDir;
        }
        const parentCatalogue = (0, node_path_1.resolve)(parentDir, CATALOGUE_FILENAME);
        if (!(0, node_fs_1.existsSync)(parentCatalogue)) {
            return currentDir;
        }
        currentDir = parentDir;
    }
}
function buildAssetSpecSymbolSvg() {
    return [
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">',
        '  <path d="M14 18h36v28H14z" stroke="#fff" stroke-width="4" stroke-linejoin="round"/>',
        '  <path d="M24 28h16" stroke="#fff" stroke-width="4" stroke-linecap="round"/>',
        '  <path d="M24 36h10" stroke="#fff" stroke-width="4" stroke-linecap="round"/>',
        '</svg>',
        '',
    ].join('\n');
}
function buildAssetSpecContractTemplate() {
    return `${JSON.stringify({
        primaryRole: 'primary',
        roles: [
            {
                name: 'primary',
                required: true,
                contentTypes: ['application/json'],
                extensions: ['.json'],
                mode: 'TEXT',
            },
        ],
    }, null, 2)}\n`;
}
function buildAssetSpecReadmeTemplate(name) {
    return [
        `# ${name}`,
        '',
        'Describe the custom asset format in plain English.',
        '',
        '## Files',
        '',
        '- `primary`: Main JSON payload for this asset format.',
        '',
        '## Notes',
        '',
        '- Update `contract.json` as the format evolves.',
        '- Keep this documentation in sync with the published version.',
        '',
    ].join('\n');
}
async function createAssetSpecProject(name) {
    const normalizedName = name.trim();
    if (!normalizedName) {
        (0, messages_1.printErrorWithHelp)('An asset spec name is required.', ['Example: playdrop project create asset-spec platformer-level'], {
            command: 'project create asset-spec',
        });
        process.exitCode = process.exitCode || 1;
        return;
    }
    if (!(0, types_1.isValidAssetSpecName)(normalizedName)) {
        (0, messages_1.printErrorWithHelp)(`Asset spec name "${normalizedName}" is not valid.`, ['Use lowercase kebab-case, for example: playdrop project create asset-spec platformer-level'], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return;
    }
    const cataloguePath = (0, node_path_1.resolve)(process.cwd(), CATALOGUE_FILENAME);
    const assetSpecDir = (0, node_path_1.resolve)(process.cwd(), 'asset-specs', normalizedName);
    const contractPath = (0, node_path_1.resolve)(assetSpecDir, 'contract.json');
    const symbolPath = (0, node_path_1.resolve)(assetSpecDir, 'symbol.svg');
    const readmePath = (0, node_path_1.resolve)(assetSpecDir, 'README.md');
    if ((0, node_fs_1.existsSync)(assetSpecDir)) {
        (0, messages_1.printErrorWithHelp)(`Asset spec directory already exists at ${(0, node_path_1.relative)(process.cwd(), assetSpecDir) || assetSpecDir}.`, ['Choose a different asset spec name or remove the existing directory before rerunning the command.'], { command: 'project create asset-spec' });
        process.exitCode = process.exitCode || 1;
        return;
    }
    (0, node_fs_1.mkdirSync)(assetSpecDir, { recursive: true });
    (0, node_fs_1.writeFileSync)(symbolPath, buildAssetSpecSymbolSvg());
    (0, node_fs_1.writeFileSync)(contractPath, buildAssetSpecContractTemplate());
    (0, node_fs_1.writeFileSync)(readmePath, buildAssetSpecReadmeTemplate(normalizedName));
    const catalogueResult = ensureAssetSpecCatalogueEntry(normalizedName, cataloguePath);
    const catalogueUpdated = logAssetSpecCatalogueOutcome(catalogueResult, normalizedName);
    if (!catalogueUpdated) {
        (0, node_fs_1.rmSync)(assetSpecDir, { recursive: true, force: true });
        return;
    }
    const relativeAssetSpecDir = (0, node_path_1.relative)(process.cwd(), assetSpecDir) || assetSpecDir;
    const catalogueLabel = (0, node_path_1.relative)(process.cwd(), cataloguePath) || CATALOGUE_FILENAME;
    console.log('\nNext steps:');
    console.log(` - Review ${catalogueLabel} and update the assetSpecs entry for ${normalizedName}.`);
    console.log(` - Edit ${relativeAssetSpecDir}/contract.json to define the file contract.`);
    console.log(` - Replace ${relativeAssetSpecDir}/symbol.svg with your final icon.`);
    console.log(` - Expand ${relativeAssetSpecDir}/README.md with format documentation.`);
    console.log(` - Validate locally with "playdrop project validate ." and publish with "playdrop project publish ${normalizedName}".`);
}
function warnMissingWorkspaceReadme(cataloguePath) {
    const workspaceRoot = findWorkspaceRootDirectory(cataloguePath);
    const readmePath = (0, node_path_1.resolve)(workspaceRoot, 'README.md');
    if ((0, node_fs_1.existsSync)(readmePath)) {
        return;
    }
    const workspaceLabel = (0, node_path_1.relative)(process.cwd(), workspaceRoot) || '.';
    const initCommandTarget = workspaceLabel === '.' ? '.' : workspaceLabel;
    console.log(`\nWorkspace docs reminder: README.md is missing at ${workspaceLabel}. Add documentation there or run "playdrop project init ${initCommandTarget}" before creating apps next time to auto-populate it.`);
}
function parseTemplateKey(value) {
    const parts = value.split('/').map((part) => part.trim()).filter(Boolean);
    if (parts.length >= 3) {
        const [creator, typeSegment, ...rest] = parts;
        if (!creator) {
            return null;
        }
        const normalizedType = typeSegment.toLowerCase();
        if (normalizedType !== 'template' && normalizedType !== 'app') {
            return null;
        }
        const slug = rest.join('/');
        if (!slug) {
            return null;
        }
        return { namespace: creator, slug, key: `${creator}/template/${slug}` };
    }
    return null;
}
function parseRemixRef(value) {
    const parsed = (0, types_1.parseContentVersionRef)(value);
    if (!parsed || parsed.kind !== 'app') {
        return null;
    }
    return {
        creator: parsed.creatorUsername,
        name: parsed.name,
        version: parsed.version,
        ref: (0, types_1.formatContentVersionRef)(parsed),
    };
}
function parseRemixScaffoldResponse(response, version) {
    const remixMode = typeof response?.remixMode === 'string' ? response.remixMode : '';
    const archiveUrl = typeof response?.archiveUrl === 'string' && response.archiveUrl.trim().length > 0
        ? response.archiveUrl.trim()
        : null;
    const htmlContent = typeof response?.html === 'string' ? response.html : null;
    const externalListingUrl = typeof response?.externalListingUrl === 'string' && response.externalListingUrl.trim().length > 0
        ? response.externalListingUrl.trim()
        : null;
    const metadata = response.metadata;
    const entryPoint = extractEntryPointFromMetadata(metadata);
    if (remixMode === 'archive') {
        if (!archiveUrl) {
            throw new Error('Remix response missing archiveUrl');
        }
        return {
            html: null,
            metadata,
            archiveUrl,
            entryPoint,
            sourceVersion: version,
        };
    }
    if (remixMode === 'inline-html') {
        if (!htmlContent) {
            throw new Error('Remix response missing html');
        }
        return {
            html: htmlContent,
            metadata,
            entryPoint,
            sourceVersion: null,
        };
    }
    if (remixMode === 'external-metadata-only') {
        if (!externalListingUrl) {
            throw new Error('Remix response missing externalListingUrl');
        }
        const externalMetadata = parseExternalRemixMetadata(response.metadata);
        return {
            html: null,
            metadata: externalMetadata,
            externalListingUrl,
            entryPoint,
            sourceVersion: null,
        };
    }
    throw new Error(`Unsupported remix mode: ${remixMode || 'missing'}`);
}
function parseExternalRemixMetadata(value) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        throw new Error('Remix response missing external metadata');
    }
    const record = value;
    const name = typeof record.name === 'string' ? record.name.trim() : '';
    const displayName = typeof record.displayName === 'string' ? record.displayName.trim() : '';
    const description = typeof record.description === 'string' ? record.description.trim() : '';
    const creatorUsername = typeof record.creatorUsername === 'string' ? record.creatorUsername.trim() : '';
    const type = (0, app_1.parseAppType)(record.type) ?? '';
    const emoji = record.emoji;
    const color = record.color;
    if (!name || !displayName || !description || !creatorUsername || !type) {
        throw new Error('Remix response missing required external metadata fields');
    }
    if (emoji !== null && typeof emoji !== 'string') {
        throw new Error('Remix response has invalid external metadata emoji');
    }
    if (color !== null && typeof color !== 'string') {
        throw new Error('Remix response has invalid external metadata color');
    }
    return {
        name,
        displayName,
        description,
        creatorUsername,
        type,
        emoji,
        color,
        surfaceTargets: (0, catalogue_utils_1.parseSurfaceTargets)(record.surfaceTargets).list,
    };
}
async function fetchRemixScaffold(client, creator, app, version) {
    try {
        const response = await client.fetchRemixScaffold(creator, app, version);
        return parseRemixScaffoldResponse(response, version);
    }
    catch (unknownError) {
        if (unknownError instanceof types_1.UnsupportedClientError) {
            (0, http_1.handleUnsupportedError)(unknownError, 'Remix scaffold');
        }
        if (unknownError instanceof types_1.ApiError) {
            const detail = unknownError.message?.trim();
            throw new Error(detail || `status ${unknownError.status}`);
        }
        throw unknownError;
    }
}
async function resolveAuthenticatedCreatorUsername(client) {
    const response = await client.me();
    const username = typeof response?.user?.username === 'string' ? response.user.username.trim() : '';
    if (!username) {
        throw new Error('missing_creator_username');
    }
    return username;
}
async function fetchTemplateScaffold(client, namespace, slug) {
    try {
        const response = await client.fetchTemplateScaffold(namespace, slug);
        const archiveUrl = typeof response?.archiveUrl === 'string' && response.archiveUrl.trim().length > 0
            ? response.archiveUrl.trim()
            : null;
        const htmlContent = typeof response?.html === 'string' ? response.html : null;
        if (!archiveUrl && !htmlContent) {
            throw new Error('Template response missing scaffold content');
        }
        const metadata = response.metadata;
        const entryPoint = extractEntryPointFromMetadata(metadata);
        return {
            html: htmlContent,
            metadata,
            archiveUrl,
            entryPoint,
        };
    }
    catch (unknownError) {
        if (unknownError instanceof types_1.UnsupportedClientError) {
            (0, http_1.handleUnsupportedError)(unknownError, 'Template scaffold');
        }
        if (unknownError instanceof types_1.ApiError) {
            const detail = unknownError.message?.trim();
            throw new Error(detail || `status ${unknownError.status}`);
        }
        throw unknownError;
    }
}
function describeInitSummary(summary) {
    if (!summary) {
        return null;
    }
    const parts = [];
    parts.push(summary.readmeStatus === 'created' ? 'created README.md' : 'kept existing README.md');
    parts.push(summary.agentsStatus === 'created' ? 'created AGENTS.md' : 'kept existing AGENTS.md');
    parts.push(summary.catalogueStatus === 'created' ? 'created catalogue.json' : 'kept existing catalogue.json');
    if (summary.gitStatus === 'initialized') {
        parts.push('initialized git repository');
    }
    else if (summary.gitStatus === 'declined') {
        parts.push('skipped git init');
    }
    return `Project bootstrap recap: ${parts.join('; ')}.`;
}
async function create(name, options = {}) {
    const file = (0, node_path_1.resolve)(`${name}.html`);
    const projectDirCandidate = (0, node_path_1.resolve)(name);
    const templateOptionRaw = options.template?.trim();
    const remixOptionRaw = options.remix?.trim();
    let parsedTemplate = null;
    let parsedRemix = null;
    const existingLookup = (0, catalogue_1.resolveCatalogueEntries)(process.cwd(), { filterName: name });
    if (existingLookup.errors.length > 0) {
        for (const error of existingLookup.errors) {
            (0, messages_1.printErrorWithHelp)(error, ['Fix the catalogue issues noted above, then rerun "playdrop project create app".'], {
                command: 'project create app',
                includeGeneralHelp: false,
            });
        }
        process.exitCode = 1;
        return;
    }
    if (existingLookup.apps.length > 1) {
        (0, messages_1.printErrorWithHelp)(`Multiple catalogue entries share the app name "${name}".`, [
            'Ensure each app has a unique name across catalogue.json files before running "playdrop project create app".'
        ], { command: 'project create app' });
        process.exitCode = 1;
        return;
    }
    const existingTask = existingLookup.apps.length === 1 ? existingLookup.apps[0] : null;
    const reuseExistingApp = existingTask !== null && !templateOptionRaw && !remixOptionRaw;
    if (!reuseExistingApp && ((0, node_fs_1.existsSync)(file) || (0, node_fs_1.existsSync)(projectDirCandidate))) {
        (0, messages_1.printErrorWithHelp)(`Cannot create ${name} because a file or directory with that name already exists.`, [
            'Choose a different app name or remove the existing file before running "playdrop project create app" again.'
        ], { command: 'project create app' });
        process.exitCode = 1;
        return;
    }
    if (templateOptionRaw && remixOptionRaw) {
        (0, messages_1.printErrorWithHelp)('Cannot use --template and --remix at the same time.', ['Choose one source: use "--remix app:creator/name@1.2.3" to remix an app version, or "--template {creator}/template/{name}" to start from a template.'], { command: 'project create app' });
        process.exitCode = 1;
        return;
    }
    if (!templateOptionRaw && !remixOptionRaw) {
        if (!reuseExistingApp) {
            (0, messages_1.printErrorWithHelp)('Specify either --template or --remix when creating an app.', [
                'Use "playdrop browse --kind app --app-type template" to discover available templates.',
                'Browse https://playdrop.ai, pick an app version, and pass its canonical ref with "--remix app:creator/name@1.2.3".'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
    }
    const templateOption = templateOptionRaw || undefined;
    const remixOption = remixOptionRaw || undefined;
    if (!reuseExistingApp && remixOption) {
        parsedRemix = parseRemixRef(remixOption);
        if (!parsedRemix) {
            (0, messages_1.printErrorWithHelp)('The --remix value must use canonical app version ref format.', [
                'Example: app:playdrop/hangingout@1.2.3.',
                'Use "playdrop browse --kind app" or "playdrop search <query> --kind app" to find remixable apps.'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
    }
    else if (templateOption && !reuseExistingApp) {
        parsedTemplate = parseTemplateKey(templateOption);
        if (!parsedTemplate) {
            (0, messages_1.printErrorWithHelp)('The --template value must use the {creator}/template/{name} or {creator}/app/{name} key format.', [
                'Examples: playdrop/template/typescript_template or playdrop/app/typescript_template.',
                'Use the exact template ref shown in Playdrop.'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
    }
    const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project create app', 'Creating an app', { workspacePath: process.cwd() });
    if (!ctx) {
        return;
    }
    const envName = ctx.env;
    const currentDir = process.cwd();
    const localCataloguePath = (0, node_path_1.resolve)(currentDir, CATALOGUE_FILENAME);
    let initSummary = null;
    let workspaceCatalogueResult = null;
    let workspaceCatalogueUpdateSucceeded = false;
    let projectCatalogueResult = null;
    let cataloguePathToUse = null;
    let projectCataloguePath = null;
    const client = ctx.client;
    const sourceInfo = {};
    let remixSourceTarget = null;
    let scaffold = null;
    let provenanceMessage = '';
    if (!reuseExistingApp && remixOption) {
        const parsed = parsedRemix;
        remixSourceTarget = { creator: parsed.creator, name: parsed.name, version: parsed.version };
        sourceInfo.remixRef = parsed.ref;
        try {
            scaffold = await fetchRemixScaffold(client, parsed.creator, parsed.name, parsed.version);
            provenanceMessage = `Remixed from ${parsed.ref}.`;
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                process.exitCode = 1;
                return;
            }
            const detailMessage = typeof error?.message === 'string' && error.message.trim()
                ? error.message.trim()
                : '';
            const problem = detailMessage
                ? `Failed to fetch remix source: ${detailMessage}`
                : 'Failed to fetch remix source.';
            (0, messages_1.printErrorWithHelp)(problem, [
                'Confirm the key is correct and that you have access to remix the source app.',
                'Retry once you are logged in and the Playdrop API is reachable.'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
    }
    else if (templateOption) {
        if (reuseExistingApp) {
            // ignore template when reusing local files
        }
        else {
            const parsed = parsedTemplate;
            sourceInfo.templateSlug = parsed.key;
            try {
                scaffold = await fetchTemplateScaffold(client, parsed.namespace, parsed.slug);
                provenanceMessage = `Scaffolded from template ${parsed.key}.`;
            }
            catch (error) {
                if (error instanceof http_1.CLIUnsupportedClientError) {
                    process.exitCode = 1;
                    return;
                }
                const detailMessage = typeof error?.message === 'string' && error.message.trim()
                    ? error.message.trim()
                    : '';
                const problem = detailMessage
                    ? `Failed to fetch template: ${detailMessage}`
                    : 'Failed to fetch template.';
                (0, messages_1.printErrorWithHelp)(problem, [
                    'Verify the template key and ensure you are logged in.',
                    'Retry when the Playdrop API is reachable.'
                ], { command: 'project create app' });
                process.exitCode = 1;
                return;
            }
        }
    }
    if (!reuseExistingApp && !scaffold) {
        (0, messages_1.printErrorWithHelp)('Failed to retrieve scaffold data. Specify a valid --template or --remix key and try again.', [
            'Use "playdrop browse --kind app --app-type template" to discover available templates.',
            'Browse https://playdrop.ai and copy an exact app version ref to pass with "--remix".'
        ], { command: 'project create app' });
        process.exitCode = 1;
        return;
    }
    const hasArchive = !reuseExistingApp && typeof scaffold?.archiveUrl === 'string' && scaffold.archiveUrl?.trim().length > 0;
    const externalRemixListingUrl = !reuseExistingApp && typeof scaffold?.externalListingUrl === 'string' && scaffold.externalListingUrl.trim().length > 0
        ? scaffold.externalListingUrl.trim()
        : null;
    let htmlFilePath = reuseExistingApp && existingTask ? existingTask.filePath : '';
    let projectDir = reuseExistingApp && existingTask ? existingTask.projectDir : null;
    let packageJsonPath = reuseExistingApp && existingTask ? existingTask.packageJsonPath ?? null : null;
    if (!reuseExistingApp && !hasArchive) {
        const ensuredCatalogue = await ensureLocalCatalogue(localCataloguePath);
        if (!ensuredCatalogue) {
            return;
        }
        initSummary = ensuredCatalogue.initSummary;
        cataloguePathToUse = ensuredCatalogue.path;
    }
    if (!reuseExistingApp && hasArchive) {
        projectDir = projectDirCandidate;
        try {
            (0, node_fs_1.mkdirSync)(projectDir);
        }
        catch (error) {
            (0, messages_1.printErrorWithHelp)(`Cannot create ${projectDir} because it already exists.`, [
                'Remove or rename the existing directory before running "playdrop project create app" again.'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
        try {
            const archiveBuffer = remixSourceTarget
                ? new Uint8Array(await (await client.downloadAppSource(remixSourceTarget.creator, remixSourceTarget.name, remixSourceTarget.version)).blob.arrayBuffer())
                : await downloadArchive(resolveArchiveDownloadUrl(scaffold.archiveUrl, ctx.envConfig.apiBase), ctx.envConfig.apiBase, ctx.token);
            const extraction = extractProjectArchive(archiveBuffer, projectDir);
            const extractedCatalogue = (0, node_path_1.resolve)(projectDir, CATALOGUE_FILENAME);
            projectCataloguePath = (0, node_fs_1.existsSync)(extractedCatalogue) ? extractedCatalogue : null;
            const entryPointCandidates = [
                extraction.entryPoint,
                scaffold.entryPoint ?? null,
                'dist/index.html',
                `${name}.html`,
            ].filter((value) => typeof value === 'string' && value.trim().length > 0);
            let resolvedEntryPoint = null;
            for (const candidate of entryPointCandidates) {
                const normalized = candidate.replace(/^\.\/?/, '').replace(/^\\+/, '').replace(/\\/g, '/');
                const candidatePath = (0, node_path_1.resolve)(projectDir, normalized);
                if ((0, node_fs_1.existsSync)(candidatePath) && (0, node_fs_1.statSync)(candidatePath).isFile()) {
                    resolvedEntryPoint = candidatePath;
                    break;
                }
            }
            if (!resolvedEntryPoint) {
                const fallbackHtml = findFirstHtmlFile(projectDir);
                if (fallbackHtml) {
                    resolvedEntryPoint = fallbackHtml;
                }
            }
            if (!resolvedEntryPoint) {
                (0, node_fs_1.rmSync)(projectDir, { recursive: true, force: true });
                (0, messages_1.printErrorWithHelp)('Extracted project archive did not include an HTML entry point.', [
                    'Check the template archive and ensure playdrop.project.json specifies a valid entryPoint before retrying.'
                ], { command: 'project create app' });
                process.exitCode = 1;
                return;
            }
            htmlFilePath = resolvedEntryPoint;
            const packageCandidate = (0, node_path_1.join)(projectDir, 'package.json');
            packageJsonPath = (0, node_fs_1.existsSync)(packageCandidate) ? packageCandidate : null;
        }
        catch (error) {
            (0, node_fs_1.rmSync)(projectDir, { recursive: true, force: true });
            const detailMessage = typeof error?.message === 'string' && error.message.trim()
                ? error.message.trim()
                : 'Failed to download or extract the project archive.';
            (0, messages_1.printNetworkIssue)(detailMessage, 'project create app');
            process.exitCode = 1;
            return;
        }
        console.log(`Created ${projectDir}`);
    }
    else {
        if (reuseExistingApp) {
            htmlFilePath = existingTask.filePath;
            if (!(0, node_fs_1.existsSync)(htmlFilePath) || !(0, node_fs_1.statSync)(htmlFilePath).isFile()) {
                (0, messages_1.printErrorWithHelp)(`Could not reuse existing app because ${htmlFilePath} does not exist.`, [
                    'Update catalogue.json or remove the entry before rerunning "playdrop project create app".'
                ], { command: 'project create app' });
                process.exitCode = 1;
                return;
            }
            projectDir = existingTask.projectDir;
            packageJsonPath = existingTask.packageJsonPath ?? null;
            cataloguePathToUse = existingTask.catalogueAbsolutePath;
        }
        else if (externalRemixListingUrl) {
            htmlFilePath = file;
        }
        else if (!scaffold?.html) {
            (0, messages_1.printNetworkIssue)('Unable to retrieve template content for playdrop project create app.', 'project create app');
            process.exitCode = 1;
            return;
        }
        if (!reuseExistingApp && !externalRemixListingUrl) {
            htmlFilePath = file;
            const scaffoldHtml = scaffold.html;
            (0, node_fs_1.writeFileSync)(htmlFilePath, scaffoldHtml);
            console.log(`Created ${htmlFilePath}`);
        }
    }
    if (provenanceMessage) {
        console.log(provenanceMessage);
    }
    if (!reuseExistingApp && projectCataloguePath) {
        projectCatalogueResult = updateExtractedProjectCatalogue(name, scaffold.metadata, projectCataloguePath, sourceInfo, htmlFilePath);
        const projectUpdateSucceeded = logProjectCatalogueOutcome(projectCatalogueResult, name);
        if (!projectUpdateSucceeded) {
            return;
        }
    }
    const workspaceCatalogueExists = (0, node_fs_1.existsSync)(localCataloguePath);
    const workspaceCatalogueRequired = !projectCataloguePath;
    const shouldEnsureWorkspaceCatalogue = workspaceCatalogueRequired || workspaceCatalogueExists;
    if (!reuseExistingApp && shouldEnsureWorkspaceCatalogue) {
        if (!cataloguePathToUse) {
            const ensuredCatalogue = await ensureLocalCatalogue(localCataloguePath);
            if (!ensuredCatalogue) {
                return;
            }
            initSummary = ensuredCatalogue.initSummary;
            cataloguePathToUse = ensuredCatalogue.path;
        }
        try {
            workspaceCatalogueResult = ensureCatalogueEntry(name, scaffold.metadata, cataloguePathToUse, sourceInfo, htmlFilePath);
        }
        catch (error) {
            const fallbackLabel = cataloguePathToUse
                ? ((0, node_path_1.relative)(process.cwd(), cataloguePathToUse) || CATALOGUE_FILENAME)
                : CATALOGUE_FILENAME;
            const message = typeof error?.message === 'string' && error.message.includes('surfaceTargets')
                ? error.message
                : `Could not update ${fallbackLabel} because it contains invalid JSON.`;
            const suggestions = typeof error?.message === 'string' && error.message.includes('surfaceTargets')
                ? ['Fix the catalogue entry described above, then rerun "playdrop project create app".']
                : [`Fix the JSON in ${fallbackLabel} and add ${name} again before rerunning "playdrop project create app".`];
            (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
        workspaceCatalogueUpdateSucceeded = logCatalogueOutcome(workspaceCatalogueResult, name);
        if (!workspaceCatalogueUpdateSucceeded) {
            return;
        }
    }
    let registrationTask = null;
    if (reuseExistingApp && existingTask) {
        registrationTask = existingTask;
    }
    else if (!reuseExistingApp && externalRemixListingUrl && !(0, node_fs_1.existsSync)(htmlFilePath)) {
        const pendingCataloguePath = workspaceCatalogueResult?.path ?? cataloguePathToUse;
        if (!pendingCataloguePath) {
            throw new Error('Missing catalogue path for external remix scaffold.');
        }
        registrationTask = buildPendingExternalRemixTask(name, scaffold.metadata, sourceInfo, pendingCataloguePath, htmlFilePath);
    }
    else {
        const registrationLookup = (0, catalogue_1.resolveCatalogueEntries)(process.cwd(), { filterName: name });
        if (registrationLookup.errors.length > 0) {
            const otherErrors = registrationLookup.errors.filter((error) => typeof error !== 'string' || !error.includes('Duplicate app name'));
            if (otherErrors.length > 0) {
                for (const error of otherErrors) {
                    (0, messages_1.printErrorWithHelp)(error, ['Fix the catalogue issues noted above, then rerun "playdrop project create app".'], { command: 'project create app', includeGeneralHelp: false });
                }
                process.exitCode = 1;
                return;
            }
        }
        if (registrationLookup.apps.length === 0) {
            (0, messages_1.printErrorWithHelp)(`App "${name}" was not found in any catalogue.json after scaffolding.`, [
                'Ensure catalogue.json was updated, then rerun "playdrop project create app".'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
        const candidates = registrationLookup.apps;
        const preferredCataloguePaths = [];
        if (workspaceCatalogueResult && workspaceCatalogueUpdateSucceeded) {
            preferredCataloguePaths.push((0, node_path_1.resolve)(workspaceCatalogueResult.path));
        }
        if (projectCatalogueResult?.updated) {
            preferredCataloguePaths.push((0, node_path_1.resolve)(projectCatalogueResult.path));
        }
        let selectedTask = null;
        for (const preferred of preferredCataloguePaths) {
            const match = candidates.find(task => (0, node_path_1.resolve)(task.catalogueAbsolutePath) === preferred);
            if (match) {
                selectedTask = match;
                break;
            }
        }
        if (!selectedTask && candidates.length === 1) {
            selectedTask = candidates[0];
        }
        if (!selectedTask) {
            const matchingByHtml = candidates.filter(task => task.filePath === htmlFilePath);
            if (matchingByHtml.length === 1) {
                selectedTask = matchingByHtml[0];
            }
        }
        if (!selectedTask) {
            (0, messages_1.printErrorWithHelp)(`Multiple catalogue entries share the app name "${name}".`, [
                'Ensure each app has a unique name across catalogue.json files before running "playdrop project create app".'
            ], { command: 'project create app' });
            process.exitCode = 1;
            return;
        }
        registrationTask = selectedTask;
    }
    if (registrationTask) {
        const metadataWarning = (0, catalogue_1.formatMissingMetadataWarnings)(registrationTask);
        if (metadataWarning) {
            console.log(`⚠️ ${metadataWarning}`);
        }
        const displayName = registrationTask.displayName && registrationTask.displayName.trim().length > 0
            ? registrationTask.displayName
            : registrationTask.name;
        const createPayload = {
            name,
            displayName,
        };
        let creatorUsername = '';
        try {
            creatorUsername = await resolveAuthenticatedCreatorUsername(client);
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Resolve creator account');
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.ApiError) {
                (0, messages_1.printErrorWithHelp)(`Failed to resolve your creator account (status ${error.status}).`, ['Run "playdrop auth login" to refresh your session, then try again.'], { command: 'project create app' });
                process.exitCode = 1;
                return;
            }
            if (error instanceof TypeError) {
                (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your creator account.', 'project create app');
                process.exitCode = 1;
                return;
            }
            if (error instanceof Error && error.message === 'missing_creator_username') {
                (0, messages_1.printErrorWithHelp)('The API did not return a creator username.', ['Run "playdrop auth login" again, then retry.'], { command: 'project create app' });
                process.exitCode = 1;
                return;
            }
            throw error;
        }
        let createConflict = false;
        try {
            await client.createApp(creatorUsername, createPayload);
            console.log(`Registered ${displayName} with the Playdrop API.`);
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Register app');
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.ApiError) {
                if (error.status === 409) {
                    createConflict = true;
                    console.log(`ℹ️ App ${name} already exists on ${envName}. Updating metadata from the local catalogue.`);
                }
                else {
                    (0, messages_1.printErrorWithHelp)(`Failed to register ${name} (status ${error.status}).`, [
                        'Ensure your account has creator permissions and retry once the issue is resolved.'
                    ], { command: 'project create app' });
                    process.exitCode = 1;
                    return;
                }
            }
            else if (error instanceof TypeError) {
                (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to register the app.', 'project create app');
                process.exitCode = 1;
                return;
            }
            else {
                throw error;
            }
        }
        // Note: emoji, color, description are now per-version fields (set via version upload)
        const updatePayload = {};
        if (registrationTask.type) {
            updatePayload.type = registrationTask.type;
        }
        if (registrationTask.displayName && registrationTask.displayName.trim().length > 0) {
            updatePayload.displayName = registrationTask.displayName;
        }
        if (Object.keys(updatePayload).length > 0) {
            try {
                await client.updateApp(creatorUsername, name, updatePayload);
                if (createConflict) {
                    console.log(`Updated ${name} metadata on the Playdrop API.`);
                }
            }
            catch (error) {
                if (error instanceof http_1.CLIUnsupportedClientError) {
                    process.exitCode = 1;
                    return;
                }
                if (error instanceof types_1.UnsupportedClientError) {
                    (0, http_1.handleUnsupportedError)(error, 'Update app');
                    process.exitCode = 1;
                    return;
                }
                if (error instanceof types_1.ApiError) {
                    (0, messages_1.printErrorWithHelp)(`Failed to update ${name} metadata (status ${error.status}).`, [
                        'Update the catalogue entry and retry, or contact the Playdrop team if the issue persists.'
                    ], { command: 'project create app' });
                    process.exitCode = 1;
                    return;
                }
                if (error instanceof TypeError) {
                    (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to update the app.', 'project create app');
                    process.exitCode = 1;
                    return;
                }
                throw error;
            }
        }
    }
    const projectDirLabel = projectDir ? ((0, node_path_1.relative)(process.cwd(), projectDir) || projectDir) : null;
    const entryPointLabel = (0, node_path_1.relative)(process.cwd(), htmlFilePath) || (0, node_path_1.basename)(htmlFilePath);
    const hasPackageJson = Boolean(packageJsonPath);
    let dependenciesInstalled = false;
    if (!reuseExistingApp && projectDir && packageJsonPath && node_process_1.stdin.isTTY && node_process_1.stdout.isTTY) {
        const accepted = await promptInstallDependencies(projectDir);
        if (accepted) {
            const success = runNpmInstall(projectDir);
            dependenciesInstalled = success;
            if (!success) {
                console.warn('npm install failed. Install dependencies manually before continuing.');
            }
        }
    }
    if (initSummary) {
        const recap = describeInitSummary(initSummary);
        if (recap) {
            console.log(recap);
        }
    }
    const projectCatalogueUpdated = Boolean(projectCatalogueResult?.updated);
    const workspaceCatalogueUpdated = Boolean(workspaceCatalogueResult && workspaceCatalogueUpdateSucceeded);
    let catalogueLabel;
    if (projectCatalogueUpdated && projectCatalogueResult) {
        catalogueLabel = (0, node_path_1.relative)(process.cwd(), projectCatalogueResult.path) || CATALOGUE_FILENAME;
    }
    else if (workspaceCatalogueUpdated && workspaceCatalogueResult) {
        catalogueLabel = (0, node_path_1.relative)(process.cwd(), workspaceCatalogueResult.path) || CATALOGUE_FILENAME;
    }
    let hasCatalogue = projectCatalogueUpdated || workspaceCatalogueUpdated;
    if (reuseExistingApp && existingTask) {
        hasCatalogue = true;
        if (!catalogueLabel) {
            catalogueLabel = (0, node_path_1.relative)(process.cwd(), existingTask.catalogueAbsolutePath) || CATALOGUE_FILENAME;
        }
    }
    logNextSteps(name, {
        hasCatalogue,
        catalogueLabel,
        projectDir: projectDirLabel,
        entryPointLabel,
        hasPackageJson,
        dependenciesInstalled,
        externalListingUrl: externalRemixListingUrl,
    });
    if (workspaceCatalogueUpdated && workspaceCatalogueResult) {
        warnMissingWorkspaceReadme(workspaceCatalogueResult.path);
    }
}
