#!/usr/bin/env python3
"""
CHECK-2026-11-02-jobs-table
Extract/verify jobs table for the ledger lane.

Single bare command verdict: exit 0 iff the committed jobs.csv on disk
re-derives byte-for-byte from the pinned source JSONs AND its sha256 matches
manifest.json (recorded at extraction time). No fallback arms; a failure
prints the measured facts and exits nonzero.
"""
import hashlib
import html
import json
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
MANIFEST = os.path.join(HERE, "manifest.json")
CSV = os.path.join(HERE, "jobs.csv")

SRC_FILES = {
    "more_candidates.json": "more",
    "ashby_candidates.json": "ashby",
    "greenhouse_candidates.json": "greenhouse",
}
SRC_ROOT = "/root"

TAG_RE = re.compile(r"<[^>]+>")


def strip_html(s):
    if not s:
        return ""
    s = html.unescape(s)
    s = TAG_RE.sub(" ", s)
    return " ".join(s.split())


def emp_of(rec):
    for k in ("employmentType", "workplaceType"):
        v = rec.get(k)
        if v:
            return v
    return ""


def publish_at(rec):
    return rec.get("publishedAt") or rec.get("updated_at") or ""


def desc_of(rec):
    for k in ("descriptionPlain", "content", "descriptionHtml"):
        v = rec.get(k)
        if v:
            return strip_html(v)[:220]
    return ""


def build_expected_rows():
    rows = []
    counts = {}
    for src_file, src_label in SRC_FILES.items():
        path = os.path.join(SRC_ROOT, src_file)
        with open(path) as fh:
            data = json.load(fh)
        counts[src_label] = 0
        for rec in data:
            rows.append({
                "source": src_label,
                "org": rec.get("org") or rec.get("board") or "",
                "title": rec.get("title") or "",
                "location": rec.get("location") or "",
                "department": rec.get("department") or "",
                "employment_type": emp_of(rec),
                "job_url": rec.get("jobUrl") or rec.get("url") or "",
                "apply_url": rec.get("applyUrl") or "",
                "published_at": publish_at(rec),
                "description": desc_of(rec),
            })
            counts[src_label] += 1
    return rows, counts


def main():
    ok = True
    failures = []

    if not os.path.exists(MANIFEST):
        failures.append("missing manifest.json")
    if not os.path.exists(CSV):
        failures.append("missing jobs.csv")

    if failures:
        print("FAIL: " + "; ".join(failures))
        sys.exit(1)

    manifest = json.load(open(MANIFEST))
    expected, expected_counts = build_expected_rows()
    expected_bytes = csv_bytes(expected)

    on_disk_bytes = open(CSV, "rb").read()

    # 1) committed CSV byte-for-byte equals a clean re-derivation from source
    same_bytes = on_disk_bytes == expected_bytes
    if not same_bytes:
        ok = False
        failures.append(
            "jobs.csv bytes differ from a fresh re-derivation from source "
            "(on-disk %d bytes, derived %d bytes)" % (len(on_disk_bytes), len(expected_bytes))
        )
        # diff report
        try:
            o = on_disk_bytes.decode().splitlines()
            e = expected_bytes.decode().splitlines()
            for i in range(max(len(o), len(e))):
                a = o[i] if i < len(o) else "<missing>"
                b = e[i] if i < len(e) else "<missing>"
                if a != b:
                    failures.append("line %d: ondisk=%r derived=%r" % (i, a[:60], b[:60]))
                    break
        except Exception as ex:
            failures.append("diff failed: %r" % ex)

    # 2) sha256 recorded in manifest matches the committed file on disk
    disk_sha = hashlib.sha256(on_disk_bytes).hexdigest()
    sha_ok = disk_sha == manifest.get("sha256")
    if not sha_ok:
        ok = False
        failures.append(
            "sha256 mismatch: on-disk=%s manifest=%s" % (disk_sha, manifest.get("sha256"))
        )

    # 3) row counts and per-source counts
    n_data = on_disk_bytes.decode().count("\n") - 1  # minus header
    count_ok = manifest.get("row_count") == n_data == len(expected)
    if not count_ok:
        ok = False
        failures.append(
            "row_count mismatch: manifest=%s ondisk_data=%s derived=%s"
            % (manifest.get("row_count"), n_data, len(expected))
        )
    if expected_counts != manifest.get("counts"):
        ok = False
        failures.append("counts mismatch: manifest=%s derived=%s"
                        % (manifest.get("counts"), expected_counts))

    # 4) required key fields must be populated
    req = ["source", "title", "job_url"]
    null_rows = [i for i, r in enumerate(expected) if any(not r.get(k) for k in req)]
    if null_rows:
        ok = False
        failures.append("rows with empty required field: %s" % null_rows[:5])

    # 5) fieldnames match
    import csv, io
    with open(CSV, newline="") as fh:
        reader = csv.DictReader(fh)
        fields = reader.fieldnames
    fields_ok = fields == manifest.get("fieldnames")
    if not fields_ok:
        ok = False
        failures.append("fieldnames mismatch: %s" % fields)

    # ---- measured report ----
    print("CHECK-2026-11-02-jobs-table")
    print("  source_files: %s" % ", ".join("%s=%d" % kv for kv in expected_counts.items()))
    print("  derived_rows: %d" % len(expected))
    print("  manifest.row_count: %s" % manifest.get("row_count"))
    print("  on-disk_rows: %d" % n_data)
    print("  sha256: %s" % disk_sha)
    print("  csv_bytes: %d" % len(on_disk_bytes))
    print("  fieldnames_match: %s" % fields_ok)

    if ok:
        print("VERDICT: PASS (jobs.csv re-derives from source and matches manifest)")
        sys.exit(0)
    print("VERDICT: FAIL")
    for f in failures:
        print("  - " + f)
    sys.exit(1)


def csv_bytes(rows):
    import csv, io
    buf = io.StringIO()
    w = csv.DictWriter(buf, fieldnames=list(rows[0].keys()) if rows else [])
    w.writeheader()
    w.writerows(rows)
    return buf.getvalue().encode("utf-8")


if __name__ == "__main__":
    main()
