#!/usr/bin/env python3
"""OLY Black Friday Price Watch - price snapshot.
Fetches public product pages listed in data/watchlist.csv (one polite GET each, honest User-Agent,
no bypassing of bot protection; pages that refuse are logged as ERROR and left out) and appends
rows to data/price_snapshots.csv. Reads schema.org JSON-LD offers.
Usage: python3 snapshot_prices.py [label]   (label e.g. baseline, pre-bf-1, bf-day, cyber-monday)
Schedule suggestion (Europe/Stockholm): 2026-11-04 baseline, 11-13, 11-20, 11-24, 11-26, 11-27 (BF),
11-30 (Cyber Monday), 12-07.
"""
import csv,re,json,sys,time,datetime,urllib.request,os
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UA="Mozilla/5.0 (compatible; OLYPriceWatch/1.0; +https://ol-y.com; info@ol-y.com)"
label=sys.argv[1] if len(sys.argv)>1 else 'adhoc'
def get(u):
    r=urllib.request.Request(u,headers={"User-Agent":UA,"Accept-Language":"sv,en;q=0.8"})
    return urllib.request.urlopen(r,timeout=30).read().decode('utf8','replace')
def offers(h):
    acc=[]
    def walk(o,name=None):
        if isinstance(o,dict):
            if o.get('@type')=='Product': name=o.get('name') or name
            if o.get('@type')=='Offer' or ('price' in o and 'priceCurrency' in o):
                if o.get('price') not in (None,''):
                    acc.append(dict(name=name or '',price=float(str(o['price']).replace(',','.')),cur=o.get('priceCurrency'),
                        avail=(o.get('availability') or '').split('/')[-1],sku=o.get('sku') or ''))
            for v in o.values(): walk(v,name)
        elif isinstance(o,list):
            for v in o: walk(v,name)
    for m in re.findall(r'<script[^>]+application/ld\+json[^>]*>(.*?)</script>',h,re.S):
        try: walk(json.loads(m))
        except Exception: pass
    return acc
def page_text(h):
    import html
    s=re.sub(r'<(script|style)[^>]*>.*?</\1>','',h,flags=re.S)
    return html.unescape(re.sub(r'\s+',' ',re.sub(r'<[^>]+>',' ',s)))
out=os.path.join(ROOT,'data','price_snapshots.csv')
new=not os.path.exists(out)
now=datetime.datetime.now().astimezone()
rows=[]
for w in csv.DictReader(open(os.path.join(ROOT,'data','watchlist.csv'),encoding='utf8')):
    r=dict(snapshot_label=label,ts=now.isoformat(timespec='seconds'),id=w['id'],price='',currency=w['currency'],availability='',
           offer_name='',struck_price='',lowest30='',status='OK')
    try:
        h=get(w['url']); of=offers(h)
        if not of: raise ValueError('no JSON-LD offers')
        pick=None
        if w['match']:
            rx=re.compile(w['match'])
            for o in of:
                if rx.search(o['name']) or rx.search(o['sku']): pick=o;break
            if pick is None:
                # fall back: match against price text (e.g. VitaSanum has an unnamed offer list)
                for o in of:
                    if rx.search(f"{o['price']:.2f}"): pick=o;break
        if pick is None: pick=of[-1] if len(of)>1 and w['id'].startswith('nmn-nc-nl') else of[0]
        if w['id']=='nmn-nc-nl-120':
            pick=[o for o in of if o['sku'].endswith('120')][0] if any(o['sku'].endswith('120') for o in of) else of[0]
        r.update(price=pick['price'],currency=pick['cur'] or w['currency'],availability=pick['avail'],offer_name=pick['name'][:80])
        t=page_text(h)
        m=re.search(r'(?:Lägsta pris senaste 30 dagarna|Tidigare lägsta pris|Prior price|lowest price[^.]{0,40}30)[^0-9]{0,30}([0-9][0-9\s.,]*)',t,re.I)
        if m: r['lowest30']=m.group(1).strip()[:12]
    except Exception as e:
        r['status']='ERROR: '+str(e)[:80]
    rows.append(r); time.sleep(1.5)
with open(out,'a',newline='',encoding='utf8') as f:
    wr=csv.DictWriter(f,fieldnames=list(rows[0].keys()))
    if new: wr.writeheader()
    wr.writerows(rows)
ok=sum(r['status']=='OK' for r in rows)
print(f"{label}: {ok}/{len(rows)} OK -> {out}")
for r in rows: print(r['id'],r['price'],r['currency'],r['availability'],r['status'],'|',r['offer_name'])
