import { type UniDeskConfig } from "./config";

export interface NetworkPerfOptions {
  baseUrl: string;
  username: string;
  password: string;
  serviceId: string;
  path: string;
  count: number;
  concurrency: number;
  timeoutMs: number;
  cacheBust: boolean;
  label: string;
}

interface NetworkPerfSample {
  index: number;
  ok: boolean;
  status: number;
  durationMs: number;
  cache: string;
  proxyMode: string;
  upstreamProxyMode: string;
  responseTruncated: string;
  bytes: number;
  error: string | null;
}

function argValue(args: string[], name: string): string | null {
  const index = args.indexOf(name);
  if (index === -1) return null;
  const value = args[index + 1];
  if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function numberArg(args: string[], name: string, fallback: number): number {
  const raw = argValue(args, name);
  if (raw === null) return fallback;
  const value = Number(raw);
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
  return value;
}

function normalizeBaseUrl(value: string): string {
  return value.replace(/\/+$/u, "");
}

function frontendBaseUrl(host: string, config: UniDeskConfig): string {
  if (host.startsWith("http://") || host.startsWith("https://")) return normalizeBaseUrl(host);
  if (/:\d+$/u.test(host)) return `http://${host}`;
  return `http://${host}:${config.network.frontend.port}`;
}

export function parseNetworkPerfOptions(config: UniDeskConfig, args: string[], fallbackHost?: string): NetworkPerfOptions {
  const baseUrl = argValue(args, "--url") ?? (fallbackHost === undefined
    ? `http://${config.network.publicHost}:${config.network.frontend.port}`
    : frontendBaseUrl(fallbackHost, config));
  return {
    baseUrl: normalizeBaseUrl(baseUrl),
    username: argValue(args, "--username") ?? config.auth.username,
    password: argValue(args, "--password") ?? config.auth.password,
    serviceId: argValue(args, "--service") ?? argValue(args, "--service-id") ?? "code-queue",
    path: argValue(args, "--path") ?? "/api/tasks/overview?limit=30",
    count: numberArg(args, "--count", 20),
    concurrency: numberArg(args, "--concurrency", 1),
    timeoutMs: numberArg(args, "--timeout-ms", 45_000),
    cacheBust: !args.includes("--no-cache-bust"),
    label: argValue(args, "--label") ?? "network-perf",
  };
}

async function login(options: NetworkPerfOptions): Promise<string> {
  const response = await fetch(`${options.baseUrl}/login`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ username: options.username, password: options.password }),
    signal: AbortSignal.timeout(options.timeoutMs),
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`login failed: HTTP ${response.status} ${text.slice(0, 300)}`);
  const cookie = response.headers.get("set-cookie")?.split(";", 1)[0] ?? "";
  if (cookie.length === 0) throw new Error("login response did not set a session cookie");
  return cookie;
}

function sampleUrl(options: NetworkPerfOptions, index: number): string {
  const [pathOnly = "/", query = ""] = options.path.split("?", 2);
  const params = new URLSearchParams(query);
  if (options.cacheBust) params.set("__networkPerf", `${Date.now()}-${index}`);
  const search = params.toString();
  return `${options.baseUrl}/api/microservices/${encodeURIComponent(options.serviceId)}/proxy${pathOnly}${search.length > 0 ? `?${search}` : ""}`;
}

async function runOne(options: NetworkPerfOptions, cookie: string, index: number): Promise<NetworkPerfSample> {
  const started = performance.now();
  try {
    const response = await fetch(sampleUrl(options, index), {
      headers: {
        accept: "application/json",
        cookie,
      },
      signal: AbortSignal.timeout(options.timeoutMs),
    });
    const bodyText = await response.text();
    return {
      index,
      ok: response.ok,
      status: response.status,
      durationMs: Math.round((performance.now() - started) * 10) / 10,
      cache: response.headers.get("x-unidesk-cache") ?? "",
      proxyMode: response.headers.get("x-unidesk-proxy-mode") ?? "",
      upstreamProxyMode: response.headers.get("x-unidesk-upstream-proxy-mode") ?? "",
      responseTruncated: response.headers.get("x-unidesk-response-truncated") ?? "",
      bytes: bodyText.length,
      error: null,
    };
  } catch (error) {
    return {
      index,
      ok: false,
      status: 0,
      durationMs: Math.round((performance.now() - started) * 10) / 10,
      cache: "",
      proxyMode: "",
      upstreamProxyMode: "",
      responseTruncated: "",
      bytes: 0,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

function percentile(values: number[], percentileValue: number): number {
  if (values.length === 0) return 0;
  const sorted = values.slice().sort((left, right) => left - right);
  if (percentileValue <= 0) return sorted[0] ?? 0;
  if (percentileValue >= 100) return sorted[sorted.length - 1] ?? 0;
  const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((percentileValue / 100) * sorted.length) - 1));
  return sorted[index] ?? 0;
}

function countBy(samples: NetworkPerfSample[], key: keyof NetworkPerfSample): Record<string, number> {
  const counts: Record<string, number> = {};
  for (const sample of samples) {
    const value = String(sample[key] ?? "");
    counts[value] = (counts[value] ?? 0) + 1;
  }
  return counts;
}

export async function runNetworkPerf(options: NetworkPerfOptions): Promise<Record<string, unknown>> {
  const cookie = await login(options);
  const samples: NetworkPerfSample[] = [];
  let nextIndex = 0;
  const workers = Array.from({ length: Math.min(options.concurrency, options.count) }, async () => {
    while (nextIndex < options.count) {
      const index = nextIndex;
      nextIndex += 1;
      samples.push(await runOne(options, cookie, index));
    }
  });
  const startedAt = Date.now();
  await Promise.all(workers);
  samples.sort((left, right) => left.index - right.index);
  const durations = samples.map((sample) => sample.durationMs);
  const successfulDurations = samples.filter((sample) => sample.ok).map((sample) => sample.durationMs);
  return {
    ok: samples.every((sample) => sample.ok),
    label: options.label,
    measuredAt: new Date(startedAt).toISOString(),
    baseUrl: options.baseUrl,
    serviceId: options.serviceId,
    path: options.path,
    count: options.count,
    concurrency: options.concurrency,
    timeoutMs: options.timeoutMs,
    cacheBust: options.cacheBust,
    successCount: samples.filter((sample) => sample.ok).length,
    failureCount: samples.filter((sample) => !sample.ok).length,
    statusCounts: countBy(samples, "status"),
    cacheCounts: countBy(samples, "cache"),
    proxyModeCounts: countBy(samples, "proxyMode"),
    upstreamProxyModeCounts: countBy(samples, "upstreamProxyMode"),
    durationMs: {
      min: percentile(durations, 0),
      p50: percentile(durations, 50),
      p90: percentile(durations, 90),
      p95: percentile(durations, 95),
      max: percentile(durations, 100),
      successfulP50: percentile(successfulDurations, 50),
      successfulP95: percentile(successfulDurations, 95),
    },
    samples,
  };
}
