#!/usr/bin/env python3
"""
CROVIA – Hash-chain verifier

Verifies a hash-chain generated by hashchain_writer.py for a given NDJSON file.

- Recomputes the rolling SHA-256 hash-chain over the NDJSON source.
- Compares each block digest with the entries in the chain file.
- Returns 0 on full match, non-zero on any mismatch or I/O error.
"""

import argparse
import hashlib
import os
import sys
from typing import List, Tuple


def parse_chain_file(path: str) -> List[Tuple[int, int, str]]:
    """
    Parse a hash-chain text file.

    Each non-empty line must have the format:
        <block_idx> <tab> <upto_line> <tab> <digest_hex>

    Returns a list of (block_idx, upto_line, digest_hex).
    """
    entries: List[Tuple[int, int, str]] = []
    with open(path, "r", encoding="utf-8") as f:
        for lineno, line in enumerate(f, start=1):
            s = line.strip()
            if not s:
                continue
            parts = s.split("\t")
            if len(parts) != 3:
                print(
                    f"[VERIFY] Invalid chain line {lineno}: "
                    f"expected 3 columns, got {len(parts)}",
                    file=sys.stderr,
                )
                continue
            blk_raw, upto_raw, dg = parts
            try:
                blk = int(blk_raw)
                upto = int(upto_raw)
            except ValueError:
                print(
                    f"[VERIFY] Invalid block index or upto_line at chain line {lineno}: "
                    f"{blk_raw!r}, {upto_raw!r}",
                    file=sys.stderr,
                )
                continue
            if len(dg) != 64:
                print(
                    f"[VERIFY] Invalid digest length at chain line {lineno}: {dg!r}",
                    file=sys.stderr,
                )
                continue
            entries.append((blk, upto, dg))
    return entries


def main() -> None:
    ap = argparse.ArgumentParser(
        description="CROVIA hash-chain verifier for NDJSON logs."
    )
    ap.add_argument(
        "--source",
        required=True,
        help="Original NDJSON file (same used by hashchain_writer.py).",
    )
    ap.add_argument(
        "--chain",
        required=True,
        help="hashchain_*.txt file produced by hashchain_writer.py.",
    )
    ap.add_argument(
        "--chunk",
        type=int,
        default=10000,
        help="Lines per block (must match the value used when writing the chain).",
    )
    args = ap.parse_args()

    if not os.path.exists(args.source):
        print(f"[FATAL] Source NDJSON not found: {args.source}", file=sys.stderr)
        sys.exit(2)
    if not os.path.exists(args.chain):
        print(f"[FATAL] Chain file not found: {args.chain}", file=sys.stderr)
        sys.exit(2)

    print(f"[VERIFY] reading source: {args.source}")
    print(f"[VERIFY] using chain file: {args.chain}")
    print(f"[VERIFY] chunk size: {args.chunk} lines")

    chain_entries = parse_chain_file(args.chain)
    if not chain_entries:
        print(
            f"[FATAL] No valid entries found in chain file: {args.chain}",
            file=sys.stderr,
        )
        sys.exit(2)

    prev = b"\x00" * 32          # initial anchor (must match hashchain_writer.py)
    h = hashlib.sha256()
    count = 0
    ok = True
    entry_idx = 0

    with open(args.source, "r", encoding="utf-8-sig") as fs:
        for raw in fs:
            s = raw.rstrip("\n")
            if not s:
                continue

            # Same folding convention as hashchain_writer.py:
            # for each line, hash(prev || line_bytes) inside the current block.
            h.update(prev)
            h.update(s.encode("utf-8"))
            count += 1

            # End-of-block
            if (count % args.chunk) == 0:
                digest = h.hexdigest()
                if entry_idx >= len(chain_entries):
                    print(
                        "[VERIFY] ERROR – chain shorter than required "
                        f"(missing entry for block index {entry_idx}).",
                        file=sys.stderr,
                    )
                    ok = False
                    break

                blk, upto, expected_dg = chain_entries[entry_idx]

                # Sanity check: upto_line must match the global line counter
                if upto != count:
                    print(
                        f"[VERIFY] Mismatch in 'upto_line' at block={blk}: "
                        f"expected upto={upto}, got count={count}",
                        file=sys.stderr,
                    )
                    ok = False

                if expected_dg != digest:
                    print(
                        f"[VERIFY] Digest mismatch at block={blk} upto={upto}: "
                        f"expected {expected_dg}, computed {digest}",
                        file=sys.stderr,
                    )
                    ok = False

                # Prepare next block
                prev = bytes.fromhex(digest)
                h = hashlib.sha256()
                entry_idx += 1

    # Final (partial) block, if any
    if ok and (count % args.chunk) != 0:
        digest = h.hexdigest()
        if entry_idx >= len(chain_entries):
            print(
                "[VERIFY] ERROR – chain shorter than required (final block).",
                file=sys.stderr,
            )
            ok = False
        else:
            blk, upto, expected_dg = chain_entries[entry_idx]
            if upto != count:
                print(
                    f"[VERIFY] Mismatch in 'upto_line' at final block={blk}: "
                    f"expected upto={upto}, got count={count}",
                    file=sys.stderr,
                )
                ok = False
            if expected_dg != digest:
                print(
                    f"[VERIFY] Digest mismatch in final block={blk}: "
                    f"expected {expected_dg}, computed {digest}",
                    file=sys.stderr,
                )
                ok = False

    if ok:
        print(
            f"[VERIFY] OK — chain consistent "
            f"(lines={count}, blocks={len(chain_entries)})"
        )
        sys.exit(0)
    else:
        print("[VERIFY] FAIL — chain NOT consistent.")
        sys.exit(3)


if __name__ == "__main__":
    main()
