import http from "k6/http";
import { defaultOptions } from "./checks.js";
import { emitArtifact } from "./artifacts.js";

export { defaultOptions };

const REDACTED_QUERY_PARAMS = new Set(["token", "organizationId"]);
const REDACTED_HEADERS = new Set(["authorization", "cookie", "x-admin-key"]);
const TRACE_PREVIEW_LIMIT = 320;
const traceState = createTraceState();

export function getEnv() {
  const BASE = __ENV.BASE_URL;
  const MACHINE_ID = __ENV.MACHINE_ID;

  if (!BASE) {
    throw new Error("BASE_URL env var required");
  }

  const routeParams = MACHINE_ID ? { "fly-force-instance-id": MACHINE_ID } : {};
  return { BASE, MACHINE_ID, routeParams };
}

export function createHttpClient(config) {
  const {
    baseUrl,
    routeHeaders = {},
    defaultHeaders = { "Content-Type": "application/json" },
    getHeaders = null,
    getRawHeaders = null,
  } = config;

  if (!baseUrl) {
    throw new Error("baseUrl is required");
  }

  function buildHeaders(builder, setupData, extraHeaders = {}) {
    return {
      ...defaultHeaders,
      ...safeHeaders(builder, setupData),
      ...routeHeaders,
      ...extraHeaders,
    };
  }

  function request(method, path, setupData, body, extraHeaders = {}) {
    const url = `${baseUrl}${path}`;
    const headers = buildHeaders(getHeaders, setupData, extraHeaders);
    return runHttpRequest(method, path, url, body, headers);
  }

  function raw(method, path, body, extraHeaders = {}) {
    const url = `${baseUrl}${path}`;
    const headers = buildHeaders(getRawHeaders, null, extraHeaders);
    return runHttpRequest(method, path, url, body, headers);
  }

  function getWithHeaders(path, setupData, extraHeaders = {}) {
    return runHttpRequest(
      "GET",
      path,
      `${baseUrl}${path}`,
      null,
      buildHeaders(getHeaders, setupData, extraHeaders)
    );
  }

  return {
    rawHttp: http,
    request,
    raw,
    get(path, setupData, extraHeaders = {}) {
      return request("GET", path, setupData, null, extraHeaders);
    },
    put(path, setupData, body, extraHeaders = {}) {
      return request("PUT", path, setupData, body, extraHeaders);
    },
    post(path, setupData, body, extraHeaders = {}) {
      return request("POST", path, setupData, body, extraHeaders);
    },
    patch(path, setupData, body, extraHeaders = {}) {
      return request("PATCH", path, setupData, body, extraHeaders);
    },
    delete(path, setupData, extraHeaders = {}) {
      return request("DELETE", path, setupData, null, extraHeaders);
    },
    rawGet(path, extraHeaders = {}) {
      return raw("GET", path, null, extraHeaders);
    },
    rawPost(path, body, extraHeaders = {}) {
      return raw("POST", path, body, extraHeaders);
    },
    rawPut(path, body, extraHeaders = {}) {
      return raw("PUT", path, body, extraHeaders);
    },
    rawPatch(path, body, extraHeaders = {}) {
      return raw("PATCH", path, body, extraHeaders);
    },
    rawDelete(path, extraHeaders = {}) {
      return raw("DELETE", path, null, extraHeaders);
    },
    getWithHeaders,
  };
}

export function makeReq(baseUrl, routeHeaders = {}, getHeaders = null) {
  return createHttpClient({
    baseUrl,
    routeHeaders,
    getHeaders,
  }).request;
}

export function makeRawReq(baseUrl, routeHeaders = {}, getRawHeaders = null) {
  return createHttpClient({
    baseUrl,
    routeHeaders,
    getRawHeaders,
  }).raw;
}

export function makeGetWithHeaders(baseUrl, routeHeaders = {}, getHeaders = null) {
  return createHttpClient({
    baseUrl,
    routeHeaders,
    getHeaders,
  }).getWithHeaders;
}

function runHttpRequest(method, path, url, body, headers) {
  const ordinal = nextTraceOrdinal();
  const requestId = buildRequestId(ordinal);
  const finalHeaders = {
    ...headers,
    "x-request-id": headers?.["x-request-id"] || headers?.["X-Request-Id"] || requestId,
  };
  const trace = createTrace({
    ordinal,
    requestId: finalHeaders["x-request-id"],
    method,
    path,
    url,
    requestHeaders: finalHeaders,
  });
  const options = { headers: finalHeaders };

  let rawResponse;
  if (method === "GET") rawResponse = http.get(url, options);
  else if (method === "PUT") rawResponse = http.put(url, JSON.stringify(body), options);
  else if (method === "POST") rawResponse = http.post(url, JSON.stringify(body), options);
  else if (method === "PATCH") rawResponse = http.patch(url, JSON.stringify(body), options);
  else if (method === "DELETE") rawResponse = http.del(url, null, options);
  else throw new Error(`unsupported method: ${method}`);

  finalizeTrace(trace, rawResponse);
  traceState.traces.push(trace);
  trimTraceBuffer();
  return createWrappedResponse(rawResponse, trace);
}

export function startHttpTraceCollection(phase) {
  traceState.phase = normalizeLabel(phase, "exec");
  traceState.traces = [];
  traceState.counter = 0;
}

export function emitHttpTraceCollectionArtifact() {
  const traces = traceState.traces.map((trace) => ({ ...trace }));
  if (traces.length > 0) {
    emitArtifact(
      "http-traces",
      {
        phase: traceState.phase,
        traces,
      },
      {
        kind: "testkit.http-traces",
        summary: `${traces.length} HTTP trace(s)`,
      }
    );
  }
  startHttpTraceCollection(traceState.phase);
}

export function getHttpTrace(response) {
  return response?.__testkit?.httpTrace || null;
}

export function summarizeHttpTrace(response) {
  const trace = getHttpTrace(response);
  if (!trace) return null;
  return {
    id: trace.id,
    requestId: trace.requestId,
    method: trace.method,
    path: trace.path,
    url: trace.url,
    requestHeaders: trace.requestHeaders,
    response: trace.response,
    startedAt: trace.startedAt,
    finishedAt: trace.finishedAt,
    durationMs: trace.durationMs,
  };
}

export function safeJson(response) {
  try {
    return {
      ok: true,
      value: JSON.parse(response?.body || ""),
    };
  } catch (error) {
    return {
      ok: false,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

export function toBodyPreview(response) {
  const contentType = toHeaderText(
    response?.headers?.["Content-Type"] || response?.headers?.["content-type"] || null
  );
  const rawBody = typeof response?.body === "string" ? response.body : "";
  if (!rawBody.trim()) return null;

  if (contentType?.includes("application/json")) {
    try {
      return truncate(JSON.stringify(JSON.parse(rawBody)), TRACE_PREVIEW_LIMIT);
    } catch {
      return truncate(rawBody, TRACE_PREVIEW_LIMIT);
    }
  }

  return truncate(rawBody, TRACE_PREVIEW_LIMIT);
}

function createTrace({ ordinal, requestId, method, path, url, requestHeaders }) {
  return {
    id: `${traceState.phase}-${String(ordinal).padStart(3, "0")}`,
    requestId,
    startedAt: new Date().toISOString(),
    method,
    path: sanitizePath(path),
    url: sanitizeUrl(url),
    requestHeaders: sanitizeHeaders(requestHeaders),
  };
}

function finalizeTrace(trace, response) {
  const finishedAt = new Date();
  trace.finishedAt = finishedAt.toISOString();
  trace.durationMs = Math.max(0, finishedAt.getTime() - Date.parse(trace.startedAt));
  trace.response = {
    status: Number(response?.status ?? 0) || null,
    contentType: toHeaderText(
      response?.headers?.["Content-Type"] || response?.headers?.["content-type"] || null
    ),
    bodyPreview: toBodyPreview(response),
    bodyTruncated: typeof response?.body === "string" && response.body.length > TRACE_PREVIEW_LIMIT,
  };
}

function trimTraceBuffer() {
  if (traceState.traces.length <= traceState.maxEntries) return;
  traceState.traces.splice(0, traceState.traces.length - traceState.maxEntries);
}

function createTraceState() {
  return {
    phase: "exec",
    traces: [],
    counter: 0,
    maxEntries: 50,
  };
}

function sanitizePath(rawPath) {
  if (typeof rawPath !== "string") return rawPath;
  const [pathname, query] = rawPath.split("?");
  if (!query) return pathname;
  const parts = query.split("&").map((entry) => {
    const [rawKey, ...rawValueParts] = entry.split("=");
    const key = decodeQueryComponent(rawKey);
    if (!REDACTED_QUERY_PARAMS.has(key)) return entry;
    const encodedKey = rawKey || encodeURIComponent(key);
    return `${encodedKey}=${encodeURIComponent("[REDACTED]")}`;
  });
  return `${pathname}?${parts.join("&")}`;
}

function sanitizeUrl(rawUrl) {
  if (typeof rawUrl !== "string") return rawUrl;
  const protocolIndex = rawUrl.indexOf("://");
  if (protocolIndex === -1) return sanitizePath(rawUrl);
  const pathIndex = rawUrl.indexOf("/", protocolIndex + 3);
  if (pathIndex === -1) return rawUrl;
  return `${rawUrl.slice(0, pathIndex)}${sanitizePath(rawUrl.slice(pathIndex))}`;
}

function sanitizeHeaders(headers) {
  if (!headers || typeof headers !== "object") return {};
  const output = {};
  for (const [name, value] of Object.entries(headers)) {
    const normalizedName = String(name);
    const lower = normalizedName.toLowerCase();
    output[normalizedName] = REDACTED_HEADERS.has(lower) ? "[REDACTED]" : normalizeHeaderValue(value);
  }
  return output;
}

function normalizeHeaderValue(value) {
  if (Array.isArray(value)) return value.map((entry) => String(entry));
  if (value == null) return null;
  return String(value);
}

function toHeaderText(value) {
  const normalized = normalizeHeaderValue(value);
  return Array.isArray(normalized) ? normalized[0] || null : normalized;
}

function buildRequestId(ordinal) {
  const runtimeId = normalizeLabel(__ENV.TESTKIT_RUNTIME_ID, "runtime");
  return `tk_${runtimeId}_${traceState.phase}_${String(ordinal).padStart(4, "0")}`;
}

function nextTraceOrdinal() {
  traceState.counter += 1;
  return traceState.counter;
}

function truncate(value, limit) {
  const normalized = String(value || "").replace(/\s+/g, " ").trim();
  if (normalized.length <= limit) return normalized;
  return `${normalized.slice(0, Math.max(0, limit - 3))}...`;
}

function normalizeLabel(value, fallback) {
  if (typeof value !== "string") return fallback;
  const normalized = value.trim();
  return normalized.length > 0 ? normalized : fallback;
}

function safeHeaders(builder, setupData) {
  if (typeof builder !== "function") return {};
  return builder(setupData) || {};
}

function createWrappedResponse(rawResponse, trace) {
  if (!rawResponse || typeof rawResponse !== "object") {
    return rawResponse;
  }

  const wrapped = {};
  for (const key of Object.keys(rawResponse)) {
    wrapped[key] = rawResponse[key];
  }

  const prototype = Object.getPrototypeOf(rawResponse);
  if (prototype && typeof prototype === "object") {
    for (const name of Object.getOwnPropertyNames(prototype)) {
      if (name === "constructor" || name in wrapped) continue;
      const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
      if (!descriptor || typeof descriptor.value !== "function") continue;
      wrapped[name] = descriptor.value.bind(rawResponse);
    }
  }

  wrapped.__testkit = {
    httpTrace: trace,
    rawResponse,
  };
  return wrapped;
}

function decodeQueryComponent(value) {
  try {
    return decodeURIComponent(String(value || ""));
  } catch {
    return String(value || "");
  }
}
