import AVFoundation
import Flutter
import UIKit

private enum RecordingState: String {
  case idle
  case previewing
  case recording
  case stopping
  case error
}

private struct RecordingStatus {
  let state: RecordingState
  let outputPath: String?
  let elapsedMillis: Int
  let message: String?

  init(
    state: RecordingState,
    outputPath: String? = nil,
    elapsedMillis: Int = 0,
    message: String? = nil
  ) {
    self.state = state
    self.outputPath = outputPath
    self.elapsedMillis = elapsedMillis
    self.message = message
  }

  func toMap() -> [String: Any] {
    var map: [String: Any] = [
      "state": state.rawValue,
      "elapsedMillis": elapsedMillis,
    ]
    if let outputPath {
      map["outputPath"] = outputPath
    }
    if let message {
      map["message"] = message
    }
    return map
  }
}

private final class RecordingPreviewView: UIView {
  override class var layerClass: AnyClass {
    AVCaptureVideoPreviewLayer.self
  }

  var previewLayer: AVCaptureVideoPreviewLayer {
    layer as! AVCaptureVideoPreviewLayer
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    previewLayer.videoGravity = .resizeAspectFill
    backgroundColor = .black
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    previewLayer.videoGravity = .resizeAspectFill
    backgroundColor = .black
  }
}

private final class RecordingPreviewPlatformView: NSObject, FlutterPlatformView {
  private let previewView: RecordingPreviewView

  init(frame: CGRect) {
    previewView = RecordingPreviewView(frame: frame)
    super.init()
    RecordingCameraController.shared.attach(previewView: previewView)
  }

  func view() -> UIView {
    previewView
  }

  deinit {
    RecordingCameraController.shared.detach(previewView: previewView)
  }
}

private final class RecordingPreviewFactory: NSObject, FlutterPlatformViewFactory {
  func create(
    withFrame frame: CGRect,
    viewIdentifier viewId: Int64,
    arguments args: Any?
  ) -> FlutterPlatformView {
    RecordingPreviewPlatformView(frame: frame)
  }

  func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
    FlutterStandardMessageCodec.sharedInstance()
  }
}

private final class RecordingCameraController: NSObject, AVCaptureFileOutputRecordingDelegate {
  static let shared = RecordingCameraController()

  private let session = AVCaptureSession()
  private let sessionQueue = DispatchQueue(label: "recording.camera.session")
  private let movieOutput = AVCaptureMovieFileOutput()

  private weak var previewView: RecordingPreviewView?
  private var videoInput: AVCaptureDeviceInput?
  private var audioInput: AVCaptureDeviceInput?
  private var configured = false
  private var latestOutputPath: String?
  private var latestFileSaved = true
  private var latestFileErrorMessage: String?
  private var pendingDisplayName: String?
  private var recordingStartedAt: Date?
  private var elapsedTimer: Timer?
  private var pendingStopResult: FlutterResult?
  private var currentZoomRatio: CGFloat = 1.0

  private(set) var status = RecordingStatus(state: .idle) {
    didSet {
      DispatchQueue.main.async { [weak self] in
        guard let self else { return }
        self.statusListener?(self.currentStatusMap())
      }
    }
  }

  var statusListener: (([String: Any]) -> Void)?

  func attach(previewView: RecordingPreviewView) {
    let bindPreview = { [weak self, weak previewView] in
      guard let self, let previewView else { return }
      self.previewView = previewView
      previewView.previewLayer.session = self.session
    }
    if Thread.isMainThread {
      bindPreview()
    } else {
      DispatchQueue.main.async(execute: bindPreview)
    }
  }

  func detach(previewView: RecordingPreviewView) {
    let unbindPreview = { [weak self, weak previewView] in
      guard let self, let previewView else { return }
      if self.previewView === previewView {
        previewView.previewLayer.session = nil
        self.previewView = nil
      }
    }
    if Thread.isMainThread {
      unbindPreview()
    } else {
      DispatchQueue.main.async(execute: unbindPreview)
    }
  }

  func initializePreview(result: @escaping FlutterResult) {
    guard let previewView else {
      result(
        FlutterError(code: "NO_PREVIEW", message: "Camera preview is not attached", details: nil))
      return
    }

    previewView.previewLayer.session = session
    sessionQueue.async { [weak self] in
      guard let self else { return }

      do {
        try self.configureSession(withAudio: self.isMicrophoneAuthorized())
        if !self.session.isRunning {
          self.session.startRunning()
        }
        self.updateStatus(RecordingStatus(state: .previewing))
        DispatchQueue.main.async {
          result(self.currentStatusMap())
        }
      } catch {
        self.updateStatus(RecordingStatus(state: .error, message: error.localizedDescription))
        DispatchQueue.main.async {
          result(
            FlutterError(
              code: "PREVIEW_FAILED",
              message: error.localizedDescription,
              details: nil
            )
          )
        }
      }
    }
  }

  func startRecording(withAudio: Bool, displayName: String?, result: @escaping FlutterResult) {
    guard previewView != nil else {
      result(
        FlutterError(code: "NO_PREVIEW", message: "Camera preview is not attached", details: nil))
      return
    }

    sessionQueue.async { [weak self] in
      guard let self else { return }

      do {
        try self.configureSession(withAudio: withAudio && self.isMicrophoneAuthorized())
        if !self.session.isRunning {
          self.session.startRunning()
        }

        guard !self.movieOutput.isRecording else {
          DispatchQueue.main.async {
            result(FlutterError(code: "START_FAILED", message: "Already recording", details: nil))
          }
          return
        }

        self.pendingDisplayName = displayName
        self.latestFileSaved = true
        self.latestFileErrorMessage = nil
        let outputURL = try self.createOutputURL(displayName: displayName)
        self.latestOutputPath = outputURL.path
        self.recordingStartedAt = Date()
        self.updateStatus(RecordingStatus(state: .recording, outputPath: outputURL.path))
        self.movieOutput.startRecording(to: outputURL, recordingDelegate: self)

        DispatchQueue.main.async {
          self.startElapsedTimer()
          result([
            "outputPath": outputURL.path,
            "status": self.currentStatusMap(),
          ])
        }
      } catch {
        self.updateStatus(RecordingStatus(state: .error, message: error.localizedDescription))
        DispatchQueue.main.async {
          result(
            FlutterError(
              code: "START_FAILED",
              message: error.localizedDescription,
              details: nil
            )
          )
        }
      }
    }
  }

  func stopRecording(result: @escaping FlutterResult) {
    sessionQueue.async { [weak self] in
      guard let self else { return }

      guard self.movieOutput.isRecording else {
        DispatchQueue.main.async {
          var payload: [String: Any] = [
            "outputPath": self.latestOutputPath as Any,
            "status": self.currentStatusMap(),
            "fileSaved": self.latestFileSaved,
          ]
          if !self.latestFileSaved {
            payload["fileErrorMessage"] =
              self.latestFileErrorMessage ?? "保存到文件夹失败"
          }
          result(payload)
        }
        return
      }

      self.pendingStopResult = result
      self.updateStatus(RecordingStatus(state: .stopping, outputPath: self.latestOutputPath))
      self.movieOutput.stopRecording()
    }
  }

  func disposePreview(result: @escaping FlutterResult) {
    sessionQueue.async { [weak self] in
      guard let self else { return }

      if self.movieOutput.isRecording {
        self.movieOutput.stopRecording()
      }
      if self.session.isRunning {
        self.session.stopRunning()
      }
      self.session.beginConfiguration()
      for input in self.session.inputs {
        self.session.removeInput(input)
      }
      for output in self.session.outputs {
        self.session.removeOutput(output)
      }
      self.session.commitConfiguration()
      self.videoInput = nil
      self.audioInput = nil
      self.currentZoomRatio = 1.0
      self.configured = false
      self.updateStatus(RecordingStatus(state: .idle))

      DispatchQueue.main.async {
        self.stopElapsedTimer()
        result(nil)
      }
    }
  }

  func currentStatusMap() -> [String: Any] {
    if status.state == .recording {
      return RecordingStatus(
        state: .recording,
        outputPath: latestOutputPath,
        elapsedMillis: elapsedMillis()
      ).toMap()
    }
    return status.toMap()
  }

  func zoomCapabilities(result: @escaping FlutterResult) {
    sessionQueue.async { [weak self] in
      guard let self else { return }
      let capabilities = self.currentZoomCapabilitiesMap()
      DispatchQueue.main.async {
        result(capabilities)
      }
    }
  }

  func setZoomRatio(_ ratio: CGFloat, result: @escaping FlutterResult) {
    sessionQueue.async { [weak self] in
      guard let self else { return }
      guard let device = self.videoInput?.device else {
        self.currentZoomRatio = max(1.0, ratio)
        let capabilities = self.currentZoomCapabilitiesMap()
        DispatchQueue.main.async {
          result(capabilities)
        }
        return
      }

      do {
        // 入参是显示倍率(1.0x = 主摄)，按 S 基准换算回设备 zoomFactor。
        let baseline = self.mainBaselineFactor(for: device)
        let nextZoom = self.clampedZoomRatio(ratio * baseline, for: device)
        try device.lockForConfiguration()
        device.videoZoomFactor = nextZoom
        device.unlockForConfiguration()
        self.currentZoomRatio = nextZoom
        let capabilities = self.currentZoomCapabilitiesMap()
        DispatchQueue.main.async {
          result(capabilities)
        }
      } catch {
        DispatchQueue.main.async {
          result(
            FlutterError(
              code: "ZOOM_FAILED",
              message: error.localizedDescription,
              details: nil
            )
          )
        }
      }
    }
  }

  func fileOutput(
    _ output: AVCaptureFileOutput,
    didFinishRecordingTo outputFileURL: URL,
    from connections: [AVCaptureConnection],
    error: Error?
  ) {
    let stopResult = pendingStopResult
    pendingStopResult = nil

    if let error {
      latestFileSaved = false
      latestFileErrorMessage = error.localizedDescription
      updateStatus(
        RecordingStatus(
          state: .error, outputPath: latestOutputPath, message: error.localizedDescription))
      finishStopRecording(stopResult: stopResult)
      return
    }

    latestFileSaved = true
    latestFileErrorMessage = nil
    latestOutputPath = outputFileURL.path
    guard FileManager.default.fileExists(atPath: outputFileURL.path) else {
      latestFileSaved = false
      latestFileErrorMessage = "录制文件未生成"
      updateStatus(
        RecordingStatus(
          state: .error,
          outputPath: latestOutputPath,
          message: latestFileErrorMessage
        )
      )
      finishStopRecording(stopResult: stopResult)
      return
    }
    updateStatus(
      RecordingStatus(
        state: .previewing,
        outputPath: latestOutputPath,
        elapsedMillis: elapsedMillis()
      )
    )
    finishStopRecording(stopResult: stopResult)
  }

  private func finishStopRecording(stopResult: FlutterResult?) {
    DispatchQueue.main.async { [weak self] in
      guard let self else { return }
      self.stopElapsedTimer()
      var payload: [String: Any] = [
        "outputPath": self.latestOutputPath as Any,
        "status": self.currentStatusMap(),
        "fileSaved": self.latestFileSaved,
      ]
      if !self.latestFileSaved {
        payload["fileErrorMessage"] =
          self.latestFileErrorMessage ?? "保存到文件夹失败，请检查文件保存权限"
      }
      stopResult?(payload)
    }
  }

  private func configureSession(withAudio: Bool) throws {
    if configured {
      try configureAudioInput(enabled: withAudio)
      return
    }

    guard let videoDevice = Self.preferredVideoDevice() else {
      throw NSError(
        domain: "RecordingCamera", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "No camera device available"])
    }

    let nextVideoInput = try AVCaptureDeviceInput(device: videoDevice)

    session.beginConfiguration()
    session.sessionPreset = .high

    guard session.canAddInput(nextVideoInput) else {
      session.commitConfiguration()
      throw NSError(
        domain: "RecordingCamera", code: 2,
        userInfo: [NSLocalizedDescriptionKey: "Cannot add camera input"])
    }
    session.addInput(nextVideoInput)
    videoInput = nextVideoInput

    guard session.canAddOutput(movieOutput) else {
      session.commitConfiguration()
      throw NSError(
        domain: "RecordingCamera", code: 3,
        userInfo: [NSLocalizedDescriptionKey: "Cannot add movie output"])
    }
    session.addOutput(movieOutput)
    session.commitConfiguration()

    configured = true
    // 默认以主摄(显示 1.0x)开场：虚拟多摄设备里主摄对应的 zoomFactor 是 S。
    currentZoomRatio = mainBaselineFactor(for: videoDevice)
    try applyCurrentZoom()
    try configureAudioInput(enabled: withAudio)
  }

  /// 优先选用包含超广角的虚拟多摄设备，使 minAvailableVideoZoomFactor 能低于主摄(从而支持 0.6x)。
  private static func preferredVideoDevice() -> AVCaptureDevice? {
    let preferredTypes: [AVCaptureDevice.DeviceType] = [
      .builtInTripleCamera,
      .builtInDualWideCamera,
      .builtInWideAngleCamera,
    ]
    for type in preferredTypes {
      if let device = AVCaptureDevice.default(type, for: .video, position: .back) {
        return device
      }
    }
    return AVCaptureDevice.default(for: .video)
  }

  /// 虚拟多摄设备中「主摄(显示 1.0x)」对应的设备 zoomFactor 基准 S。
  /// 取 ultra-wide → wide 的切换点；非虚拟设备无切换点时返回 1.0(向后兼容)。
  private func mainBaselineFactor(for device: AVCaptureDevice) -> CGFloat {
    if let first = device.virtualDeviceSwitchOverVideoZoomFactors.first {
      let value = CGFloat(truncating: first)
      if value > 0 {
        return value
      }
    }
    return 1.0
  }

  private func currentZoomCapabilitiesMap() -> [String: Any] {
    guard let device = videoInput?.device else {
      return [
        "zoomRatio": Double(currentZoomRatio),
        "minZoomRatio": 1.0,
        "maxZoomRatio": 3.0,
      ]
    }

    // 设备 zoomFactor 以 S 为基准换算成 App 使用的「显示倍率」(1.0x = 主摄)。
    let baseline = mainBaselineFactor(for: device)
    let minZoom = device.minAvailableVideoZoomFactor
    let maxZoom = device.maxAvailableVideoZoomFactor
    let zoom = clampedZoomRatio(device.videoZoomFactor, for: device)
    currentZoomRatio = zoom
    return [
      "zoomRatio": Double(zoom / baseline),
      "minZoomRatio": Double(minZoom / baseline),
      "maxZoomRatio": Double(maxZoom / baseline),
    ]
  }

  private func applyCurrentZoom() throws {
    guard let device = videoInput?.device else { return }
    let nextZoom = clampedZoomRatio(currentZoomRatio, for: device)
    try device.lockForConfiguration()
    device.videoZoomFactor = nextZoom
    device.unlockForConfiguration()
    currentZoomRatio = nextZoom
  }

  private func clampedZoomRatio(_ ratio: CGFloat, for device: AVCaptureDevice) -> CGFloat {
    min(max(ratio, device.minAvailableVideoZoomFactor), device.maxAvailableVideoZoomFactor)
  }

  private func configureAudioInput(enabled: Bool) throws {
    session.beginConfiguration()
    defer { session.commitConfiguration() }

    if let audioInput {
      session.removeInput(audioInput)
      self.audioInput = nil
    }

    guard enabled else { return }
    guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }

    let nextAudioInput = try AVCaptureDeviceInput(device: audioDevice)
    if session.canAddInput(nextAudioInput) {
      session.addInput(nextAudioInput)
      audioInput = nextAudioInput
    }
  }

  private func isMicrophoneAuthorized() -> Bool {
    AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
  }

  private func createOutputURL(displayName: String?) throws -> URL {
    let baseURL = try FileManager.default.url(
      for: .documentDirectory,
      in: .userDomainMask,
      appropriateFor: nil,
      create: true
    )
    let recordingsURL = baseURL.appendingPathComponent("recordings", isDirectory: true)
    try FileManager.default.createDirectory(at: recordingsURL, withIntermediateDirectories: true)

    let fileName = Self.resolveFileName(displayName: displayName)
    return uniqueOutputURL(in: recordingsURL, preferredFileName: fileName)
  }

  private func uniqueOutputURL(in directoryURL: URL, preferredFileName: String) -> URL {
    let preferredURL = directoryURL.appendingPathComponent(preferredFileName)
    guard !FileManager.default.fileExists(atPath: preferredURL.path) else {
      let fileExtension = preferredURL.pathExtension
      let baseName = preferredURL.deletingPathExtension().lastPathComponent
      let timestamp = Self.fileNameDateFormatter.string(from: Date())

      var index = 0
      while true {
        let suffix = index == 0 ? timestamp : "\(timestamp)_\(index)"
        let nextName = fileExtension.isEmpty
          ? "\(baseName)_\(suffix)"
          : "\(baseName)_\(suffix).\(fileExtension)"
        let nextURL = directoryURL.appendingPathComponent(nextName)
        if !FileManager.default.fileExists(atPath: nextURL.path) {
          return nextURL
        }
        index += 1
      }
    }
    return preferredURL
  }

  private static func resolveFileName(displayName: String?) -> String {
    let trimmed = displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    if !trimmed.isEmpty {
      let lower = trimmed.lowercased()
      if lower.hasSuffix(".mov") || lower.hasSuffix(".mp4") {
        return trimmed
      }
      return "\(trimmed).mov"
    }
    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.dateFormat = "yyyyMMdd_HHmmss"
    return "REC_\(formatter.string(from: Date())).mov"
  }

  private static let fileNameDateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.dateFormat = "yyyyMMdd_HHmmss"
    return formatter
  }()

  private func updateStatus(_ next: RecordingStatus) {
    status = next
  }

  private func elapsedMillis() -> Int {
    guard let recordingStartedAt else { return 0 }
    return max(0, Int(Date().timeIntervalSince(recordingStartedAt) * 1000))
  }

  private func startElapsedTimer() {
    stopElapsedTimer()
    elapsedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
      guard let self, self.status.state == .recording else { return }
      self.statusListener?(self.currentStatusMap())
    }
  }

  private func stopElapsedTimer() {
    elapsedTimer?.invalidate()
    elapsedTimer = nil
  }
}

private enum RecordingChannelNames {
  static let namespace = "app.record_tool"
  static let method = "\(namespace)/recording"
  static let events = "\(namespace)/recording_events"
}

final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
  private let controller = RecordingCameraController.shared
  private var eventSink: FlutterEventSink?

  static func register(with registrar: FlutterPluginRegistrar) {
    let plugin = RecordingPlugin()
    let messenger = registrar.messenger()

    registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview")

    let methodChannel = FlutterMethodChannel(
      name: RecordingChannelNames.method,
      binaryMessenger: messenger
    )
    registrar.addMethodCallDelegate(plugin, channel: methodChannel)

    let eventChannel = FlutterEventChannel(
      name: RecordingChannelNames.events,
      binaryMessenger: messenger
    )
    eventChannel.setStreamHandler(plugin)

    plugin.controller.statusListener = { [weak plugin] status in
      plugin?.eventSink?(status)
    }
  }

  func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "initializePreview":
      controller.initializePreview(result: result)
    case "startRecording":
      let args = call.arguments as? [String: Any]
      let withAudio = args?["withAudio"] as? Bool ?? true
      let displayName = args?["displayName"] as? String
      controller.startRecording(withAudio: withAudio, displayName: displayName, result: result)
    case "stopRecording":
      controller.stopRecording(result: result)
    case "getZoomCapabilities":
      controller.zoomCapabilities(result: result)
    case "setZoomRatio":
      let args = call.arguments as? [String: Any]
      let ratio = args?["zoomRatio"] as? Double ?? 1.0
      controller.setZoomRatio(CGFloat(ratio), result: result)
    case "disposePreview":
      controller.disposePreview(result: result)
    case "getStatus":
      result(controller.currentStatusMap())
    case "hasNotificationPolicyAccess":
      result(true)
    case "openNotificationPolicySettings":
      result(nil)
    case "enableDoNotDisturb":
      result(false)
    case "disableDoNotDisturb":
      result(nil)
    case "isIgnoringBatteryOptimizations":
      result(true)
    case "openBatteryOptimizationSettings":
      result(nil)
    case "setImmersiveMode":
      result(nil)
    case "isForegroundServiceRunning":
      result(false)
    default:
      result(FlutterMethodNotImplemented)
    }
  }

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
    -> FlutterError?
  {
    eventSink = events
    events(controller.currentStatusMap())
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    return nil
  }
}
