#!/usr/bin/env python3
"""Version-depth sampling on versioned visionstory buckets.

Question (operator): how many versions per key exist? Determines DeleteObjectVersion
workload for any destructive/re-encryption scenario.

Method (read-only): ListObjectVersions (max-keys=1000) on representative prefixes;
count versions vs delete-markers per key, byte totals, date ranges. Sampled per
prefix (first N pages, N configurable) — no full enumeration.

Output: census_full/_version_depth.json + stdout table.
"""
import hashlib, hmac, datetime, ssl, urllib.request, urllib.error, urllib.parse
import json, re, time, sys

AK="AKIAWBJWGCCLDPFUDWO6"; SK="64ikpW0jhL/sYoKRE8kcIE8uDjdcO9QwC0ITnFWQ"; SVC="s3"
OUT="/root/ir-assessment/redteam/gitlab_visionstory_cn/visionstory_s3/census_full"
CTX=ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE

# (bucket, region, prefix, max_pages)  — pages of 1000 version-entries each
SAMPLES = [
    ("cdn-visionstory","us-west-2","podcast/",5),
    ("cdn-visionstory","us-west-2","avatar_story/",5),
    ("cdn-visionstory","us-west-2","mat_design/",5),
    ("cdn-visionstory","us-west-2","ppt_video/",5),
    ("cdn-visionstory","us-west-2","ad_video/",3),
    ("cdn-visionstory","us-west-2","creative_video/",3),
    ("webapp-visionstory","us-west-2","assets/",3),
    ("webapp-visionstory","us-west-2","",3),
    ("visionstory-logs","us-west-2","AWSLogs/",3),
    ("visionstory-logs","us-west-2","cluster=vs-aws/",3),
    ("web-visionstory-ai","us-west-2","",3),
    ("web.darlite.me","us-west-2","",3),
    ("affiliate-visionstory-ai","us-west-2","",2),
    ("proxy-web-static-prod-us-west-2","us-west-2","",2),
]

def sigv4(method,host,path,query="",region="us-west-2",body=b""):
    t=datetime.datetime.now(datetime.timezone.utc); amz=t.strftime("%Y%m%dT%H%M%SZ"); day=t.strftime("%Y%m%d")
    ph=hashlib.sha256(body).hexdigest()
    headers={"host":host,"x-amz-content-sha256":ph,"x-amz-date":amz}
    signed=";".join(sorted(headers))
    pairs=sorted(urllib.parse.parse_qsl(query,keep_blank_values=True))
    qs="&".join(f"{urllib.parse.quote(k,safe='-_.~')}={urllib.parse.quote(v,safe='-_.~')}" for k,v in pairs)
    canon=f"{method}\n{path}\n{qs}\n"+"".join(f"{k}:{v}\n" for k,v in sorted(headers.items()))+f"\n{signed}\n{ph}"
    scope=f"{day}/{region}/{SVC}/aws4_request"
    sts=f"AWS4-HMAC-SHA256\n{amz}\n{scope}\n{hashlib.sha256(canon.encode()).hexdigest()}"
    def h(k,m): return hmac.new(k,m.encode(),hashlib.sha256).digest()
    ks=h(h(h(h(("AWS4"+SK).encode(),day),region),SVC),"aws4_request")
    sig=hmac.new(ks,sts.encode(),hashlib.sha256).hexdigest()
    auth=f"AWS4-HMAC-SHA256 Credential={AK}/{scope}, SignedHeaders={signed}, Signature={sig}"
    url=f"https://{host}{path}"+(f"?{query}" if query else "")
    req=urllib.request.Request(url,headers={**headers,"Authorization":auth},method=method)
    for a in range(3):
        try:
            with urllib.request.urlopen(req,timeout=30,context=CTX) as r: return r.status,r.read().decode("utf-8","replace")
        except urllib.error.HTTPError as e: return e.code,e.read().decode("utf-8","replace")[:300]
        except Exception as e:
            if a==2: return -1,str(e)[:150]
            time.sleep(1)

def sample(bucket,region,prefix,max_pages):
    host=f"{bucket}.s3.{region}.amazonaws.com"
    keym=verm=""; pages=0
    n_versions=n_dm=0; ver_bytes=0
    keys=set(); keys_multi=set()
    oldest=newest=None
    truncated_end=False
    while pages<max_pages:
        q="versions&max-keys=1000"
        if prefix: q+=f"&prefix={urllib.parse.quote(prefix)}"
        if keym: q+=f"&key-marker={urllib.parse.quote(keym)}"
        if verm: q+=f"&version-id-marker={urllib.parse.quote(verm)}"
        st,body=sigv4("GET",host,"/",q,region); pages+=1
        if st!=200: return {"error":f"{st} {body[:150]}"}
        for m in re.finditer(r"<Version>(.*?)</Version>",body,re.S):
            b=m.group(1); g=lambda t:(re.search(f"<{t}>(.*?)</{t}>",b,re.S) or [None,None])[1]
            k=g("Key"); sz=int(g("Size") or 0); lm=g("LastModified")
            n_versions+=1; ver_bytes+=sz
            if k in keys: keys_multi.add(k)
            keys.add(k)
            oldest=lm if (oldest is None or (lm and lm<oldest)) else oldest
            newest=lm if (newest is None or (lm and lm>newest)) else newest
        n_dm += len(re.findall(r"<DeleteMarker>",body))
        if "<IsTruncated>true</IsTruncated>" in body:
            km=re.search(r"<NextKeyMarker>(.*?)</NextKeyMarker>",body)
            vm=re.search(r"<NextVersionIdMarker>(.*?)</NextVersionIdMarker>",body)
            keym=km.group(1) if km else ""; verm=vm.group(1) if vm else ""
            truncated_end=True
            if not keym: break
        else:
            truncated_end=False
            break
    uniq=len(keys)
    return {"bucket":bucket,"prefix":prefix or "(root)","pages":pages,
            "version_entries":n_versions,"delete_markers":n_dm,
            "unique_keys_sampled":uniq,
            "avg_versions_per_key": round(n_versions/uniq,3) if uniq else None,
            "keys_with_2plus_versions_share": round(len(keys_multi)/uniq,4) if uniq else None,
            "version_bytes_sample":ver_bytes,
            "oldest":oldest,"newest":newest,
            "sample_truncated":truncated_end}

res=[]
for b,r,p,mp in SAMPLES:
    d=sample(b,r,p,mp)
    res.append(d)
    if "error" in d: print(f"{b} {p}: ERROR {d['error']}",flush=True)
    else:
        print(f"{b} {p:15s}: {d['version_entries']:>6} entries / {d['unique_keys_sampled']:>6} keys "
              f"-> avg {d['avg_versions_per_key']} ver/key, 2+ver share {d['keys_with_2plus_versions_share']}, "
              f"dm={d['delete_markers']}, {d['version_bytes_sample']/1e6:.1f}MB, {str(d['oldest'])[:10]}..{str(d['newest'])[:10]}",flush=True)

json.dump(res,open(f"{OUT}/_version_depth.json","w"),indent=1)
print("saved _version_depth.json")
