#!/usr/bin/env python3
"""
2026-11-01 daily guide: JSON field values alternating between null and empty object {}

Explicit empty-value column policy:
  - null:   the JSON null literal  ->  meta_state = "null",   meta_json = "null"
  - {}:     empty JSON object      ->  meta_state = "empty_object", meta_json = "{}"
  - value:  a non-empty object     ->  meta_state = "object",  meta_json = original JSON

This script:
  1. Exports a five-record fixture to CSV with an explicit column policy.
  2. Reads the CSV back and verifies every field.
  3. Demonstrates a naive exporter that collapses null and {} into the same blank cell.
  4. Writes execution evidence JSON.

Run: python null-empty-object-to-csv.py <input.json> <output.csv> <naive.csv> <evidence.json>
"""

import json, csv, hashlib, sys, os
from pathlib import Path

SCHEMA = ["id", "meta_state", "meta_key_count", "meta_json"]

def export_explicit(source_path, csv_path):
    with open(source_path, "r", encoding="utf-8") as f:
        raw_text = f.read()
    records = json.loads(raw_text)

    if not isinstance(records, list):
        raise ValueError("Top-level JSON must be an array")
    for rec in records:
        if not isinstance(rec, dict):
            raise ValueError(f"Each record must be an object, got {type(rec).__name__}")
        if "id" not in rec or not isinstance(rec["id"], str):
            raise ValueError("Each record must have a string 'id' field")
        if "meta" not in rec:
            raise ValueError(f"Record {rec.get('id')} missing 'meta' field")

    rows = []
    for rec in records:
        row = {col: "" for col in SCHEMA}
        row["id"] = rec["id"]
        meta = rec["meta"]
        row["meta_json"] = json.dumps(meta, ensure_ascii=False)

        if meta is None:
            row["meta_state"] = "null"
            row["meta_key_count"] = 0
        elif isinstance(meta, dict):
            if len(meta) == 0:
                row["meta_state"] = "empty_object"
                row["meta_key_count"] = 0
            else:
                row["meta_state"] = "object"
                row["meta_key_count"] = len(meta)
        elif isinstance(meta, str):
            row["meta_state"] = "string"
            row["meta_key_count"] = 0
        else:
            raise ValueError(f"Record {rec['id']}: unsupported meta type {type(meta).__name__}")

        rows.append(row)

    # Exclusive file creation: refuse to overwrite existing output
    if os.path.exists(csv_path):
        raise FileExistsError(f"Output file already exists: {csv_path}")

    with open(csv_path, "x", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=SCHEMA)
        writer.writeheader()
        writer.writerows(rows)

    return rows, raw_text, records

def export_naive(records, naive_path):
    """Naive exporter: writes a single 'meta' column, blank for both null and {}."""
    if os.path.exists(naive_path):
        raise FileExistsError(f"Naive output file already exists: {naive_path}")

    naive_rows = []
    for rec in records:
        meta = rec["meta"]
        if meta is None:
            cell = ""
        elif isinstance(meta, dict):
            cell = "" if len(meta) == 0 else str(meta)
        elif isinstance(meta, str):
            cell = meta
        else:
            cell = str(meta)
        naive_rows.append({"id": rec["id"], "meta": cell})

    with open(naive_path, "x", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["id", "meta"])
        writer.writeheader()
        writer.writerows(naive_rows)

    return naive_rows

def read_back_verify(csv_path, records):
    """Read the CSV back and compare every field against parsed source."""
    with open(csv_path, "r", encoding="utf-8", newline="") as f:
        reader = csv.DictReader(f)
        csv_rows = list(reader)

    results = []
    assert len(csv_rows) == len(records), f"Row count mismatch: CSV {len(csv_rows)} vs source {len(records)}"

    for i, (csv_row, src_rec) in enumerate(zip(csv_rows, records)):
        meta = src_rec["meta"]
        checks = {}

        # Determine expected values
        if meta is None:
            exp_state = "null"
            exp_count = 0
            exp_json = "null"
        elif isinstance(meta, dict):
            if len(meta) == 0:
                exp_state = "empty_object"
                exp_count = 0
                exp_json = "{}"
            else:
                exp_state = "object"
                exp_count = len(meta)
                exp_json = json.dumps(meta, ensure_ascii=False)
        elif isinstance(meta, str):
            exp_state = "string"
            exp_count = 0
            exp_json = json.dumps(meta, ensure_ascii=False)
        else:
            exp_state = "other"
            exp_count = 0
            exp_json = json.dumps(meta, ensure_ascii=False)

        checks["id"] = csv_row["id"] == src_rec["id"]
        checks["meta_state"] = csv_row["meta_state"] == exp_state
        checks["meta_key_count"] = int(csv_row["meta_key_count"]) == exp_count
        checks["meta_json"] = csv_row["meta_json"] == exp_json

        all_pass = all(checks.values())
        results.append({
            "row": i,
            "id": src_rec["id"],
            "source_type": "null" if meta is None else ("dict" if isinstance(meta, dict) else type(meta).__name__),
            "expected_state": exp_state,
            "csv_state": csv_row["meta_state"],
            "expected_key_count": exp_count,
            "csv_key_count": int(csv_row["meta_key_count"]),
            "expected_json": exp_json,
            "csv_json": csv_row["meta_json"],
            "checks": checks,
            "pass": all_pass
        })

    return results

def main():
    args = sys.argv[1:]
    source_path = args[0] if len(args) > 0 else "null-empty-object-fixture.json"
    csv_path = args[1] if len(args) > 1 else "null-empty-object-output.csv"
    naive_path = args[2] if len(args) > 2 else "null-empty-object-naive.csv"
    evidence_path = args[3] if len(args) > 3 else "null-empty-object-evidence.json"

    rows, raw_text, records = export_explicit(source_path, csv_path)

    # Naive export for counterexample
    naive_rows = export_naive(records, naive_path)

    # Read-back verification
    verify_results = read_back_verify(csv_path, records)

    # File hashes
    with open(source_path, "rb") as f:
        source_sha = hashlib.sha256(f.read()).hexdigest()
    with open(csv_path, "rb") as f:
        csv_sha = hashlib.sha256(f.read()).hexdigest()
    with open(naive_path, "rb") as f:
        naive_sha = hashlib.sha256(f.read()).hexdigest()

    source_size = os.path.getsize(source_path)
    csv_size = os.path.getsize(csv_path)
    naive_size = os.path.getsize(naive_path)

    all_pass = all(r["pass"] for r in verify_results)

    evidence = {
        "date": "2026-11-01",
        "python_version": "3.13.14",
        "fixture": {
            "path": str(Path(source_path).resolve()),
            "sha256": source_sha,
            "bytes": source_size,
            "record_count": len(records)
        },
        "explicit_output": {
            "path": str(Path(csv_path).resolve()),
            "sha256": csv_sha,
            "bytes": csv_size,
            "columns": len(SCHEMA),
            "data_rows": len(rows),
            "schema": SCHEMA
        },
        "naive_output": {
            "path": str(Path(naive_path).resolve()),
            "sha256": naive_sha,
            "bytes": naive_size,
            "columns": 2,
            "data_rows": len(naive_rows),
            "schema": ["id", "meta"]
        },
        "verification": {
            "all_pass": all_pass,
            "row_results": verify_results
        },
        "naive_problem": {
            "description": "Naive exporter writes blank cell for both null and {}; the two are indistinguishable in CSV",
            "affected_records": [r["id"] for r in records if r["meta"] is None or (isinstance(r["meta"], dict) and len(r["meta"]) == 0)]
        },
        "sources": [
            "https://www.rfc-editor.org/rfc/rfc8259#section-3",
            "https://docs.python.org/3.13/library/csv.html"
        ]
    }

    with open(evidence_path, "w", encoding="utf-8") as f:
        json.dump(evidence, f, indent=2, ensure_ascii=False)

    print(f"Export complete: {len(rows)} rows, {len(SCHEMA)} columns, {csv_size} bytes")
    print(f"All read-back checks passed: {all_pass}")
    print(f"Evidence written to {evidence_path}")

if __name__ == "__main__":
    main()
