import type {
  AudioRendition,
  MediaEngine,
  RenditionId,
  VideoRendition,
} from "../../transcoding/types";
import type { ManifestResponse } from "../../transcoding/types/index.js";
import type { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
import type { EFMedia } from "../EFMedia.js";
import { BaseMediaEngine } from "./BaseMediaEngine";

export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
  private urlGenerator: UrlGenerator;
  private data: ManifestResponse = {} as ManifestResponse;

  static async fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string) {
    const engine = new JitMediaEngine(host, urlGenerator);
    const data = await engine.fetchManifest(url);
    engine.data = data;
    return engine;
  }

  constructor(host: EFMedia, urlGenerator: UrlGenerator) {
    super(host);
    this.urlGenerator = urlGenerator;
  }

  get durationMs() {
    return this.data.durationMs;
  }

  get src() {
    return this.data.sourceUrl;
  }

  get audioRendition(): AudioRendition | undefined {
    if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {
      return undefined;
    }

    const rendition = this.data.audioRenditions[0];
    if (!rendition) return undefined;

    return {
      id: rendition.id as RenditionId,
      trackId: undefined,
      src: this.data.sourceUrl,
      segmentDurationMs: rendition.segmentDurationMs,
      segmentDurationsMs: rendition.segmentDurationsMs,
    };
  }

  get videoRendition(): VideoRendition | undefined {
    if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {
      return undefined;
    }

    const rendition = this.data.videoRenditions[0];
    if (!rendition) return undefined;

    return {
      id: rendition.id as RenditionId,
      trackId: undefined,
      src: this.data.sourceUrl,
      segmentDurationMs: rendition.segmentDurationMs,
      segmentDurationsMs: rendition.segmentDurationsMs,
    };
  }

  get templates() {
    return this.data.endpoints;
  }

  async fetchInitSegment(
    rendition: { id?: RenditionId; trackId: number | undefined; src: string },
    signal: AbortSignal,
  ) {
    if (!rendition.id) {
      throw new Error("Rendition ID is required for JIT metadata");
    }
    const url = this.urlGenerator.generateSegmentUrl(
      "init",
      rendition.id,
      this,
    );

    // Use unified fetch method
    return this.fetchMedia(url, signal);
  }

  async fetchMediaSegment(
    segmentId: number,
    rendition: { id?: RenditionId; trackId: number | undefined; src: string },
  ) {
    if (!rendition.id) {
      throw new Error("Rendition ID is required for JIT metadata");
    }
    const url = this.urlGenerator.generateSegmentUrl(
      segmentId,
      rendition.id,
      this,
    );
    return this.fetchMedia(url);
  }

  computeSegmentId(
    desiredSeekTimeMs: number,
    rendition: VideoRendition | AudioRendition,
  ) {
    // Don't request segments beyond the actual file duration
    // Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)
    if (desiredSeekTimeMs > this.durationMs) {
      return undefined;
    }

    // Use actual segment durations if available (more accurate)
    if (
      rendition.segmentDurationsMs &&
      rendition.segmentDurationsMs.length > 0
    ) {
      let cumulativeTime = 0;

      for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
        const segmentDuration = rendition.segmentDurationsMs[i];
        if (segmentDuration === undefined) {
          throw new Error("Segment duration is required for JIT metadata");
        }
        const segmentStartMs = cumulativeTime;
        const segmentEndMs = cumulativeTime + segmentDuration;

        // Check if the desired seek time falls within this segment
        // Special case: for the last segment, include the exact end time
        const isLastSegment = i === rendition.segmentDurationsMs.length - 1;
        const includesEndTime =
          isLastSegment && desiredSeekTimeMs === this.durationMs;

        if (
          desiredSeekTimeMs >= segmentStartMs &&
          (desiredSeekTimeMs < segmentEndMs || includesEndTime)
        ) {
          return i + 1; // Convert 0-based to 1-based segment ID
        }

        cumulativeTime += segmentDuration;

        // If we've reached or exceeded file duration, stop
        if (cumulativeTime >= this.durationMs) {
          break;
        }
      }

      // If we didn't find a segment, return undefined
      return undefined;
    }

    // Fall back to fixed duration calculation for backward compatibility
    if (!rendition.segmentDurationMs) {
      throw new Error("Segment duration is required for JIT metadata");
    }

    const segmentIndex = Math.floor(
      desiredSeekTimeMs / rendition.segmentDurationMs,
    );

    // Calculate the actual segment start time
    const segmentStartMs = segmentIndex * rendition.segmentDurationMs;

    // If this segment would start at or beyond file duration, it doesn't exist
    if (segmentStartMs >= this.durationMs) {
      return undefined;
    }

    return segmentIndex + 1; // Convert 0-based to 1-based
  }

  getScrubVideoRendition(): VideoRendition | undefined {
    if (!this.data.videoRenditions) return undefined;

    const scrubManifestRendition = this.data.videoRenditions.find(
      (r) => r.id === "scrub",
    );

    if (!scrubManifestRendition) return this.videoRendition; // Fallback to main

    return {
      id: scrubManifestRendition.id as any,
      trackId: undefined,
      src: this.src,
      segmentDurationMs: scrubManifestRendition.segmentDurationMs,
      segmentDurationsMs: scrubManifestRendition.segmentDurationsMs,
    };
  }
}
