#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Extraction d'un catalogue cadeaux -> dataset JSON + images de pages + images produit.

Usage:
    python scripts/extract.py [chemin_pdf] [nom_catalogue]

Sorties:
  data/products.json         produits structures
  data/pages.json            meta pages + nom catalogue
  public/pages/p001.jpg      rendu de chaque page (feuilletage)
  public/products/<id>.jpg   image individuelle par produit (article distinct)
"""
import fitz, re, json, os, sys, unicodedata

DEFAULT_PDF = r"C:\Users\ERP 07\Downloads\md FR - GIFTS_FR_EUR_2026-1-70.pdf"
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA = os.path.join(ROOT, "data")
PAGES_IMG = os.path.join(ROOT, "public", "pages")
PROD_IMG = os.path.join(ROOT, "public", "products")

REF_RE = re.compile(r"MO\d{3,5}")
PRICE_ROW_RE = re.compile(r"\d+[.,]\d{2}")

CATS = {
    "TECHNOLOGY & ACCESSORIES": "Technology & Accessories",
    "OFFICE & WRITING": "Office & Writing",
    "EATING & DRINKING": "Eating & Drinking",
    "BAGS & TRAVEL": "Bags & Travel",
    "UMBRELLAS": "Umbrellas & Rain",
    "RECREATION": "Recreation & Kids",
    "KIDS": "Recreation & Kids",
}
ECO_KEYWORDS = [
    "recycl", "rpet", "bambou", "bio", "organic", "coton bio", "liège", "liege",
    "durable", "rcs", "grs", "naturel", "biodégrad", "biodegrad", "kraft",
    "paille", "cork", "recyclé", "recycle", "réutilis", "reutilis",
]

PAGE_ZOOM = 1.6      # rendu feuilletage
CROP_ZOOM = 2.3      # rendu haute-def pour decouper les produits


def clean(s):
    return re.sub(r"\s+", " ", (s or "").replace("­", "").strip())

def slugify(s):
    s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
    s = re.sub(r"[^a-zA-Z0-9]+", "-", s).strip("-").lower()
    return s or "item"

def line_bbox(line):
    xs = [sp["bbox"] for sp in line["spans"]]
    return (min(b[0] for b in xs), min(b[1] for b in xs),
            max(b[2] for b in xs), max(b[3] for b in xs))

def line_text(line):
    return "".join(sp["text"] for sp in line["spans"])


def fill_categories(doc):
    n = doc.page_count
    raw = [None] * n
    for i in range(n):
        up = doc[i].get_text().upper()
        for key, val in CATS.items():
            if key in up:
                raw[i] = val
                break
    cat = list(raw)
    for i in range(n):
        if cat[i]:
            continue
        p1 = i + 1
        part1 = p1 + 1 if p1 % 2 == 0 else p1 - 1
        pi = part1 - 1
        if 0 <= pi < n and raw[pi]:
            cat[i] = raw[pi]
    last = None
    for i in range(n):
        if cat[i]:
            last = cat[i]
        elif last:
            cat[i] = last
    nxt = None
    for i in range(n - 1, -1, -1):
        if cat[i]:
            nxt = cat[i]
        elif nxt:
            cat[i] = nxt
    return cat


def pick_image(ref_bbox, images, used):
    """Associe la meilleure image produit a une reference (au-dessus, meme colonne)."""
    rcx = (ref_bbox[0] + ref_bbox[2]) / 2
    rtop = ref_bbox[1]
    cands = []
    for idx, im in enumerate(images):
        b = im["bbox"]
        icx = (b[0] + b[2]) / 2
        icy = (b[1] + b[3]) / 2
        # l'image doit etre au-dessus (ou englobante) et dans la colonne
        if icy > rtop + 12:
            continue
        if abs(icx - rcx) > 150:
            continue
        area = (b[2] - b[0]) * (b[3] - b[1])
        gap = rtop - b[3]                      # distance verticale image->texte
        score = gap + abs(icx - rcx) * 0.6 - area * 0.0006
        cands.append((score, area, idx))
    if not cands:
        return None
    cands.sort()
    # preferer une image non deja utilisee
    for score, area, idx in cands:
        if idx not in used:
            return idx
    return cands[0][2]


def main():
    pdf_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PDF
    catalog_name = sys.argv[2] if len(sys.argv) > 2 else os.path.splitext(os.path.basename(pdf_path))[0]

    os.makedirs(DATA, exist_ok=True)
    os.makedirs(PAGES_IMG, exist_ok=True)
    os.makedirs(PROD_IMG, exist_ok=True)
    # nettoyer les anciennes images
    for d in (PAGES_IMG, PROD_IMG):
        for f in os.listdir(d):
            if f.lower().endswith((".jpg", ".png")):
                os.remove(os.path.join(d, f))

    doc = fitz.open(pdf_path)
    n = doc.page_count
    cat = fill_categories(doc)

    products = []
    pages_meta = []

    for i in range(n):
        page = doc[i]
        p1 = i + 1
        pix = page.get_pixmap(matrix=fitz.Matrix(PAGE_ZOOM, PAGE_ZOOM))
        pix.save(os.path.join(PAGES_IMG, f"p{p1:03d}.jpg"), jpg_quality=80)
        spread_left = p1 if p1 % 2 == 0 else p1 - 1
        pages_meta.append({
            "page": p1, "image": f"/pages/p{p1:03d}.jpg",
            "spread": f"{spread_left}-{spread_left+1}", "category": cat[i],
            "width": pix.width, "height": pix.height,
        })
        if p1 <= 9:
            continue

        d = page.get_text("dict")
        lines = []
        for block in d["blocks"]:
            for ln in block.get("lines", []):
                if not line_text(ln).strip():
                    continue
                bb = line_bbox(ln)
                lines.append({"text": line_text(ln), "bbox": bb,
                              "xc": (bb[0]+bb[2])/2, "yc": (bb[1]+bb[3])/2})
        lines.sort(key=lambda l: (round(l["yc"]), l["xc"]))

        refs = []
        for idx, ln in enumerate(lines):
            for m in REF_RE.finditer(ln["text"]):
                refs.append({"ref": m.group(0), "line": idx, "bbox": ln["bbox"],
                             "xc": ln["xc"], "yc": ln["yc"]})
        if not refs:
            continue

        price_lines = []
        for idx, ln in enumerate(lines):
            nums = PRICE_ROW_RE.findall(ln["text"])
            if len(nums) >= 2:
                vals = sorted({float(x.replace(",", ".")) for x in nums})
                price_lines.append({"xc": ln["xc"], "yc": ln["yc"], "prices": vals})

        # images produit "candidates" (on ecarte les tres petits pictogrammes)
        raw_imgs = page.get_image_info()
        pw = page.rect.width
        imgs = []
        for im in raw_imgs:
            b = im["bbox"]
            w, h = b[2] - b[0], b[3] - b[1]
            if w < 55 or h < 55:
                continue                       # pictos de marquage / swatches
            if w > pw * 0.98 and h > page.rect.height * 0.98:
                continue                       # fond pleine page
            imgs.append(im)

        used = set()

        for r in refs:
            lt = lines[r["line"]]["text"]
            before = clean(lt.split(r["ref"])[0].strip(" -\t"))
            def ok(c):
                return (c and 1 < len(c) <= 32 and re.search(r"[A-Za-z]", c)
                        and not REF_RE.search(c) and not c.endswith(".")
                        and "Technique" not in c and "Prix" not in c and not c[0].islower())
            name = before if ok(before) else None
            if not name and r["line"] > 0 and ok(clean(lines[r["line"]-1]["text"])):
                name = clean(lines[r["line"]-1]["text"])
            name = name or r["ref"]

            desc_parts, technique = [], None
            for idx in range(r["line"]+1, min(r["line"]+8, len(lines))):
                t = clean(lines[idx]["text"])
                if not t:
                    continue
                if REF_RE.search(t):
                    break
                if t.startswith("Technique"):
                    technique = t.replace("Technique marquage", "").strip(" :")
                    continue
                if t.startswith("Prix") or PRICE_ROW_RE.search(t) or t.isdigit():
                    break
                desc_parts.append(t)
            desc = clean(" ".join(desc_parts))[:400]

            best, bestd = None, 1e9
            for pl in price_lines:
                if pl["yc"] < r["yc"] - 5:
                    continue
                dist = abs(pl["xc"] - r["xc"]) * 1.5 + (pl["yc"] - r["yc"])
                if dist < bestd:
                    bestd, best = dist, pl
            prices = best["prices"] if best else []
            price_from = min(prices) if prices else None
            price_start = max(prices) if prices else None
            hay = (name + " " + desc + " " + (technique or "")).lower()

            pid = slugify(name) + "-" + r["ref"].lower()

            # --- image produit : decoupe de la cellule (rendu direct de la region) ---
            img_url = None
            im_idx = pick_image(r["bbox"], imgs, used)
            if im_idx is not None:
                used.add(im_idx)
                b = imgs[im_idx]["bbox"]
                pad = 6
                clip = fitz.Rect(
                    max(0, b[0] - pad), max(0, b[1] - pad),
                    min(page.rect.width, b[2] + pad), min(page.rect.height, b[3] + pad),
                )
                if clip.width > 10 and clip.height > 10:
                    crop = page.get_pixmap(matrix=fitz.Matrix(CROP_ZOOM, CROP_ZOOM), clip=clip)
                    fn = f"{pid}.jpg"
                    crop.save(os.path.join(PROD_IMG, fn), jpg_quality=82)
                    img_url = f"/products/{fn}"

            products.append({
                "id": pid, "ref": r["ref"], "name": name, "description": desc,
                "technique": technique, "category": cat[i], "page": p1,
                "spread": f"{spread_left}-{spread_left+1}",
                "priceFrom": round(price_from, 2) if price_from else None,
                "priceStart": round(price_start, 2) if price_start else None,
                "eco": any(k in hay for k in ECO_KEYWORDS),
                "priceConfident": bool(best and bestd < 120),
                "image": img_url,
            })

    def is_real(p):
        return bool(p["priceFrom"] or p["technique"]
                    or (p["description"] and len(p["description"]) > 15))
    products = [p for p in products if is_real(p)]

    seen = {}
    for p in products:
        k = p["ref"]
        if k not in seen or (not seen[k]["priceFrom"] and p["priceFrom"]) \
           or (not seen[k]["image"] and p["image"]):
            seen[k] = p
    uniq = list(seen.values())

    with open(os.path.join(DATA, "products.json"), "w", encoding="utf-8") as f:
        json.dump(uniq, f, ensure_ascii=False, indent=1)
    with open(os.path.join(DATA, "pages.json"), "w", encoding="utf-8") as f:
        json.dump({"catalog": catalog_name, "pages": pages_meta}, f, ensure_ascii=False, indent=1)

    wp = sum(1 for p in uniq if p["priceFrom"])
    wi = sum(1 for p in uniq if p["image"])
    print(f"catalogue : {catalog_name}")
    print(f"pages     : {n}")
    print(f"produits  : {len(uniq)}")
    print(f"avec prix : {wp} ({100*wp//max(len(uniq),1)}%)")
    print(f"avec image: {wi} ({100*wi//max(len(uniq),1)}%)")


if __name__ == "__main__":
    main()
