import { useEffect, useMemo, useRef, useState } from "react";

import {
  fetchObservationRecordedMediaManifest,
  ObservationSessionContractError,
  type ObservationRecordedMediaEpoch,
  type ObservationRecordedMediaManifest,
  type ObservationRecordedMediaSource,
  type ObservationSessionFetch,
} from "../core/observation/sessionArchive";
import {
  type RecordedAdmissionPhase,
  type RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
import type { ObservationSourceDescriptor } from "../core/runtime/contracts";

export interface RecordedObservationPlayback {
  currentSeconds: number;
  playing: boolean;
}

export interface RecordedMediaArchive {
  manifest: ObservationRecordedMediaManifest;
  byteLength: number;
}

export type RecordedMediaPresentationState = "loading" | "ready" | "waiting" | "error";

export const RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS = 1;
let recordedMediaWorkerGeneration = 0;

export function recordedMediaPresentationState(
  state: "loading" | "ready" | "error",
  readyGeneration: string | null,
  selectedGeneration: string | null,
  waitingForEpoch: boolean,
  sessionGate: RecordedAdmissionPhase = "ready",
): RecordedMediaPresentationState {
  if (state === "error" || sessionGate === "error") return "error";
  if (sessionGate !== "ready") return "loading";
  if (waitingForEpoch) return "waiting";
  return state === "ready" &&
    selectedGeneration !== null &&
    readyGeneration === selectedGeneration
    ? "ready"
    : "loading";
}

export function selectRecordedMediaEpoch(
  epochs: readonly ObservationRecordedMediaEpoch[],
  currentSeconds: number,
): ObservationRecordedMediaEpoch | null {
  if (!epochs.length || !Number.isFinite(currentSeconds)) return null;
  let selected: ObservationRecordedMediaEpoch | null = null;
  for (const epoch of epochs) {
    if (epoch.timelineStartSeconds > currentSeconds) break;
    selected = epoch;
  }
  return selected && currentSeconds <= selected.timelineEndSeconds ? selected : null;
}

export function recordedMediaSeekableCoverage(
  durationSeconds: number,
  seekableEndSeconds: number,
  declaredDurationSeconds: number,
  toleranceSeconds = RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS,
  seekableStartSeconds = 0,
): boolean {
  return (
    Number.isFinite(durationSeconds) &&
    Number.isFinite(seekableEndSeconds) &&
    Number.isFinite(declaredDurationSeconds) &&
    Number.isFinite(seekableStartSeconds) &&
    declaredDurationSeconds > 0 &&
    toleranceSeconds >= 0 &&
    durationSeconds + toleranceSeconds >= declaredDurationSeconds &&
    seekableStartSeconds <= toleranceSeconds &&
    seekableStartSeconds >= -toleranceSeconds &&
    seekableEndSeconds + toleranceSeconds >= declaredDurationSeconds
  );
}

export function recordedMediaLocalTime(
  epochStartSeconds: number,
  currentSeconds: number,
  durationSeconds = Number.POSITIVE_INFINITY,
): number {
  if (!Number.isFinite(epochStartSeconds) || !Number.isFinite(currentSeconds)) return 0;
  const local = Math.max(0, currentSeconds - epochStartSeconds);
  return Number.isFinite(durationSeconds)
    ? Math.min(local, Math.max(0, durationSeconds))
    : local;
}

function sourceContract(source: ObservationSourceDescriptor): ObservationRecordedMediaSource | null {
  const delivery = source.delivery;
  if (!delivery || delivery.kind !== "recorded-fmp4-manifest") return null;
  return {
    id: source.id,
    label: source.label,
    modality: "video",
    manifestUrl: delivery.url,
    manifestGenerationSha256: delivery.manifestGenerationSha256,
    byteLength: delivery.byteLength,
    mediaType: delivery.mediaType,
    timelineStartSeconds: delivery.timelineStartSeconds,
    timelineEndSeconds: delivery.timelineEndSeconds,
    seekable: true,
    synchronization: "host-arrival-best-effort",
  };
}

export async function fetchRecordedMediaArchive(
  source: ObservationRecordedMediaSource,
  {
    signal,
    fetcher = globalThis.fetch,
  }: {
    signal?: AbortSignal;
    fetcher?: ObservationSessionFetch;
  } = {},
): Promise<RecordedMediaArchive> {
  const manifest = await fetchObservationRecordedMediaManifest(source, {
    signal,
    fetcher,
    expectedGenerationSha256: source.manifestGenerationSha256,
  });
  const totalBytes = manifest.epochs.reduce((total, epoch) => total + epoch.byteLength, 0);
  if (
    !Number.isSafeInteger(totalBytes) ||
    totalBytes < 1 ||
    totalBytes !== manifest.byteLength ||
    totalBytes !== source.byteLength
  ) {
    throw new ObservationSessionContractError(
      "Размеры потоков записанного медиаканала не совпадают с manifest.",
    );
  }
  return { manifest, byteLength: totalBytes };
}

function videoHasSeekableArchive(
  video: HTMLVideoElement,
  declaredDurationSeconds: number,
): boolean {
  if (video.readyState < 1 || video.seekable.length < 1) return false;
  return recordedMediaSeekableCoverage(
    video.duration,
    video.seekable.end(video.seekable.length - 1),
    declaredDurationSeconds,
    RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS,
    video.seekable.start(0),
  );
}

function waitForSeekableArchive(
  video: HTMLVideoElement,
  declaredDurationSeconds: number,
  signal: AbortSignal,
): Promise<void> {
  if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
  if (videoHasSeekableArchive(video, declaredDurationSeconds)) return Promise.resolve();
  return new Promise((resolve, reject) => {
    const events = ["loadedmetadata", "durationchange", "progress", "canplay"] as const;
    let stallTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
    const armStallTimer = () => {
      if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
      stallTimer = globalThis.setTimeout(() => {
        cleanup();
        reject(new Error("Archived camera stream stalled"));
      }, 45_000);
    };
    const cleanup = () => {
      if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
      for (const event of events) video.removeEventListener(event, onProgress);
      video.removeEventListener("error", onError);
      signal.removeEventListener("abort", onAbort);
    };
    const onProgress = () => {
      armStallTimer();
      if (!videoHasSeekableArchive(video, declaredDurationSeconds)) return;
      cleanup();
      resolve();
    };
    const onError = () => {
      cleanup();
      reject(new Error("Archived camera decode failed"));
    };
    const onAbort = () => {
      cleanup();
      reject(new DOMException("Aborted", "AbortError"));
    };
    for (const event of events) video.addEventListener(event, onProgress);
    video.addEventListener("error", onError, { once: true });
    signal.addEventListener("abort", onAbort, { once: true });
    armStallTimer();
  });
}

async function mountRecordedEpochStream(
  video: HTMLVideoElement,
  descriptor: ObservationRecordedMediaEpoch,
  signal: AbortSignal,
): Promise<() => void> {
  if (!descriptor.mediaType.startsWith("video/mp4;") || !video.canPlayType(descriptor.mediaType)) {
    throw new Error("Archived camera codec is not supported");
  }
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");

  // The generation token binds the native media request to the exact manifest.
  // The browser range-streams the virtual init+fragment file from the server;
  // no complete camera archive is copied into JavaScript memory.
  const cleanup = () => {
    video.pause();
    video.removeAttribute("src");
    video.load();
  };
  video.preload = "auto";
  video.src = descriptor.streamUrl;
  video.load();
  try {
    await waitForSeekableArchive(
      video,
      descriptor.timelineEndSeconds - descriptor.timelineStartSeconds,
      signal,
    );
    return cleanup;
  } catch (error) {
    cleanup();
    throw error;
  }
}

export function RecordedFmp4Player({
  source,
  playback,
  prepare = true,
  sessionGate = "ready",
  admissionKey = null,
  onAdmissionChange,
}: {
  source: ObservationSourceDescriptor;
  playback?: RecordedObservationPlayback | null;
  prepare?: boolean;
  sessionGate?: RecordedAdmissionPhase;
  admissionKey?: string | null;
  onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
}) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const onAdmissionChangeRef = useRef(onAdmissionChange);
  onAdmissionChangeRef.current = onAdmissionChange;
  const workerRef = useRef<{ admissionKey: string | null; generation: number } | null>(null);
  if (!workerRef.current || workerRef.current.admissionKey !== admissionKey) {
    recordedMediaWorkerGeneration += 1;
    workerRef.current = { admissionKey, generation: recordedMediaWorkerGeneration };
  }
  const workerGeneration = workerRef.current.generation;
  const reportAdmission = (next: RecordedCameraAdmissionState) => {
    onAdmissionChangeRef.current?.({
      ...next,
      admissionKey,
      workerGeneration,
    });
  };
  const recordedDelivery = source.delivery?.kind === "recorded-fmp4-manifest"
    ? source.delivery
    : null;
  const contract = useMemo(
    () => sourceContract(source),
    [
      source.id,
      source.label,
      recordedDelivery?.id,
      recordedDelivery?.url,
      recordedDelivery?.mediaType,
      recordedDelivery?.manifestGenerationSha256,
      recordedDelivery?.byteLength,
      recordedDelivery?.timelineStartSeconds,
      recordedDelivery?.timelineEndSeconds,
    ],
  );
  const [archive, setArchive] = useState<RecordedMediaArchive | null>(null);
  const [state, setState] = useState<"loading" | "ready" | "error">("loading");
  const [readyGeneration, setReadyGeneration] = useState<string | null>(null);
  const [bufferRevision, setBufferRevision] = useState(0);
  const currentSeconds = playback?.currentSeconds ?? contract?.timelineStartSeconds ?? 0;
  const epoch = useMemo(
    () => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
    [archive?.manifest.epochs, currentSeconds],
  );
  const waitingForEpoch = Boolean(archive && !epoch);
  const selectedGeneration = contract && epoch
    ? `${contract.manifestGenerationSha256}:${epoch.ordinal}:${epoch.timelineStartSeconds}:${epoch.timelineEndSeconds}`
    : null;
  const visualState = recordedMediaPresentationState(
    state,
    readyGeneration,
    selectedGeneration,
    waitingForEpoch,
    sessionGate,
  );

  useEffect(() => {
    if (!contract) {
      setArchive(null);
      setReadyGeneration(null);
      setState("error");
      reportAdmission({
        phase: "error",
        byteLength: null,
        message: "Некорректный descriptor записанной камеры.",
      });
      return;
    }
    if (!prepare) return;
    const abort = new AbortController();
    setArchive(null);
    setReadyGeneration(null);
    setState("loading");
    reportAdmission({
      phase: "loading",
      byteLength: contract.byteLength,
      message: null,
    });
    void fetchRecordedMediaArchive(contract, { signal: abort.signal })
      .then((loaded) => {
        if (abort.signal.aborted) return;
        setArchive(loaded);
      })
      .catch((error: unknown) => {
        if (abort.signal.aborted || (error instanceof DOMException && error.name === "AbortError")) {
          return;
        }
        setArchive(null);
        setReadyGeneration(null);
        setState("error");
        reportAdmission({
          phase: "error",
          byteLength: contract.byteLength,
          message: "Архив записанной камеры не прошёл проверку.",
        });
      });
    return () => abort.abort();
  }, [admissionKey, contract, prepare]);

  useEffect(() => {
    if (!archive || !contract || !prepare) return;
    const abort = new AbortController();
    let disposed = false;
    void (async () => {
      for (const candidate of archive.manifest.epochs) {
        const probe = document.createElement("video");
        probe.muted = true;
        probe.playsInline = true;
        const cleanup = await mountRecordedEpochStream(probe, candidate, abort.signal);
        cleanup();
        if (disposed || abort.signal.aborted) return;
      }
      if (disposed || abort.signal.aborted) return;
      reportAdmission({
        phase: "ready",
        byteLength: archive.byteLength,
        message: null,
      });
    })().catch((error: unknown) => {
      if (
        disposed ||
        abort.signal.aborted ||
        (error instanceof DOMException && error.name === "AbortError")
      ) return;
      setReadyGeneration(null);
      setState("error");
      reportAdmission({
        phase: "error",
        byteLength: archive.byteLength,
        message: "Не все codec epoch записанной камеры декодируются и доступны для seek.",
      });
    });
    return () => {
      disposed = true;
      abort.abort();
    };
  }, [admissionKey, archive, contract, prepare]);

  useEffect(() => {
    const video = videoRef.current;
    if (!video || !epoch) return;
    const epochDescriptor = epoch;
    const generation = contract
      ? `${contract.manifestGenerationSha256}:${epochDescriptor.ordinal}:${epochDescriptor.timelineStartSeconds}:${epochDescriptor.timelineEndSeconds}`
      : null;
    setReadyGeneration(null);
    setState("loading");
    const abort = new AbortController();
    let disposed = false;
    let cleanup: (() => void) | null = null;

    const loadEpoch = async () => {
      try {
        cleanup = await mountRecordedEpochStream(video, epochDescriptor, abort.signal);
        if (disposed || abort.signal.aborted) {
          cleanup();
          cleanup = null;
          return;
        }
        setBufferRevision((revision) => revision + 1);
        setReadyGeneration(generation);
        setState("ready");
      } catch (error) {
        if (
          disposed ||
          abort.signal.aborted ||
          (error instanceof DOMException && error.name === "AbortError")
        ) {
          return;
        }
        setReadyGeneration(null);
        setState("error");
        reportAdmission({
          phase: "error",
          byteLength: archive?.byteLength ?? null,
          message: "Записанная камера не стала seekable.",
        });
      }
    };
    void loadEpoch();

    return () => {
      disposed = true;
      abort.abort();
      cleanup?.();
    };
  }, [archive?.byteLength, contract, epoch]);

  useEffect(() => {
    const video = videoRef.current;
    if (!video || !epoch || visualState !== "ready") return;
    const target = recordedMediaLocalTime(
      epoch.timelineStartSeconds,
      currentSeconds,
      video.duration,
    );
    if (Number.isFinite(target) && Math.abs(video.currentTime - target) > 0.35) {
      try {
        video.currentTime = target;
      } catch {
        setReadyGeneration(null);
        setState("error");
        reportAdmission({
          phase: "error",
          byteLength: archive?.byteLength ?? null,
          message: "Seek записанной камеры завершился ошибкой.",
        });
        return;
      }
    }
    if (playback?.playing) {
      void video.play().catch(() => undefined);
    } else {
      video.pause();
    }
  }, [archive?.byteLength, bufferRevision, currentSeconds, epoch, playback?.playing, visualState]);

  return (
    <div
      className="recorded-media-player"
      data-state={visualState}
      aria-busy={visualState === "loading"}
    >
      <video
        ref={videoRef}
        className="observation-media__asset"
        muted
        playsInline
        preload="auto"
        aria-label={source.label}
      />
      {visualState !== "ready" ? (
        <div
          className="recorded-media-player__notice"
          role={visualState === "error" ? "alert" : "status"}
        >
          {visualState === "waiting"
            ? "Камера на этой позиции ещё не записывалась"
            : visualState === "error"
              ? "Записанное видео недоступно"
              : archive
                ? "Проверяем seek и codec записанного видео…"
                : "Читаем manifest записанного видео…"}
        </div>
      ) : null}
    </div>
  );
}
