"""
reproducibility_proof/canonicalizer.py

Handles byte-exact canonical serialization of preprocessed data.
This ensures identical byte output regardless of OS (Linux vs Windows line endings),
preventing false FAIL results due to platform differences.

Also absorbs the version_integrity responsibilities:
- compute_creator_script_hash(): fingerprints the calling script for auditability.
- compute_dag_hash(): deterministically hashes the pipeline DAG config.
- verify_integrity(): raises an error if stored hash doesn't match recomputed hash.
"""
import hashlib
import inspect
import io
import csv
import json
from pathlib import Path
from typing import List, Tuple

DataRows = List[Tuple[str, str]]


def to_canonical_bytes(rows: DataRows) -> bytes:
    """
    Serialize rows into a byte-exact, canonical TSV format.

    Rules enforced:
    - Unix line endings (\\n) only — never \\r\\n.
    - Columns always in order: label, text, n_tokens.
    - Token count uses whitespace split — consistent float-free integers.
    - Encoding is always UTF-8.
    """
    buf = io.StringIO()
    writer = csv.writer(buf, delimiter='\t', lineterminator='\n', quoting=csv.QUOTE_MINIMAL)
    writer.writerow(['label', 'text', 'n_tokens'])
    for lbl, txt in rows:
        n_tokens = len(txt.split()) if txt else 0
        writer.writerow([lbl, txt, str(n_tokens)])
    return buf.getvalue().encode('utf-8')


def compute_canonical_hash(rows: DataRows) -> str:
    """Compute SHA-256 hash of the canonical byte representation."""
    return hashlib.sha256(to_canonical_bytes(rows)).hexdigest()


def detect_stochastic_ops(pipeline_spec: dict) -> List[str]:
    """
    Scan pipeline configuration for steps that could be non-deterministic.
    Returns a list of warning strings for any risky steps detected.
    """
    # These node functions are inherently stochastic without an explicit seed.
    STOCHASTIC_FUNCS = {'shuffle', 'random_sample', 'random_split'}
    warnings = []
    for node_name, node_spec in pipeline_spec.items():
        func = node_spec.get('func', '')
        params = node_spec.get('params', {})
        if func in STOCHASTIC_FUNCS:
            if 'seed' not in params:
                warnings.append(
                    f"Node '{node_name}' uses '{func}' without a 'seed' parameter. "
                    f"Reproducibility cannot be guaranteed across runs!"
                )
    return warnings


def compute_dag_hash(pipeline_spec: dict) -> str:
    """
    Compute a deterministic SHA-256 hash of the full pipeline DAG specification.
    Uses canonical JSON (sorted keys, no whitespace) to ensure the same config
    always produces the same hash regardless of key insertion order.
    """
    canonical = json.dumps(pipeline_spec, sort_keys=True, separators=(',', ':'))
    return hashlib.sha256(canonical.encode('utf-8')).hexdigest()


def compute_creator_script_hash(frame_depth: int = 2) -> str:
    """
    Fingerprint the Python script that called create_version() at runtime.

    Walks up the call stack to find the outermost caller's source file
    and returns a SHA-256 hash of its content. This allows auditors to
    verify that a dataset version was created by a specific, unmodified script.

    If the script file cannot be read (e.g. called from a REPL or Jupyter),
    returns 'unknown'.

    Args:
        frame_depth: How many frames to walk up the call stack to find
                     the creator script (default 2 — caller's caller).
    """
    try:
        stack = inspect.stack()
        # Walk up until we find a real file outside this package
        for frame_info in stack[frame_depth:]:
            filename = frame_info.filename
            if filename and not filename.startswith('<'):
                source_path = Path(filename)
                if source_path.exists():
                    content = source_path.read_bytes()
                    return hashlib.sha256(content).hexdigest()
        return 'unknown'
    except Exception:
        return 'unknown'


def verify_integrity(version_id: str, repo_path='.') -> bool:
    """
    Strict integrity check — raises a RuntimeError if the stored output hash
    does not match the recomputed hash.

    This is the version_integrity contract: load → recompute → compare → raise.
    For a softer check with detailed reporting, use verify() from verify.py.

    Returns True if integrity is confirmed.
    Raises RuntimeError on mismatch.
    """
    from pathlib import Path as _Path
    import json as _json

    repo_path = _Path(repo_path)
    version_dir = repo_path / '.marco' / 'versions' / version_id
    manifest_path = version_dir / 'manifest.json'
    preprocessed_path = version_dir / 'preprocessed.tsv'

    if not manifest_path.exists():
        raise FileNotFoundError(f"Manifest not found for version '{version_id}'.")
    if not preprocessed_path.exists():
        raise FileNotFoundError(f"preprocessed.tsv not found for version '{version_id}'.")

    manifest = _json.loads(manifest_path.read_text(encoding='utf-8'))
    stored_hash = manifest.get('output_hash')
    if not stored_hash:
        raise ValueError(f"No output_hash found in manifest for version '{version_id}'.")

    # Recompute hash directly from the stored preprocessed.tsv
    content = preprocessed_path.read_bytes().replace(b'\r\n', b'\n')
    computed_hash = hashlib.sha256(content).hexdigest()

    if computed_hash != stored_hash:
        raise RuntimeError(
            f"INTEGRITY VIOLATION: Version '{version_id[:16]}...' has been tampered with!\n"
            f"  Stored hash:   {stored_hash}\n"
            f"  Current hash:  {computed_hash}\n"
            f"The preprocessed.tsv file does not match its recorded SHA-256 hash."
        )

    return True
