#!/usr/bin/env python3
"""The field-tag landmine: reuse a tag number, silently corrupt. DDIA 2e Ch. 5.

Protobuf identifies each field by its tag NUMBER, not its name. The wire bytes
carry (tag, value) pairs; the field name lives only in the schema. So if a new
schema version REUSES an old tag number for a DIFFERENT field of the same wire
type, old encoded bytes are silently reinterpreted as the new field -- no error,
wrong meaning. The safe move is to give the new field a FRESH tag number.

Here: v1 has `int64 timestamp_ms = 2`. We encode a real epoch-ms timestamp.
  - v2  (DANGEROUS): reuses tag 2 as `int64 user_id`   -> old bytes decode as a
                     user_id of 1719950400000. No error. Wrong meaning.
  - v2b (SAFE):      keeps tag 2 as timestamp_ms, adds `int64 user_id = 3`
                     -> old bytes keep the timestamp; user_id defaults to 0.
  - v2c (WRONG TYPE): reuses tag 2 as `string user_id` -> different wire type,
                     so protobuf sees the mismatch and silently DROPS the field
                     (no error, value lost) rather than misreading it.

Run it:
    uv run --python 3.12 --with grpcio-tools --with protobuf \
        python3 field_tag_landmine.py
(Each .proto is compiled at runtime via grpc_tools; distinct file/message names
avoid import clashes. Deterministic -> the bytes reproduce exactly.)
"""
import os
import sys
import tempfile

# A real epoch-millisecond timestamp: 2024-07-02T20:00:00Z.
TIMESTAMP_MS = 1719950400000


def compile_proto(work, filename, body):
    """Write a .proto, compile it with grpc_tools, import and return the module."""
    path = os.path.join(work, filename)
    with open(path, "w") as f:
        f.write(body)
    from grpc_tools import protoc
    protoc.main(["protoc", f"-I{work}", f"--python_out={work}", path])
    if work not in sys.path:
        sys.path.insert(0, work)
    modname = filename[:-len(".proto")] + "_pb2"
    return __import__(modname)


def hexbytes(b):
    return " ".join(f"{x:02x}" for x in b)


def main():
    work = tempfile.mkdtemp()

    # --- v1: the original schema. Tag 2 == timestamp_ms. ---
    v1 = compile_proto(work, "event_v1.proto",
                        'syntax = "proto3";\n'
                        'message EventV1 {\n'
                        '  int64 timestamp_ms = 2;\n'
                        '}\n')
    old = v1.EventV1(timestamp_ms=TIMESTAMP_MS)
    old_bytes = old.SerializeToString()

    print("v1 schema:  message Event { int64 timestamp_ms = 2; }")
    print(f"  encode timestamp_ms = {TIMESTAMP_MS}  (epoch-ms, 2024-07-02T20:00:00Z)")
    print(f"  wire bytes ({len(old_bytes)}):  {hexbytes(old_bytes)}")
    print("  the leading 0x10 is (tag=2, wire type 0 = varint); the rest is the value.")
    print()

    # --- v2 (DANGEROUS): REUSE tag 2 for a different int64 field. ---
    v2 = compile_proto(work, "event_v2.proto",
                       'syntax = "proto3";\n'
                       'message EventV2 {\n'
                       '  int64 user_id = 2;\n'
                       '}\n')
    reused = v2.EventV2()
    reused.ParseFromString(old_bytes)  # decode the OLD bytes with the NEW schema

    print("v2 schema:  message Event { int64 user_id = 2; }   <-- REUSED tag 2")
    print("  decode the SAME old bytes with v2:")
    print(f"    REUSED TAG -> user_id = {reused.user_id}   <-- wrong value, no error")
    print(f"    (that is the timestamp {TIMESTAMP_MS} silently read as a user id)")
    print()

    # --- v2b (SAFE): keep tag 2, add user_id on a FRESH tag 3. ---
    v2b = compile_proto(work, "event_v2b.proto",
                        'syntax = "proto3";\n'
                        'message EventV2b {\n'
                        '  int64 timestamp_ms = 2;\n'
                        '  int64 user_id = 3;\n'
                        '}\n')
    safe = v2b.EventV2b()
    safe.ParseFromString(old_bytes)  # decode the OLD bytes with the SAFE schema

    print("v2b schema: message Event { int64 timestamp_ms = 2; int64 user_id = 3; }")
    print("  decode the SAME old bytes with v2b (fresh tag 3 for user_id):")
    print(f"    FRESH TAG  -> timestamp_ms = {safe.timestamp_ms}   <-- correct, preserved")
    print(f"                  user_id      = {safe.user_id}            <-- absent -> default 0")
    print()

    # --- v2c (WRONG TYPE): reuse tag 2 but as a string (different wire type). ---
    v2c = compile_proto(work, "event_v2c.proto",
                        'syntax = "proto3";\n'
                        'message EventV2c {\n'
                        '  string user_id = 2;\n'
                        '}\n')
    print("v2c schema: message Event { string user_id = 2; }   <-- REUSED tag 2, new wire type")
    print("  decode the SAME old bytes with v2c (int bytes read as a string):")
    wrongtype = v2c.EventV2c()
    try:
        wrongtype.ParseFromString(old_bytes)
        print(f"    WRONG TYPE -> no error; user_id = {wrongtype.user_id!r}   <-- field silently DROPPED")
        print("    (the wire types differ (varint vs length-delimited), so protobuf")
        print("     treats tag 2 as an unknown field and skips it -- data lost, not misread)")
    except Exception as e:
        print(f"    WRONG TYPE -> {type(e).__name__}: {e}")

    print()
    print("takeaway: reuse a tag with the SAME wire type -> SILENT corruption, a")
    print("          plausible wrong value with no error (v2, the landmine).")
    print("          Reuse it with a DIFFERENT wire type -> still no error, but the")
    print("          value is silently dropped (v2c). A FRESH tag is the only safe")
    print("          evolution (v2b): old data keeps its meaning, new field defaults.")


if __name__ == "__main__":
    main()
