import { VideoViewEvent } from '@peertube/peertube-models'
import { sha256 } from '@peertube/peertube-node-utils'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
import { VideoScope, VideoViewerCounters, VideoViewerStats, VideoViews, ViewerScope } from './shared/index.js'

/**
 * If processing a local view:
 *  - We update viewer information (segments watched, watch time etc)
 *  - We add +1 to video viewers counter if this is a new viewer
 *  - We add +1 to video views counter if this is a new view and if the user watched enough seconds
 *  - We send AP message to notify about this viewer and this view
 *  - We update last video time for the user if authenticated
 *
 * If processing a remote view:
 *  - We add +1 to video viewers counter
 *  - We add +1 to video views counter
 *
 * A viewer is a someone that watched one or multiple sections of a video
 * A viewer that watched only a few seconds of a video may not increment the video views counter
 * Viewers statistics are sent to origin instance using the `WatchAction` ActivityPub object
 */

const lTags = loggerTagsFactory('views')

export class VideoViewsManager {
  private static instance: VideoViewsManager

  private videoViewerStats: VideoViewerStats
  private videoViewerCounters: VideoViewerCounters
  private videoViews: VideoViews

  private constructor () {
  }

  init () {
    this.videoViewerStats = new VideoViewerStats()
    this.videoViewerCounters = new VideoViewerCounters()
    this.videoViews = new VideoViews()
  }

  async processLocalView (options: {
    video: MVideoImmutable
    currentTime: number
    ip: string | null
    sessionId?: string
    viewEvent?: VideoViewEvent
    client: string
    operatingSystem: string
    device: string
  }) {
    const { video, ip, viewEvent, currentTime, client, operatingSystem, device } = options

    let sessionId = options.sessionId
    if (!sessionId || CONFIG.VIEWS.VIDEOS.TRUST_VIEWER_SESSION_ID !== true) {
      sessionId = sha256(CONFIG.SECRETS + '-' + ip)
    }

    logger.debug(`Processing local view for ${video.url}, ip ${ip} and session id ${sessionId}.`, lTags())

    await this.videoViewerStats.addLocalViewer({ video, ip, sessionId, viewEvent, currentTime, client, operatingSystem, device })

    const successViewer = await this.videoViewerCounters.addLocalViewer({ video, sessionId })

    // Do it after added local viewer to fetch updated information
    const watchTime = await this.videoViewerStats.getWatchTime(video.id, sessionId)

    const successView = await this.videoViews.addLocalView({ video, watchTime, sessionId })

    return { successView, successViewer }
  }

  async processRemoteView (options: {
    video: MVideo
    viewerId: string | null
    viewerExpires?: Date
    viewerResultCounter?: number
  }) {
    const { video, viewerId, viewerExpires, viewerResultCounter } = options

    logger.debug('Processing remote view for %s.', video.url, { viewerExpires, viewerId, ...lTags() })

    // Viewer
    if (viewerExpires) {
      if (video.remote === false) {
        this.videoViewerCounters.addRemoteViewerOnLocalVideo({ video, viewerId, viewerExpires })
        return
      }

      this.videoViewerCounters.addRemoteViewerOnRemoteVideo({ video, viewerId, viewerExpires, viewerResultCounter })
      return
    }

    // Just a view
    await this.videoViews.addRemoteView({ video })
  }

  getTotalViewersOf (video: MVideo) {
    return this.videoViewerCounters.getTotalViewersOf(video)
  }

  getTotalViewers (options: {
    viewerScope: ViewerScope
    videoScope: VideoScope
  }) {
    return this.videoViewerCounters.getTotalViewers(options)
  }

  buildViewerExpireTime () {
    return this.videoViewerCounters.buildViewerExpireTime()
  }

  processViewerStats () {
    return this.videoViewerStats.processViewerStats()
  }

  static get Instance () {
    return this.instance || (this.instance = new this())
  }
}
