import SwiftUI
import WatchKit
import Darwin

@main
struct RideFormWatchApp: App {
    @StateObject private var controller = RecordingController()
    @StateObject private var runtime = WatchCaptureRuntime()
    @StateObject private var transfer = WatchSessionTransfer()
    @Environment(\.scenePhase) private var scenePhase
    @State private var probeStarted = false
    var body: some Scene {
        WindowGroup {
            NavigationStack { WatchRecordingView(controller: controller, transfer: transfer, runtimeCheck: { runtime.runProbe(seconds: 600) }) }
                .task {
                    runtime.controller = controller
                    controller.runtimeDiagnostic = { [weak controller] event in
                        guard let controller else { return }
                        WatchRuntimeJournal.write(event, controller: controller)
                    }
                    WatchRuntimeJournal.write("viewAppeared", controller: controller)
                    #if DEBUG
                    guard !probeStarted, ProcessInfo.processInfo.arguments.contains("--runtime-probe") else { return }
                    probeStarted = true
                    runtime.runProbe(seconds: 120)
                    #endif
                }
                .safeAreaInset(edge: .bottom) {
                    if let failure = runtime.failure { Text(failure).font(.caption).padding(4) }
                }
                .onChange(of: controller.state) { _, state in
                    WatchRuntimeJournal.write("stateChanged", controller: controller)
                    runtime.update(state)
                }
                .onChange(of: scenePhase) { _, phase in
                    controller.watchSceneChanged(phase, backgroundRuntime: runtime.isRunning)
                }
                .onChange(of: controller.calibration.stage) { _, _ in
                    WKInterfaceDevice.current().play(.notification)
                    runtime.updateCalibration()
                }
                .onChange(of: controller.calibrationSpeechPending) { _, _ in runtime.updateCalibration() }
        }
    }
}

@MainActor
extension RecordingController {
    func watchSceneChanged(_ phase: ScenePhase, backgroundRuntime: Bool = false) {
        WatchRuntimeJournal.write("scene_\(phase)", controller: self)
        switch phase {
        case .active:
            recordSceneTransition("ACTIVE")
            resumePreview()
        case .inactive:
            // Dimming can leave us frontmost. Preserve acquisition and speech;
            // the saved timestamps will establish whether delivery continued.
            recordSceneTransition("INACTIVE")
        case .background:
            recordSceneTransition("BACKGROUND")
            if !backgroundRuntime { sceneInterrupted() }
        @unknown default:
            recordSceneTransition("UNKNOWN")
            sceneInterrupted()
        }
    }
}

/// Small, separate launch/lifecycle evidence survives an incomplete recording.
@MainActor
private enum WatchRuntimeJournal {
    private static let launchID = UUID().uuidString
    private static let timeScale: Double = {
        var info = mach_timebase_info_data_t()
        mach_timebase_info(&info)
        return Double(info.numer) / Double(info.denom) / 1_000_000_000
    }()
    static func write(_ event: String, controller: RecordingController) {
        let record: [String: Any] = [
            "event": event, "launchID": launchID, "pid": ProcessInfo.processInfo.processIdentifier,
            "continuousSeconds": Double(mach_continuous_time()) * timeScale,
            "uptime": ProcessInfo.processInfo.systemUptime, "wallTime": Date().timeIntervalSince1970,
            "applicationState": WKApplication.shared().applicationState.rawValue,
            "state": controller.state.rawValue, "motionCount": controller.counts[.motion, default: 0],
            "build": Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
        ]
        do {
            let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
                .appendingPathComponent("RuntimeLogs", isDirectory: true)
            try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
            let url = directory.appendingPathComponent(launchID + ".jsonl")
            if !FileManager.default.fileExists(atPath: url.path) { try Data().write(to: url) }
            let handle = try FileHandle(forWritingTo: url)
            defer { try? handle.close() }
            try handle.seekToEnd()
            var data = try JSONSerialization.data(withJSONObject: record, options: .sortedKeys)
            data.append(0x0a)
            try handle.write(contentsOf: data)
            try handle.synchronize()
        } catch { NSLog("RideForm runtime log failed: %@", error.localizedDescription) }
    }
}

/// The guided stationary drill is a range-of-motion exercise. This session is
/// bounded by watchOS; it is not a general-purpose or indefinite background mode.
@MainActor
final class WatchCaptureRuntime: NSObject, ObservableObject, WKExtendedRuntimeSessionDelegate {
    @Published private(set) var isRunning = false
    @Published private(set) var failure: String?
    weak var controller: RecordingController?
    private var session: WKExtendedRuntimeSession?
    private var probeTask: Task<Void, Never>?
    private var probeToken = UUID()

    func runProbe(seconds: Int) {
        guard probeTask == nil, let controller,
              [.uncalibrated, .ready].contains(controller.state) else { return }
        // Start synchronously from the Watch button's user interaction.
        start()
        let token = UUID()
        probeToken = token
        probeTask = Task { [weak self, weak controller] in
            guard let self, let controller else { return }
            defer { if self.probeToken == token { self.probeTask = nil } }
            do {
                for _ in 0..<50 {
                    if self.isRunning || self.failure != nil { break }
                    try await Task.sleep(for: .milliseconds(100))
                }
                guard self.isRunning else {
                    WatchRuntimeJournal.write("probeRuntimeUnavailable", controller: controller)
                    self.stop()
                    return
                }
                WatchRuntimeJournal.write("probeStart duration=\(seconds)", controller: controller)
                controller.startDiagnostics(mode: .fusedOnly)
                let deadline = ContinuousClock.now.advanced(by: .seconds(seconds))
                while ContinuousClock.now < deadline {
                    try await Task.sleep(for: .seconds(10))
                    WatchRuntimeJournal.write("probeTick", controller: controller)
                    if controller.state != .recording { break }
                }
                controller.stopRecording()
                WatchRuntimeJournal.write("probeStop", controller: controller)
            } catch {
                guard self.probeToken == token else { return }
                WatchRuntimeJournal.write("probeCancelled", controller: controller)
                controller.sceneInterrupted()
                if controller.state != .finalizing { self.stop() }
            }
        }
    }

    func update(_ state: RecordingController.State) {
        switch state {
        case .calibrating, .recording: start()
        case .ready: break
        case .finalizing: retireProbe() // Retain execution through final save.
        case .uncalibrated, .saved, .saveError: retireProbe(); stop()
        }
    }
    private func retireProbe() {
        probeToken = UUID()
        probeTask?.cancel(); probeTask = nil
    }
    func updateCalibration() {
        guard let controller, controller.state == .calibrating else { return }
        if controller.calibration.isCalibrating { start(); return }
        guard controller.calibration.stage == .retry, !controller.calibrationSpeechPending else { return }
        controller.pauseFailedCalibrationCapture()
        stop()
    }
    func handleRuntimeInvalidation(_ reason: String, message: String = "Open RideForm to start a new session.") {
        log(reason)
        stop()
        failure = "Recording runtime ended. " + message
        probeTask?.cancel()
        controller?.sceneInterrupted()
    }
    func start() {
        guard session == nil else { return }
        #if targetEnvironment(simulator)
        // Simulator fixtures cannot establish a physical execution grant.
        if controller?.synthetic == true { isRunning = true; return }
        #endif
        failure = nil
        let next = WKExtendedRuntimeSession()
        session = next; next.delegate = self
        log("runtimeRequested")
        next.start()
    }
    func stop() {
        let old = session
        session = nil; isRunning = false
        if let old, old.state != .invalid { old.invalidate() }
    }
    private func log(_ event: String) {
        guard let controller else { return }
        WatchRuntimeJournal.write(event, controller: controller)
        controller.recordSceneTransition(event)
    }
    nonisolated func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
        Task { @MainActor [weak self] in
            guard let self, self.session === extendedRuntimeSession else { return }
            self.isRunning = true
            self.log("runtimeGranted expiration=\(String(describing: extendedRuntimeSession.expirationDate))")
        }
    }
    nonisolated func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
        Task { @MainActor [weak self] in
            guard let self, self.session === extendedRuntimeSession else { return }
            self.log("runtimeWillExpire")
            self.failure = "Session time is ending. Saving your recording."
            self.controller?.sceneInterrupted()
        }
    }
    nonisolated func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession,
        didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
        Task { @MainActor [weak self] in
            guard let self, self.session === extendedRuntimeSession else { return }
            self.handleRuntimeInvalidation("runtimeInvalidated reason=\(reason.rawValue) error=\(String(describing: error))", message: error?.localizedDescription ?? "Open RideForm to start a new session.")
        }
    }
}
