const DEFAULT_WEB_PERFORMANCE_BODY_LIMIT_BYTES = 64 * 1024;
const DURATION_BUCKETS_SECONDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30];
const CLS_BUCKETS = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5];
const ALLOWED_KINDS = new Set(["navigation", "web_vital", "api", "long_task"]);
const DURATION_METRICS = new Set([
  "navigation_ttfb",
  "navigation_dom_content_loaded",
  "navigation_load",
  "lcp",
  "inp",
  "fid",
  "api_request",
  "long_task"
]);

interface WebPerformanceStoreOptions {
  env?: Record<string, unknown>;
  maxSeries?: number;
}

interface WebPerformancePayload {
  schemaVersion?: unknown;
  serviceId?: unknown;
  page?: unknown;
  events?: unknown;
}

interface WebPerformanceEvent {
  kind?: unknown;
  metric?: unknown;
  route?: unknown;
  valueMs?: unknown;
  value?: unknown;
  method?: unknown;
  status?: unknown;
  statusClass?: unknown;
  outcome?: unknown;
}

interface HistogramSeries {
  labels: Record<string, string>;
  buckets: number[];
  sum: number;
  count: number;
}

interface CounterSeries {
  labels: Record<string, string>;
  value: number;
}

interface PerformanceSummaryRow {
  kind: string;
  metric: string;
  route: string;
  method: string;
  statusClass: string;
  outcome: string;
  count: number;
  average: number;
  p50: number;
  p95: number;
  unit: "seconds" | "score";
  tone: "ok" | "warn" | "blocked" | "source";
  problem: string;
}

export function createWebPerformanceStore(options: WebPerformanceStoreOptions = {}) {
  const env = options.env ?? process.env;
  const maxSeries = parsePositiveInteger(env.HWLAB_WEB_PERFORMANCE_MAX_SERIES, options.maxSeries ?? 240);
  const durationSeries = new Map<string, HistogramSeries>();
  const clsSeries = new Map<string, HistogramSeries>();
  const sampleSeries = new Map<string, CounterSeries>();
  const baseLabels = {
    service: "hwlab-cloud-web",
    namespace: sanitizeLabelValue(env.HWLAB_METRICS_NAMESPACE ?? env.POD_NAMESPACE ?? "hwlab-v02"),
    gitops_target: sanitizeLabelValue(env.HWLAB_GITOPS_TARGET ?? env.HWLAB_GITOPS_PROFILE ?? "v02")
  };

  function record(payload: WebPerformancePayload) {
    const events = Array.isArray(payload.events) ? payload.events.slice(0, 80) : [];
    let accepted = 0;
    let dropped = 0;
    for (const rawEvent of events) {
      const event = normalizeEvent(rawEvent, payload);
      if (!event) {
        dropped += 1;
        continue;
      }
      const counterLabels = sampleLabels(event.labels);
      incrementCounter(sampleSeries, counterLabels, maxSeries);
      if (event.metric === "cls") {
        recordHistogram(clsSeries, event.labels, event.value, CLS_BUCKETS, maxSeries);
      } else {
        recordHistogram(durationSeries, event.labels, event.value, DURATION_BUCKETS_SECONDS, maxSeries);
      }
      accepted += 1;
    }
    return { accepted, dropped, received: events.length };
  }

  function normalizeEvent(rawEvent: unknown, payload: WebPerformancePayload) {
    if (!rawEvent || typeof rawEvent !== "object" || Array.isArray(rawEvent)) return null;
    const input = rawEvent as WebPerformanceEvent;
    const kind = sanitizeMetricName(input.kind, "unknown");
    if (!ALLOWED_KINDS.has(kind)) return null;
    const metric = sanitizeMetricName(input.metric, "unknown");
    const isCls = metric === "cls";
    if (!isCls && !DURATION_METRICS.has(metric)) return null;
    const rawValue = isCls ? finiteNumber(input.value, NaN) : finiteNumber(input.valueMs, NaN) / 1000;
    if (!Number.isFinite(rawValue) || rawValue < 0) return null;
    const value = isCls ? Math.min(rawValue, 10) : Math.min(rawValue, 120);
    const route = webPerformanceRouteTemplate(input.route ?? payload.page ?? "/workspace");
    const pageRoute = webPerformanceRouteTemplate(payload.page ?? "/workspace");
    if (isObservabilityNoise({ kind, metric, route, pageRoute })) return null;
    const labels = {
      ...baseLabels,
      kind,
      metric,
      route,
      method: normalizeMethod(input.method),
      status_class: normalizeStatusClass(input.statusClass ?? (input.status === undefined || input.status === null ? "unknown" : statusClass(input.status))),
      outcome: sanitizeLabelValue(input.outcome ?? "ok", "ok")
    };
    return { metric, value, labels };
  }

  function metricsText() {
    const lines = [
      "# HELP hwlab_webui_performance_sample_total Browser-observed HWLAB Cloud Web performance samples accepted by cloud-api.",
      "# TYPE hwlab_webui_performance_sample_total counter",
      ...renderCounters("hwlab_webui_performance_sample_total", sampleSeries),
      "# HELP hwlab_webui_performance_duration_seconds Browser-observed HWLAB Cloud Web duration metrics in seconds.",
      "# TYPE hwlab_webui_performance_duration_seconds histogram",
      ...renderHistogram("hwlab_webui_performance_duration_seconds", durationSeries, DURATION_BUCKETS_SECONDS),
      "# HELP hwlab_webui_layout_shift_score Browser-observed cumulative layout shift score.",
      "# TYPE hwlab_webui_layout_shift_score histogram",
      ...renderHistogram("hwlab_webui_layout_shift_score", clsSeries, CLS_BUCKETS),
      ""
    ];
    return lines.join("\n");
  }

  function summary() {
    const durationRows = histogramSummaryRows(durationSeries, DURATION_BUCKETS_SECONDS, "seconds");
    const clsRows = histogramSummaryRows(clsSeries, CLS_BUCKETS, "score");
    const rows = [...durationRows, ...clsRows];
    const apiRoutes = durationRows
      .filter((row) => row.metric === "api_request")
      .sort(sortByProblemThenP95)
      .slice(0, 12);
    const webVitals = rows
      .filter((row) => row.kind === "web_vital" || row.metric.startsWith("navigation_"))
      .sort(sortByProblemThenP95)
      .slice(0, 12);
    const longTasks = durationRows
      .filter((row) => row.metric === "long_task")
      .sort(sortByProblemThenP95)
      .slice(0, 12);
    const problems = rows
      .filter((row) => row.tone === "blocked" || row.tone === "warn" || row.outcome !== "ok")
      .sort(sortByProblemThenP95)
      .slice(0, 16);
    const sampleCount = [...sampleSeries.values()].reduce((sum, entry) => sum + entry.value, 0);
    return {
      serviceId: "hwlab-cloud-api",
      route: "/v1/web-performance/summary",
      source: "cloud-api-rum-store",
      observedAt: new Date().toISOString(),
      namespace: baseLabels.namespace,
      gitopsTarget: baseLabels.gitops_target,
      summary: {
        sampleCount,
        sampleSeries: sampleSeries.size,
        durationSeries: durationSeries.size,
        clsSeries: clsSeries.size,
        routeCount: new Set(rows.map((row) => row.route)).size,
        problemCount: problems.length,
        status: sampleCount === 0 ? "waiting" : sampleCount < 10 ? "warming" : problems.length > 0 ? "watch" : "ok"
      },
      apiRoutes,
      webVitals,
      longTasks,
      problems,
      rows: rows.sort(sortByProblemThenP95).slice(0, 40)
    };
  }

  function snapshot() {
    return {
      sampleSeries: sampleSeries.size,
      durationSeries: durationSeries.size,
      clsSeries: clsSeries.size,
      sampleCount: [...sampleSeries.values()].reduce((sum, entry) => sum + entry.value, 0)
    };
  }

  return { record, metricsText, snapshot, summary };
}

export async function handleWebPerformanceIngestHttp(request, response, options = {}) {
  const body = await readBody(request, options.bodyLimitBytes ?? DEFAULT_WEB_PERFORMANCE_BODY_LIMIT_BYTES);
  let payload;
  try {
    payload = body ? JSON.parse(body) : {};
  } catch (error) {
    sendJson(response, 400, { accepted: false, error: { code: "parse_error", message: "Invalid web performance JSON body", reason: error.message } });
    return;
  }
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    sendJson(response, 400, { accepted: false, error: { code: "invalid_params", message: "web performance body must be a JSON object" } });
    return;
  }
  const result = options.store.record(payload);
  sendJson(response, 202, { accepted: true, ...result });
}

export function handleWebPerformanceMetricsHttp(request, response, options = {}) {
  if (!isLoopbackMetricsRequest(request)) {
    sendJson(response, 404, { error: { code: "not_found", message: "route is not public" } });
    return;
  }
  const payload = options.store.metricsText();
  response.writeHead(200, {
    "content-type": "text/plain; version=0.0.4; charset=utf-8",
    "content-length": Buffer.byteLength(payload)
  });
  response.end(payload);
}

export function handleWebPerformanceSummaryHttp(_request, response, options = {}) {
  sendJson(response, 200, options.store.summary());
}

export function webPerformanceRouteTemplate(input: unknown): string {
  const raw = String(input ?? "").trim() || "/";
  const normalizedRaw = raw.startsWith("#") ? raw.slice(1) || "/" : raw;
  const pathOnly = normalizedRaw.split(/[?#]/u, 1)[0] || "/";
  let pathname = pathOnly;
  try {
    pathname = new URL(normalizedRaw, "http://hwlab.local").pathname || pathOnly;
  } catch {
    // Keep pathOnly.
  }
  const parts = pathname.split("/").filter(Boolean).map((part) => routeSegmentTemplate(safeDecode(part)));
  const path = `/${parts.join("/")}`;
  return path.length > 120 ? `${path.slice(0, 117)}...` : path;
}

function sampleLabels(labels: Record<string, string>) {
  return {
    service: labels.service,
    namespace: labels.namespace,
    gitops_target: labels.gitops_target,
    kind: labels.kind,
    metric: labels.metric,
    route: labels.route,
    method: labels.method,
    status_class: labels.status_class,
    outcome: labels.outcome
  };
}

function isObservabilityNoise(event: { kind: string; metric: string; route: string; pageRoute: string }) {
  if (event.pageRoute === "/performance") return true;
  if (event.route === "/performance" && (event.kind === "navigation" || event.kind === "web_vital" || event.kind === "long_task")) return true;
  return event.route === "/v1/web-performance" || event.route.startsWith("/v1/web-performance/");
}

function recordHistogram(series: Map<string, HistogramSeries>, labels: Record<string, string>, value: number, buckets: number[], maxSeries: number) {
  const key = stableLabelKey(labels);
  let entry = series.get(key);
  if (!entry) {
    if (series.size >= maxSeries) return;
    entry = { labels, buckets: buckets.map(() => 0), sum: 0, count: 0 };
    series.set(key, entry);
  }
  for (let index = 0; index < buckets.length; index += 1) {
    if (value <= buckets[index]) entry.buckets[index] += 1;
  }
  entry.sum += value;
  entry.count += 1;
}

function incrementCounter(series: Map<string, CounterSeries>, labels: Record<string, string>, maxSeries: number) {
  const key = stableLabelKey(labels);
  let entry = series.get(key);
  if (!entry) {
    if (series.size >= maxSeries) return;
    entry = { labels, value: 0 };
    series.set(key, entry);
  }
  entry.value += 1;
}

function renderCounters(metricName: string, series: Map<string, CounterSeries>) {
  return [...series.values()].map((entry) => `${metricName}{${renderLabels(entry.labels)}} ${entry.value}`);
}

function renderHistogram(metricName: string, series: Map<string, HistogramSeries>, buckets: number[]) {
  const lines: string[] = [];
  for (const entry of series.values()) {
    for (let index = 0; index < buckets.length; index += 1) {
      lines.push(`${metricName}_bucket{${renderLabels({ ...entry.labels, le: String(buckets[index]) })}} ${entry.buckets[index]}`);
    }
    lines.push(`${metricName}_bucket{${renderLabels({ ...entry.labels, le: "+Inf" })}} ${entry.count}`);
    lines.push(`${metricName}_sum{${renderLabels(entry.labels)}} ${entry.sum.toFixed(6)}`);
    lines.push(`${metricName}_count{${renderLabels(entry.labels)}} ${entry.count}`);
  }
  return lines;
}

function histogramSummaryRows(series: Map<string, HistogramSeries>, buckets: number[], unit: "seconds" | "score"): PerformanceSummaryRow[] {
  return [...series.values()].map((entry) => {
    const labels = entry.labels;
    const count = entry.count;
    const average = count > 0 ? entry.sum / count : 0;
    const p50 = histogramQuantile(entry, buckets, 0.5);
    const p95 = histogramQuantile(entry, buckets, 0.95);
    const tone = performanceTone(labels, p95, count, unit);
    return {
      kind: labels.kind,
      metric: labels.metric,
      route: labels.route,
      method: labels.method,
      statusClass: labels.status_class,
      outcome: labels.outcome,
      count,
      average: roundMetric(average),
      p50: roundMetric(p50),
      p95: roundMetric(p95),
      unit,
      tone,
      problem: performanceProblem(labels, p95, count, unit, tone)
    };
  });
}

function histogramQuantile(entry: HistogramSeries, buckets: number[], quantile: number) {
  if (entry.count <= 0) return 0;
  const rank = Math.max(1, Math.ceil(entry.count * quantile));
  for (let index = 0; index < entry.buckets.length; index += 1) {
    if (entry.buckets[index] >= rank) return buckets[index];
  }
  return buckets[buckets.length - 1] ?? 0;
}

function performanceTone(labels: Record<string, string>, p95: number, count: number, unit: "seconds" | "score") {
  if (labels.outcome !== "ok" || labels.status_class === "network" || labels.status_class === "5xx") return "blocked";
  if (labels.status_class === "4xx") return "warn";
  if (count <= 0) return "source";
  const metric = labels.metric;
  if (unit === "score") {
    if (p95 >= 0.25) return "blocked";
    if (p95 >= 0.1) return "warn";
    return "ok";
  }
  if (metric === "lcp") return thresholdTone(p95, 2.5, 4);
  if (metric === "inp" || metric === "fid") return thresholdTone(p95, 0.2, 0.5);
  if (metric === "api_request") return thresholdTone(p95, 1, 2.5);
  if (metric === "long_task") return thresholdTone(p95, 0.2, 0.5);
  if (metric === "navigation_dom_content_loaded") return thresholdTone(p95, 2, 4);
  if (metric === "navigation_load") return thresholdTone(p95, 3, 6);
  if (metric === "navigation_ttfb") return thresholdTone(p95, 0.8, 1.8);
  return p95 > 1 ? "warn" : "ok";
}

function thresholdTone(value: number, warn: number, blocked: number) {
  if (value >= blocked) return "blocked";
  if (value >= warn) return "warn";
  return "ok";
}

function performanceProblem(labels: Record<string, string>, p95: number, count: number, unit: "seconds" | "score", tone: string) {
  if (count <= 0) return "no-sample";
  if (labels.outcome !== "ok") return labels.outcome;
  if (labels.status_class === "network") return "network";
  if (labels.status_class === "5xx") return "server-error";
  if (labels.status_class === "4xx") return "client-error";
  if (tone === "blocked" || tone === "warn") return unit === "score" ? "layout-shift" : `slow-${labels.metric}`;
  return "ok";
}

function sortByProblemThenP95(left: PerformanceSummaryRow, right: PerformanceSummaryRow) {
  return toneWeight(right.tone) - toneWeight(left.tone) || right.p95 - left.p95 || right.count - left.count;
}

function toneWeight(tone: string) {
  if (tone === "blocked") return 4;
  if (tone === "warn") return 3;
  if (tone === "ok") return 2;
  return 1;
}

function roundMetric(value: number) {
  if (!Number.isFinite(value)) return 0;
  return Math.round(value * 10000) / 10000;
}

function renderLabels(labels: Record<string, string>) {
  return Object.entries(labels)
    .map(([key, value]) => `${key}="${escapeLabelValue(value)}"`)
    .join(",");
}

function stableLabelKey(labels: Record<string, string>) {
  return Object.entries(labels).map(([key, value]) => `${key}=${value}`).join("\u0001");
}

function isLoopbackMetricsRequest(request) {
  const remoteAddress = String(request.socket?.remoteAddress ?? "");
  const host = String(request.headers?.host ?? "").trim().toLowerCase();
  const loopbackRemote = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
  const loopbackHost = /^(?:127\.0\.0\.1|localhost)(?::\d+)?$/u.test(host);
  return loopbackRemote && loopbackHost;
}

function routeSegmentTemplate(segment: string): string {
  if (/^(?:trc|trace|thread|run|cmd|job|ses|cnv|conv|gws|box|res|pod|dp|usr)[_-]/iu.test(segment)) return ":id";
  if (/^[0-9a-f]{8,}(?:-[0-9a-f]{4,}){2,}$/iu.test(segment)) return ":uuid";
  if (/^[A-Za-z0-9_-]{20,}$/u.test(segment)) return ":id";
  if (/^\d{5,}$/u.test(segment)) return ":id";
  return sanitizeLabelValue(segment, "_");
}

function sanitizeMetricName(value: unknown, fallback: string) {
  const text = String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9_:-]/gu, "_").slice(0, 64);
  return text || fallback;
}

function sanitizeLabelValue(value: unknown, fallback = "unknown") {
  const text = String(value ?? "").trim();
  const sanitized = text.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 120);
  return sanitized || fallback;
}

function escapeLabelValue(value: unknown) {
  return String(value ?? "").replace(/\\/gu, "\\\\").replace(/\n/gu, "\\n").replace(/"/gu, "\\\"");
}

function normalizeMethod(value: unknown) {
  const method = String(value ?? "NONE").trim().toUpperCase();
  return /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/u.test(method) ? method : "NONE";
}

function normalizeStatusClass(value: unknown) {
  const text = String(value ?? "unknown").trim().toLowerCase();
  if (/^[1-5]xx$/u.test(text)) return text;
  if (text === "network" || text === "unknown") return text;
  return "unknown";
}

function statusClass(value: unknown) {
  const status = finiteNumber(value, 0);
  if (status >= 100 && status < 600) return `${Math.floor(status / 100)}xx`;
  if (status === 0) return "network";
  return "unknown";
}

function finiteNumber(value: unknown, fallback: number) {
  const number = typeof value === "number" ? value : Number(value);
  return Number.isFinite(number) ? number : fallback;
}

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

function safeDecode(value: string) {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

function readBody(request, limitBytes) {
  const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_WEB_PERFORMANCE_BODY_LIMIT_BYTES;
  return new Promise((resolve, reject) => {
    let body = "";
    request.setEncoding("utf8");
    request.on("data", (chunk) => {
      body += chunk;
      if (Buffer.byteLength(body, "utf8") > limit) {
        request.destroy(new Error(`request body exceeds ${limit} bytes`));
      }
    });
    request.on("end", () => resolve(body));
    request.on("error", reject);
  });
}

function sendJson(response, statusCode, body) {
  response.writeHead(statusCode, {
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-store"
  });
  response.end(`${JSON.stringify(body)}\n`);
}
