import UIKit
import AVFoundation
import ExpoModulesCore

protocol CameraVideoRecordingDelegate: AnyObject {
  var responsiveWhenOrientationLocked: Bool { get }
  var physicalOrientation: UIDeviceOrientation { get }
  var mirror: Bool { get }
  var appContext: AppContext? { get }
  var videoBitrate: Int? { get }
  var videoStabilizationMode: VideoStabilizationMode { get }
}

class CameraVideoRecording: NSObject, AVCaptureFileOutputRecordingDelegate {
  weak var delegate: CameraVideoRecordingDelegate?

  private var videoRecordedPromise: Promise?
  private var videoCodecType: AVVideoCodecType?
  private var isValidVideoOptions = true

  init(delegate: CameraVideoRecordingDelegate) {
    self.delegate = delegate
    super.init()
  }

  func record(options: CameraRecordingOptions, videoFileOutput: AVCaptureMovieFileOutput, promise: Promise) async {
    guard !videoFileOutput.isRecording && videoRecordedPromise == nil else {
      return
    }

    if let connection = videoFileOutput.connection(with: .video) {
      let orientation = await delegate?.responsiveWhenOrientationLocked == true ?
        delegate?.physicalOrientation ?? .unknown : UIDevice.current.orientation
      connection.videoOrientation = ExpoCameraUtils.videoOrientation(for: orientation)
      await setVideoOptions(options: options, for: connection, videoFileOutput: videoFileOutput, promise: promise)

      if connection.isVideoOrientationSupported && delegate?.mirror == true {
        connection.isVideoMirrored = delegate?.mirror ?? false
      }
    }

    if !isValidVideoOptions {
      return
    }

    guard let appContext = delegate?.appContext else {
      promise.reject(CameraRecordingFailedException())
      return
    }

    let path = FileSystemUtilities.generatePathInCache(appContext, in: "Camera", extension: ".mov")
    let fileUrl = URL(fileURLWithPath: path)
    videoRecordedPromise = promise

    videoFileOutput.startRecording(to: fileUrl, recordingDelegate: self)
  }

  @available(iOS 18.0, *)
  func toggleRecording(videoFileOutput: AVCaptureMovieFileOutput) {
    if videoFileOutput.isRecordingPaused {
      videoFileOutput.resumeRecording()
    } else {
      videoFileOutput.pauseRecording()
    }
  }

  func stopRecording(videoFileOutput: AVCaptureMovieFileOutput?) {
    videoFileOutput?.stopRecording()
  }

  private func setVideoOptions(
    options: CameraRecordingOptions,
    for connection: AVCaptureConnection,
    videoFileOutput: AVCaptureMovieFileOutput,
    promise: Promise
  ) async {
    isValidVideoOptions = true

    if let maxDuration = options.maxDuration {
      videoFileOutput.maxRecordedDuration = CMTime(seconds: maxDuration, preferredTimescale: 1000)
    }

    if let maxFileSize = options.maxFileSize {
      videoFileOutput.maxRecordedFileSize = Int64(maxFileSize)
    }

    let avMode = (delegate?.videoStabilizationMode ?? .auto).toAVCaptureVideoStabilizationMode()
    if connection.isVideoStabilizationSupported {
      connection.preferredVideoStabilizationMode = avMode
    }

    if let codec = options.codec {
      let codecType = codec.codecType()
      if videoFileOutput.availableVideoCodecTypes.contains(codecType) {
        var outputSettings: [String: Any] = [AVVideoCodecKey: codecType]
        if let videoBitrate = delegate?.videoBitrate {
          outputSettings[AVVideoCompressionPropertiesKey] = [AVVideoAverageBitRateKey: videoBitrate]
        }
        videoFileOutput.setOutputSettings(outputSettings, for: connection)
        self.videoCodecType = codecType
      } else {
        promise.reject(CameraRecordingException(options.codec?.rawValue))
        videoRecordedPromise = nil
        isValidVideoOptions = false
      }
    }
  }

  func fileOutput(
    _ output: AVCaptureFileOutput,
    didFinishRecordingTo outputFileURL: URL,
    from connections: [AVCaptureConnection],
    error: Error?
  ) {
    defer {
      videoRecordedPromise = nil
      videoCodecType = nil
    }

    let success = error == nil
      || (error as? NSError)?.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool == true

    if success {
      videoRecordedPromise?.resolve(["uri": outputFileURL.absoluteString])
    } else {
      videoRecordedPromise?.reject(CameraRecordingFailedException())
    }
  }

  func cleanup() {
    videoRecordedPromise?.reject(CameraUnmountedException())
    videoRecordedPromise = nil
    videoCodecType = nil
  }
}
