import Foundation
import Combine
import Darwin
import RideFormCore

@MainActor
final class RecordingController: ObservableObject {
    enum State: String { case uncalibrated, calibrating, ready, recording, finalizing, saved, saveError }
    @Published private(set) var state: State = .uncalibrated
    @Published private(set) var status = "Mount the phone rigidly, then calibrate."
    @Published private(set) var counts: [StreamKind: Int] = [:]
    @Published private(set) var capabilities: [StreamKind: CapabilityState] = [:]
    @Published private(set) var measuredHz: Double?
    @Published private(set) var elapsed = 0.0
    @Published private(set) var angles: Vector3?
    @Published private(set) var savedURL: URL?
    @Published private(set) var sessions: [URL] = []
    @Published var captureMode: CaptureMode = .fusedOnly
    @Published var audibleDrill = false
    @Published var spokenCalibration = true
    @Published var recordAfterCalibration = false
    @Published private(set) var calibrationLogURL: URL?
    @Published private(set) var calibrationLogError: String?
    private let calibrationLog: CalibrationAttemptLog
    private let calibrationSpeaker = CalibrationSpeaker()
    var runtimeDiagnostic: ((String) -> Void)?
    @Published private(set) var audioTestStatus = ""
    private var calibrationGeneration = 0
    private var calibrationActivity = 0.0
    private var calibrationStarted = 0.0
    private var calibrationVoiceStarted = 0.0
    private var calibrationVoiceFinished = 0.0
    @Published private(set) var calibrationSpeechPending = false
    private var calibrationSpeechSequence = 0
    private let guidanceSpeech: ((String, @escaping () -> Void) -> Bool)?
    private var snapshotPending = false
    private var calibrationIdleState: Bool?
    @Published private(set) var diagnosticsActive = false
    @Published private(set) var diagnosticInstruction = ""
    @Published private(set) var diagnosticMovementReady = false
    private var diagnosticPreparing = false
    private var diagnosticTimer: Timer?
    private var diagnosticDrillIndex = 0
    static let diagnosticDrills = ["SLOW LEFT", "RETURN CENTER", "SLOW RIGHT", "RETURN CENTER", "SLOW FORWARD", "RETURN CENTER", "RAPID LEFT", "RETURN CENTER", "RAPID RIGHT", "RETURN CENTER", "RAPID FORWARD", "RETURN CENTER"]
    let calibration: CalibrationController
    let repository: SessionRepository
    let capture: MotionCapture
    let cues: CuePlayer
    let synthetic: Bool
    private var updates: Timer?
    private var calibrationObservation: AnyCancellable?
    private var convention = RotationConvention()
    private var started: Double?
    private var eventSequence: UInt64 = 0
    private var previousRate: (time: Double, count: Int)?
    private var lastCheckpoint = 0.0
    private var idleBeforeRecording = false
    private var ownsIdleTimer = false
    private var capturing = false
    private var referenceValid = false
    private let sink = RecordingSink()
    private var sessionWarning: String?
    #if targetEnvironment(simulator)
    private var fixture: SimulatorMotionFixture?
    #endif

    init(repository: SessionRepository = SessionRepository(), calibration: CalibrationController? = nil, synthetic: Bool? = nil, guidanceSpeech: ((String, @escaping () -> Void) -> Bool)? = nil) {
        #if targetEnvironment(simulator)
        // Saved-review UI fixtures need their own library; ordinary recording UI tests
        // intentionally share the persistent library to verify relaunch recovery.
        let repository = ProcessInfo.processInfo.arguments.contains("--uitest-review")
            ? SessionRepository(directory: FileManager.default.temporaryDirectory.appendingPathComponent("RideForm-review-test-" + UUID().uuidString)) : repository
        #endif
        self.guidanceSpeech = guidanceSpeech
        self.repository = repository
        calibrationLog = CalibrationAttemptLog(directory: repository.directory)
        calibrationLogURL = calibrationLog.url
        self.calibration = calibration ?? CalibrationController()
        capture = MotionCapture(); cues = CuePlayer(now: { SessionClock().now() })
        #if targetEnvironment(simulator)
        self.synthetic = synthetic ?? ProcessInfo.processInfo.arguments.contains("--uitest-motion")
        #else
        self.synthetic = false
        #endif
        if self.calibration.canRecord { state = .uncalibrated }
        calibrationObservation = self.calibration.objectWillChange.sink { [weak self] in self?.objectWillChange.send() }
        calibrationSpeaker.diagnostic = { [weak self] details in
            self?.logCalibration("audioOutput", details: details)
        }
        refreshSessions()
        #if targetEnvironment(simulator) && os(iOS)
        if self.synthetic && ProcessInfo.processInfo.arguments.contains("--uitest-review") {
            do {
                try ReviewUIFixture.create(in: repository.directory) { [weak self] result in
                    DispatchQueue.main.async { self?.saveFinished(result) }
                }
            } catch { status = "Synthetic fixture failed: \(error)" }
        }
        #endif
        updates = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
            Task { @MainActor in self?.refreshLive() }
        }
    }
    var canUseMotion: Bool { synthetic || capture.isDeviceMotionAvailable }
    func refreshSessions() {
        do { sessions = try repository.list() } catch { status = "Cannot read saved sessions: \(error.localizedDescription)" }
    }
    var canTestCalibrationAudio: Bool { [.uncalibrated, .ready].contains(state) && !diagnosticPreparing && !calibrationSpeechPending }
    func testCalibrationAudio() {
        guard canTestCalibrationAudio else { return }
        calibrationSpeaker.cancel()
        calibrationLog.begin(); logCalibration("audioTest")
        audioTestStatus = "Listen for the spoken instruction."
        let done: () -> Void = { [weak self] in
            self?.audioTestStatus = "If you heard nothing, check Watch volume and connected headphones before calibrating."
        }
        let started: Bool
        if let guidanceSpeech { started = guidanceSpeech("RideForm audio test. Sit centered and breathe normally.", done) }
        else if synthetic { done(); started = true }
        else { started = calibrationSpeaker.say("RideForm audio test. Sit centered and breathe normally.", completion: done) }
        if !started { audioTestStatus = "Audio unavailable. Check volume and audio output, then test again." }
    }
    func beginCalibration(method: CalibrationController.SetupMethod = .guided) {
        guard !diagnosticPreparing else { return }
        guard [.uncalibrated, .ready].contains(state) || (state == .calibrating && calibration.stage == .retry) else { return }
        guard captureMode != .rawOnly else { status = "Raw-only capture is for diagnostics. Select fused motion before calibration."; return }
        guard canUseMotion else { status = "Device motion or required reference frame unavailable. Record is disabled."; return }
        guard calibration.begin(recording: false, method: method, handsFree: spokenCalibration) else { return }
        calibrationLog.begin(); logCalibration("started")
        calibrationGeneration += 1; snapshotPending = false
        calibrationActivity = capture.clock.now(); calibrationStarted = calibrationActivity
        if calibrationIdleState == nil { calibrationIdleState = CapturePlatform.idleTimerDisabled }
        CapturePlatform.idleTimerDisabled = true
        state = .calibrating; status = calibration.prompt
        sink.setCalibrationSampling(true)
        if !capturing { startCapture(writer: nil) }
        announceCalibrationStage()
    }
    func advanceCalibration() {
        guard state == .calibrating else { return }
        guard calibration.advance() else { announceCalibrationStage(); return }
        announceCalibrationStage()
        finishCalibrationIfNeeded()
    }
    private func finishCalibrationIfNeeded() {
        guard !snapshotPending else { return }
        if calibration.stage == .awaitingConvention {
            snapshotPending = true
            let generation = calibrationGeneration
            capture.snapshot { [weak self] summary in
                DispatchQueue.main.async {
                    guard let self, self.state == .calibrating, self.calibrationGeneration == generation else { return }
                    self.snapshotPending = false
                    if self.calibration.finish(summary: summary) {
                        self.convention = summary.rotationConvention; self.referenceValid = true; self.state = .ready
                    }
                    self.logCalibration("fit", details: "convention verified=\(summary.rotationConvention.verified); \(summary.rotationConvention.capturedToCanonical); \(summary.conventionDiagnostics)")
                    if !self.calibration.isCalibrating {
                        self.sink.setCalibrationSampling(false)
                        self.restoreCalibrationIdleTimer()
                    }
                    self.announceCalibrationStage()
                }
            }
        }
    }
    func cancelCalibration() {
        guard state == .calibrating else { return }
        logCalibration("cancelled")
        calibrationGeneration += 1; snapshotPending = false
        calibrationSpeechPending = false
        calibrationSpeaker.cancel(); restoreCalibrationIdleTimer()
        sink.setCalibrationSampling(false)
        calibration.cancel(); state = referenceValid && calibration.canRecord ? .ready : .uncalibrated
        status = calibration.prompt
        if state == .uncalibrated { stopLiveCapture() }
    }
    private func restoreCalibrationIdleTimer() {
        if let value = calibrationIdleState { CapturePlatform.idleTimerDisabled = value; calibrationIdleState = nil }
    }
    private func receiveCalibrationSample(_ sample: MotionSample) {
        guard state == .calibrating, sample.receiptUptime >= calibrationStarted else { return }
        calibrationActivity = capture.clock.now()
        let previous = calibration.stage, count = calibration.countdown
        let retries = calibration.automaticRetries
        calibration.ingest(sample)
        if previous != calibration.stage || retries != calibration.automaticRetries { announceCalibrationStage() }
        else if count != calibration.countdown, calibration.stage == .preparingNeutral, calibration.countdown > 0 {
            speakCalibration(String(calibration.countdown))
        }
        finishCalibrationIfNeeded()
    }
    private func announceCalibrationStage() {
        status = calibration.prompt
        logCalibration("stage", details: status)
        if calibration.stage == .retry { sink.setCalibrationSampling(false); restoreCalibrationIdleTimer() }
        guard calibration.handsFree else { return }
        let stage = calibration.stage, generation = calibrationGeneration
        var words = calibration.prompt
        if stage == .ready { words += recordAfterCalibration ? " Recording starts now." : " You can start recording when ready." }
        speakCalibration(words) { [weak self] in
            guard let self, self.calibrationGeneration == generation, self.calibration.stage == stage else { return }
            self.calibration.guidanceDidFinish()
            if stage == .preparingNeutral { self.speakCalibration("3") }
            if stage == .ready && self.recordAfterCalibration { self.startRecording() }
        }
    }
    private func speakCalibration(_ text: String, completion: (() -> Void)? = nil) {
        logCalibration("speech", details: text)
        calibrationVoiceStarted = capture.clock.now()
        calibrationSpeechPending = true; calibrationSpeechSequence += 1
        let sequence = calibrationSpeechSequence, generation = calibrationGeneration
        let done = { [weak self] in
            guard let self, self.calibrationGeneration == generation, self.calibrationSpeechSequence == sequence else { return }
            self.calibrationSpeechPending = false
            self.calibrationVoiceFinished = self.capture.clock.now()
            completion?()
        }
        let started: Bool
        if let guidanceSpeech { started = guidanceSpeech(text, done) }
        else if synthetic { done(); started = true }
        else { started = calibrationSpeaker.say(text, completion: done) }
        if !started {
            calibrationSpeechPending = false; calibrationGeneration += 1; state = .calibrating
            calibration.fail(.invalidInput, message: "Audio unavailable. Check volume and audio output, then retry, or turn off spoken calibration.")
            status = calibration.prompt; sink.setCalibrationSampling(false); restoreCalibrationIdleTimer()
            logCalibration("audioFailure", details: status)
        }
    }
    private func logCalibration(_ event: String, details: String = "") {
        do {
            try calibrationLog.record(event, calibration: calibration, details: details)
            calibrationLogURL = calibrationLog.url; calibrationLogError = nil
        } catch {
            calibrationLogError = "Could not save calibration log: \(error.localizedDescription)"
        }
    }
    /// Driven by the host timer, so missing sensor callbacks cannot silence the guide.
    func updateCalibrationFeedback(now: Double) {
        guard state == .calibrating, calibration.handsFree, !calibrationSpeechPending,
              now - calibrationVoiceFinished >= 4, let words = calibration.waitingPrompt else { return }
        status = words
        // Reminder completion must not reset stage timing or pause sample collection.
        speakCalibration(words)
    }
    func changeCaptureMode(_ mode: CaptureMode) {
        guard !diagnosticPreparing, [.uncalibrated, .ready].contains(state), mode != captureMode else { return }
        captureMode = mode
        stopLiveCapture(); state = .uncalibrated
        status = "Capture mode changed. Calibrate the new reference frame."
    }
    func startRecording() {
        guard state == .ready, referenceValid, calibration.canRecord, canUseMotion, capturing else { return }
        beginRecording(diagnostic: false)
    }
    func startDiagnostics(mode: CaptureMode) {
        guard !diagnosticPreparing, [.uncalibrated, .ready].contains(state) else { return }
        guard mode == .rawOnly || canUseMotion else { status = "Device motion unavailable."; return }
        if mode != captureMode { changeCaptureMode(mode) }
        if !capturing { startCapture(writer: nil) }
        diagnosticPreparing = true; status = "Preparing diagnostics…"
        // Queue barrier follows any old stop/new start. Install the writer only afterward,
        // so callbacks from the previous capture cannot enter the new diagnostic master.
        capture.snapshot { [weak self] _ in
            DispatchQueue.main.async {
                guard let self, self.diagnosticPreparing, self.capturing,
                      [.uncalibrated, .ready].contains(self.state) else { return }
                self.diagnosticPreparing = false
                self.beginRecording(diagnostic: true)
                guard self.state == .recording else { return }
                self.diagnosticInstruction = "Remain stationary for 30 seconds"
                self.diagnosticMovementReady = false; self.diagnosticDrillIndex = 0
                self.appendEvent(EventMarker(kind: "diagnosticPhase", label: "STATIONARY", hostUptime: self.capture.clock.now(), note: "protocol rideform-diagnostics-1; mode " + mode.rawValue))
                self.diagnosticTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false) { [weak self] _ in
                    Task { @MainActor in self?.beginDiagnosticMovement() }
                }
            }
        }
    }
    private func beginDiagnosticMovement() {
        guard state == .recording, diagnosticsActive else { return }
        diagnosticTimer?.invalidate(); diagnosticTimer = nil
        diagnosticMovementReady = true
        appendEvent(EventMarker(kind: "diagnosticPhase", label: "MOVEMENT", hostUptime: capture.clock.now()))
        issueDiagnosticDrill()
    }
    func nextDiagnosticDrill() {
        guard state == .recording, diagnosticsActive, diagnosticMovementReady else { return }
        diagnosticDrillIndex += 1
        if diagnosticDrillIndex >= Self.diagnosticDrills.count { stopRecording(); return }
        issueDiagnosticDrill()
    }
    private func issueDiagnosticDrill() {
        diagnosticInstruction = Self.diagnosticDrills[diagnosticDrillIndex]
        appendEvent(EventMarker(kind: "diagnosticDrill", label: diagnosticInstruction, hostUptime: capture.clock.now(), note: "Instruction dispatch; not movement truth"))
    }
    private func beginRecording(diagnostic: Bool) {
        diagnosticsActive = diagnostic
        state = .recording; status = "Preparing recording…"
        do {
            let anchor = capture.clock.anchor(reason: "recording started")
            var system = utsname(); uname(&system)
            let machineCapacity = MemoryLayout.size(ofValue: system.machine)
            let model = withUnsafePointer(to: &system.machine) { pointer in
                pointer.withMemoryRebound(to: CChar.self, capacity: machineCapacity) { String(cString: $0) }
            }
            let manifest = SessionManifest(platform: CapturePlatform.platform, deviceModel: model, osVersion: CapturePlatform.osVersion,
                appVersion: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown",
                appBuild: Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown",
                boundaries: SessionBoundaries(started: anchor), clockMapping: capture.clock.mapping,
                rotationConvention: convention, calibrationID: referenceValid ? calibration.currentCalibration?.id : nil,
                analysisConfiguration: MotionAnalyzer.defaultConfiguration)
            try repository.begin(manifest: manifest, calibration: referenceValid ? calibration.currentCalibration : nil)
            sink.install(repository.writer)
            sessionWarning = nil
            started = anchor.hostUptime; elapsed = 0; eventSequence = 0; counts = [:]; previousRate = nil; lastCheckpoint = anchor.hostUptime
            idleBeforeRecording = CapturePlatform.idleTimerDisabled; ownsIdleTimer = true
            CapturePlatform.idleTimerDisabled = true
            status = "Recording"
            if audibleDrill && !diagnostic { cues.start { [weak self] event in self?.appendEvent(event) } }
        } catch { state = .saveError; status = "Cannot create session: \(error.localizedDescription)" }
    }
    func marker(_ label: String) {
        let tapped = capture.clock.now()
        guard state == .recording, repository.writer != nil else { return }
        appendEvent(EventMarker(label: label, hostUptime: tapped))
    }
    private func appendEvent(_ event: EventMarker) {
        guard state == .recording else { return }
        var value = event; value.sequence = eventSequence; eventSequence += 1
        repository.enqueue(value)
    }
    func stopRecording(interrupted: Bool = false) {
        guard state == .recording else { return }
        runtimeDiagnostic?("stopRequested")
        diagnosticTimer?.invalidate(); diagnosticTimer = nil
        if diagnosticsActive { appendEvent(EventMarker(kind: "diagnosticPhase", label: "END", hostUptime: capture.clock.now(), note: interrupted ? "interrupted" : "stopped")) }
        if cues.active { appendEvent(EventMarker(kind: "cueCancellation", label: interrupted ? "INTERRUPTED" : "STOPPED", hostUptime: capture.clock.now())) }
        cues.cancel(); state = .finalizing; status = interrupted ? "Saving interrupted recording…" : "Saving…"
        if ownsIdleTimer { CapturePlatform.idleTimerDisabled = idleBeforeRecording; ownsIdleTimer = false }
        stopFixture(); capturing = false; referenceValid = false
        capture.stop { [weak self] summary in
            DispatchQueue.main.async {
                guard let self else { return }
                self.runtimeDiagnostic?("captureStopped")
                guard self.repository.writer != nil else {
                    self.state = self.calibration.canRecord ? .ready : .uncalibrated; self.status = "Recording stopped before capture began."; return
                }
                var recordingSummary = summary
                recordingSummary.acceptedCounts = self.sink.detach()
                recordingSummary = self.normalized(recordingSummary)
                self.runtimeDiagnostic?("saveStarted")
                self.repository.finalize(summary: recordingSummary, interrupted: interrupted) { [weak self] result in self?.saveFinished(result) }
            }
        }
    }
    func retrySave() {
        guard state == .saveError else { return }
        guard repository.writer != nil else { state = referenceValid && calibration.canRecord ? .ready : .uncalibrated; status = "No session created. Retry recording."; return }
        state = .finalizing; repository.retrySave { [weak self] result in self?.saveFinished(result) }
    }
    private func saveFinished(_ result: Result<URL, Error>) {
        runtimeDiagnostic?("saveFinished: \(result)")
        switch result {
        case .success(let url): savedURL = url; state = .saved; status = sessionWarning.map { "Saved with warning: \($0)" } ?? "Saved"; refreshSessions()
        case .failure(let error): state = .saveError; status = "Save failed. Master retained for retry: \(error.localizedDescription)"
        }
    }
    func acknowledgeSaved() {
        guard state == .saved else { return }
        savedURL = nil; diagnosticsActive = false; diagnosticInstruction = ""; state = .uncalibrated; status = "Saved session preserved. Calibrate before another recording."
    }
    func pauseFailedCalibrationCapture() {
        guard state == .calibrating, calibration.stage == .retry, !calibrationSpeechPending else { return }
        stopLiveCapture()
    }
    func recordSceneTransition(_ phase: String) {
        appendEvent(EventMarker(kind: "lifecycle", label: phase, hostUptime: capture.clock.now()))
        if state == .calibrating { logCalibration("scene", details: phase) }
    }
    func sceneInterrupted() {
        calibrationSpeaker.cancel(); calibrationGeneration += 1; calibrationSpeechPending = false
        diagnosticPreparing = false
        if state == .recording { stopRecording(interrupted: true) }
        else if state == .calibrating { cancelCalibration(); stopLiveCapture() }
        else { stopLiveCapture() }
    }
    func resumePreview() {
        if state == .ready && !capturing { startCapture(writer: nil) }
    }
    private func stopLiveCapture() { stopFixture(); capturing = false; referenceValid = false; angles = nil; capture.stop { _ in }
        if state == .ready { state = .uncalibrated; status = "Capture interrupted. Recalibrate before recording." }
    }
    private func stopFixture() {
        #if targetEnvironment(simulator)
        fixture?.stop(); fixture = nil
        #endif
    }
    private func startCapture(writer: SessionWriter?) {
        capturing = true
        previousRate = nil; measuredHz = nil
        var lastUI = 0.0
        let onRecord: (CapturedRecord) -> Void = { [weak self] record in
            // Acquisition values reach the writer directly on capture's serial control queue.
            let calibrating = self?.sink.consume(record) ?? false
            guard case .motion(let sample) = record else { return }
            // Calibration receives every sample; live presentation alone is throttled.
            if calibrating { DispatchQueue.main.async { [weak self] in self?.receiveCalibrationSample(sample) } }
            if sample.receiptUptime - lastUI >= 0.1 {
                lastUI = sample.receiptUptime
                DispatchQueue.main.async { [weak self] in
                    guard let self else { return }
                    if self.referenceValid, let calibration = self.calibration.currentCalibration {
                        self.angles = (try? OrientationMath.riderMotion(sample: sample, calibration: calibration, convention: self.convention))?.displayAnglesRadians
                    }
                }
            }
        }
        #if targetEnvironment(simulator)
        if synthetic {
            capture.startSimulatorFixture(mode: captureMode, onRecord: onRecord)
            let source = SimulatorMotionFixture(capture: capture, mode: captureMode) { [weak self] in
                (self?.calibration.stage ?? .idle, self?.calibration.setupMethod ?? .guided)
            }
            fixture = source; source.start(); return
        }
        #endif
        capture.start(mode: captureMode, onRecord: onRecord) { [weak self] diagnostic in
            self?.sink.diagnostic(diagnostic)
            guard diagnostic.code == "acquisition_error" else { return }
            DispatchQueue.main.async {
                guard let self else { return }
                if self.state == .recording { self.stopRecording(interrupted: true) }
                else { self.cancelCalibration(); self.stopLiveCapture() }
                self.status = "Motion failed: \(diagnostic.message)"
            }
        }
    }
    private func refreshLive() {
        guard capturing else { return }
        if (state == .calibrating && calibration.isCalibrating) || (state == .ready && calibrationSpeechPending),
           capture.clock.now()-calibrationActivity > 15 || (calibrationSpeechPending && capture.clock.now()-calibrationVoiceStarted > 15) {
            calibrationGeneration += 1; snapshotPending = false; calibrationSpeaker.cancel(); calibrationSpeechPending = false; state = .calibrating
            calibration.fail(.tooShort, message: "Calibration stopped because sensor data or spoken guidance stalled. Retry when ready.", allowRecovery: false)
            announceCalibrationStage()
        }
        updateCalibrationFeedback(now: capture.clock.now())
        capture.snapshot { [weak self] summary in
            DispatchQueue.main.async {
                guard let self, self.capturing else { return }
                if self.referenceValid && (!summary.rotationConvention.verified || summary.rotationConvention.capturedToCanonical != self.convention.capturedToCanonical) {
                    let warning = "Attitude convention changed or became invalid. Recalibration required."
                    self.angles = nil
                    if self.state == .recording {
                        self.sessionWarning = warning
                        self.repository.enqueue(DiagnosticRecord(code: "rotation_convention_invalidated", message: warning, hostUptime: self.capture.clock.now(), stream: .motion))
                        self.stopRecording(interrupted: true)
                    } else { self.stopLiveCapture(); self.status = warning }
                    return
                }
                self.capabilities = self.normalized(summary).capabilities; self.counts = self.state == .recording ? self.sink.counts : summary.acceptedCounts
                let now = summary.ended.hostUptime, count = self.counts[self.captureMode == .rawOnly ? .gyroscope : .motion, default: 0]
                if let previous = self.previousRate, now - previous.time >= 1 {
                    self.measuredHz = Double(count - previous.count) / (now - previous.time)
                    self.previousRate = (now, count)
                } else if self.previousRate == nil { self.previousRate = (now, count) }
                if let started = self.started, self.state == .recording {
                    self.elapsed = now - started
                    if now - self.lastCheckpoint >= 5 {
                        self.lastCheckpoint = now
                        self.repository.checkpoint { [weak self] result in
                            if case .failure(let error) = result {
                                DispatchQueue.main.async {
                                    guard let self, self.state == .recording else { return }
                                    let warning = "Checkpoint failed; stopped and retried final save: \(error.localizedDescription)"
                                    self.sessionWarning = warning
                                    self.repository.enqueue(DiagnosticRecord(code: "checkpoint_failed", message: warning, hostUptime: self.capture.clock.now()))
                                    self.stopRecording(interrupted: true)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    private func normalized(_ summary: CaptureSummary) -> CaptureSummary {
        var result = summary
        if synthetic && captureMode == .rawOnly { result.capabilities[.motion] = .unavailable(reason: "raw-only diagnostics") }
        if synthetic && captureMode == .fusedOnly {
            result.capabilities[.accelerometer] = .unavailable(reason: "fused-only mode")
            result.capabilities[.gyroscope] = .unavailable(reason: "fused-only mode")
        }
        return result
    }
}

/// The lock makes installing/detaching the recording destination atomic relative to accepted callbacks.
/// Disk writes are still queued by SessionWriter from the off-main capture callback.
private final class RecordingSink {
    private let lock = NSLock()
    private var writer: SessionWriter?
    private var accepted: [StreamKind: Int] = [:]
    private var calibrating = false
    func setCalibrationSampling(_ value: Bool) { lock.lock(); defer { lock.unlock() }; calibrating = value }
    var counts: [StreamKind: Int] { lock.lock(); defer { lock.unlock() }; return accepted }
    func install(_ writer: SessionWriter?) { lock.lock(); defer { lock.unlock() }; self.writer = writer; accepted = [:] }
    func detach() -> [StreamKind: Int] { lock.lock(); defer { lock.unlock() }; writer = nil; return accepted }
    func diagnostic(_ value: DiagnosticRecord) { lock.lock(); defer { lock.unlock() }; writer?.enqueue(value) }
    @discardableResult func consume(_ record: CapturedRecord) -> Bool {
        lock.lock(); defer { lock.unlock() }
        guard let writer else { return calibrating }
        let kind: StreamKind
        switch record {
        case .motion(var sample):
            kind = .motion; sample.sequence = UInt64(accepted[kind, default: 0]); writer.enqueue(sample)
        case .accelerometer(var sample):
            kind = .accelerometer; sample.sequence = UInt64(accepted[kind, default: 0]); writer.enqueue(sample)
        case .gyroscope(var sample):
            kind = .gyroscope; sample.sequence = UInt64(accepted[kind, default: 0]); writer.enqueue(sample)
        }
        accepted[kind, default: 0] += 1
        return calibrating
    }
}
