#!/usr/bin/env python3
"""
The People's Tips - race results poller
=======================================
Pulls finishing positions for the meetings we're tipping and pushes them into
Supabase, so the Results tab fills in on its own.

Standard library only. No pip install, no npm, nothing to maintain.

Source: the Sportsbet public racing feed. It carries every Australian meeting
for a date with the first three placings per race, needs no login, and answers
from an ordinary home connection.

Why not TAB: their API is cleaner, but it sits behind Akamai and refuses
programmatic clients - 403 from cloud servers, and HTTP/2 stream errors or
hangs even from a residential Australian connection. Tested and abandoned
29 Aug 2026.

Usage
-----
  ./tips-poller.py              poll today (Melbourne time)
  ./tips-poller.py 2026-08-29   backfill a specific date
  ./tips-poller.py --check      test connectivity and exit

Safe to run any time. Days with no meeting cost about a second, and re-posting
a result that is already stored does nothing.
"""

import json
import os
import ssl
import sys
import urllib.request
import urllib.error
from datetime import datetime, timezone, timedelta

SUPABASE = "https://pcvqowploippjbbyynuj.supabase.co"
ANON = ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6"
        "InBjdnFvd3Bsb2lwcGpiYnl5bnVqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ0NTQxODks"
        "ImV4cCI6MjEwMDAzMDE4OX0.OwMMaDsDJydRhmiXfv6JLxAgn9xumT4V0TQim-IG8WU")

SPORTSBET = "https://www.sportsbet.com.au/apigw/sportsbook-racing/Sportsbook/Racing/AllRacing"

UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")


def ssl_ctx():
    """
    Python installed from python.org ships without a CA bundle, so HTTPS fails
    with CERTIFICATE_VERIFY_FAILED until you run Install Certificates.command.
    macOS always has a full bundle at /etc/ssl/cert.pem, so use that instead of
    asking anyone to run anything.
    """
    ctx = ssl.create_default_context()
    paths = ssl.get_default_verify_paths()
    if not paths.cafile and not paths.capath:
        for cand in ("/etc/ssl/cert.pem", "/private/etc/ssl/cert.pem"):
            if os.path.exists(cand):
                ctx.load_verify_locations(cafile=cand)
                return ctx
        try:
            import certifi
            ctx.load_verify_locations(cafile=certifi.where())
        except Exception:
            pass
    return ctx


CTX = ssl_ctx()


def log(*a):
    print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), *a, flush=True)


def melbourne_today():
    # AEST +10 / AEDT +11. Using +10 means that between midnight and 1am during
    # daylight saving we look at yesterday's card - which is what you want anyway.
    return (datetime.now(timezone.utc) + timedelta(hours=10)).strftime("%Y-%m-%d")


def fetch(url, headers=None, data=None, timeout=30):
    req = urllib.request.Request(url, data=data, headers=headers or {})
    with urllib.request.urlopen(req, timeout=timeout, context=CTX) as r:
        return r.status, r.read().decode("utf-8", "replace")


def sb(path):
    _, body = fetch(f"{SUPABASE}/rest/v1/{path}",
                    {"apikey": ANON, "Authorization": f"Bearer {ANON}"})
    return json.loads(body) if body else []


def sportsbet(date):
    _, body = fetch(f"{SPORTSBET}/{date}",
                    {"User-Agent": UA, "Accept": "application/json"})
    return json.loads(body)


def norm(s):
    return "".join(c for c in str(s or "").upper() if c.isalpha())


def post_ingest(payload):
    return fetch(f"{SUPABASE}/functions/v1/ingest-race-results",
                 {"apikey": ANON, "Authorization": f"Bearer {ANON}",
                  "Content-Type": "application/json"},
                 data=json.dumps(payload).encode(), timeout=60)


def check():
    ok = True
    try:
        sb("tips_meetings?select=venue&limit=1")
        log("Supabase: OK")
    except Exception as e:
        log("Supabase: FAILED -", e)
        ok = False
    try:
        d = sportsbet(melbourne_today())
        horses = [s for s in d["dates"][0]["sections"] if s["displayName"] == "Horses"]
        n = len(horses[0]["meetings"]) if horses else 0
        log(f"Sportsbet: OK ({n} horse meetings visible today)")
    except Exception as e:
        log("Sportsbet: FAILED -", e)
        log("     If this machine is on a VPN, turn it off and try again.")
        ok = False
    log("READY" if ok else "NOT READY")
    return 0 if ok else 1


def main():
    args = [a for a in sys.argv[1:] if a]
    if "--check" in args:
        sys.exit(check())

    date = args[0] if args and args[0][0].isdigit() else melbourne_today()

    try:
        meetings = sb(f"tips_meetings?race_date=eq.{date}&select=venue,state")
    except Exception as e:
        log("could not reach Supabase:", e)
        sys.exit(1)

    payload = {"date": date, "source": "sportsbet", "meetings": []}

    if not meetings:
        log(f"no tracked meetings for {date} - sending heartbeat only")
        try:
            post_ingest(payload)
        except Exception as e:
            log("heartbeat failed:", e)
        return

    log(f"{date}: tracking " + ", ".join(m["venue"] for m in meetings))

    try:
        data = sportsbet(date)
    except Exception as e:
        log("Sportsbet fetch failed:", e)
        try:
            post_ingest(payload)
        except Exception:
            pass
        sys.exit(1)

    horses = [s for s in data.get("dates", [{}])[0].get("sections", [])
              if s.get("displayName") == "Horses"]
    theirs = horses[0].get("meetings", []) if horses else []

    for ours in meetings:
        match = next((m for m in theirs if norm(m.get("name")) == norm(ours["venue"])), None)
        if not match:
            log(f"{ours['venue']}: not found in feed")
            continue

        races = []
        for e in match.get("events", []):
            raw = (e.get("result") or "").strip()
            if not raw:
                continue
            nums = [n.strip() for n in raw.split(",") if n.strip().isdigit()]
            if not nums:
                continue
            races.append({"race_no": e["raceNumber"],
                          "positions": [[int(n)] for n in nums]})

        if races:
            payload["meetings"].append({"venue": ours["venue"], "races": races})
            log(f"{ours['venue']}: {len(races)} race(s) done - "
                + " ".join("R%s=%s" % (r["race_no"], r["positions"][0][0]) for r in races))
        else:
            log(f"{ours['venue']}: no results yet")

    # always call ingest, even with nothing new - it records a heartbeat so the
    # site can show the poller is alive without going near the Mac
    if not payload["meetings"]:
        log("no new results - sending heartbeat")

    try:
        status, body = post_ingest(payload)
        log("pushed:" if payload["meetings"] else "heartbeat:",
            status, " ".join(body.split())[:200])
    except urllib.error.HTTPError as e:
        log("push failed:", e.code, e.read().decode("utf-8", "replace")[:200])
    except Exception as e:
        log("push failed:", e)


if __name__ == "__main__":
    main()
