import AVFoundation
import CoreImage
import Foundation

protocol HooksCameraManagerDelegate: AnyObject {
    func didStartWritingVideoAsset(_ to: URL?)
    func didUpdateWritingVideoAsset(_ formattedTime: String, completionRatio: CGFloat)
    func didFinishRecording() /* Does not mean asset is ready yet */
    func didEndWritingVideoAsset(_ to: URL?, duration: TimeInterval) /* This is when the asset is actually ready */
}

final class HooksCameraManager: NSObject {
    weak var delegate: HooksCameraManagerDelegate?

    private let assetWriter = HooksCameraAssetWriter()
    private let captureSession = AVCaptureSession()
    private var deviceInput: AVCaptureDeviceInput?
    private var videoOutput: AVCaptureVideoDataOutput?

    private var currentWritingURL: URL?

    private let systemPreferredCamera = AVCaptureDevice.default(for: .video)
    private var sessionQueue = DispatchQueue(label: "video.preview.session")

    var isFlipped: Bool = false
    var isFlashOn: Bool = false
    var isAuthorized: Bool {
        get async {
            let status = AVCaptureDevice.authorizationStatus(for: .video)
            var isAuthorized = status == .authorized
            if status == .notDetermined {
                isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
            }
            return isAuthorized
        }
    }

    private var addToPreviewStream: ((CGImage) -> Void)?
    lazy var previewStream: AsyncStream<CGImage> = AsyncStream { continuation in
        addToPreviewStream = { cgImage in
            continuation.yield(cgImage)
        }
    }

    override init() {
        super.init()
        HooksCameraAssetWriter.deleteTemporaryFiles()
        assetWriter.delegate = self

        Task {
            await configureSession()
            await startSession()
            await assetWriter.warmUpEncoder()
        }

        currentWritingURL = HooksCameraAssetWriter
            .createUniqueFileURLInDocuments("mp4")
    }

    func startWriting() {
        guard let writeURL = currentWritingURL else { return }
        assetWriter.startRecording(
            to: writeURL,
            triggerDelegate: true
        )
    }

    func endWriting() {
        Task {
            await assetWriter.stopRecording()
            Task { @MainActor [weak self] in
                self?.delegate?.didFinishRecording()
            }
        }
    }

    /// Flip between front and back camera
    func flipCamera() {
        sessionQueue.async { [weak self] in
            guard let strongSelf = self else { return }
            self?.isFlipped = !strongSelf.isFlipped
            strongSelf.captureSession.beginConfiguration()
            defer { strongSelf.captureSession.commitConfiguration() }

            guard let currentInput = strongSelf.deviceInput else {
                return
            }

            strongSelf.captureSession.removeInput(currentInput)

            let newPosition: AVCaptureDevice.Position = (currentInput.device.position == .back) ? .front : .back

            guard let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: newPosition),
                  let newInput = try? AVCaptureDeviceInput(device: newDevice)
            else {
                strongSelf.captureSession.addInput(currentInput)
                return
            }

            if strongSelf.captureSession.canAddInput(newInput) {
                strongSelf.captureSession.addInput(newInput)
                strongSelf.deviceInput = newInput

                if let connection = strongSelf.videoOutput?.connection(with: .video),
                   connection.isVideoMirroringSupported
                {
                    connection.isVideoMirrored = (newPosition == .front)
                }
            } else {
                strongSelf.captureSession.addInput(currentInput)
            }
        }
    }

    /// Torch (flash) control
    func setFlash(on: Bool) {
        sessionQueue.async { [weak self] in
            guard let device = self?.deviceInput?.device, device.hasTorch else { return }
            do {
                try device.lockForConfiguration()
                if on {
                    try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel)
                } else {
                    device.torchMode = .off
                }
                device.unlockForConfiguration()
                self?.isFlashOn = on
            } catch {
                log.error("[ERROR] Unable to set torch: \(error)")
            }
        }
    }

    /// Toggle flash on/off
    func toggleFlash() {
        setFlash(on: !isFlashOn)
    }

    private func configureSession() async {
        guard await isAuthorized,
              let systemCamera = systemPreferredCamera,
              let input = try? AVCaptureDeviceInput(device: systemCamera)
        else { return }
        captureSession.beginConfiguration()
        defer { captureSession.commitConfiguration() }

        let newVideoOutput = AVCaptureVideoDataOutput()
        newVideoOutput.setSampleBufferDelegate(self, queue: sessionQueue)

        guard captureSession.canAddInput(input) else {
            log.error("[ERROR] Unable to add device input to capture session.")
            return
        }
        guard captureSession.canAddOutput(newVideoOutput) else {
            log.error("[ERROR] Unable to add video output to capture session.")
            return
        }

        captureSession.addInput(input)
        self.deviceInput = input

        captureSession.addOutput(newVideoOutput)
        self.videoOutput = newVideoOutput
    }

    private func startSession() async {
        guard await isAuthorized else { return }
        captureSession.startRunning()
    }
}

extension HooksCameraManager: HooksCameraAssetWriterDelegate {
    func didStartWriting(_ to: URL?) {
        delegate?.didStartWritingVideoAsset(to)
    }

    func didUpdateWriting(_ formattedTime: String, _ completionRatio: CGFloat) {
        delegate?.didUpdateWritingVideoAsset(formattedTime, completionRatio: completionRatio)
    }

    func didEndWriting(_ to: URL?) {
        Task {
            guard let toURL = to, let duration = await toURL.asyncVideoDuration() else { return }
            // TODO: Document this
            let result = await VideoRotator.rotatePortraitToLandscape(inputURL: toURL, isFlipped: isFlipped)

            switch result {
            case .success(let savedURL):
                delegate?.didEndWritingVideoAsset(savedURL, duration: duration)
            case .failure(let error):
                print(error)
            }
        }
    }
}

extension HooksCameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(
        _: AVCaptureOutput,
        didOutput sampleBuffer: CMSampleBuffer,
        from _: AVCaptureConnection
    ) {
        let videoRotation = videoRotationAngle /* + connection.videoRotationAngle*/ // We don't properly set `videoRotationAngle` during setup time, so best to ignore it to guarantee a baseline rotation of "0" before computing the flip angle.

        guard
            let cgImage = sampleBuffer.cgImage,
            let rotatedImage = cgImage.rotated(
                byDegrees: videoRotation
            )
        else { return }

        assetWriter.writeSampleBuffer(sampleBuffer)
        addToPreviewStream?(rotatedImage)
    }

    var videoRotationAngle: CGFloat {
        isFlipped ? 90.0 : 270.0
    }
}
