#!/usr/bin/env python3
"""Costco Pokemon card drop tracker, powered by the Scavio Costco API.

Checks which Costco warehouses carry the Pokemon items you list, at what shelf
price, and prints (or posts to a Discord webhook) every change since the last
run: a warehouse newly listing an item, a price change, or a clearance price.

Python 3.9+, standard library only. Configure tracker.json next to this file
(see https://scavio.dev/skills/costco-pokemon-tracker/SKILL.md), put your
Scavio API key in ~/.config/scavio/api_key, then run it every 30 minutes.
"""
import json
import math
import pathlib
import sys
import time
import urllib.error
import urllib.request

API = "https://api.scavio.dev"
HERE = pathlib.Path(__file__).resolve().parent
CONFIG = HERE / "tracker.json"
STATE = HERE / "tracker_state.json"
WAREHOUSES_CACHE = HERE / "us-warehouses.json"
WAREHOUSES_URL = "https://scavio.dev/skills/costco-pokemon-tracker/us-warehouses.json"
KEY_FILE = pathlib.Path.home() / ".config" / "scavio" / "api_key"
MAX_ITEMS = 20       # item numbers per prices call
WAREHOUSE_CHUNK = 9  # warehouses per prices call: 1-9 costs 1 credit, 10 costs 2


def api_key() -> str:
    if not KEY_FILE.exists():
        sys.exit(f"No Scavio API key at {KEY_FILE}. Get one free at https://dashboard.scavio.dev/sign-up")
    return KEY_FILE.read_text().strip()


def post(path: str, body: dict, key: str) -> dict:
    req = urllib.request.Request(
        API + path,
        data=json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
    )
    for attempt in range(4):
        try:
            with urllib.request.urlopen(req, timeout=120) as r:
                return json.load(r)
        except urllib.error.HTTPError as e:
            if e.code in (429, 502, 503) and attempt < 3:
                time.sleep(3 * (attempt + 1))
                continue
            raise SystemExit(f"{path} failed: HTTP {e.code} {e.read()[:300]!r}")
    raise SystemExit(f"{path} failed after retries")


def miles(lat1, lon1, lat2, lon2) -> float:
    p = math.pi / 180
    a = (math.sin((lat2 - lat1) * p / 2) ** 2
         + math.cos(lat1 * p) * math.cos(lat2 * p) * math.sin((lon2 - lon1) * p / 2) ** 2)
    return 7917.5 * math.asin(math.sqrt(a))


def all_us_warehouses() -> list:
    if not WAREHOUSES_CACHE.exists():
        with urllib.request.urlopen(WAREHOUSES_URL, timeout=60) as r:
            WAREHOUSES_CACHE.write_bytes(r.read())
    rows = json.loads(WAREHOUSES_CACHE.read_text())["warehouses"]
    return [w for w in rows if w["type"] == "warehouse"]  # business centers and showrooms carry no TCG


def pick_warehouses(cfg: dict, key: str) -> list:
    """Returns [{warehouse_id, name, state}] for the configured scope."""
    if cfg.get("country", "us") == "ca":
        r = post("/api/v1/costco/warehouses", {"country": "ca", "latitude": cfg["latitude"],
                 "longitude": cfg["longitude"], "limit": cfg.get("limit", 10)}, key)
        return [{"warehouse_id": w["warehouse_id"], "name": w["name"], "state": ""}
                for w in r["data"]["warehouses"] if "business" not in w["name"].lower()]
    if cfg.get("warehouse_ids"):
        ids = set(cfg["warehouse_ids"])
        return [w for w in all_us_warehouses() if w["warehouse_id"] in ids]
    if cfg.get("zip"):
        r = post("/api/v1/costco/warehouses", {"zip": cfg["zip"], "limit": 50}, key)
        radius = cfg.get("radius_miles", 30)
        near = [w for w in r["data"]["warehouses"] if w["distance_miles"] <= radius]
        return [{"warehouse_id": w["warehouse_id"], "name": w["name"], "state": ""}
                for w in near if "business" not in w["name"].lower()]
    rows = all_us_warehouses()
    if cfg.get("states"):
        states = {s.upper() for s in cfg["states"]}
        rows = [w for w in rows if w["state"] in states]
    if cfg.get("near"):  # {"latitude": .., "longitude": .., "radius_miles": ..}
        n = cfg["near"]
        rows = [w for w in rows if miles(n["latitude"], n["longitude"], w["latitude"], w["longitude"]) <= n["radius_miles"]]
    return rows  # no scope keys = every regular US warehouse


def check(items: list, warehouses: list, cfg: dict, key: str) -> tuple:
    now, credits = {}, 0
    country = cfg.get("country", "us")
    for i in range(0, len(warehouses), WAREHOUSE_CHUNK):
        chunk = [w["warehouse_id"] for w in warehouses[i:i + WAREHOUSE_CHUNK]]
        r = post("/api/v1/costco/prices", {"country": country, "item_ids": items, "warehouse_ids": chunk}, key)
        credits += r.get("credits_used", 0)
        for item in r["data"]["items"]:
            for w in item["warehouses"]:
                code = (w.get("price_code") or {}).get("meaning")
                now[f'{item["item_number"]}@{w["warehouse_id"]}'] = (
                    {"price": w["price"], "code": code} if w["available"] else None)
    return now, credits


def notify(lines: list, webhook: str) -> None:
    for line in lines:
        print(line)
    if webhook and lines:
        body = json.dumps({"content": "\n".join(lines)[:1900]}).encode()
        req = urllib.request.Request(webhook, data=body, headers={"Content-Type": "application/json"})
        urllib.request.urlopen(req, timeout=30)


def main() -> None:
    cfg = json.loads(CONFIG.read_text())
    items = [str(i) for i in cfg["items"]][:MAX_ITEMS]
    key = api_key()
    warehouses = pick_warehouses(cfg, key)
    if not warehouses:
        sys.exit("No warehouses match the configured scope.")
    names = {w["warehouse_id"]: f'{w["name"]} {w["state"]}'.strip() for w in warehouses}
    before = json.loads(STATE.read_text()) if STATE.exists() else None
    now, credits = check(items, warehouses, cfg, key)

    lines = []
    if before is None:
        for item in items:
            listed = [k for k, v in now.items() if v and k.startswith(item + "@")]
            prices = sorted({now[k]["price"] for k in listed})
            lines.append(f"Item {item}: listed at {len(listed)} of {len(warehouses)} warehouses"
                         + (f", shelf price {', '.join(f'${p:.2f}' for p in prices)}" if prices else ""))
    else:
        for k, v in now.items():
            item, wid = k.split("@")
            old = before.get(k)
            where = f"{names.get(wid, wid)} (#{wid})"
            if v and not old:
                lines.append(f"NEW: item {item} now listed at {where}, ${v['price']:.2f}")
            elif v and old and v["price"] != old["price"]:
                lines.append(f"PRICE: item {item} at {where}: ${old['price']:.2f} -> ${v['price']:.2f}")
            elif not v and old:
                lines.append(f"GONE: item {item} no longer listed at {where}")
            if v and v.get("code") == "clearance" and (not old or old.get("code") != "clearance"):
                lines.append(f"CLEARANCE: item {item} at {where} is ${v['price']:.2f} (.97 ending)")
    STATE.write_text(json.dumps(now))
    notify(lines, cfg.get("discord_webhook", ""))
    print(f"checked {len(items)} item(s) at {len(warehouses)} warehouse(s), {credits} credit(s)", file=sys.stderr)


if __name__ == "__main__":
    main()
