"""Canonical benchmark result handling (spec 08, test-pipeline-validation).

Every implementation of a benchmark query returns the same canonical rows:
tuples of str / int / float columns. Rows are compared SORTED; floats agree
within 1e-9. The canonical expected artifacts are produced from PostgreSQL
and cross-validated against SQLite (task 08); every measured run (task 09)
re-validates its rows against them.

Two serializations:
- full: floats via repr() - lossless, used for stdout and expected/*.rows.csv;
- rounded (6 decimals): used ONLY for the sha256 fingerprint, so engines
  whose aggregate arithmetic differs in the last bits hash identically.
The tolerant row comparison is the authoritative check; the sha256 is a
compact fingerprint for reports and markers.
"""

from __future__ import annotations

import hashlib
import sys

from common.corpus import coerce

FLOAT_TOL = 1e-9

Row = tuple


def sort_rows(rows: list[Row]) -> list[Row]:
	return sorted(rows)


def _fmt(value, rounded: bool) -> str:
	if isinstance(value, float):
		return f"{round(value, 6):.6f}" if rounded else repr(value)
	return str(value)


def serialize(rows: list[Row], rounded: bool = False) -> str:
	return "\n".join(",".join(_fmt(v, rounded) for v in row) for row in rows) + "\n"


def sha256_of(rows: list[Row]) -> str:
	return hashlib.sha256(serialize(sort_rows(rows), rounded=True).encode("ascii")).hexdigest()


def print_rows(rows: list[Row]) -> None:
	sys.stdout.write(serialize(sort_rows(rows)))


def parse_rows(text: str) -> list[Row]:
	rows = []
	for line in text.splitlines():
		if line:
			rows.append(tuple(coerce(cell) for cell in line.split(",")))
	return rows


def compare(expected: list[Row], actual: list[Row], tol: float = FLOAT_TOL) -> str | None:
	"""Return None when equal within tolerance, else a first-difference message."""
	exp, act = sort_rows(expected), sort_rows(actual)
	if len(exp) != len(act):
		return f"row count {len(act)} != expected {len(exp)}"
	for i, (er, ar) in enumerate(zip(exp, act)):
		if len(er) != len(ar):
			return f"row {i}: arity {len(ar)} != {len(er)}"
		for j, (ev, av) in enumerate(zip(er, ar)):
			if isinstance(ev, float) or isinstance(av, float):
				if abs(float(ev) - float(av)) > tol:
					return f"row {i} col {j}: {av!r} != {ev!r} (tol {tol})"
			elif ev != av:
				return f"row {i} col {j}: {av!r} != {ev!r}"
	return None
