#!/usr/bin/env python3
"""Independent U6 numeric oracle; compiles core into an isolated temporary probe.

Run from any directory. JSON artifacts remain in the printed /tmp directory.
No package tests, shared build directory, or master session are changed.
"""
import hashlib
import json
import math
from pathlib import Path
import shutil
import subprocess
import tempfile

ROOT = Path(__file__).resolve().parents[1]
OUT = Path(tempfile.mkdtemp(prefix="rideform-u6-independent-", dir="/tmp"))
SWIFT = r'''
import Foundation
let out = URL(fileURLWithPath: CommandLine.arguments[1])
func save<T: Encodable>(_ x: T, _ name: String) throws {
    let e = JSONEncoder(); e.outputFormatting = [.sortedKeys]
    try e.encode(x).write(to: out.appendingPathComponent(name))
}
func session(_ samples: [MotionSample]) -> MotionSession {
    MotionSession(manifest: SessionManifest(rotationConvention: RotationConvention(capturedToCanonical: "identity", verified: true)), motion: MotionStream(samples: samples), calibration: Calibration(verified: true))
}
func point(_ t: Double, _ angle: Double, _ speed: Double, _ jerk: Double = 1.2, _ segment: Int = 0) -> AnalysisPoint {
    AnalysisPoint(timestamp: t, sequence: UInt64((t*1000).rounded()), segmentID: segment, anglesRadians: Vector3(x: angle), angularVelocity: Vector3(x: -speed), angularAcceleration: Vector3(), angularJerk: Vector3(x: -jerk), userAcceleration: Vector3())
}
func path(_ knots: [(Double,Double)]) -> [AnalysisPoint] {
    (0...Int(knots.last!.0/0.025)).map { i in
        let t = Double(i)*0.025
        let k = (0..<(knots.count-1)).last(where: { knots[$0].0 <= t+1e-12 }) ?? 0
        let a = knots[k], b = knots[k+1], v = (b.1-a.1)/(b.0-a.0)
        return point(t, a.1+v*(t-a.0), v)
    }
}
var c = MotionAnalyzer.defaultConfiguration
c.filterParameters["maximumGapSeconds"] = 0.5
let times = [0.0, 0.04, 0.11, 0.19, 0.28, 0.36, 0.47, 0.58, 0.70]
let samples = times.enumerated().map { i,t in
    MotionSample(sequence: UInt64(i), sensorTimestamp: t, attitudeQuaternion: Quaternion(x: sin(-0.2*t*t*t/2), w: cos(-0.2*t*t*t/2)), rotationRate: Vector3(x: -0.6*t*t))
}
let original = session(samples)
try save(original, "input.json")
let loaded = try JSONDecoder().decode(MotionSession.self, from: Data(contentsOf: out.appendingPathComponent("input.json")))
let result = try MotionAnalyzer.analyze(session: loaded, configuration: c)
try save(result, "analysis.json")
try save(SmoothnessMetrics.compute(trace: result.rawTrace, excursionRadians: 0.2*pow(times.last!,3), configuration: result.configuration), "metrics.json")
let master = out.appendingPathComponent("master")
try FileManager.default.createDirectory(at: master, withIntermediateDirectories: true)
try Data(contentsOf: out.appendingPathComponent("input.json")).write(to: master.appendingPathComponent("input.json"))
let before = try Data(contentsOf: master.appendingPathComponent("input.json"))
let saved = try MotionAnalyzer.analyzeAndSave(session: loaded, configuration: c, masterURL: master, sidecarURL: out.appendingPathComponent("derived/result.json"))
try save(saved.configuration, "configuration.json")
var safety: [String: Bool] = ["repeatEqual": result == (try MotionAnalyzer.analyze(session: loaded, configuration: result.configuration)), "masterBytesUnchanged": before == (try Data(contentsOf: master.appendingPathComponent("input.json")))]
do { _ = try MotionAnalyzer.analyzeAndSave(session: loaded, configuration: c, masterURL: master, sidecarURL: master.appendingPathComponent("inside.json")); safety["insideRejected"] = false } catch AnalysisError.sidecarInsideMaster { safety["insideRejected"] = true }
let alias = out.appendingPathComponent("alias")
try FileManager.default.createSymbolicLink(at: alias, withDestinationURL: master)
do { _ = try MotionAnalyzer.analyzeAndSave(session: loaded, configuration: c, masterURL: master, sidecarURL: alias.appendingPathComponent("inside.json")); safety["symlinkRejected"] = false } catch AnalysisError.sidecarInsideMaster { safety["symlinkRejected"] = true }
var changed = loaded; changed.events = [EventMarker(sequence: 12, hostUptime: 0.2)]
safety["eventChangesHash"] = try DerivedSidecar<AnalysisResult>.identity(changed) != result.inputSHA256
try save(safety, "safety.json")
var filtered = c; filtered.filterMethod = "localPolynomial"; filtered.derivativeWindowSeconds = 0.7
try save(MotionAnalyzer.analyze(session: loaded, configuration: filtered), "filtered.json")
let summaryLegs = [0.2,0.6,-0.2,-0.2].flatMap { a in TransitionDetector.detect(trace: path([(0,0),(0.8,0),(1.8,a),(2.5,a)]), configuration: c, onsetThreshold: c.transitionThresholds["onsetSpeed"]!) }
try save(SessionSummary(transitions: summaryLegs), "summary.json")
try save(TransitionDetector.detect(trace: path([(0,0.4),(0.8,0.4),(2.8,-0.4),(3.5,-0.4)]), configuration: c, onsetThreshold: c.transitionThresholds["onsetSpeed"]!), "direct-cross.json")
try save(MotionAnalyzer.analyze(session: session([])), "empty.json")
let rx = 0.3, ry = -0.2, rz = 0.4
let q = Quaternion(x: sin(rx/2)*cos(ry/2)*cos(rz/2)-cos(rx/2)*sin(ry/2)*sin(rz/2), y: cos(rx/2)*sin(ry/2)*cos(rz/2)+sin(rx/2)*cos(ry/2)*sin(rz/2), z: cos(rx/2)*cos(ry/2)*sin(rz/2)-sin(rx/2)*sin(ry/2)*cos(rz/2), w: cos(rx/2)*cos(ry/2)*cos(rz/2)+sin(rx/2)*sin(ry/2)*sin(rz/2))
try save(MotionAnalyzer.analyze(session: session([MotionSample(attitudeQuaternion: q, rotationRate: Vector3(x: 1,y: 2,z: 3), userAcceleration: Vector3(x: 4,y: 5,z: 6))])), "orientation.json")
let phases = path([(0,0),(0.8,0),(1.8,0.4),(2.5,0.4),(3.5,0),(4.2,0),(5.2,-0.3),(6,-0.3)])
try save(TransitionDetector.detect(trace: phases, configuration: c, onsetThreshold: c.transitionThresholds["onsetSpeed"]!), "phases.json")
let corrections = path([(0,0),(0.8,0),(1.8,0.48),(2.05,0.35),(2.3,0.4),(3,0.4),(4,0),(5,0)])
try save(TransitionDetector.detect(trace: corrections, configuration: c, onsetThreshold: c.transitionThresholds["onsetSpeed"]!), "corrections.json")
try save(MetricDistribution(values: [1,2,4,8]), "distribution.json")
let spectra = (0..<100).map { i in point(Double(i)/50, 0, cos(.pi*Double(i)), 0) }
var rectangular = c; rectangular.resamplingParameters["windowHann"] = 0.1
try save(SmoothnessMetrics.compute(trace: spectra, excursionRadians: 1, configuration: rectangular), "nyquist.json")
let mixed = (0..<151).map { i in let t = Double(i)/50; return point(t, 0, sin(2 * .pi*2*t)+0.5*sin(2 * .pi*10*t), 0) }
try save(SmoothnessMetrics.compute(trace: mixed, excursionRadians: 1, configuration: c), "mixed-spectrum.json")
let limited = (0..<21).map { i in point(Double(i)*0.1, 0, sin(Double(i)), 0) }
try save(SmoothnessMetrics.compute(trace: limited, excursionRadians: 1, configuration: c), "limited-spectrum.json")
let truncated = path([(0,0),(1,0.4)])
try save(TransitionDetector.detect(trace: truncated, configuration: c, onsetThreshold: c.transitionThresholds["onsetSpeed"]!), "boundary-truncated.json")
var gap = result.rawTrace; for i in 5..<gap.count { gap[i].segmentID = 1 }
try save(SmoothnessMetrics.compute(trace: gap, excursionRadians: 1, configuration: c), "gap-metrics.json")
'''
(OUT / "main.swift").write_text(SWIFT)
(OUT / "source").mkdir()
sources = sorted((ROOT / "Packages/RideFormCore/Sources/RideFormCore").glob("*.swift"))
source_hashes = {}
for source in sources:
    target = OUT / "source" / source.name
    shutil.copy2(source, target)
    source_hashes[source.name] = hashlib.sha256(target.read_bytes()).hexdigest()
subprocess.run([
    "/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc",
    "-sdk", "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk",
    "-target", "arm64-apple-macosx13.0",
    "-module-cache-path", str(OUT / "module-cache"),
    *map(str, sorted((OUT / "source").glob("*.swift"))),
    str(OUT / "main.swift"), "-o", str(OUT / "probe"),
], check=True)
subprocess.run([str(OUT / "probe"), str(OUT)], check=True)

def read(name):
    return json.loads((OUT / name).read_text())

def close(a, b, tolerance=1e-8):
    assert abs(a-b) <= tolerance, (a, b)

checks = []
failures = []

def check(name, action):
    try:
        action()
        checks.append(name)
    except AssertionError as error:
        failures.append({"check": name, "detail": str(error)})

def numeric():
    trace = read("analysis.json")["rawTrace"]
    for p in trace:
        t = p["timestamp"]
        close(p["anglesRadians"]["x"], 0.2*t**3)
        close(p["angularAcceleration"]["x"], -1.2*t)
        close(p["angularJerk"]["x"], -1.2)
    m = read("metrics.json")
    close(m["integratedSquaredLateralJerk"]["value"], 1.44*0.7)
    close(m["peakLateralJerk"]["value"], 1.2)
    close(m["rmsLateralJerk"]["value"], 1.2)
    close(m["normalizedSquaredLateralJerk"]["value"], 36)

def orientation():
    # Matrix Rz(.4) Ry(-.2) Rx(.3), independent of production quaternion math.
    x,y,z = .3,-.2,.4
    cx,sx,cy,sy,cz,sz = math.cos(x),math.sin(x),math.cos(y),math.sin(y),math.cos(z),math.sin(z)
    matrix = [[cz*cy,cz*sy*sx-sz*cx,cz*sy*cx+sz*sx],[sz*cy,sz*sy*sx+cz*cx,sz*sy*cx-cz*sx],[-sy,cy*sx,cy*cx]]
    p = read("orientation.json")["rawTrace"][0]
    for axis, expected in zip("xyz", [-x,y,z]): close(p["anglesRadians"][axis], expected)
    for field, vector in [("angularVelocity", [1,2,3]), ("userAcceleration", [4,5,6])]:
        for axis,row in zip("xyz",matrix): close(p[field][axis], sum(a*b for a,b in zip(row,vector)))

def phases():
    legs = read("phases.json")
    assert [x["kind"] for x in legs] == ["outbound","return","outbound"]
    assert [x["side"] for x in legs] == ["left","left","right"]
    for leg,start,end in zip(legs,[.8,2.5,4.2],[1.8,3.5,5.2]):
        close(leg["onsetTimestamp"],start)
        close(leg["endDecelerationTimestamp"],end)
        close(leg["stableHoldTimestamp"],end+.4)
        close(leg["durationSeconds"],1)
    assert legs[1]["linkedOutboundID"] == legs[0]["id"]
    correction = read("corrections.json")[0]
    close(correction["overshootRadians"],.08)
    assert len(correction["corrections"]) == 2
    for run, magnitude in zip(correction["corrections"],[.13,.05]): close(run["magnitudeRadians"],magnitude)

def distribution():
    d = read("distribution.json")
    for key,value in [("sampleCount",4),("mean",3.75),("median",3),("interquartileRange",3.25),("variance",115/12)]: close(d[key],value)

def safety():
    assert all(value for key,value in read("safety.json").items() if key != "symlinkRejected"), read("safety.json")
    s = read("derived/result.json")
    assert s["inputSHA256"] == hashlib.sha256((OUT/"input.json").read_bytes()).hexdigest()
    assert s["configurationSHA256"] == hashlib.sha256((OUT/"configuration.json").read_bytes()).hexdigest()
    assert (OUT/"input.json").read_bytes() == (OUT/"master/input.json").read_bytes()

def symlink():
    assert read("safety.json")["symlinkRejected"], sorted(x.name for x in (OUT/"master").iterdir())

def summary_and_filter():
    s = read("summary.json")
    left,right = s["groups"][:2]
    close(left["rangeRadians"]["mean"],.4)
    close(left["rangeRadians"]["variance"],.08)
    close(right["rangeRadians"]["mean"],.2)
    close(s["rangeAsymmetry"][0]["normalizedDifference"],2/3)
    assert "normalizedDifference" not in s["rangeAsymmetry"][1]
    for group in s["groups"][2:]:
        assert group["rangeRadians"]["sampleCount"] == 0
        assert "mean" not in group["rangeRadians"]
    for point in read("filtered.json")["configuredTrace"]:
        assert point["filterApplied"]
        close(point["angularAcceleration"]["x"],-1.2*point["timestamp"])
        close(point["angularJerk"]["x"],-1.2)
    direct = read("direct-cross.json")[0]
    assert direct["kind"] == "directCross" and "direct_crossing" in direct["qualityFlags"]
    assert "empty_motion" in read("empty.json")["qualityFlags"]
    truncated = read("boundary-truncated.json")[0]
    assert "durationSeconds" not in truncated and "overshootRadians" not in truncated
    assert truncated["smoothness"]["integratedSquaredLateralJerk"]["unavailableReason"] == "transition_endpoint_unavailable"

def spectrum():
    s = read("mixed-spectrum.json")["spectral"]
    values = s["lateralVelocity"]
    centered = [v-sum(values)/len(values) for v in values]
    windowed = [v*.5*(1-math.cos(2*math.pi*i/(len(values)-1))) for i,v in enumerate(centered)]
    energies = []
    for k in range(1,len(values)//2+1):
        real = sum(v*math.cos(2*math.pi*k*i/len(values)) for i,v in enumerate(windowed))
        imag = sum(v*math.sin(2*math.pi*k*i/len(values)) for i,v in enumerate(windowed))
        energies.append((k*50/len(values),real*real+imag*imag))
    ratio = sum(e for f,e in energies if f>5)/sum(e for f,e in energies)
    close(s["highFrequencyEnergyRatio"]["value"], ratio, 1e-10)
    assert read("limited-spectrum.json")["spectral"]["highFrequencyEnergyRatio"]["unavailableReason"] == "ineffective_sampling_bandwidth"
    assert read("gap-metrics.json")["integratedSquaredLateralJerk"]["unavailableReason"] == "sampling_gap"

def nyquist():
    metric = read("nyquist.json")["spectral"]["highFrequencyEnergyRatio"]
    assert metric.get("value") is not None, metric
    close(metric["value"],1)

def boundary():
    flags = read("boundary-truncated.json")[0]["qualityFlags"]
    assert "onset_at_segment_boundary" in flags, flags

for name,action in [("irregular analytic derivatives/integrals",numeric),("Euler/matrix orientation",orientation),("phase/return/corrections",phases),("independent distribution",distribution),("SHA256/config/master bytes",safety),("summary/filter/direct-cross/empty/incomplete",summary_and_filter),("independent DFT/bandwidth/gap",spectrum),("symlink master isolation",symlink),("even-grid Nyquist energy",nyquist),("truncated boundary onset flag",boundary)]:
    check(name,action)
report = {"artifacts":str(OUT),"sourceSHA256":source_hashes,"passed":checks,"failures":failures}
(OUT/"report.json").write_text(json.dumps(report,indent=2)+"\n")
print(json.dumps(report,indent=2))
raise SystemExit(bool(failures))
