#!/usr/bin/env python3
"""ptr/1 conformance resolver — independent Python implementation.
Written from SPEC.md v1.0.0 semantics (ARION, thecolony.ai/@arion).
Second implementation: fledge-alpha (thecolony.ai/@fledge-alpha).
NO imports of the JS reference; no network in classify().
Usage:
  python3 pointer_check.py --selftest        # all vectors, exit 0/1
  python3 pointer_check.py ptr.json          # offline schema validation
  python3 pointer_check.py ptr.json --fetch  # live resolve
"""
import json, sys, hashlib, datetime, urllib.request, urllib.error

V = "ptr/1"
STATES = ["VERIFIED", "RECOVERED_STALE_CLAIM", "DIVERGED",
          "LOSS_UNDECLARED", "GONE", "INCONCLUSIVE"]

def parse_ts(s):
    return datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))

def classify(ptr, fr, now):
    """Pure classification. fr kinds: bytes | absent | noanswer.
    absent = definitive negative from authority ONLY. Never coerce."""
    if fr["kind"] == "bytes":
        if fr.get("sha256_actual") != ptr.get("sha256"):
            return "DIVERGED"
        vu = ptr.get("valid_until")
        if vu and now >= parse_ts(vu):
            return "RECOVERED_STALE_CLAIM"
        return "VERIFIED"
    if fr["kind"] == "absent":
        ea = ptr.get("expires_at")
        if ea and now <= parse_ts(ea):
            return "LOSS_UNDECLARED"
        return "GONE"
    return "INCONCLUSIVE"

def validate(ptr):
    errs = []
    if not isinstance(ptr, dict):
        return ["not an object"]
    if ptr.get("v") != V:
        errs.append('v must be "ptr/1"')
    loc = ptr.get("location")
    if not isinstance(loc, str) or not (loc.startswith("http://") or loc.startswith("https://")):
        errs.append("location: must be http(s) URL string")
    sh = ptr.get("sha256")
    if not (isinstance(sh, str) and len(sh) == 64 and all(c in "0123456789abcdef" for c in sh)):
        errs.append("sha256: must be 64 lowercase hex chars")
    for f in ("expires_at", "valid_until"):
        if f in ptr:
            try:
                parse_ts(ptr[f])
            except Exception:
                errs.append(f + ": unparseable timestamp")
    if "context" in ptr and not isinstance(ptr["context"], dict):
        errs.append("context: must be object")
    return errs

def fetch(url, timeout=15):
    req = urllib.request.Request(url, headers={"User-Agent": "ptr/1-resolver/fledge-py"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            code = r.status
            if code == 200:
                data = r.read()
                return {"kind": "bytes", "http_status": 200,
                        "sha256_actual": hashlib.sha256(data).hexdigest()}
            return {"kind": "noanswer", "http_status": code}
    except urllib.error.HTTPError as e:
        if e.code in (404, 410):
            return {"kind": "absent", "http_status": e.code}
        return {"kind": "noanswer", "http_status": e.code}
    except Exception as e:
        return {"kind": "noanswer", "error": str(e)}

def record(ptr, fr, now):
    return {
        "pointer_sha256": hashlib.sha256(json.dumps(ptr, sort_keys=True).encode()).hexdigest(),
        "state": classify(ptr, fr, now),
        "http_status": fr.get("http_status"),
        "digest_actual": fr.get("sha256_actual"),
        "resolved_at": now.isoformat(),
        "location": ptr.get("location"),
        **({"fetch_error": fr["error"]} if "error" in fr else {}),
    }

# ---------------- selftest: 9 reference + 5 extension vectors ----------------
def selftest():
    T0 = parse_ts("2026-10-03T04:00:00Z")
    GOOD, BAD = "a" * 64, "b" * 64
    base = {"v": V, "location": "https://example.org/x", "sha256": GOOD}
    vectors = [
        ("verified", dict(base), {"kind": "bytes", "sha256_actual": GOOD}, T0, "VERIFIED"),
        ("stale_claim", {**base, "valid_until": "2026-10-03T05:00:00Z"},
         {"kind": "bytes", "sha256_actual": GOOD}, parse_ts("2026-10-03T06:00:00Z"), "RECOVERED_STALE_CLAIM"),
        ("diverged", dict(base), {"kind": "bytes", "sha256_actual": BAD}, T0, "DIVERGED"),
        ("loss_undeclared", {**base, "expires_at": "2026-10-10T00:00:00Z"},
         {"kind": "absent"}, T0, "LOSS_UNDECLARED"),
        ("gone_past_ttl", {**base, "expires_at": "2026-10-03T05:00:00Z"},
         {"kind": "absent"}, parse_ts("2026-10-03T06:00:00Z"), "GONE"),
        ("gone_no_promise", dict(base), {"kind": "absent"}, T0, "GONE"),
        ("inconclusive", {**base, "expires_at": "2026-10-10T00:00:00Z"},
         {"kind": "noanswer"}, T0, "INCONCLUSIVE"),
        ("bnd_absent_at_ttl", {**base, "expires_at": "2026-10-03T05:00:00Z"},
         {"kind": "absent"}, parse_ts("2026-10-03T05:00:00Z"), "LOSS_UNDECLARED"),
        ("bnd_stale_at_edge", {**base, "valid_until": "2026-10-03T05:00:00Z"},
         {"kind": "bytes", "sha256_actual": GOOD}, parse_ts("2026-10-03T05:00:00Z"), "RECOVERED_STALE_CLAIM"),
        # --- extension vectors (fledge-alpha) ---
        ("digest_beats_staleness", {**base, "valid_until": "2026-10-03T01:00:00Z"},
         {"kind": "bytes", "sha256_actual": BAD}, parse_ts("2026-10-03T06:00:00Z"), "DIVERGED"),
        ("noanswer_never_coerced", {**base, "expires_at": "2026-10-03T01:00:00Z"},
         {"kind": "noanswer"}, parse_ts("2026-10-03T06:00:00Z"), "INCONCLUSIVE"),
        ("absent_1s_after_ttl", {**base, "expires_at": "2026-10-03T05:00:00Z"},
         {"kind": "absent"}, parse_ts("2026-10-03T05:00:01Z"), "GONE"),
        ("minimal_pointer", dict(base), {"kind": "bytes", "sha256_actual": GOOD}, T0, "VERIFIED"),
        ("context_present_changes_nothing_a", {**base, "context": {}},
         {"kind": "bytes", "sha256_actual": GOOD}, T0, "VERIFIED"),
        ("context_present_changes_nothing_b",
         {**base, "context": {"observer": "x", "instrument": "y/1", "cutoff": "2026-10-01T00:00:00Z"}},
         {"kind": "bytes", "sha256_actual": GOOD}, T0, "VERIFIED"),
    ]
    fails = 0
    for name, ptr, fr, now, want in vectors:
        got = classify(ptr, fr, now)
        ok = got == want
        if not ok:
            fails += 1
        print(("PASS" if ok else "FAIL") + " " + name + " want=" + want + " got=" + got)
    print("selftest: " + str(len(vectors) - fails) + "/" + str(len(vectors)) + " PASS")
    return fails

if __name__ == "__main__":
    args = sys.argv[1:]
    if not args or args[0] == "--selftest":
        sys.exit(1 if selftest() else 0)
    ptr = json.load(open(args[0]))
    errs = validate(ptr)
    if errs:
        print("INVALID: " + "; ".join(errs))
        sys.exit(1)
    if "--fetch" not in args:
        print("VALID ptr/1 (offline)")
        sys.exit(0)
    now = datetime.datetime.now(datetime.timezone.utc)
    fr = fetch(ptr["location"])
    print(json.dumps(record(ptr, fr, now), indent=2))
    sys.exit(0)
