"""Independent CSV arithmetic and actual SVG/raster checks; no builder import."""
import csv
import hashlib
import json
import math
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from xml.etree import ElementTree as ET

from PIL import Image

ROOT = Path(__file__).parent
IDENTITY = (1, 0, 0, 1, 0, 0)


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def read_csv(path):
    with path.open(newline='') as f:
        return list(csv.DictReader(f))


def compose(a, b):
    return (a[0]*b[0]+a[2]*b[1], a[1]*b[0]+a[3]*b[1],
            a[0]*b[2]+a[2]*b[3], a[1]*b[2]+a[3]*b[3],
            a[0]*b[4]+a[2]*b[5]+a[4], a[1]*b[4]+a[3]*b[5]+a[5])


def transform(raw):
    m = IDENTITY
    for name, args in re.findall(r'(\w+)\(([^)]*)\)', raw):
        n = [float(x) for x in re.split(r'[\s,]+', args.strip()) if x]
        if name == 'matrix':
            t = tuple(n)
        elif name == 'translate':
            t = (1, 0, 0, 1, n[0], n[1] if len(n) > 1 else 0)
        elif name == 'scale':
            t = (n[0], 0, 0, n[1] if len(n) > 1 else n[0], 0, 0)
        else:
            raise AssertionError(f'Unreviewed SVG transform: {name}')
        m = compose(m, t)
    return m


def point(m, x, y):
    return (m[0]*x+m[2]*y+m[4], m[1]*x+m[3]*y+m[5])


def inspect_svg(path):
    marks, texts, lines, text_positions = [], [], [], []
    def walk(e, parent=IDENTITY):
        m = compose(parent, transform(e.get('transform', '')))
        kind = e.tag.split('}')[-1]
        if kind == 'text':
            value = ''.join(e.itertext())
            texts.append(value)
            text_positions.append((value, point(m, float(e.get('x', 0)), float(e.get('y', 0)))))
        if kind == 'rect':
            style = e.get('style', '')
            is_dark = 'fill: rgb(15%,36%,55%)' in style
            is_light = 'fill: rgb(86%,89%,91%)' in style
            if is_dark or is_light:
                x, y, w, h = (float(e.get(k)) for k in ['x', 'y', 'width', 'height'])
                corners = [point(m, x, y), point(m, x+w, y), point(m, x, y+h), point(m, x+w, y+h)]
                xs, ys = zip(*corners)
                # Exclude the small legend swatches by actual transformed width/height.
                if max(ys)-min(ys) > 20:
                    marks.append({'state': 'checked' if is_dark else 'remaining',
                                  'x': min(xs), 'y': min(ys), 'width': max(xs)-min(xs),
                                  'height': max(ys)-min(ys)})
        if kind == 'line':
            lines.append((point(m, float(e.get('x1')), float(e.get('y1'))),
                          point(m, float(e.get('x2')), float(e.get('y2')))))
        if kind == 'path':
            # ReportLab serializes axis lines as two-point paths, not <line>.
            p = re.fullmatch(r'M\s+([-\d.]+),([-\d.]+)\s+L\s+([-\d.]+),([-\d.]+)\s+Z', e.get('d', ''))
            if p:
                x1, y1, x2, y2 = map(float, p.groups())
                lines.append((point(m, x1, y1), point(m, x2, y2)))
        for child in e:
            walk(child, m)
    walk(ET.parse(path).getroot())
    return marks, texts, lines, text_positions


def check(source, folder, wrong=False):
    raw = read_csv(source)
    derived = read_csv(folder / ('wrong-chart-data.csv' if wrong else 'chart-data.csv'))
    name = 'wrong-chart' if wrong else 'correct-chart'
    marks, texts, lines, text_positions = inspect_svg(folder / (name + '.svg'))
    image = Image.open(folder / (name + '.png')).convert('RGB')
    assert image.size == (1920, 1000)
    # Actual transformed axis anchors, not only renderer configuration.
    assert any(math.isclose(a[0], 160) and math.isclose(b[0], 640) and
               math.isclose(a[1], 368) and math.isclose(b[1], 368) for a, b in lines)
    assert all(str(t) in texts for t in [0, 8, 16, 24, 32])
    assert len(raw) == len(derived) == 4
    assert len({r['snapshot_utc'] for r in raw}) == 1
    actual_y = sorted({round(m['y'], 7) for m in marks})
    assert len(actual_y) == 4
    rows = []
    for r, d, y in zip(raw, derived, actual_y):
        total, checked = int(r['total_cards']), int(r['checked_cards'])
        remaining = total if wrong else total-checked
        plotted = checked + remaining
        if not wrong:
            assert checked + remaining == total
        denominator = checked + total if wrong else total
        pct = str((Decimal(checked) * 100 / Decimal(denominator)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
        assert d['batch_id'] == r['batch_id'] and d['snapshot_utc'] == r['snapshot_utc']
        assert int(d['checked_cards']) == checked and int(d['total_cards']) == total
        assert int(d['incorrect_remaining_series' if wrong else 'remaining_cards']) == remaining
        assert int(d['plotted_total']) == plotted
        assert d['wrong_checked_share_pct' if wrong else 'checked_share_pct'] == pct
        assert r['batch_id'] + '  ' + r['batch_label'] in texts
        assert f'{checked} checked | {remaining} remaining' in texts
        unit = 'plotted total' if wrong else 'cards total'
        assert f'{plotted} {unit} | {pct}% checked' in texts
        row_marks = [m for m in marks if math.isclose(m['y'], y, abs_tol=1e-6)]
        center = row_marks[0]['y'] + row_marks[0]['height']/2
        for label in [r['batch_id'] + '  ' + r['batch_label'], f'{checked} checked | {remaining} remaining']:
            positions = [pos for value, pos in text_positions if value == label]
            assert len(positions) == 1 and abs(positions[0][1] - center) < 10
        for state, count, start in [('checked', checked, 160), ('remaining', remaining, 160+checked*15)]:
            found = [m for m in row_marks if m['state'] == state]
            if count == 0:
                assert not found, 'Zero must not gain a positive-width mark.'
                continue
            assert len(found) == 1
            mark = found[0]
            assert math.isclose(mark['x'], start, abs_tol=1e-6)
            assert math.isclose(mark['width'], count*15, abs_tol=1e-6)
            # Inspect actual raster fills at an interior point of every nonzero segment.
            px = image.getpixel((round((mark['x']+mark['width']/2)*2), round((mark['y']+mark['height']/2)*2)))
            expected = (38, 92, 140) if state == 'checked' else (219, 227, 232)
            assert max(abs(a-b) for a, b in zip(px, expected)) <= 1, (state, px)
        rows.append({'batch':r['batch_id'], 'checked':checked, 'secondSeries':remaining,
                     'plottedTotal':plotted, 'checkedSharePct':pct,
                     'actualSVGWidths':[round(m['width'], 5) for m in row_marks]})
    assert len(marks) == sum(int(r['checked_cards']) > 0 for r in raw) + sum(
        (int(r['total_cards']) if wrong else int(r['total_cards'])-int(r['checked_cards'])) > 0 for r in raw)
    return rows


if __name__ == '__main__':
    node = os.environ.get('CUESPEC_NODE', 'node')
    def build(folder=ROOT, variant='both'):
        subprocess.run([sys.executable, str(ROOT/'build_charts.py'), '--source', str(ROOT/'source-data.csv'),
                        '--out', str(folder), '--variant', variant], check=True)
        names = ['correct-chart'] if variant == 'correct' else ['wrong-chart', 'correct-chart']
        subprocess.run([node, str(ROOT/'render_png.cjs'), *[str(folder/(n+'.svg')) for n in names]], check=True)
    build()
    baseline = check(ROOT/'source-data.csv', ROOT)
    wrong_rows = check(ROOT/'source-data.csv', ROOT, True)
    originals = (ROOT/'source-data.csv').read_bytes()
    stable_names = ['chart-data.csv','wrong-chart-data.csv','correct-chart.svg','correct-chart.png','wrong-chart.svg','wrong-chart.png']
    before = {n:sha(ROOT/n) for n in stable_names}
    trial = ROOT/'mutation'
    trial.mkdir(exist_ok=True)
    try:
        changed = originals.replace(b'E02,Garden,20,8', b'E02,Garden,20,10')
        assert changed != originals
        (ROOT/'source-data.csv').write_bytes(changed)
        (trial/'source-data.csv').write_bytes(changed)
        build(trial, 'correct')
        changed_rows = check(trial/'source-data.csv', trial)
        assert changed_rows[1]['checked'] == 10 and changed_rows[1]['secondSeries'] == 10
        assert changed_rows[1]['checkedSharePct'] == '50.0'
        assert [changed_rows[i] for i in [0,2,3]] == [baseline[i] for i in [0,2,3]]
    finally:
        (ROOT/'source-data.csv').write_bytes(originals)
        build()
    assert (ROOT/'source-data.csv').read_bytes() == originals
    assert {n:sha(ROOT/n) for n in stable_names} == before
    check(ROOT/'source-data.csv', ROOT)
    total = sum(int(r['total_cards']) for r in read_csv(ROOT/'source-data.csv'))
    checked = sum(int(r['checked_cards']) for r in read_csv(ROOT/'source-data.csv'))
    report = {'task':'B391-RECOMPUTABLE-CHART-DRAFT', 'atUtc':datetime.now(timezone.utc).isoformat(),
              'role':'Author local verification, not independent review',
              'checks':'All four raw/derived rows; actual transformed SVG segment geometry and axis; each positive raster segment; all direct value labels',
              'correctRows':baseline, 'deliberatelyWrongRows':wrong_rows,
              'aggregate':{'total':total,'checked':checked,'remaining':total-checked,'checkedShareExact':f'{checked}/{total}'},
              'mutation':{'field':'E02.checked_cards','before':8,'temporary':10,'checkedWidthSVGUnits':[120,150],
                          'remainingWidthSVGUnits':[180,150],'sharePct':['40.0','50.0'],
                          'otherRowsUnchanged':True,'originalInputBytesRestored':True,'all6BaselineOutputHashesRestored':True,
                          'sourceHashBeforeAndRestored':hashlib.sha256(originals).hexdigest(),
                          'changedSourceHash':sha(trial/'source-data.csv')},
              'visualReview':'Pending separate image inspection; this script does not judge clipping/legibility.',
              'limits':['No model trial','No native Excel','No actual exhibition data','No accessibility certification','No efficiency/SEO claims']}
    (ROOT/'verification.json').write_text(json.dumps(report, indent=2)+'\n')
    print(json.dumps({'fourRowsAndBothFigures':'PASS','mutation':'PASS','restoration':'PASS'}))
