"use strict";
/* eslint-disable max-lines */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveListingCaptureRouteSegment = resolveListingCaptureRouteSegment;
exports.resolveListingCaptureSceneId = resolveListingCaptureSceneId;
exports.parseCaptureListingOptions = parseCaptureListingOptions;
exports.assertSupportedListingEnvironment = assertSupportedListingEnvironment;
exports.resolveListingRecorderPath = resolveListingRecorderPath;
exports.computeRecordedCrop = computeRecordedCrop;
exports.captureListing = captureListing;
const node_child_process_1 = require("node:child_process");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const types_1 = require("@playdrop/types");
const catalogue_utils_1 = require("../catalogue-utils");
const commandContext_1 = require("../commandContext");
const http_1 = require("../http");
const messages_1 = require("../messages");
const playwright_1 = require("../playwright");
const devShared_1 = require("./devShared");
const devServer_1 = require("./devServer");
const appUrls_1 = require("../appUrls");
const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
const DEFAULT_DURATION_SECONDS = 8;
const DEFAULT_WIDTH = 1280;
const DEFAULT_HEIGHT = 720;
const DEFAULT_FPS = 30;
const DEFAULT_POSTER_AT_SECONDS = 2;
const MAX_CAPTURE_SECONDS = 300;
const MIN_CAPTURE_SECONDS = 1;
const MIN_DIMENSION = 16;
const MAX_DIMENSION = 8192;
const MAX_FPS = 120;
const SUPPORTED_MACOS_PREFIX = '26.4';
const LISTING_CAPTURE_HOOK_TIMEOUT_MS = 60000;
function resolveListingCaptureRouteSegment(previewable) {
    return previewable ? 'dev-preview' : 'dev';
}
function resolveListingCaptureSceneId(width, height) {
    return height > width ? 'listing-portrait' : 'listing-landscape';
}
function parsePositiveNumber(raw, fallback, fieldName, { minimum = 0, maximum = Number.POSITIVE_INFINITY, integer = false, } = {}) {
    if (raw === undefined) {
        return fallback;
    }
    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`);
    }
    if (integer && !Number.isInteger(parsed)) {
        throw new Error(`${fieldName}_invalid`);
    }
    return parsed;
}
function parseCaptureListingOptions(targetArg, options = {}) {
    const durationSeconds = parsePositiveNumber(options.duration, DEFAULT_DURATION_SECONDS, 'duration', {
        minimum: MIN_CAPTURE_SECONDS,
        maximum: MAX_CAPTURE_SECONDS,
    });
    const width = parsePositiveNumber(options.width, DEFAULT_WIDTH, 'width', {
        minimum: MIN_DIMENSION,
        maximum: MAX_DIMENSION,
        integer: true,
    });
    const height = parsePositiveNumber(options.height, DEFAULT_HEIGHT, 'height', {
        minimum: MIN_DIMENSION,
        maximum: MAX_DIMENSION,
        integer: true,
    });
    const fps = parsePositiveNumber(options.fps, DEFAULT_FPS, 'fps', {
        minimum: 1,
        maximum: MAX_FPS,
        integer: true,
    });
    const posterAtSeconds = parsePositiveNumber(options.posterAt, DEFAULT_POSTER_AT_SECONDS, 'poster_at', {
        minimum: 0,
        maximum: durationSeconds,
    });
    const outputDir = options.outputDir?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.outputDir.trim()) : null;
    return {
        targetArg,
        appName: options.app?.trim() || undefined,
        durationSeconds,
        width,
        height,
        fps,
        posterAtSeconds,
        audio: Boolean(options.audio),
        outputDir,
        keepRaw: Boolean(options.keepRaw),
    };
}
function readMacOsVersion() {
    const result = (0, node_child_process_1.spawnSync)('sw_vers', ['-productVersion'], {
        encoding: 'utf8',
    });
    if (result.status !== 0) {
        throw new Error('macos_version_probe_failed');
    }
    return result.stdout.trim();
}
function assertSupportedListingEnvironment(platform = process.platform, macosVersion = platform === 'darwin' ? readMacOsVersion() : '') {
    if (platform !== 'darwin') {
        throw new Error('unsupported_platform');
    }
    if (!macosVersion.startsWith(SUPPORTED_MACOS_PREFIX)) {
        throw new Error(`unsupported_macos_version:${macosVersion}`);
    }
}
function commandExists(command) {
    const result = (0, node_child_process_1.spawnSync)('which', [command], {
        encoding: 'utf8',
    });
    return result.status === 0 && result.stdout.trim().length > 0;
}
async function ensureExecutableFile(filePath) {
    await (0, promises_1.access)(filePath);
}
function resolveListingRecorderPath(baseDir = __dirname) {
    return (0, node_path_1.resolve)(baseDir, '..', '..', '..', '..', 'clients', 'apple', 'playdrop-listing-recorder', '.build', 'release', 'playdrop-listing-recorder');
}
function buildTimestampFolder(date = new Date()) {
    const iso = date.toISOString();
    return iso.replace(/[:]/g, '-').replace(/\.\d{3}Z$/, 'Z');
}
async function ensureOutputPaths(appName, explicitOutputDir) {
    const outputDir = explicitOutputDir
        ? explicitOutputDir
        : (0, node_path_1.resolve)(process.cwd(), 'output', 'listing-capture', appName, buildTimestampFolder());
    await (0, promises_1.mkdir)(outputDir, { recursive: true });
    return {
        outputDir,
        rawVideoPath: (0, node_path_1.join)(outputDir, 'listing-raw.mp4'),
        metadataPath: (0, node_path_1.join)(outputDir, 'listing-raw.json'),
        reportPath: (0, node_path_1.join)(outputDir, 'capture-report.json'),
        posterPath: (0, node_path_1.join)(outputDir, 'poster.png'),
        finalVideoPath: (0, node_path_1.join)(outputDir, 'listing.mp4'),
    };
}
async function grantListingBrowserLocalNetworkAccess(context, origin) {
    const grantPermissions = context.grantPermissions;
    if (typeof grantPermissions !== 'function') {
        return;
    }
    await grantPermissions.call(context, ['local-network-access'], { origin });
}
async function launchListingBrowser(initialViewport, automationOrigin) {
    const playwright = await Promise.resolve().then(() => __importStar(require('playwright-core')));
    const args = [
        '--no-sandbox',
        '--disable-web-security',
        '--app=about:blank',
        `--window-size=${initialViewport.width},${initialViewport.height}`,
        '--autoplay-policy=no-user-gesture-required',
        '--disable-extensions',
        '--disable-background-networking',
        '--disable-component-update',
        '--disable-sync',
        '--metrics-recording-only',
        '--no-first-run',
        '--no-default-browser-check',
    ];
    try {
        const server = await playwright.chromium.launchServer({
            headless: false,
            args,
        });
        const processId = server.process()?.pid ?? 0;
        if (processId <= 0) {
            await server.close();
            throw new Error('browser_process_missing');
        }
        const browser = await playwright.chromium.connect(server.wsEndpoint());
        const context = await browser.newContext({ viewport: null });
        await grantListingBrowserLocalNetworkAccess(context, automationOrigin);
        const page = await context.newPage();
        return {
            browser,
            context,
            page,
            server,
            processId,
            close: async () => {
                try {
                    await context.close();
                }
                catch {
                    // ignore close errors
                }
                try {
                    await browser.close();
                }
                catch {
                    // ignore close errors
                }
                try {
                    await server.close();
                }
                catch {
                    // ignore close errors
                }
            },
        };
    }
    catch (error) {
        throw (0, playwright_1.createPlaywrightLaunchError)('playdrop', error);
    }
}
async function waitForHostedGameMeasurement(page, timeoutMs) {
    const startedAt = Date.now();
    let lastSignature = '';
    let stableHits = 0;
    while (Date.now() - startedAt < timeoutMs) {
        try {
            const measurement = await page.evaluate((selector) => {
                const frame = document.querySelector(selector);
                if (!(frame instanceof HTMLIFrameElement)) {
                    throw new Error('hosted_game_frame_missing');
                }
                const rect = frame.getBoundingClientRect();
                if (rect.width <= 0 || rect.height <= 0) {
                    throw new Error('hosted_game_frame_hidden');
                }
                return {
                    outerWidth: window.outerWidth,
                    outerHeight: window.outerHeight,
                    innerWidth: window.innerWidth,
                    innerHeight: window.innerHeight,
                    devicePixelRatio: window.devicePixelRatio,
                    iframeRect: {
                        x: rect.x,
                        y: rect.y,
                        width: rect.width,
                        height: rect.height,
                        top: rect.top,
                        left: rect.left,
                        right: rect.right,
                        bottom: rect.bottom,
                    },
                };
            }, CAPTURE_FRAME_SELECTOR);
            const signature = JSON.stringify([
                Math.round(measurement.outerWidth),
                Math.round(measurement.outerHeight),
                Math.round(measurement.innerWidth),
                Math.round(measurement.innerHeight),
                Math.round(measurement.iframeRect.left),
                Math.round(measurement.iframeRect.top),
                Math.round(measurement.iframeRect.width),
                Math.round(measurement.iframeRect.height),
            ]);
            if (signature === lastSignature) {
                stableHits += 1;
            }
            else {
                lastSignature = signature;
                stableHits = 1;
            }
            if (stableHits >= 3) {
                return measurement;
            }
        }
        catch (error) {
            if (error instanceof Error && error.message !== 'hosted_game_frame_missing' && error.message !== 'hosted_game_frame_hidden') {
                throw error;
            }
        }
        await page.waitForTimeout(250);
    }
    throw new Error('hosted_game_measurement_timeout');
}
async function waitForHostedGameFrame(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('hosted_game_frame_missing');
}
async function prepareHostedListingScene(page, sceneId) {
    const frame = await waitForHostedGameFrame(page, LISTING_CAPTURE_HOOK_TIMEOUT_MS);
    await frame.waitForFunction(() => {
        const captureWindow = window;
        return typeof captureWindow.__listingCapture?.prepare === 'function';
    }, undefined, { timeout: LISTING_CAPTURE_HOOK_TIMEOUT_MS });
    await frame.evaluate(async (requestedSceneId) => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.prepare !== 'function') {
            throw new Error('listing_capture_hook_missing');
        }
        await hook.prepare(requestedSceneId);
    }, sceneId);
}
async function startHostedListingAudioCapture(page) {
    const frame = await waitForHostedGameFrame(page, 20000);
    await frame.waitForFunction(() => {
        const captureWindow = window;
        return typeof captureWindow.__listingCapture?.startAudioCapture === 'function';
    }, undefined, { timeout: 20000 });
    await frame.evaluate(async () => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.startAudioCapture !== 'function') {
            throw new Error('listing_audio_capture_hook_missing');
        }
        await hook.startAudioCapture();
    });
}
function resolveExportedAudioFileName(mimeType) {
    if (mimeType.includes('webm')) {
        return 'listing-audio.webm';
    }
    if (mimeType.includes('mp4') || mimeType.includes('aac') || mimeType.includes('m4a')) {
        return 'listing-audio.m4a';
    }
    throw new Error(`unsupported_listing_audio_mime_type:${mimeType}`);
}
async function stopHostedListingAudioCapture(page, outputDir) {
    const frame = await waitForHostedGameFrame(page, 20000);
    const exportedAudio = await frame.evaluate(async () => {
        const captureWindow = window;
        const hook = captureWindow.__listingCapture;
        if (!hook || typeof hook.stopAudioCapture !== 'function') {
            throw new Error('listing_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 fitWindowToRequestedGameplay(page, requestedWidth, requestedHeight) {
    const cdpSession = await page.context().newCDPSession(page);
    let measurement = await waitForHostedGameMeasurement(page, 15000);
    for (let attempt = 0; attempt < 5; attempt += 1) {
        const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
        const deltaHeight = requestedHeight - 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);
        const nextWidth = Math.max(400, currentWidth + deltaWidth);
        const nextHeight = Math.max(300, currentHeight + deltaHeight);
        await cdpSession.send('Browser.setWindowBounds', {
            windowId: windowState.windowId,
            bounds: {
                width: nextWidth,
                height: nextHeight,
            },
        });
        await page.waitForTimeout(800);
        measurement = await waitForHostedGameMeasurement(page, 8000);
    }
    throw new Error('hosted_game_resize_failed');
}
function roundCropValue(value) {
    return Math.round(value);
}
function computeRecordedCrop(measurement, rawWidth, rawHeight) {
    if (measurement.outerWidth <= 0 || measurement.outerHeight <= 0) {
        throw new Error('invalid_window_geometry');
    }
    if (measurement.innerWidth <= 0 || measurement.innerHeight <= 0) {
        throw new Error('invalid_viewport_geometry');
    }
    if (rawWidth <= 0 || rawHeight <= 0) {
        throw new Error('invalid_raw_dimensions');
    }
    const horizontalChrome = measurement.outerWidth - measurement.innerWidth;
    const verticalChrome = measurement.outerHeight - measurement.innerHeight;
    if (horizontalChrome < 0 || verticalChrome < 0) {
        throw new Error('invalid_chrome_geometry');
    }
    const leftChrome = horizontalChrome / 2;
    const bottomChrome = leftChrome;
    const topChrome = verticalChrome - bottomChrome;
    if (topChrome < 0) {
        throw new Error('invalid_vertical_chrome_geometry');
    }
    const cropLeftCss = leftChrome + measurement.iframeRect.left;
    const cropTopCss = topChrome + measurement.iframeRect.top;
    const scaleX = rawWidth / measurement.outerWidth;
    const scaleY = rawHeight / measurement.outerHeight;
    const x = roundCropValue(cropLeftCss * scaleX);
    const y = roundCropValue(cropTopCss * scaleY);
    const width = roundCropValue(measurement.iframeRect.width * scaleX);
    const height = roundCropValue(measurement.iframeRect.height * scaleY);
    if (x < 0 || y < 0 || width <= 0 || height <= 0) {
        throw new Error('invalid_crop_geometry');
    }
    if (x + width > rawWidth || y + height > rawHeight) {
        throw new Error('crop_out_of_bounds');
    }
    return { x, y, width, height };
}
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 stderr = result.stderr?.trim() || '';
        const stdout = result.stdout?.trim() || '';
        const combined = [stdout, stderr].filter(Boolean).join('\n');
        throw new Error(`${failureCode}:${combined}`);
    }
    return {
        stdout: result.stdout || '',
        stderr: result.stderr || '',
    };
}
async function probeMediaFile(filePath) {
    const { stdout } = runTool('ffprobe', [
        '-v',
        'error',
        '-show_streams',
        '-show_format',
        '-of',
        'json',
        filePath,
    ], 'ffprobe_failed');
    return JSON.parse(stdout);
}
function parseFrameRate(raw) {
    if (!raw) {
        return null;
    }
    const parts = raw.split('/');
    if (parts.length !== 2) {
        const parsed = Number.parseFloat(raw);
        return Number.isFinite(parsed) ? parsed : null;
    }
    const numerator = Number.parseFloat(parts[0]);
    const denominator = Number.parseFloat(parts[1]);
    if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) {
        return null;
    }
    return numerator / denominator;
}
function extractVideoStream(probe) {
    const videoStream = probe.streams?.find(stream => stream.codec_type === 'video');
    if (!videoStream || typeof videoStream.width !== 'number' || typeof videoStream.height !== 'number') {
        throw new Error('video_stream_missing');
    }
    return videoStream;
}
function countAudioStreams(probe) {
    return probe.streams?.filter(stream => stream.codec_type === 'audio').length ?? 0;
}
function readDurationSeconds(probe) {
    const raw = probe.format?.duration;
    const parsed = raw ? Number.parseFloat(raw) : Number.NaN;
    if (!Number.isFinite(parsed)) {
        throw new Error('duration_missing');
    }
    return parsed;
}
async function runListingRecorder(recorderPath, pid, durationSeconds, rawOutputPath, metadataPath, audio) {
    const args = [
        '--pid',
        String(pid),
        '--duration',
        String(durationSeconds),
        '--output',
        rawOutputPath,
        '--metadata',
        metadataPath,
    ];
    if (audio) {
        args.push('--audio');
    }
    const result = (0, node_child_process_1.spawnSync)(recorderPath, args, {
        encoding: 'utf8',
        maxBuffer: 10 * 1024 * 1024,
    });
    if (result.status !== 0) {
        const message = result.stderr?.trim() || result.stdout?.trim() || 'Native recorder failed.';
        throw new Error(`listing_recorder_failed:${message}`);
    }
    const metadata = JSON.parse(await (0, promises_1.readFile)(metadataPath, 'utf8'));
    return metadata;
}
function formatCommandError(error) {
    if (error.message === 'unsupported_platform') {
        return {
            message: 'Listing capture only runs on macOS.',
            suggestions: [],
        };
    }
    if (error.message.startsWith('unsupported_macos_version:')) {
        const version = error.message.split(':')[1] || 'unknown';
        return {
            message: `Listing capture only supports macOS ${SUPPORTED_MACOS_PREFIX} on this machine. Current version: ${version}.`,
            suggestions: [],
        };
    }
    if (error.message === 'macos_version_probe_failed') {
        return {
            message: 'Could not determine the local macOS version.',
            suggestions: [],
        };
    }
    if (error.message.startsWith('listing_recorder_failed:')) {
        return {
            message: error.message.slice('listing_recorder_failed:'.length),
            suggestions: [],
        };
    }
    if (error.message.startsWith('ffprobe_failed:') || error.message.startsWith('ffmpeg_failed:')) {
        return {
            message: error.message.slice(error.message.indexOf(':') + 1).trim() || 'ffmpeg validation failed.',
            suggestions: [],
        };
    }
    return {
        message: error.message,
        suggestions: [],
    };
}
async function encodeListingVideo(rawVideoPath, finalVideoPath, posterPath, crop, width, height, fps, posterAtSeconds) {
    const filter = `crop=${crop.width}:${crop.height}:${crop.x}:${crop.y},scale=${width}:${height}:flags=lanczos,fps=${fps}`;
    runTool('ffmpeg', [
        '-y',
        '-i',
        rawVideoPath,
        '-vf',
        filter,
        '-c:v',
        'h264_videotoolbox',
        '-pix_fmt',
        'yuv420p',
        '-c:a',
        'aac',
        finalVideoPath,
    ], 'ffmpeg_failed');
    runTool('ffmpeg', [
        '-y',
        '-ss',
        String(posterAtSeconds),
        '-i',
        finalVideoPath,
        '-frames:v',
        '1',
        posterPath,
    ], 'ffmpeg_failed');
}
async function muxListingAudio(finalVideoPath, audioPath) {
    const muxedVideoPath = `${finalVideoPath}.muxed.mp4`;
    runTool('ffmpeg', [
        '-y',
        '-i',
        finalVideoPath,
        '-i',
        audioPath,
        '-c:v',
        'copy',
        '-c:a',
        'aac',
        '-shortest',
        muxedVideoPath,
    ], 'ffmpeg_failed');
    await (0, promises_1.rename)(muxedVideoPath, finalVideoPath);
}
function ensureTolerance(actual, expected, tolerance, failureCode) {
    if (Math.abs(actual - expected) > tolerance) {
        throw new Error(`${failureCode}:${actual}:${expected}`);
    }
}
function createWarnings(finalProbe, requestedAudio) {
    const warnings = [];
    const audioTrackCount = countAudioStreams(finalProbe);
    if (!requestedAudio && audioTrackCount > 0) {
        warnings.push('final output contains audio even though --audio was not requested');
    }
    return warnings;
}
function ensureCaptureListingToolAvailable(command) {
    if (commandExists(command)) {
        return true;
    }
    (0, messages_1.printErrorWithHelp)(`${command} is required for listing capture.`, [`Install ${command} and make sure it is available on PATH.`], {
        command: 'project capture listing',
    });
    process.exitCode = 1;
    return false;
}
async function ensureListingRecorderAvailable(recorderPath) {
    try {
        await ensureExecutableFile(recorderPath);
        return true;
    }
    catch {
        (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture listing' });
        process.exitCode = 1;
        return false;
    }
}
function resolveCaptureListingTarget(parsedOptions) {
    try {
        return (0, devShared_1.resolveDevTarget)(parsedOptions.targetArg, parsedOptions.appName);
    }
    catch (error) {
        const code = error?.code;
        const message = error?.message || 'Unable to resolve capture target.';
        const suggestions = code === 'app_required'
            ? ['Use "--app <name>" to choose which app to capture.']
            : code === 'catalogue_missing'
                ? ['Run the command inside a Playdrop workspace or pass the direct HTML file path.']
                : ['Provide a valid app target or HTML file path.'];
        (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture listing' });
        process.exitCode = 1;
        return null;
    }
}
async function captureListing(targetArg, options = {}) {
    let parsedOptions;
    try {
        parsedOptions = parseCaptureListingOptions(targetArg, options);
        assertSupportedListingEnvironment();
    }
    catch (error) {
        const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
        (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture listing' });
        process.exitCode = 1;
        return;
    }
    if (!ensureCaptureListingToolAvailable('ffmpeg') || !ensureCaptureListingToolAvailable('ffprobe')) {
        return;
    }
    const recorderPath = resolveListingRecorderPath();
    if (!await ensureListingRecorderAvailable(recorderPath)) {
        return;
    }
    const resolvedTarget = resolveCaptureListingTarget(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 defaults for direct-file targets outside a registered catalogue
    }
    const outputPaths = await ensureOutputPaths(appName, parsedOptions.outputDir);
    const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
    const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
    // eslint-disable-next-line complexity
    await (0, commandContext_1.withEnvironment)('project capture listing', 'Capturing listing media', async ({ client, env, envConfig }) => {
        let currentUsername = '';
        let registeredApp = null;
        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 capture listing' });
                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 capture listing');
                process.exitCode = 1;
                return;
            }
            throw error;
        }
        try {
            registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Listing capture');
                process.exitCode = 1;
                return;
            }
            if (error instanceof types_1.ApiError && error.status === 404) {
                (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [`Run "playdrop project create app ${appName}" before using listing capture.`], { command: 'project capture listing' });
                process.exitCode = 1;
                return;
            }
            if ((0, devShared_1.isNetworkError)(error)) {
                (0, messages_1.printNetworkIssue)('Could not verify the app registration.', 'project capture listing');
                process.exitCode = 1;
                return;
            }
            throw error;
        }
        const entryLabel = (0, node_path_1.relative)(process.cwd(), resolvedTarget.htmlPath) || resolvedTarget.htmlPath;
        console.log(`[listing] Preparing ${entryLabel} for ${env} (${appTypeSlug}).`);
        const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
            creatorUsername: currentUsername,
            appType: appTypeSlug,
            appName,
            port: devServer_1.DEV_ROUTER_PORT,
        }, 750);
        const devServerStartedByCapture = !serverAlreadyRunning;
        let serverHandle = null;
        let browserHandle = null;
        let cleanupRequested = false;
        const cleanup = async () => {
            if (cleanupRequested) {
                return;
            }
            cleanupRequested = true;
            if (browserHandle) {
                await browserHandle.close();
            }
            if (serverHandle) {
                await serverHandle.close();
            }
        };
        try {
            if (serverAlreadyRunning) {
                console.log(`[listing] Reusing dev server at ${(0, devServer_1.buildLocalDevAppUrl)({
                    creatorUsername: currentUsername,
                    appType: appTypeSlug,
                    appName,
                    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,
                });
            }
            if (!serverAlreadyRunning && projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
                const projectLabel = (0, devShared_1.formatProjectLabel)(projectInfo);
                if (projectLabel) {
                    console.log(`[listing] package.json detected at ${projectLabel}, but no "dev" script was found. Run your app build manually if needed.`);
                }
            }
            if (devServerStartedByCapture) {
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
            const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
            const routeSegment = resolveListingCaptureRouteSegment(Boolean(registeredApp?.previewable));
            const frameUrlObject = new URL(`${(0, appUrls_1.normalizePlaydropWebBase)(webBase)}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/${routeSegment}`);
            frameUrlObject.searchParams.set('launchCheck', '1');
            const frameUrl = frameUrlObject.toString();
            const captureSceneId = resolveListingCaptureSceneId(parsedOptions.width, parsedOptions.height);
            const shouldExportPreviewAudio = parsedOptions.audio && routeSegment === 'dev-preview';
            browserHandle = await launchListingBrowser({
                width: parsedOptions.width,
                height: parsedOptions.height,
            }, frameUrlObject.origin);
            await browserHandle.page.goto(frameUrl, { waitUntil: 'domcontentloaded' });
            await browserHandle.page.waitForTimeout(1000);
            if (routeSegment === 'dev-preview') {
                console.log(`[listing] Preparing preview scene ${captureSceneId}.`);
                await prepareHostedListingScene(browserHandle.page, captureSceneId);
                await browserHandle.page.waitForTimeout(1000);
            }
            if (shouldExportPreviewAudio) {
                console.log('[listing] Starting in-app preview audio capture.');
                await startHostedListingAudioCapture(browserHandle.page);
            }
            const measurement = await fitWindowToRequestedGameplay(browserHandle.page, parsedOptions.width, parsedOptions.height);
            console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
            await browserHandle.page.waitForTimeout(750);
            const recorderMetadata = await runListingRecorder(recorderPath, browserHandle.processId, parsedOptions.durationSeconds, outputPaths.rawVideoPath, outputPaths.metadataPath, shouldExportPreviewAudio ? false : parsedOptions.audio);
            const exportedAudioPath = shouldExportPreviewAudio
                ? await stopHostedListingAudioCapture(browserHandle.page, outputPaths.outputDir)
                : null;
            const rawProbe = await probeMediaFile(outputPaths.rawVideoPath);
            const rawVideoStream = extractVideoStream(rawProbe);
            const rawDurationSeconds = readDurationSeconds(rawProbe);
            const rawAudioTrackCount = countAudioStreams(rawProbe);
            const crop = computeRecordedCrop(measurement, rawVideoStream.width, rawVideoStream.height);
            console.log(`[listing] Cropping raw capture to gameplay at ${crop.width}x${crop.height}+${crop.x}+${crop.y}.`);
            await encodeListingVideo(outputPaths.rawVideoPath, outputPaths.finalVideoPath, outputPaths.posterPath, crop, parsedOptions.width, parsedOptions.height, parsedOptions.fps, parsedOptions.posterAtSeconds);
            if (exportedAudioPath) {
                await muxListingAudio(outputPaths.finalVideoPath, exportedAudioPath);
            }
            const finalProbe = await probeMediaFile(outputPaths.finalVideoPath);
            const finalVideoStream = extractVideoStream(finalProbe);
            const finalDurationSeconds = readDurationSeconds(finalProbe);
            const finalAudioTrackCount = countAudioStreams(finalProbe);
            const finalFps = parseFrameRate(finalVideoStream.avg_frame_rate);
            ensureTolerance(finalVideoStream.width, parsedOptions.width, 0, 'final_width_mismatch');
            ensureTolerance(finalVideoStream.height, parsedOptions.height, 0, 'final_height_mismatch');
            ensureTolerance(finalDurationSeconds, parsedOptions.durationSeconds, 1.5, 'final_duration_mismatch');
            if (parsedOptions.audio && finalAudioTrackCount === 0) {
                throw new Error('audio_track_missing');
            }
            const warnings = createWarnings(finalProbe, parsedOptions.audio);
            const report = {
                appName,
                targetUrl: frameUrl,
                outputDir: outputPaths.outputDir,
                command: {
                    durationSeconds: parsedOptions.durationSeconds,
                    width: parsedOptions.width,
                    height: parsedOptions.height,
                    fps: parsedOptions.fps,
                    posterAtSeconds: parsedOptions.posterAtSeconds,
                    audio: parsedOptions.audio,
                    keepRaw: parsedOptions.keepRaw,
                },
                measurement,
                recorder: recorderMetadata,
                crop,
                rawVideo: {
                    path: parsedOptions.keepRaw ? outputPaths.rawVideoPath : null,
                    width: rawVideoStream.width,
                    height: rawVideoStream.height,
                    durationSeconds: rawDurationSeconds,
                    audioTrackCount: rawAudioTrackCount,
                },
                finalVideo: {
                    path: outputPaths.finalVideoPath,
                    width: finalVideoStream.width,
                    height: finalVideoStream.height,
                    durationSeconds: finalDurationSeconds,
                    audioTrackCount: finalAudioTrackCount,
                    fps: finalFps,
                },
                posterPath: outputPaths.posterPath,
                warnings,
            };
            await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
            if (!parsedOptions.keepRaw) {
                await (0, promises_1.rm)(outputPaths.rawVideoPath, { force: true });
            }
            console.log(`[listing] Saved video to ${(0, node_path_1.relative)(process.cwd(), outputPaths.finalVideoPath) || outputPaths.finalVideoPath}`);
            console.log(`[listing] Saved poster to ${(0, node_path_1.relative)(process.cwd(), outputPaths.posterPath) || outputPaths.posterPath}`);
            console.log(`[listing] Saved report to ${(0, node_path_1.relative)(process.cwd(), outputPaths.reportPath) || outputPaths.reportPath}`);
            if (warnings.length > 0) {
                for (const warning of warnings) {
                    console.warn(`[listing][warn] ${warning}`);
                }
            }
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Listing capture');
                process.exitCode = 1;
                return;
            }
            const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
            (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture listing' });
            process.exitCode = 1;
        }
        finally {
            await cleanup();
        }
    }, { workspacePath: resolvedTarget.cataloguePath ?? (0, node_path_1.dirname)(resolvedTarget.htmlPath) });
}
