import AVFoundation
import ComposableArchitecture
import Foundation

@DependencyClient
public struct AudioRecorderClient {
    public var currentTime: @Sendable () async -> TimeInterval?
    public var requestRecordPermission: @Sendable () async -> Bool = { false }
    public var startRecording: @Sendable (_ url: URL, _ shouldStop: Bool) async throws -> Bool
    public var prepareToRecordWithEngine: @Sendable () async throws -> Void = {}
    public var startRecordingWithEngine: @Sendable (_ url: URL, _ shouldStop: Bool) async throws -> Bool = { _, _ in throw NSError(domain: "AudioRecorderClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "startRecordingWithEngine not implemented"]) }
    public var stopRecordingWithEngine: @Sendable (_ shouldDeactivateSession: Bool) async -> Void = { _ in }
    public var currentTimeWithEngine: @Sendable () async -> TimeInterval? = { nil }
    public var powerLevelsWithEngine: @Sendable () async -> PowerLevels = { .zero }
    public var stopRecording: @Sendable () async -> Void
    public var powerLevels: @Sendable () async -> PowerLevels = { .zero }
}

extension AudioRecorderClient: DependencyKey {
    public static var liveValue: Self {
        let audioRecorder = AudioRecorder()
        let audioEngineRecorder = AudioEngineRecorder()
        return Self(
            currentTime: { await audioRecorder.currentTime },
            requestRecordPermission: { await AudioRecorder.requestPermission() },
            startRecording: { url, shouldStop in try await audioRecorder.start(url: url, shouldStop: shouldStop) },
            prepareToRecordWithEngine: { try await audioEngineRecorder.prepare() },
            startRecordingWithEngine: { url, shouldStop in
                if shouldStop {
                    await audioEngineRecorder.stopRecordingWithSession(shouldDeactivateSession: false)
                }
                try await audioEngineRecorder.start(url: url)
                return true
            },
            stopRecordingWithEngine: { shouldDeactivateSession in await audioEngineRecorder.stopRecordingWithSession(shouldDeactivateSession: shouldDeactivateSession) },
            currentTimeWithEngine: { await audioEngineRecorder.currentTime() },
            powerLevelsWithEngine: { await audioEngineRecorder.powerLevels() },
            stopRecording: { await audioRecorder.stop() },
            powerLevels: { await audioRecorder.powerLevels() }
        )
    }
}

public struct PowerLevels: Equatable {
    public let averagePower: Double
    public let peakPower: Double

    public static var zero = Self(averagePower: 0, peakPower: 0)
}

private actor AudioRecorder {
    var delegate: Delegate?
    var recorder: AVAudioRecorder?

    var currentTime: TimeInterval? {
        guard
            let recorder = self.recorder,
            recorder.isRecording
        else { return nil }
        return recorder.currentTime
    }

    static func requestPermission() async -> Bool {
        await AVAudioApplication.requestRecordPermission()
    }

    func stop() {
        self.recorder?.stop()
        try? AVAudioSession.sharedInstance().setActive(false)
    }

    func start(url: URL, shouldStop: Bool = true) async throws -> Bool {
        if shouldStop { self.stop() }

        let stream = AsyncThrowingStream<Bool, Error> { continuation in
            do {
                self.delegate = Delegate(
                    didFinishRecording: { flag in
                        continuation.yield(flag)
                        continuation.finish()
                        try? AVAudioSession.sharedInstance().setActive(false)
                    },
                    encodeErrorDidOccur: { error in
                        continuation.finish(throwing: error)
                        try? AVAudioSession.sharedInstance().setActive(false)
                    }
                )
                let recorder = try AVAudioRecorder(
                    url: url,
                    settings: [
                        AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                        AVSampleRateKey: 44100,
                        AVNumberOfChannelsKey: 1,
                        AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
                    ]
                )
                recorder.isMeteringEnabled = true
                self.recorder = recorder
                recorder.delegate = self.delegate

                continuation.onTermination = { [recorder = UncheckedSendable(recorder)] _ in
                    recorder.wrappedValue.stop()
                }

                try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker)
                try AVAudioSession.sharedInstance().setActive(true)
                self.recorder?.record()
            } catch {
                continuation.finish(throwing: error)
            }
        }

        for try await didFinish in stream {
            return didFinish
        }
        throw CancellationError()
    }

    func powerLevels() -> PowerLevels {
        recorder?.updateMeters()
        return .init(
            averagePower: Double(recorder?.averagePower(forChannel: 0) ?? 0),
            peakPower: Double(recorder?.peakPower(forChannel: 0) ?? 0)
        )
    }
}

private final class Delegate: NSObject, AVAudioRecorderDelegate, Sendable {
    let didFinishRecording: @Sendable (Bool) -> Void
    let encodeErrorDidOccur: @Sendable (Error?) -> Void

    init(
        didFinishRecording: @escaping @Sendable (Bool) -> Void,
        encodeErrorDidOccur: @escaping @Sendable (Error?) -> Void
    ) {
        self.didFinishRecording = didFinishRecording
        self.encodeErrorDidOccur = encodeErrorDidOccur
    }

    func audioRecorderDidFinishRecording(_: AVAudioRecorder, successfully flag: Bool) {
        self.didFinishRecording(flag)
    }

    func audioRecorderEncodeErrorDidOccur(_: AVAudioRecorder, error: Error?) {
        self.encodeErrorDidOccur(error)
    }
}

actor AudioEngineRecorder {
    private var engine: AVAudioEngine?
    private var inputNode: AVAudioInputNode?
    private var file: AVAudioFile?
    private var isRecording = false
    private var startTime: Date?

    private var continuation: CheckedContinuation<Void, Error>?

    private(set) var latestPowerLevels: PowerLevels = .zero

    private let bufferSize: AVAudioFrameCount = 102

    /// Prepare audio session + engine. Call once before recording (e.g., view appears).
    func prepare() async throws {
        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
        try audioSession.setPreferredIOBufferDuration(0.005)
        try audioSession.setActive(true)

        if engine == nil {
            let audioEngine = AVAudioEngine()
            let inputNode = audioEngine.inputNode

            let inputFormat = inputNode.inputFormat(forBus: 0)
            guard inputFormat.sampleRate > 0 && inputFormat.channelCount > 0 else {
                throw NSError(domain: "AudioEngineRecorder", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid input format"])
            }

            self.engine = audioEngine
            self.inputNode = inputNode

            try audioEngine.start()
        } else {
            try AVAudioSession.sharedInstance().setActive(true)
            if let engine = engine, !engine.isRunning {
                try engine.start()
            }
        }
    }

    /// Start recording to `url`. Suspends until `stop()` is called or an error occurs.
    func start(url: URL) async throws {
        try await prepare()

        if isRecording {
            // If already recording, stop first
            await stopRecordingWithSession(shouldDeactivateSession: false)
        }

        guard let engine = engine, let inputNode = inputNode else {
            throw NSError(domain: "AudioEngineRecorder", code: -1, userInfo: [NSLocalizedDescriptionKey: "Engine not prepared. Call prepare() first."])
        }

        if !engine.isRunning {
            try engine.start()
        }

        let inputFormat = inputNode.inputFormat(forBus: 0)

        let audioFile: AVAudioFile
        do {
            audioFile = try AVAudioFile(forWriting: url, settings: inputFormat.settings)
        } catch {
            throw error
        }

        self.file = audioFile

        inputNode.removeTap(onBus: 0)

        let localFile = audioFile
        let localInputFormat = inputFormat
        let localBufferSize = bufferSize

        inputNode.installTap(onBus: 0, bufferSize: localBufferSize, format: localInputFormat) { [weak self] buffer, when in
            let levels = Self.calculatePowerLevels(from: buffer)

            do {
                try localFile.write(from: buffer)
            } catch {
                if let strongSelf = self {
                    Task {
                        await strongSelf.handleWriteError(error)
                    }
                }
                return
            }

            if let strongSelf = self {
                Task {
                    await strongSelf.updatePowerLevels(levels)
                }
            }
        }

        // Update actor state
        self.isRecording = true
        self.startTime = Date()
        self.latestPowerLevels = .zero

        // Suspend until stop() resumes or an error occurs
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            self.continuation = continuation
        }
    }

    /// Stop recording safely.
    /// - Parameter shouldDeactivateSession: If `true`, deactivates AVAudioSession (use for Continue/Finish).
    ///   If `false`, keeps session active (use for Cancel/Re-record to allow immediate restart).
    /// - Note: This method is idempotent and safe to call multiple times.
    func stopRecordingWithSession(shouldDeactivateSession: Bool = true) async {
        guard isRecording || continuation != nil else {
            if shouldDeactivateSession {
                try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
            }
            return
        }

        if let inputNode = inputNode {
            inputNode.removeTap(onBus: 0)
        }

        self.file = nil

        self.isRecording = false
        self.startTime = nil
        self.latestPowerLevels = .zero

        let cont = self.continuation
        self.continuation = nil

        if shouldDeactivateSession {
            try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
        }

        cont?.resume()
    }

    private func handleWriteError(_ error: Error) async {
        if let inputNode = inputNode {
            inputNode.removeTap(onBus: 0)
        }

        self.file = nil
        self.isRecording = false
        self.startTime = nil
        self.latestPowerLevels = .zero

        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)

        let cont = self.continuation
        self.continuation = nil
        cont?.resume(throwing: error)
    }

    func currentTime() -> TimeInterval? {
        guard isRecording, let start = startTime else { return nil }
        return Date().timeIntervalSince(start)
    }

    func powerLevels() -> PowerLevels {
        latestPowerLevels
    }

    fileprivate func updatePowerLevels(_ levels: PowerLevels) {
        self.latestPowerLevels = levels
    }

    static func calculatePowerLevels(from buffer: AVAudioPCMBuffer) -> PowerLevels {
        guard let channelData = buffer.floatChannelData else {
            return .zero
        }

        let channelCount = Int(buffer.format.channelCount)
        let frameLength = Int(buffer.frameLength)
        var sum: Float = 0
        var peak: Float = 0

        for channel in 0..<channelCount {
            let cBuf = channelData[channel]
            for i in 0..<frameLength {
                let sample = fabsf(cBuf[i])
                sum += sample * sample
                peak = max(peak, sample)
            }
        }

        let rms = sqrt(sum / Float(frameLength * channelCount))
        let avgDB = 20.0 * log10(max(Double(rms), 1e-10))
        let peakDB = 20.0 * log10(max(Double(peak), 1e-10))

        return PowerLevels(averagePower: avgDB, peakPower: peakDB)
    }
}

public extension DependencyValues {
    var audioRecorder: AudioRecorderClient {
        get { self[AudioRecorderClient.self] }
        set { self[AudioRecorderClient.self] = newValue }
    }
}

extension AudioRecorderClient: TestDependencyKey {
    public static let previewValue = Self.noop

    public static let testValue = Self()
}

public extension AudioRecorderClient {
    static let noop = Self()
}
