import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
} from "react";
import { StatusBadge } from "@nodedc/ui-react";

import {
  fetchGroundReview,
  fetchGroundReviewFrame,
  type GroundReview,
  type GroundReviewFrame,
  type GroundReviewFrameSummary,
} from "../core/polygon/groundQualification";
import {
  LidarGroundPointCloud,
  type LidarGroundViewMode,
} from "./LidarGroundPointCloud";

interface GooseDatasetReviewProps {
  runId: string;
}

export interface GooseDatasetSequence {
  datasetSequenceId: string;
  frames: GroundReviewFrameSummary[];
  durationSeconds: number;
  minimumGapSeconds: number;
  maximumGapSeconds: number;
}

interface SequenceMetricComparison {
  current: number;
  patchwork: number;
  delta: number;
}

export interface GooseDatasetSequenceAnalysis {
  groundIou: SequenceMetricComparison;
  naturalGroundRecall: SequenceMetricComparison;
  obstacleNonGroundRecall: SequenceMetricComparison;
  patchworkBetterFrames: number;
  currentBetterFrames: number;
  unchangedFrames: number;
  weakestPatchworkFrame: GroundReviewFrameSummary;
  worstRegressionFrame: GroundReviewFrameSummary;
  largestGapFrame: GroundReviewFrameSummary | null;
  largestGapSeconds: number;
}

const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
  ["intensity", "Исходный скан"],
  ["ground-truth", "Ground truth"],
  ["current", "Текущий алгоритм"],
  ["candidate", "Patchwork++"],
  ["disagreement", "Ошибки текущего"],
  ["candidate-disagreement", "Ошибки Patchwork++"],
];

function errorMessage(error: unknown): string {
  if (error instanceof Error && error.message.trim()) return error.message;
  return "Не удалось открыть фрагменты GOOSE.";
}

function secondsBetween(
  left: GroundReviewFrameSummary,
  right: GroundReviewFrameSummary,
): number {
  const delta = BigInt(right.sensorTimestampNs) - BigInt(left.sensorTimestampNs);
  return Number(delta) / 1_000_000_000;
}

export function groupGooseReviewFrames(
  review: GroundReview,
): GooseDatasetSequence[] {
  const groups = new Map<string, GroundReviewFrameSummary[]>();
  review.frames.forEach((frame) => {
    const frames = groups.get(frame.datasetSequenceId) ?? [];
    frames.push(frame);
    groups.set(frame.datasetSequenceId, frames);
  });
  return [...groups.entries()].map(([datasetSequenceId, frames]) => {
    const gaps = frames.slice(1).map((frame, index) => (
      secondsBetween(frames[index], frame)
    ));
    return {
      datasetSequenceId,
      frames,
      durationSeconds: frames.length > 1
        ? secondsBetween(frames[0], frames[frames.length - 1])
        : 0,
      minimumGapSeconds: gaps.length ? Math.min(...gaps) : 0,
      maximumGapSeconds: gaps.length ? Math.max(...gaps) : 0,
    };
  });
}

function average(
  frames: GroundReviewFrameSummary[],
  value: (frame: GroundReviewFrameSummary) => number,
): number {
  return frames.reduce((total, frame) => total + value(frame), 0) / frames.length;
}

function comparison(
  frames: GroundReviewFrameSummary[],
  value: (frame: GroundReviewFrameSummary) => {
    current: number;
    patchwork: number;
  },
): SequenceMetricComparison {
  const current = average(frames, (frame) => value(frame).current);
  const patchwork = average(frames, (frame) => value(frame).patchwork);
  return { current, patchwork, delta: patchwork - current };
}

export function analyzeGooseDatasetSequence(
  sequence: GooseDatasetSequence,
): GooseDatasetSequenceAnalysis {
  const first = sequence.frames[0];
  if (!first) {
    throw new Error("Фрагмент датасета не содержит сканов.");
  }
  let weakestPatchworkFrame = first;
  let worstRegressionFrame = first;
  let largestGapFrame: GroundReviewFrameSummary | null = null;
  let largestGapSeconds = 0;
  let patchworkBetterFrames = 0;
  let currentBetterFrames = 0;
  let unchangedFrames = 0;

  sequence.frames.forEach((frame, index) => {
    if (frame.patchwork.groundIou < weakestPatchworkFrame.patchwork.groundIou) {
      weakestPatchworkFrame = frame;
    }
    if (frame.groundIouDelta < worstRegressionFrame.groundIouDelta) {
      worstRegressionFrame = frame;
    }
    if (frame.groundIouDelta > 0.000_001) patchworkBetterFrames += 1;
    else if (frame.groundIouDelta < -0.000_001) currentBetterFrames += 1;
    else unchangedFrames += 1;

    if (index > 0) {
      const gapSeconds = secondsBetween(sequence.frames[index - 1], frame);
      if (gapSeconds > largestGapSeconds) {
        largestGapSeconds = gapSeconds;
        largestGapFrame = frame;
      }
    }
  });

  return {
    groundIou: comparison(sequence.frames, (frame) => ({
      current: frame.current.groundIou,
      patchwork: frame.patchwork.groundIou,
    })),
    naturalGroundRecall: comparison(sequence.frames, (frame) => ({
      current: frame.current.naturalGroundRecall,
      patchwork: frame.patchwork.naturalGroundRecall,
    })),
    obstacleNonGroundRecall: comparison(sequence.frames, (frame) => ({
      current: frame.current.obstacleNonGroundRecall,
      patchwork: frame.patchwork.obstacleNonGroundRecall,
    })),
    patchworkBetterFrames,
    currentBetterFrames,
    unchangedFrames,
    weakestPatchworkFrame,
    worstRegressionFrame,
    largestGapFrame,
    largestGapSeconds,
  };
}

function sequenceLabel(sequenceId: string): string {
  const date = sequenceId.slice(0, 10);
  const title = sequenceId.slice(11).replaceAll("_", " ");
  return `${date} · ${title}`;
}

function formatDuration(seconds: number): string {
  const rounded = Math.max(0, Math.round(seconds));
  const minutes = Math.floor(rounded / 60);
  const remainder = rounded % 60;
  return `${minutes}:${String(remainder).padStart(2, "0")}`;
}

function formatGap(seconds: number): string {
  return `${seconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с`;
}

function formatPercent(value: number): string {
  return new Intl.NumberFormat("ru-RU", {
    style: "percent",
    maximumFractionDigits: 1,
  }).format(value);
}

function formatDelta(value: number): string {
  const points = value * 100;
  return `${points >= 0 ? "+" : ""}${points.toLocaleString("ru-RU", {
    maximumFractionDigits: 1,
  })} п.п.`;
}

function frameScene(frame: GroundReviewFrame | null) {
  if (!frame) return null;
  return {
    pointCount: frame.pointCount,
    pointsXyzM: frame.pointsXyzM,
    intensity0To255: frame.intensity0To255,
    masks: {
      currentGround: frame.currentGround,
      currentAssigned: frame.evaluated,
      candidateGround: frame.patchworkGround,
      candidateAssigned: frame.evaluated,
      disagreement: frame.currentDisagreement,
      candidateDisagreement: frame.patchworkDisagreement,
      groundTruthGround: frame.groundTruthGround,
    },
  };
}

function modeExplanation(mode: LidarGroundViewMode): string {
  if (mode === "intensity") return "Один sensor-frame оборот VLS-128; цвет — remission.";
  if (mode === "ground-truth") return "Публичная point-aligned разметка GOOSE.";
  if (mode === "current") return "Результат текущего алгоритма для этого исходного скана.";
  if (mode === "candidate") return "Результат Patchwork++ для этого исходного скана.";
  if (mode === "disagreement") {
    return "Красным отмечены ошибки текущего алгоритма относительно разметки.";
  }
  return "Красным отмечены ошибки Patchwork++ относительно labels.";
}

export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
  const [review, setReview] = useState<GroundReview | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [selectedSequenceId, setSelectedSequenceId] = useState<string | null>(null);
  const [selectedFrameId, setSelectedFrameId] = useState<string | null>(null);
  const [reviewFrame, setReviewFrame] = useState<GroundReviewFrame | null>(null);
  const [mode, setMode] = useState<LidarGroundViewMode>("intensity");
  const [playing, setPlaying] = useState(false);
  const [frameLoading, setFrameLoading] = useState(false);
  const frameCache = useRef(new Map<string, GroundReviewFrame>());
  const playerRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const controller = new AbortController();
    setError(null);
    setReview(null);
    setReviewFrame(null);
    setPlaying(false);
    frameCache.current.clear();
    void fetchGroundReview(runId, { signal: controller.signal })
      .then((value) => {
        if (controller.signal.aborted || !value) return;
        setReview(value);
        const first = value.frames[0] ?? null;
        setSelectedSequenceId(first?.datasetSequenceId ?? null);
        setSelectedFrameId(first?.frameId ?? null);
      })
      .catch((loadError: unknown) => {
        if (!controller.signal.aborted) setError(errorMessage(loadError));
      });
    return () => controller.abort();
  }, [runId]);

  const sequences = useMemo(
    () => review ? groupGooseReviewFrames(review) : [],
    [review],
  );
  const selectedSequence = sequences.find(
    (sequence) => sequence.datasetSequenceId === selectedSequenceId,
  ) ?? sequences[0] ?? null;
  const sequenceAnalysis = useMemo(
    () => selectedSequence ? analyzeGooseDatasetSequence(selectedSequence) : null,
    [selectedSequence],
  );
  const selectedPosition = Math.max(
    0,
    selectedSequence?.frames.findIndex((frame) => frame.frameId === selectedFrameId) ?? 0,
  );
  const selectedSummary = selectedSequence?.frames[selectedPosition] ?? null;
  const previousSummary = selectedSequence && selectedPosition > 0
    ? selectedSequence.frames[selectedPosition - 1]
    : null;
  const gapFromPrevious = selectedSummary && previousSummary
    ? secondsBetween(previousSummary, selectedSummary)
    : null;

  useEffect(() => {
    if (!selectedFrameId) return;
    const cached = frameCache.current.get(selectedFrameId);
    if (cached) {
      setReviewFrame(cached);
      setFrameLoading(false);
      return;
    }
    const controller = new AbortController();
    setFrameLoading(true);
    void fetchGroundReviewFrame(runId, selectedFrameId, {
      signal: controller.signal,
    })
      .then((frame) => {
        if (controller.signal.aborted) return;
        frameCache.current.set(frame.frameId, frame);
        while (frameCache.current.size > 32) {
          const oldest = frameCache.current.keys().next().value as string | undefined;
          if (oldest) frameCache.current.delete(oldest);
          else break;
        }
        setReviewFrame(frame);
        setFrameLoading(false);
      })
      .catch((loadError: unknown) => {
        if (controller.signal.aborted) return;
        setFrameLoading(false);
        setError(errorMessage(loadError));
      });
    return () => controller.abort();
  }, [runId, selectedFrameId]);

  useEffect(() => {
    if (
      !playing
      || !selectedSequence
      || !selectedFrameId
      || reviewFrame?.frameId !== selectedFrameId
    ) return;
    const timer = window.setTimeout(() => {
      const next = selectedSequence.frames[selectedPosition + 1];
      if (!next) {
        setPlaying(false);
        return;
      }
      setSelectedFrameId(next.frameId);
    }, 700);
    return () => window.clearTimeout(timer);
  }, [
    playing,
    reviewFrame?.frameId,
    selectedFrameId,
    selectedPosition,
    selectedSequence,
  ]);

  const scene = useMemo(() => frameScene(reviewFrame), [reviewFrame]);

  const selectPosition = (position: number) => {
    if (!selectedSequence) return;
    const bounded = Math.max(0, Math.min(position, selectedSequence.frames.length - 1));
    setPlaying(false);
    setSelectedFrameId(selectedSequence.frames[bounded]?.frameId ?? null);
  };

  const inspectFrame = (
    frame: GroundReviewFrameSummary | null,
    viewMode: LidarGroundViewMode,
  ) => {
    if (!selectedSequence || !frame) return;
    selectPosition(selectedSequence.frames.indexOf(frame));
    setMode(viewMode);
    window.requestAnimationFrame(() => {
      playerRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  };

  const selectSequence = (sequenceId: string) => {
    const sequence = sequences.find((candidate) => (
      candidate.datasetSequenceId === sequenceId
    ));
    setPlaying(false);
    setSelectedSequenceId(sequenceId);
    setSelectedFrameId(sequence?.frames[0]?.frameId ?? null);
    setReviewFrame(null);
  };

  if (error) {
    return (
      <section className="polygon-review-unavailable">
        <StatusBadge tone="danger">Источник недоступен</StatusBadge>
        <h3>Не удалось открыть GOOSE</h3>
        <p>{error}</p>
      </section>
    );
  }

  if (!review || !selectedSequence) {
    return (
      <section className="polygon-review-unavailable">
        <StatusBadge tone="accent">Читаем индекс</StatusBadge>
        <h3>Открываем фрагменты GOOSE</h3>
        <p>Загружаем только bounded preview, исходный архив остаётся на worker.</p>
      </section>
    );
  }

  return (
    <section className="dataset-sequence-review" aria-label="Просмотр GOOSE validation">
      <header className="dataset-sequence-review__truth">
        <div>
          <span className="section-eyebrow">СОДЕРЖИМОЕ ДАТАСЕТА</span>
          <h2>8 отдельных фрагментов исходных записей</h2>
          <p>
            Validation ZIP содержит {review.frameCount} разреженных размеченных
            LiDAR-оборотов. Каждый фрагмент относится к отдельному эпизоду съёмки.
            Play ниже — ускоренный просмотр доступных сканов выбранного фрагмента,
            а не воспроизведение движения машины в реальном времени.
          </p>
        </div>
        <dl>
          <div><dt>Фрагментов</dt><dd>{sequences.length}</dd></div>
          <div><dt>Размеченных сканов</dt><dd>{review.frameCount}</dd></div>
          <div><dt>Preview</dt><dd>{review.previewPoints.toLocaleString("ru-RU")} точек</dd></div>
        </dl>
      </header>

      <div className="dataset-sequence-select">
        <label htmlFor="goose-sequence">Фрагмент датасета</label>
        <select
          id="goose-sequence"
          value={selectedSequence.datasetSequenceId}
          onChange={(event) => selectSequence(event.target.value)}
        >
          {sequences.map((sequence) => (
            <option
              key={sequence.datasetSequenceId}
              value={sequence.datasetSequenceId}
            >
              {sequenceLabel(sequence.datasetSequenceId)}
              {" · "}{sequence.frames.length} сканов
            </option>
          ))}
        </select>
        <span>
          {selectedSequence.frames.length} сканов · интервалы{" "}
          {formatGap(selectedSequence.minimumGapSeconds)}–{
            formatGap(selectedSequence.maximumGapSeconds)
          } · охват {formatDuration(selectedSequence.durationSeconds)}
        </span>
      </div>

      <section className="polygon-review-player" ref={playerRef}>
        <header className="polygon-review-player__header">
          <div>
            <span className="section-eyebrow">
              SCAN {selectedPosition + 1} / {selectedSequence.frames.length}
              {" · SOURCE FRAME "}{selectedSummary?.datasetFrameNumber ?? "—"}
            </span>
            <strong>{selectedSummary?.frameId ?? "Загружаем скан"}</strong>
          </div>
          <div className="polygon-review-mode" aria-label="Режим визуализации">
            {viewModes.map(([viewMode, label]) => (
              <button
                type="button"
                key={viewMode}
                data-active={mode === viewMode ? "true" : undefined}
                onClick={() => setMode(viewMode)}
              >
                {label}
              </button>
            ))}
          </div>
        </header>

        <div className="polygon-review-controls">
          <button
            type="button"
            aria-label={playing ? "Пауза" : "Воспроизвести фрагмент"}
            onClick={() => setPlaying((value) => !value)}
          >
            {playing ? "Пауза" : "Play review"}
          </button>
          <button
            type="button"
            aria-label="Предыдущий скан"
            disabled={selectedPosition <= 0}
            onClick={() => selectPosition(selectedPosition - 1)}
          >
            ←
          </button>
          <button
            type="button"
            aria-label="Следующий скан"
            disabled={selectedPosition >= selectedSequence.frames.length - 1}
            onClick={() => selectPosition(selectedPosition + 1)}
          >
            →
          </button>
          <input
            type="range"
            min={0}
            max={Math.max(0, selectedSequence.frames.length - 1)}
            value={selectedPosition}
            aria-label="Позиция во фрагменте"
            onChange={(event) => selectPosition(Number(event.target.value))}
          />
          <span>{selectedPosition + 1} / {selectedSequence.frames.length}</span>
        </div>

        <div className="polygon-review-stage">
          {scene ? (
            <LidarGroundPointCloud
              key={selectedSequence.datasetSequenceId}
              frame={scene}
              mode={mode}
              coordinateFrame="sensor-fixed"
              sensorHeightM={2.24}
            />
          ) : (
            <div className="polygon-review-stage__loading">Загружаем скан…</div>
          )}
          <div className="polygon-review-stage__legend">
            <strong>{viewModes.find(([viewMode]) => viewMode === mode)?.[1]}</strong>
            <span>{modeExplanation(mode)}</span>
            {frameLoading ? <span>Подгружаем выбранный scan…</span> : null}
          </div>
        </div>

        {selectedSummary ? (
          <div className="polygon-review-frame-metrics">
            <article>
              <span>Интервал от предыдущего</span>
              <strong>{gapFromPrevious === null ? "Начало" : formatGap(gapFromPrevious)}</strong>
              <small>реальный timestamp, не cadence playback</small>
            </article>
            <article>
              <span>Текущий алгоритм · Ground IoU</span>
              <strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
              <small>только выбранный скан</small>
            </article>
            <article>
              <span>Patchwork++ · Ground IoU</span>
              <strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
              <small>только выбранный скан</small>
            </article>
            <article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
              <span>Разница</span>
              <strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
              <small>Patchwork++ минус текущий алгоритм</small>
            </article>
          </div>
        ) : null}

        <div
          className="polygon-review-timeline dataset-sequence-timeline"
          aria-label={`Все ${selectedSequence.frames.length} сканов фрагмента`}
        >
          {selectedSequence.frames.map((frame, index) => (
            <button
              type="button"
              key={frame.frameId}
              aria-label={`Scan ${index + 1}, source frame ${frame.datasetFrameNumber}`}
              title={`${frame.frameId}`}
              data-active={frame.frameId === selectedFrameId ? "true" : undefined}
              style={{ "--frame-color": "rgb(255 255 255 / 0.34)" } as CSSProperties}
              onClick={() => selectPosition(index)}
            />
          ))}
        </div>
        <p className="polygon-review-timeline-note">
          Полоса показывает только доступные размеченные сканы этого фрагмента.
          Пропуски времени не интерполируются; ракурс сохраняется между соседними сканами.
        </p>
      </section>

      {sequenceAnalysis ? (
        <section
          className="dataset-fragment-analysis"
          aria-label="Результаты выбранного фрагмента"
        >
          <header>
            <div>
              <span className="section-eyebrow">РЕЗУЛЬТАТЫ ЭТОГО ФРАГМЕНТА</span>
              <h3>Что изменилось после Patchwork++</h3>
              <p>
                Средние значения рассчитаны только по{" "}
                {selectedSequence.frames.length} сканам выбранного фрагмента.
                Они не смешиваются с остальными частями датасета.
              </p>
            </div>
            <div className="dataset-fragment-analysis__verdict">
              <strong>{formatDelta(sequenceAnalysis.groundIou.delta)}</strong>
              <span>средний Ground IoU</span>
              <small>
                лучше на {sequenceAnalysis.patchworkBetterFrames} · хуже на{" "}
                {sequenceAnalysis.currentBetterFrames} · без изменения на{" "}
                {sequenceAnalysis.unchangedFrames}
              </small>
            </div>
          </header>

          <div className="dataset-fragment-comparison">
            {([
              ["Ground IoU", sequenceAnalysis.groundIou],
              ["Natural ground recall", sequenceAnalysis.naturalGroundRecall],
              ["Obstacle preservation", sequenceAnalysis.obstacleNonGroundRecall],
            ] as const).map(([label, metric]) => (
              <article key={label}>
                <span>{label}</span>
                <div>
                  <small>Текущий</small>
                  <strong>{formatPercent(metric.current)}</strong>
                </div>
                <i>→</i>
                <div>
                  <small>Patchwork++</small>
                  <strong>{formatPercent(metric.patchwork)}</strong>
                </div>
                <em data-positive={metric.delta >= 0 ? "true" : "false"}>
                  {formatDelta(metric.delta)}
                </em>
              </article>
            ))}
          </div>

          <div className="dataset-fragment-risks">
            <button
              type="button"
              onClick={() => inspectFrame(
                sequenceAnalysis.worstRegressionFrame,
                "candidate-disagreement",
              )}
            >
              <span>Самое сильное ухудшение</span>
              <strong>{formatDelta(sequenceAnalysis.worstRegressionFrame.groundIouDelta)}</strong>
              <small>
                source frame {sequenceAnalysis.worstRegressionFrame.datasetFrameNumber}
              </small>
            </button>
            <button
              type="button"
              onClick={() => inspectFrame(
                sequenceAnalysis.weakestPatchworkFrame,
                "candidate",
              )}
            >
              <span>Самый слабый результат Patchwork++</span>
              <strong>
                {formatPercent(sequenceAnalysis.weakestPatchworkFrame.patchwork.groundIou)}
              </strong>
              <small>
                source frame {sequenceAnalysis.weakestPatchworkFrame.datasetFrameNumber}
              </small>
            </button>
            <button
              type="button"
              disabled={!sequenceAnalysis.largestGapFrame}
              onClick={() => inspectFrame(
                sequenceAnalysis.largestGapFrame,
                "intensity",
              )}
            >
              <span>Самый большой пропуск записи</span>
              <strong>{formatGap(sequenceAnalysis.largestGapSeconds)}</strong>
              <small>
                {sequenceAnalysis.largestGapFrame
                  ? `перед source frame ${sequenceAnalysis.largestGapFrame.datasetFrameNumber}`
                  : "фрагмент содержит один скан"}
              </small>
            </button>
          </div>
        </section>
      ) : null}

      <p className="dataset-sequence-review__raw-note">
        Для настоящего движения машины, odometry и синхронного sensor playback нужен
        отдельный raw ROS bag GOOSE с localization. Он не входит в принятый 3.3 ГБ
        annotated validation ZIP и сейчас не притворяется доступным.
      </p>
    </section>
  );
}
