import { createOpenAI } from "@ai-sdk/openai";
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
import { embed, embedMany, NoOutputGeneratedError, stepCountIs, streamText, } from "ai";
import { AIProviderName } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { calculateCost } from "../utils/pricing.js";
import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
import { isZodSchema } from "../utils/schemaConversion.js";
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
import { resolveToolChoice } from "../utils/toolChoice.js";
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
import { getModelId } from "./providerTypeUtils.js";
/**
 * Retrieve a tool's schema, handling both AI SDK v6 (`inputSchema`) and
 * legacy v4 (`parameters`) field names.
 */
function getToolSchema(tool) {
    const t = tool;
    return t.inputSchema ?? t.parameters;
}
// Configuration helpers - now using consolidated utility
const getOpenAIApiKey = () => {
    return validateApiKey(createOpenAIConfig());
};
const getOpenAIModel = () => {
    return getProviderModel("OPENAI_MODEL", "gpt-4o");
};
const streamTracer = trace.getTracer("neurolink.provider.openai");
/**
 * OpenAI Provider v2 - BaseProvider Implementation
 * Migrated to use factory pattern with exact Google AI provider pattern
 */
export class OpenAIProvider extends BaseProvider {
    model;
    credentials;
    constructor(modelName, neurolink, _region, credentials) {
        super(modelName || getOpenAIModel(), AIProviderName.OPENAI, neurolink);
        this.credentials = credentials;
        // Initialize OpenAI provider with proxy support
        const openai = createOpenAI({
            apiKey: credentials?.apiKey ?? getOpenAIApiKey(),
            ...(credentials?.baseURL ? { baseURL: credentials.baseURL } : {}),
            fetch: createProxyFetch(),
        });
        // Initialize model
        this.model = openai(this.modelName);
        logger.debug("OpenAIProvider constructor called", {
            model: this.modelName,
            provider: this.providerName,
            supportsTools: this.supportsTools(),
            className: this.constructor.name,
        });
    }
    // ===================
    // ABSTRACT METHOD IMPLEMENTATIONS
    // ===================
    /**
     * Check if this provider supports tool/function calling
     */
    supportsTools() {
        return true; // Re-enable tools now that we understand the issue
    }
    getProviderName() {
        return AIProviderName.OPENAI;
    }
    getDefaultModel() {
        return getOpenAIModel();
    }
    /**
     * Get the default embedding model for OpenAI
     * @returns The default OpenAI embedding model name
     */
    getDefaultEmbeddingModel() {
        return process.env.OPENAI_EMBEDDING_MODEL || "text-embedding-3-small";
    }
    /**
     * Returns the Vercel AI SDK model instance for OpenAI
     */
    getAISDKModel() {
        return this.model;
    }
    /**
     * OpenAI-specific tool validation and filtering
     * Filters out tools that might cause streaming issues
     */
    validateAndFilterToolsForOpenAI(tools) {
        const validTools = {};
        for (const [name, tool] of Object.entries(tools)) {
            try {
                // Basic validation - ensure tool has required structure
                if (tool && typeof tool === "object") {
                    // Check if tool has description (required by OpenAI)
                    if (tool.description && typeof tool.description === "string") {
                        // Keep the original tool structure - AI SDK will handle Zod schema conversion internally
                        const processedTool = { ...tool };
                        // Validate that Zod schemas are properly structured for AI SDK processing
                        const toolSchema = getToolSchema(tool);
                        if (toolSchema && isZodSchema(toolSchema)) {
                            logger.debug(`OpenAI: Tool ${name} has Zod schema - AI SDK will handle conversion`);
                            // Basic validation that the Zod schema has the required structure
                            this.validateZodSchema(name, toolSchema);
                        }
                        // Include the tool with original Zod schema for AI SDK processing
                        if (this.isValidToolStructure(processedTool)) {
                            validTools[name] = processedTool;
                        }
                        else {
                            logger.warn(`OpenAI: Filtering out tool with invalid structure: ${name}`, {
                                parametersType: typeof getToolSchema(processedTool),
                                hasDescription: !!processedTool.description,
                                hasExecute: !!processedTool.execute,
                            });
                        }
                    }
                    else {
                        logger.warn(`OpenAI: Filtering out tool without description: ${name}`);
                    }
                }
                else {
                    logger.warn(`OpenAI: Filtering out invalid tool: ${name}`);
                }
            }
            catch (error) {
                logger.warn(`OpenAI: Error validating tool ${name}:`, error);
            }
        }
        return validTools;
    }
    /**
     * Validate Zod schema structure
     */
    validateZodSchema(toolName, schema) {
        try {
            const zodSchema = schema;
            if (zodSchema._def && zodSchema._def.typeName) {
                logger.debug(`OpenAI: Zod schema for ${toolName} appears valid`, {
                    typeName: zodSchema._def.typeName,
                });
            }
            else {
                logger.warn(`OpenAI: Zod schema for ${toolName} missing typeName - may cause issues`);
            }
        }
        catch (zodValidationError) {
            logger.warn(`OpenAI: Zod schema validation failed for ${toolName}:`, zodValidationError);
            // Continue anyway - let AI SDK handle it
        }
    }
    /**
     * Validate tool structure for OpenAI compatibility
     * More lenient validation to avoid filtering out valid tools
     */
    /** Shared helper: mark a stream span as ERROR, record the exception, and end it. */
    endStreamSpanWithError(span, error) {
        span.setStatus({
            code: SpanStatusCode.ERROR,
            message: error instanceof Error ? error.message : String(error),
        });
        if (error instanceof Error) {
            span.recordException(error);
        }
        span.end();
    }
    isValidToolStructure(tool) {
        if (!tool || typeof tool !== "object") {
            return false;
        }
        const toolObj = tool;
        // Ensure tool has description and execute function
        if (!toolObj.description || typeof toolObj.description !== "string") {
            return false;
        }
        if (!toolObj.execute || typeof toolObj.execute !== "function") {
            return false;
        }
        // AI SDK v6 uses inputSchema; v4 used parameters — check both
        const schema = "inputSchema" in toolObj
            ? toolObj.inputSchema
            : "parameters" in toolObj
                ? toolObj.parameters
                : undefined;
        return this.isValidToolParameters(schema);
    }
    /**
     * Validate tool parameters for OpenAI compatibility
     * Ensures the tool has either valid Zod schema or valid JSON schema
     */
    isValidToolParameters(parameters) {
        if (!parameters) {
            // For OpenAI, tools without parameters need an empty object schema
            return true;
        }
        // Check if it's a Zod schema - these are valid
        if (isZodSchema(parameters)) {
            return true;
        }
        // Check if it's a JSON schema
        if (typeof parameters !== "object" || parameters === null) {
            return false;
        }
        const params = parameters;
        // If it's a JSON schema, it should have type "object" for OpenAI
        if (params.type && params.type !== "object") {
            return false;
        }
        // OpenAI requires schemas to have properties field, even if empty
        // If there's no properties field, the schema is incomplete
        if (params.type === "object" && !params.properties) {
            logger.warn(`Tool parameter schema missing properties field:`, params);
            return false;
        }
        // If properties exist, they should be an object
        if (params.properties && typeof params.properties !== "object") {
            return false;
        }
        // If required exists, it should be an array
        if (params.required && !Array.isArray(params.required)) {
            return false;
        }
        return true;
    }
    formatProviderError(error) {
        if (error instanceof TimeoutError) {
            return new NetworkError(error.message, this.providerName);
        }
        const errorObj = error;
        const message = errorObj?.message && typeof errorObj.message === "string"
            ? errorObj.message
            : "Unknown error";
        const errorType = errorObj?.type && typeof errorObj.type === "string"
            ? errorObj.type
            : undefined;
        const statusCode = typeof errorObj?.status === "number"
            ? errorObj.status
            : typeof errorObj?.statusCode === "number"
                ? errorObj.statusCode
                : undefined;
        // Curator P1-1 / Reviewer Finding #4: only the explicit auth markers
        // map to AuthenticationError. Earlier we treated every
        // `invalid_request_error` as an auth failure — that's OpenAI's catch-all
        // for any bad request (unsupported parameter, malformed JSON, etc.) and
        // mislabelled them as "invalid API key". Use credential-specific
        // signals only.
        if (message.includes("API_KEY_INVALID") ||
            message.includes("Invalid API key") ||
            message.includes("Incorrect API key") ||
            message.includes("invalid_api_key") ||
            errorType === "invalid_api_key" ||
            statusCode === 401) {
            return new AuthenticationError(message.includes("Incorrect API key") ||
                message.includes("Invalid API key")
                ? message
                : "Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.", this.providerName);
        }
        if (message.includes("rate limit") || errorType === "rate_limit_error") {
            return new RateLimitError("OpenAI rate limit exceeded. Please try again later.", this.providerName);
        }
        if (message.includes("model_not_found")) {
            return new InvalidModelError(`Model not found: ${this.modelName}`, this.providerName);
        }
        // Generic provider error
        return new ProviderError(`OpenAI error: ${message}`, this.providerName);
    }
    /**
     * executeGenerate method removed - generation is now handled by BaseProvider.
     * For details on the changes and migration steps, refer to the BaseProvider documentation
     * and the migration guide in the project repository.
     */
    async executeStream(options, _analysisSchema) {
        this.validateStreamOptions(options);
        const startTime = Date.now();
        const timeout = this.getTimeout(options);
        const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
        try {
            // Get tools - options.tools is pre-merged by BaseProvider.stream() with
            // base tools (MCP/built-in) + user-provided tools (RAG, etc.)
            const shouldUseTools = !options.disableTools && this.supportsTools();
            const allTools = shouldUseTools
                ? options.tools || (await this.getAllTools())
                : {};
            // OpenAI-specific fix: Validate tools format and filter out problematic ones
            let tools = this.validateAndFilterToolsForOpenAI(allTools);
            // OpenAI max tools limit - configurable via environment variable
            const MAX_TOOLS = parseInt(process.env.OPENAI_MAX_TOOLS || "150", 10);
            if (Object.keys(tools).length > MAX_TOOLS) {
                logger.warn(`OpenAI: Too many tools (${Object.keys(tools).length}), limiting to ${MAX_TOOLS} tools`);
                const toolEntries = Object.entries(tools);
                tools = Object.fromEntries(toolEntries.slice(0, MAX_TOOLS));
            }
            // Count tools with Zod schemas for debugging
            const zodToolsCount = Object.values(allTools).filter((tool) => {
                if (!tool || typeof tool !== "object") {
                    return false;
                }
                const schema = getToolSchema(tool);
                return schema !== null && schema !== undefined && isZodSchema(schema);
            }).length;
            logger.info("OpenAI streaming tools", {
                shouldUseTools,
                allToolsCount: Object.keys(allTools).length,
                filteredToolsCount: Object.keys(tools).length,
                zodToolsCount,
                toolNames: Object.keys(tools),
                filteredOutTools: Object.keys(allTools).filter((name) => !tools[name]),
            });
            // Build message array from options with multimodal support
            // Using protected helper from BaseProvider to eliminate code duplication
            const messages = await this.buildMessagesForStream(options);
            let resolvedToolChoice = resolveToolChoice(options, tools, shouldUseTools);
            // Guard: if toolChoice names a specific tool that was filtered out, fall back to "auto"
            if (resolvedToolChoice !== null &&
                typeof resolvedToolChoice === "object" &&
                "toolName" in resolvedToolChoice &&
                typeof resolvedToolChoice.toolName === "string" &&
                !tools[resolvedToolChoice.toolName]) {
                logger.warn(`OpenAI: toolChoice references tool "${resolvedToolChoice.toolName}" which was removed during filtering; falling back to "auto"`);
                resolvedToolChoice = "auto";
            }
            // Debug the actual request being sent to OpenAI
            logger.debug(`OpenAI: streamText request parameters:`, {
                modelName: this.modelName,
                messagesCount: messages.length,
                temperature: options.temperature,
                maxTokens: options.maxTokens,
                toolsCount: Object.keys(tools).length,
                toolChoice: resolvedToolChoice,
                maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
                firstToolExample: Object.keys(tools).length > 0
                    ? {
                        name: Object.keys(tools)[0],
                        description: tools[Object.keys(tools)[0]]?.description,
                        parametersType: typeof getToolSchema(tools[Object.keys(tools)[0]]),
                    }
                    : "no-tools",
            });
            const model = await this.getAISDKModelWithMiddleware(options); // This is where network connection happens!
            // Wrap streamText in an OTel span to capture provider-level latency and token usage
            const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
                kind: SpanKind.CLIENT,
                attributes: {
                    "gen_ai.system": "openai",
                    "gen_ai.request.model": getModelId(model) || this.modelName || "unknown",
                },
            });
            let result;
            try {
                result = streamText({
                    model,
                    messages: messages,
                    temperature: options.temperature,
                    maxOutputTokens: options.maxTokens, // No default limit - unlimited unless specified
                    maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
                    tools,
                    stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
                    toolChoice: resolvedToolChoice,
                    abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
                    experimental_repairToolCall: this.getToolCallRepairFn(options),
                    experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
                    onStepFinish: ({ toolCalls, toolResults }) => {
                        logger.info("Tool execution completed", {
                            toolResults,
                            toolCalls,
                        });
                        // Emit tool:end for each completed tool result so Pipeline B
                        // captures telemetry for AI-SDK-driven tool calls (gap S2).
                        emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), toolResults);
                        // Handle tool execution storage
                        this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
                            logger.warn("[OpenAIProvider] Failed to store tool executions", {
                                provider: this.providerName,
                                error: error instanceof Error ? error.message : String(error),
                            });
                        });
                    },
                });
            }
            catch (streamError) {
                this.endStreamSpanWithError(streamSpan, streamError);
                throw streamError;
            }
            // Collect token usage and finish reason asynchronously when the stream completes,
            // then end the span. This avoids blocking the stream consumer.
            Promise.resolve(result.usage)
                .then((usage) => {
                streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens || 0);
                streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens || 0);
                const cost = calculateCost(this.providerName, this.modelName, {
                    input: usage.inputTokens || 0,
                    output: usage.outputTokens || 0,
                    total: (usage.inputTokens || 0) + (usage.outputTokens || 0),
                });
                if (cost && cost > 0) {
                    streamSpan.setAttribute("neurolink.cost", cost);
                }
            })
                .catch(() => {
                // Usage may not be available if the stream is aborted
            });
            Promise.resolve(result.finishReason)
                .then((reason) => {
                streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
            })
                .catch(() => {
                // Finish reason may not be available if the stream is aborted
            });
            Promise.resolve(result.text)
                .then(() => {
                streamSpan.end();
            })
                .catch((err) => {
                this.endStreamSpanWithError(streamSpan, err);
            });
            timeoutController?.cleanup();
            // Debug the actual result structure
            logger.debug(`OpenAI: streamText result structure:`, {
                resultKeys: Object.keys(result),
                hasTextStream: !!result.textStream,
                hasToolCalls: !!result.toolCalls,
                hasToolResults: !!result.toolResults,
                resultType: typeof result,
            });
            const transformedStream = this.createOpenAITransformedStream(result, shouldUseTools, tools);
            // Create analytics promise that resolves after stream completion
            const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
                requestId: `openai-stream-${Date.now()}`,
                streamingMode: true,
            });
            return {
                stream: transformedStream,
                provider: this.providerName,
                model: this.modelName,
                analytics: analyticsPromise,
                metadata: {
                    startTime,
                    streamId: `openai-${Date.now()}`,
                },
            };
        }
        catch (error) {
            timeoutController?.cleanup();
            throw this.handleProviderError(error);
        }
    }
    async *createOpenAITransformedStream(result, shouldUseTools, tools) {
        try {
            logger.debug(`OpenAI: Starting stream transformation`, {
                hasTextStream: !!result.textStream,
                hasFullStream: !!result.fullStream,
                resultKeys: Object.keys(result),
                toolsEnabled: shouldUseTools,
                toolsCount: Object.keys(tools).length,
            });
            let chunkCount = 0;
            let contentYielded = 0;
            const streamToUse = result.fullStream || result.textStream;
            if (!streamToUse) {
                logger.error("OpenAI: No stream available in result", {
                    resultKeys: Object.keys(result),
                });
                return;
            }
            logger.debug(`OpenAI: Stream source selected:`, {
                usingFullStream: !!result.fullStream,
                usingTextStream: !!result.textStream && !result.fullStream,
                streamSourceType: result.fullStream ? "fullStream" : "textStream",
            });
            for await (const chunk of streamToUse) {
                chunkCount++;
                logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
                    chunkType: typeof chunk,
                    chunkValue: typeof chunk === "string"
                        ? chunk.substring(0, 50)
                        : "not-string",
                    chunkKeys: chunk && typeof chunk === "object"
                        ? Object.keys(chunk)
                        : "not-object",
                    hasText: chunk && typeof chunk === "object" && "text" in chunk,
                    hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
                    hasType: chunk && typeof chunk === "object" && "type" in chunk,
                    chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
                        ? chunk.type
                        : "no-type",
                });
                const contentToYield = this.extractOpenAIChunkContent(chunk);
                if (contentToYield) {
                    contentYielded++;
                    logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
                        content: contentToYield.substring(0, 50),
                        length: contentToYield.length,
                    });
                    yield { content: contentToYield };
                }
            }
            logger.debug(`OpenAI: Stream transformation completed`, {
                totalChunks: chunkCount,
                contentYielded,
                success: contentYielded > 0,
            });
            if (contentYielded === 0) {
                logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
            }
        }
        catch (streamError) {
            if (NoOutputGeneratedError.isInstance(streamError)) {
                logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
                return;
            }
            logger.error(`OpenAI: Stream transformation error:`, streamError);
            throw streamError;
        }
    }
    extractOpenAIChunkContent(chunk) {
        if (chunk && typeof chunk === "object") {
            if (process.env.NEUROLINK_DEBUG === "true") {
                logger.debug(`OpenAI: Full chunk structure:`, {
                    chunkKeys: Object.keys(chunk),
                    fullChunk: JSON.stringify(chunk).substring(0, 500),
                });
            }
            if ("type" in chunk && chunk.type === "error") {
                const errorChunk = chunk;
                logger.error(`OpenAI: Error chunk received:`, {
                    errorType: errorChunk.type,
                    errorDetails: errorChunk.error,
                    fullChunk: JSON.stringify(chunk),
                });
                const errorMessage = errorChunk.error &&
                    typeof errorChunk.error === "object" &&
                    "message" in errorChunk.error
                    ? String(errorChunk.error.message)
                    : "OpenAI API error when tools are enabled";
                throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
            }
            if ("type" in chunk &&
                chunk.type === "text-delta" &&
                "textDelta" in chunk) {
                const textDelta = chunk.textDelta;
                logger.debug(`OpenAI: Found text-delta:`, { textDelta });
                return textDelta;
            }
            if ("text" in chunk) {
                const text = chunk.text;
                logger.debug(`OpenAI: Found direct text:`, { text });
                return text;
            }
            if (process.env.NEUROLINK_DEBUG === "true") {
                logger.debug(`OpenAI: Unhandled object chunk:`, {
                    chunkKeys: Object.keys(chunk),
                    chunkType: "type" in chunk
                        ? String(chunk.type)
                        : "no-type",
                    fullChunk: JSON.stringify(chunk).substring(0, 500),
                });
            }
            return null;
        }
        if (typeof chunk === "string") {
            logger.debug(`OpenAI: Found string chunk:`, {
                content: chunk,
            });
            return chunk;
        }
        logger.warn(`OpenAI: Unhandled chunk type:`, {
            type: typeof chunk,
            value: String(chunk).substring(0, 100),
        });
        return null;
    }
    /**
     * Generate embeddings for text using OpenAI text-embedding models
     * @param text - The text to embed
     * @param modelName - The embedding model to use (default: text-embedding-3-small)
     * @returns Promise resolving to the embedding vector
     */
    async embed(text, modelName) {
        const embeddingModelName = modelName || "text-embedding-3-small";
        logger.debug("Generating embedding", {
            provider: this.providerName,
            model: embeddingModelName,
            textLength: text.length,
        });
        try {
            // Create embedding model using the AI SDK
            // Create the OpenAI provider, preferring per-instance credentials over env vars
            const openai = createOpenAI({
                apiKey: this.credentials?.apiKey ?? getOpenAIApiKey(),
                ...(this.credentials?.baseURL
                    ? { baseURL: this.credentials.baseURL }
                    : {}),
                fetch: createProxyFetch(),
            });
            // Get the text embedding model
            const embeddingModel = openai.textEmbeddingModel(embeddingModelName);
            // Generate the embedding
            const result = await embed({
                model: embeddingModel,
                value: text,
            });
            logger.debug("Embedding generated successfully", {
                provider: this.providerName,
                model: embeddingModelName,
                embeddingDimension: result.embedding.length,
            });
            return result.embedding;
        }
        catch (error) {
            logger.error("Embedding generation failed", {
                error: error instanceof Error ? error.message : String(error),
                model: embeddingModelName,
                textLength: text.length,
            });
            throw this.handleProviderError(error);
        }
    }
    /**
     * Generate embeddings for multiple texts in a single batch
     * @param texts - The texts to embed
     * @param modelName - The embedding model to use (default: text-embedding-3-small)
     * @returns Promise resolving to an array of embedding vectors
     */
    async embedMany(texts, modelName) {
        const embeddingModelName = modelName || "text-embedding-3-small";
        logger.debug("Generating batch embeddings", {
            provider: this.providerName,
            model: embeddingModelName,
            count: texts.length,
        });
        try {
            // Prefer per-instance credentials over env vars
            const openai = createOpenAI({
                apiKey: this.credentials?.apiKey ?? getOpenAIApiKey(),
                ...(this.credentials?.baseURL
                    ? { baseURL: this.credentials.baseURL }
                    : {}),
                fetch: createProxyFetch(),
            });
            const embeddingModel = openai.textEmbeddingModel(embeddingModelName);
            const result = await embedMany({
                model: embeddingModel,
                values: texts,
            });
            logger.debug("Batch embeddings generated successfully", {
                provider: this.providerName,
                model: embeddingModelName,
                count: result.embeddings.length,
                embeddingDimension: result.embeddings[0]?.length,
            });
            return result.embeddings;
        }
        catch (error) {
            logger.error("Batch embedding generation failed", {
                error: error instanceof Error ? error.message : String(error),
                model: embeddingModelName,
                count: texts.length,
            });
            throw this.handleProviderError(error);
        }
    }
}
// Export for factory registration
export default OpenAIProvider;
//# sourceMappingURL=openAI.js.map