#!/usr/bin/env node
import { randomBytes } from "node:crypto";
import { createServer, request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { Transform } from "node:stream";

const serviceId = process.env.OTEL_SERVICE_NAME || process.env.HWLAB_SERVICE_ID || "opencode-provider-proxy";
const port = parsePositiveInteger(process.env.PORT || process.env.HWLAB_OPENCODE_PROVIDER_PROXY_PORT, 4097);
const upstreamBaseUrl = new URL(requiredEnv("HWLAB_OPENCODE_PROVIDER_PROXY_UPSTREAM_BASE_URL"));
const publicBasePath = normalizeBasePath(process.env.HWLAB_OPENCODE_PROVIDER_PROXY_PUBLIC_BASE_PATH || "/v1");
const timeoutMs = parsePositiveInteger(process.env.HWLAB_OPENCODE_PROVIDER_PROXY_TIMEOUT_MS, 600000);
const otelEndpoint = otelTracesEndpoint(process.env);
const startedAt = new Date();

const hopByHopHeaders = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);

const server = createServer((clientReq, clientRes) => {
  const url = new URL(clientReq.url || "/", "http://opencode-provider-proxy.local");
  if (url.pathname === "/health" || url.pathname === "/health/live" || url.pathname === "/health/readiness") {
    sendJson(clientRes, 200, healthPayload());
    return;
  }
  proxyProviderRequest(clientReq, clientRes, url).catch((error) => {
    if (!clientRes.headersSent) {
      sendJson(clientRes, 502, {
        error: "opencode_provider_proxy_failed",
        message: "OpenCode provider proxy failed",
        reason: error instanceof Error ? error.message : String(error)
      });
    } else {
      clientRes.destroy(error);
    }
  });
});

server.listen(port, "0.0.0.0", () => {
  process.stdout.write(JSON.stringify({ serviceId, status: "listening", port, upstreamHost: upstreamBaseUrl.host, publicBasePath, valuesPrinted: false }) + "\n");
});

async function proxyProviderRequest(clientReq, clientRes, url) {
  const trace = traceContextFromHeaders(clientReq.headers);
  const startSpanId = randomOtelSpanId();
  const started = process.hrtime.bigint();
  const startTime = Date.now();
  const target = providerTargetUrl(url);
  const route = providerRoute(url.pathname);
  const attrs = {
    "http.request.method": clientReq.method || "GET",
    "http.route": route,
    "url.scheme": target.protocol.replace(":", ""),
    "server.address": target.hostname,
    "server.port": Number(target.port || (target.protocol === "https:" ? 443 : 80)),
    "opencode.provider.upstream_host": upstreamBaseUrl.host,
    "opencode.provider.public_base_path": publicBasePath,
    valuesPrinted: false
  };

  emitOtelSpanAsync({
    name: "opencode.provider.request.start",
    traceId: trace.traceId,
    spanId: startSpanId,
    parentSpanId: trace.parentSpanId,
    kind: 1,
    startTimeMs: startTime,
    endTimeMs: startTime,
    attributes: { ...attrs, "opencode.provider.phase": "start" }
  });

  clientRes.setHeader("traceparent", traceparent(trace.traceId, startSpanId));
  clientRes.setHeader("x-hwlab-otel-trace-id", trace.traceId);

  await new Promise((resolve) => {
    let settled = false;
    let statusCode = 0;
    let errorCode = "";
    let timeout = null;
    let streamStats = createOpenAIStreamStats(false);
    const settle = () => {
      if (settled) return;
      settled = true;
      if (timeout) clearTimeout(timeout);
      const elapsedMs = Number((process.hrtime.bigint() - started) / 1000000n);
      emitOtelSpanAsync({
        name: "opencode.provider.request",
        traceId: trace.traceId,
        spanId: randomOtelSpanId(),
        parentSpanId: startSpanId,
        kind: 3,
        startTimeMs: startTime,
        endTimeMs: startTime + Math.max(elapsedMs, 0),
        statusCode,
        errorCode,
        attributes: {
          ...attrs,
          "http.response.status_code": statusCode || undefined,
          "http.response.status_class": statusCode ? `${Math.floor(statusCode / 100)}xx` : "unknown",
          "opencode.provider.elapsed_ms": elapsedMs,
          "opencode.provider.timeout_ms": timeoutMs,
          "opencode.provider.phase": errorCode ? "failed" : "complete",
          ...openAIStreamStatsAttributes(streamStats)
        }
      });
      resolve();
    };

    const requestImpl = target.protocol === "https:" ? httpsRequest : httpRequest;
    const upstreamReq = requestImpl({
      protocol: target.protocol,
      hostname: target.hostname,
      port: target.port || (target.protocol === "https:" ? 443 : 80),
      method: clientReq.method,
      path: `${target.pathname}${target.search}`,
      headers: providerRequestHeaders(clientReq.headers, target, trace, startSpanId)
    }, (upstreamRes) => {
      statusCode = upstreamRes.statusCode || 502;
      const filterReasoning = shouldFilterOpenAIReasoning(route, upstreamRes.headers);
      streamStats = createOpenAIStreamStats(filterReasoning);
      clientRes.writeHead(statusCode, {
        ...providerResponseHeaders(upstreamRes.headers),
        traceparent: traceparent(trace.traceId, startSpanId),
        "x-hwlab-otel-trace-id": trace.traceId
      });
      const responseStream = filterReasoning ? upstreamRes.pipe(openAIReasoningFilterStream(streamStats)) : upstreamRes;
      responseStream.pipe(clientRes);
      upstreamRes.on("end", settle);
      upstreamRes.on("close", settle);
      upstreamRes.on("error", (error) => {
        errorCode = error?.code || "upstream_response_error";
        if (!clientRes.writableEnded) clientRes.destroy(error);
        settle();
      });
    });

    timeout = setTimeout(() => {
      errorCode = "upstream_timeout";
      upstreamReq.destroy(new Error(`opencode provider proxy timed out after ${timeoutMs}ms`));
    }, timeoutMs);

    upstreamReq.on("error", (error) => {
      if (!errorCode) errorCode = error?.code || "upstream_request_error";
      if (!clientRes.headersSent) {
        statusCode = errorCode === "upstream_timeout" ? 504 : 502;
        sendJson(clientRes, statusCode, {
          error: "opencode_provider_upstream_error",
          message: "OpenCode provider upstream request failed",
          reason: error instanceof Error ? error.message : String(error),
          traceId: trace.traceId,
          valuesPrinted: false
        });
      } else if (!clientRes.writableEnded) {
        clientRes.destroy(error);
      }
      settle();
    });

    clientReq.on("error", (error) => {
      errorCode = error?.code || "client_request_error";
      upstreamReq.destroy(error);
    });
    clientReq.pipe(upstreamReq);
  });
}

function healthPayload() {
  return { serviceId, status: "ok", port, upstreamHost: upstreamBaseUrl.host, publicBasePath, startedAt: startedAt.toISOString(), uptimeSeconds: Math.round((Date.now() - startedAt.getTime()) / 1000), otelEnabled: Boolean(otelEndpoint), valuesPrinted: false };
}

function providerTargetUrl(url) {
  const suffix = stripBasePath(url.pathname, publicBasePath);
  const basePath = upstreamBaseUrl.pathname.replace(/\/+$/u, "");
  const nextPath = suffix ? `${basePath}/${suffix.replace(/^\/+/, "")}` : basePath || "/";
  const target = new URL(upstreamBaseUrl.toString());
  target.pathname = nextPath;
  target.search = url.search;
  return target;
}

function stripBasePath(pathname, basePath) {
  if (!basePath || basePath === "/") return pathname;
  if (pathname === basePath) return "/";
  if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length) || "/";
  return pathname;
}

function providerRoute(pathname) {
  const stripped = stripBasePath(pathname, publicBasePath);
  if (/^\/chat\/completions\/?$/u.test(stripped)) return `${publicBasePath}/chat/completions`;
  if (/^\/models\/?$/u.test(stripped)) return `${publicBasePath}/models`;
  return `${publicBasePath}${stripped.startsWith("/") ? stripped : `/${stripped}`}`;
}

function shouldFilterOpenAIReasoning(route, headers) {
  return route === `${publicBasePath}/chat/completions` && /^text\/event-stream(?:\s*;|$)/iu.test(firstHeaderValue(headers?.["content-type"]));
}

function createOpenAIStreamStats(filterEnabled) {
  return {
    filterEnabled: Boolean(filterEnabled),
    dataLines: 0,
    outputDataLines: 0,
    doneLines: 0,
    jsonErrors: 0,
    choices: 0,
    contentChunks: 0,
    contentChars: 0,
    reasoningChunks: 0,
    reasoningChars: 0,
    reasoningOnlyChoicesDropped: 0,
    emptyDeltaChoicesDropped: 0,
    finishChoices: 0,
    usageChunks: 0
  };
}

function openAIStreamStatsAttributes(stats) {
  return {
    "opencode.provider.sse.filter_enabled": stats.filterEnabled,
    "opencode.provider.sse.data_lines": stats.dataLines,
    "opencode.provider.sse.output_data_lines": stats.outputDataLines,
    "opencode.provider.sse.done_lines": stats.doneLines,
    "opencode.provider.sse.json_errors": stats.jsonErrors,
    "opencode.provider.sse.choice_count": stats.choices,
    "opencode.provider.sse.content_chunks": stats.contentChunks,
    "opencode.provider.sse.content_chars": stats.contentChars,
    "opencode.provider.sse.reasoning_chunks": stats.reasoningChunks,
    "opencode.provider.sse.reasoning_chars": stats.reasoningChars,
    "opencode.provider.sse.reasoning_only_choices_dropped": stats.reasoningOnlyChoicesDropped,
    "opencode.provider.sse.empty_delta_choices_dropped": stats.emptyDeltaChoicesDropped,
    "opencode.provider.sse.finish_choices": stats.finishChoices,
    "opencode.provider.sse.usage_chunks": stats.usageChunks
  };
}

function openAIReasoningFilterStream(stats = createOpenAIStreamStats(true)) {
  let pending = "";
  return new Transform({
    transform(chunk, _encoding, callback) {
      pending += chunk.toString("utf8");
      let newlineIndex = pending.indexOf("\n");
      while (newlineIndex >= 0) {
        const line = pending.slice(0, newlineIndex + 1);
        pending = pending.slice(newlineIndex + 1);
        const next = filterOpenAISseLine(line, stats);
        if (next) {
          stats.outputDataLines += isOpenAIDataLine(next) ? 1 : 0;
          this.push(next);
        }
        newlineIndex = pending.indexOf("\n");
      }
      callback();
    },
    flush(callback) {
      if (pending) {
        const next = filterOpenAISseLine(pending, stats);
        if (next) {
          stats.outputDataLines += isOpenAIDataLine(next) ? 1 : 0;
          this.push(next);
        }
      }
      callback();
    }
  });
}

function isOpenAIDataLine(line) {
  return /^data:\s*/u.test(line);
}

function filterOpenAISseLine(line, stats = createOpenAIStreamStats(true)) {
  const match = line.match(/^(data:\s*)(.*?)(\r?\n)?$/u);
  if (!match) return line;
  const [, prefix, payload, lineEnding = ""] = match;
  const text = payload.trim();
  if (!text) return line;
  stats.dataLines += 1;
  if (text === "[DONE]") {
    stats.doneLines += 1;
    return line;
  }
  try {
    const parsed = JSON.parse(text);
    if (parsed.usage != null) stats.usageChunks += 1;
    const choices = Array.isArray(parsed.choices) ? parsed.choices.map((choice) => filterOpenAIChoiceReasoning(choice, stats)).filter(Boolean) : parsed.choices;
    if (Array.isArray(parsed.choices)) {
      if (choices.length === 0 && parsed.usage === null) return "";
      parsed.choices = choices;
    }
    return `${prefix}${JSON.stringify(parsed)}${lineEnding}`;
  } catch {
    stats.jsonErrors += 1;
    return line;
  }
}

function filterOpenAIChoiceReasoning(choice, stats = createOpenAIStreamStats(true)) {
  if (!choice || typeof choice !== "object") return choice;
  const next = { ...choice };
  stats.choices += 1;
  if (next.finish_reason != null) stats.finishChoices += 1;
  if (next.delta && typeof next.delta === "object") {
    const delta = { ...next.delta };
    const reasoningText = [delta.reasoning_content, delta.reasoning, delta.reasoning_details]
      .filter((value) => typeof value === "string")
      .join("");
    if (reasoningText) {
      stats.reasoningChunks += 1;
      stats.reasoningChars += reasoningText.length;
    }
    if (typeof delta.content === "string" && delta.content.length > 0) {
      stats.contentChunks += 1;
      stats.contentChars += delta.content.length;
    }
    delete delta.reasoning_content;
    delete delta.reasoning;
    delete delta.reasoning_details;
    if (delta.content === null) delete delta.content;
    next.delta = delta;
  }
  if (next.message && typeof next.message === "object") {
    next.message = { ...next.message };
    delete next.message.reasoning_content;
    delete next.message.reasoning;
    delete next.message.reasoning_details;
  }
  const deltaEmpty = next.delta && typeof next.delta === "object" && Object.keys(next.delta).length === 0;
  if (deltaEmpty && next.finish_reason == null && next.logprobs == null) {
    stats.emptyDeltaChoicesDropped += 1;
    stats.reasoningOnlyChoicesDropped += 1;
    return null;
  }
  return next;
}

function providerRequestHeaders(headers, target, trace, parentSpanId) {
  const result = {};
  for (const [name, value] of Object.entries(headers)) {
    const lower = name.toLowerCase();
    if (hopByHopHeaders.has(lower) || lower === "host") continue;
    if (value !== undefined) result[lower] = value;
  }
  result.host = target.host;
  result.traceparent = traceparent(trace.traceId, parentSpanId);
  result["x-hwlab-otel-trace-id"] = trace.traceId;
  result["x-source-service-id"] = serviceId;
  return result;
}

function providerResponseHeaders(headers) {
  const result = {};
  for (const [name, value] of Object.entries(headers)) {
    const lower = name.toLowerCase();
    if (hopByHopHeaders.has(lower) || lower === "content-length") continue;
    if (value !== undefined) result[name] = value;
  }
  return result;
}

function sendJson(response, statusCode, body) {
  const payload = JSON.stringify(body);
  response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(payload) });
  response.end(payload);
}

function emitOtelSpanAsync(span) {
  if (!otelEndpoint) return;
  setTimeout(() => {
    emitOtelSpan(span).catch((error) => {
      process.stderr.write(JSON.stringify({ event: "opencode-provider-proxy-otel-emit-failed", serviceId, error: error instanceof Error ? error.message : String(error), valuesPrinted: false }) + "\n");
    });
  }, 0);
}

async function emitOtelSpan(span) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 1500);
  try {
    await fetch(otelEndpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(otelPayload(span)), signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
}

function otelPayload(span) {
  return { resourceSpans: [{ resource: { attributes: otelAttributes({ "service.name": serviceId, "deployment.environment": process.env.HWLAB_ENVIRONMENT || "v03" }) }, scopeSpans: [{ scope: { name: "hwlab.opencode.provider_proxy", version: "1" }, spans: [{ traceId: span.traceId, spanId: span.spanId, parentSpanId: span.parentSpanId || undefined, name: span.name, kind: span.kind, startTimeUnixNano: unixNano(span.startTimeMs), endTimeUnixNano: unixNano(span.endTimeMs), attributes: otelAttributes({ ...span.attributes, "otel.trace_id": span.traceId }), status: span.errorCode || (span.statusCode && span.statusCode >= 500) ? { code: 2, message: span.errorCode || `http_${span.statusCode}` } : { code: 1 } }] }] }] };
}

function otelAttributes(values) {
  const result = [];
  for (const [key, value] of Object.entries(values || {})) {
    if (value === undefined || value === null || value === "") continue;
    if (typeof value === "number" && Number.isFinite(value)) result.push({ key, value: Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value } });
    else if (typeof value === "boolean") result.push({ key, value: { boolValue: value } });
    else result.push({ key, value: { stringValue: String(value) } });
  }
  return result;
}

function unixNano(ms) {
  return String(BigInt(Math.max(0, Math.trunc(ms))) * 1000000n);
}

function traceContextFromHeaders(headers) {
  const parsed = parseTraceparent(headers.traceparent);
  return { traceId: parsed?.traceId || normalizedTraceId(headers["x-hwlab-otel-trace-id"]) || randomOtelTraceId(), parentSpanId: parsed?.spanId || "" };
}

function parseTraceparent(value) {
  const match = firstHeaderValue(value).trim().match(/^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/u);
  if (!match) return null;
  const traceId = normalizedTraceId(match[1]);
  const spanId = normalizedSpanId(match[2]);
  return traceId && spanId ? { traceId, spanId } : null;
}

function traceparent(traceId, spanId) {
  return `00-${traceId}-${spanId}-01`;
}

function randomOtelTraceId() {
  const value = randomBytes(16).toString("hex");
  return value === "00000000000000000000000000000000" ? "00000000000000000000000000000001" : value;
}

function randomOtelSpanId() {
  const value = randomBytes(8).toString("hex");
  return value === "0000000000000000" ? "0000000000000001" : value;
}

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

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

function firstHeaderValue(value) {
  if (Array.isArray(value)) return String(value[0] ?? "");
  return String(value ?? "");
}

function otelTracesEndpoint(env) {
  const explicit = String(env.HWLAB_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || "").trim();
  if (explicit) return explicit;
  const base = String(env.HWLAB_OTEL_EXPORTER_OTLP_ENDPOINT || env.OTEL_EXPORTER_OTLP_ENDPOINT || "").trim();
  return base ? `${base.replace(/\/+$/u, "")}/v1/traces` : "";
}

function normalizeBasePath(value) {
  const text = String(value || "/").trim();
  const normalized = `/${text.replace(/^\/+|\/+$/gu, "")}`;
  return normalized === "/" ? "/" : normalized;
}

function parsePositiveInteger(value, fallback) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function requiredEnv(name) {
  const value = String(process.env[name] || "").trim();
  if (!value) throw new Error(`${name} is required`);
  return value;
}
