import { HttpStatusCode } from '@peertube/peertube-models'
import { stopScheduledLiveSession } from './live-stream.js'
import { tryAtomicMove } from '@server/helpers/fs.js'
import { createReqFiles } from '@server/helpers/express-utils.js'
import { generateImageFilename } from '@server/helpers/image-utils.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { logger } from '@server/helpers/logger.js'
import { createHash } from 'crypto'
import { CONFIG } from '@server/initializers/config.js'
import { MIMETYPES, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { removeLiveVideoStartJob, syncLiveVideoStartJob } from '@server/lib/live-videos/live-video-jobs.js'
import { LiveVideoProcessManager } from '@server/lib/live-videos/live-video-process-manager.js'
import {
  getLiveVideoBurnedPath,
  getLiveVideoSubtitlePath,
  syncPreprocessedLiveVideo
} from '@server/lib/live-videos/preprocessed-video.js'
import { PeerTubeSocket } from '@server/lib/peertube-socket.js'
import { processImageFromWorker } from '@server/lib/worker/parent-process.js'
import {
  apiRateLimiter,
  asyncMiddleware,
  authenticate,
  paginationValidator,
  setDefaultPagination
} from '@server/middlewares/index.js'
import {
  createLiveVideoValidator,
  deleteLiveVideoValidator,
  getLiveVideoValidator,
  listLiveVideosValidator,
  updateLiveVideoValidator
} from '@server/middlewares/validators/live-videos.js'
import { LiveVideoModel, LiveVideoStatus } from '@server/models/video/live-video.js'
import express from 'express'
import { ensureDir, remove } from 'fs-extra/esm'
import { readFile } from 'fs/promises'
import { basename, extname, join } from 'node:path'
const liveVideosRouter = express.Router()

const reqLiveVideoFiles = createReqFiles(
  [ 'thumbnailfile', 'videofile', 'subtitlefile' ],
  { ...MIMETYPES.IMAGE.MIMETYPE_EXT, ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT }
)

const LIVE_VIDEOS_STORAGE_DIR = join(CONFIG.STORAGE.TMP_PERSISTENT_DIR, 'live-videos')
const SPECIAL_THUMBNAILS_TMP_DIR = join(CONFIG.STORAGE.TMP_DIR, 'special-thumbnails')

liveVideosRouter.use(apiRateLimiter)

liveVideosRouter.get(
  '/',
  paginationValidator,
  setDefaultPagination,
  listLiveVideosValidator,
  asyncMiddleware(listLiveVideos)
)

liveVideosRouter.get(
  '/:id',
  getLiveVideoValidator,
  asyncMiddleware(getLiveVideo)
)

liveVideosRouter.post(
  '/',
  authenticate,
  reqLiveVideoFiles,
  createLiveVideoValidator,
  asyncMiddleware(createLiveVideo)
)

liveVideosRouter.put(
  '/:id',
  authenticate,
  reqLiveVideoFiles,
  updateLiveVideoValidator,
  asyncMiddleware(updateLiveVideo)
)

liveVideosRouter.delete(
  '/:id',
  authenticate,
  deleteLiveVideoValidator,
  asyncMiddleware(deleteLiveVideo)
)

export {
  liveVideosRouter
}

async function listLiveVideos (req: express.Request, res: express.Response) {
  const result = await LiveVideoModel.listForApi({
    start: req.query.start as number,
    count: req.query.count as number,
    targetCamera: req.query.targetCamera as string,
    status: req.query.status as string
  })

  const formatted = getFormattedObjects(result.rows, result.count)

  formatted.data = formatted.data.map(v => attachSubtitlePath(v))

  return res.json(formatted)

}

async function getLiveVideo (req: express.Request, res: express.Response) {
  const liveVideo = (res.locals as any).liveVideo as LiveVideoModel
  const json = liveVideo.toFormattedJSON()
  return res.json(attachSubtitlePath(json))

}

async function createLiveVideo (req: express.Request, res: express.Response) {
  const files = req.files as { [fieldname: string]: Express.Multer.File[] }
  const thumbnailFile = files.thumbnailfile?.[0]
  const videoFile = files.videofile?.[0]
  const subtitleFile = files.subtitlefile?.[0]
  const user = res.locals.oauth.token.User

  const liveVideo = await sequelizeTypescript.transaction(async t => {
    const thumbnail = thumbnailFile
      ? await saveThumbnailToTemporaryFile(thumbnailFile.path)
      : null

    const videoStoragePath = await storeVideoFile(videoFile)
    if (subtitleFile) {
      await storeSubtitleFile(subtitleFile, videoStoragePath)
    }

    await syncPreprocessedLiveVideo(videoStoragePath)

    const created = await LiveVideoModel.create({
      title: req.body.title,
      thumbnailFilename: thumbnail?.filename || null,
      thumbnailData: thumbnail?.data || null,
      thumbnailMimeType: thumbnail?.mimeType || null,
      thumbnailSize: thumbnail?.size || null,
      thumbnailEtag: thumbnail?.etag || null,
      videoOriginalName: basename(videoFile.originalname),
      videoStoragePath,
      videoMimeType: videoFile.mimetype,
      videoSize: videoFile.size,
      startTime: new Date(req.body.startTime),
      endTime: new Date(req.body.endTime),
      targetCamera: req.body.targetCamera,
      status: LiveVideoStatus.SCHEDULED,
      createdByUserId: user.id
    }, {
      transaction: t
    })

    return created
  })

  await syncLiveVideoStartJob(liveVideo, { replace: true })
    .catch(err => logger.error('Cannot sync scheduled live video start job after creation.', { err, liveVideoId: liveVideo.id }))

  await PeerTubeSocket.Instance.closeLiveChatRoom(liveVideo.targetCamera, 'rescheduled')
    .catch(err => logger.error('Cannot reset live chat room after live video creation.', { err, liveVideoId: liveVideo.id, targetCamera: liveVideo.targetCamera }))

    const json = liveVideo.toFormattedJSON()
return res.status(HttpStatusCode.CREATED_201).json(attachSubtitlePath(json))
}

async function updateLiveVideo (req: express.Request, res: express.Response) {
  const liveVideo = (res.locals as any).liveVideo as LiveVideoModel
  const previousTargetCamera = liveVideo.targetCamera
  const previousStartTime = liveVideo.startTime.getTime()
  const previousEndTime = liveVideo.endTime.getTime()
  const previousStatus = liveVideo.status
  const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined
  const thumbnailFile = files?.thumbnailfile?.[0]
  const videoFile = files?.videofile?.[0]
  const subtitleFile = files?.subtitlefile?.[0]

  const updatedLiveVideo = await sequelizeTypescript.transaction(async t => {
    let thumbnailFilename = liveVideo.thumbnailFilename
    let videoStoragePath = liveVideo.videoStoragePath
    let videoOriginalName = liveVideo.videoOriginalName
    let videoMimeType = liveVideo.videoMimeType
    let videoSize = liveVideo.videoSize

    if (thumbnailFile) {
      const thumbnail = await saveThumbnailToTemporaryFile(thumbnailFile.path)
      thumbnailFilename = thumbnail.filename

      Object.assign(liveVideo, {
        thumbnailData: thumbnail.data,
        thumbnailMimeType: thumbnail.mimeType,
        thumbnailSize: thumbnail.size,
        thumbnailEtag: thumbnail.etag
      })
    }

    if (videoFile) {
      await remove(liveVideo.videoStoragePath).catch(() => undefined)
      await remove(getLiveVideoBurnedPath(liveVideo.videoStoragePath)).catch(() => undefined)
      await remove(liveVideo.videoStoragePath + '.srt').catch(() => undefined)
      await remove(liveVideo.videoStoragePath + '.vtt').catch(() => undefined)

      videoStoragePath = await storeVideoFile(videoFile)
      videoOriginalName = basename(videoFile.originalname)
      videoMimeType = videoFile.mimetype
      videoSize = videoFile.size
    }
    if (subtitleFile) {
      if (!videoFile) {
        await remove(liveVideo.videoStoragePath + '.srt').catch(() => undefined)
        await remove(liveVideo.videoStoragePath + '.vtt').catch(() => undefined)
      }

      await storeSubtitleFile(subtitleFile, videoStoragePath)
    }

    if (videoFile || subtitleFile) {
      await syncPreprocessedLiveVideo(videoStoragePath)
    }

    await liveVideo.update({
      title: req.body.title ?? liveVideo.title,
      thumbnailFilename,
      thumbnailData: thumbnailFile ? liveVideo.thumbnailData : liveVideo.thumbnailData,
      thumbnailMimeType: thumbnailFile ? liveVideo.thumbnailMimeType : liveVideo.thumbnailMimeType,
      thumbnailSize: thumbnailFile ? liveVideo.thumbnailSize : liveVideo.thumbnailSize,
      thumbnailEtag: thumbnailFile ? liveVideo.thumbnailEtag : liveVideo.thumbnailEtag,
      videoOriginalName,
      videoStoragePath,
      videoMimeType,
      videoSize,
      startTime: req.body.startTime ? new Date(req.body.startTime) : liveVideo.startTime,
      endTime: req.body.endTime ? new Date(req.body.endTime) : liveVideo.endTime,
      targetCamera: req.body.targetCamera ?? liveVideo.targetCamera,
      status: req.body.status ?? liveVideo.status
    }, {
      transaction: t
    })

    return liveVideo
  })

  const scheduleChanged = previousTargetCamera !== updatedLiveVideo.targetCamera ||
    previousStartTime !== updatedLiveVideo.startTime.getTime() ||
    previousEndTime !== updatedLiveVideo.endTime.getTime()

  if (updatedLiveVideo.status === LiveVideoStatus.SCHEDULED) {
    if (scheduleChanged) {
      await LiveVideoProcessManager.Instance.stop(updatedLiveVideo.id, 'manual')
        .catch(err => logger.error('Cannot stop scheduled live video ffmpeg process after reschedule.', { err, liveVideoId: updatedLiveVideo.id }))

      await stopScheduledLiveSession(previousTargetCamera)
        .catch(err => logger.error('Cannot stop previous scheduled sink stream session after reschedule.', {
          err,
          liveVideoId: updatedLiveVideo.id,
          targetCamera: previousTargetCamera
        }))

      if (previousTargetCamera !== updatedLiveVideo.targetCamera) {
        await stopScheduledLiveSession(updatedLiveVideo.targetCamera)
          .catch(err => logger.error('Cannot stop new scheduled sink stream session before reschedule restart.', {
            err,
            liveVideoId: updatedLiveVideo.id,
            targetCamera: updatedLiveVideo.targetCamera
          }))
      }
    }

    await syncLiveVideoStartJob(updatedLiveVideo, { replace: true })
      .catch(err => logger.error('Cannot sync scheduled live video start job after update.', { err, liveVideoId: updatedLiveVideo.id }))
  } else {
    await removeLiveVideoStartJob(updatedLiveVideo.id)
      .catch(err => logger.error('Cannot remove scheduled live video start job after update.', { err, liveVideoId: updatedLiveVideo.id }))

    await LiveVideoProcessManager.Instance.stop(
      updatedLiveVideo.id,
      updatedLiveVideo.status === LiveVideoStatus.CANCELLED ? 'cancelled' : 'manual'
    )
      .catch(err => logger.error('Cannot stop scheduled live video ffmpeg process after update.', { err, liveVideoId: updatedLiveVideo.id }))

    await stopScheduledLiveSession(updatedLiveVideo.targetCamera)
      .catch(err => logger.error('Cannot stop scheduled sink stream session after update.', { err, liveVideoId: updatedLiveVideo.id, targetCamera: updatedLiveVideo.targetCamera }))

    if (previousTargetCamera !== updatedLiveVideo.targetCamera) {
      await stopScheduledLiveSession(previousTargetCamera)
        .catch(err => logger.error('Cannot stop previous scheduled sink stream session after update.', { err, liveVideoId: updatedLiveVideo.id, targetCamera: previousTargetCamera }))
    }
  }

  const shouldResetChat = previousTargetCamera !== updatedLiveVideo.targetCamera ||
    previousStartTime !== updatedLiveVideo.startTime.getTime() ||
    previousEndTime !== updatedLiveVideo.endTime.getTime() ||
    previousStatus !== updatedLiveVideo.status

  if (!shouldResetChat) {
    return res.json(attachSubtitlePath(updatedLiveVideo.toFormattedJSON()))
  }

  const chatRoomsToClose = new Set<string>([ updatedLiveVideo.targetCamera ])
  if (previousTargetCamera !== updatedLiveVideo.targetCamera) {
    chatRoomsToClose.add(previousTargetCamera)
  }

  for (const targetCamera of chatRoomsToClose) {
    await PeerTubeSocket.Instance.closeLiveChatRoom(
      targetCamera,
      updatedLiveVideo.status === LiveVideoStatus.SCHEDULED ? 'rescheduled' : 'live-ended'
    )
      .catch(err => logger.error('Cannot reset live chat room after live video update.', {
        err,
        liveVideoId: updatedLiveVideo.id,
        targetCamera
      }))
  }

  const json = updatedLiveVideo.toFormattedJSON()
return res.json(attachSubtitlePath(json))
}

function attachSubtitlePath (liveVideo: any) {
  const basePath = liveVideo.videoStoragePath

  if (!basePath) {
    liveVideo.subtitlePath = null
    return liveVideo
  }

  liveVideo.subtitlePath = getLiveVideoSubtitlePath(basePath)

  return liveVideo
}

async function deleteLiveVideo (req: express.Request, res: express.Response) {
  const liveVideo = (res.locals as any).liveVideo as LiveVideoModel

  await removeLiveVideoStartJob(liveVideo.id)
    .catch(err => logger.error('Cannot remove scheduled live video start job before deletion.', { err, liveVideoId: liveVideo.id }))

  await LiveVideoProcessManager.Instance.stop(liveVideo.id, 'deleted')
    .catch(err => logger.error('Cannot stop scheduled live video ffmpeg process before deletion.', { err, liveVideoId: liveVideo.id }))

  await stopScheduledLiveSession(liveVideo.targetCamera)
    .catch(err => logger.error('Cannot stop scheduled sink stream session before deletion.', { err, liveVideoId: liveVideo.id, targetCamera: liveVideo.targetCamera }))

  await PeerTubeSocket.Instance.closeLiveChatRoom(liveVideo.targetCamera, 'deleted')
    .catch(err => logger.error('Cannot reset live chat room before live video deletion.', { err, liveVideoId: liveVideo.id, targetCamera: liveVideo.targetCamera }))

  await sequelizeTypescript.transaction(async t => {
    await liveVideo.destroy({ transaction: t })
  })

  await remove(liveVideo.videoStoragePath).catch(() => undefined)
  await remove(getLiveVideoBurnedPath(liveVideo.videoStoragePath)).catch(() => undefined)
  await remove(liveVideo.videoStoragePath + '.srt').catch(() => undefined)
  await remove(liveVideo.videoStoragePath + '.vtt').catch(() => undefined)


  return res.status(HttpStatusCode.NO_CONTENT_204).end()
}

async function saveThumbnailToTemporaryFile (sourcePath: string) {
  const thumbnailFilename = generateImageFilename('.jpg')
  const destination = join(SPECIAL_THUMBNAILS_TMP_DIR, thumbnailFilename)

  await ensureDir(SPECIAL_THUMBNAILS_TMP_DIR)

  await processImageFromWorker({
    path: sourcePath,
    destination,
    newSize: THUMBNAILS_SIZE,
    keepOriginal: false
  })

  const data = await readFile(destination)
  await remove(destination).catch(() => undefined)

  return {
    filename: thumbnailFilename,
    data,
    size: data.byteLength,
    mimeType: 'image/jpeg',
    etag: createHash('sha256').update(data).digest('hex')
  }
}

async function storeVideoFile (file: Express.Multer.File) {
  const extension = extname(file.originalname) || '.mp4'
  const filename = `${Date.now()}-${basename(file.filename, extname(file.filename))}${extension}`
  const destination = join(LIVE_VIDEOS_STORAGE_DIR, filename)

  await ensureDir(LIVE_VIDEOS_STORAGE_DIR)
  await tryAtomicMove(file.path, destination)

  return destination
}
async function storeSubtitleFile (file: Express.Multer.File, videoPath: string) {
  const extension = extname(file.originalname) || '.srt'
  const destination = videoPath + extension

  await tryAtomicMove(file.path, destination)
  return destination
}
