"use strict";
/* eslint-disable max-lines */
Object.defineProperty(exports, "__esModule", { value: true });
exports.MARKETING_AUDIO_POLICIES = exports.MARKETING_SURFACES = void 0;
exports.parseMarketingCaptureOptions = parseMarketingCaptureOptions;
exports.assertSupportedMarketingCapturePlatform = assertSupportedMarketingCapturePlatform;
exports.resolveMarketingOutputPaths = resolveMarketingOutputPaths;
exports.validateMarketingPreviewConfig = validateMarketingPreviewConfig;
exports.buildMarketingFfmpegArgs = buildMarketingFfmpegArgs;
exports.buildMarketingCaptureManifest = buildMarketingCaptureManifest;
exports.createMarketingReport = createMarketingReport;
exports.marketingDoctor = marketingDoctor;
exports.marketingCapture = marketingCapture;
const node_child_process_1 = require("node:child_process");
const promises_1 = require("node:fs/promises");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_os_1 = require("node:os");
const types_1 = require("@playdrop/types");
const catalogue_utils_1 = require("../catalogue-utils");
const catalogue_1 = require("../catalogue");
const commandContext_1 = require("../commandContext");
const http_1 = require("../http");
const appUrls_1 = require("../appUrls");
const messages_1 = require("../messages");
const playwright_1 = require("../playwright");
const devServer_1 = require("./devServer");
const devRuntimeAssets_1 = require("./devRuntimeAssets");
const dev_1 = require("./dev");
const devShared_1 = require("./devShared");
exports.MARKETING_SURFACES = ['desktop', 'mobile-landscape', 'mobile-portrait'];
exports.MARKETING_AUDIO_POLICIES = ['music-and-sfx', 'sfx-only', 'silent'];
const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
const SURFACE_PRESETS = {
    desktop: {
        width: 1920,
        height: 1080,
        sceneId: 'listing-landscape',
        viewport: {
            width: 1280,
            height: 720,
            deviceScaleFactor: 1,
            isMobile: false,
            hasTouch: false,
        },
    },
    'mobile-landscape': {
        width: 1920,
        height: 1080,
        sceneId: 'listing-landscape',
        viewport: {
            width: 896,
            height: 414,
            deviceScaleFactor: 2,
            isMobile: true,
            hasTouch: true,
            userAgent: MOBILE_USER_AGENT,
        },
    },
    'mobile-portrait': {
        width: 1080,
        height: 1920,
        sceneId: 'listing-portrait',
        viewport: {
            width: 414,
            height: 896,
            deviceScaleFactor: 2,
            isMobile: true,
            hasTouch: true,
            userAgent: MOBILE_USER_AGENT,
        },
    },
};
const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
const DEFAULT_DURATION_SECONDS = 12;
const DEFAULT_FPS = 60;
const DEFAULT_AUDIO_POLICY = 'music-and-sfx';
const MIN_DURATION_SECONDS = 1;
const MAX_DURATION_SECONDS = 300;
const MIN_FPS = 1;
const MAX_FPS = 120;
const MIN_MARKETING_AUDIO_INTEGRATED_LUFS = -28;
const MIN_MARKETING_AUDIO_PEAK_DB = -12;
const MIN_MARKETING_PREVIEW_FRAME_SIMILARITY = 0.25;
function parsePositiveNumber(raw, defaultValue, fieldName, minimum, maximum) {
    if (raw === undefined) {
        return defaultValue;
    }
    const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw));
    if (!Number.isFinite(parsed)) {
        throw new Error(`${fieldName}_invalid`);
    }
    if (parsed < minimum || parsed > maximum) {
        throw new Error(`${fieldName}_out_of_range`);
    }
    return parsed;
}
function normalizeSurface(raw) {
    const normalized = raw.trim().toLowerCase().replace(/[_\s]/g, '-');
    if (exports.MARKETING_SURFACES.includes(normalized)) {
        return normalized;
    }
    throw new Error(`surface_invalid:${raw}`);
}
function parseSurfaces(raw) {
    if (!raw?.trim()) {
        return [...exports.MARKETING_SURFACES];
    }
    const surfaces = raw.split(',').map(normalizeSurface);
    const seen = new Set();
    const unique = [];
    for (const surface of surfaces) {
        if (!seen.has(surface)) {
            seen.add(surface);
            unique.push(surface);
        }
    }
    return unique;
}
function parseAudioPolicy(raw) {
    const normalized = raw?.trim().toLowerCase() || DEFAULT_AUDIO_POLICY;
    if (exports.MARKETING_AUDIO_POLICIES.includes(normalized)) {
        return normalized;
    }
    throw new Error('audio_policy_invalid');
}
function parseMarketingCaptureOptions(targetArg, options = {}) {
    const durationSeconds = parsePositiveNumber(options.duration, DEFAULT_DURATION_SECONDS, 'duration', MIN_DURATION_SECONDS, MAX_DURATION_SECONDS);
    const fps = parsePositiveNumber(options.fps, DEFAULT_FPS, 'fps', MIN_FPS, MAX_FPS);
    const outputDir = options.outputDir?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.outputDir.trim()) : null;
    const seed = options.seed?.trim() || `marketing-${new Date().toISOString().slice(0, 10)}`;
    return {
        targetArg,
        appName: options.app?.trim() || undefined,
        surfaces: parseSurfaces(options.surfaces),
        durationSeconds,
        fps,
        audioPolicy: parseAudioPolicy(options.audioPolicy),
        seed,
        outputDir,
        screenDevice: options.screenDevice?.trim() || null,
        keepRaw: Boolean(options.keepRaw),
    };
}
function assertSupportedMarketingCapturePlatform(platform = process.platform) {
    if (platform !== 'darwin' && platform !== 'win32') {
        throw new Error(`unsupported_marketing_capture_platform:${platform}`);
    }
}
function resolveMarketingOutputPaths(projectRoot, _appName, explicitOutputDir) {
    const rootDir = explicitOutputDir ?? (0, node_path_1.join)(projectRoot, 'assets', 'marketing');
    const marketingRoot = (0, node_path_1.join)(projectRoot, 'assets', 'marketing');
    const rootRelativeToMarketing = (0, node_path_1.relative)(marketingRoot, rootDir);
    if (rootRelativeToMarketing === '..' || rootRelativeToMarketing.startsWith(`..${node_path_1.sep}`) || (0, node_path_1.isAbsolute)(rootRelativeToMarketing)) {
        throw new Error(`marketing_output_outside_assets:${rootDir}`);
    }
    return {
        rootDir,
        capturesDir: (0, node_path_1.join)(rootDir, 'captures'),
        playdropDir: (0, node_path_1.join)(rootDir, 'playdrop'),
        socialDir: (0, node_path_1.join)(rootDir, 'social'),
        thumbnailsDir: (0, node_path_1.join)(rootDir, 'thumbnails'),
        screenshotsDir: (0, node_path_1.join)(rootDir, 'screenshots'),
        manifestPath: (0, node_path_1.join)(rootDir, 'capture-manifest.json'),
        reportPath: (0, node_path_1.join)(rootDir, 'marketing-report.json'),
    };
}
function validateMarketingPreviewConfig(rawApp) {
    const errors = [];
    const record = rawApp && typeof rawApp === 'object' ? rawApp : {};
    if (record.previewable !== true) {
        errors.push('catalogue_previewable_missing');
    }
    const preview = record.preview && typeof record.preview === 'object'
        ? record.preview
        : null;
    const audioPolicy = typeof preview?.audioPolicy === 'string' ? preview.audioPolicy.trim() : '';
    if (!audioPolicy) {
        errors.push('catalogue_preview_audio_policy_missing');
    }
    else if (!exports.MARKETING_AUDIO_POLICIES.includes(audioPolicy)) {
        errors.push(`catalogue_preview_audio_policy_invalid:${audioPolicy}`);
    }
    return errors;
}
function buildMarketingFfmpegArgs(input) {
    const videoFilter = [
        `crop=${input.crop.width}:${input.crop.height}:${input.crop.x}:${input.crop.y}`,
        `scale=${input.output.width}:${input.output.height}:flags=lanczos`,
        `fps=${input.fps}`,
    ].join(',');
    const commonTail = [
        '-t',
        String(input.durationSeconds),
        '-vf',
        videoFilter,
        '-c:v',
        'libx264',
        '-preset',
        'slow',
        '-crf',
        '16',
        '-pix_fmt',
        'yuv420p',
        '-movflags',
        '+faststart',
        input.output.path,
    ];
    if (input.platform === 'darwin') {
        const device = input.inputDevice?.trim() || process.env.PLAYDROP_MARKETING_CAPTURE_AVFOUNDATION_DEVICE?.trim() || '1';
        return [
            '-y',
            '-f',
            'avfoundation',
            '-framerate',
            String(input.fps),
            '-capture_cursor',
            '0',
            '-i',
            `${device}:none`,
            ...commonTail,
        ];
    }
    if (input.platform === 'win32') {
        return [
            '-y',
            '-f',
            'gdigrab',
            '-framerate',
            String(input.fps),
            '-offset_x',
            String(input.crop.x),
            '-offset_y',
            String(input.crop.y),
            '-video_size',
            `${input.crop.width}x${input.crop.height}`,
            '-i',
            'desktop',
            '-t',
            String(input.durationSeconds),
            '-vf',
            `scale=${input.output.width}:${input.output.height}:flags=lanczos,fps=${input.fps}`,
            '-c:v',
            'libx264',
            '-preset',
            'slow',
            '-crf',
            '16',
            '-pix_fmt',
            'yuv420p',
            '-movflags',
            '+faststart',
            input.output.path,
        ];
    }
    throw new Error(`unsupported_marketing_capture_platform:${input.platform}`);
}
function buildMarketingCaptureManifest(input) {
    return {
        appName: input.appName,
        createdAt: input.createdAt,
        captureSource: 'playdrop-cli-local-screen',
        captureEngine: input.captureEngine,
        audioPolicy: input.audioPolicy,
        seed: input.seed,
        captures: input.captures,
    };
}
function getMarketingCaptureEngine(platform) {
    if (platform === 'darwin') {
        return 'avfoundation';
    }
    if (platform === 'win32') {
        return 'gdigrab';
    }
    throw new Error(`unsupported_marketing_capture_platform:${platform}`);
}
function commandExists(command) {
    const result = (0, node_child_process_1.spawnSync)(command, ['-version'], {
        encoding: 'utf8',
        maxBuffer: 4 * 1024 * 1024,
    });
    return result.status === 0;
}
function runTool(command, args, failureCode) {
    const result = (0, node_child_process_1.spawnSync)(command, args, {
        encoding: 'utf8',
        maxBuffer: 20 * 1024 * 1024,
    });
    if (result.status !== 0) {
        const stdout = result.stdout?.trim() || '';
        const stderr = result.stderr?.trim() || '';
        const combined = [stdout, stderr].filter(Boolean).join('\n');
        throw new Error(`${failureCode}:${combined}`);
    }
    return {
        stdout: result.stdout || '',
        stderr: result.stderr || '',
    };
}
function readFfmpegCapabilities() {
    const encoders = runTool('ffmpeg', ['-hide_banner', '-encoders'], 'ffmpeg_probe_failed');
    const formats = runTool('ffmpeg', ['-hide_banner', '-formats'], 'ffmpeg_probe_failed');
    return `${encoders.stdout}\n${encoders.stderr}\n${formats.stdout}\n${formats.stderr}`;
}
function validateFfmpegCapabilities(platform) {
    const capabilities = readFfmpegCapabilities();
    const errors = [];
    if (!/\blibx264\b/.test(capabilities)) {
        errors.push('ffmpeg_libx264_missing');
    }
    if (platform === 'darwin' && !/\bavfoundation\b/.test(capabilities)) {
        errors.push('ffmpeg_avfoundation_missing');
    }
    if (platform === 'win32' && !/\bgdigrab\b/.test(capabilities)) {
        errors.push('ffmpeg_gdigrab_missing');
    }
    return errors;
}
async function ensureOutputDirectories(paths) {
    await (0, promises_1.mkdir)(paths.capturesDir, { recursive: true });
    await (0, promises_1.mkdir)(paths.playdropDir, { recursive: true });
    await (0, promises_1.mkdir)(paths.socialDir, { recursive: true });
    await (0, promises_1.mkdir)(paths.thumbnailsDir, { recursive: true });
    await (0, promises_1.mkdir)(paths.screenshotsDir, { recursive: true });
    await (0, promises_1.access)(paths.rootDir);
}
function loadCatalogueApp(cataloguePath, appName) {
    if (!cataloguePath || !(0, node_fs_1.existsSync)(cataloguePath)) {
        return null;
    }
    const raw = JSON.parse(require('node:fs').readFileSync(cataloguePath, 'utf8'));
    const apps = raw && typeof raw === 'object' && Array.isArray(raw.apps)
        ? raw.apps
        : Array.isArray(raw)
            ? raw
            : [];
    return apps.find(entry => entry && typeof entry === 'object' && entry.name === appName) ?? null;
}
function formatMarketingError(error) {
    if (error.message.startsWith('unsupported_marketing_capture_platform:')) {
        const platform = error.message.split(':')[1] || process.platform;
        return {
            message: `Marketing capture only supports macOS and Windows in v1. Current platform: ${platform}.`,
            suggestions: [],
        };
    }
    if (error.message === 'ffmpeg_missing') {
        return {
            message: 'ffmpeg is required for local marketing capture.',
            suggestions: ['Install ffmpeg and make sure it is available on PATH.'],
        };
    }
    if (error.message === 'ffprobe_missing') {
        return {
            message: 'ffprobe is required for local marketing capture.',
            suggestions: ['Install ffmpeg and make sure ffprobe is available on PATH.'],
        };
    }
    if (error.message === 'ffmpeg_libx264_missing') {
        return {
            message: 'ffmpeg must support the libx264 encoder for marketing capture.',
            suggestions: ['Install a full ffmpeg build with libx264 enabled.'],
        };
    }
    if (error.message === 'ffmpeg_avfoundation_missing') {
        return {
            message: 'ffmpeg must support avfoundation screen capture on macOS.',
            suggestions: ['Install a macOS ffmpeg build with avfoundation enabled.'],
        };
    }
    if (error.message === 'ffmpeg_gdigrab_missing') {
        return {
            message: 'ffmpeg must support gdigrab screen capture on Windows.',
            suggestions: ['Install a Windows ffmpeg build with gdigrab enabled.'],
        };
    }
    if (error.message.startsWith('marketing_output_outside_assets:')) {
        return {
            message: 'Marketing capture output must stay under assets/marketing/.',
            suggestions: ['Remove --output-dir or set it to assets/marketing or a subdirectory inside assets/marketing.'],
        };
    }
    if (error.message === 'catalogue_previewable_missing') {
        return {
            message: 'catalogue.json must mark the app as previewable before marketing capture.',
            suggestions: ['Set previewable: true on the app entry.'],
        };
    }
    if (error.message === 'catalogue_preview_audio_policy_missing') {
        return {
            message: 'catalogue.json must declare preview.audioPolicy before marketing capture.',
            suggestions: ['Set preview.audioPolicy to music-and-sfx, sfx-only, or silent.'],
        };
    }
    if (error.message.startsWith('catalogue_preview_audio_policy_invalid:')) {
        return {
            message: 'catalogue.json preview.audioPolicy must be music-and-sfx, sfx-only, or silent.',
            suggestions: [],
        };
    }
    if (error.message.startsWith('ffmpeg_failed:') || error.message.startsWith('ffprobe_failed:')) {
        return {
            message: error.message.slice(error.message.indexOf(':') + 1).trim() || 'ffmpeg failed.',
            suggestions: [],
        };
    }
    if (error.message === 'marketing_preview_hook_missing') {
        return {
            message: 'The game did not expose window.__listingCapture.prepare for marketing preview capture.',
            suggestions: ['Implement the PlayDrop preview hook before running marketing capture.'],
        };
    }
    if (error.message === 'marketing_audio_capture_hook_missing') {
        return {
            message: 'The game did not expose preview audio capture hooks.',
            suggestions: ['Expose startAudioCapture and stopAudioCapture when preview audioPolicy is not silent.'],
        };
    }
    if (error.message.startsWith('marketing_capture_wrong_window:')) {
        const [, surface, similarity] = error.message.split(':');
        return {
            message: `Marketing capture for ${surface || 'the surface'} does not match the PlayDrop preview frame. Similarity: ${similarity || 'unknown'}.`,
            suggestions: [
                'Make sure the visible Chromium window is on the selected capture display.',
                'On macOS, rerun with the correct --screen-device value for the display that contains the PlayDrop preview.',
                'Do not continue with manual or browser-frame capture output.',
            ],
        };
    }
    if (error.message.startsWith('marketing_capture_audio_too_quiet:')) {
        const [, surface, integratedLufs, peakDb] = error.message.split(':');
        return {
            message: `Marketing capture audio for ${surface || 'the surface'} is too quiet (${integratedLufs || '?'} LUFS, peak ${peakDb || '?'} dB).`,
            suggestions: [
                'Add or raise preview background music and SFX through PlayDrop catalogue audio or PlayDrop AI generation.',
                'Rerun marketing capture after the preview audio is clearly audible.',
            ],
        };
    }
    if (error.message === 'preview_game_frame_missing') {
        return {
            message: 'The local dev preview did not expose a playable game iframe for capture.',
            suggestions: ['Run playdrop project validate . and confirm the app opens on /dev-preview.'],
        };
    }
    if (error.message === 'preview_game_frame_hidden') {
        return {
            message: 'The local dev preview game frame is hidden or has no measurable size.',
            suggestions: ['Make sure the preview scene renders the game immediately without a menu or loading screen.'],
        };
    }
    return {
        message: error.message,
        suggestions: [],
    };
}
function ensureToolingAvailable(platform) {
    assertSupportedMarketingCapturePlatform(platform);
    if (!commandExists('ffmpeg')) {
        throw new Error('ffmpeg_missing');
    }
    if (!commandExists('ffprobe')) {
        throw new Error('ffprobe_missing');
    }
    const capabilityErrors = validateFfmpegCapabilities(platform);
    if (capabilityErrors.length > 0) {
        throw new Error(capabilityErrors[0]);
    }
}
function resolveProjectRoot(resolvedTarget) {
    if (resolvedTarget.cataloguePath) {
        return (0, node_path_1.dirname)(resolvedTarget.cataloguePath);
    }
    const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
    return projectInfo.projectDir ?? (0, node_path_1.dirname)(resolvedTarget.htmlPath);
}
async function waitForPreviewGameFrame(page, timeoutMs) {
    const frameHandle = await page.waitForSelector(CAPTURE_FRAME_SELECTOR, {
        state: 'attached',
        timeout: timeoutMs,
    });
    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
        const frame = await frameHandle.contentFrame();
        if (frame) {
            return frame;
        }
        await page.waitForTimeout(200);
    }
    throw new Error('preview_game_frame_missing');
}
async function prepareMarketingPreview(page, surface, audioPolicy, seed) {
    const frame = await waitForPreviewGameFrame(page, 60000);
    const preset = SURFACE_PRESETS[surface];
    await frame.waitForFunction(() => {
        const captureWindow = window;
        return typeof captureWindow.__listingCapture?.prepare === 'function';
    }, undefined, { timeout: 60000 });
    await frame.evaluate(async (payload) => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.prepare !== 'function') {
            throw new Error('marketing_preview_hook_missing');
        }
        await hook.prepare(payload);
    }, {
        active: true,
        sceneId: preset.sceneId,
        surface,
        seed,
        audioPolicy,
    });
}
async function startMarketingAudioCapture(page) {
    const frame = await waitForPreviewGameFrame(page, 20000);
    await frame.evaluate(async () => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.startAudioCapture !== 'function') {
            throw new Error('marketing_audio_capture_hook_missing');
        }
        await hook.startAudioCapture();
    });
}
function resolveExportedAudioFileName(mimeType) {
    if (mimeType.includes('webm')) {
        return 'preview-audio.webm';
    }
    if (mimeType.includes('mp4') || mimeType.includes('aac') || mimeType.includes('m4a')) {
        return 'preview-audio.m4a';
    }
    throw new Error(`unsupported_marketing_audio_mime_type:${mimeType}`);
}
async function stopMarketingAudioCapture(page, outputDir) {
    const frame = await waitForPreviewGameFrame(page, 20000);
    const exportedAudio = await frame.evaluate(async () => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.stopAudioCapture !== 'function') {
            throw new Error('marketing_audio_capture_hook_missing');
        }
        return hook.stopAudioCapture();
    });
    const filePath = (0, node_path_1.join)(outputDir, resolveExportedAudioFileName(exportedAudio.mimeType));
    await (0, promises_1.writeFile)(filePath, Buffer.from(exportedAudio.base64, 'base64'));
    return filePath;
}
async function measurePreviewGameFrame(page) {
    const cdpSession = await page.context().newCDPSession(page);
    const windowState = await cdpSession.send('Browser.getWindowForTarget');
    const bounds = windowState.bounds ?? {};
    return await page.evaluate((input) => {
        const frame = document.querySelector(input.selector);
        if (!(frame instanceof HTMLIFrameElement)) {
            throw new Error('preview_game_frame_missing');
        }
        const rect = frame.getBoundingClientRect();
        if (rect.width <= 0 || rect.height <= 0) {
            throw new Error('preview_game_frame_hidden');
        }
        return {
            outerWidth: window.outerWidth,
            outerHeight: window.outerHeight,
            innerWidth: window.innerWidth,
            innerHeight: window.innerHeight,
            windowX: Math.round(input.windowBounds.left ?? 0),
            windowY: Math.round(input.windowBounds.top ?? 0),
            devicePixelRatio: window.devicePixelRatio,
            iframeRect: {
                x: rect.x,
                y: rect.y,
                width: rect.width,
                height: rect.height,
            },
        };
    }, {
        selector: CAPTURE_FRAME_SELECTOR,
        windowBounds: {
            left: bounds.left,
            top: bounds.top,
            width: bounds.width,
            height: bounds.height,
        },
    });
}
async function fitWindowToSurface(page, surface) {
    const cdpSession = await page.context().newCDPSession(page);
    const preset = SURFACE_PRESETS[surface];
    let measurement = await measurePreviewGameFrame(page);
    for (let attempt = 0; attempt < 5; attempt += 1) {
        const deltaWidth = preset.viewport.width - Math.round(measurement.iframeRect.width);
        const deltaHeight = preset.viewport.height - Math.round(measurement.iframeRect.height);
        if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
            return measurement;
        }
        const windowState = await cdpSession.send('Browser.getWindowForTarget');
        const currentWidth = typeof windowState.bounds.width === 'number' ? windowState.bounds.width : Math.round(measurement.outerWidth);
        const currentHeight = typeof windowState.bounds.height === 'number' ? windowState.bounds.height : Math.round(measurement.outerHeight);
        await cdpSession.send('Browser.setWindowBounds', {
            windowId: windowState.windowId,
            bounds: {
                width: Math.max(400, currentWidth + deltaWidth),
                height: Math.max(300, currentHeight + deltaHeight),
            },
        });
        await page.waitForTimeout(800);
        measurement = await measurePreviewGameFrame(page);
    }
    return measurement;
}
function computeScreenCrop(measurement, platform) {
    const scale = platform === 'darwin' ? measurement.devicePixelRatio || 1 : 1;
    const x = Math.round((measurement.windowX + measurement.iframeRect.x) * scale);
    const y = Math.round((measurement.windowY + measurement.iframeRect.y) * scale);
    const width = Math.round(measurement.iframeRect.width * scale);
    const height = Math.round(measurement.iframeRect.height * scale);
    if (width <= 0 || height <= 0) {
        throw new Error('invalid_marketing_capture_crop');
    }
    return { x, y, width, height };
}
function probeMediaFile(filePath) {
    const { stdout } = runTool('ffprobe', [
        '-v',
        'error',
        '-show_streams',
        '-show_format',
        '-of',
        'json',
        filePath,
    ], 'ffprobe_failed');
    const probe = JSON.parse(stdout);
    const video = probe.streams?.find(stream => stream.codec_type === 'video');
    if (!video || typeof video.width !== 'number' || typeof video.height !== 'number') {
        throw new Error('marketing_video_stream_missing');
    }
    const durationSeconds = probe.format?.duration ? Number.parseFloat(probe.format.duration) : Number.NaN;
    if (!Number.isFinite(durationSeconds)) {
        throw new Error('marketing_duration_missing');
    }
    return {
        width: video.width,
        height: video.height,
        durationSeconds,
        audioTracks: probe.streams?.filter(stream => stream.codec_type === 'audio').length ?? 0,
    };
}
function muxMarketingAudio(videoPath, audioPath) {
    const muxedPath = `${videoPath}.muxed.mp4`;
    runTool('ffmpeg', [
        '-y',
        '-i',
        videoPath,
        '-i',
        audioPath,
        '-c:v',
        'copy',
        '-c:a',
        'aac',
        '-shortest',
        muxedPath,
    ], 'ffmpeg_failed');
    require('node:fs').renameSync(muxedPath, videoPath);
}
function extractPoster(videoPath, posterPath) {
    runTool('ffmpeg', [
        '-y',
        '-ss',
        '2',
        '-i',
        videoPath,
        '-frames:v',
        '1',
        posterPath,
    ], 'ffmpeg_failed');
}
async function capturePreviewReferenceFrame(page, outputPath) {
    await page.locator(CAPTURE_FRAME_SELECTOR).screenshot({ path: outputPath });
}
function calculatePreviewFrameSimilarity(referencePath, posterPath) {
    const { stderr } = runTool('ffmpeg', [
        '-hide_banner',
        '-i',
        referencePath,
        '-i',
        posterPath,
        '-filter_complex',
        '[0:v]scale=320:320:force_original_aspect_ratio=decrease,pad=320:320:(ow-iw)/2:(oh-ih)/2,format=yuv420p[ref];[1:v]scale=320:320:force_original_aspect_ratio=decrease,pad=320:320:(ow-iw)/2:(oh-ih)/2,format=yuv420p[cap];[ref][cap]ssim',
        '-f',
        'null',
        '-',
    ], 'ffmpeg_failed');
    const match = stderr.match(/All:([0-9.]+)/);
    if (!match) {
        throw new Error('marketing_capture_similarity_missing');
    }
    return Number.parseFloat(match[1]);
}
function validatePreviewFrameSimilarity(surface, similarity) {
    if (!Number.isFinite(similarity) || similarity < MIN_MARKETING_PREVIEW_FRAME_SIMILARITY) {
        throw new Error(`marketing_capture_wrong_window:${surface}:${Number.isFinite(similarity) ? similarity.toFixed(3) : 'unknown'}`);
    }
}
function measureMarketingAudio(videoPath) {
    const ebur = runTool('ffmpeg', [
        '-hide_banner',
        '-nostats',
        '-i',
        videoPath,
        '-filter_complex',
        'ebur128',
        '-f',
        'null',
        '-',
    ], 'ffmpeg_failed');
    const eburText = `${ebur.stdout}\n${ebur.stderr}`;
    const integratedMatches = [...eburText.matchAll(/I:\s*(-?\d+(?:\.\d+)?) LUFS/g)];
    const integratedLufs = integratedMatches.length
        ? Number.parseFloat(integratedMatches[integratedMatches.length - 1]?.[1] ?? '')
        : Number.NaN;
    const volume = runTool('ffmpeg', [
        '-hide_banner',
        '-nostats',
        '-i',
        videoPath,
        '-af',
        'volumedetect',
        '-f',
        'null',
        '-',
    ], 'ffmpeg_failed');
    const volumeText = `${volume.stdout}\n${volume.stderr}`;
    const peakMatch = volumeText.match(/max_volume:\s*(-?\d+(?:\.\d+)?) dB/);
    const peakDb = peakMatch ? Number.parseFloat(peakMatch[1] ?? '') : Number.NaN;
    if (!Number.isFinite(integratedLufs) || !Number.isFinite(peakDb)) {
        throw new Error('marketing_audio_metrics_missing');
    }
    return { integratedLufs, peakDb };
}
function validateMarketingAudio(surface, metrics) {
    if (metrics.integratedLufs < MIN_MARKETING_AUDIO_INTEGRATED_LUFS || metrics.peakDb < MIN_MARKETING_AUDIO_PEAK_DB) {
        throw new Error(`marketing_capture_audio_too_quiet:${surface}:${metrics.integratedLufs.toFixed(1)}:${metrics.peakDb.toFixed(1)}`);
    }
}
function createMarketingReport(input) {
    const warnings = ['Exciting moment validation requires visual review from the marketing-pack workflow.'];
    if (!input.audioValidated) {
        warnings.push('Required audio was not validated.');
    }
    return {
        appName: input.appName,
        createdAt: input.createdAt,
        status: 'passed',
        durationSeconds: Math.round((Date.now() - input.startedAt) / 1000),
        gates: [
            {
                id: 'marketing-capture',
                status: 'passed',
                summary: 'Local marketing capture completed.',
            },
        ],
        assetCounts: {
            captures: input.surfaces.length,
            videos: 0,
            images: 0,
            listingArt: 0,
        },
        captureValidation: {
            surfaces: input.surfaces,
            fps: input.fps,
            audioValidated: input.audioValidated,
            excitingMomentValidated: false,
        },
        timings: {
            captureSeconds: Math.round((Date.now() - input.startedAt) / 1000),
            assetRenderSeconds: 0,
            documentationSeconds: 0,
        },
        warnings,
    };
}
async function resolveMarketingTarget(parsedOptions) {
    try {
        return (0, devShared_1.resolveDevTarget)(parsedOptions.targetArg, parsedOptions.appName);
    }
    catch (error) {
        (0, messages_1.printErrorWithHelp)(error?.message || 'Unable to resolve marketing capture target.', [
            'Run the command inside a PlayDrop workspace or pass an app target.',
        ], { command: 'project marketing capture' });
        process.exitCode = 1;
        return null;
    }
}
async function marketingDoctor(targetArg, options = {}) {
    let parsedOptions;
    try {
        parsedOptions = parseMarketingCaptureOptions(targetArg, options);
        ensureToolingAvailable(process.platform);
    }
    catch (error) {
        const detail = formatMarketingError(error instanceof Error ? error : new Error(String(error)));
        (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project marketing doctor' });
        process.exitCode = 1;
        return;
    }
    const resolvedTarget = await resolveMarketingTarget(parsedOptions);
    if (!resolvedTarget) {
        return;
    }
    const projectRoot = resolveProjectRoot(resolvedTarget);
    const paths = resolveMarketingOutputPaths(projectRoot, resolvedTarget.appName, parsedOptions.outputDir);
    try {
        await ensureOutputDirectories(paths);
        const app = loadCatalogueApp(resolvedTarget.cataloguePath, resolvedTarget.appName);
        const previewErrors = validateMarketingPreviewConfig(app);
        if (previewErrors.length > 0) {
            throw new Error(previewErrors[0]);
        }
        const userDataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-marketing-doctor-'));
        const context = await (0, playwright_1.launchPersistentChromiumContext)(userDataDir, { viewport: SURFACE_PRESETS.desktop.viewport });
        await context.close();
        await (0, promises_1.rm)(userDataDir, { recursive: true, force: true });
    }
    catch (error) {
        const detail = formatMarketingError(error instanceof Error ? error : new Error(String(error)));
        (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project marketing doctor' });
        process.exitCode = 1;
        return;
    }
    console.log('[marketing] Doctor passed.');
}
async function marketingCapture(targetArg, options = {}) {
    const startedAt = Date.now();
    let parsedOptions;
    try {
        parsedOptions = parseMarketingCaptureOptions(targetArg, options);
        ensureToolingAvailable(process.platform);
    }
    catch (error) {
        const detail = formatMarketingError(error instanceof Error ? error : new Error(String(error)));
        (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project marketing capture' });
        process.exitCode = 1;
        return;
    }
    const resolvedTarget = await resolveMarketingTarget(parsedOptions);
    if (!resolvedTarget) {
        return;
    }
    let appName = resolvedTarget.appName;
    let appTypeSlug = (0, appUrls_1.getAppTypeSlug)(null);
    try {
        const match = (0, catalogue_utils_1.findAppDefinition)(resolvedTarget.htmlPath);
        appName = match.name;
        appTypeSlug = (0, appUrls_1.getAppTypeSlug)(match.type);
    }
    catch {
        // Keep resolved target defaults for direct-file targets.
    }
    const projectRoot = resolveProjectRoot(resolvedTarget);
    const outputPaths = resolveMarketingOutputPaths(projectRoot, appName, parsedOptions.outputDir);
    const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
    const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
    await (0, commandContext_1.withEnvironment)('project marketing capture', 'Capturing marketing media', async ({ client, env, envConfig }) => {
        let currentUsername = '';
        try {
            const currentUser = await (0, devShared_1.fetchDevUser)(client);
            currentUsername = currentUser.username.trim();
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Authentication');
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.ApiError) {
                (0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
                    'Run "playdrop auth login" to refresh your session.',
                    'Use "playdrop auth whoami" afterwards to verify the current account.',
                ], { command: 'project marketing capture' });
                process.exitCode = 1;
                return;
            }
            if ((0, devShared_1.isNetworkError)(error)) {
                (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project marketing capture');
                process.exitCode = 1;
                return;
            }
            throw error;
        }
        const taskLookup = (0, catalogue_1.findAppTaskByFile)(resolvedTarget.htmlPath);
        if (taskLookup.errors.length > 0) {
            (0, messages_1.printErrorWithHelp)(taskLookup.errors[0] || 'Failed to resolve the app task from catalogue.json.', taskLookup.errors.slice(1), {
                command: 'project marketing capture',
            });
            process.exitCode = 1;
            return;
        }
        let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
        if (taskLookup.task) {
            try {
                runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
                    client,
                    apiBase: envConfig.apiBase,
                    task: taskLookup.task,
                    creatorUsername: currentUsername,
                    appBaseUrl: new URL('.', (0, devServer_1.buildLocalDevAppUrl)({
                        creatorUsername: currentUsername,
                        appType: appTypeSlug,
                        appName,
                        port: devServer_1.DEV_ROUTER_PORT,
                    })).toString(),
                });
            }
            catch (error) {
                const formatted = (0, dev_1.formatDevRuntimeAssetManifestFailure)(error);
                (0, messages_1.printErrorWithHelp)(formatted.message, formatted.suggestions, { command: 'project marketing capture' });
                process.exitCode = 1;
                return;
            }
        }
        await ensureOutputDirectories(outputPaths);
        const app = loadCatalogueApp(resolvedTarget.cataloguePath, appName);
        const previewErrors = validateMarketingPreviewConfig(app);
        if (previewErrors.length > 0) {
            const detail = formatMarketingError(new Error(previewErrors[0] || 'Marketing preview configuration is invalid.'));
            (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project marketing capture' });
            process.exitCode = 1;
            return;
        }
        const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
            creatorUsername: currentUsername,
            appType: appTypeSlug,
            appName,
            port: devServer_1.DEV_ROUTER_PORT,
        }, 750);
        let serverHandle = null;
        let userDataDir = null;
        let context = null;
        const cleanup = async () => {
            if (context) {
                await context.close().catch(() => { });
            }
            if (userDataDir) {
                await (0, promises_1.rm)(userDataDir, { recursive: true, force: true }).catch(() => { });
            }
            if (serverHandle) {
                await serverHandle.close().catch(() => { });
            }
        };
        try {
            if (serverAlreadyRunning) {
                await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
                    creatorUsername: currentUsername,
                    appName,
                    runtimeAssetManifest,
                    port: devServer_1.DEV_ROUTER_PORT,
                });
            }
            else {
                serverHandle = await (0, devServer_1.startDevServer)({
                    appName,
                    appType: appTypeSlug,
                    creatorUsername: currentUsername,
                    htmlPath: resolvedTarget.htmlPath,
                    port: devServer_1.DEV_ROUTER_PORT,
                    projectInfo,
                    runtimeAssetManifest,
                });
            }
            if (!serverAlreadyRunning && projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
                const projectLabel = (0, devShared_1.formatProjectLabel)(projectInfo);
                if (projectLabel) {
                    console.log(`[marketing] package.json detected at ${projectLabel}, but no "dev" script was found. Run your app build manually if needed.`);
                }
            }
            if (!serverAlreadyRunning) {
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
            const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
            const frameUrlObject = new URL(`${(0, appUrls_1.normalizePlaydropWebBase)(webBase)}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/dev-preview`);
            frameUrlObject.searchParams.set('launchCheck', '1');
            const frameUrl = frameUrlObject.toString();
            userDataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-marketing-capture-'));
            const manifestCaptures = [];
            let audioValidated = parsedOptions.audioPolicy === 'silent';
            for (const surface of parsedOptions.surfaces) {
                const preset = SURFACE_PRESETS[surface];
                context = await (0, playwright_1.launchPersistentChromiumContext)(userDataDir, {
                    viewport: preset.viewport,
                    deviceScaleFactor: preset.viewport.deviceScaleFactor,
                    isMobile: preset.viewport.isMobile,
                    hasTouch: preset.viewport.hasTouch,
                    userAgent: preset.viewport.userAgent,
                    automationOrigin: frameUrlObject.origin,
                });
                const page = context.pages()[0] ?? await context.newPage();
                await page.goto(frameUrl, { waitUntil: 'domcontentloaded' });
                await prepareMarketingPreview(page, surface, parsedOptions.audioPolicy, parsedOptions.seed);
                await page.waitForTimeout(1000);
                const measurement = await fitWindowToSurface(page, surface);
                const crop = computeScreenCrop(measurement, process.platform);
                await page.bringToFront();
                await page.waitForTimeout(250);
                const videoPath = (0, node_path_1.join)(outputPaths.capturesDir, `${surface}.mp4`);
                const posterPath = (0, node_path_1.join)(outputPaths.capturesDir, `${surface}-poster.png`);
                const referencePath = (0, node_path_1.join)(outputPaths.capturesDir, `${surface}-reference.png`);
                await capturePreviewReferenceFrame(page, referencePath);
                const args = buildMarketingFfmpegArgs({
                    platform: process.platform,
                    fps: parsedOptions.fps,
                    durationSeconds: parsedOptions.durationSeconds,
                    inputDevice: parsedOptions.screenDevice,
                    crop,
                    output: {
                        width: preset.width,
                        height: preset.height,
                        path: videoPath,
                    },
                });
                let audioMetrics = null;
                if (parsedOptions.audioPolicy !== 'silent') {
                    await startMarketingAudioCapture(page);
                }
                runTool('ffmpeg', args, 'ffmpeg_failed');
                if (parsedOptions.audioPolicy !== 'silent') {
                    const audioPath = await stopMarketingAudioCapture(page, outputPaths.capturesDir);
                    muxMarketingAudio(videoPath, audioPath);
                    audioMetrics = measureMarketingAudio(videoPath);
                    validateMarketingAudio(surface, audioMetrics);
                    audioValidated = true;
                }
                extractPoster(videoPath, posterPath);
                const previewFrameSimilarity = calculatePreviewFrameSimilarity(referencePath, posterPath);
                validatePreviewFrameSimilarity(surface, previewFrameSimilarity);
                if (!parsedOptions.keepRaw) {
                    await (0, promises_1.rm)(referencePath, { force: true }).catch(() => { });
                }
                const probe = probeMediaFile(videoPath);
                if (probe.width !== preset.width || probe.height !== preset.height) {
                    throw new Error(`marketing_capture_dimension_mismatch:${surface}:${probe.width}x${probe.height}`);
                }
                if (parsedOptions.audioPolicy !== 'silent' && probe.audioTracks === 0) {
                    throw new Error(`marketing_capture_audio_missing:${surface}`);
                }
                manifestCaptures.push({
                    surface,
                    path: (0, node_path_1.relative)(projectRoot, videoPath),
                    width: preset.width,
                    height: preset.height,
                    fps: parsedOptions.fps,
                    durationSeconds: probe.durationSeconds,
                    hasAudio: parsedOptions.audioPolicy !== 'silent',
                    audio: {
                        policy: parsedOptions.audioPolicy,
                        backgroundMusic: parsedOptions.audioPolicy === 'music-and-sfx',
                        sfx: parsedOptions.audioPolicy !== 'silent',
                        peakDb: audioMetrics?.peakDb ?? null,
                        integratedLufs: audioMetrics?.integratedLufs ?? null,
                    },
                    validation: {
                        previewFrameSimilarity,
                    },
                });
                await context.close();
                context = null;
            }
            const createdAt = new Date().toISOString();
            const manifest = buildMarketingCaptureManifest({
                appName,
                createdAt,
                audioPolicy: parsedOptions.audioPolicy,
                seed: parsedOptions.seed,
                captureEngine: getMarketingCaptureEngine(process.platform),
                captures: manifestCaptures,
            });
            const report = createMarketingReport({
                appName,
                startedAt,
                createdAt,
                surfaces: parsedOptions.surfaces,
                fps: parsedOptions.fps,
                audioValidated,
            });
            await (0, promises_1.writeFile)(outputPaths.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
            await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
            console.log(`[marketing] Saved capture manifest to ${(0, node_path_1.relative)(process.cwd(), outputPaths.manifestPath) || outputPaths.manifestPath}`);
            console.log(`[marketing] Saved report to ${(0, node_path_1.relative)(process.cwd(), outputPaths.reportPath) || outputPaths.reportPath}`);
        }
        catch (error) {
            const detail = formatMarketingError(error instanceof Error ? error : new Error(String(error)));
            (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project marketing capture' });
            process.exitCode = 1;
        }
        finally {
            await cleanup();
        }
    }, { workspacePath: resolvedTarget.cataloguePath ?? (0, node_path_1.dirname)(resolvedTarget.htmlPath) });
}
