#!/usr/bin/env python3
"""
SneakerPicks Feed Sync v3 — Category filtering + Size dedup + Cross-shop matching + Price history
Handles Webgains feeds with:
- Sneaker-only filtering (category + name-based)
- Size variant aggregation (one offer per product-per-shop)
- Cross-shop product matching (EAN/fingerprint)
- Price history logging
"""
import requests
import psycopg2
import psycopg2.extras
import re
import hashlib
import time
import json
import sys
from collections import defaultdict

# ── Config ──
WEBGAINS_API = "https://platform-api.webgains.com"
PUBLISHER_ID = "1347930"
WEBGAINS_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxMDAwMzEiLCJqdGkiOiIwNjM5ODEwYzQ0YzM1MTBlZmZiMGRkYWNiM2EyNWFlNTg2YzFmNmQ5ODNjNGQ2MDA4NzkwZmM0NTkzY2EyNjg4ZGViZDE4NTgwOThmYjJjYiIsImlhdCI6MTc3MTUxNjc4OS44MzAxMzUsIm5iZiI6MTc3MTUxNjc4OS44MzAxMzcsImV4cCI6MjA4NzA0OTU4OS44MjcwODgsInN1YiI6IjIwMTcwOCIsInNjb3BlcyI6W119.ZxlCDsyBI7_SNUWjO-PUxDYKvQVfuUBzU5n-SgqHdAZCnOj9253yVcJIS5NRXJLrnyx0E_9bn8ymfXS71cvPISA4hPAlKCSMjdkv9f2k2FFwdAlQQ6mdhBB2m9dzHUIn34QwA8s4-ikB1ATKIZeqiJL-3Gh9eiMYPpvaJdCCjN-oGPMhhlehkgA04cZSgwy6HfKgb6uPvUSG9w_tvN8U06uEHKV11DedQHNWWGzuXpd2FEL3baV0z9KjSNMZsJZHx2yHUb6gVWJwaR7_louahMvrPXG-YLgnGvNMcPDPjm-z5HJ-JKcPv9_FBqAYGItZxgeGgVoS8MVhW6lc1a-6CvYtVanfXF42MPPNP-Qq76qRtHPLRBdkIATMuBxVJrMzlprT3n4F0sUUltGR4ixZuzMUm5LeyPMHgmjy2UfYzsl9eVqx0oJCmKUyY9maBXwFLJa8J1NHM_nWp3hPpCGkMyhzHK1DIf_z8Wirg1hp5vKts93eERWJBlQwhucUUS_KnZeC_gVaQ7kT4jCwaEWkpPxPxyXWRoK0mcYS_JGz3JbpVhRLj5DVYIB9k54DXqjYlCx8ezJZE9sf2SX3VWvPvD1xSQD1caTCm0To0B-MkeU1YpEfWdUslpCJXrpOy2Wbs8iDHR1BzNrMnOEr4zbz_EoB4JJe3tGTGjzNLNIw6XI"

DB_URL = "postgresql://supabase_admin:HzWrn2rMMxUYWO6OT5X0PScIVEvSjQMup9YSLIrkPQ@172.18.0.5:5432/sneakerpicks"

CAMPAIGNS = {
    "manify.nl": "1622230",
    "ladify.nl": "1622250",
    "thehike.nl": "1622240",
    "mellowed.nl": "1622260",
}

HEADERS = {
    "Authorization": f"Bearer {WEBGAINS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
}

PRODUCT_FIELDS = [
    "title", "image_link", "link", "price", "brand", "description", "id",
    "sale_price", "gtin", "mpn", "colour", "size", "availability", "condition",
    "product_type", "google_product_category"
]

# ── Sneaker Detection ──

# Known sneaker brands (always accept footwear from these)
SNEAKER_BRANDS = {
    'nike', 'adidas', 'adidas originals', 'new balance', 'puma', 'asics', 'asisc',
    'jordan', 'reebok', 'converse', 'vans', 'saucony', 'hoka', 'on running', 'on',
    'salomon', 'mizuno', 'diadora', 'karhu', 'filling pieces', 'axel arigato',
    'common projects', 'autry', 'golden goose', 'versace', 'gucci', 'balenciaga',
    'alexander mcqueen', 'off-white', 'fear of god', 'rick owens', 'maison margiela',
    'mm6 maison margiela', 'y-3', 'stone island', 'moncler', 'premiata', 'ghoud',
    'philippe model', 'p448', 'date', 'd.a.t.e.', 'womsh', 'copenhagen', 
    'copenhagen studios', 'nubikk', 'mason garments', 'mercer amsterdam',
    'floris van bommel', 'cruyff', 'android homme', 'etq amsterdam', 'ekn',
    'yeezy', 'li-ning', 'anta', 'hi-tec', 'hts', 'mikakus',
    'cph', 'cph40', 'birkenstock', 'clarks', 'dr. martens',
    'lacoste', 'tommy hilfiger', 'calvin klein', 'hugo boss', 'boss',
    'fila', 'k-swiss', 'le coq sportif', 'ellesse', 'umbro', 'lotto',
    'under armour', 'brooks', 'altra', 'topo athletic',
}

# Category patterns that indicate sneakers/footwear
SNEAKER_CATEGORY_PATTERNS = [
    'sneaker', 'trainer', 'running shoe', 'sportschoenen', 'footwear',
    'athletic shoe', 'casual shoe', 'schoen', 'shoes', 'boots',  # boots ok for sneaker brands
    'apparel & accessories > shoes',
]

# Category patterns that indicate NOT sneakers
NON_SNEAKER_CATEGORY_PATTERNS = [
    'clothing', 'apparel > clothing', 'accessori', 'bag', 'hat', 'cap',
    'fragrance', 'home', 'art', 'book', 'candle', 'poster',
    'shirt', 'jacket', 'trouser', 'pant', 'hoodie', 'sweater',
]

# Title exclusion patterns (definitely not sneakers)
EXCLUDE_TITLE_PATTERNS = [
    r'\bt-shirt', r'\btshirt', r'\btee\b', r'\bhoodie', r'\bhoody\b',
    r'\bsweater', r'\bsweatshirt', r'\bjacket\b', r'\bcoat\b',
    r'\bpants\b', r'\btrouser', r'\bjogger', r'\bshorts\b', r'\bjeans\b',
    r'\bshirt\b', r'\bpolo\b', r'\bvest\b', r'\bcap\b', r'\bhat\b',
    r'\bbeanie\b', r'\bscarf\b', r'\bglove', r'\bsock\b', r'\bsocks\b',
    r'\bbackpack', r'\bbag\b', r'\bwallet\b', r'\bbelt\b', r'\bwatch\b',
    r'\bsunglasses', r'\bunderwear', r'\bboxer', r'\bswim',
    r'\bperfume', r'\bfragrance', r'\bdeodorant', r'\bcandle',
    r'\bkeychain', r'\bkeyring', r'\bphone case', r'\bairpod',
    r'\binsole', r'\blaces\b', r'\bcleaner\b', r'\bprotector\b',
    r'\bspray\b', r'\bbrush\b', r'\btracksuit', r'\bwindbreaker',
    r'\bpuffer\b', r'\bfleece\b', r'\bcardigan', r'\bcrewneck',
    r'\blongsleeve', r'\btank top', r'\blegging', r'\bskirt\b',
    r'\bdress\b', r'\bjumpsuit', r'\bblanket', r'\bpillow',
    r'\bmug\b', r'\bcup\b', r'\bbottle\b', r'\bincense',
    r'\bsticker\b', r'\bpatch\b', r'\blanyard', r'\btote\b',
    r'\bflip.?flop', r'\bsandal', r'\bslipper', r'\bclog\b',
    r'\bloafer', r'\bpump\b', r'\bheel\b', r'\bwedge\b',
    r'\bdress shoe', r'\boxford\b', r'\bderby\b', r'\bbrogue\b',
    r'\bchelsea boot',  # debatable but not a sneaker
]

# Known sneaker model patterns (accept even without category)
SNEAKER_MODEL_PATTERNS = [
    r'air max', r'air force', r'air jordan', r'\baj\d', r'\bdunk\b',
    r'\bwaffle\b', r'\bcortez\b', r'\bblaze\b', r'\bpegasus\b',
    r'\b574\b', r'\b990\b', r'\b992\b', r'\b993\b', r'\b550\b', r'\b530\b', r'\b2002r?\b',
    r'\bgel.?lyte', r'\bgel.?kayano', r'\bgel.?1130', r'\bgel.?nyc',
    r'\bsamba\b', r'\bgazelle\b', r'\bsuperstar\b', r'\bstan smith\b', r'\bforum\b',
    r'\bozweego\b', r'\byung\b', r'\bnite jogger', r'\bultraboost\b', r'\bnmd\b',
    r'\bold skool\b', r'\bsk8.?hi\b', r'\bera\b', r'\bauthentic\b', r'\bslip.?on\b',
    r'\bchuck taylor\b', r'\ball star\b', r'\bone star\b', r'\bjack purcell\b',
    r'\bclassic leather\b', r'\bclub c\b', r'\bquestion\b',
    r'\bsuede\b', r'\brs-?x\b', r'\brs.?connect', r'\beasy rider\b',
    r'\bshadow\b.*\b6000\b', r'\bjazz\b', r'\bgrid\b',
    r'\bclifton\b', r'\bbondi\b', r'\bmafate\b', r'\bspeedgoat\b',
    r'\bxt-?\d', r'\bspeedcross\b', r'\bwave rider\b', r'\bmorelia\b',
    r'\bchunky\b', r'\bretro\b', r'\bplatform\b',
    r'\bprogrind\b', r'\bprogrid\b',
    r'\bcloud\b.*\bmonster', r'\bcloudmonster\b',
]


def is_sneaker(title, brand, product_type, google_category):
    """Determine if a product is a sneaker. Returns (bool, reason)."""
    title_lower = (title or '').lower()
    brand_lower = (brand or '').lower()
    ptype_lower = (product_type or '').lower()
    gcat_lower = (google_category or '').lower()
    
    # Step 1: Exclude by title (clothing, accessories, etc.)
    for pat in EXCLUDE_TITLE_PATTERNS:
        if re.search(pat, title_lower):
            return False, f'title_exclude:{pat}'
    
    # Step 2: Check category exclusions
    for pat in NON_SNEAKER_CATEGORY_PATTERNS:
        if pat in ptype_lower or pat in gcat_lower:
            return False, f'category_exclude:{pat}'
    
    # Step 3: Check category inclusions
    for pat in SNEAKER_CATEGORY_PATTERNS:
        if pat in ptype_lower or pat in gcat_lower:
            return True, f'category_match:{pat}'
    
    # Step 4: Known sneaker brand → accept (footwear from sneaker brands)
    if brand_lower in SNEAKER_BRANDS:
        # But reject if it's clearly not footwear
        return True, f'brand_match:{brand_lower}'
    
    # Step 5: Known sneaker model patterns in name
    for pat in SNEAKER_MODEL_PATTERNS:
        if re.search(pat, title_lower):
            return True, f'model_match:{pat}'
    
    # Step 6: No signal → reject
    return False, 'no_match'


# ── Size Normalization ──

SIZE_PATTERNS = [
    # "EU 42", "EU42", "Maat 42"
    r'\b(?:eu|eur|maat)\s*(\d{2}(?:[.,]\d)?)\b',
    # "42.5", "43" at end of string or followed by non-alpha
    r'\b(\d{2}(?:[.,]\d{1,2})?)\s*$',
    # "Size 42"
    r'\bsize\s*(\d{2}(?:[.,]\d)?)\b',
    # US sizes
    r'\bus\s*(\d{1,2}(?:[.,]\d)?)\b',
]

def extract_size(title, size_field):
    """Extract size from size field or title."""
    if size_field and size_field.strip():
        return size_field.strip()
    
    for pat in SIZE_PATTERNS:
        m = re.search(pat, title.lower())
        if m:
            return f"EU {m.group(1)}"
    return None


def normalize_product_name(title, brand):
    """Remove size info and normalize product name for grouping."""
    name = title.strip()
    # Remove size suffixes like "42", "EU 42", "Maat 42.5"
    name = re.sub(r'\s+(?:eu|eur|maat|size)\s*\d{2}(?:[.,]\d{1,2})?\s*$', '', name, flags=re.I)
    name = re.sub(r'\s+\d{2}(?:[.,]\d{1,2})?\s*$', '', name)
    # Remove trailing whitespace/dashes
    name = name.strip(' -–—')
    return name


def make_fingerprint(brand, name, colorway):
    """Create a match fingerprint for cross-shop dedup."""
    parts = [
        (brand or '').lower().strip(),
        re.sub(r'[^\w\s]', '', (name or '').lower()).strip(),
        (colorway or '').lower().strip(),
    ]
    raw = '|'.join(parts)
    return hashlib.md5(raw.encode()).hexdigest()


# ── Utilities ──

def slugify(text):
    text = text.lower().strip()
    text = re.sub(r'[^\w\s-]', '', text)
    text = re.sub(r'[\s_]+', '-', text)
    text = re.sub(r'-+', '-', text)
    return text[:200]


def parse_price(val):
    if not val:
        return None
    val = str(val).replace(',', '.').strip()
    match = re.search(r'[\d.]+', val)
    if match:
        try:
            return float(match.group())
        except:
            return None
    return None


# ── Webgains API ──

def get_feeds():
    """Get all unique feeds across campaigns."""
    all_feeds = {}
    for site, camp_id in CAMPAIGNS.items():
        url = f"{WEBGAINS_API}/auth/publishers/{PUBLISHER_ID}/campaigns/{camp_id}/feeds"
        try:
            r = requests.get(url, headers=HEADERS, params={"size": 100}, timeout=30)
            r.raise_for_status()
            data = r.json()
            for f in data.get("data", []):
                fid = f["id"]
                if fid not in all_feeds:
                    all_feeds[fid] = {
                        "id": fid,
                        "name": f.get("name", ""),
                        "program_name": f.get("program", {}).get("name", ""),
                        "program_id": str(f.get("program", {}).get("id", "")),
                        "product_count": f.get("products_successfully_stored", 0),
                        "campaign_id": camp_id,
                    }
            print(f"  {site} ({camp_id}): {len(data.get('data', []))} feeds")
        except Exception as e:
            print(f"  {site} ERROR: {e}")
    return all_feeds


def fetch_all_products_cursor(campaign_id, feed_id, max_pages=200):
    """Fetch ALL products using cursor-based pagination."""
    url = f"{WEBGAINS_API}/auth/publishers/{PUBLISHER_ID}/campaigns/{campaign_id}/feeds/products"
    all_products = []
    seen_ids = set()
    cursor = None

    for page in range(max_pages):
        params = {
            "feeds[]": [feed_id],
            "size": 1000,
            "fields[]": PRODUCT_FIELDS,
        }
        if cursor:
            params["search_cursor"] = cursor

        try:
            r = requests.get(url, headers=HEADERS, params=params, timeout=60)
            r.raise_for_status()
            data = r.json()
        except Exception as e:
            print(f"    Page {page} error: {e}")
            break

        products = data.get("data", [])
        if not products:
            break

        pagination = data.get("pagination", {})
        new_cursor = pagination.get("search_cursor")

        new_products = []
        for p in products:
            pid = p.get("id")
            if pid and pid not in seen_ids:
                seen_ids.add(pid)
                new_products.append(p)

        if not new_products:
            break

        all_products.extend(new_products)

        if page % 10 == 0 or len(new_products) < 900:
            print(f"    Page {page}: +{len(new_products)} (total: {len(all_products)})")

        if new_cursor == cursor or not new_cursor:
            break

        cursor = new_cursor
        time.sleep(0.3)

    return all_products


# ── Import Pipeline ──

def process_feed(cur, conn, feed_info, provider_id, brand_cache, ean_product_cache, fingerprint_cache, only_feed=None):
    """Process a single feed: fetch → filter → dedupe sizes → upsert."""
    feed_ext_id = feed_info["id"]
    fname = feed_info["name"]
    camp_id = feed_info["campaign_id"]

    if only_feed and str(feed_ext_id) != str(only_feed):
        return 0, 0, 0

    print(f"\n{'='*60}")
    print(f"Feed: {fname} (ext_id={feed_ext_id})")
    print(f"{'='*60}")

    # Upsert feed in DB
    cur.execute("""
        INSERT INTO feeds (provider_id, external_id, name, merchant_name, product_count)
        VALUES (%s, %s, %s, %s, %s)
        ON CONFLICT (provider_id, external_id) DO UPDATE SET
            name = EXCLUDED.name, merchant_name = EXCLUDED.merchant_name
        RETURNING id
    """, (provider_id, str(feed_ext_id), fname, feed_info["program_name"], feed_info.get("product_count") or 0))
    db_feed_id = cur.fetchone()["id"]
    conn.commit()

    # Fetch products
    products = fetch_all_products_cursor(camp_id, feed_ext_id)
    if not products:
        print("  No products, skipping")
        return 0, 0, 0

    print(f"  Fetched: {len(products)} raw products")

    # ── Step 1: Filter for sneakers ──
    accepted = []
    rejected_count = 0
    reject_batch = []

    for p in products:
        title = (p.get("title") or "").strip()
        brand_name = (p.get("brand") or "").strip()
        ptype_raw = p.get("product_type") or ""
        if isinstance(ptype_raw, list):
            ptype_raw = ptype_raw[0] if ptype_raw else ""
        ptype = str(ptype_raw).strip()
        gcat_raw = p.get("google_product_category") or ""
        if isinstance(gcat_raw, list):
            gcat_raw = gcat_raw[0] if gcat_raw else ""
        gcat = str(gcat_raw).strip()

        is_snkr, reason = is_sneaker(title, brand_name, ptype, gcat)

        if is_snkr:
            accepted.append(p)
        else:
            rejected_count += 1
            reject_batch.append((db_feed_id, str(p.get("id", "")), title, brand_name, ptype or gcat, reason))

    # Log rejects in batches
    if reject_batch:
        psycopg2.extras.execute_batch(cur, """
            INSERT INTO import_rejects (feed_id, external_id, name, brand, category, reason)
            VALUES (%s, %s, %s, %s, %s, %s)
        """, reject_batch, page_size=500)
        conn.commit()

    print(f"  Filtered: {len(accepted)} accepted, {rejected_count} rejected")

    # ── Step 2: Group by product (size dedup) ──
    # Group key: brand_lower + normalized_name
    groups = defaultdict(list)
    for p in accepted:
        title = (p.get("title") or "").strip()
        brand_name = (p.get("brand") or "").strip()
        norm_name = normalize_product_name(title, brand_name)
        key = f"{brand_name.lower()}||{norm_name.lower()}"
        groups[key].append(p)

    print(f"  Grouped into {len(groups)} unique products (from {len(accepted)} variants)")

    # ── Step 3: Upsert products + offers ──
    new_products = 0
    total_offers = 0
    price_history_batch = []

    for group_key, variants in groups.items():
        # Pick the "best" variant for product info (first one with most data)
        primary = variants[0]
        title = normalize_product_name((primary.get("title") or "").strip(), (primary.get("brand") or "").strip())
        brand_name = (primary.get("brand") or "").strip()
        image = (primary.get("image_link") or "").strip()
        colour = (primary.get("colour") or "").strip()
        description = (primary.get("description") or "")[:500]
        ptype_raw2 = primary.get("product_type") or ""
        if isinstance(ptype_raw2, list):
            ptype_raw2 = ptype_raw2[0] if ptype_raw2 else ""
        ptype = str(ptype_raw2).strip()
        gcat_raw2 = primary.get("google_product_category") or ""
        if isinstance(gcat_raw2, list):
            gcat_raw2 = gcat_raw2[0] if gcat_raw2 else ""
        gcat = str(gcat_raw2).strip()

        # Collect all EANs and sizes
        sizes_data = []
        all_eans = set()
        min_price = None
        min_original_price = None
        best_link = (primary.get("link") or "").strip()

        for v in variants:
            price = parse_price(v.get("sale_price") or v.get("price"))
            orig_price = parse_price(v.get("price")) if v.get("sale_price") else None
            link = (v.get("link") or "").strip()

            gtin_val = v.get("gtin") or ""
            if isinstance(gtin_val, list):
                gtin_val = gtin_val[0] if gtin_val else ""
            ean = str(gtin_val).strip() or None

            size_val = extract_size((v.get("title") or ""), (v.get("size") or ""))

            if ean:
                all_eans.add(ean)

            if price:
                size_entry = {"size": size_val or "One Size", "price": price}
                if ean:
                    size_entry["ean"] = ean
                if link:
                    size_entry["url"] = link
                sizes_data.append(size_entry)

                if min_price is None or price < min_price:
                    min_price = price
                    best_link = link
                if orig_price and (min_original_price is None or orig_price < min_original_price):
                    min_original_price = orig_price

        if not min_price or not title or not best_link:
            continue

        primary_ean = next(iter(all_eans), None)

        # ── Cross-shop matching: find or create product ──
        product_id = None

        # Try EAN match first
        for ean in all_eans:
            if ean in ean_product_cache:
                product_id = ean_product_cache[ean]
                break
            cur.execute("SELECT id FROM products WHERE ean = %s", (ean,))
            row = cur.fetchone()
            if row:
                product_id = row["id"]
                ean_product_cache[ean] = product_id
                break

        # Try fingerprint match
        if not product_id:
            fp = make_fingerprint(brand_name, title, colour)
            if fp in fingerprint_cache:
                product_id = fingerprint_cache[fp]
            else:
                cur.execute("SELECT id FROM products WHERE match_fingerprint = %s", (fp,))
                row = cur.fetchone()
                if row:
                    product_id = row["id"]
                    fingerprint_cache[fp] = product_id

        # Create new product
        if not product_id:
            brand_slug = slugify(brand_name) if brand_name else "unknown"
            if brand_slug not in brand_cache:
                try:
                    cur.execute("""
                        INSERT INTO brands (name, slug) VALUES (%s, %s)
                        ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name
                        RETURNING id
                    """, (brand_name or "Unknown", brand_slug))
                    brand_cache[brand_slug] = cur.fetchone()["id"]
                except Exception:
                    conn.rollback()
                    cur.execute("SELECT id FROM brands WHERE slug = %s", (brand_slug,))
                    row = cur.fetchone()
                    if row:
                        brand_cache[brand_slug] = row["id"]
            brand_id = brand_cache.get(brand_slug)

            fp = make_fingerprint(brand_name, title, colour)
            base_slug = slugify(f"{brand_name}-{title}")[:180]
            slug_hash = hashlib.md5(f"{feed_ext_id}-{group_key}".encode()).hexdigest()[:8]
            product_slug = f"{base_slug}-{slug_hash}"

            try:
                cur.execute("""
                    INSERT INTO products (name, slug, brand_id, brand, image_url, description, ean,
                                          colorway, brand_normalized, model_normalized, colorway_normalized,
                                          match_fingerprint, category, product_type, first_seen_at)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
                    RETURNING id
                """, (title, product_slug, brand_id, brand_name, image, description,
                      primary_ean, colour,
                      brand_name.lower() if brand_name else None,
                      title.lower() if title else None,
                      colour.lower() if colour else None,
                      fp, gcat or ptype, ptype))
                product_id = cur.fetchone()["id"]
                new_products += 1

                # Cache
                fingerprint_cache[fp] = product_id
                for ean in all_eans:
                    ean_product_cache[ean] = product_id
            except psycopg2.errors.UniqueViolation:
                conn.rollback()
                cur.execute("SELECT id FROM products WHERE slug = %s", (product_slug,))
                row = cur.fetchone()
                if row:
                    product_id = row["id"]
                else:
                    continue
            except Exception as e:
                conn.rollback()
                print(f"    Error creating product '{title}': {e}")
                continue

        # ── Upsert offer with aggregated sizes ──
        # Use first external_id as the canonical one for the offer
        ext_id = str(variants[0].get("id", "")).strip()
        sizes_json = json.dumps(sizes_data, ensure_ascii=False)

        try:
            cur.execute("""
                INSERT INTO product_offers (product_id, feed_id, external_id, price, original_price,
                                             currency, product_url, image_url, in_stock, sizes, last_seen_at)
                VALUES (%s, %s, %s, %s, %s, 'EUR', %s, %s, true, %s::jsonb, NOW())
                ON CONFLICT (feed_id, external_id) DO UPDATE SET
                    price = EXCLUDED.price,
                    original_price = EXCLUDED.original_price,
                    product_url = EXCLUDED.product_url,
                    image_url = EXCLUDED.image_url,
                    in_stock = true,
                    sizes = EXCLUDED.sizes,
                    last_seen_at = NOW(),
                    updated_at = NOW()
            """, (product_id, db_feed_id, ext_id, min_price, min_original_price,
                  best_link, image, sizes_json))
            total_offers += 1

            # Price history
            price_history_batch.append((product_id, db_feed_id, min_price))
        except Exception as e:
            conn.rollback()
            print(f"    Error upserting offer for '{title}': {e}")
            continue

        # Commit every 200 products
        if total_offers % 200 == 0:
            conn.commit()

    conn.commit()

    # ── Price history (only insert if price changed) ──
    for product_id, feed_id, price in price_history_batch:
        try:
            cur.execute("""
                INSERT INTO price_history (product_id, feed_id, price, recorded_at)
                SELECT %s, %s, %s, NOW()
                WHERE NOT EXISTS (
                    SELECT 1 FROM price_history
                    WHERE product_id = %s AND feed_id = %s AND price = %s
                    AND recorded_at > NOW() - INTERVAL '23 hours'
                )
            """, (product_id, feed_id, price, product_id, feed_id, price))
        except Exception:
            conn.rollback()
    conn.commit()

    # Update feed stats
    cur.execute("""
        UPDATE feeds SET product_count = %s, last_sync_at = NOW(), last_sync_status = 'success'
        WHERE id = %s
    """, (total_offers, db_feed_id))
    conn.commit()

    print(f"  ✓ {new_products} new products, {total_offers} offers, {len(price_history_batch)} price records")
    return new_products, total_offers, rejected_count


def main():
    only_feed = None
    clean_first = False

    for arg in sys.argv[1:]:
        if arg == '--clean':
            clean_first = True
        elif arg.startswith('--feed='):
            only_feed = arg.split('=')[1]

    print("=== SneakerPicks Feed Sync v3 ===")
    print(f"  Options: only_feed={only_feed}, clean={clean_first}")
    print()

    conn = psycopg2.connect(DB_URL)
    conn.autocommit = False
    cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)

    if clean_first:
        print("Cleaning old data...")
        cur.execute("""
            TRUNCATE product_offers, price_history, featured_products, collection_products,
                     collection_overrides, product_categories, product_tags, price_alerts,
                     import_rejects
            RESTART IDENTITY CASCADE
        """)
        cur.execute("DELETE FROM products")
        cur.execute("DELETE FROM feeds")
        cur.execute("DELETE FROM sync_logs")
        cur.execute("DELETE FROM brands WHERE slug != 'unknown'")
        conn.commit()
        print("Done.\n")

    # Get provider
    cur.execute("SELECT id FROM providers WHERE slug = 'webgains'")
    provider_id = cur.fetchone()["id"]

    # Caches
    brand_cache = {}
    cur.execute("SELECT id, slug FROM brands")
    for row in cur.fetchall():
        brand_cache[row["slug"]] = row["id"]

    ean_product_cache = {}
    fingerprint_cache = {}

    # If not clean, pre-load caches
    if not clean_first:
        cur.execute("SELECT id, ean, match_fingerprint FROM products WHERE ean IS NOT NULL OR match_fingerprint IS NOT NULL")
        for row in cur.fetchall():
            if row["ean"]:
                ean_product_cache[row["ean"]] = row["id"]
            if row["match_fingerprint"]:
                fingerprint_cache[row["match_fingerprint"]] = row["id"]

    print("Fetching feeds from Webgains API...")
    feeds = get_feeds()
    print(f"Found {len(feeds)} unique feeds\n")

    total_new = 0
    total_offers = 0
    total_rejected = 0

    for feed_ext_id, feed_info in sorted(feeds.items()):
        new, offers, rejected = process_feed(
            cur, conn, feed_info, provider_id,
            brand_cache, ean_product_cache, fingerprint_cache,
            only_feed=only_feed
        )
        total_new += new
        total_offers += offers
        total_rejected += rejected

    # ── Summary ──
    print(f"\n{'='*60}")
    print(f"SYNC COMPLETE")
    print(f"{'='*60}")
    print(f"New products: {total_new}")
    print(f"Total offers: {total_offers}")
    print(f"Rejected: {total_rejected}")

    cur.execute("SELECT COUNT(*) FROM products")
    print(f"Products in DB: {cur.fetchone()[0]}")
    cur.execute("SELECT COUNT(*) FROM product_offers")
    print(f"Offers in DB: {cur.fetchone()[0]}")
    cur.execute("SELECT COUNT(*) FROM import_rejects")
    print(f"Rejects logged: {cur.fetchone()[0]}")

    cur.execute("""
        SELECT f.name, COUNT(po.id) as offers, COUNT(DISTINCT po.product_id) as products
        FROM feeds f LEFT JOIN product_offers po ON po.feed_id = f.id
        GROUP BY f.name ORDER BY offers DESC
    """)
    print("\nPer feed:")
    for row in cur.fetchall():
        print(f"  {row['name']}: {row['offers']} offers, {row['products']} products")

    cur.execute("SELECT brand, COUNT(*) c FROM products GROUP BY brand ORDER BY c DESC LIMIT 15")
    print("\nTop brands:")
    for row in cur.fetchall():
        print(f"  {row[0] or '(empty)'}: {row[1]}")

    cur.close()
    conn.close()


if __name__ == "__main__":
    main()
