#!/usr/bin/env python3
"""
JSON to CSV: Distinguish Leading Plus +42, Unsigned 42, and String "+42"

Explicit sign-token column policy.
Executed locally with Python 3.13.14 on 2026-11-07.

Per RFC 8259 section 6, JSON numbers cannot have a leading plus sign.
A value like +42 can only appear in JSON as a string "+42".
This guide distinguishes:
  - JSON number 42 (unsigned, type=number)
  - JSON string "+42" (leading plus, type=string)
  - JSON string "42" (no plus, looks numeric, type=string)
And records the sign marker, value type, source token and JSON serialization
so a CSV reader can filter leading-plus strings from unsigned numbers
without re-parsing the original JSON.
"""

import json
import csv
import hashlib
import re
from pathlib import Path

SCHEMA = ["id", "score_kind", "score_sign_marker", "score_value_type", "score_token", "score_json"]
MAX_BYTES = 1_000_000


def classify_sign_token(raw_text: str, val):
    """
    Classify a JSON value for the sign-token column policy.

    Returns: (kind, sign_marker, value_type, token, json_repr)

    kind: human-readable category combining type and sign
    sign_marker: what sign prefix the source token has
    value_type: JSON type (number, string, boolean, null)
    token: exact source spelling
    json_repr: JSON serialization of the parsed value
    """
    if isinstance(val, bool):
        return "boolean", "none", "boolean", raw_text, json.dumps(val)

    if isinstance(val, int):
        if raw_text.startswith("-"):
            return "number_negative", "leading_minus", "number", raw_text, json.dumps(val)
        return "number_unsigned", "none", "number", raw_text, json.dumps(val)

    if isinstance(val, float):
        if raw_text.startswith("-"):
            return "number_negative", "leading_minus", "number", raw_text, json.dumps(val)
        return "number_unsigned", "none", "number", raw_text, json.dumps(val)

    if isinstance(val, str):
        # For strings, check the first character of the actual string value
        if val.startswith("+"):
            return "string_leading_plus", "leading_plus", "string", raw_text, json.dumps(val)
        elif val.startswith("-"):
            return "string_leading_minus", "leading_minus", "string", raw_text, json.dumps(val)
        else:
            # String without a sign prefix — may look numeric ("42") or not
            return "string_no_sign", "none", "string", raw_text, json.dumps(val)

    if val is None:
        return "null", "none", "null", raw_text, json.dumps(val)

    raise ValueError(f"Unexpected type: {type(val).__name__}")


def export_explicit(source_path, csv_path, naive_path, evidence_path):
    """Export JSON to CSV with explicit sign-token column policy."""
    with open(source_path, "r", encoding="utf-8") as f:
        raw_text = f.read()

    if len(raw_text.encode("utf-8")) > MAX_BYTES:
        raise ValueError(f"Input exceeds {MAX_BYTES} bytes policy")

    source_sha = hashlib.sha256(raw_text.encode("utf-8")).hexdigest()
    records = json.loads(raw_text)

    # Extract raw value tokens from source text
    # This captures the text between the colon and the comma/brace
    token_pattern = re.compile(r'"score"\s*:\s*([^\s,}\]]+)')
    raw_tokens = token_pattern.findall(raw_text)

    rows = []
    for i, rec in enumerate(records):
        row = {col: "" for col in SCHEMA}
        row["id"] = rec["id"]
        val = rec["score"]

        raw_token = raw_tokens[i]

        # For string values, raw_token includes surrounding quotes in the JSON source.
        # We strip the outer quotes to get the actual string content for sign classification.
        if isinstance(val, str):
            token_for_classify = raw_token.strip('"')
        else:
            token_for_classify = raw_token

        kind, sign_marker, value_type, token, json_repr = classify_sign_token(token_for_classify, val)
        row["score_kind"] = kind
        row["score_sign_marker"] = sign_marker
        row["score_value_type"] = value_type
        row["score_token"] = token
        row["score_json"] = json_repr
        rows.append(row)

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

    # Write naive CSV: single value column, json.dumps for everything
    naive_schema = ["id", "score"]
    naive_rows = []
    for rec in records:
        val = rec["score"]
        if isinstance(val, str):
            naive_rows.append({"id": rec["id"], "score": val})
        else:
            naive_rows.append({"id": rec["id"], "score": str(val)})

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

    # Read back and verify
    with open(csv_path, "r", encoding="utf-8", newline="") as f:
        reader = csv.DictReader(f)
        csv_rows = list(reader)

    checks_passed = 0
    checks_total = 0
    for csv_row, src_rec, raw_token in zip(csv_rows, records, raw_tokens):
        val = src_rec["score"]
        if isinstance(val, str):
            token_for_classify = raw_token.strip('"')
        else:
            token_for_classify = raw_token

        exp_kind, exp_sign, exp_type, exp_token, exp_json = classify_sign_token(token_for_classify, val)

        checks_total += 6
        if csv_row["id"] == src_rec["id"]:
            checks_passed += 1
        if csv_row["score_kind"] == exp_kind:
            checks_passed += 1
        if csv_row["score_sign_marker"] == exp_sign:
            checks_passed += 1
        if csv_row["score_value_type"] == exp_type:
            checks_passed += 1
        if csv_row["score_token"] == exp_token:
            checks_passed += 1
        if csv_row["score_json"] == exp_json:
            checks_passed += 1

    # Build evidence
    output_text = Path(csv_path).read_text(encoding="utf-8")
    output_sha = hashlib.sha256(output_text.encode("utf-8")).hexdigest()

    naive_text = Path(naive_path).read_text(encoding="utf-8")
    naive_sha = hashlib.sha256(naive_text.encode("utf-8")).hexdigest()

    evidence = {
        "date": "2026-11-07",
        "python_version": "3.13.14",
        "source_sha256": source_sha,
        "source_bytes": len(raw_text.encode("utf-8")),
        "output_sha256": output_sha,
        "output_bytes": len(output_text.encode("utf-8")),
        "naive_sha256": naive_sha,
        "naive_bytes": len(naive_text.encode("utf-8")),
        "csv_data_rows": len(rows),
        "csv_columns": len(SCHEMA),
        "checks_passed": checks_passed,
        "checks_total": checks_total,
        "schema": SCHEMA,
        "rows": rows,
        "naive_rows": naive_rows,
        "raw_tokens": raw_tokens,
        "source_records": [
            {"id": r["id"], "score_type": type(r["score"]).__name__, "score_value": r["score"]}
            for r in records
        ],
        "rfc_note": "RFC 8259 section 6: JSON number grammar does not allow a leading plus sign. A value like +42 can only appear in JSON as a string.",
    }

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

    print(f"Export complete: {len(rows)} rows, {len(SCHEMA)} columns")
    print(f"Checks: {checks_passed}/{checks_total} passed")
    print(f"Source SHA-256: {source_sha[:16]}...{source_sha[-4:]}")
    print(f"Output SHA-256: {output_sha[:16]}...{output_sha[-4:]}")
    print(f"Output bytes: {len(output_text.encode('utf-8'))}")

    return evidence


if __name__ == "__main__":
    base = Path(__file__).parent
    source = base / "leading-plus-fixture.json"
    csv_out = base / "leading-plus-output.csv"
    naive_out = base / "leading-plus-naive.csv"
    evidence_out = base / "leading-plus-evidence.json"

    # Remove old outputs if they exist (for re-runs during development)
    for p in [csv_out, naive_out, evidence_out]:
        if p.exists():
            p.unlink()

    evidence = export_explicit(source, csv_out, naive_out, evidence_out)
