#!/usr/bin/env python3
"""Build the LogShip import catalog for the lead "Hess" (Livipur Kinder. Spiel. Versand GmbH,
https://www.livipur.de — plentymarkets / plentyShop LTS) from the storefront's own IO feed
`GET /rest/io/variations?itemsPerPage=500&page=N` (answers unauthenticated, 3 pages ≈ 1 227
variations of 916 items) plus `GET /rest/io/categorytree` for the category names.

Output: catalog.json — one entry per shop variation for import-merchant-catalog.sh (+ `parentValue` = key of
its master article when the shop item has 2+ variations) and masters.json — the master ("summary")
articles for create-master-articles.sh:
  { value (= shop article number), name, grossPrice, netPrice, upc, sku, weight, length, width,
    height, group1 (brand = manufacturer), group2 (category path), classification (top category),
    description, documentNote, imageUrl, stock }

The feed exposes neither barcodes nor weights/dimensions: `upc` is a GS1 restricted-circulation
("in-store", prefix 20) EAN-13 derived from the variation id, weights/dims are estimates per
top category and price. Articles the shop does not sell any more (filter.isSalable = false) are
kept with stock 0, salable ones get a deterministic opening stock of 3…80.

  python3 build-catalog.py [catalog-raw.json] catalog.json      # fetches the feed when the raw file is missing
"""
import html, json, os, re, sys, time, urllib.request

BASE = "https://www.livipur.de"
UA = {"User-Agent": "Mozilla/5.0"}
args = sys.argv[1:]
out_path = args[-1] if args else "catalog.json"
raw_path = args[0] if len(args) > 1 else os.path.join(os.path.dirname(out_path) or ".", "catalog-raw.json")
tree_path = os.path.join(os.path.dirname(raw_path) or ".", "cattree.json")

def get_json(url):
    with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=120) as r:
        return json.load(r)

if not os.path.exists(raw_path):
    docs, page = [], 1
    while True:
        d = get_json(f"{BASE}/rest/io/variations?itemsPerPage=500&page={page}")["data"]
        docs += [x["data"] for x in d["documents"]]
        print(f"page {page}: {len(docs)}/{d['total']}")
        if len(docs) >= d["total"] or not d["documents"]: break
        page += 1; time.sleep(1)
    json.dump(docs, open(raw_path, "w", encoding="utf-8"), ensure_ascii=False)
if not os.path.exists(tree_path):
    json.dump(get_json(f"{BASE}/rest/io/categorytree"), open(tree_path, "w", encoding="utf-8"), ensure_ascii=False)
raw = json.load(open(raw_path, encoding="utf-8"))
tree = json.load(open(tree_path, encoding="utf-8"))["data"]

# category id -> (name, parent id)
cats = {}
def walk(nodes, parent=None):
    for n in nodes:
        name = next((d["name"] for d in n.get("details") or [] if d.get("lang") == "de"), None) or (n.get("details") or [{}])[0].get("name", "")
        cats[n["id"]] = (name, parent)
        walk(n.get("children") or [], n["id"])
walk(tree)
def cat_path(cid):
    path = []
    while cid in cats and len(path) < 6:
        name, cid = cats[cid]; path.insert(0, name)
    return path

SLUG_NAMES = {"moebel": "Möbel", "spielen": "Spielen", "entdecken": "Entdecken", "jahreszeiten": "Jahreszeiten",
              "angebote": "Angebote", "baby": "Baby", "marken": "Marken", "geburtstag": "Geburtstag",
              "lernen-handarbeiten": "Lernen & Handarbeiten", "neuheiten": "Neuheiten"}
# (kg, L, W, H in cm) by top category; furniture additionally scaled by price
SIZE = {"Möbel": (6.0, 60, 40, 15), "Spielen": (0.6, 30, 20, 8), "Baby": (0.3, 25, 20, 5),
        "Jahreszeiten": (0.2, 15, 10, 6), "Jahreszeitentisch": (0.2, 15, 10, 6), "Jahresfeste": (0.25, 18, 12, 8),
        "Lernen & Handarbeiten": (0.4, 30, 22, 4), "Geburtstag": (0.3, 20, 15, 8), "Entdecken": (0.5, 30, 20, 8)}
def size_for(top, price):
    kg, l, w, h = SIZE.get(top, (0.5, 30, 20, 10))
    if top == "Möbel":
        if price >= 400: return 28.0, 120, 70, 20
        if price >= 150: return 14.0, 90, 50, 15
    elif price >= 120: return round(kg * 3, 2), l * 2, w * 2, h * 2
    elif price >= 40: return round(kg * 1.6, 2), int(l * 1.4), int(w * 1.4), int(h * 1.4)
    return kg, l, w, h

def strip_html(s):
    s = re.sub(r"<[^>]+>", " ", s or "")
    return html.unescape(re.sub(r"\s+", " ", s)).strip()

def ean_instore(n):
    body = f"20{int(n):010d}"[:12]
    chk = (10 - sum(int(c) * (3 if i % 2 else 1) for i, c in enumerate(body)) % 10) % 10
    return body + str(chk)

def stock_for(vid, salable):
    if not salable: return 0
    return 3 + (int(vid) * 7919) % 78          # deterministic 3…80

items, seen = [], set()
for d in raw:
    v, t, it = d["variation"], d["texts"], d["item"]
    price = (d.get("prices") or {}).get("default") or {}
    gross = float(((price.get("price") or {}).get("value")) or 0)
    net = float(((price.get("data") or {}).get("priceNet")) or (gross / 1.19))
    vat = ((price.get("vat") or {}).get("value"))
    attrs = ", ".join(a["value"]["names"]["name"] for a in d.get("attributes") or [] if (a.get("value") or {}).get("names"))
    name1 = (t.get("name1") or "").strip()
    name = (v.get("name") or "").strip() or name1
    # the shop's variation name often lacks the distinguishing attribute (size, motif …) → append what is missing
    missing = [a["value"]["names"]["name"].strip() for a in d.get("attributes") or []
               if (a.get("value") or {}).get("names") and a["value"]["names"]["name"].strip().lower() not in name.lower()]
    if missing: name = f"{name}, {', '.join(missing)}"
    brand = ((it.get("manufacturer") or {}).get("externalName") or "Livipur").strip()
    cat_id = (d.get("defaultCategories") or [{}])[0].get("id")
    path = cat_path(cat_id) if cat_id else []
    if not path:
        slugs = (t.get("urlPath") or "").split("/")[:-1]
        path = [SLUG_NAMES.get(s, s.replace("-", " ").title()) for s in slugs]
    if (not path or path[0] in ("Angebote", "Neuheiten")):      # promo categories: prefer the real one from the URL
        slugs = (t.get("urlPath") or "").split("/")[:-1]
        if slugs and slugs[0] not in ("angebote", "neuheiten", "marken"):
            path = [SLUG_NAMES.get(s, s.replace("-", " ").title()) for s in slugs]
    top = path[0] if path else "Sonstiges"
    kg, l, w, h = size_for(top, gross)
    value = (v.get("number") or "").strip() or f"LV-{v['id']}"
    if value in seen: value = f"{value}-{v['id']}"
    seen.add(value)
    imgs = (d.get("images") or {}).get("variation") or (d.get("images") or {}).get("all") or []
    img = imgs[0].get("urlMiddle") if imgs else None
    salable = bool((d.get("filter") or {}).get("isSalable"))
    short = strip_html(t.get("shortDescription"))
    items.append({
        "value": value,
        "name": name[:120],
        "grossPrice": round(gross, 2),
        "netPrice": round(net, 2),
        "vat": vat,
        "upc": ean_instore(v["id"]),
        "sku": value,
        "weight": kg, "length": l, "width": w, "height": h,
        "group1": brand,
        "group2": " > ".join(path)[:255],
        "classification": top[:255],
        "description": (short or f"{brand} · {' > '.join(path)}")[:255],
        "documentNote": strip_html(t.get("description"))[:1900],
        "imageUrl": img,
        "stock": stock_for(v["id"], salable),
        "shopItemId": it.get("id"), "shopVariationId": v["id"], "shopUrl": f"{BASE}/{t.get('urlPath')}_{it.get('id')}_{v['id']}/",
        "salable": salable,
    })

# master ("summary") articles: one per shop item with 2+ variations — M_Product.IsSummary = Y, not
# stocked/sold/purchased; the variations reference the master's search key in M_Product.ParentId.
# Master key = the article number shared by all variations (1227812 for 1227812-1 / -2), else LV-<itemId>.
by_item = {}
for it in items: by_item.setdefault(it["shopItemId"], []).append(it)
values = {it["value"] for it in items}
masters = []
for item_id, vs in by_item.items():
    if len(vs) < 2: continue
    stems = {re.sub(r"-\d+$", "", v["value"]) for v in vs}
    key = next(iter(stems)) if len(stems) == 1 and next(iter(stems)) not in values else f"LV-{item_id}"
    src = next((v for v in vs if v["salable"]), vs[0])
    name = next((d["texts"].get("name1") for d in raw if d["item"]["id"] == item_id), None) or src["name"]
    masters.append({
        "value": key, "name": name.strip()[:120], "shopItemId": item_id, "variants": [v["value"] for v in vs],
        "group1": src["group1"], "group2": src["group2"], "classification": src["classification"],
        "description": (f"Stammartikel · {len(vs)} Varianten: " + ", ".join(v["value"] for v in vs))[:255],
        "documentNote": src["documentNote"], "imageUrl": src["imageUrl"],
        "shopUrl": f"{BASE}/{next(d['texts'].get('urlPath') for d in raw if d['item']['id'] == item_id)}_{item_id}/",
    })
    for v in vs: v["parentValue"] = key
json.dump(masters, open(os.path.join(os.path.dirname(out_path) or ".", "masters.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=0)
print(f"{len(masters)} master articles for {sum(len(m['variants']) for m in masters)} variations -> masters.json")
json.dump(items, open(out_path, "w", encoding="utf-8"), ensure_ascii=False, indent=0)
import collections
print(f"{len(items)} articles from {len({i['shopItemId'] for i in items})} items -> {out_path}")
print("salable", sum(1 for i in items if i["salable"]), "| no image", sum(1 for i in items if not i["imageUrl"]),
      "| price 0", sum(1 for i in items if not i["grossPrice"]), "| vat", collections.Counter(i["vat"] for i in items))
print("top categories", collections.Counter(i["classification"] for i in items).most_common())
print("brands", len({i["group1"] for i in items}), collections.Counter(i["group1"] for i in items).most_common(8))
