import type { MetricPoint } from "./metrics.ts";
import type { LogRecord } from "./logging.ts";
import type { MetricExporter } from "./server.ts";
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";

export type FrameworkSpanKind =
  "database" | "cache" | "queue" | "realtime" | "server-action" | "application";

export interface OperationTracer {
  span<T>(
    kind: FrameworkSpanKind,
    name: string,
    operation: () => T | Promise<T>,
    attributes?: Record<string, string | number | boolean>,
  ): Promise<T>;
}

const randomHex = (bytes: number) =>
  Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) =>
    byte.toString(16).padStart(2, "0"),
  ).join("");

export function createOperationTracer(
  options: {
    exporter?: SpanExporter;
    context?: () => Partial<TraceContext>;
    now?: () => number;
    onExportError?: (error: unknown) => void;
    diagnostic?: ObservabilityDiagnostic;
  } = {},
): OperationTracer {
  const now = options.now ?? Date.now;
  return {
    async span(kind, name, operation, attributes = {}) {
      const context = options.context?.() ?? {};
      const started = now();
      let status: SpanRecord["status"] = "ok";
      let failure: unknown;
      try {
        return await operation();
      } catch (error) {
        status = "error";
        failure = error;
        throw error;
      } finally {
        const ended = now();
        const record: SpanRecord = {
          name,
          traceId: context.traceId ?? randomHex(16),
          spanId: randomHex(8),
          ...(context.spanId ? { parentSpanId: context.spanId } : {}),
          sampled: context.sampled ?? true,
          startTime: started,
          endTime: ended,
          durationMs: ended - started,
          status,
          attributes: { "wrnexus.span.kind": kind, ...attributes },
          ...(failure
            ? {
                error: {
                  name: failure instanceof Error ? failure.name : "Error",
                  message: failure instanceof Error ? failure.message : String(failure),
                },
              }
            : {}),
        };
        try {
          await options.exporter?.export([record]);
        } catch (error) {
          if (options.onExportError) options.onExportError(error);
          else
            reportObservabilityFailure("operation span export failed", error, options.diagnostic);
        }
      }
    },
  };
}

const safeMetricName = (name: string) => name.replace(/[^a-zA-Z0-9_:]/g, "_");
const labels = (point: MetricPoint) => {
  const entries = Object.entries(point.labels);
  return entries.length
    ? `{${entries.map(([key, value]) => `${safeMetricName(key)}=${JSON.stringify(String(value))}`).join(",")}}`
    : "";
};

export function renderPrometheus(points: readonly MetricPoint[]): string {
  return `${points.map((point) => `${safeMetricName(point.name)}${labels(point)} ${point.type === "histogram" ? (point.sum ?? 0) : point.value} ${point.timestamp}`).join("\n")}\n`;
}

export function createPrometheusPushExporter(
  endpoint: string,
  options: { fetch?: typeof fetch; headers?: HeadersInit } = {},
): MetricExporter {
  return {
    async export(points) {
      const response = await (options.fetch ?? fetch)(endpoint, {
        method: "POST",
        headers: {
          "content-type": "text/plain; version=0.0.4",
          ...Object.fromEntries(new Headers(options.headers)),
        },
        body: renderPrometheus(points),
      });
      if (!response.ok) throw new Error(`Prometheus export failed with ${response.status}.`);
    },
  };
}

export function createZipkinExporter(
  endpoint: string,
  options: { fetch?: typeof fetch; serviceName?: string } = {},
): SpanExporter {
  return {
    async export(spans) {
      const body = spans.map((span) => ({
        traceId: span.traceId,
        id: span.spanId,
        parentId: span.parentSpanId,
        name: span.name,
        timestamp: Math.trunc(span.startTime * 1000),
        duration: Math.trunc(span.durationMs * 1000),
        localEndpoint: { serviceName: options.serviceName ?? "wrnexus" },
        tags: Object.fromEntries(
          Object.entries(span.attributes).map(([key, value]) => [key, String(value)]),
        ),
        ...(span.error ? { error: span.error.message } : {}),
      }));
      const response = await (options.fetch ?? fetch)(endpoint, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!response.ok) throw new Error(`Zipkin export failed with ${response.status}.`);
    },
  };
}

/** Jaeger accepts Zipkin v2 JSON at its compatibility endpoint. */
export const createJaegerExporter = createZipkinExporter;

export interface LogExporter {
  export(records: readonly LogRecord[]): void | Promise<void>;
}
export function createOtlpLogExporter(
  endpoint: string,
  options: { fetch?: typeof fetch; headers?: HeadersInit } = {},
): LogExporter {
  return {
    async export(records) {
      const response = await (options.fetch ?? fetch)(endpoint, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          ...Object.fromEntries(new Headers(options.headers)),
        },
        body: JSON.stringify({
          resourceLogs: [
            {
              scopeLogs: [
                {
                  scope: { name: "@wrnexus/observability" },
                  logRecords: records.map((record) => ({
                    timeUnixNano: String(BigInt(new Date(record.timestamp).getTime()) * 1_000_000n),
                    severityText: record.level.toUpperCase(),
                    body: { stringValue: record.message },
                    attributes: Object.entries(record.attributes).map(([key, value]) => ({
                      key,
                      value: { stringValue: String(value) },
                    })),
                  })),
                },
              ],
            },
          ],
        }),
      });
      if (!response.ok) throw new Error(`OTLP log export failed with ${response.status}.`);
    },
  };
}

export interface ErrorReporter {
  capture(error: unknown, context?: Record<string, unknown>): Promise<string>;
}
export function createSentryCompatibleReporter(
  endpoint: string,
  options: { fetch?: typeof fetch; publicKey?: string } = {},
): ErrorReporter {
  return {
    async capture(error, context = {}) {
      const eventId = randomHex(16);
      const failure = error instanceof Error ? error : new Error(String(error));
      const response = await (options.fetch ?? fetch)(endpoint, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          ...(options.publicKey
            ? { "x-sentry-auth": `Sentry sentry_key=${options.publicKey}, sentry_version=7` }
            : {}),
        },
        body: JSON.stringify({
          event_id: eventId,
          timestamp: new Date().toISOString(),
          platform: "javascript",
          level: "error",
          exception: {
            values: [{ type: failure.name, value: failure.message, stacktrace: failure.stack }],
          },
          contexts: context,
        }),
      });
      if (!response.ok) throw new Error(`Error report failed with ${response.status}.`);
      return eventId;
    },
  };
}

export function createPerformanceProfiler(
  options: {
    now?: () => number;
    onProfile?: (profile: {
      name: string;
      durationMs: number;
      attributes: Record<string, unknown>;
    }) => void;
  } = {},
) {
  const now = options.now ?? performance.now.bind(performance);
  return async <T>(
    name: string,
    operation: () => T | Promise<T>,
    attributes: Record<string, unknown> = {},
  ): Promise<T> => {
    const started = now();
    try {
      return await operation();
    } finally {
      options.onProfile?.({ name, durationMs: now() - started, attributes });
    }
  };
}
