// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0.
// Responsibility: AgentRun manager HTTP client, internal service URL authority, and upstream timing metrics.

import { backendPerformanceRouteTemplate, currentBackendPerformanceContext } from "./backend-performance.ts";
import { truthyFlag } from "./server-http-utils.ts";
import { isIP } from "node:net";

export const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";

const AGENTRUN_MANAGER_HOST_PATTERN = /^agentrun-mgr\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\.svc\.cluster\.local$/u;

export async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, timeoutMs = 20_000, env = process.env } = {}) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  const performanceContext = currentBackendPerformanceContext();
  const startedAt = nowMs();
  let statusCode = 0;
  let outcome = "ok";
  try {
    const headers: Record<string, string> = {};
    const apiKey = resolveAgentRunApiKey(env);
    if (body !== undefined) headers["content-type"] = "application/json";
    if (apiKey) headers.authorization = `Bearer ${apiKey}`;
    const response = await fetchImpl(`${managerUrl}${path}`, {
      method,
      headers: Object.keys(headers).length === 0 ? undefined : headers,
      body: body === undefined ? undefined : JSON.stringify(body),
      signal: controller.signal
    });
    statusCode = Number(response.status ?? 0);
    const text = await response.text();
    const parsed = text ? JSON.parse(text) : {};
    if (!response.ok || parsed?.ok === false) {
      outcome = "error";
      throw Object.assign(new Error(parsed?.message ?? `AgentRun ${method} ${path} failed with HTTP ${response.status}`), {
        code: parsed?.failureKind ?? "agentrun_request_failed",
        statusCode: response.status,
        agentRunError: parsed
      });
    }
    return parsed?.ok === true && Object.hasOwn(parsed, "data") ? parsed.data : parsed;
  } catch (error) {
    if (error?.name === "AbortError") {
      outcome = "timeout";
      statusCode = 504;
      throw Object.assign(new Error(`AgentRun ${method} ${path} timed out after ${timeoutMs}ms`), agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code: "agentrun_timeout", statusCode: 504, category: "upstream-timeout" }));
    }
    if (outcome === "ok") outcome = "error";
    throw error;
  } finally {
    clearTimeout(timeout);
    performanceContext?.recordUpstream?.({
      component: "agentrun",
      operation: agentRunOperationName(method, path),
      route: backendPerformanceRouteTemplate(path),
      method,
      statusClass: statusClass(statusCode),
      outcome,
      durationMs: nowMs() - startedAt
    });
  }
}

export function resolveAgentRunManagerUrl(env = process.env, override = null) {
  const raw = firstNonEmpty(override, env.AGENTRUN_MGR_URL, env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL, DEFAULT_AGENTRUN_MGR_URL);
  const url = new URL(raw);
  const host = url.hostname.toLowerCase();
  const allowedByTest = truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL) && ["127.0.0.1", "localhost"].includes(host);
  const allowedByNative = env.WORKBENCH_MODE === "agentrun-native" && truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_PRIVATE_URL) && isPrivateIp(host);
  if (url.protocol !== "http:" || (!isAgentRunManagerInternalServiceHost(host) && !allowedByTest && !allowedByNative)) {
    throw Object.assign(new Error(`AGENTRUN_MGR_URL must use internal k3s Service DNS agentrun-mgr.<namespace>.svc.cluster.local; got ${redactUrl(raw)}`), {
      code: "agentrun_internal_url_required",
      statusCode: 500
    });
  }
  url.pathname = url.pathname.replace(/\/+$/u, "");
  url.search = "";
  url.hash = "";
  return url.toString().replace(/\/+$/u, "");
}

function isPrivateIp(host: string) {
  if (isIP(host) !== 4) return false;
  const [a, b] = host.split(".").map(Number);
  return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
}

export function isAgentRunManagerInternalServiceHost(host) {
  return AGENTRUN_MANAGER_HOST_PATTERN.test(String(host ?? "").trim().toLowerCase());
}

function resolveAgentRunApiKey(env = process.env) {
  return firstNonEmpty(env.AGENTRUN_API_KEY, env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY, env.HWLAB_API_KEY);
}

function agentRunOperationName(method, path) {
  const route = backendPerformanceRouteTemplate(path).replace(/^\/api\/v1\//u, "").replace(/\/:id/gu, "");
  return `${String(method ?? "GET").toLowerCase()}_${route || "root"}`.replace(/[^a-z0-9]+/giu, "_").replace(/^_+|_+$/gu, "").toLowerCase();
}

function statusClass(value) {
  const status = Number(value);
  if (!Number.isFinite(status) || status <= 0) return "unknown";
  return `${Math.floor(status / 100)}xx`;
}

function nowMs() {
  if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
  return Date.now();
}

function agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code, statusCode, category }) {
  return {
    code,
    statusCode,
    layer: "agentrun",
    category,
    retryable: true,
    method,
    route: path,
    path,
    timeoutMs,
    timeoutStage: "agentrun-http",
    managerHost: safeUrlHost(managerUrl),
    valuesPrinted: false
  };
}

function safeUrlHost(value) {
  try { return new URL(value).host; } catch { return null; }
}

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

function redactUrl(value) {
  try {
    const url = new URL(value);
    if (url.username) url.username = "***";
    if (url.password) url.password = "***";
    return url.toString();
  } catch {
    return String(value ?? "").replace(/([?&](?:token|key|secret|password)=)[^&]+/giu, "$1***");
  }
}
