import AVFoundation
// ObservableObject and @Published live in Combine. The project enables
// MemberImportVisibility, which makes a type visible only where its defining
// module is imported in THAT file — SwiftUI re-exporting it elsewhere is not
// enough.
import Combine
import Foundation

/// Microphone capture on the watch.
///
/// AAC in an .m4a container: Steward already maps audio/mp4 to m4a, and
/// process-capture.sh normalises everything through ffmpeg before whisper, so
/// the format costs nothing either way. 22.05kHz mono is deliberate — whisper
/// downsamples to 16kHz regardless, and a smaller file matters a great deal on
/// a watch that may be uploading over Bluetooth.
@MainActor
final class Recorder: NSObject, ObservableObject {
    @Published private(set) var isRecording = false
    @Published private(set) var elapsed: TimeInterval = 0
    /// Current input level, 0...1, for the meter ring.
    @Published private(set) var level: Double = 0
    /// Seconds of continuous near-silence. The point of the meter is not
    /// decoration — it is to answer "is this mic actually hearing me" while
    /// there is still time to do something about it.
    @Published private(set) var silentFor: TimeInterval = 0

    private var recorder: AVAudioRecorder?
    private var timer: Timer?
    private var fileURL: URL?

    /// Ask once; the system remembers the answer.
    func requestPermission() async -> Bool {
        if #available(watchOS 10.0, *) {
            return await AVAudioApplication.requestRecordPermission()
        } else {
            return await withCheckedContinuation { cont in
                AVAudioSession.sharedInstance().requestRecordPermission { cont.resume(returning: $0) }
            }
        }
    }

    func start() throws {
        let session = AVAudioSession.sharedInstance()
        // .record rather than .playAndRecord: this app never plays anything, and
        // asking for playback makes the session fight whatever is already using
        // the speaker.
        try session.setCategory(.record, mode: .default)
        try session.setActive(true, options: .notifyOthersOnDeactivation)

        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("capture-\(UUID().uuidString).m4a")
        let settings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 22_050,
            AVNumberOfChannelsKey: 1,
            AVEncoderAudioQualityKey: AVAudioQuality.medium.rawValue,
        ]

        let rec = try AVAudioRecorder(url: url, settings: settings)
        rec.isMeteringEnabled = true
        guard rec.record() else { throw RecorderError.couldNotStart }

        recorder = rec
        fileURL = url
        isRecording = true
        elapsed = 0
        level = 0
        silentFor = 0

        // 10Hz: fast enough that the ring tracks your voice, slow enough not to
        // matter on a watch battery for a recording capped at two minutes.
        let tick = 0.1
        timer = Timer.scheduledTimer(withTimeInterval: tick, repeats: true) { [weak self] _ in
            Task { @MainActor in
                guard let self, let r = self.recorder else { return }
                self.elapsed = r.currentTime

                r.updateMeters()
                // averagePower is dBFS: 0 is clipping, -160 is digital silence.
                // Speech at arm's length sits around -30..-10, so anchoring the
                // bottom at -50 gives the ring a usable range instead of
                // flattening everything quiet into nothing.
                let db = Double(r.averagePower(forChannel: 0))
                let clamped = min(0, max(-50, db))
                self.level = (clamped + 50) / 50
                self.silentFor = self.level < 0.08 ? self.silentFor + tick : 0

                if self.elapsed >= Config.maxRecordingSeconds { _ = self.stop() }
            }
        }
    }

    /// A finished recording: the file and how long it ran.
    struct Finished {
        let url: URL
        let durationMs: Int
    }

    /// Throw the recording away. Separate from stop() so there is no path where
    /// changing your mind still uploads — the file is deleted here, not handed
    /// back to a caller who might forget.
    func cancel() {
        timer?.invalidate(); timer = nil
        recorder?.stop(); recorder = nil
        isRecording = false
        level = 0
        silentFor = 0
        elapsed = 0
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
        if let url = fileURL { try? FileManager.default.removeItem(at: url) }
        fileURL = nil
    }

    /// Why a recording produced nothing worth sending — each needs its own
    /// message, because "say it again" and "hold it longer" are different
    /// instructions and guessing wrong is worse than saying nothing.
    enum StopResult {
        case ok(Finished)
        case tooShort
        case silent
    }

    @discardableResult
    func stop() -> StopResult {
        // Read the length BEFORE stopping — currentTime is 0 once the recorder
        // has stopped, and the file's own duration would need decoding it back.
        let seconds = recorder?.currentTime ?? elapsed
        timer?.invalidate(); timer = nil
        recorder?.stop(); recorder = nil
        isRecording = false
        level = 0
        silentFor = 0
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)

        guard let url = fileURL else { return .silent }
        fileURL = nil

        // A tap-tap is not a memo. Checked before the size guard so a quick
        // double-tap reports as too short rather than as a dead microphone.
        if seconds < Config.minRecordingSeconds {
            try? FileManager.default.removeItem(at: url)
            return .tooShort
        }

        // A file this small is silence — the watch mic failing is a real and
        // quiet failure mode, and sending it would just make whisper hallucinate
        // a word. Better to say so than to transcribe nothing.
        let size = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int) ?? 0
        guard size > 8_000 else {
            try? FileManager.default.removeItem(at: url)
            return .silent
        }
        return .ok(Finished(url: url, durationMs: Int((seconds * 1000).rounded())))
    }

    enum RecorderError: LocalizedError {
        case couldNotStart
        var errorDescription: String? { "Could not start recording" }
    }
}
