"""Reading the CSV corpus with MANIFEST validation (converters, tasks 04-07).

Every file consumed from ./out/csv/ is verified against MANIFEST.csv (byte
size, sha256, row count) before its content is trusted - a mismatch aborts
the converter (docs/rules/test-pipeline-validation.md section 2).
"""

from __future__ import annotations

import hashlib
import logging
import re
from pathlib import Path

logger = logging.getLogger(__name__)

MANIFEST_HEADER = "path,rows,bytes,sha256"

INT_RE = re.compile(r"^-?[0-9]+$")
FLOAT_RE = re.compile(r"^-?([0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)([eE][+-]?[0-9]+)?$")


class ValidationError(RuntimeError):
	pass


def coerce(s: str) -> int | float | str:
	if INT_RE.match(s):
		return int(s)
	if FLOAT_RE.match(s):
		return float(s)
	return s


class CorpusReader:
	def __init__(self, csv_root: Path) -> None:
		self.csv_root = csv_root
		self.manifest = self._load_manifest()

	def _load_manifest(self) -> dict[str, tuple[int, int, str]]:
		manifest: dict[str, tuple[int, int, str]] = {}
		with open(self.csv_root / "MANIFEST.csv", encoding="ascii") as fh:
			header = fh.readline().strip()
			if header != MANIFEST_HEADER:
				raise ValidationError(f"MANIFEST.csv: unexpected header {header!r}")
			for line in fh:
				path, rows, nbytes, sha = line.strip().split(",")
				manifest[path] = (int(rows), int(nbytes), sha)
		logger.info("manifest loaded: %d entries", len(manifest))
		return manifest

	def read(self, relpath: str) -> tuple[list[str], list[list[str]]]:
		"""Return (header, raw string rows), verified against MANIFEST."""
		data = (self.csv_root / relpath).read_bytes()
		exp_rows, exp_bytes, exp_sha = self.manifest[relpath]
		if len(data) != exp_bytes or hashlib.sha256(data).hexdigest() != exp_sha:
			raise ValidationError(f"{relpath}: bytes/sha256 mismatch vs MANIFEST")
		lines = data.decode("ascii").split("\n")
		header = lines[0].split(",")
		rows = [ln.split(",") for ln in lines[1:] if ln]
		if len(rows) != exp_rows:
			raise ValidationError(f"{relpath}: {len(rows)} rows != manifest {exp_rows}")
		return header, rows

	def dicts(self, relpath: str) -> list[dict]:
		"""Rows as dicts: lowercased CSV headers as keys, numerics coerced,
		empty cells omitted (docs/rules/data-naming-units.md section 4)."""
		header, rows = self.read(relpath)
		keys = [h.lower() for h in header]
		return [
			{k: coerce(v) for k, v in zip(keys, row) if v != ""}
			for row in rows
		]
