/**
 * @license
 * Copyright 2025 ReverseCraft
 * SPDX-License-Identifier: Apache-2.0
 */
/**
 * Script tools for rc-devtools-mcp.
 * Provides script evaluation and source saving capabilities.
 */
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { zod } from '../third-party/index.js';
import { getCdpSession } from '../utils/cdp.js';
import { getScriptCache } from '../utils/smart-breakpoint-utils.js';
import { ToolCategory } from './categories.js';
import { defineTool } from './tool-definition.js';
async function formatResult(session, result) {
    const { type, subtype, objectId, value, description, unserializableValue, preview } = result;
    if (type === 'undefined') {
        return { value: 'undefined' };
    }
    if (subtype === 'null') {
        return { value: 'null' };
    }
    if (unserializableValue) {
        return { value: unserializableValue };
    }
    if (type === 'boolean' || type === 'number' || type === 'bigint') {
        return { value: String(value) };
    }
    if (type === 'string') {
        return { value: JSON.stringify(value) };
    }
    if (type === 'symbol') {
        return { value: description || 'Symbol()' };
    }
    if (type === 'function' && objectId) {
        const location = await getFunctionLocation(session, objectId);
        const source = description || '[Function]';
        return { value: source, location };
    }
    if (subtype === 'node' && objectId) {
        const nodeDesc = await getNodeDescription(session, objectId);
        return { value: nodeDesc || description || '[Node]' };
    }
    if (subtype === 'array' && objectId) {
        const arrayStr = await serializeObject(session, objectId);
        return { value: arrayStr || description || '[]' };
    }
    if (subtype === 'regexp' ||
        subtype === 'date' ||
        subtype === 'map' ||
        subtype === 'set' ||
        subtype === 'error') {
        return { value: description || `[${subtype}]` };
    }
    if (subtype === 'promise' && preview) {
        return { value: description || 'Promise' };
    }
    if (type === 'object' && objectId) {
        const objStr = await serializeObject(session, objectId);
        return { value: objStr || description || '[Object]' };
    }
    return { value: description || `[${type}]` };
}
async function getFunctionLocation(session, objectId) {
    try {
        const props = (await session.send('Runtime.getProperties', {
            objectId,
            ownProperties: false,
        }));
        const locationProp = props.internalProperties?.find(p => p.name === '[[FunctionLocation]]');
        if (locationProp?.value?.value) {
            const { scriptId, lineNumber, columnNumber } = locationProp.value.value;
            const scriptCache = getScriptCache(session);
            const scriptInfo = scriptCache.get(scriptId);
            const url = scriptInfo?.url || `VM${scriptId}`;
            return `${url}:${lineNumber + 1}:${columnNumber + 1}`;
        }
    }
    catch {
        // Ignore errors
    }
    return undefined;
}
async function getNodeDescription(session, objectId) {
    try {
        const result = (await session.send('Runtime.callFunctionOn', {
            objectId,
            functionDeclaration: `function() {
        const el = this;
        if (el.nodeType === 1) {
          let s = '<' + el.tagName.toLowerCase();
          if (el.id) s += ' id="' + el.id + '"';
          if (el.className) s += ' class="' + el.className + '"';
          s += '>';
          return s;
        }
        return el.nodeName;
      }`,
            returnByValue: true,
        }));
        return result.result.value;
    }
    catch {
        return undefined;
    }
}
async function serializeObject(session, objectId) {
    try {
        const result = (await session.send('Runtime.callFunctionOn', {
            objectId,
            functionDeclaration: `function() {
        try {
          return JSON.stringify(this, null, 2);
        } catch {
          return null;
        }
      }`,
            returnByValue: true,
        }));
        return result.result.value ?? undefined;
    }
    catch {
        return undefined;
    }
}
function formatExceptionDetails(exceptionDetails) {
    if (exceptionDetails.exception?.description) {
        return exceptionDetails.exception.description;
    }
    if (exceptionDetails.text) {
        const loc = exceptionDetails.lineNumber !== undefined
            ? ` at line ${exceptionDetails.lineNumber + 1}:${(exceptionDetails.columnNumber ?? 0) + 1}`
            : '';
        return `${exceptionDetails.text}${loc}`;
    }
    return 'Unknown error';
}
export const saveScriptSource = defineTool({
    name: 'save_script_source',
    description: 'Save a script source to a local file. Use VM<id> pattern to match by scriptId, otherwise matches by URL substring.',
    annotations: {
        category: ToolCategory.DEBUGGING,
        readOnlyHint: false,
    },
    schema: {
        pattern: zod
            .string()
            .describe('Pattern to match script. If starts with "VM" (e.g., "VM123"), matches by scriptId. Otherwise matches by URL substring.'),
        filePath: zod.string().describe('Path to save the script file.'),
    },
    handler: async (request, response, context) => {
        const { pattern, filePath } = request.params;
        const page = context.getSelectedPage();
        const session = await getCdpSession(page);
        try {
            let scriptId;
            let matchedUrl;
            const vmMatch = pattern.match(/^VM(\d+)$/i);
            if (vmMatch) {
                scriptId = vmMatch[1];
            }
            else {
                const scriptCache = getScriptCache(session);
                for (const [id, info] of scriptCache.entries()) {
                    if (info.url && info.url.includes(pattern)) {
                        scriptId = id;
                        matchedUrl = info.url;
                        break;
                    }
                }
            }
            if (!scriptId) {
                response.appendResponseLine(`Error: No script found matching pattern "${pattern}".`);
                return;
            }
            const result = (await session.send('Debugger.getScriptSource', {
                scriptId,
            }));
            const source = result.scriptSource;
            if (!source) {
                response.appendResponseLine(`Error: Script ${scriptId} has no source available.`);
                return;
            }
            const fullPath = path.resolve(filePath);
            await fs.mkdir(path.dirname(fullPath), { recursive: true });
            await fs.writeFile(fullPath, source, 'utf-8');
            response.appendResponseLine(`Saved script to ${fullPath}`);
            if (matchedUrl) {
                response.appendResponseLine(`Matched URL: ${matchedUrl}`);
            }
            response.appendResponseLine(`ScriptId: ${scriptId}, Size: ${source.length} bytes`);
        }
        catch (error) {
            response.appendResponseLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
        }
    },
});
const DEFAULT_MAX_OUTPUT_CHARS = 10000;
export const evaluateScript = defineTool({
    name: 'evaluate_script',
    description: `Evaluate JavaScript code inside the currently selected page, similar to DevTools Console.`,
    annotations: {
        category: ToolCategory.DEBUGGING,
        readOnlyHint: false,
    },
    schema: {
        script: zod.string().describe('JavaScript code to execute in the page context.'),
        maxOutputChars: zod
            .number()
            .int()
            .positive()
            .optional()
            .describe(`Maximum number of characters in the output. Default is ${DEFAULT_MAX_OUTPUT_CHARS}.`),
    },
    handler: async (request, response, context) => {
        const page = context.getSelectedPage();
        const script = request.params.script;
        const maxOutputChars = request.params.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
        const session = await getCdpSession(page);
        await context.waitForEventsAfterAction(async () => {
            const evalResult = (await session.send('Runtime.evaluate', {
                expression: script,
                awaitPromise: true,
                returnByValue: false,
                generatePreview: true,
                userGesture: true,
                replMode: true,
            }));
            if (evalResult.exceptionDetails) {
                const errorMsg = formatExceptionDetails(evalResult.exceptionDetails);
                const truncatedError = truncateToMaxChars(errorMsg, maxOutputChars);
                response.appendResponseLine('❌ Error: ' + truncatedError.text);
                if (truncatedError.truncated) {
                    response.appendResponseLine(`⚠️ Output truncated: ${truncatedError.originalLength} chars → ${maxOutputChars} chars`);
                }
                return;
            }
            const formatted = await formatResult(session, evalResult.result);
            const truncatedResult = truncateToMaxChars(formatted.value, maxOutputChars);
            response.appendResponseLine(truncatedResult.text);
            if (truncatedResult.truncated) {
                response.appendResponseLine(`⚠️ Output truncated: ${truncatedResult.originalLength} chars → ${maxOutputChars} chars`);
            }
            if (formatted.location) {
                response.appendResponseLine(`📍 ${formatted.location}`);
            }
        });
    },
});
function truncateToMaxChars(text, maxChars) {
    const originalLength = text.length;
    if (originalLength <= maxChars) {
        return { text, truncated: false, originalLength };
    }
    return {
        text: text.substring(0, maxChars) + '\n... <truncated>',
        truncated: true,
        originalLength,
    };
}
//# sourceMappingURL=script.js.map