/*
 * Copyright OpenSearch Contributors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ExportResultCode } from '@opentelemetry/core';
import { SpanKind, SpanStatusCode } from '@opentelemetry/api';
import { Client } from '@opensearch-project/opensearch';
/**
 * Convert OTel HrTime [seconds, nanoseconds] to ISO date string
 */
function hrTimeToDate(hrTime) {
    const ms = hrTime[0] * 1000 + hrTime[1] / 1_000_000;
    return new Date(ms).toISOString();
}
/**
 * Convert OTel HrTime to nanoseconds
 */
function hrTimeToNanos(hrTime) {
    return hrTime[0] * 1_000_000_000 + hrTime[1];
}
/**
 * Convert duration between two HrTimes to nanoseconds
 */
function durationNanos(start, end) {
    return hrTimeToNanos(end) - hrTimeToNanos(start);
}
/**
 * Replace dots with @ in attribute keys (OSIS format)
 */
function flattenKey(key) {
    return key.replace(/\./g, '@');
}
/**
 * Map OTel SpanKind to OSIS string format
 */
function spanKindToString(kind) {
    switch (kind) {
        case SpanKind.INTERNAL: return 'SPAN_KIND_INTERNAL';
        case SpanKind.SERVER: return 'SPAN_KIND_SERVER';
        case SpanKind.CLIENT: return 'SPAN_KIND_CLIENT';
        case SpanKind.PRODUCER: return 'SPAN_KIND_PRODUCER';
        case SpanKind.CONSUMER: return 'SPAN_KIND_CONSUMER';
        default: return 'SPAN_KIND_INTERNAL';
    }
}
/**
 * Map OTel SpanStatusCode to OSIS integer
 */
function statusCodeToInt(code) {
    switch (code) {
        case SpanStatusCode.UNSET: return 0;
        case SpanStatusCode.OK: return 1;
        case SpanStatusCode.ERROR: return 2;
        default: return 0;
    }
}
/**
 * Convert a ReadableSpan to an OSIS-compatible OpenSearch document
 */
function spanToDocument(span) {
    const doc = {
        traceId: span.spanContext().traceId,
        spanId: span.spanContext().spanId,
        parentSpanId: (span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== '0000000000000000')
            ? span.parentSpanContext.spanId : '',
        name: span.name,
        kind: spanKindToString(span.kind),
        startTime: hrTimeToDate(span.startTime),
        endTime: hrTimeToDate(span.endTime),
        durationInNanos: durationNanos(span.startTime, span.endTime),
        'status.code': statusCodeToInt(span.status.code),
        'status.message': span.status.message || '',
        traceState: '',
        droppedAttributesCount: span.droppedAttributesCount,
        droppedEventsCount: span.droppedEventsCount,
        droppedLinksCount: span.droppedLinksCount,
    };
    // Flatten span attributes with @ separator
    for (const [key, value] of Object.entries(span.attributes)) {
        if (value !== undefined) {
            doc[`span.attributes.${flattenKey(key)}`] = value;
        }
    }
    // Flatten resource attributes with @ separator
    const resource = span.resource;
    if (resource?.attributes) {
        for (const [key, value] of Object.entries(resource.attributes)) {
            if (value !== undefined) {
                doc[`resource.attributes.${flattenKey(key)}`] = value;
            }
        }
    }
    // Set serviceName from resource
    const serviceName = resource?.attributes?.['service.name'] || 'unknown';
    doc.serviceName = serviceName;
    // Instrumentation scope — runtime property is instrumentationScope (not instrumentationLibrary)
    const scope = span.instrumentationScope ?? span.instrumentationLibrary ?? {};
    doc['instrumentationScope.name'] = scope.name || '';
    if (scope.version) {
        doc['instrumentationScope.version'] = scope.version;
    }
    // Events (nested array with @ separator in attribute keys)
    if (span.events.length > 0) {
        doc.events = span.events.map(event => {
            const eventDoc = {
                name: event.name,
                time: hrTimeToDate(event.time),
                droppedAttributesCount: event.droppedAttributesCount || 0,
            };
            if (event.attributes) {
                const attrs = {};
                for (const [key, value] of Object.entries(event.attributes)) {
                    if (value !== undefined) {
                        attrs[flattenKey(key)] = value;
                    }
                }
                eventDoc.attributes = attrs;
            }
            return eventDoc;
        });
    }
    else {
        doc.events = [];
    }
    // Links
    if (span.links.length > 0) {
        doc.links = span.links.map(link => ({
            traceId: link.context.traceId,
            spanId: link.context.spanId,
            traceState: link.context.traceState?.serialize() || '',
            attributes: link.attributes || {},
            droppedAttributesCount: link.droppedAttributesCount || 0,
        }));
    }
    else {
        doc.links = [];
    }
    // traceGroupFields (populated for root spans)
    const hasRealParent = span.parentSpanContext?.spanId && span.parentSpanContext.spanId !== '0000000000000000';
    if (!hasRealParent) {
        doc.traceGroup = span.name;
        doc.traceGroupFields = {
            endTime: hrTimeToDate(span.endTime),
            durationInNanos: durationNanos(span.startTime, span.endTime),
            statusCode: statusCodeToInt(span.status.code),
        };
    }
    return doc;
}
/**
 * Custom SpanExporter that writes directly to OpenSearch.
 *
 * Formats spans in OSIS-compatible format so eval spans co-exist
 * with agent spans in the same otel-v1-apm-span-* indices.
 */
export class OpenSearchSpanExporter {
    constructor(config) {
        this.shutdownRequested = false;
        const auth = config.username && config.password
            ? `${config.username}:${config.password}`
            : undefined;
        this.client = new Client({
            node: config.endpoint,
            auth: auth ? { username: config.username, password: config.password } : undefined,
            ssl: {
                rejectUnauthorized: config.tlsSkipVerify !== true,
            },
        });
        this.indexName = config.indexName || 'otel-v1-apm-span-000001';
        console.log(`[Telemetry] OpenSearch exporter initialized → ${config.endpoint} (index: ${this.indexName})`);
    }
    export(spans, resultCallback) {
        if (this.shutdownRequested) {
            resultCallback({ code: ExportResultCode.FAILED });
            return;
        }
        this._export(spans)
            .then(() => resultCallback({ code: ExportResultCode.SUCCESS }))
            .catch((err) => {
            console.error('[Telemetry] OpenSearch export failed:', err instanceof Error ? err.message : err);
            resultCallback({ code: ExportResultCode.FAILED });
        });
    }
    async _export(spans) {
        if (spans.length === 0)
            return;
        // Use bulk API for efficiency
        const body = [];
        for (const span of spans) {
            const doc = spanToDocument(span);
            const docId = `${doc.traceId}/${doc.spanId}`;
            body.push({ index: { _index: this.indexName, _id: docId } });
            body.push(doc);
        }
        const response = await this.client.bulk({ body });
        if (response.body.errors) {
            const errors = response.body.items
                .filter((item) => item.index?.error)
                .map((item) => `${item.index.error.type}: ${item.index.error.reason}`);
            if (errors.length > 0) {
                console.warn(`[Telemetry] OpenSearch bulk write had ${errors.length} error(s):`, errors[0]);
            }
        }
        else {
            console.log(`[Telemetry] Exported ${spans.length} span(s) to OpenSearch`);
        }
    }
    async shutdown() {
        this.shutdownRequested = true;
        await this.client.close();
    }
    async forceFlush() {
        // No buffering — exports are immediate via bulk API
    }
}
//# sourceMappingURL=opensearchExporter.js.map