import { DatePipe, NgFor, NgIf } from '@angular/common'
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  ViewChild,
  inject
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import type Hls from 'hls.js'
import { Subscription, firstValueFrom } from 'rxjs'
import { LiveApiService } from '../+schedule-live/live-api-service'
import { LocalStorageService } from '../core/wrappers/storage.service'
import { LiveChatMessage, LiveChatService } from '../+live-stream/live-chat.service'
import { StreamCardsComponent } from './stream-cards2.component'

@Component({
  selector: 'my-live',
  standalone: true,
  templateUrl: './live-stream2.component.html',
  styleUrls: [ './live-stream2.component.scss' ],
  imports: [ NgIf, NgFor, FormsModule, DatePipe, StreamCardsComponent ]
})
export class LiveStreamComponent implements OnInit, AfterViewInit, OnDestroy {
  private static readonly CHAT_USERNAME_LS_KEY = 'live-chat-username'

  private route = inject(ActivatedRoute)
  private router = inject(Router)
  private liveApi = inject(LiveApiService)
  private liveChat = inject(LiveChatService)
  private localStorage = inject(LocalStorageService)

  @ViewChild('nativeVideo')
  set nativeVideoRef (value: ElementRef<HTMLVideoElement>) {
    if (!value) return

    this.nativeVideo = value

    this.nativeVideo.nativeElement.addEventListener('pause', () => {
      this.nativeVideo?.nativeElement.play().catch((): void => undefined)
    })

    if (this.playerUrl && !this.error) {
      setTimeout(() => this.attachPlayer(), 0)
    }
  }

  @ViewChild('chatMessageInput') chatMessageInput?: ElementRef<HTMLInputElement>
  @ViewChild('chatMessagesContainer') chatMessagesContainer?: ElementRef<HTMLDivElement>

  private nativeVideo?: ElementRef<HTMLVideoElement>
  private hls?: Hls
  private viewReady = false
  private readonly chatSubscriptions: Subscription[] = []
  private shouldStickChatToBottom = true
  private statusRefreshTimer: any
  nowTimestamp = Date.now()

  streamTitles: Record<string, string> = {}
  streamThumbnails: Record<string, string> = {}
  streamStartTimes: Record<string, string> = {}
  streamEndTimes: Record<string, string> = {}

  streams: string[] = []
  cameraId = ''
  playerUrl = ''
  error = ''
  loadingStreams = true
  showPlayer = false
  showUsernamePrompt = false
  usernameDraft = ''
  chatUsername = ''
  chatMessages: LiveChatMessage[] = []
  chatMessage = ''
  chatPresenceCount = 0
  chatError = ''
  showEmojiPicker = false
  readonly chatEmojiOptions = [
    '😀', '😁', '😂', '😊', '😍', '😘',
    '😎', '🤩', '🥳', '😇', '🤔', '😴',
    '👍', '👏', '🙌', '🙏', '💯', '🔥',
    '🎉', '❤️', '💙', '🚀', '👀', '✅'
  ]
  private countdownInterval: any

  ngOnInit () {
    this.initChatSubscriptions()
    void this.loadStreams()
    this.countdownInterval = setInterval(() => {
      this.nowTimestamp = Date.now()
    }, 1000)

    this.route.paramMap.subscribe(params => {
      void this.handleRouteChange(params.get('cameraId'))
    })
  }

  ngAfterViewInit () {
    this.viewReady = true
  }

  ngOnDestroy () {
    this.destroyHlsPlayer()
    this.liveChat.disconnect()
    clearInterval(this.countdownInterval)
    this.chatSubscriptions.forEach(subscription => subscription.unsubscribe())
    clearTimeout(this.statusRefreshTimer)
  }

  async loadStream () {
    const shouldAttachPlayer = this.updatePlayerUrl()

    if (!shouldAttachPlayer) {
      this.destroyHlsPlayer()
      return
    }

    await this.attachPlayer()
  }

  selectStream (cameraId: string) {
    this.router.navigate([ '/live-stream-via-solace', cameraId ])
  }
  get visibleStreams(): string[] {
    return this.streams.filter(stream => {
      const timing = this.getStreamTiming(stream)
      if (!timing) return false
  
      return this.nowTimestamp < timing.endTime
    })
  }
  joinChat () {
    const username = this.usernameDraft.trim().replace(/\s+/g, ' ')

    if (username.length < 2 || username.length > 40) {
      this.chatError = 'Username must be between 2 and 40 characters.'
      return
    }

    this.chatUsername = username
    this.chatError = ''
    this.showUsernamePrompt = false
    this.localStorage.setItem(LiveStreamComponent.CHAT_USERNAME_LS_KEY, username)

    void this.liveChat.joinRoom(this.cameraId, username)
  }

  sendChatMessage () {
    if (!this.chatUsername) {
      this.chatError = 'Add your name to join chat and send messages.'
      return
    }

    const text = this.chatMessage.trim()
    if (!text) return

    this.chatError = ''
    this.liveChat.sendMessage(text)
    this.chatMessage = ''
    this.showEmojiPicker = false
  }

  reopenUsernamePrompt () {
    this.usernameDraft = this.chatUsername || this.usernameDraft
    this.chatError = ''
    this.showUsernamePrompt = true
  }

  get canSendChat () {
    return !!this.chatUsername
  }

  closeUsernamePrompt () {
    this.showUsernamePrompt = false
  }

  onChatScroll () {
    const container = this.chatMessagesContainer?.nativeElement
    if (!container) return

    const threshold = 24
    const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
    this.shouldStickChatToBottom = distanceFromBottom <= threshold
  }

  toggleEmojiPicker () {
    if (!this.canSendChat || this.showUsernamePrompt) return

    this.showEmojiPicker = !this.showEmojiPicker
  }

  addEmoji (emoji: string) {
    const input = this.chatMessageInput?.nativeElement

    if (!input) {
      this.chatMessage += emoji
      this.showEmojiPicker = false
      return
    }

    const start = input.selectionStart ?? this.chatMessage.length
    const end = input.selectionEnd ?? this.chatMessage.length

    this.chatMessage = this.chatMessage.slice(0, start) + emoji + this.chatMessage.slice(end)
    this.showEmojiPicker = false

    queueMicrotask(() => {
      input.focus()
      const cursor = start + emoji.length
      input.setSelectionRange(cursor, cursor)
    })
  }

  get streamTitle (): string {
    return this.streamTitles[this.cameraId] || this.cameraId
  }

  isStreamLive = (cameraId: string): boolean => {
    const timing = this.getStreamTiming(cameraId)
    if (!timing) return false

    return this.nowTimestamp >= timing.startTime && this.nowTimestamp < timing.endTime
  }

  getStreamStartTime = (cameraId: string): string => {
    const start = this.streamStartTimes[cameraId]
    if (!start) return ''

    return this.formatStreamDateTime(start)
  }

  private async handleRouteChange (cameraId: string | null) {
    if (!cameraId) {
      this.showPlayer = false
      this.showUsernamePrompt = false
      this.cameraId = ''
      this.playerUrl = ''
      this.error = ''
      this.liveChat.leaveRoom()
      this.destroyHlsPlayer()
      this.scheduleStatusRefresh()
      return
    }

    this.cameraId = cameraId.trim()
    this.showPlayer = true

    const loaded = await this.ensureCameraLoaded(this.cameraId)
    this.scheduleStatusRefresh()
    this.joinChatAsViewer()

    if (!loaded) {
      this.destroyHlsPlayer()
      return
    }

    await this.loadStream()
  }

  private async loadStreams () {
    this.loadingStreams = true

    try {
      const result = await firstValueFrom(this.liveApi.listLiveVideos({
        pagination: {
          start: 0,
          count: 100
        }
      }))

      const orderedStreams: string[] = []
      for (const video of result.data || []) {
        const cameraId = this.applyLiveVideo(video)
        if (!orderedStreams.includes(cameraId)) orderedStreams.push(cameraId)
      }

      this.streams = orderedStreams
      this.nowTimestamp = Date.now()
      this.scheduleStatusRefresh()

      if (this.showPlayer && this.cameraId) {
        await this.ensureCameraLoaded(this.cameraId)
        await this.loadStream()
      }
    } catch (err) {
      this.error = 'Could not load live videos.'
    } finally {
      this.loadingStreams = false
    }
  }

  getCountdown(cameraId: string): string {
    const timing = this.getStreamTiming(cameraId)
    if (!timing) return ''
  
    const diff = timing.startTime - this.nowTimestamp
  
    if (diff <= 0) return 'Starting...'
  
    const totalSeconds = Math.floor(diff / 1000)
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
  
    const pad = (n: number) => n.toString().padStart(2, '0')
  
    return `Starts in ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
  }

  private async ensureCameraLoaded (cameraId: string) {
    if (this.streamStartTimes[cameraId]) return true
    if (!cameraId) {
      this.error = 'Invalid camera id.'
      return false
    }

    try {
      const result = await firstValueFrom(this.liveApi.listLiveVideos({
        pagination: {
          start: 0,
          count: 1
        },
        targetCamera: cameraId
      }))

      const video = result.data?.[0]
      if (!video) {
        this.error = 'Could not load this scheduled live camera.'
        return false
      }

      this.applyLiveVideo(video)
      return true
    } catch (err) {
      this.error = 'Could not load this scheduled live camera.'
      return false
    }
  }

  private applyLiveVideo (video: any) {
    const cameraId = video.targetCamera

    this.streamTitles[cameraId] = video.title
    this.streamThumbnails[cameraId] = video.thumbnailFilename
      ? '/lazy-static/thumbnails/' + video.thumbnailFilename
      : 'assets/images/default.png'
    this.streamStartTimes[cameraId] = video.startTime
    this.streamEndTimes[cameraId] = video.endTime

    return cameraId
  }

  private updatePlayerUrl () {
    const trimmedCameraId = this.cameraId.trim()

    if (!trimmedCameraId) {
      this.error = 'Please provide a camera id.'
      this.playerUrl = ''
      return false
    }

    const timing = this.getStreamTiming(trimmedCameraId)
    if (!timing) {
      this.error = 'This stream does not have a valid schedule.'
      this.playerUrl = ''
      return false
    }

    const { startTime, endTime } = timing
    const now = this.nowTimestamp

    if (now < startTime) {
      this.error = 'This stream will go live at ' + this.formatStreamDateTime(this.streamStartTimes[trimmedCameraId]) + '.'
      this.playerUrl = ''
      return false
    }

    if (now >= endTime) {
      this.error = 'This stream is no longer live.'
      this.playerUrl = ''
      return false
    }

    this.error = ''
    this.cameraId = trimmedCameraId
    this.playerUrl = '/api/v1/live/scheduled-streams/' + encodeURIComponent(trimmedCameraId) + '/index.m3u8'
    return true
  }

  private async attachPlayer () {
    if (!this.viewReady || !this.nativeVideo || this.error) return

    this.destroyHlsPlayer()

    const video = this.nativeVideo.nativeElement
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)

    if (isSafari) {
      video.src = this.playerUrl
      video.muted = true
      void video.play().catch((): void => undefined)
      return
    }

    const { default: HlsJs } = await import('hls.js')
    if (!HlsJs.isSupported()) {
      this.error = 'HLS is not supported by this browser.'
      return
    }

    this.hls = new HlsJs({
      lowLatencyMode: true,
      liveSyncDurationCount: 2,
      liveMaxLatencyDurationCount: 5,
      maxLiveSyncPlaybackRate: 1.5,
      backBufferLength: 15
    })

    this.hls.on(HlsJs.Events.ERROR, (_event, data) => {
      if (!data?.fatal) return

      if (data.type === HlsJs.ErrorTypes.NETWORK_ERROR) {
        this.hls?.startLoad()
        return
      }

      if (data.type === HlsJs.ErrorTypes.MEDIA_ERROR) {
        this.hls?.recoverMediaError()
        return
      }

      this.error = 'HLS error: ' + data.type + ' / ' + data.details
    })

    this.hls.loadSource(this.playerUrl)
    this.hls.attachMedia(video)

    void video.play().catch((): void => undefined)
  }

  private destroyHlsPlayer () {
    if (this.hls) {
      this.hls.destroy()
      this.hls = undefined
    }

    if (!this.nativeVideo) return

    const video = this.nativeVideo.nativeElement
    video.pause()
    video.removeAttribute('src')
    video.load()
  }

  private scrollChatToBottom () {
    const container = this.chatMessagesContainer?.nativeElement
    if (!container) return

    container.scrollTop = container.scrollHeight
  }

  private initChatSubscriptions () {
    this.chatSubscriptions.push(
      this.liveChat.messages$.subscribe(messages => {
        this.chatMessages = messages
        if (this.shouldStickChatToBottom) {
          this.scrollChatToBottomAfterRender()
        }
      })
    )

    this.chatSubscriptions.push(
      this.liveChat.presence$.subscribe(count => {
        this.chatPresenceCount = count
      })
    )

    this.chatSubscriptions.push(
      this.liveChat.errors$.subscribe(message => {
        this.chatError = message
      })
    )
  }

  private joinChatAsViewer () {
    this.liveChat.leaveRoom()
    this.chatMessages = []
    this.chatMessage = ''
    this.chatPresenceCount = 0
    this.chatError = ''
    this.showEmojiPicker = false
    this.shouldStickChatToBottom = true
    this.chatUsername = this.localStorage.getItem<string>(LiveStreamComponent.CHAT_USERNAME_LS_KEY) || ''
    this.usernameDraft = this.chatUsername
    this.showUsernamePrompt = false

    void this.liveChat.joinRoom(this.cameraId, this.chatUsername || undefined)
  }

  private scrollChatToBottomAfterRender () {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => this.scrollChatToBottom())
    })
  }

  private getStreamTiming (cameraId: string) {
    const start = this.streamStartTimes[cameraId]
    const end = this.streamEndTimes[cameraId]

    if (!start || !end) return undefined

    const startTime = new Date(start).getTime()
    const endTime = new Date(end).getTime()

    if (Number.isNaN(startTime) || Number.isNaN(endTime)) return undefined

    return { startTime, endTime }
  }

  private scheduleStatusRefresh () {
    clearTimeout(this.statusRefreshTimer)

    const nextBoundary = this.getNextScheduleBoundary()
    if (nextBoundary === undefined) return

    const delay = Math.max(0, nextBoundary - Date.now() + 1000)

    this.statusRefreshTimer = setTimeout(() => {
      this.nowTimestamp = Date.now()

      if (this.showPlayer && this.cameraId) {
        void this.loadStream()
      }

      this.scheduleStatusRefresh()
    }, delay)
  }

  private getNextScheduleBoundary () {
    const now = Date.now()
    const streamsToCheck = this.showPlayer && this.cameraId
      ? [ this.cameraId ]
      : Object.keys(this.streamStartTimes)

    const boundaries = streamsToCheck
      .flatMap(cameraId => {
        const timing = this.getStreamTiming(cameraId)
        return timing ? [ timing.startTime, timing.endTime ] : []
      })
      .filter(time => time > now)
      .sort((a, b) => a - b)

    return boundaries[0]
  }

  private formatStreamDateTime (value: string) {
    return new Date(value).toLocaleString([], {
      dateStyle: 'medium',
      timeStyle: 'short'
    })
  }
}
