import ctypes
import functools
import hashlib
import json
from pathlib import Path
import statistics
import threading
import time

import numpy as np
import scipy.linalg
import app.byte_tracking as adapter
from app.contracts.frames import DetectionObservation, FrameEnvelope
from supervision.tracker.byte_tracker import core, matching
from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter

R=Path('/evidence');INPUT=Path('/input/fp16.jsonl')
data=[json.loads(line) for line in INPUT.open()]
assert len(data)==200 and all(len(r['detections'])==300 for r in data)
frames=[]
for i,row in enumerate(data):
    f=FrameEnvelope('fixture','fixture','fixture','fixture',1,i,i*100000000,0,1280,720)
    frames.append(tuple(DetectionObservation(f,'RFDETRMedium',int(c),s,(x/1280,y/720,(x+w)/1280,(y+h)/720))
                        for c,s,x,y,w,h in row['detections']))

local=threading.local()
originals=[]
def install():
    targets=[(KalmanFilter,'multi_predict','kalman_predict'),(KalmanFilter,'predict','kalman_predict_one'),
        (KalmanFilter,'update','kalman_update'),(KalmanFilter,'project','kalman_project'),
        (matching,'iou_distance','iou_distance'),(matching,'linear_assignment','matching'),
        (matching,'fuse_score','fuse_score'),(core,'box_iou_batch','output_iou'),
        (adapter.ByteTracker,'_prepare_observations','unused')]
    for owner,name,label in targets[:-1]:
        original=getattr(owner,name);originals.append((owner,name,original))
        def wrap(fn,tag):
            @functools.wraps(fn)
            def measured(*args,**kwargs):
                start,cpu=time.monotonic_ns(),time.thread_time_ns()
                try:return fn(*args,**kwargs)
                finally:
                    if hasattr(local,'costs'):
                        d=local.costs.setdefault(tag,[0,0,0]);d[0]+=1;d[1]+=time.monotonic_ns()-start;d[2]+=time.thread_time_ns()-cpu
            return measured
        setattr(owner,name,wrap(original,label))
def uninstall():
    while originals:
        owner,name,original=originals.pop();setattr(owner,name,original)
def state(tracker):
    return {str(c):dict(clock=t.frame_id,tracked=len(t.tracked_tracks),lost=len(t.lost_tracks),removed=len(t.removed_tracks)) for c,t in tracker._trackers.items()}
def stats(values):
    values=sorted(values);return dict(mean=statistics.mean(values),p95=values[int(np.ceil(len(values)*.95))-1],maximum=max(values))
def run(workers):
    results=[None]*workers;errors=[]
    start=threading.Barrier(workers);instrument=threading.Barrier(workers,action=install)
    def worker(index):
        try:
            tracker=adapter.ByteTracker(adapter.TrackingSettings(frame_rate=10))
            elapsed=[];cpu_times=[];outputs=[];local.costs={}
            start.wait(20)
            for seq,obs in enumerate(frames):
                if seq==180:instrument.wait(30)
                wall,cpu=time.monotonic_ns(),time.thread_time_ns();outputs.append(tracker.update(obs))
                elapsed.append((time.monotonic_ns()-wall)/1e6);cpu_times.append((time.thread_time_ns()-cpu)/1e6)
            results[index]=dict(uninstrumented_wall_ms=stats(elapsed[20:180]),uninstrumented_cpu_ms=stats(cpu_times[20:180]),
                instrumented_wall_ms=stats(elapsed[180:]),instrumented_cpu_ms=stats(cpu_times[180:]),
                costs={k:dict(calls=v[0],wall_ms=v[1]/1e6,cpu_ms=v[2]/1e6) for k,v in local.costs.items()},
                outputs=outputs,state=state(tracker),thread_id=threading.get_native_id())
        except Exception as error:
            import traceback
            errors.append(traceback.format_exc());start.abort();instrument.abort()
    threads=[threading.Thread(target=worker,args=(i,)) for i in range(workers)]
    for t in threads:t.start()
    for t in threads:t.join(100)
    uninstall()
    assert not any(t.is_alive() for t in threads),'REPLAY_TIMEOUT'
    assert not errors,errors
    return results

backend=[]
for path in sorted({line.split()[-1] for line in Path('/proc/self/maps').read_text().splitlines() if 'openblas' in line and '/' in line}):
    lib=ctypes.CDLL(path)
    for symbol in ('openblas_get_num_threads','openblas_get_num_threads64_','scipy_openblas_get_num_threads','scipy_openblas_get_num_threads64_'):
        try:fn=getattr(lib,symbol)
        except AttributeError:continue
        fn.restype=ctypes.c_int;backend.append(dict(library=Path(path).name,threads=fn()));break
assert all(x['threads']==1 for x in backend)
out=dict(input_sha256=hashlib.sha256(INPUT.read_bytes()).hexdigest(),frames=200,candidates_per_frame=300,
         warmup=20,plain_frames=160,instrumented_frames=20,backend=backend,source=str(Path(adapter.__file__)),results={})
for count in (1,2):
    out['results'][str(count)]=run(count)
    print('workers',count,[{k:v for k,v in r.items() if k not in ('outputs','state')} for r in out['results'][str(count)]],flush=True)
assert all(r['outputs']==out['results']['1'][0]['outputs'] and r['state']==out['results']['1'][0]['state'] for r in out['results']['2'])
out['state_id_parity']=True
(R/'replay.json').write_text(json.dumps(out,indent=2));print('same input/state/ID parity PASS',flush=True)
