import SwiftUI
import Charts
import UIKit
import RideFormCore

struct SessionReviewView: View {
    let url: URL
    @State private var session: MotionSession?
    @State private var analysis: AnalysisResult?
    @State private var rawDisplay: [AnalysisPoint] = []
    @State private var configuredDisplay: [AnalysisPoint] = []
    @State private var diagnostics: SensorDiagnosticResult?
    @State private var message: String?
    @State private var busy = false
    @State private var showRaw = false
    @State private var smoothing = false
    @State private var window = 0.12
    @State private var selection: Double?
    @State private var share: ReviewShareFiles?
    @State private var revision = 0
    var body: some View {
        VStack(alignment: .leading, spacing: 14) {
            if let session {
                Text("Saved session").font(.title2).accessibilityIdentifier("savedSession")
                Text(session.manifest.boundaries.status.uppercased()).bold()
                Text(session.manifest.boundaries.started.wallClock.formatted()).font(.caption)
                Text("\(session.events.count) events · calibration \(session.calibration?.verified == true ? "verified" : "unavailable")").accessibilityIdentifier("savedEventCount")
                Button("Export now") { export() }.buttonStyle(.borderedProminent).frame(minHeight: 52).disabled(busy).accessibilityIdentifier("exportSession")
                sampling(session)
                if let diagnostics { diagnosticSummary(diagnostics) }
                if session.calibration?.verified == true {
                    Toggle("Derived polynomial smoothing", isOn: $smoothing).disabled(busy).accessibilityIdentifier("analysisFilter")
                    HStack {
                        Text("Window \(window, specifier: "%.2f") s")
                        Slider(value: $window, in: 0.04...0.5, step: 0.02).disabled(busy).accessibilityIdentifier("filterWindow")
                    }
                    Button("Recompute from saved samples") { busy = true; revision += 1 }.buttonStyle(.bordered).disabled(busy).accessibilityIdentifier("recomputeAnalysis")
                    if let analysis { analysisReview(analysis, session: session) }
                } else { Text("Rider orientation and transition metrics unavailable until calibrated. Device-frame diagnostics and raw export remain available.").accessibilityIdentifier("analysisUnavailable") }
                Text("Cue times identify speech dispatch. Acoustic onset and physical latency are unmeasured.").font(.caption).foregroundStyle(.secondary)

            } else if message == nil { ProgressView("Loading saved session…") }
            if busy { ProgressView("Analyzing full saved data…") }
            if let message { Text(message).foregroundStyle(.red).accessibilityIdentifier("reviewError") }
        }
        .task { await load() }
        .task(id: revision) { if let session { await recompute(session) } }
        .sheet(item: $share) { ReviewActivityShare(files: $0.urls) }
    }
    private func load() async {
        let loaded = await Task.detached { Result {
            let value = try SessionIO.load(url)
            let diagnostics = value.events.contains(where: { $0.kind == "diagnosticPhase" }) ? try SensorDiagnostics.analyze(session: value) : nil
            return (value, diagnostics)
        } }.value
        guard !Task.isCancelled else { return }
        switch loaded {
        case .success(let value):
            session = value.0; diagnostics = value.1
            revision += 1
        case .failure(let error): message = error.localizedDescription
        }
    }
    private func recompute(_ input: MotionSession) async {
        guard input.calibration?.verified == true else { return }
        busy = true; message = nil
        var configuration = MotionAnalyzer.defaultConfiguration
        configuration.filterMethod = smoothing ? "localPolynomial" : "none"
        configuration.derivativeWindowSeconds = window
        let request = revision
        let result = await Task.detached { Result {
            let value = try MotionAnalyzer.analyze(session: input, configuration: configuration)
            return (value, TraceDisplayDecimator.decimate(value.rawTrace), TraceDisplayDecimator.decimate(value.configuredTrace))
        } }.value
        guard !Task.isCancelled, request == revision else { return }
        busy = false
        switch result {
        case .success(let value): analysis = value.0; rawDisplay = value.1; configuredDisplay = value.2
        case .failure(let error): message = "Analysis unavailable: \(error)"
        }
    }
    private func export() {
        busy = true; message = nil
        let configuration = analysis?.configuration
        Task {
            let result = await Task.detached { Result { try ShareExporter().prepare(bundle: url, analysisConfiguration: configuration) } }.value
            busy = false
            switch result { case .success(let files): share = ReviewShareFiles(urls: files); case .failure(let error): message = "Export failed: \(error.localizedDescription)" }
        }
    }
    @ViewBuilder private func sampling(_ input: MotionSession) -> some View {
        ForEach(input.manifest.streams, id: \.kind.rawValue) { stream in
            DisclosureGroup("\(stream.kind.rawValue) · n=\(stream.writtenCount) · mean \(number(stream.statistics.meanHz)) Hz") {
                let s = stream.statistics
                Text("Requested \(number(stream.requestedIntervalSeconds > 0 ? 1/stream.requestedIntervalSeconds : nil)) Hz; duration \(number(s.durationSeconds)) s; median \(number(s.medianHz)) Hz")
                Text("Interval min/max \(number(s.minimumIntervalSeconds))/\(number(s.maximumIntervalSeconds)) s; SD \(number(s.intervalStandardDeviationSeconds)) s; p95/p99 \(number(s.p95IntervalSeconds))/\(number(s.p99IntervalSeconds)) s")
                Text("Gaps \(s.gapCount) (\(number(s.gapPercentage))%); threshold \(number(s.gapThresholdSeconds)) s; nonpositive intervals \(s.nonpositiveIntervalCount). Estimated missing intervals \(s.estimatedMissingIntervals.map(String.init) ?? "unavailable").")
                Text("Writer accepted/written \(stream.acceptedCount)/\(stream.writtenCount). writer loss \(stream.lossCount). This measures accepted-minus-written, not hardware drops.")
                Text("Delivery age n=\(s.deliveryAgeSeconds.count); mean \(number(DiagnosticStatistic(s.deliveryAgeSeconds).mean)) s. Delivery age is not physical latency.")
                Text(String(describing: stream.capability))
            }.font(.caption).accessibilityIdentifier("sampling_\(stream.kind.rawValue)")
        }
    }
    @ViewBuilder private func analysisReview(_ result: AnalysisResult, session: MotionSession) -> some View {
        let points = showRaw ? rawDisplay : configuredDisplay
        Toggle("Show unfiltered measured trace", isOn: $showRaw).accessibilityIdentifier("showRawTrace")
        Text("Full data n=\(result.rawTrace.count) · displayed n=\(points.count) · \(showRaw ? "unfiltered display" : result.configuration.filterMethod), window \(result.configuration.derivativeWindowSeconds ?? 0, specifier: "%.2f") s").font(.caption).accessibilityIdentifier("analysisConfiguration")
        Text("Quality: \(result.qualityFlags.isEmpty ? "no analysis flags" : result.qualityFlags.joined(separator: ", "))").font(.caption)
        Text("Onset \(number(result.onsetThresholdRadiansPerSecond)) rad/s; minimum \(number(result.configuration.transitionThresholds["onsetDuration"])) s; settle speed \(number(result.configuration.transitionThresholds["settleSpeed"])) rad/s, angle variation \(number(result.configuration.transitionThresholds["settleAngleVariation"])) rad, dwell \(number(result.configuration.transitionThresholds["settleDwell"])) s; neutral band \(number(result.configuration.transitionThresholds["neutralBand"])) rad").font(.caption)
        ForEach(ReviewTimelineKind.allCases, id: \.rawValue) { kind in
            ReviewTimeline(kind: kind, points: points, result: result, mapping: session.manifest.clockMapping, selection: $selection)
        }
        if let selection { Text("Shared cursor \(number(selection)) sensor seconds").font(.caption) }
        DisclosureGroup("Markers and cues · n=\(result.events.count)") {
            ForEach(result.events, id: \.id) { event in Text("\(event.label) · \(event.kind) · host \(number(event.hostUptime)) s · dispatch \(number(event.audioDispatchUptime)) · scheduled \(number(event.scheduledUptime))").font(.caption) }
        }
        Text("Transitions · n=\(result.transitions.count)").font(.headline).accessibilityIdentifier("transitionCount")
        ForEach(result.transitions) { transition in transitionRow(transition) }
        Text("Distributions use complete, unflagged comparable legs; outbound and return are separate. Variance requires two transitions.").font(.caption)
        ForEach(Array(result.summary.groups.enumerated()), id: \.offset) { _, group in
            DisclosureGroup("\(group.kind) \(group.side) · range n=\(group.rangeRadians.sampleCount)") {
                distribution("Range", group.rangeRadians, units: "rad")
                distribution("Duration", group.durationSeconds, units: "s")
                distribution("Squared jerk", group.squaredJerk, units: "rad²/s⁵")
                distribution("Normalized jerk", group.normalizedSquaredJerk, units: "dimensionless")
                distribution("Peak jerk", group.peakJerk, units: "rad/s³")
                distribution("RMS jerk", group.rmsJerk, units: "rad/s³")
                distribution("High-frequency energy", group.highFrequencyEnergyRatio, units: "ratio")
            }.font(.caption)
        }
        ForEach(Array(result.summary.rangeAsymmetry.enumerated()), id: \.offset) { _, value in Text("\(value.kind) left − right range \(number(value.signedDifferenceRadians)) rad; normalized \(number(value.normalizedDifference))").font(.caption) }
        DisclosureGroup("Metric definitions and limitations") {
            Text("Jerk differentiates gyro twice and amplifies noise. Squared jerk integrates lateral jerk² over actual time; RMS divides by duration before square root; normalized jerk multiplies by T⁵/range². None is a validated riding-quality score.")
            Text("Spectral ratio uses a derived, interpolated grid, removes the mean, and applies the saved window. It is not measured data. Reversals, velocity peaks and corrections depend on thresholds and sampling. Filtering changes estimates and can hide corrections.")
            ForEach(result.configuration.metricFormulas.keys.sorted(), id: \.self) { key in Text("\(key): \(result.configuration.metricFormulas[key] ?? "")") }
            Text("Spectral grid \(number(result.configuration.resamplingParameters["gridHz"])) Hz, cutoff \(number(result.configuration.resamplingParameters["cutoffHz"])) Hz. Euler decomposition near ±90° forward tilt has singularity limits.")
        }.font(.caption)
    }
    @ViewBuilder private func transitionRow(_ t: RideFormCore.Transition) -> some View {
        DisclosureGroup("#\(t.id) \(t.kind) \(t.side) · range \(number(t.rangeRadians*180 / .pi))° · duration \(number(t.durationSeconds)) s") {
            Text("Group \(t.groupID); linked outbound \(t.linkedOutboundID.map(String.init) ?? "unavailable"). Onset \(number(t.onsetTimestamp)), peak \(number(t.peakTimestamp)), end deceleration \(number(t.endDecelerationTimestamp)), hold \(number(t.stableHoldTimestamp)) sensor s")
            Text("Start \(number(t.startAngleRadians*180 / .pi))°, end \(number(t.endAngleRadians.map { $0*180 / .pi }))°, peak \(number(t.peakAngleRadians*180 / .pi))°; settling \(number(t.settlingTimeSeconds)) s; overshoot \(number(t.overshootRadians.map { $0*180 / .pi }))°")
            Text("Corrections \(t.corrections.count); magnitudes \(t.corrections.map { number($0.magnitudeRadians*180 / .pi) }.joined(separator: ", "))°")
            metric("Integrated squared jerk (rad²/s⁵)", t.smoothness.integratedSquaredLateralJerk)
            metric("Peak jerk (rad/s³)", t.smoothness.peakLateralJerk)
            metric("RMS jerk (rad/s³)", t.smoothness.rmsLateralJerk)
            metric("Normalized jerk", t.smoothness.normalizedSquaredLateralJerk)
            metric("High-frequency ratio", t.smoothness.spectral.highFrequencyEnergyRatio)
            let p = t.smoothness.velocityProfile
            Text("Velocity peaks \(p.peakCount); peak \(number(p.peakSpeedRadiansPerSecond)) rad/s, timing fraction \(number(p.peakTimingFraction)); acceleration/deceleration \(number(p.accelerationDurationSeconds))/\(number(p.decelerationDurationSeconds)) s")
            Text("Spectrum: \(t.smoothness.spectral.gridHz) Hz derived grid, cutoff \(t.smoothness.spectral.cutoffHz) Hz, Nyquist \(number(t.smoothness.spectral.analyzedNyquistHz)) Hz; \(t.smoothness.spectral.interpolation); \(t.smoothness.spectral.window)")
            Text("Quality: \(t.qualityFlags.isEmpty ? "no flags" : t.qualityFlags.joined(separator: ", "))")
        }.font(.caption).accessibilityIdentifier("transition_\(t.id)")
    }
    private func metric(_ label: String, _ m: CandidateMetric) -> some View { Text("\(label): \(number(m.value))\(m.unavailableReason.map { " (\($0))" } ?? "")") }
    @ViewBuilder private func distribution(_ label: String, _ d: MetricDistribution, units: String) -> some View {
        Text("\(label) [\(units)] n=\(d.sampleCount); mean \(number(d.mean)), median \(number(d.median)), IQR \(number(d.interquartileRange)), SD \(number(d.standardDeviation)), variance \(number(d.variance))")
        if !d.values.isEmpty {
            Chart(Array(d.values.enumerated()), id: \.offset) { item in PointMark(x: .value(label, item.element), y: .value("Transition index", item.offset)) }.frame(height: 70)
        }
    }
    @ViewBuilder private func diagnosticSummary(_ d: SensorDiagnosticResult) -> some View {
        DisclosureGroup("Stationary sensor diagnostics") {
            Text("Host interval \(number(d.stationaryStartHostUptime))–\(number(d.stationaryEndHostUptime)) s; movement excluded. Population SD; all full saved stationary samples.")
            vectorStatistics("Fused gyro rad/s", d.fusedGyro)
            vectorStatistics("User acceleration g", d.userAcceleration)
            vectorStatistics("Raw gyro rad/s", d.rawGyro)
            vectorStatistics("Raw acceleration including gravity g", d.rawAccelerationIncludingGravity)
            statistic("Gravity norm g", d.gravityNorm)
            statistic("Quaternion-angle dispersion rad", d.quaternionAngleDispersionRadians)
            Text("Drift slope rad/s \(vector(d.driftSlopeRadiansPerSecond)); end−start rad \(vector(d.driftEndMinusStartRadians)); \(d.driftUnavailableReason ?? "first stationary device-reference rotation vector; no rider calibration needed")")
            Text("Quality: \(d.qualityFlags.joined(separator: ", "))")
            ForEach(d.stationarySampling.keys.sorted(), id: \.self) { key in
                if let s = d.stationarySampling[key] { Text("Stationary \(key) n=\(s.count) · mean \(number(s.meanHz)) Hz · p99 interval \(number(s.p99IntervalSeconds)) s · gaps \(s.gapCount)") }
            }
        }.font(.caption).accessibilityIdentifier("diagnosticSummary")
    }
    @ViewBuilder private func vectorStatistics(_ label: String, _ s: DiagnosticVectorStatistics) -> some View { statistic(label+" x",s.x); statistic(label+" y",s.y); statistic(label+" z",s.z) }
    private func statistic(_ label: String, _ s: DiagnosticStatistic) -> some View { Text("\(label): n=\(s.sampleCount); mean \(number(s.mean)), SD \(number(s.standardDeviation)), RMS \(number(s.rms))\(s.unavailableReason.map { " (\($0))" } ?? "")") }
    private func vector(_ v: Vector3?) -> String { v.map { "(\(number($0.x)), \(number($0.y)), \(number($0.z)))" } ?? "unavailable" }
}
private func number(_ value: Double?) -> String { value.map { String(format: "%.6g",$0) } ?? "unavailable" }

private enum ReviewTimelineKind: String, CaseIterable {
    case angle, velocity, angularAcceleration, userAcceleration
    var title: String {
        switch self { case .angle: return "Relative angle (°)"; case .velocity: return "Gyro angular velocity (rad/s)"; case .angularAcceleration: return "Angular acceleration (rad/s²)"; case .userAcceleration: return "User acceleration (g)" }
    }
    func vector(_ point: AnalysisPoint) -> Vector3? {
        switch self { case .angle: return OrientationMath.scale(point.anglesRadians,180 / .pi); case .velocity: return point.angularVelocity; case .angularAcceleration: return point.angularAcceleration; case .userAcceleration: return point.userAcceleration }
    }
}
private struct ReviewChartValue: Identifiable {
    var id: String
    var timestamp: Double
    var value: Double
    var axis: String
    var series: String
}
private struct ReviewBand: Identifiable { var id: String; var start: Double; var end: Double; var phase: String }
private struct ReviewTimeline: View {
    let kind: ReviewTimelineKind
    let points: [AnalysisPoint]
    let result: AnalysisResult
    let mapping: ClockMapping
    @Binding var selection: Double?
    private var domain: ClosedRange<Double> { let start = result.rawTrace.first?.timestamp ?? 0; return start...max(start+0.01,result.rawTrace.last?.timestamp ?? 1) }
    private var values: [ReviewChartValue] {
        points.flatMap { p -> [ReviewChartValue] in
            guard let v = kind.vector(p) else { return [] }
            return zip(["x","y","z"],[v.x,v.y,v.z]).map { axis,value in
                ReviewChartValue(id: "\(p.sequence)-\(axis)",timestamp: p.timestamp,value: value,axis: axis,series: "\(axis)-\(p.segmentID)")
            }
        }
    }
    private var bands: [ReviewBand] {
        var output: [ReviewBand] = []
        for (i,t) in result.transitions.enumerated() {
            let end = t.endDecelerationTimestamp ?? t.peakTimestamp
            let hold = t.stableHoldTimestamp ?? end
            output.append(ReviewBand(id: "\(i)a",start:t.onsetTimestamp,end:t.peakTimestamp,phase: "movement"))
            output.append(ReviewBand(id: "\(i)d",start:t.peakTimestamp,end:end,phase: "deceleration"))
            output.append(ReviewBand(id: "\(i)s",start:end,end:hold,phase: "settling"))
            let next = result.transitions.dropFirst(i+1).first { $0.segmentID == t.segmentID }?.onsetTimestamp
            let segmentEnd = result.rawTrace.last { $0.segmentID == t.segmentID }?.timestamp ?? hold
            if t.stableHoldTimestamp != nil { output.append(ReviewBand(id: "\(i)h",start:hold,end:next ?? segmentEnd,phase:"hold")) }
        }
        for pair in zip(result.rawTrace,result.rawTrace.dropFirst()) where pair.0.segmentID != pair.1.segmentID { output.append(ReviewBand(id:"gap\(pair.0.sequence)",start:pair.0.timestamp,end:pair.1.timestamp,phase:"gap")) }
        return output
    }
    var body: some View {
        VStack(alignment: .leading) {
            Text(kind.title).font(.headline)
            Chart {
                ForEach(bands) { b in RectangleMark(xStart:.value("Start",b.start),xEnd:.value("End",b.end)).foregroundStyle(b.phase == "gap" ? Color.red.opacity(0.18) : Color.blue.opacity(b.phase == "hold" ? 0.03 : 0.08)).annotation(position:.top) { if kind == .angle { Text(b.phase).font(.system(size:8)) } } }
                ForEach(values) { v in LineMark(x:.value("Sensor seconds",v.timestamp),y:.value(kind.title,v.value),series:.value("Segment",v.series)).foregroundStyle(by:.value("Axis",v.axis)) }
                ForEach(result.transitions) { t in RuleMark(x:.value("Peak",t.peakTimestamp)).foregroundStyle(.purple.opacity(0.5)).lineStyle(StrokeStyle(lineWidth:1,dash:[2,3])) }
                if mapping.validated, let offset = mapping.sensorToHostOffset {
                    ForEach(result.events, id: \.id) { e in RuleMark(x:.value("Event",(e.audioDispatchUptime ?? e.hostUptime)-offset)).foregroundStyle(.orange).annotation(position:.top) { if kind == .angle { Text(e.label).font(.system(size:8)).rotationEffect(.degrees(-45)) } } }
                }
                if let selection { RuleMark(x:.value("Cursor",selection)).foregroundStyle(.gray) }
            }.chartXScale(domain:domain).chartXSelection(value:$selection).chartForegroundStyleScale(["x":Color.blue,"y":Color.green,"z":Color.purple]).frame(height:190).accessibilityIdentifier("timeline_\(kind.rawValue)")
            if !mapping.validated { Text("Event alignment unavailable: clock mapping unvalidated.").font(.caption) }
            if kind == .angle { Text("x: lateral +left; y: forward; z: axial left turn. Other plots: fixed neutral rider-frame vector axes +X forward, +Y left, +Z up. Blue shading: detected phases; red: sampling gaps.").font(.caption) }
        }
    }
}
private struct ReviewShareFiles: Identifiable { let id = UUID(); let urls: [URL] }
private struct ReviewActivityShare: UIViewControllerRepresentable {
    let files: [URL]
    func makeUIViewController(context: Context) -> UIActivityViewController { UIActivityViewController(activityItems:files,applicationActivities:nil) }
    func updateUIViewController(_ controller: UIActivityViewController, context: Context) {}
}

#if targetEnvironment(simulator)
/// Explicit saved-data UI fixture; never used on a physical device or by normal launches.
enum ReviewUIFixture {
    static func create(in directory: URL, completion: @escaping (Result<URL, Error>) -> Void) throws {
        let calibration = Calibration(verified: true)
        let manifest = SessionManifest(deviceModel: "SYNTHETIC review fixture", boundaries: SessionBoundaries(started: ClockAnchor(hostUptime: 0)), clockMapping: ClockMapping(sensorToHostOffset: 0, validated: true), rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true), calibrationID: calibration.id, analysisConfiguration: MotionAnalyzer.defaultConfiguration)
        let url = directory.appendingPathComponent(manifest.id.uuidString+".rideform")
        let writer = try SessionWriter(directory: url, manifest: manifest, calibration: calibration)
        for i in 0...600 {
            let t = Double(i)*0.01
            let sensor = t+(i >= 301 ? 1 : 0)
            let phase = min(1,max(0,(t-0.5)))
            let angle = 0.25*(1-cos(phase * .pi))/2
            let speed = t > 0.5 && t < 1.5 ? 0.25 * .pi*sin(phase * .pi)/2 : 0
            let q = OrientationMath.quaternion(rotationVector:Vector3(x:-angle))
            let a = OrientationMath.rotate(q,Vector3(x:1)), b = OrientationMath.rotate(q,Vector3(y:1)), c = OrientationMath.rotate(q,Vector3(z:1))
            let matrix = RotationMatrix(m11:a.x,m12:b.x,m13:c.x,m21:a.y,m22:b.y,m23:c.y,m31:a.z,m32:b.z,m33:c.z)
            writer.enqueue(MotionSample(sequence:UInt64(i),sensorTimestamp:sensor,receiptUptime:sensor,
                attitudeQuaternion:q,attitudeRotationMatrix:matrix,rotationRate:Vector3(x:-speed,y:0.01*sin(t),z:0.02*cos(t)),gravity:OrientationMath.rotate(OrientationMath.inverse(q),Vector3(z:-1)),userAcceleration:Vector3(x:0.02*sin(t),y:0.03*cos(t))))
        }
        writer.enqueue(EventMarker(sequence:0,label:"LEFT",hostUptime:0.6))
        writer.enqueue(EventMarker(sequence:1,kind:"cue",label:"CENTER",hostUptime:2,scheduledUptime:1.99,audioDispatchUptime:2))
        var final = manifest
        final.boundaries.status = "completed"; final.boundaries.ended = ClockAnchor(hostUptime:7)
        final.streams[0].acceptedCount = 601
        writer.finalize(manifest:final,completion:completion)
    }
}
#endif
