"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.capture = capture;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const types_1 = require("@playdrop/types");
const http_1 = require("../http");
const messages_1 = require("../messages");
const catalogue_utils_1 = require("../catalogue-utils");
const catalogue_1 = require("../catalogue");
const devShared_1 = require("./devShared");
const devServer_1 = require("./devServer");
const commandContext_1 = require("../commandContext");
const appUrls_1 = require("../appUrls");
const captureRuntime_1 = require("../captureRuntime");
const devAuth_1 = require("../devAuth");
const devRuntimeAssets_1 = require("./devRuntimeAssets");
const dev_1 = require("./dev");
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: {
        label: 'desktop',
        contextOptions: {
            viewport: { width: 1280, height: 720 },
            deviceScaleFactor: 1,
            isMobile: false,
            hasTouch: false,
        },
    },
    mobileLandscape: {
        label: 'mobile-landscape',
        contextOptions: {
            viewport: { width: 896, height: 414 },
            deviceScaleFactor: 2,
            isMobile: true,
            hasTouch: true,
            userAgent: MOBILE_USER_AGENT,
        },
    },
    mobilePortrait: {
        label: 'mobile-portrait',
        contextOptions: {
            viewport: { width: 414, height: 896 },
            deviceScaleFactor: 2,
            isMobile: true,
            hasTouch: true,
            userAgent: MOBILE_USER_AGENT,
        },
    },
};
const SURFACE_ORDER = ['desktop', 'mobileLandscape', 'mobilePortrait'];
function normalizeSurfaceOverride(value) {
    if (!value)
        return null;
    const normalized = value.trim().toLowerCase().replace(/[-_\s]/g, '');
    if (normalized === 'desktop')
        return 'desktop';
    if (normalized === 'mobilelandscape')
        return 'mobileLandscape';
    if (normalized === 'mobileportrait')
        return 'mobilePortrait';
    return null;
}
function appSurfaceToId(surface) {
    if (surface === 'DESKTOP')
        return 'desktop';
    if (surface === 'MOBILE_LANDSCAPE')
        return 'mobileLandscape';
    if (surface === 'MOBILE_PORTRAIT')
        return 'mobilePortrait';
    return null;
}
function determineDefaultSurface(targets) {
    for (const entry of targets.list) {
        const id = appSurfaceToId(entry);
        if (id)
            return id;
    }
    for (const id of SURFACE_ORDER) {
        if (targets.map[id])
            return id;
    }
    return 'desktop';
}
function formatSurfaceList(targets) {
    const enabled = SURFACE_ORDER.filter(id => targets.map[id]).map(id => SURFACE_PRESETS[id].label);
    if (enabled.length === 0) {
        return SURFACE_PRESETS.desktop.label;
    }
    return enabled.join(', ');
}
function resolveCaptureLoginOverride(options) {
    const username = options.username?.trim() || '';
    const password = options.password || '';
    if ((username && !password) || (!username && password)) {
        throw new Error('invalid_credentials_pair');
    }
    if (!username) {
        return null;
    }
    return { username, password };
}
function resolveCaptureDevOptions(options) {
    const selection = (0, devAuth_1.parseHostedDevAuthSelection)({
        devAuth: options.devAuth,
        player: options.player,
        defaultMode: 'anonymous',
        allowPrompt: false,
    });
    const resetMode = (0, devAuth_1.parseResetDevPlayerMode)(options.resetDevPlayer);
    if (selection.devAuth !== 'player' && resetMode !== 'never') {
        throw new Error('reset_dev_player_requires_player_auth');
    }
    return {
        selection,
        resetMode,
    };
}
// eslint-disable-next-line max-lines-per-function
async function capture(targetArg, options = {}) {
    const timeoutSeconds = (0, captureRuntime_1.validateCaptureTimeout)(options.timeoutSeconds);
    const timeoutMs = Math.round(timeoutSeconds * 1000);
    let minimumLogLevel;
    let loginOverride;
    let devOptions;
    try {
        minimumLogLevel = (0, captureRuntime_1.resolveCaptureLogLevel)(options.logLevel);
        loginOverride = resolveCaptureLoginOverride(options);
        devOptions = resolveCaptureDevOptions(options);
    }
    catch (error) {
        if (error instanceof Error && error.message === 'invalid_credentials_pair') {
            (0, messages_1.printErrorWithHelp)('Use --username and --password together.', [], { command: 'project capture' });
        }
        else if (error instanceof Error && error.message === 'dev_auth_prompt_not_supported') {
            (0, messages_1.printErrorWithHelp)('Use --dev-auth anonymous, viewer, or player for deterministic capture.', [], { command: 'project capture' });
        }
        else if (error instanceof Error && error.message === 'dev_auth_player_slot_required') {
            (0, messages_1.printErrorWithHelp)('Use --player 1, 2, 3, or 4 when --dev-auth player is selected.', [], { command: 'project capture' });
        }
        else if (error instanceof Error && error.message === 'reset_dev_player_requires_player_auth') {
            (0, messages_1.printErrorWithHelp)('Use --reset-dev-player only with --dev-auth player.', [], { command: 'project capture' });
        }
        else if (error instanceof Error && error.message === 'reset_dev_player_invalid') {
            (0, messages_1.printErrorWithHelp)('Use --reset-dev-player before, after, before-and-after, or never.', [], { command: 'project capture' });
        }
        else {
            (0, messages_1.printErrorWithHelp)(error?.message || 'Invalid capture options.', [
                'Valid log levels: debug, info, warn, error',
                'Valid --dev-auth values: anonymous, viewer, player',
            ], { command: 'project capture' });
        }
        process.exitCode = 1;
        return;
    }
    const screenshotPath = options.screenshotPath ? (0, node_path_1.resolve)(process.cwd(), options.screenshotPath) : null;
    let resolvedTarget;
    try {
        resolvedTarget = (0, devShared_1.resolveDevTarget)(targetArg, options.appName);
    }
    catch (error) {
        const code = error?.code;
        const message = error?.message || 'Unable to resolve capture target.';
        let suggestions = [];
        switch (code) {
            case 'app_required':
                suggestions = ['Use "--app <name>" to select which app to capture or provide the HTML file path directly.'];
                break;
            case 'app_not_found':
                suggestions = ['Confirm the app exists in catalogue.json, use "--app <name>", or pass the direct HTML file path.'];
                break;
            case 'catalogue_missing':
                suggestions = ['Run "playdrop project capture" from a workspace with catalogue.json or provide the HTML file path.'];
                break;
            case 'invalid_target':
                suggestions = ['Provide a valid HTML file path or app name.'];
                break;
            case 'empty_catalogue':
                suggestions = ['Add at least one app entry to catalogue.json before running capture.'];
                break;
            case 'invalid_catalogue':
                suggestions = ['Fix the catalogue.json file and rerun "playdrop project capture".'];
                break;
            default:
                suggestions = ['Provide an app name, HTML file path, or run inside a Playdrop workspace.'];
                break;
        }
        (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture' });
        process.exitCode = 1;
        return;
    }
    let appName = resolvedTarget.appName;
    const filePath = resolvedTarget.htmlPath;
    if (!(0, node_fs_1.existsSync)(filePath) || !(0, node_fs_1.statSync)(filePath).isFile()) {
        (0, messages_1.printErrorWithHelp)(`App HTML was not found at ${filePath}.`, [
            'Ensure the catalogue entry points to a compiled HTML file or provide the correct file path.'
        ], { command: 'project capture' });
        process.exitCode = 1;
        return;
    }
    const availableSurfaces = resolvedTarget.surfaceTargets;
    const overrideSurface = normalizeSurfaceOverride(options.surfaceTarget);
    if (options.surfaceTarget && !overrideSurface) {
        (0, messages_1.printErrorWithHelp)(`Unsupported surface target "${options.surfaceTarget}".`, ['Valid values: desktop, mobile-landscape, mobile-portrait'], { command: 'project capture' });
        process.exitCode = 1;
        return;
    }
    if (overrideSurface && !availableSurfaces.map[overrideSurface]) {
        (0, messages_1.printErrorWithHelp)(`Surface target "${options.surfaceTarget}" is not enabled for this app.`, [`Catalogue surfaces: ${formatSurfaceList(availableSurfaces)}`], { command: 'project capture' });
        process.exitCode = 1;
        return;
    }
    const selectedSurface = overrideSurface ?? determineDefaultSurface(availableSurfaces);
    const surfacePreset = SURFACE_PRESETS[selectedSurface];
    const surfaceContextOptions = {
        ...surfacePreset.contextOptions,
        viewport: surfacePreset.contextOptions.viewport
            ? { ...surfacePreset.contextOptions.viewport }
            : undefined,
    };
    const viewportLabel = surfaceContextOptions.viewport
        ? ` (${surfaceContextOptions.viewport.width}x${surfaceContextOptions.viewport.height})`
        : '';
    console.log(`[capture] Surface: ${surfacePreset.label}${viewportLabel}`);
    let appType = null;
    let appTypeSlug = (0, appUrls_1.getAppTypeSlug)(appType);
    try {
        const match = (0, catalogue_utils_1.findAppDefinition)(filePath);
        appName = match.name;
        appType = match.type;
        appTypeSlug = (0, appUrls_1.getAppTypeSlug)(appType);
    }
    catch (error) {
        if (resolvedTarget.cataloguePath) {
            (0, messages_1.printErrorWithHelp)(error?.message || 'Catalogue lookup failed.', [
                'Ensure the HTML file is listed exactly once in catalogue.json with a valid type.',
                'Run "playdrop project create app <name>" to scaffold a new entry if needed.',
            ], { command: 'project capture' });
            process.exitCode = 1;
            return;
        }
    }
    const projectInfo = (0, devShared_1.findProjectInfo)(filePath);
    const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
    await (0, commandContext_1.withEnvironment)('project capture', 'Capturing app logs', 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 and ensure the API is reachable.',
                    'Use "playdrop auth whoami" afterwards to confirm your status.',
                ], { command: 'project 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 capture');
                process.exitCode = 1;
                return;
            }
            if (error instanceof Error && error.message === 'DEV_USERNAME_EMPTY') {
                (0, messages_1.printErrorWithHelp)('Could not determine your Playdrop creator username.', [
                    'Retry "playdrop project capture" in a moment.',
                    'If the problem persists, contact the Playdrop team.',
                ], { command: 'project capture' });
                process.exitCode = 1;
                return;
            }
            throw error;
        }
        if ((devOptions.selection.devAuth === 'viewer' || devOptions.selection.devAuth === 'player') && !loginOverride) {
            (0, messages_1.printErrorWithHelp)(`Use --username and --password to establish a browser session before running --dev-auth ${devOptions.selection.devAuth}.`, [], { command: 'project capture' });
            process.exitCode = 1;
            return;
        }
        const entryLabel = (0, node_path_1.relative)(process.cwd(), filePath) || filePath;
        console.log(`[capture] 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 signalHandler = null;
        let cleanupRequested = false;
        const cleanup = async () => {
            if (cleanupRequested)
                return;
            cleanupRequested = true;
            if (serverHandle) {
                try {
                    await serverHandle.close();
                }
                catch (error) {
                    console.error(error instanceof Error ? error.message : String(error));
                }
            }
        };
        let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
        const taskLookup = (0, catalogue_1.findAppTaskByFile)(filePath);
        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 capture' });
            process.exitCode = 1;
            return;
        }
        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 capture' });
                process.exitCode = 1;
                return;
            }
        }
        if (serverAlreadyRunning) {
            console.log(`[capture] Reusing dev server at ${(0, devServer_1.buildLocalDevAppUrl)({
                creatorUsername: currentUsername,
                appType: appTypeSlug,
                appName,
                port: devServer_1.DEV_ROUTER_PORT,
            })}`);
            try {
                await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
                    creatorUsername: currentUsername,
                    appName,
                    runtimeAssetManifest,
                    port: devServer_1.DEV_ROUTER_PORT,
                });
            }
            catch (error) {
                (0, messages_1.printErrorWithHelp)(error?.message || 'Failed to refresh the local runtime asset manifest on the shared dev router.', [
                    'Restart "playdrop project dev" for this app and try capture again.',
                ], { command: 'project capture' });
                process.exitCode = 1;
                return;
            }
        }
        else {
            try {
                serverHandle = await (0, devServer_1.startDevServer)({
                    appName,
                    appType: appTypeSlug,
                    creatorUsername: currentUsername,
                    htmlPath: filePath,
                    port: devServer_1.DEV_ROUTER_PORT,
                    projectInfo,
                    runtimeAssetManifest,
                });
                signalHandler = () => {
                    void cleanup().finally(() => process.exit(130));
                };
                process.on('SIGINT', signalHandler);
                process.on('SIGTERM', signalHandler);
            }
            catch (error) {
                const mountConflict = (0, devServer_1.parseMountConflictError)(typeof error?.message === 'string' ? error.message : '');
                if (mountConflict) {
                    (0, messages_1.printErrorWithHelp)(`A different dev session already owns ${mountConflict.ref}.`, [
                        `Active owner pid: ${mountConflict.ownerPid}.`,
                        `Mounted repo root: ${mountConflict.repoRoot}.`,
                        `Mounted HTML path: ${mountConflict.htmlPath}.`,
                    ], { command: 'project capture' });
                }
                else {
                    (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devServer_1.DEV_ROUTER_PORT}.`, [
                        'Close the conflicting process or wait for the stale mount to exit.',
                        'Ensure the HTML file exists and is readable.',
                    ], { command: 'project capture' });
                }
                process.exitCode = 1;
                return;
            }
        }
        if (!serverAlreadyRunning && projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
            const projectLabel = (0, devShared_1.formatProjectLabel)(projectInfo);
            if (projectLabel) {
                console.log(`[capture] package.json detected at ${projectLabel}, but no "dev" script was found. Run your build/watch scripts manually if needed.`);
            }
        }
        if (devServerStartedByCapture) {
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
        const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
        const shouldResetBefore = devOptions.selection.devAuth === 'player'
            && (devOptions.resetMode === 'before' || devOptions.resetMode === 'before-and-after');
        const shouldResetAfter = devOptions.selection.devAuth === 'player'
            && (devOptions.resetMode === 'after' || devOptions.resetMode === 'before-and-after');
        const requiresRegisteredApp = devOptions.selection.devAuth !== 'anonymous' || shouldResetBefore || shouldResetAfter;
        let registeredApp = null;
        if (requiresRegisteredApp) {
            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, 'Capture');
                    process.exitCode = 1;
                    return;
                }
                if (error instanceof types_1.ApiError) {
                    if (error.status === 404) {
                        (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
                            `Run "playdrop project create app ${appName}" to register the app before using viewer or player capture.`,
                            'Use anonymous capture for local launch validation before upload.',
                        ], { command: 'project capture' });
                    }
                    else {
                        (0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, [
                            'Retry in a moment.',
                            'If the issue persists, contact the Playdrop team.',
                        ], { command: 'project capture' });
                    }
                    process.exitCode = 1;
                    return;
                }
                if ((0, devShared_1.isNetworkError)(error)) {
                    (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project capture');
                    process.exitCode = 1;
                    return;
                }
                throw error;
            }
        }
        const captureBaseUrl = (0, appUrls_1.buildPlatformDevUrl)(webBase, {
            creatorUsername: currentUsername,
            appName,
            appType,
            devAuth: devOptions.selection.devAuth,
            player: devOptions.selection.player ? String(devOptions.selection.player) : null,
            launchCheck: true,
        });
        const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(captureBaseUrl, devOptions.selection);
        console.log(`[capture] Launching Playwright against ${frameUrl}`);
        const selectedPlayerSlot = devOptions.selection.player;
        const registeredAppId = typeof registeredApp?.id === 'number' ? registeredApp.id : null;
        if ((shouldResetBefore || shouldResetAfter) && registeredAppId === null) {
            (0, messages_1.printErrorWithHelp)('The registered app is missing a numeric id, so test-player reset is unavailable.', [], { command: 'project capture' });
            process.exitCode = 1;
            return;
        }
        if (shouldResetBefore && selectedPlayerSlot !== null) {
            await client.resetDevPlayer(registeredAppId, selectedPlayerSlot);
        }
        try {
            const result = await (0, captureRuntime_1.runCapture)({
                targetUrl: frameUrl,
                expectedUrl: frameUrl,
                timeoutMs,
                settleAfterReadyMs: timeoutMs,
                minimumLogLevel,
                screenshotPath,
                contextOptions: surfaceContextOptions,
                login: loginOverride ? {
                    username: loginOverride.username,
                    password: loginOverride.password,
                } : undefined,
                savedSessionBootstrap: false,
                enableCaptureBridge: true,
                requireHostedLaunchReady: true,
            });
            if (result.errorCount > 0) {
                console.error(`[capture] Completed with ${result.errorCount} error(s) after ${timeoutSeconds} seconds.`);
                process.exitCode = 1;
            }
            else {
                console.log(`[capture] Completed without console errors after ${timeoutSeconds} seconds.`);
            }
        }
        catch (error) {
            if (error instanceof http_1.CLIUnsupportedClientError) {
                return;
            }
            if (error instanceof types_1.UnsupportedClientError) {
                (0, http_1.handleUnsupportedError)(error, 'Capture');
                process.exitCode = 1;
                return;
            }
            if (error instanceof Error && error.stack) {
                console.error(error.stack);
            }
            else {
                console.error(`[capture] ${error instanceof Error ? error.message : String(error)}`);
            }
            process.exitCode = 1;
        }
        finally {
            if (shouldResetAfter && selectedPlayerSlot !== null) {
                try {
                    await client.resetDevPlayer(registeredAppId, selectedPlayerSlot);
                }
                catch (error) {
                    console.error(error instanceof Error ? error.message : String(error));
                    process.exitCode = 1;
                }
            }
            if (signalHandler) {
                process.off('SIGINT', signalHandler);
                process.off('SIGTERM', signalHandler);
            }
            await cleanup();
        }
    }, { workspacePath: resolvedTarget.cataloguePath ?? (0, node_path_1.dirname)(filePath) });
}
