import AVFoundation
import Foundation

enum CameraStatus: Equatable {
    case idle
    case running
    case unauthorized
    case failed(String)
}

final class CameraController: NSObject, ObservableObject {

    @Published private(set) var status: CameraStatus = .idle
    @Published private(set) var position: AVCaptureDevice.Position = .back

    private let frameLock = NSLock()
    private var frameHandler: ((CVPixelBuffer) -> Void)?

    private let session = AVCaptureSession()
    private let sessionQueue = DispatchQueue(label: "com.terminalbay.dithercam.session")
    private let videoQueue = DispatchQueue(label: "com.terminalbay.dithercam.video", qos: .userInitiated)
    private let videoOutput = AVCaptureVideoDataOutput()

    private var videoInput: AVCaptureDeviceInput?
    private var sessionPosition: AVCaptureDevice.Position = .back
    private var isConfigured = false
    private var observersAdded = false

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func setFrameHandler(_ handler: @escaping (CVPixelBuffer) -> Void) {
        frameLock.lock()
        frameHandler = handler
        frameLock.unlock()
    }

    func start() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            beginSession()
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                guard let self else { return }
                if granted {
                    self.beginSession()
                } else {
                    self.publish(.unauthorized)
                }
            }
        default:
            publish(.unauthorized)
        }
    }

    func stop() {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            if self.session.isRunning {
                self.session.stopRunning()
            }
        }
    }

    func flip() {
        sessionQueue.async { [weak self] in
            guard let self, self.isConfigured else { return }
            let target: AVCaptureDevice.Position = self.sessionPosition == .back ? .front : .back
            guard let device = CameraController.device(for: target),
                  let newInput = try? AVCaptureDeviceInput(device: device) else { return }

            self.session.beginConfiguration()
            let previousInput = self.videoInput
            if let previousInput {
                self.session.removeInput(previousInput)
            }
            if self.session.canAddInput(newInput) {
                self.session.addInput(newInput)
                self.videoInput = newInput
                self.sessionPosition = target
            } else if let previousInput, self.session.canAddInput(previousInput) {
                self.session.addInput(previousInput)
            }
            self.session.commitConfiguration()
            self.applyConnectionSettings(for: self.sessionPosition)

            let resolved = self.sessionPosition
            DispatchQueue.main.async {
                self.position = resolved
            }
        }
    }

    private func beginSession() {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            if !self.isConfigured {
                self.configure()
            }
            guard self.isConfigured else { return }
            if !self.session.isRunning {
                self.session.startRunning()
            }
            let running = self.session.isRunning
            DispatchQueue.main.async {
                self.status = running ? .running : .failed("The camera session did not start.")
            }
        }
    }

    private func configure() {
        addObservers()

        session.beginConfiguration()

        if session.canSetSessionPreset(.hd1920x1080) {
            session.sessionPreset = .hd1920x1080
        } else if session.canSetSessionPreset(.high) {
            session.sessionPreset = .high
        }

        guard let device = CameraController.device(for: sessionPosition),
              let input = try? AVCaptureDeviceInput(device: device),
              session.canAddInput(input) else {
            session.commitConfiguration()
            publish(.failed("No usable camera was found."))
            return
        }
        session.addInput(input)
        videoInput = input

        videoOutput.videoSettings = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
        ]
        videoOutput.alwaysDiscardsLateVideoFrames = true
        videoOutput.setSampleBufferDelegate(self, queue: videoQueue)

        guard session.canAddOutput(videoOutput) else {
            session.commitConfiguration()
            publish(.failed("The video output could not be attached."))
            return
        }
        session.addOutput(videoOutput)

        session.commitConfiguration()
        applyConnectionSettings(for: sessionPosition)
        isConfigured = true
    }

    private func applyConnectionSettings(for position: AVCaptureDevice.Position) {
        guard let connection = videoOutput.connection(with: .video) else { return }
        if connection.isVideoRotationAngleSupported(90) {
            connection.videoRotationAngle = 90
        }
        if connection.isVideoMirroringSupported {
            connection.automaticallyAdjustsVideoMirroring = false
            connection.isVideoMirrored = position == .front
        }
    }

    private static func device(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
        if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) {
            return device
        }
        let discovery = AVCaptureDevice.DiscoverySession(
            deviceTypes: [.builtInWideAngleCamera, .builtInDualWideCamera, .builtInDualCamera],
            mediaType: .video,
            position: position
        )
        return discovery.devices.first
    }

    private func addObservers() {
        guard !observersAdded else { return }
        observersAdded = true
        let center = NotificationCenter.default
        center.addObserver(
            self,
            selector: #selector(handleRuntimeError(_:)),
            name: .AVCaptureSessionRuntimeError,
            object: session
        )
        center.addObserver(
            self,
            selector: #selector(handleInterruptionEnded(_:)),
            name: .AVCaptureSessionInterruptionEnded,
            object: session
        )
    }

    @objc private func handleRuntimeError(_ notification: Notification) {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            if !self.session.isRunning {
                self.session.startRunning()
            }
            let running = self.session.isRunning
            DispatchQueue.main.async {
                self.status = running ? .running : .failed("The camera stopped and could not be restarted.")
            }
        }
    }

    @objc private func handleInterruptionEnded(_ notification: Notification) {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            if !self.session.isRunning {
                self.session.startRunning()
            }
            let running = self.session.isRunning
            DispatchQueue.main.async {
                self.status = running ? .running : .idle
            }
        }
    }

    private func publish(_ newStatus: CameraStatus) {
        DispatchQueue.main.async { [weak self] in
            self?.status = newStatus
        }
    }
}

extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput,
                       didOutput sampleBuffer: CMSampleBuffer,
                       from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        frameLock.lock()
        let handler = frameHandler
        frameLock.unlock()
        handler?(pixelBuffer)
    }
}
