import type {
  AudioRendition,
  VideoRendition,
} from "../../../transcoding/types";

/**
 * State interface for media buffering - generic for both audio and video
 */
export interface MediaBufferState {
  currentSeekTimeMs: number;
  activeRequests: Set<number>;
  cachedSegments: Set<number>;
  requestQueue: number[];
}

/**
 * Configuration interface for media buffering - generic for both audio and video
 */
export interface MediaBufferConfig {
  bufferDurationMs: number;
  maxParallelFetches: number;
  enableBuffering: boolean;
  enableContinuousBuffering?: boolean;
}

/**
 * Dependencies interface for media buffering - generic for both audio and video
 */
export interface MediaBufferDependencies<
  T extends AudioRendition | VideoRendition,
> {
  computeSegmentId: (
    timeMs: number,
    rendition: T,
  ) => Promise<number | undefined>;
  fetchSegment: (segmentId: number, rendition: T) => Promise<ArrayBuffer>;
  getRendition: () => Promise<T>;
  logError: (message: string, error: any) => void;
}

/**
 * Compute segment range for a time window
 * Pure function - determines which segments are needed for a time range
 */
export const computeSegmentRange = <T extends AudioRendition | VideoRendition>(
  startTimeMs: number,
  endTimeMs: number,
  rendition: T,
  computeSegmentId: (timeMs: number, rendition: T) => number | undefined,
): number[] => {
  const segments: number[] = [];
  const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;

  // Calculate segment indices that overlap with [startTimeMs, endTimeMs]
  const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);
  const endSegmentIndex = Math.floor(endTimeMs / segmentDurationMs);

  for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
    const segmentId = computeSegmentId(i * segmentDurationMs, rendition);
    if (segmentId !== undefined) {
      segments.push(segmentId);
    }
  }

  return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates
};

/**
 * Async version of computeSegmentRange for when computeSegmentId is async
 */
export const computeSegmentRangeAsync = async <
  T extends AudioRendition | VideoRendition,
>(
  startTimeMs: number,
  endTimeMs: number,
  durationMs: number,
  rendition: T,
  computeSegmentId: (
    timeMs: number,
    rendition: T,
  ) => Promise<number | undefined>,
): Promise<number[]> => {
  const segments: number[] = [];
  const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;

  // Calculate segment indices that overlap with [startTimeMs, endTimeMs]
  const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);
  const endSegmentIndex = Math.floor(
    Math.min(endTimeMs, durationMs) / segmentDurationMs,
  );

  for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
    const timeMs = i * segmentDurationMs;
    if (timeMs < durationMs) {
      const segmentId = await computeSegmentId(timeMs, rendition);
      if (segmentId !== undefined) {
        segments.push(segmentId);
      }
    }
  }

  return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates
};

/**
 * Compute buffer queue based on current state and desired segments
 * Pure function - determines what segments should be fetched
 */
export const computeBufferQueue = (
  desiredSegments: number[],
  activeRequests: Set<number>,
  cachedSegments: Set<number>,
): number[] => {
  return desiredSegments.filter(
    (segmentId) =>
      !activeRequests.has(segmentId) && !cachedSegments.has(segmentId),
  );
};

/**
 * Handle seek time change and recompute buffer queue
 * Pure function - computes new queue when seek time changes
 */
export const handleSeekTimeChange = <T extends AudioRendition | VideoRendition>(
  newSeekTimeMs: number,
  bufferDurationMs: number,
  rendition: T,
  currentState: MediaBufferState,
  computeSegmentId: (timeMs: number, rendition: T) => number | undefined,
): { newQueue: number[]; overlappingRequests: number[] } => {
  const endTimeMs = newSeekTimeMs + bufferDurationMs;
  const desiredSegments = computeSegmentRange(
    newSeekTimeMs,
    endTimeMs,
    rendition,
    computeSegmentId,
  );

  // Find segments that are already being requested
  const overlappingRequests = desiredSegments.filter((segmentId) =>
    currentState.activeRequests.has(segmentId),
  );

  const newQueue = computeBufferQueue(
    desiredSegments,
    currentState.activeRequests,
    currentState.cachedSegments,
  );

  return { newQueue, overlappingRequests };
};

/**
 * Check if a specific segment is cached in the buffer
 * Pure function for accessing buffer cache state
 */
export const getCachedSegment = (
  segmentId: number,
  bufferState: MediaBufferState | undefined,
): boolean => {
  return bufferState?.cachedSegments.has(segmentId) ?? false;
};

/**
 * Get cached segments from a list of segment IDs
 * Pure function that returns which segments are available in cache
 */
export const getCachedSegments = (
  segmentIds: number[],
  bufferState: MediaBufferState | undefined,
): Set<number> => {
  if (!bufferState) {
    return new Set();
  }
  return new Set(segmentIds.filter((id) => bufferState.cachedSegments.has(id)));
};

/**
 * Get missing segments from a list of segment IDs
 * Pure function that returns which segments need to be fetched
 */
export const getMissingSegments = (
  segmentIds: number[],
  bufferState: MediaBufferState | undefined,
): number[] => {
  if (!bufferState) {
    return segmentIds;
  }
  return segmentIds.filter((id) => !bufferState.cachedSegments.has(id));
};

/**
 * Core media buffering logic with explicit dependencies
 * Generic implementation that works for both audio and video
 */
export const manageMediaBuffer = async <
  T extends AudioRendition | VideoRendition,
>(
  seekTimeMs: number,
  config: MediaBufferConfig,
  currentState: MediaBufferState,
  durationMs: number,
  signal: AbortSignal,
  deps: MediaBufferDependencies<T>,
): Promise<MediaBufferState> => {
  if (!config.enableBuffering) {
    return currentState;
  }

  const rendition = await deps.getRendition();
  const endTimeMs = seekTimeMs + config.bufferDurationMs;

  const desiredSegments = await computeSegmentRangeAsync(
    seekTimeMs,
    endTimeMs,
    durationMs,
    rendition,
    deps.computeSegmentId,
  );

  const newQueue = computeBufferQueue(
    desiredSegments,
    currentState.activeRequests,
    currentState.cachedSegments,
  );

  // Start fetching segments up to maxParallelFetches limit
  const segmentsToFetch = newQueue.slice(0, config.maxParallelFetches);
  const newActiveRequests = new Set(currentState.activeRequests);
  const newCachedSegments = new Set(currentState.cachedSegments);

  // Function to start next individual segment when a slot becomes available
  const startNextSegment = (remainingQueue: number[]): void => {
    if (remainingQueue.length === 0 || signal.aborted) return;

    const availableSlots = config.maxParallelFetches - newActiveRequests.size;
    if (availableSlots <= 0) return;

    const nextSegmentId = remainingQueue[0];
    if (nextSegmentId === undefined) return;

    if (
      newActiveRequests.has(nextSegmentId) ||
      newCachedSegments.has(nextSegmentId)
    ) {
      // Skip this segment and try the next
      startNextSegment(remainingQueue.slice(1));
      return;
    }

    newActiveRequests.add(nextSegmentId);

    deps
      .fetchSegment(nextSegmentId, rendition)
      .then(() => {
        if (signal.aborted) return;
        newActiveRequests.delete(nextSegmentId);
        newCachedSegments.add(nextSegmentId);
        startNextSegment(remainingQueue.slice(1));
      })
      .catch((error) => {
        if (signal.aborted) return;
        newActiveRequests.delete(nextSegmentId);
        deps.logError(`Failed to fetch segment ${nextSegmentId}`, error);
        startNextSegment(remainingQueue.slice(1));
      });
  };

  // Start fetch promises for new segments
  for (const segmentId of segmentsToFetch) {
    if (signal.aborted) break;

    newActiveRequests.add(segmentId);

    // Start fetch (don't await - let it run in background)
    deps
      .fetchSegment(segmentId, rendition)
      .then(() => {
        if (signal.aborted) return;
        // On success, move from active to cached
        newActiveRequests.delete(segmentId);
        newCachedSegments.add(segmentId);

        // Continue buffering if there are more segments needed and continuous buffering is enabled
        if (config.enableContinuousBuffering ?? true) {
          const remainingQueue = newQueue.slice(segmentsToFetch.length);
          startNextSegment(remainingQueue);
        }
      })
      .catch((error) => {
        if (signal.aborted) return;
        // On error, remove from active and continue
        newActiveRequests.delete(segmentId);
        deps.logError(`Failed to fetch segment ${segmentId}`, error);

        // Continue buffering even after error if continuous buffering is enabled
        if (config.enableContinuousBuffering ?? true) {
          const remainingQueue = newQueue.slice(segmentsToFetch.length);
          startNextSegment(remainingQueue);
        }
      });
  }

  return {
    currentSeekTimeMs: seekTimeMs,
    activeRequests: newActiveRequests,
    cachedSegments: newCachedSegments,
    requestQueue: newQueue.slice(segmentsToFetch.length), // Remaining queue
  };
};
