import { FfmpegInfo } from '@/ffmpeg/ffmpegInfo.js';
import type { FfprobeAudioStream, FfprobeVideoStream } from '@/types/ffmpeg.js';
import type { Maybe, Nullable } from '@/types/util.js';
import dayjs from '@/util/dayjs.js';
import { fileExists } from '@/util/fsUtil.js';
import { isNonEmptyString } from '@/util/index.js';
import { LoggerFactory } from '@/util/logging/LoggerFactory.js';
import { inject, injectable } from 'inversify';
import { filter, find, isEmpty, map } from 'lodash-es';
import type { NonEmptyArray } from 'ts-essentials';
import { StreamDetailsFetcher } from '../StreamDetailsFetcher.ts';
import type {
  AudioStreamDetails,
  ProgramStreamResult,
  VideoStreamDetails,
} from '../types.ts';
import { FileStreamSource, HttpStreamSource } from '../types.ts';

type LocalFileStreamDetailsRequest = {
  path: string;
};

@injectable()
export class LocalFileStreamDetails
  implements StreamDetailsFetcher<LocalFileStreamDetailsRequest>
{
  private logger = LoggerFactory.child({ className: this.constructor.name });

  constructor(@inject(FfmpegInfo) private ffmpegInfo: FfmpegInfo) {}

  async getStream({
    path,
  }: LocalFileStreamDetailsRequest): Promise<Nullable<ProgramStreamResult>> {
    if (!(await fileExists(path))) {
      this.logger.warn('Cannot find file at path: %s', path);
      return null;
    }

    const probeResult = await this.ffmpegInfo.probeFile(path);

    if (!probeResult) {
      return null;
    }

    const videoStream = find(
      probeResult.streams,
      (stream): stream is FfprobeVideoStream => stream.codec_type === 'video',
    );

    let videoDetails: Maybe<VideoStreamDetails>;
    if (videoStream) {
      videoDetails = {
        sampleAspectRatio: isNonEmptyString(videoStream?.sample_aspect_ratio)
          ? videoStream.sample_aspect_ratio
          : '1:1',
        scanType:
          videoStream.field_order === 'interlaced'
            ? 'interlaced'
            : videoStream.field_order === 'progressive'
              ? 'progressive'
              : 'unknown',
        width: videoStream.width,
        height: videoStream.height,
        framerate: videoStream.r_frame_rate ?? undefined,
        displayAspectRatio: videoStream.display_aspect_ratio,
        // chapters
        anamorphic: extractIsAnamorphic(
          videoStream.width,
          videoStream.height,
          videoStream.display_aspect_ratio,
        ),
        pixelFormat: videoStream.pix_fmt,
        bitDepth: videoStream.bits_per_raw_sample,
        bitrate: videoStream.bit_rate,
        codec: videoStream.codec_name,
        profile: videoStream.profile?.toLowerCase(),
        streamIndex: videoStream.index?.toString() ?? '0',
      } satisfies VideoStreamDetails;
    }

    const audioStreamDetails = map(
      filter(
        probeResult.streams,
        (stream): stream is FfprobeAudioStream => stream.codec_type === 'audio',
      ),
      (audioStream) => {
        return {
          bitrate: audioStream.bit_rate ?? undefined,
          channels: audioStream.channels,
          codec: audioStream.codec_name,
          index: audioStream.index.toFixed(),
          language: audioStream.tags?.['language'],
          profile: audioStream.profile,
        } satisfies AudioStreamDetails;
      },
    );

    return {
      streamDetails: {
        videoDetails: videoDetails ? [videoDetails] : undefined,
        audioDetails: isEmpty(audioStreamDetails)
          ? undefined
          : (audioStreamDetails as NonEmptyArray<AudioStreamDetails>),
        duration: dayjs.duration({ seconds: probeResult.format.duration }),
      },
      streamSource: path.startsWith('http')
        ? new HttpStreamSource(path)
        : new FileStreamSource(path),
    };
  }
}

function extractIsAnamorphic(
  width: number,
  height: number,
  aspectRatioString: string,
) {
  const resolutionRatio = width / height;
  const [numS, denS] = aspectRatioString.split(':');
  const num = parseFloat(numS);
  const den = parseFloat(denS);
  if (isNaN(num) || isNaN(den)) {
    return false;
  }

  return Math.abs(resolutionRatio - num / den) > 0.01;
}
