"""Display-only score bands; never a scenario decision or identity association."""
import colorsys
import math

HIGH = '#40df78'
LOW = '#ff6262'
MUTED = '#aab4bf'
EDGES = ((5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16))
CORE = (5,6,11,12,7,8,9,10)
EXTRA = (13,14,15,16)
GROUPS = (('어깨',5,6),('엉덩이',11,12),('팔꿈치',7,8),('손목',9,10),('무릎',13,14),('발목',15,16))
KOREAN = {'HIGH':'높음','LOW':'낮음','BELOW':'기준미만','INVALID':'무효'}


def finite(v):
    return type(v) in (int,float) and math.isfinite(v)


def joint_state(k, width, height):
    if (not isinstance(k,(list,tuple)) or len(k)!=3 or not all(finite(x) for x in k)
            or not 0 <= k[0] <= width or not 0 <= k[1] <= height or not 0 <= k[2] <= 1):
        return 'INVALID'
    return 'HIGH' if k[2]>=.10 else 'LOW' if k[2]>=.05 else 'BELOW'


def person_state(p,width,height):
    score=p.get('score');b=p.get('bbox')
    if not finite(score) or not 0<=score<=1 or not isinstance(b,list) or len(b)!=4 or not all(finite(v) for v in b):
        return 'INVALID'
    if not (b[0]<b[2] and b[1]<b[3] and b[2]>0 and b[3]>0 and b[0]<width and b[1]<height):
        return 'INVALID'
    return 'HIGH' if score>=.25 else 'LOW' if score>=.20 else 'BELOW'


def edge_state(a,b):
    if a not in ('HIGH','LOW') or b not in ('HIGH','LOW'):return None
    return 'HIGH' if a==b=='HIGH' else 'LOW'


def key(p):
    return 'T'+str(p['track_id']) if p['track_id'] is not None else 'q'+str(p['query'])


def palette(rows):
    # Fixed per real ID; choose globally unique accents with co-visible contrast.
    ids=sorted({p['track_id'] for r in rows for p in r['persons'] if p['track_id'] is not None})
    neighbors={tid:set() for tid in ids}
    for r in rows:
        current={p['track_id'] for p in r['persons'] if p['track_id'] is not None and person_state(p,r['width'],r['height']) in ('HIGH','LOW')}
        for tid in current:neighbors[tid].update(current-{tid})
    colors=[tuple(round(c*255) for c in colorsys.hsv_to_rgb((i*.618033988749895)%1,.68+(i%3)*.1,.97)) for i in range(max(1024,len(ids)*4))]
    result={};used=set()
    for tid in sorted(ids,key=lambda t:(-len(neighbors[t]),t)):
        against=[result[x] for x in neighbors[tid] if x in result]
        def distance(c):return min((sum((a-b)**2 for a,b in zip(c,x)) for x in against),default=1)
        c=max((c for c in colors if c not in used),key=distance)
        result[tid]=c;used.add(c)
    return result


def observation(row, colors):
    out=[];used={colors[p['track_id']] for p in row['persons'] if p['track_id'] is not None and person_state(p,row['width'],row['height']) in ('HIGH','LOW')}
    for index,p in enumerate(row['persons']):
        state=person_state(p,row['width'],row['height']);shown=state in ('HIGH','LOW')
        tid=p['track_id'];q=p['query'];color=colors.get(tid)
        if tid is None:
            h=(row['sample']*.371+q*.61803398875)%1
            for _ in range(1024):
                color=tuple(round(c*255) for c in colorsys.hsv_to_rgb(h,.8,.98))
                if color not in used:break
                h=(h+.0013)%1
            if shown:used.add(color)
        joints=p.get('keypoints',[])
        states=[joint_state(joints[j] if j<len(joints) else None,row['width'],row['height']) for j in range(17)]
        out.append(dict(raw_order=index,query=q,track_id=tid,identity=key(p),person_score=p.get('score'),bbox=p.get('bbox'),
                        bbox_state=state,visible=shown,identity_color=color,joints=joints,joint_states=states,
                        edge_states=[edge_state(states[a],states[b]) for a,b in EDGES],
                        counts={s:states.count(s) for s in ('HIGH','LOW','BELOW','INVALID')},
                        core_states={str(j):states[j] for j in CORE},extra_states={str(j):states[j] for j in EXTRA}))
    visible=[p for p in out if p['visible']]
    assert len({tuple(p['identity_color']) for p in visible})==len(visible)
    assert len({p['identity'] for p in visible})==len(visible)
    for i,p in enumerate(visible):p['detail_slot']=i+1 if i<3 else None
    return out
