//
//  Ingest.swift
//  Cue Light Desk 1.0
//
import AVFoundation
import Combine
import HaishinKit
import RTMPHaishinKit
import SwiftUI
import VideoToolbox

struct IngestTarget: Identifiable {
    var id: String
    var name: String
    var url: String
    var key: String
    var ready: Bool {
        !url.trimmingCharacters(in: .whitespaces).isEmpty
            && !key.trimmingCharacters(in: .whitespaces).isEmpty
    }
}

@MainActor
final class Ingest: ObservableObject {
    @Published var liveNames: [String] = []
    @Published var error: String?
    @Published var pushing = false
    @Published var signalLost = false
    @Published var reconnecting = false
    @Published var panicking = false

    private let mixer = MediaMixer()
    private var connections: [RTMPConnection] = []
    private var streams: [RTMPStream] = []
    private var mixerOn = false
    private var lastTargets: [IngestTarget] = []
    private var lastWorld = true
    private var lastBitRate = 2_000_000
    private var lastByteLimit = 250_000
    private var heartbeat: Task<Void, Never>?
    private var preview: MTHKView?

    func start(
        targets: [IngestTarget],
        world: Bool,
        bitRate: Int = 2_000_000,
        byteLimit: Int = 250_000
    ) async {
        await stop()
        let ready = targets.filter(\.ready)
        guard !ready.isEmpty else {
            error = nil
            return
        }
        lastTargets = ready
        lastWorld = world
        lastBitRate = bitRate
        lastByteLimit = byteLimit
        error = nil
        signalLost = false
        reconnecting = false
        pushing = true
        do {
            try await bootMixer(world: world)
        } catch {
            self.error = error.localizedDescription
            pushing = false
            return
        }
        await publishAll()
        watchLink()
    }

    func attachPreview(_ view: MTHKView) async {
        preview = view
        await mixer.addOutput(view)
    }

    func flip(world: Bool) async {
        lastWorld = world
        let position: AVCaptureDevice.Position = world ? .back : .front
        try? await mixer.attachVideo(
            AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position)
                ?? AVCaptureDevice.default(for: .video)
        )
    }

    func mute(_ on: Bool) async {
        try? await mixer.attachAudio(on ? nil : AVCaptureDevice.default(for: .audio))
    }

    func panic(_ on: Bool, message: String) async {
        panicking = on
        _ = message
    }

    func stop() async {
        heartbeat?.cancel()
        heartbeat = nil
        pushing = false
        reconnecting = false
        signalLost = false
        panicking = false
        await dropPipes()
        liveNames = []
        error = nil
        if mixerOn {
            await mixer.stopRunning()
            mixerOn = false
        }
        try? await mixer.attachVideo(nil)
        try? await mixer.attachAudio(nil)
    }

    private func bootMixer(world: Bool) async throws {
        try AVAudioSession.sharedInstance().setCategory(
            .playAndRecord,
            mode: .videoRecording,
            options: [.defaultToSpeaker, .allowBluetoothHFP]
        )
        try AVAudioSession.sharedInstance().setActive(true)
        try await Task.sleep(nanoseconds: 400_000_000)
        try await mixer.setFrameRate(30)
        try? await mixer.setSessionPreset(.hd1280x720)
        try await mixer.attachAudio(AVCaptureDevice.default(for: .audio))
        let position: AVCaptureDevice.Position = world ? .back : .front
        let camera =
            AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position)
            ?? AVCaptureDevice.default(for: .video)
        if let camera {
            try? camera.lockForConfiguration()
            camera.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 30)
            camera.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 30)
            camera.unlockForConfiguration()
        }
        try await mixer.attachVideo(camera)
        await mixer.startRunning()
        mixerOn = true
        if let preview {
            await mixer.addOutput(preview)
        }
    }

    private func publishAll() async {
        var ok: [String] = []
        for target in lastTargets {
            if let name = await publish(target) {
                ok.append(name)
            }
        }
        liveNames = ok
        if ok.isEmpty {
            signalLost = true
            error = reconnecting ? nil : "Could not reach YouTube. Check the key."
        } else {
            signalLost = false
            reconnecting = false
            error = nil
        }
    }

    private func publish(_ target: IngestTarget) async -> String? {
        let connection = RTMPConnection()
        let stream = RTMPStream(connection: connection)
        do {
            try await stream.setVideoSettings(
                VideoCodecSettings(
                    videoSize: .init(width: 1280, height: 720),
                    bitRate: lastBitRate,
                    profileLevel: kVTProfileLevel_H264_Main_AutoLevel as String,
                    scalingMode: .letterbox,
                    bitRateMode: .average,
                    maxKeyFrameIntervalDuration: 2,
                    allowFrameReordering: false,
                    dataRateLimits: [Double(lastByteLimit), 1],
                    isLowLatencyRateControlEnabled: true,
                    isHardwareAcceleratedEnabled: true,
                    expectedFrameRate: 30
                )
            )
            try await stream.setAudioSettings(
                AudioCodecSettings(bitRate: 128_000)
            )
            await mixer.addOutput(stream)
            _ = try await connection.connect(target.url.trimmingCharacters(in: .whitespaces))
            guard await connection.connected else {
                if !reconnecting {
                    error = "\(target.name): could not reach the server."
                }
                return nil
            }
            _ = try await stream.publish(target.key.trimmingCharacters(in: .whitespaces))
            connections.append(connection)
            streams.append(stream)
            return target.name
        } catch {
            let text = String(describing: error).lowercased()
            if text.contains("publish.start") {
                connections.append(connection)
                streams.append(stream)
                return target.name
            }
            if !reconnecting {
                self.error = "\(target.name): \(error.localizedDescription)"
            }
            return nil
        }
    }

    private func dropPipes() async {
        for stream in streams { _ = try? await stream.close() }
        for connection in connections { _ = try? await connection.close() }
        streams = []
        connections = []
    }

    private func watchLink() {
        heartbeat?.cancel()
        heartbeat = Task { [weak self] in
            while let self, self.pushing, !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 2_000_000_000)
                guard self.pushing else { return }
                var up = false
                for connection in self.connections {
                    if await connection.connected { up = true }
                }
                if up {
                    if self.signalLost || self.reconnecting {
                        self.signalLost = false
                        self.reconnecting = false
                        self.error = nil
                    }
                    continue
                }
                await self.recover()
            }
        }
    }

    private func recover() async {
        guard pushing, !lastTargets.isEmpty else { return }
        reconnecting = true
        signalLost = true
        error = nil
        await dropPipes()
        liveNames = []
        if mixerOn {
            await mixer.stopRunning()
            mixerOn = false
        }
        try? await Task.sleep(nanoseconds: 400_000_000)
        guard pushing else { return }
        do {
            try await bootMixer(world: lastWorld)
        } catch {
            self.error = nil
            return
        }
        for _ in 0..<8 {
            guard pushing else { return }
            await publishAll()
            if !liveNames.isEmpty { return }
            reconnecting = true
            signalLost = true
            try? await Task.sleep(nanoseconds: 1_000_000_000)
        }
    }
}

enum IngestDefaults {
    static let youtube = "rtmp://a.rtmp.youtube.com/live2"
    static let twitch = "rtmp://live.twitch.tv/app"
}

struct MixerPreview: UIViewRepresentable {
    let ingest: Ingest

    func makeUIView(context: Context) -> MTHKView {
        let view = MTHKView(frame: .zero)
        view.videoGravity = .resizeAspectFill
        Task { await ingest.attachPreview(view) }
        return view
    }

    func updateUIView(_ uiView: MTHKView, context: Context) {}
}
