"""Audit self-authored fixtures and saved outputs; never imports a tracing engine.
Requires Python >=3.10 and Pillow==12.3.0. Optional --rerender: CairoSVG==2.9.1.
The audit is read-only and does not reproduce the private tracing pipeline.
"""
from pathlib import Path
from PIL import Image, ImageChops, ImageDraw
import argparse
import io
import json
import re
import xml.etree.ElementTree as ET

TOKEN = re.compile(r'[A-Za-z]|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?')
ARITY = {'M':2,'L':2,'H':1,'V':1,'C':6,'S':4,'Q':4,'T':2,'A':7}

def reference_fixture(detail):
    scale=4
    image=Image.new('RGB',(384*scale,384*scale),'white')
    draw=ImageDraw.Draw(image)
    box=lambda values:tuple(value*scale for value in values)
    draw.rounded_rectangle(box((40,64,160,320)),radius=28*scale,fill=(20,60,160))
    draw.rectangle(box((208,64,344,320)),fill=(20,140,140))
    if detail:
        draw.rectangle(box((248,112,252,272)),fill='white')
        draw.ellipse(box((181,85,191,95)),fill=(220,80,60))
    return image.resize((384,384),Image.Resampling.LANCZOS)

def count_nodes(data):
    if re.sub(TOKEN,'',data).strip(' \t\r\n,'):
        raise ValueError('Unexpected SVG path-data syntax')
    tokens=TOKEN.findall(data)
    index=nodes=0
    command=None
    while index<len(tokens):
        if tokens[index].isalpha():
            command=tokens[index]
            index+=1
            if command.upper()=='Z':
                nodes+=1
                command=None
                continue
        if command is None or command.upper() not in ARITY:
            raise ValueError('Missing or unsupported SVG command')
        arity=ARITY[command.upper()]
        group=tokens[index:index+arity]
        if len(group)!=arity or any(token.isalpha() for token in group):
            raise ValueError('Incomplete SVG segment')
        for token in group:
            float(token)
        index+=arity
        if command.upper()=='M':
            command='L' if command=='M' else 'l'
        else:
            nodes+=1
    return nodes

def svg_stats(path):
    text=path.read_text(encoding='utf-8').replace('\r\n','\n')
    root=ET.fromstring(text)
    paths=[element for element in root.iter() if element.tag.split('}')[-1]=='path']
    unsupported=[element.tag for element in root.iter() if element.tag.split('}')[-1] not in ('svg','path')]
    if unsupported:
        raise ValueError('This captured-output packet should contain only SVG path shapes')
    return {'paths':len(paths),'nodes':sum(count_nodes(element.get('d','')) for element in paths),'svg_bytes_lf':len(text.encode())}

def image_metrics(reference,render,detail):
    if reference.size!=(384,384) or render.size!=(384,384):
        raise ValueError('Expected shared 384 by 384 comparison canvas')
    reference=reference.convert('RGB')
    render=render.convert('RGB')
    error=foreground_error=changed=union=intersection=0
    for a,b in zip(reference.getdata(),render.getdata()):
        difference=tuple(abs(x-y) for x,y in zip(a,b))
        error+=sum(difference)
        changed+=max(difference)>32
        foreground_a=min(a)<240
        foreground_b=min(b)<240
        if foreground_a or foreground_b:
            union+=1
            foreground_error+=sum(difference)
        intersection+=foreground_a and foreground_b
    pixels=384*384
    result={'rgb_mae_0_to_255':round(error/(pixels*3),5),
      'pixels_max_channel_error_gt32':changed,
      'pixels_max_channel_error_gt32_pct':round(changed/pixels*100,5),
      'foreground_union_rgb_mae':round(foreground_error/(union*3),5),
      'foreground_iou':round(intersection/union,6)}
    if detail:
        gap=list(render.crop((249,120,252,265)).getdata())
        dot=list(render.crop((176,80,197,101)).getdata())
        white=sum(min(pixel)>=245 for pixel in gap)
        result.update({'gap_white_pixels':white,'gap_total_pixels':len(gap),
          'gap_white_fraction':round(white/len(gap),6),
          'dot_nonwhite_pixels_in_roi':sum(min(pixel)<240 for pixel in dot),
          'dot_coral_pixels_rgb_distance_lt45':sum(sum((a-b)**2 for a,b in zip(pixel,(220,80,60)))<45**2 for pixel in dot),
          'dot_center_rgb':list(render.getpixel((186,90)))})
    return result

def same_pixels(a,b):
    return a.size==b.size and ImageChops.difference(a.convert('RGB'),b.convert('RGB')).getbbox() is None

def relative_asset(root,value):
    relative=Path(value)
    resolved=(root/relative).resolve()
    if relative.is_absolute() or root.resolve() not in resolved.parents:
        raise ValueError('Asset path must stay relative to this bundle')
    return resolved

def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--root',type=Path,default=Path(__file__).resolve().parent)
    parser.add_argument('--rerender',action='store_true')
    args=parser.parse_args()
    root=args.root
    data=json.loads((root/'results.json').read_text(encoding='utf-8'))
    if args.rerender:
        import cairosvg
        if cairosvg.__version__!='2.9.1':
            raise ValueError('Use the documented CairoSVG 2.9.1 renderer for this comparison')
    references={}
    for fixture in ('tiny_features','simple_control'):
        generated=reference_fixture(fixture=='tiny_features')
        saved=Image.open(root/fixture/'native-reference.png').convert('RGB')
        if not same_pixels(generated,saved):
            raise ValueError('Fixture reference differs: '+fixture)
        references[fixture]=saved
    checked=[]
    for row in data['rows']:
        fixture=row['fixture'];variant=row['variant'];reference=references[fixture]
        low=reference.resize((96,96),Image.Resampling.LANCZOS)
        expected_input={'original_96':low,'nearest4x_384':low.resize((384,384),Image.Resampling.NEAREST),
          'lanczos4x_384':low.resize((384,384),Image.Resampling.LANCZOS),'native_reference_384':reference}[variant]
        actual_input=Image.open(relative_asset(root,row['input'])).convert('RGB')
        if not same_pixels(expected_input,actual_input):
            raise ValueError('Input variant differs: '+fixture+'/'+variant)
        svg=relative_asset(root,row['svg'])
        for metric,value in svg_stats(svg).items():
            if value!=row[metric]:
                raise ValueError('SVG metric differs: '+fixture+'/'+variant+'/'+metric)
        render=Image.open(relative_asset(root,row['render'])).convert('RGB')
        computed=image_metrics(reference,render,fixture=='tiny_features')
        if computed!=row['metrics']:
            raise ValueError('Image metrics differ: '+fixture+'/'+variant+' '+json.dumps(computed))
        if args.rerender:
            png=cairosvg.svg2png(url=str(svg),output_width=384,output_height=384,background_color='white',unsafe=False)
            rerendered=Image.open(io.BytesIO(png)).convert('RGB')
            if not same_pixels(rerendered,render):
                raise ValueError('Re-rendered pixels differ: '+fixture+'/'+variant)
        checked.append(fixture+'/'+variant)
    if len(checked)!=8 or len(set(checked))!=8:
        raise ValueError('Expected eight distinct captured cases')
    if image_metrics(references['tiny_features'],references['tiny_features'],True)!=data['reference_feature_metrics']:
        raise ValueError('Reference feature metrics differ')
    print(json.dumps({'status':'MATCH','cases':len(checked),'fixture_references':2,
      'all_input_pixels_match':True,'all_svg_counts_and_bytes_match':True,'all_recorded_image_metrics_match':True,
      'rerendered_svg_pixels_match':True if args.rerender else 'NOT RUN',
      'trace_pipeline_reproduced':False},indent=2))

if __name__=='__main__':
    main()
