//
//  LivestreamManager.swift
//  humand
//

import AVFoundation
import Foundation
import StreamVideo
import StreamWebRTC

@objc
class LivestreamManager: NSObject {
  @objc static let shared = LivestreamManager()

  var streamVideo: StreamVideo?
  var streamCall: Call?
  var currentCallingState: String = ""
  var localUserId: String?

  private var participantsObservationTask: Task<Void, Never>?
  private var localParticipantObservationTask: Task<Void, Never>?
  private var activeSpeakersObservationTask: Task<Void, Never>?
  private var dominantSpeakerObservationTask: Task<Void, Never>?
  private var screenSharingObservationTask: Task<Void, Never>?
  private var reconnectionObservationTask: Task<Void, Never>?
  private var callEndObservationTask: Task<Void, Never>?
  private var microphoneObservationTask: Task<Void, Never>?
  private var speakerObservationTask: Task<Void, Never>?
  private var closedCaptionObservationTask: Task<Void, Never>?

  private var isHost: Bool = false
  private var livestreamId: String?
  private var providerCallId: String?
  private func hasActiveLivestream() -> Bool {
    return hasActiveStreamCall() || currentCallingState == LivestreamConstants.CallingState.joining
  }

  override init() {
    super.init()
    currentCallingState = LivestreamConstants.CallingState.idle
  }

  deinit {
    closedCaptionObservationTask?.cancel()
    stopStateObservation()
  }

  // MARK: - Event Emitters

  func emitStateChanged(_ state: String) {
    RNLivestreamManager.emitStateChanged(state)
  }

  func emitParticipantsChanged(_ participants: [[String: Any]], _ totalCount: UInt32) {
    RNLivestreamManager.emitParticipantsChanged(participants, totalCount)
  }

  func emitMediaStateChanged(microphoneEnabled: Bool?, cameraEnabled: Bool?, speakerEnabled: Bool?) {
    RNLivestreamManager.emitMediaStateChanged(microphoneEnabled: microphoneEnabled ?? false, cameraEnabled: cameraEnabled ?? false, speakerEnabled: speakerEnabled ?? false)
  }

  func onCallEndedByRemote() {
    livestreamLog("Livestream ended by host")

    stopStateObservation()

    Task {
      await cleanupStreamClient()

      await MainActor.run {
        self.resetState(emitIdleState: false)
        RNLivestreamManager.emitStateChanged(LivestreamConstants.CallingState.left)
      }
    }
  }

  // MARK: - Public Methods

  @objc func startHostLivestream(id: String, providerCallId: String, cameraEnabled: Bool) {
    livestreamLog("Starting host livestream: \(id)")

    guard !hasActiveLivestream() else {
      livestreamLog("startHostLivestream: Already active or joining, ignoring")
      return
    }

    guard !CallManager.shared.hasActiveCall() else {
      livestreamLog("startHostLivestream: Call is active, cannot start livestream")
      emitError("Cannot start livestream while in a call")
      return
    }

    self.livestreamId = id
    self.providerCallId = providerCallId
    self.isHost = true

    Task {
      await joinLivestream(cameraEnabled: cameraEnabled, isHls: false)
    }
  }

  @objc func joinViewerLivestream(id: String, providerCallId: String, isHls: Bool) {
    livestreamLog("Joining viewer livestream: \(id), isHls: \(isHls)")

    guard !hasActiveLivestream() else {
      livestreamLog("joinViewerLivestream: Already active or joining, ignoring")
      return
    }

    guard !CallManager.shared.hasActiveCall() else {
      livestreamLog("joinViewerLivestream: Call is active, cannot join livestream")
      emitError("Cannot join livestream while in a call")
      return
    }

    self.livestreamId = id
    self.providerCallId = providerCallId
    self.isHost = false

    Task {
      await joinLivestream(cameraEnabled: false, isHls: isHls)
    }
  }

  @objc func leaveLivestream() {
    livestreamLog("leaveLivestream called")

    Task {
      await leaveLivestreamAsync()
    }
  }

  /// Async version of leaveLivestream that can be awaited.
  /// Use this when you need to wait for cleanup to complete before proceeding.
  /// Emits exited state (user left, broadcast still ongoing).
  func leaveLivestreamAsync() async {
    livestreamLog("leaveLivestreamAsync called")

    stopStateObservation()

    await cleanupStreamClient()

    await MainActor.run {
      self.resetState(emitIdleState: false)
      RNLivestreamManager.emitStateChanged(LivestreamConstants.CallingState.exited)
    }
  }

  @objc func stopLivestream() {
    livestreamLog("Stopping livestream")

    Task {
      if isHost {
        do {
          try await streamCall?.stopLive()
          try await streamCall?.end()
        } catch {
          livestreamLog("Error stopping livestream: \(error.localizedDescription)")
        }
      }

      stopStateObservation()
      await cleanupStreamClient()

      await MainActor.run {
        self.resetState(emitIdleState: false)
        RNLivestreamManager.emitStateChanged(LivestreamConstants.CallingState.left)
      }
    }
  }

  // MARK: - Media Controls

  @objc func setMicrophoneEnabled(_ enabled: Bool) {
    Task {
      guard let call = self.streamCall else { return }
      do {
        if enabled {
          try await call.microphone.enable()
        } else {
          try await call.microphone.disable()
        }
        livestreamLog("Microphone \(enabled ? "enabled" : "disabled")")
      } catch {
        livestreamLog("setMicrophoneEnabled error: \(error.localizedDescription)")
      }
    }
  }

  @objc func setCameraEnabled(_ enabled: Bool) {
    Task {
      guard let call = self.streamCall else { return }
      do {
        if enabled {
          try await call.camera.enable()
        } else {
          try await call.camera.disable()
        }
        emitCurrentMediaState()
        await MainActor.run {
          self.emitCurrentParticipants()
        }
      } catch {
        livestreamLog("setCameraEnabled error: \(error.localizedDescription)")
      }
    }
  }

  @objc func switchCamera() {
    Task {
      guard let call = self.streamCall else { return }
      do {
        try await call.camera.flip()
      } catch {
        livestreamLog("switchCamera error: \(error.localizedDescription)")
      }
    }
  }

  @objc func setSpeakerEnabled(_ enabled: Bool) {
    Task {
      guard let call = self.streamCall else { return }
      do {
        if enabled {
          try await call.speaker.enableSpeakerPhone()
        } else {
          try await call.speaker.disableSpeakerPhone()
        }
        livestreamLog("Speaker \(enabled ? "enabled" : "disabled")")
      } catch {
        livestreamLog("setSpeakerEnabled error: \(error.localizedDescription)")
      }
    }
  }

  // MARK: - State

  @MainActor
  @objc func getCurrentState() -> [String: Any] {
    let participants = streamCall?.state.participants.map { participantToDict($0) } ?? []
    let localParticipant = streamCall?.state.localParticipant.map { participantToDict($0) }
    let micEnabled = streamCall?.microphone.status == .enabled
    let speakerEnabled = streamCall?.speaker.status == .enabled

    return [
      "callingState": currentCallingState,
      "participants": participants,
      "localParticipant": localParticipant as Any,
      "microphoneEnabled": micEnabled,
      "cameraEnabled": streamCall?.camera.status == .enabled,
      "speakerEnabled": speakerEnabled,
      "isHost": isHost
    ]
  }

  // MARK: - Private Methods

  private func joinLivestream(cameraEnabled: Bool, isHls: Bool) async {
    livestreamLog("joinLivestream started (isHost=\(isHost), isHls=\(isHls))")

    updateCallingState(LivestreamConstants.CallingState.joining)

    guard let callId = providerCallId else {
      livestreamLog("joinLivestream: Missing provider call ID")
      emitError("Missing provider call ID")
      updateCallingState(LivestreamConstants.CallingState.idle)
      return
    }

    do {
      let tokenFetcher: () async -> (any TokenResponse)? = { [weak self] in
        guard let self = self else { return nil }
        return await self.getLivestreamToken()
      }

      let (client, participantId) = try await CallApiService.shared.createStreamClient(
        existingClient: streamVideo,
        existingUserId: localUserId,
        log: livestreamLog,
        context: "LivestreamManager",
        tokenFetcher: tokenFetcher
      )

      streamVideo = client
      localUserId = participantId

      // Livestream client hijacks InjectedValues; restore call client so CallKit keeps working
      CallManager.shared.restoreCallKitBinding()

      let streamCallObj = client.call(callType: "livestream", callId: callId)

      try await CallApiService.withRetry(logger: livestreamLog) { try await streamCallObj.get() }

      let shouldEnableSpeaker = !isExternalAudioRouteActive()
      livestreamLog("joinLivestream: shouldEnableSpeaker=\(shouldEnableSpeaker), isHost=\(isHost), isHls=\(isHls)")

      if isHost {
        let policy = DefaultAudioSessionPolicy()
        try await streamCallObj.updateAudioSessionPolicy(policy)
        livestreamLog("joinLivestream: Applied DefaultAudioSessionPolicy for host")
      } else {
        let policy = HumandLivestreamViewerPolicy()
        try await streamCallObj.updateAudioSessionPolicy(policy)
        livestreamLog("joinLivestream: Applied HumandLivestreamViewerPolicy for viewer")
      }

      if isHost || !isHls {
        let callSettings = CallSettings(audioOn: true, videoOn: cameraEnabled, speakerOn: shouldEnableSpeaker)
        try await CallApiService.withRetry(logger: livestreamLog) { [self] in try await streamCallObj.join(create: self.isHost, callSettings: callSettings) }
      }

      self.streamCall = streamCallObj
      updateCallingState(LivestreamConstants.CallingState.joined)

      // Enable media after join completes
      if isHost {
        await enableHostMedia(cameraEnabled: cameraEnabled)
      }

      startStateObservation()

      livestreamLog("joinLivestream: Successfully joined")
    } catch {
      livestreamLog("joinLivestream: Error - \(error)")
      emitError(error.localizedDescription)
      await cleanupStreamClient()
      updateCallingState(LivestreamConstants.CallingState.idle)
    }
  }

  private func getLivestreamToken() async -> LivestreamTokenResponse? {
    return await CallApiService.shared.getLivestreamToken(isHost: isHost)
  }

  private func enableHostMedia(cameraEnabled: Bool) async {
    livestreamLog("enableHostMedia started")

    guard let call = streamCall else {
      livestreamLog("enableHostMedia: No active stream call")
      return
    }

    do {
      try await call.microphone.enable()
      try await call.speaker.enableSpeakerPhone()

      if cameraEnabled {
        try await call.camera.enable()
      }

      emitCurrentMediaState()
      livestreamLog("enableHostMedia: Successfully configured")
    } catch {
      livestreamLog("enableHostMedia: Error - \(error.localizedDescription)")
    }
  }

  private func emitError(_ message: String) {
    RNLivestreamManager.emitError(message)
  }

  func startStateObservation() {
    stopStateObservation()

    guard let call = streamCall else { return }

    Task { @MainActor [weak self] in
      await call.updateParticipantsSorting(
        with: [pinned, screenSharing, participantSource(.webRTCUnspecified), joinedAt, userId]
      )
      self?.emitCurrentParticipants()
      self?.emitCurrentMediaState()
    }

    participantsObservationTask = Task { @MainActor [weak self] in
      for await _ in call.state.$participants.values {
        guard let self = self else { break }
        self.emitCurrentParticipants()
      }
    }

    localParticipantObservationTask = Task { @MainActor [weak self] in
      for await localParticipant in call.state.$localParticipant.values {
        guard let self = self else { break }
        if localParticipant != nil {
          livestreamLog("Local participant available")
          self.emitCurrentParticipants()
        }
      }
    }

    activeSpeakersObservationTask = Task { @MainActor [weak self] in
      for await _ in call.state.$activeSpeakers.values {
        guard let self = self else { break }
        self.emitCurrentParticipants()
      }
    }

    dominantSpeakerObservationTask = Task { @MainActor [weak self] in
      for await _ in call.state.$dominantSpeaker.values {
        guard let self = self else { break }
        self.emitCurrentParticipants()
      }
    }

    screenSharingObservationTask = Task { @MainActor [weak self] in
      for await _ in call.state.$screenSharingSession.values {
        guard let self = self else { break }
        self.emitCurrentParticipants()
      }
    }

    reconnectionObservationTask = Task { @MainActor [weak self] in
      for await status in call.state.$reconnectionStatus.values {
        guard let self = self else { break }
        self.handleReconnectionStatus(status)
      }
    }

    callEndObservationTask = Task { @MainActor [weak self] in
      for await endedAt in call.state.$endedAt.values {
        guard let self = self else { break }
        if endedAt != nil {
          livestreamLog("Stream call ended at: \(endedAt!)")
          self.onCallEndedByRemote()
          break
        }
      }
    }

    microphoneObservationTask = Task { @MainActor [weak self] in
      for await _ in call.microphone.$status.values {
        guard let self = self else { break }
        self.emitCurrentMediaState()
      }
    }

    speakerObservationTask = Task { @MainActor [weak self] in
      for await _ in call.speaker.$status.values {
        guard let self = self else { break }
        self.emitCurrentMediaState()
      }
    }

    if !isHost {
      closedCaptionObservationTask = Task { @MainActor [weak self] in
        guard let self = self else { return }
        for await event in call.subscribe(for: ClosedCaptionEvent.self) {
          let text = event.closedCaption.text
          RNLivestreamManager.emitClosedCaption(text, speakerId: event.closedCaption.speakerId ?? "")
        }
      }
    }
  }

  func stopStateObservation() {
    livestreamLog("Stopping state observation")
    participantsObservationTask?.cancel()
    participantsObservationTask = nil
    localParticipantObservationTask?.cancel()
    localParticipantObservationTask = nil
    activeSpeakersObservationTask?.cancel()
    activeSpeakersObservationTask = nil
    dominantSpeakerObservationTask?.cancel()
    dominantSpeakerObservationTask = nil
    screenSharingObservationTask?.cancel()
    screenSharingObservationTask = nil
    reconnectionObservationTask?.cancel()
    reconnectionObservationTask = nil
    callEndObservationTask?.cancel()
    callEndObservationTask = nil
    microphoneObservationTask?.cancel()
    microphoneObservationTask = nil
    speakerObservationTask?.cancel()
    speakerObservationTask = nil
    closedCaptionObservationTask?.cancel()
    closedCaptionObservationTask = nil
  }

  private func handleReconnectionStatus(_ status: ReconnectionStatus) {
    switch status {
    case .reconnecting:
      livestreamLog("Reconnection status: reconnecting")
      updateCallingState(LivestreamConstants.CallingState.reconnecting)
    case .disconnected:
      livestreamLog("Reconnection status: disconnected")
      updateCallingState(LivestreamConstants.CallingState.reconnectingFailed)
    case .connected:
      livestreamLog("Reconnection status: connected")
      if currentCallingState == LivestreamConstants.CallingState.reconnecting {
        updateCallingState(LivestreamConstants.CallingState.joined)
      }
    default:
      break
    }
  }

  func emitCurrentMediaState() {
    Task { @MainActor [weak self] in
      guard let self = self, let call = self.streamCall else { return }
      let micEnabled = call.microphone.status == .enabled
      let camEnabled = call.camera.status == .enabled
      let speakerEnabled = call.speaker.status == .enabled
      self.emitMediaStateChanged(
        microphoneEnabled: micEnabled,
        cameraEnabled: camEnabled,
        speakerEnabled: speakerEnabled
      )
    }
  }

  @MainActor
  private func emitCurrentParticipants() {
    guard let call = streamCall else { return }

    var participantDicts = call.state.participants.map { participantToDict($0) }

    if let localParticipant = call.state.localParticipant {
      let localAlreadyIncluded = call.state.participants.contains { $0.userId == localParticipant.userId }
      if !localAlreadyIncluded {
        participantDicts.insert(participantToDict(localParticipant), at: 0)
      }
    }
    
    emitParticipantsChanged(participantDicts, call.state.participantCount)
  }

  private func resetState(emitIdleState: Bool = true) {
    livestreamLog("resetState")

    livestreamId = nil
    providerCallId = nil
    isHost = false
    stopStateObservation()
    streamCall = nil
    if emitIdleState {
      updateCallingState(LivestreamConstants.CallingState.idle)
    }
  }

  func updateCallingState(_ state: String) {
    currentCallingState = state
    RNLivestreamManager.emitStateChanged(state)
  }

  private func hasActiveStreamCall() -> Bool {
    return streamCall != nil
  }

  func cleanupStreamClient(endCall: Bool = false) async {
    livestreamLog("cleanupStreamClient: Starting cleanup (endCall: \(endCall))")

    stopStateObservation()

    guard let call = streamCall else {
      livestreamLog("cleanupStreamClient: No active call to cleanup")
      return
    }

    streamCall = nil

    do {
      try await call.camera.disable()
      try await call.microphone.disable()
      livestreamLog("cleanupStreamClient: Disabled camera and microphone")
    } catch {
      livestreamLog("cleanupStreamClient: Error disabling media - \(error.localizedDescription)")
    }

    if endCall {
      do {
        try await call.end()
        livestreamLog("cleanupStreamClient: Ended call for all participants")
      } catch {
        livestreamLog("cleanupStreamClient: Error ending call - \(error.localizedDescription)")
      }
    } else {
      call.leave()
      livestreamLog("cleanupStreamClient: Left call")
    }

    await disconnectStreamVideo()
  }

  func disconnectStreamVideo() async {
    guard let client = streamVideo else {
      livestreamLog("disconnectStreamVideo: No client to disconnect")
      return
    }

    livestreamLog("disconnectStreamVideo: Disconnecting livestream client")
    await client.disconnect()
    streamVideo = nil
    localUserId = nil

    // Restore call client in InjectedValues so CallKit keeps working
    CallManager.shared.restoreCallKitBinding()
    livestreamLog("disconnectStreamVideo: done, CallKit binding restored")
  }

  private func participantToDict(_ participant: CallParticipant) -> [String: Any] {
    return [
      "sessionId": participant.sessionId,
      "userId": participant.userId,
      "name": participant.name,
      "profileImageURL": participant.profileImageURL?.absoluteString as Any,
      "isSpeaking": participant.isSpeaking,
      "isDominantSpeaker": participant.isDominantSpeaker,
      "hasVideo": participant.hasVideo,
      "hasAudio": participant.hasAudio,
      "isScreenSharing": participant.isScreensharing,
      "isLocalParticipant": participant.userId == localUserId,
      "connectionQuality": connectionQualityToString(participant.connectionQuality),
      "source": participant.source.stringValue
    ]
  }

  private func connectionQualityToString(_ quality: ConnectionQuality) -> String {
    switch quality {
    case .excellent: return "excellent"
    case .good: return "good"
    case .poor: return "poor"
    default: return "unknown"
    }
  }

  private func isExternalAudioRouteActive() -> Bool {
    let route = AVAudioSession.sharedInstance().currentRoute
    for output in route.outputs {
      switch output.portType {
      case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE, .headphones, .headsetMic:
        return true
      default:
        continue
      }
    }
    return false
  }
}
