"use client";

import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
  Activity,
  Download,
  Upload,
  Server,
  RefreshCw,
  Zap,
} from "lucide-react";
import { CyberButton } from "../ui/cyber-button";
import { cn } from "@/lib/utils";
import { Dictionary } from "@/lib/IDict";

type TestStatus =
  | "idle"
  | "pinging"
  | "downloading"
  | "uploading"
  | "done"
  | "error";

interface SpeedtestClientProps {
  dict: Dictionary["speedtest"];
}

// Мегабит везде считаем в десятичном виде (1 Мбит = 1_000_000 бит), как это
// делают провайдеры и остальные спидтесты (Ookla, fast.com) — иначе цифра на
// экране будет тише реальной на ~5% при том же подписанном "Мбит/с".
const MEGABIT = 1_000_000;
const bytesToMbps = (bytes: number, seconds: number) =>
  seconds > 0 ? (bytes * 8) / MEGABIT / seconds : 0;

const median = (values: number[]) => {
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 !== 0
    ? sorted[mid]
    : (sorted[mid - 1] + sorted[mid]) / 2;
};

/**
 * Прогревочный + N таймированных запросов на /ping. Первый запрос выкидываем
 * из выборки — на нём поднимается TCP/TLS-соединение, и его RTT в 3-5 раз
 * больше "боевого", что раздувает среднее. Из таймированных сэмплов берём
 * медиану, а не среднее — она устойчива к единичным всплескам (например,
 * фоновая задача ОС притормозила один из запросов).
 */
async function measurePing(
  baseUrl: string,
  signal: AbortSignal,
): Promise<number> {
  try {
    await fetch(`${baseUrl}/api/speedtest/ping?nc=${Date.now()}`, {
      cache: "no-store",
      signal,
    });
  } catch (e: any) {
    if (e?.name === "AbortError") throw e;
  }

  const SAMPLES = 8;
  const samples: number[] = [];
  for (let i = 0; i < SAMPLES; i++) {
    const start = performance.now();
    try {
      await fetch(`${baseUrl}/api/speedtest/ping?nc=${Date.now()}-${i}`, {
        cache: "no-store",
        signal,
      });
      samples.push(performance.now() - start);
    } catch (e: any) {
      if (e?.name === "AbortError") throw e;
    }
  }

  if (samples.length === 0) throw new Error("Ping failed");
  return Math.round(median(samples));
}

/**
 * Скачивание несколькими параллельными потоками в течение фиксированного
 * окна времени (а не фиксированного объёма!). Один TCP-поток не насыщает
 * канал на быстрых/дальних соединениях (окно перегрузки не успевает
 * разогнаться) — поэтому качаем сразу несколько потоков одновременно, и как
 * только один исчерпал свой блоб — тут же открываем новый, не давая "трубе"
 * простаивать. Первую секунду (WARMUP_MS) не учитываем в расчёте — это
 * TCP slow-start, а не установившаяся скорость канала.
 */
async function measureDownload(
  baseUrl: string,
  outerSignal: AbortSignal,
  onProgress: (mbps: number) => void,
): Promise<number> {
  const STREAMS = 4;
  const WARMUP_MS = 1000;
  const DURATION_MS = 8000;

  const controller = new AbortController();
  const onAbort = () => controller.abort();
  outerSignal.addEventListener("abort", onAbort);

  const testStart = performance.now();
  const deadline = testStart + WARMUP_MS + DURATION_MS;

  let totalBytes = 0;
  let bytesAtWarmupEnd: number | null = null;

  const runStream = async () => {
    while (performance.now() < deadline) {
      let res: Response;
      try {
        res = await fetch(
          `${baseUrl}/api/speedtest/download?nc=${Date.now()}-${Math.random()}`,
          { cache: "no-store", signal: controller.signal },
        );
      } catch (e: any) {
        if (e?.name === "AbortError") return;
        throw e;
      }
      const reader = res.body?.getReader();
      if (!reader) return;
      while (true) {
        const { done, value } = await reader.read();
        if (value) totalBytes += value.length;
        if (done) break;
        if (performance.now() >= deadline) {
          await reader.cancel().catch(() => {});
          return;
        }
      }
    }
  };

  const warmupTimer = setTimeout(() => {
    bytesAtWarmupEnd = totalBytes;
  }, WARMUP_MS);

  let lastBytes = 0;
  let lastTime = testStart;
  const progressTimer = setInterval(() => {
    if (bytesAtWarmupEnd === null) return;
    const now = performance.now();
    const dBytes = totalBytes - lastBytes;
    const dTime = (now - lastTime) / 1000;
    lastBytes = totalBytes;
    lastTime = now;
    onProgress(bytesToMbps(dBytes, dTime));
  }, 250);

  try {
    await Promise.all(Array.from({ length: STREAMS }, runStream));
  } finally {
    clearTimeout(warmupTimer);
    clearInterval(progressTimer);
    outerSignal.removeEventListener("abort", onAbort);
  }

  const measuredBytes = totalBytes - (bytesAtWarmupEnd ?? totalBytes);
  const measuredSeconds = (performance.now() - (testStart + WARMUP_MS)) / 1000;
  return bytesToMbps(measuredBytes, measuredSeconds);
}

const MAX_RANDOM_CHUNK = 65536; // лимит crypto.getRandomValues() за один вызов

function makeRandomBytes(size: number): Uint8Array<ArrayBuffer> {
  const buf = new Uint8Array(size);
  for (let offset = 0; offset < size; offset += MAX_RANDOM_CHUNK) {
    crypto.getRandomValues(
      buf.subarray(offset, Math.min(offset + MAX_RANDOM_CHUNK, size)),
    );
  }
  return buf;
}

/**
 * Отдача — тем же приёмом, что и скачивание: несколько параллельных потоков,
 * фиксированное окно времени, первые полсекунды не считаем. У fetch() нет
 * события прогресса отправки, поэтому вместо одного гигантского запроса шлём
 * последовательность отдельных чанков — так время оборота каждого запроса
 * меньше влияет на итоговую точность, и мы можем адаптировать размер чанка
 * под скорость канала (AIMD: чанк удался в срок — увеличиваем вдвое,
 * не уложились в таймаут — уменьшаем вдвое), вместо того чтобы гадать с
 * одним фиксированным размером на все каналы, от мобильного 3G до гигабита.
 * Каждый чанк — свежие случайные байты (а не повторяющийся блок), чтобы
 * сервер или прокси на пути не мог его сжать и исказить измерение.
 */
async function measureUpload(
  baseUrl: string,
  outerSignal: AbortSignal,
  onProgress: (mbps: number) => void,
): Promise<number> {
  const STREAMS = 3;
  const WARMUP_MS = 500;
  const DURATION_MS = 6000;
  const MIN_CHUNK = 256 * 1024;
  const MAX_CHUNK = 4 * 1024 * 1024;
  const REQUEST_TIMEOUT_MS = 4000;

  const controller = new AbortController();
  const onAbort = () => controller.abort();
  outerSignal.addEventListener("abort", onAbort);

  const testStart = performance.now();
  const deadline = testStart + WARMUP_MS + DURATION_MS;

  let totalBytes = 0;
  let bytesAtWarmupEnd: number | null = null;

  const runStream = async () => {
    let chunkSize = MIN_CHUNK;
    while (performance.now() < deadline) {
      const payload = makeRandomBytes(chunkSize);
      const reqController = new AbortController();
      const onOuterAbort = () => reqController.abort();
      controller.signal.addEventListener("abort", onOuterAbort);
      const timeoutId = setTimeout(
        () => reqController.abort(),
        REQUEST_TIMEOUT_MS,
      );

      try {
        await fetch(
          `${baseUrl}/api/speedtest/upload?nc=${Date.now()}-${Math.random()}`,
          {
            method: "POST",
            body: payload,
            signal: reqController.signal,
            headers: { "Content-Type": "application/octet-stream" },
          },
        );
        totalBytes += payload.length;
        chunkSize = Math.min(chunkSize * 2, MAX_CHUNK);
      } catch (e: any) {
        if (controller.signal.aborted) return;
        chunkSize = Math.max(Math.floor(chunkSize / 2), MIN_CHUNK);
      } finally {
        clearTimeout(timeoutId);
        controller.signal.removeEventListener("abort", onOuterAbort);
      }
    }
  };

  const warmupTimer = setTimeout(() => {
    bytesAtWarmupEnd = totalBytes;
  }, WARMUP_MS);

  let lastBytes = 0;
  let lastTime = testStart;
  const progressTimer = setInterval(() => {
    if (bytesAtWarmupEnd === null) return;
    const now = performance.now();
    const dBytes = totalBytes - lastBytes;
    const dTime = (now - lastTime) / 1000;
    lastBytes = totalBytes;
    lastTime = now;
    onProgress(bytesToMbps(dBytes, dTime));
  }, 250);

  try {
    await Promise.all(Array.from({ length: STREAMS }, runStream));
  } finally {
    clearTimeout(warmupTimer);
    clearInterval(progressTimer);
    outerSignal.removeEventListener("abort", onAbort);
  }

  const measuredBytes = totalBytes - (bytesAtWarmupEnd ?? totalBytes);
  const measuredSeconds = (performance.now() - (testStart + WARMUP_MS)) / 1000;
  return bytesToMbps(measuredBytes, measuredSeconds);
}

/**
 * Виджет спидтеста: пинг + скорость скачивания/отдачи через Rust-шлюз proxy-ws
 * (эндпоинты /api/speedtest/ping|download|upload, см. next.config.ts rewrites).
 *
 * В dev-режиме (NODE_ENV !== "production") реальные запросы не делаются — вместо
 * этого крутится runMockTest() с рандомными числами и искусственными задержками,
 * чтобы можно было верстать/проверять анимацию без поднятого локального proxy-ws.
 * В проде runRealTest() измеряет параллельными потоками в течение фиксированного
 * окна времени (см. measurePing/measureDownload/measureUpload выше) — так же,
 * как это делают Ookla/fast.com/Cloudflare speedtest, а не одним запросом в один
 * TCP-поток, который на быстрых каналах не успевает разогнаться и занижает результат.
 */
export function SpeedtestClient({ dict }: SpeedtestClientProps) {
  const [status, setStatus] = useState<TestStatus>("idle");
  const [metrics, setMetrics] = useState({
    ping: 0,
    download: 0.0,
    upload: 0.0,
  });

  const abortControllerRef = useRef<AbortController | null>(null);

  const getProxyUrl = () => {
    return process.env.NODE_ENV === "production"
      ? "/api/proxy"
      : "http://localhost:3001";
  };

  const runMockTest = async () => {
    const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

    setStatus("pinging");
    setMetrics({ ping: 0, download: 0, upload: 0 });
    await sleep(800);
    setMetrics((m) => ({ ...m, ping: Math.floor(Math.random() * 15) + 12 }));

    setStatus("downloading");
    for (let i = 1; i <= 20; i++) {
      await sleep(150);
      setMetrics((m) => ({
        ...m,
        download: +(15.5 * i + Math.random() * 10).toFixed(2),
      }));
    }

    setStatus("uploading");
    for (let i = 1; i <= 20; i++) {
      await sleep(150);
      setMetrics((m) => ({
        ...m,
        upload: +(8.2 * i + Math.random() * 5).toFixed(2),
      }));
    }

    setStatus("done");
  };

  const runRealTest = async () => {
    const baseUrl = getProxyUrl();
    setStatus("pinging");
    setMetrics({ ping: 0, download: 0, upload: 0 });

    const controller = new AbortController();
    abortControllerRef.current = controller;
    const signal = controller.signal;

    try {
      const ping = await measurePing(baseUrl, signal);
      setMetrics((m) => ({ ...m, ping }));

      setStatus("downloading");
      const download = await measureDownload(baseUrl, signal, (mbps) => {
        setMetrics((m) => ({ ...m, download: Number(mbps.toFixed(2)) }));
      });
      setMetrics((m) => ({ ...m, download: Number(download.toFixed(2)) }));

      setStatus("uploading");
      const upload = await measureUpload(baseUrl, signal, (mbps) => {
        setMetrics((m) => ({ ...m, upload: Number(mbps.toFixed(2)) }));
      });
      setMetrics((m) => ({ ...m, upload: Number(upload.toFixed(2)) }));

      setStatus("done");
    } catch (err: any) {
      if (err?.name !== "AbortError") console.error("Speedtest error:", err);
      setStatus("error");
    } finally {
      abortControllerRef.current = null;
    }
  };

  const runTest = () => {
    if (process.env.NODE_ENV !== "production") {
      runMockTest();
    } else {
      runRealTest();
    }
  };

  // Если пользователь уходит со страницы посреди теста — гасим все ещё
  // летящие запросы, а не оставляем их фоном дожёвывать сеть впустую.
  useEffect(() => {
    return () => abortControllerRef.current?.abort();
  }, []);

  const isRunning =
    status !== "idle" && status !== "done" && status !== "error";

  const getStatusLabel = () => {
    switch (status) {
      case "pinging":
        return dict.statusPinging;
      case "downloading":
        return dict.statusDownloading;
      case "uploading":
        return dict.statusUploading;
      default:
        return status.toUpperCase();
    }
  };

  return (
    <div className="w-full max-w-4xl mx-auto bg-card/40 backdrop-blur-xl border border-border/50 rounded-[2rem] p-8 shadow-2xl relative overflow-hidden font-mono">
      <AnimatePresence>
        {isRunning && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="absolute inset-0 bg-gradient-to-tr from-primary/5 via-transparent to-secondary/5 pointer-events-none -z-10"
          />
        )}
      </AnimatePresence>

      <div className="flex flex-col md:flex-row items-center justify-between gap-8 mb-8 border-b border-border/50 pb-8">
        <div>
          <h2 className="text-2xl font-black uppercase text-foreground flex items-center gap-3">
            <Server
              className={cn(
                "size-6",
                isRunning
                  ? "text-primary animate-pulse"
                  : "text-muted-foreground",
              )}
            />
            {dict.title}
          </h2>
          <p className="text-muted-foreground text-sm mt-2 flex items-center gap-2">
            {process.env.NODE_ENV !== "production" ? (
              <span className="text-yellow-500 flex items-center gap-1">
                <Zap className="size-3" /> {dict.mockMode}
              </span>
            ) : (
              dict.prodMode
            )}
          </p>
        </div>

        <CyberButton onClick={runTest} disabled={isRunning} variant="primary">
          {!isRunning ? (
            <span className="flex items-center gap-2">
              <Activity className="size-4" />{" "}
              {status === "error"
                ? dict.btnError
                : status === "done"
                  ? dict.btnDone
                  : dict.btnIdle}
            </span>
          ) : (
            <span className="flex items-center gap-2 text-primary-foreground">
              <RefreshCw className="size-4 animate-spin" /> {getStatusLabel()}
              ...
            </span>
          )}
        </CyberButton>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <MetricCard
          title={dict.latency}
          value={metrics.ping}
          unit={dict.unitMs}
          icon={<Activity className="size-4" />}
          isActive={status === "pinging"}
          isDone={
            status === "downloading" ||
            status === "uploading" ||
            status === "done"
          }
          color="text-secondary"
          glowColor="shadow-secondary/20"
        />

        <MetricCard
          title={dict.download}
          value={metrics.download}
          unit={dict.unitMbps}
          icon={<Download className="size-4" />}
          isActive={status === "downloading"}
          isDone={status === "uploading" || status === "done"}
          color="text-green-500"
          glowColor="shadow-green-500/20"
        />

        <MetricCard
          title={dict.upload}
          value={metrics.upload}
          unit={dict.unitMbps}
          icon={<Upload className="size-4" />}
          isActive={status === "uploading"}
          isDone={status === "done"}
          color="text-primary"
          glowColor="shadow-primary/20"
        />
      </div>
    </div>
  );
}

function MetricCard({
  title,
  value,
  unit,
  icon,
  isActive,
  isDone,
  color,
  glowColor,
}: any) {
  return (
    <motion.div
      animate={{
        scale: isActive ? 1.02 : 1,
        borderColor: isActive
          ? "rgba(var(--primary), 0.5)"
          : "rgba(255,255,255,0.1)",
      }}
      className={cn(
        "relative p-6 rounded-2xl border transition-all duration-300 overflow-hidden bg-muted/10",
        isActive &&
          `shadow-[0_0_30px_-5px_rgba(0,0,0,0.3)] ${glowColor} bg-muted/30`,
      )}
    >
      <AnimatePresence>
        {isActive && (
          <motion.div
            initial={{ top: "-10%" }}
            animate={{ top: "110%" }}
            transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
            className={cn(
              "absolute left-0 right-0 h-[2px] blur-[1px] opacity-50 z-0",
              color.replace("text-", "bg-"),
            )}
          />
        )}
      </AnimatePresence>

      <div className="relative z-10 text-muted-foreground text-xs uppercase tracking-widest mb-2 flex items-center justify-between">
        <span className="flex items-center gap-2">
          <span
            className={cn(isActive || isDone ? color : "text-muted-foreground")}
          >
            {icon}
          </span>
          {title}
        </span>
        {isActive && (
          <span
            className="flex size-2 rounded-full bg-current animate-ping"
            style={{ color: "currentColor" }}
          />
        )}
      </div>

      <div
        className={cn(
          "relative z-10 text-4xl font-black transition-colors duration-300",
          isActive || isDone ? "text-foreground" : "text-muted-foreground/50",
        )}
      >
        {value === 0 && !isDone && !isActive ? "--" : value}{" "}
        <span className="text-sm font-normal text-muted-foreground">
          {unit}
        </span>
      </div>

      <div className="absolute bottom-0 left-0 right-0 h-1 bg-black/20">
        <motion.div
          className={cn("h-full", color.replace("text-", "bg-"))}
          initial={{ width: "0%" }}
          animate={{
            width: isDone ? "100%" : isActive ? ["0%", "80%", "95%"] : "0%",
          }}
          transition={{ duration: isActive ? 10 : 0.5, ease: "easeOut" }}
        />
      </div>
    </motion.div>
  );
}
