import Bluebird from 'bluebird'
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
import { logger } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models/index.js'
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
import {
  HLSTranscodingPayload,
  MergeAudioTranscodingPayload,
  NewWebVideoResolutionTranscodingPayload,
  OptimizeTranscodingPayload,
  VideoTranscodingPayload
} from '@peertube/peertube-models'
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
import { canDoQuickTranscode } from '../../transcoding-quick-transcode.js'
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
import { AbstractJobBuilder } from './abstract-job-builder.js'

export class TranscodingJobQueueBuilder extends AbstractJobBuilder {

  async createOptimizeOrMergeAudioJobs (options: {
    video: MVideoFullLight
    videoFile: MVideoFile
    isNewVideo: boolean
    user: MUserId
    videoFileAlreadyLocked: boolean
  }) {
    const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options

    let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
    let nextTranscodingSequentialJobPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []

    const mutexReleaser = videoFileAlreadyLocked
      ? () => {}
      : await VideoPathManager.Instance.lockFiles(video.uuid)

    try {
      await video.reload()
      await videoFile.reload()

      await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
        const probe = await ffprobePromise(videoFilePath)

        const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
        const hasAudio = await hasAudioStream(videoFilePath, probe)
        const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
        const inputFPS = videoFile.isAudio()
          ? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
          : await getVideoStreamFPS(videoFilePath, probe)

        const maxResolution = await isAudioFile(videoFilePath, probe)
          ? DEFAULT_AUDIO_RESOLUTION
          : buildOriginalFileResolution(resolution)

        if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
          nextTranscodingSequentialJobPayloads.push([
            this.buildHLSJobPayload({
              deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false,

              // We had some issues with a web video quick transcoded while producing a HLS version of it
              copyCodecs: !quickTranscode,

              resolution: maxResolution,
              fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
              videoUUID: video.uuid,
              isNewVideo
            })
          ])
        }

        const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
          video,
          inputVideoResolution: maxResolution,
          inputVideoFPS: inputFPS,
          hasAudio,
          isNewVideo
        })

        nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]

        const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0
        mergeOrOptimizePayload = videoFile.isAudio()
          ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren })
          : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren })
      })
    } finally {
      mutexReleaser()
    }

    const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
      return Bluebird.mapSeries(payloads, payload => {
        return this.buildTranscodingJob({ payload, user })
      })
    })

    const transcodingJobBuilderJob: CreateJobArgument = {
      type: 'transcoding-job-builder',
      payload: {
        videoUUID: video.uuid,
        sequentialJobs: nextTranscodingSequentialJobs
      }
    }

    const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })

    await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])

    await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
  }

  // ---------------------------------------------------------------------------

  async createTranscodingJobs (options: {
    transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
    video: MVideoFullLight
    resolutions: number[]
    isNewVideo: boolean
    user: MUserId | null
  }) {
    const { video, transcodingType, resolutions, isNewVideo } = options

    const maxResolution = Math.max(...resolutions)
    const childrenResolutions = resolutions.filter(r => r !== maxResolution)

    logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })

    const { fps: inputFPS } = await video.probeMaxQualityFile()

    const children = childrenResolutions.map(resolution => {
      const fps = computeOutputFPS({ inputFPS, resolution })

      if (transcodingType === 'hls') {
        return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
      }

      if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
        return this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
      }

      throw new Error('Unknown transcoding type')
    })

    const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })

    const parent = transcodingType === 'hls'
      ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
      : this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })

    // Process the last resolution after the other ones to prevent concurrency issue
    // Because low resolutions use the biggest one as ffmpeg input
    await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
  }

  // ---------------------------------------------------------------------------

  private async createTranscodingJobsWithChildren (options: {
    videoUUID: string
    parent: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)
    children: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)[]
    user: MUserId | null
  }) {
    const { videoUUID, parent, children, user } = options

    const parentJob = await this.buildTranscodingJob({ payload: parent, user })
    const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))

    await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)

    await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
  }

  private async buildTranscodingJob (options: {
    payload: VideoTranscodingPayload
    user: MUserId | null // null means we don't want priority
  }) {
    const { user, payload } = options

    return {
      type: 'video-transcoding' as 'video-transcoding',
      priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: undefined }),
      payload
    }
  }

  private async buildLowerResolutionJobPayloads (options: {
    video: MVideoWithFileThumbnail
    inputVideoResolution: number
    inputVideoFPS: number
    hasAudio: boolean
    isNewVideo: boolean
  }) {
    const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options

    // Create transcoding jobs if there are enabled resolutions
    const resolutionsEnabled = await Hooks.wrapObject(
      computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
      'filter:transcoding.auto.resolutions-to-transcode.result',
      options
    )

    const sequentialPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []

    for (const resolution of resolutionsEnabled) {
      const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })

      if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) {
        const payloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
          this.buildWebVideoJobPayload({
            videoUUID: video.uuid,
            resolution,
            fps,
            isNewVideo
          })
        ]

        // Create a subsequent job to create HLS resolution that will just copy web video codecs
        if (CONFIG.TRANSCODING.HLS.ENABLED) {
          payloads.push(
            this.buildHLSJobPayload({
              videoUUID: video.uuid,
              resolution,
              fps,
              isNewVideo,
              copyCodecs: true
            })
          )
        }

        sequentialPayloads.push(payloads)
      } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
        sequentialPayloads.push([
          this.buildHLSJobPayload({
            videoUUID: video.uuid,
            resolution,
            fps,
            copyCodecs: false,
            isNewVideo
          })
        ])
      }
    }

    return sequentialPayloads
  }

  private buildHLSJobPayload (options: {
    videoUUID: string
    resolution: number
    fps: number
    isNewVideo: boolean
    deleteWebVideoFiles?: boolean // default false
    copyCodecs?: boolean // default false
  }): HLSTranscodingPayload {
    const { videoUUID, resolution, fps, isNewVideo, deleteWebVideoFiles = false, copyCodecs = false } = options

    return {
      type: 'new-resolution-to-hls',
      videoUUID,
      resolution,
      fps,
      copyCodecs,
      isNewVideo,
      deleteWebVideoFiles
    }
  }

  private buildWebVideoJobPayload (options: {
    videoUUID: string
    resolution: number
    fps: number
    isNewVideo: boolean
  }): NewWebVideoResolutionTranscodingPayload {
    const { videoUUID, resolution, fps, isNewVideo } = options

    return {
      type: 'new-resolution-to-web-video',
      videoUUID,
      isNewVideo,
      resolution,
      fps
    }
  }

  private buildMergeAudioPayload (options: {
    videoUUID: string
    isNewVideo: boolean
    hasChildren: boolean
  }): MergeAudioTranscodingPayload {
    const { videoUUID, isNewVideo, hasChildren } = options

    return {
      type: 'merge-audio-to-web-video',
      resolution: DEFAULT_AUDIO_RESOLUTION,
      fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
      videoUUID,
      isNewVideo,
      hasChildren
    }
  }

  private buildOptimizePayload (options: {
    videoUUID: string
    quickTranscode: boolean
    isNewVideo: boolean
    hasChildren: boolean
  }): OptimizeTranscodingPayload {
    const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options

    return {
      type: 'optimize-to-web-video',
      videoUUID,
      isNewVideo,
      hasChildren,
      quickTranscode
    }
  }
}
