"""Opik uid 추적 가능성 실측 — 프로덕션 프로젝트를 건드리지 않는다.

묻는 것 넷:
  A. 우리가 정한 id 로 trace 를 만들 수 있나 (uuid4 도 되나, uuid7 이어야 하나)
  B. litellm 이 하듯 span 을 부모 trace_id 로 붙이면 실제로 붙나
  C. span 이 trace 보다 **먼저** 닿아도 살아남나 (배치 순서 뒤집힘 위험)
  D. 우리 uid 를 metadata 에 실어 되찾을 수 있나

전부 `theroad-probe-uid` 프로젝트에 쓴다. 요금 없음(자체 호스팅, 모델 호출 0).
"""
import json
import time
import urllib.error
import urllib.request
import uuid

BASE = "http://192.168.133.87:5173/api"
PROJECT = "theroad-probe-uid"
HEADERS = {"Comet-Workspace": "default", "Content-Type": "application/json"}


def post(path, payload):
    req = urllib.request.Request(
        f"{BASE}{path}", data=json.dumps(payload).encode("utf-8"),
        headers=HEADERS, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, r.read().decode("utf-8", "replace")[:300]
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "replace")[:400]


def get(path):
    req = urllib.request.Request(f"{BASE}{path}", headers=HEADERS)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)


def uuid7():
    """litellm 이 쓰는 것과 같은 방식(utils.create_uuid7)."""
    from litellm.integrations.opik.utils import create_uuid7
    return create_uuid7()


def now_iso(offset=0.0):
    t = time.time() + offset
    return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t)) + \
        f".{int((t % 1) * 1000):03d}000Z"


print("=" * 70)
print("A. 우리가 정한 id 로 trace 를 만들 수 있나")
print("=" * 70)

id_v4 = str(uuid.uuid4())
st, body = post("/v1/private/traces", {
    "id": id_v4, "project_name": PROJECT, "name": "probe:uuid4",
    "start_time": now_iso(-5), "end_time": now_iso(),
    "input": {"note": "uuid4 로 만든 trace"}, "output": {"ok": True},
    "metadata": {"our_uid": "CALL-uuid4-test"},
    "tags": ["probe:uuid4"],
})
print(f"uuid4  id={id_v4[:8]}…  → HTTP {st}  {body[:200]}")

id_v7 = uuid7()
st7, body7 = post("/v1/private/traces", {
    "id": id_v7, "project_name": PROJECT, "name": "probe:uuid7",
    "start_time": now_iso(-5), "end_time": now_iso(),
    "input": {"note": "uuid7 로 만든 trace"}, "output": {"ok": True},
    "metadata": {"our_uid": "CALL-uuid7-test"},
    "tags": ["probe:uuid7"], "thread_id": "probe-thread-1",
})
print(f"uuid7  id={id_v7[:8]}…  → HTTP {st7}  {body7[:200]}")

print()
print("=" * 70)
print("B+C. span 을 부모 trace_id 로 붙인다 — 순서를 두 가지로")
print("=" * 70)

# B: 정상 순서 — trace 먼저, span 나중
parent_ok = uuid7()
st, _ = post("/v1/private/traces", {
    "id": parent_ok, "project_name": PROJECT, "name": "step:probe_ordered",
    "start_time": now_iso(-10), "end_time": now_iso(),
    "input": {}, "output": {}, "tags": ["step:probe_ordered"],
    "thread_id": "probe-thread-1", "metadata": {"our_uid": "PARENT-ORDERED"},
})
print(f"[정상순서] 부모 trace → HTTP {st}")
for i in range(3):
    sid = uuid7()
    sst, sbody = post("/v1/private/spans", {
        "id": sid, "trace_id": parent_ok, "project_name": PROJECT,
        "name": f"op:probe_call_{i}", "type": "llm",
        "start_time": now_iso(-8 + i), "end_time": now_iso(-7 + i),
        "input": {"messages": [{"role": "user", "content": "x"}]},
        "output": {"choices": []}, "model": "probe-model",
        "provider": "probe", "tags": ["op:probe_call", "model:probe-model"],
        "metadata": {"our_uid": f"CALL-ordered-{i}"},
        "usage": {"prompt_tokens": 10, "completion_tokens": 5,
                  "total_tokens": 15},
    })
    print(f"[정상순서]   자식 span {i} → HTTP {sst} {sbody[:120]}")

# C: 뒤집힌 순서 — span 먼저, trace 나중 (배치 지연 재현)
parent_late = uuid7()
print(f"[뒤집힘]   부모 trace 를 아직 안 만들고 span 부터 보낸다")
for i in range(2):
    sid = uuid7()
    sst, sbody = post("/v1/private/spans", {
        "id": sid, "trace_id": parent_late, "project_name": PROJECT,
        "name": f"op:probe_late_{i}", "type": "llm",
        "start_time": now_iso(-8 + i), "end_time": now_iso(-7 + i),
        "input": {"messages": [{"role": "user", "content": "x"}]},
        "output": {"choices": []}, "model": "probe-model",
        "provider": "probe", "tags": ["op:probe_late"],
        "metadata": {"our_uid": f"CALL-late-{i}"},
    })
    print(f"[뒤집힘]     자식 span {i} (고아) → HTTP {sst} {sbody[:120]}")
time.sleep(1)
st, body = post("/v1/private/traces", {
    "id": parent_late, "project_name": PROJECT, "name": "step:probe_late",
    "start_time": now_iso(-10), "end_time": now_iso(),
    "input": {}, "output": {}, "tags": ["step:probe_late"],
    "thread_id": "probe-thread-1", "metadata": {"our_uid": "PARENT-LATE"},
})
print(f"[뒤집힘]   뒤늦은 부모 trace → HTTP {st} {body[:150]}")

print()
print("=" * 70)
print("결과 확인 — 3초 대기 후 되읽기")
print("=" * 70)
time.sleep(3)

projs = get("/v1/private/projects?page=1&size=100")
pid = None
for p in projs.get("content", []):
    if p.get("name") == PROJECT:
        pid = p["id"]
if not pid:
    print("★ probe 프로젝트를 못 찾았다 — 쓰기가 실패했다")
    raise SystemExit(1)
print(f"probe 프로젝트 id = {pid}")

tr = get(f"/v1/private/traces?project_id={pid}&page=1&size=50&truncate=false")
print(f"\ntrace {tr.get('total')}건:")
for t in tr.get("content", []):
    md = t.get("metadata") or {}
    print(f"  · {t.get('name'):28s} span={t.get('span_count')} "
          f"thread={t.get('thread_id')} uid={md.get('our_uid')}")

sp = get(f"/v1/private/spans?project_id={pid}&page=1&size=50&truncate=false")
print(f"\nspan {sp.get('total')}건:")
for s in sp.get("content", []):
    md = s.get("metadata") or {}
    print(f"  · {s.get('name'):22s} trace={str(s.get('trace_id'))[:8]}… "
          f"uid={md.get('our_uid')} usage={s.get('usage')}")

print("\n=== D. 우리 uid 로 되찾기 ===")
import urllib.parse
flt = json.dumps([{"field": "metadata", "key": "our_uid", "type": "dictionary",
                   "operator": "=", "value": "CALL-ordered-1"}])
try:
    q = get(f"/v1/private/spans?project_id={pid}&page=1&size=10"
            f"&filters={urllib.parse.quote(flt)}")
    print(f"metadata.our_uid = 'CALL-ordered-1' 로 검색 → {q.get('total')}건")
    for s in q.get("content", []):
        print(f"   찾음: {s.get('name')}")
except urllib.error.HTTPError as e:
    print("검색 실패:", e.code, e.read().decode("utf-8", "replace")[:200])
