#!/usr/bin/env python3
"""DTCG 2025.10 project subset validator and deterministic CSS compiler.
No network/dependencies. Not a complete DTCG conformance certification.
"""
import argparse, json, math, re, sys
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
TYPES={'color','dimension','fontFamily','fontWeight','number','duration','cubicBezier'}
def flatten(node,prefix=''):
 out={}
 for k,v in node.items():
  if k.startswith('$'):continue
  if not isinstance(v,dict):raise ValueError(f'{prefix}{k}: expected object')
  if any(c in k for c in '.{}'):raise ValueError(f'invalid token name {k}')
  key=f'{prefix}{k}'
  if '$value' in v:out[key]=v
  else:out.update(flatten(v,key+'.'))
 return out
def resolve(name,tokens,trail=()):
 if name in trail:raise ValueError('circular alias: '+' -> '.join((*trail,name)))
 if name not in tokens:raise ValueError(f'missing alias: {name}')
 t=tokens[name];v=t['$value']
 if isinstance(v,str) and v.startswith('{'):
  if not re.fullmatch(r'\{[^{}]+\}',v):raise ValueError(f'malformed alias {name}')
  target=v[1:-1]
  if target not in tokens:raise ValueError(f'missing alias: {target}')
  if t.get('$type')!=tokens[target].get('$type'):raise ValueError(f'alias type mismatch: {name}')
  return resolve(target,tokens,(*trail,name))
 return v
def numeric(v):return isinstance(v,(int,float)) and not isinstance(v,bool) and math.isfinite(v)
def validate(tokens):
 for name,t in tokens.items():
  kind=t.get('$type');v=resolve(name,tokens)
  if kind not in TYPES:raise ValueError(f'{name}: unsupported/invalid type {kind}')
  ok=False
  if kind=='color':
   ok=isinstance(v,dict) and v.get('colorSpace')=='srgb' and isinstance(v.get('components'),list) and len(v['components'])==3 and all(numeric(x) and 0<=x<=1 for x in v['components']) and numeric(v.get('alpha',1)) and 0<=v.get('alpha',1)<=1
   if ok and 'hex' in v:
    ok=bool(re.fullmatch(r'#[0-9a-fA-F]{6}',v['hex'])) and all(abs(round(x*255)-int(v['hex'][i:i+2],16))==0 for x,i in zip(v['components'],(1,3,5)))
  elif kind in ('dimension','duration'):ok=isinstance(v,dict) and set(v)=={'value','unit'} and numeric(v['value']) and v['value']>=0 and v['unit'] in (('px','rem') if kind=='dimension' else ('ms','s'))
  elif kind=='fontFamily':ok=isinstance(v,str) or (isinstance(v,list) and len(v)>0 and all(isinstance(x,str) and x for x in v))
  elif kind=='fontWeight':ok=numeric(v) and 1<=v<=1000
  elif kind=='number':ok=numeric(v)
  elif kind=='cubicBezier':ok=isinstance(v,list) and len(v)==4 and all(numeric(x) for x in v) and 0<=v[0]<=1 and 0<=v[2]<=1
  if not ok:raise ValueError(f'{name}: invalid {kind} value')
def css_value(kind,v):
 if kind=='color':return '#'+''.join(f'{round(c*255):02x}' for c in v['components']) if v.get('alpha',1)==1 else f"rgb({' '.join(str(round(c*255)) for c in v['components'])} / {v['alpha']})"
 if kind in ('dimension','duration'):return f"{v['value']}{v['unit']}"
 if kind=='fontFamily':return ', '.join(x if x in ('system-ui','sans-serif','serif','monospace') else json.dumps(x) for x in (v if isinstance(v,list) else [v]))
 if kind=='cubicBezier':return 'cubic-bezier('+', '.join(map(str,v))+')'
 return str(v)
def render(tokens):
 base=[];dark=[];seen=set()
 for name,t in tokens.items():
  prop=t.get('$extensions',{}).get('tw.aitom.css',{}).get('name')
  if not prop:continue
  if not re.fullmatch(r'[a-z][a-z0-9-]*',prop):raise ValueError(f'invalid CSS variable {prop}')
  scope='dark' if name.startswith('semantic.dark.') else 'base'
  if (scope,prop) in seen:raise ValueError(f'duplicate CSS variable {scope} {prop}')
  seen.add((scope,prop));line=f'  --{prop}: {css_value(t["$type"],resolve(name,tokens))};'
  (dark if scope=='dark' else base).append(line)
 return '/* Generated by scripts/tokens.py. Edit tokens.json, then regenerate. */\n:root {\n'+'\n'.join(base)+'\n}\n[data-theme="dark"] {\n'+'\n'.join(dark)+'\n}\n'
def luminance(c):return sum(w*(v/12.92 if v<=0.04045 else ((v+0.055)/1.055)**2.4) for w,v in zip((.2126,.7152,.0722),c))
def contrast_checks(tokens):
 report=[]
 for theme in ('light','dark'):
  for fg,bg,minimum in [('on-danger','danger-solid',4.5),('text','canvas',4.5),('muted','canvas',4.5),('muted','surface',4.5),('text','subtle',4.5),('on-primary','primary',4.5),('danger','canvas',4.5),('success','canvas',4.5),('focus','canvas',3),('control-border','canvas',3),('control-border','surface',3)]:
   a=luminance(resolve(f'semantic.{theme}.{fg}',tokens)['components']);b=luminance(resolve(f'semantic.{theme}.{bg}',tokens)['components']);ratio=(max(a,b)+.05)/(min(a,b)+.05)
   report.append({'theme':theme,'foreground':fg,'background':bg,'ratio':round(ratio,3),'minimum':minimum,'pass':ratio>=minimum})
   if ratio<minimum:raise ValueError(f'contrast FAIL {theme} {fg}/{bg}: {ratio:.3f} < {minimum}')
 return report
def main():
 p=argparse.ArgumentParser();p.add_argument('--check',action='store_true');p.add_argument('--json',action='store_true');args=p.parse_args()
 try:
  tokens=flatten(json.loads((ROOT/'tokens.json').read_text()));validate(tokens);css=render(tokens);report=contrast_checks(tokens)
  if args.check:
   if not (ROOT/'tokens.css').exists() or (ROOT/'tokens.css').read_text()!=css:raise ValueError('CSS drift: run python3 scripts/tokens.py')
  else:(ROOT/'tokens.css').write_text(css)
  print(json.dumps({'result':'PASS','tokens':len(tokens),'contrast':report},ensure_ascii=False,indent=2) if args.json else f'PASS: {len(tokens)} typed tokens; aliases; CSS {"in sync" if args.check else "generated"}; {len(report)} contrast pairs.')
 except (ValueError,KeyError,TypeError,OSError) as e:print('FAIL: '+str(e),file=sys.stderr);sys.exit(1)
if __name__=='__main__':main()
