import { createHash, randomBytes } from "node:crypto";

const OTLP_TIMEOUT_MS = 1500;
const MIN_VALID_EPOCH_MS = Date.UTC(2020, 0, 1);
const ZERO_TRACE_ID = "00000000000000000000000000000000";
const ZERO_SPAN_ID = "0000000000000000";

export function codeAgentOtelTraceFields(traceId, env = process.env) {
  const context = codeAgentOtelTraceContext(traceId);
  return {
    otelTraceId: context.traceId,
    traceparent: context.traceparent,
    otelTrace: {
      traceId: context.traceId,
      businessTraceId: context.businessTraceId,
      tempoPath: `/api/traces/${context.traceId}`,
      exporterConfigured: Boolean(resolveOtlpTracesEndpoint(env)),
      valuesPrinted: false
    }
  };
}

export function codeAgentOtelTraceContext(traceId) {
  const businessTraceId = String(traceId ?? "").trim() || "trc_unassigned";
  const otelTraceId = nonZeroHex(createHash("sha256").update(`hwlab-code-agent:${businessTraceId}`).digest("hex").slice(0, 32), ZERO_TRACE_ID);
  const parentSpanId = nonZeroHex(createHash("sha256").update(`hwlab-code-agent-parent:${businessTraceId}`).digest("hex").slice(0, 16), ZERO_SPAN_ID);
  return {
    businessTraceId,
    traceId: otelTraceId,
    parentSpanId,
    traceparent: `00-${otelTraceId}-${parentSpanId}-01`,
    valuesPrinted: false
  };
}

export function workbenchUiOtelTraceContext(input = {}) {
  const businessTraceId = String(input.uiTraceId ?? input.traceId ?? "").trim() || "ui_unassigned";
  const explicitTraceId = normalizeOtelTraceId(input.otelTraceId);
  const otelTraceId = explicitTraceId ?? nonZeroHex(createHash("sha256").update(`hwlab-workbench-ui:${businessTraceId}`).digest("hex").slice(0, 32), ZERO_TRACE_ID);
  const parentSpanId = normalizeOtelSpanId(input.parentSpanId)
    ?? nonZeroHex(createHash("sha256").update(`hwlab-workbench-ui-parent:${businessTraceId}:${otelTraceId}`).digest("hex").slice(0, 16), ZERO_SPAN_ID);
  return {
    businessTraceId,
    traceId: otelTraceId,
    parentSpanId,
    traceparent: `00-${otelTraceId}-${parentSpanId}-01`,
    valuesPrinted: false
  };
}

export function authLoginOtelTraceContext(input = {}) {
  const parsed = parseOtelTraceparent(input.traceparent);
  const traceId = parsed?.traceId ?? nonZeroHex(randomBytes(16).toString("hex"), ZERO_TRACE_ID);
  const rootSpanId = normalizeOtelSpanId(input.rootSpanId) ?? newOtelSpanId();
  return {
    traceId,
    parentSpanId: parsed?.spanId ?? null,
    rootSpanId,
    traceparent: `00-${traceId}-${rootSpanId}-01`,
    valuesPrinted: false
  };
}

export function httpRequestOtelTraceContext(input = {}) {
  const parsed = parseOtelTraceparent(input.traceparent);
  const traceId = parsed?.traceId
    ?? normalizeOtelTraceId(input.traceId)
    ?? nonZeroHex(randomBytes(16).toString("hex"), ZERO_TRACE_ID);
  const spanId = normalizeOtelSpanId(input.spanId) ?? newOtelSpanId();
  return {
    traceId,
    parentSpanId: parsed?.spanId ?? null,
    spanId,
    traceparent: `00-${traceId}-${spanId}-01`,
    valuesPrinted: false
  };
}

export function newOtelSpanId() {
  return nonZeroHex(randomBytes(8).toString("hex"), ZERO_SPAN_ID);
}

export function traceparentForOtelSpan(context, spanId) {
  const traceId = normalizeOtelTraceId(context?.traceId);
  const normalizedSpanId = normalizeOtelSpanId(spanId);
  return traceId && normalizedSpanId ? `00-${traceId}-${normalizedSpanId}-01` : "";
}

export async function emitCodeAgentOtelSpan(name, traceId, env = process.env, options = {}) {
  const endpoint = resolveOtlpTracesEndpoint(env);
  if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false };
  const context = codeAgentOtelTraceContext(traceId);
  const now = Date.now();
  const startedAtMs = epochUnixMs(options.startTimeMs, now);
  const endedAtMs = epochUnixMs(options.endTimeMs, now);
  const spanId = nonZeroHex(String(options.spanId ?? randomBytes(8).toString("hex")).slice(0, 16), ZERO_SPAN_ID);
  const parentSpanId = typeof options.parentSpanId === "string" && /^[0-9a-f]{16}$/u.test(options.parentSpanId) ? options.parentSpanId : context.parentSpanId;
  const statusCode = options.status === "error" || options.error ? 2 : 1;
  const body = {
    resourceSpans: [{
      resource: { attributes: attributesFromRecord(resourceAttributes(env)) },
      scopeSpans: [{
        scope: { name: "hwlab.code-agent", version: "1" },
        spans: [{
          traceId: context.traceId,
          spanId,
          parentSpanId,
          name,
          kind: Number(options.kind ?? 1),
          startTimeUnixNano: unixNano(startedAtMs),
          endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)),
          attributes: attributesFromRecord({
            traceId: context.businessTraceId,
            "otel.trace_id": context.traceId,
            "code_agent.stage": name,
            ...options.attributes
          }),
          status: {
            code: statusCode,
            ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {})
          }
        }]
      }]
    }]
  };
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);
  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
      signal: controller.signal
    });
    return { ok: response.ok, status: response.status, valuesPrinted: false };
  } catch (error) {
    return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", valuesPrinted: false };
  } finally {
    clearTimeout(timeout);
  }
}

export async function emitWorkbenchUiOtelSpan(name, uiTraceId, env = process.env, options = {}) {
  const endpoint = resolveOtlpTracesEndpoint(env);
  if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false };
  const context = workbenchUiOtelTraceContext({ uiTraceId, otelTraceId: options.otelTraceId, parentSpanId: options.parentSpanId });
  const now = Date.now();
  const startedAtMs = epochUnixMs(options.startTimeMs, now);
  const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs);
  const spanId = normalizeOtelSpanId(options.spanId) ?? nonZeroHex(randomBytes(8).toString("hex"), ZERO_SPAN_ID);
  const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? context.parentSpanId;
  const statusCode = options.status === "error" || options.error ? 2 : 1;
  const body = {
    resourceSpans: [{
      resource: { attributes: attributesFromRecord(resourceAttributes(env)) },
      scopeSpans: [{
        scope: { name: "hwlab.workbench-ui", version: "1" },
        spans: [{
          traceId: context.traceId,
          spanId,
          parentSpanId,
          name,
          kind: Number(options.kind ?? 1),
          startTimeUnixNano: unixNano(startedAtMs),
          endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)),
          attributes: attributesFromRecord({
            "ui.trace_id": context.businessTraceId,
            "otel.trace_id": context.traceId,
            "workbench.ui.stage": name,
            ...options.attributes
          }),
          status: {
            code: statusCode,
            ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {})
          }
        }]
      }]
    }]
  };
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);
  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
      signal: controller.signal
    });
    return { ok: response.ok, status: response.status, traceId: context.traceId, valuesPrinted: false };
  } catch (error) {
    return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId: context.traceId, valuesPrinted: false };
  } finally {
    clearTimeout(timeout);
  }
}

export async function emitAuthOtelSpan(name, context, env = process.env, options = {}) {
  const endpoint = resolveOtlpTracesEndpoint(env);
  if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false };
  const traceContext = normalizeOtelTraceId(context?.traceId) ? context : authLoginOtelTraceContext({ traceparent: context?.traceparent });
  const now = Date.now();
  const startedAtMs = epochUnixMs(options.startTimeMs, now);
  const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs);
  const spanId = normalizeOtelSpanId(options.spanId) ?? newOtelSpanId();
  const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? null;
  const statusCode = options.status === "error" || options.error ? 2 : 1;
  const span = {
    traceId: traceContext.traceId,
    spanId,
    ...(parentSpanId ? { parentSpanId } : {}),
    name,
    kind: Number(options.kind ?? 1),
    startTimeUnixNano: unixNano(startedAtMs),
    endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)),
    attributes: attributesFromRecord({
      "otel.trace_id": traceContext.traceId,
      "auth.stage": name,
      ...options.attributes
    }),
    status: {
      code: statusCode,
      ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {})
    }
  };
  const body = {
    resourceSpans: [{
      resource: { attributes: attributesFromRecord(resourceAttributes(env)) },
      scopeSpans: [{
        scope: { name: "hwlab.auth", version: "1" },
        spans: [span]
      }]
    }]
  };
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);
  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
      signal: controller.signal
    });
    return { ok: response.ok, status: response.status, traceId: traceContext.traceId, valuesPrinted: false };
  } catch (error) {
    return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId: traceContext.traceId, valuesPrinted: false };
  } finally {
    clearTimeout(timeout);
  }
}

export async function emitHttpServerRequestSpan(context, env = process.env, options = {}) {
  const endpoint = resolveOtlpTracesEndpoint(env);
  if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false };
  const traceId = normalizeOtelTraceId(context?.traceId);
  if (!traceId) return { ok: false, skipped: true, reason: "trace-id-missing", valuesPrinted: false };
  const spanId = normalizeOtelSpanId(options.spanId) ?? newOtelSpanId();
  const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? normalizeOtelSpanId(context?.parentSpanId) ?? null;
  const now = Date.now();
  const startedAtMs = epochUnixMs(options.startTimeMs, now);
  const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs);
  const method = String(options.method ?? context?.method ?? "GET").toUpperCase();
  const route = String(options.route ?? context?.route ?? "/").split("?")[0] || "/";
  const statusCode = Number.isInteger(Number(options.statusCode)) ? Number(options.statusCode) : 0;
  const statusClassValue = statusClassFor(statusCode);
  const errorCode = options.errorCode ? String(options.errorCode) : null;
  const isError = options.status === "error" || Boolean(options.error) || statusCode >= 500;
  const name = typeof options.name === "string" && options.name.trim() ? options.name.trim() : `${method} ${route}`;
  const attributes = {
    "otel.trace_id": traceId,
    "request.id": context?.requestId ?? null,
    "http.request.method": method,
    "http.route": route,
    "http.response.status_code": statusCode || null,
    "http.response.status_class": statusClassValue,
    "hwlab.http.stage": "http.server.request",
    ...options.attributes,
    ...(errorCode ? { "error.code": errorCode, "error.category": options.errorCategory ?? null, "error.layer": options.errorLayer ?? null } : {})
  };
  const body = {
    resourceSpans: [{
      resource: { attributes: attributesFromRecord(resourceAttributes(env)) },
      scopeSpans: [{
        scope: { name: "hwlab.http.server", version: "1" },
        spans: [{
          traceId,
          spanId,
          ...(parentSpanId ? { parentSpanId } : {}),
          name,
          kind: 2,
          startTimeUnixNano: unixNano(startedAtMs),
          endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)),
          attributes: attributesFromRecord(attributes),
          status: {
            code: isError ? 2 : 1,
            ...(isError && options.statusMessage ? { message: String(options.statusMessage).slice(0, 300) } : {})
          }
        }]
      }]
    }]
  };
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);
  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
      signal: controller.signal
    });
    return { ok: response.ok, status: response.status, traceId, valuesPrinted: false };
  } catch (error) {
    return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId, valuesPrinted: false };
  } finally {
    clearTimeout(timeout);
  }
}

function statusClassFor(statusCode) {
  if (statusCode >= 500) return "5xx";
  if (statusCode >= 400) return "4xx";
  if (statusCode >= 300) return "3xx";
  if (statusCode >= 200) return "2xx";
  if (statusCode >= 100) return "1xx";
  return "unknown";
}

function resolveOtlpTracesEndpoint(env = process.env) {
  const explicit = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
  if (explicit) return explicit.replace(/\/+$/u, "");
  const base = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_ENDPOINT, env.OTEL_EXPORTER_OTLP_ENDPOINT);
  return base ? `${base.replace(/\/+$/u, "")}/v1/traces` : null;
}

function resourceAttributes(env = process.env) {
  return {
    "service.name": firstNonEmpty(env.OTEL_SERVICE_NAME, "hwlab-cloud-api"),
    "deployment.environment": firstNonEmpty(env.HWLAB_ENVIRONMENT, env.HWLAB_GITOPS_PROFILE, env.HWLAB_RUNTIME_LANE, "unknown"),
    "unidesk.node": firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.HWLAB_NODE_ID, env.UNIDESK_NODE_ID, "unknown"),
    "hwlab.lane": firstNonEmpty(env.HWLAB_RUNTIME_LANE, env.HWLAB_GITOPS_PROFILE, env.HWLAB_BOOT_REF, "unknown"),
    "k8s.namespace.name": firstNonEmpty(env.HWLAB_RUNTIME_NAMESPACE, env.POD_NAMESPACE, env.HWLAB_NAMESPACE, "unknown"),
    "git.commit": firstNonEmpty(env.HWLAB_COMMIT_ID, env.HWLAB_GITOPS_SOURCE_COMMIT, env.HWLAB_REVISION, "unknown")
  };
}

function attributesFromRecord(record = {}) {
  return Object.entries(record)
    .filter(([, value]) => value !== undefined && value !== null)
    .map(([key, value]) => ({ key, value: otlpAnyValue(value) }));
}

function otlpAnyValue(value) {
  if (typeof value === "boolean") return { boolValue: value };
  if (typeof value === "number" && Number.isInteger(value)) return { intValue: String(value) };
  if (typeof value === "number" && Number.isFinite(value)) return { doubleValue: value };
  return { stringValue: typeof value === "string" ? value : JSON.stringify(value) };
}

function unixNano(ms) {
  return String(Math.trunc(ms * 1_000_000));
}

function epochUnixMs(value, fallback) {
  const numeric = Number(value);
  return Number.isFinite(numeric) && numeric >= MIN_VALID_EPOCH_MS ? numeric : fallback;
}

function firstNonEmpty(...values) {
  for (const value of values) {
    const text = String(value ?? "").trim();
    if (text) return text;
  }
  return null;
}

function nonZeroHex(value, zero) {
  return /^[0-9a-f]+$/u.test(value) && value !== zero ? value : zero.replace(/0$/u, "1");
}

function normalizeOtelTraceId(value) {
  const text = String(value ?? "").trim().toLowerCase();
  return /^[0-9a-f]{32}$/u.test(text) && text !== ZERO_TRACE_ID ? text : null;
}

function normalizeOtelSpanId(value) {
  const text = String(value ?? "").trim().toLowerCase();
  return /^[0-9a-f]{16}$/u.test(text) && text !== ZERO_SPAN_ID ? text : null;
}

function parseOtelTraceparent(value) {
  const match = String(value ?? "").trim().toLowerCase().match(/^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/u);
  if (!match) return null;
  const traceId = normalizeOtelTraceId(match[1]);
  const spanId = normalizeOtelSpanId(match[2]);
  return traceId && spanId ? { traceId, spanId } : null;
}
