import { Task } from "@lit/task";
import type { VideoSample } from "mediabunny";
import type { VideoRendition } from "../../../transcoding/types";
import type { EFVideo } from "../../EFVideo";
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
import { ScrubInputCache } from "./ScrubInputCache";

type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;

// Shared cache for scrub inputs
const scrubInputCache = new ScrubInputCache();

export const makeUnifiedVideoSeekTask = (
  host: EFVideo,
): UnifiedVideoSeekTask => {
  return new Task(host, {
    args: () => [host.desiredSeekTimeMs] as const,
    onError: (error) => {
      console.error("unifiedVideoSeekTask error", error);
    },
    onComplete: (_value) => {},
    task: async ([desiredSeekTimeMs], { signal }) => {
      signal.throwIfAborted();

      const mediaEngine = await getLatestMediaEngine(host, signal);
      if (!mediaEngine) return undefined;

      // FIRST: Check if main quality content is already cached
      const mainRendition = mediaEngine.videoRendition;
      if (mainRendition) {
        const mainSegmentId = mediaEngine.computeSegmentId(
          desiredSeekTimeMs,
          mainRendition,
        );
        if (
          mainSegmentId !== undefined &&
          mediaEngine.isSegmentCached(mainSegmentId, mainRendition)
        ) {
          // Main content is cached! Use it directly.
          return await getMainVideoSample(
            host,
            mediaEngine,
            desiredSeekTimeMs,
            signal,
          );
        }
      }

      // SECOND: Main not cached, try scrub path (instant if cached)
      const scrubSample = await tryGetScrubSample(
        mediaEngine,
        desiredSeekTimeMs,
        signal,
      );
      if (scrubSample || signal.aborted) {
        // If scrub succeeded, start background main quality upgrade (non-blocking)
        if (scrubSample) {
          startMainQualityUpgrade(
            host,
            mediaEngine,
            desiredSeekTimeMs,
            signal,
          ).catch(() => {
            // Main upgrade failed - scrub already succeeded, that's fine
          });
        }

        return scrubSample;
      }

      // THIRD: Neither are cached, fetch main video path as final fallback
      return await getMainVideoSample(
        host,
        mediaEngine,
        desiredSeekTimeMs,
        signal,
      );
    },
  });
};

/**
 * Try to get scrub sample from cache (instant if available)
 */
async function tryGetScrubSample(
  mediaEngine: any,
  desiredSeekTimeMs: number,
  signal: AbortSignal,
): Promise<VideoSample | undefined> {
  try {
    // Get scrub rendition
    let scrubRendition: VideoRendition | undefined;
    if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) {
      scrubRendition = mediaEngine.data.videoRenditions.find(
        (r: any) => r.id === "scrub",
      );
    }

    if (!scrubRendition) return undefined;

    const scrubRenditionWithSrc = {
      ...scrubRendition,
      src: mediaEngine.src,
    };

    // Check if scrub segment is cached
    const segmentId = mediaEngine.computeSegmentId(
      desiredSeekTimeMs,
      scrubRenditionWithSrc,
    );
    if (segmentId === undefined) return undefined;

    const isCached = mediaEngine.isSegmentCached(
      segmentId,
      scrubRenditionWithSrc,
    );
    if (!isCached) return undefined; // Not cached - let main video handle it

    // Get cached scrub input and seek within it
    const scrubInput = await scrubInputCache.getOrCreateInput(
      segmentId,
      async () => {
        const [initSegment, mediaSegment] = await Promise.all([
          mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
          mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
        ]);

        if (!initSegment || !mediaSegment || signal.aborted) return undefined;

        const { BufferedSeekingInput } = await import(
          "../BufferedSeekingInput.js"
        );
        const { EFMedia } = await import("../../EFMedia.js");

        return new BufferedSeekingInput(
          await new Blob([initSegment, mediaSegment]).arrayBuffer(),
          {
            videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
            audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
            startTimeOffsetMs: scrubRendition.startTimeOffsetMs,
          },
        );
      },
    );

    if (!scrubInput) return undefined;

    const videoTrack = await scrubInput.getFirstVideoTrack();
    if (!videoTrack) return undefined;

    const sample = (await scrubInput.seek(
      videoTrack.id,
      desiredSeekTimeMs,
    )) as unknown as VideoSample | undefined;

    return sample;
  } catch (_error) {
    if (signal.aborted) return undefined;
    return undefined; // Scrub failed - let main video handle it
  }
}

/**
 * Get main video sample (slower path with fetching)
 */
async function getMainVideoSample(
  _host: EFVideo,
  mediaEngine: any,
  desiredSeekTimeMs: number,
  signal: AbortSignal,
): Promise<VideoSample | undefined> {
  try {
    // Use existing main video task chain
    const segmentId = mediaEngine.computeSegmentId(
      desiredSeekTimeMs,
      mediaEngine.getVideoRendition(),
    );
    if (segmentId === undefined) return undefined;

    // Fetch main video segment
    const [initSegment, mediaSegment] = await Promise.all([
      mediaEngine.fetchInitSegment(mediaEngine.getVideoRendition(), signal),
      mediaEngine.fetchMediaSegment(
        segmentId,
        mediaEngine.getVideoRendition(),
        signal,
      ),
    ]);

    if (!initSegment || !mediaSegment) return undefined;
    signal.throwIfAborted();

    // Create main video input
    const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
    const { EFMedia } = await import("../../EFMedia.js");

    const videoRendition = mediaEngine.videoRendition;
    const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;

    const mainInput = new BufferedSeekingInput(
      await new Blob([initSegment, mediaSegment]).arrayBuffer(),
      {
        videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
        audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
        startTimeOffsetMs,
      },
    );

    const videoTrack = await mainInput.getFirstVideoTrack();
    if (!videoTrack) return undefined;

    signal.throwIfAborted();

    const sample = (await mainInput.seek(
      videoTrack.id,
      desiredSeekTimeMs,
    )) as unknown as VideoSample | undefined;

    return sample;
  } catch (error) {
    if (signal.aborted) return undefined;
    throw error;
  }
}

/**
 * Start background upgrade to main quality (non-blocking)
 */
async function startMainQualityUpgrade(
  host: EFVideo,
  mediaEngine: any,
  targetSeekTimeMs: number,
  signal: AbortSignal,
): Promise<void> {
  // Small delay to let scrub content display first
  await new Promise((resolve) => setTimeout(resolve, 50));
  if (signal.aborted || host.desiredSeekTimeMs !== targetSeekTimeMs) return;

  // Get main quality sample and upgrade display
  const mainSample = await getMainVideoSample(
    host,
    mediaEngine,
    targetSeekTimeMs,
    signal,
  );
  if (
    mainSample &&
    !signal.aborted &&
    host.desiredSeekTimeMs === targetSeekTimeMs
  ) {
    const videoFrame = mainSample.toVideoFrame();
    try {
      host.displayFrame(videoFrame, targetSeekTimeMs);
    } finally {
      videoFrame.close();
    }
  }
}
