#!/usr/bin/env python3
"""
SneakerPicks Product Deduplication Script

Finds and merges duplicate products across feeds using:
  Pass 1: EAN/GTIN exact match (highest confidence)
  Pass 2: match_fingerprint exact match
  Pass 3: Fuzzy brand+model+colorway match (report-only unless --fuzzy)

Usage:
  python scripts/dedup-products.py                  # dry-run report
  python scripts/dedup-products.py --execute        # execute pass 1+2
  python scripts/dedup-products.py --execute --fuzzy # also execute pass 3
  python scripts/dedup-products.py --report         # detailed report with examples
"""

import argparse
import os
import sys
from collections import defaultdict
from datetime import datetime

import psycopg2
import psycopg2.extras
from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env.local'))
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))

DATABASE_URL = os.environ.get('DATABASE_URL')
if not DATABASE_URL:
    print("ERROR: DATABASE_URL not set")
    sys.exit(1)


def get_conn():
    return psycopg2.connect(DATABASE_URL)


def levenshtein(s1, s2):
    if len(s1) < len(s2):
        return levenshtein(s2, s1)
    if len(s2) == 0:
        return len(s1)
    prev = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        curr = [i + 1]
        for j, c2 in enumerate(s2):
            curr.append(min(prev[j + 1] + 1, curr[j] + 1, prev[j] + (c1 != c2)))
        prev = curr
    return prev[-1]


def pick_master(products):
    """Pick master: most offers, then oldest (lowest id), then most complete."""
    return max(products, key=lambda p: (p['offer_count'], -p['id'], len(p['name'] or '')))


def find_ean_duplicates(cur):
    """Pass 1: Group products by EAN/GTIN."""
    cur.execute("""
        SELECT p.id, p.name, p.brand, p.brand_normalized, p.model_normalized,
               p.colorway, p.ean, p.gtin, p.match_fingerprint,
               COUNT(po.id) as offer_count
        FROM products p
        LEFT JOIN product_offers po ON po.product_id = p.id
        WHERE p.is_sneaker = true
          AND (p.ean IS NOT NULL AND p.ean != '' OR p.gtin IS NOT NULL AND p.gtin != '')
        GROUP BY p.id
        ORDER BY p.id
    """)
    products = [dict(r) for r in cur.fetchall()]

    groups = defaultdict(list)
    for p in products:
        key = p['ean'] or p['gtin']
        if key and key.strip():
            groups[key.strip()].append(p)

    return {k: v for k, v in groups.items() if len(v) > 1}


def find_fingerprint_duplicates(cur, exclude_ids=None):
    """Pass 2: Group by match_fingerprint."""
    exclude_ids = exclude_ids or set()
    cur.execute("""
        SELECT p.id, p.name, p.brand, p.brand_normalized, p.model_normalized,
               p.colorway, p.colorway_normalized, p.ean, p.gtin, p.match_fingerprint,
               COUNT(po.id) as offer_count
        FROM products p
        LEFT JOIN product_offers po ON po.product_id = p.id
        WHERE p.is_sneaker = true
          AND p.match_fingerprint IS NOT NULL
          AND p.match_fingerprint != ''
        GROUP BY p.id
        ORDER BY p.id
    """)
    products = [dict(r) for r in cur.fetchall()]

    groups = defaultdict(list)
    for p in products:
        if p['id'] in exclude_ids:
            continue
        groups[p['match_fingerprint']].append(p)

    return {k: v for k, v in groups.items() if len(v) > 1}


def find_fuzzy_duplicates(cur, exclude_ids=None):
    """Pass 3: Fuzzy match on brand_normalized + model_normalized + colorway."""
    exclude_ids = exclude_ids or set()
    cur.execute("""
        SELECT p.id, p.name, p.brand, p.brand_normalized, p.model_normalized,
               p.colorway, p.colorway_normalized, p.ean, p.gtin, p.match_fingerprint,
               COUNT(po.id) as offer_count
        FROM products p
        LEFT JOIN product_offers po ON po.product_id = p.id
        WHERE p.is_sneaker = true
          AND p.brand_normalized IS NOT NULL
          AND p.model_normalized IS NOT NULL
          AND p.model_normalized != ''
        GROUP BY p.id
        ORDER BY p.brand_normalized, p.model_normalized
    """)
    products = [dict(r) for r in cur.fetchall() if dict(r)['id'] not in exclude_ids]

    # Group by brand first to reduce comparisons
    by_brand = defaultdict(list)
    for p in products:
        by_brand[p['brand_normalized']].append(p)

    merge_groups = []
    for brand, prods in by_brand.items():
        # Sort by model for efficient comparison
        prods.sort(key=lambda x: x['model_normalized'] or '')
        matched = set()
        for i, p1 in enumerate(prods):
            if p1['id'] in matched:
                continue
            group = [p1]
            m1 = p1['model_normalized'] or ''
            c1 = (p1['colorway_normalized'] or '').strip()
            for j in range(i + 1, len(prods)):
                p2 = prods[j]
                if p2['id'] in matched:
                    continue
                m2 = p2['model_normalized'] or ''
                c2 = (p2['colorway_normalized'] or '').strip()

                # Colorway must match (both empty or same)
                if c1 != c2:
                    continue

                # Check fuzzy model match
                if m1 == m2:
                    continue  # exact match handled by fingerprint pass
                dist = levenshtein(m1, m2)
                is_substring = m1 in m2 or m2 in m1
                if dist < 3 or is_substring:
                    group.append(p2)
                    matched.add(p2['id'])

            if len(group) > 1:
                matched.add(p1['id'])
                merge_groups.append(group)

    return merge_groups


def merge_group(cur, group, dry_run=True):
    """Merge a group of duplicate products. Returns (master_id, duplicate_ids)."""
    master = pick_master(group)
    duplicates = [p for p in group if p['id'] != master['id']]
    dup_ids = [p['id'] for p in duplicates]

    if not dup_ids:
        return None

    if not dry_run:
        # Move offers
        cur.execute(
            "UPDATE product_offers SET product_id = %s WHERE product_id = ANY(%s)",
            (master['id'], dup_ids)
        )

        # Move price_history if table exists
        try:
            cur.execute(
                "UPDATE price_history SET product_id = %s WHERE product_id = ANY(%s)",
                (master['id'], dup_ids)
            )
        except psycopg2.errors.UndefinedTable:
            cur.connection.rollback()
            # Re-do the offers update since we rolled back
            cur.execute(
                "UPDATE product_offers SET product_id = %s WHERE product_id = ANY(%s)",
                (master['id'], dup_ids)
            )

        # Deactivate duplicates
        cur.execute(
            "UPDATE products SET is_sneaker = false WHERE id = ANY(%s)",
            (dup_ids,)
        )

    return master, duplicates


def print_examples(groups, limit=20):
    """Print example duplicate groups."""
    shown = 0
    items = list(groups.values()) if isinstance(groups, dict) else groups
    for group in items[:limit]:
        if isinstance(group, list):
            prods = group
        else:
            prods = group
        master = pick_master(prods)
        print(f"\n  Master: [{master['id']}] {master['name']} ({master['offer_count']} offers)")
        for p in prods:
            if p['id'] != master['id']:
                print(f"    Dup:  [{p['id']}] {p['name']} ({p['offer_count']} offers)")
        shown += 1
    if len(items) > limit:
        print(f"\n  ... and {len(items) - limit} more groups")


def main():
    parser = argparse.ArgumentParser(description='SneakerPicks product deduplication')
    parser.add_argument('--execute', action='store_true', help='Execute merges (pass 1+2)')
    parser.add_argument('--fuzzy', action='store_true', help='Also execute pass 3 (fuzzy)')
    parser.add_argument('--report', action='store_true', help='Detailed report with examples')
    args = parser.parse_args()

    dry_run = not args.execute
    conn = get_conn()
    conn.autocommit = False
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)

    print(f"SneakerPicks Dedup — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Mode: {'EXECUTE' if args.execute else 'DRY-RUN'}\n")

    # Stats
    cur.execute("SELECT COUNT(*) as cnt FROM products WHERE is_sneaker = true")
    total = cur.fetchone()['cnt']
    print(f"Total active products: {total:,}")

    merged_ids = set()  # track already-merged duplicates
    total_merges = 0

    # === Pass 1: EAN/GTIN ===
    print("\n" + "=" * 60)
    print("PASS 1: EAN/GTIN exact match")
    print("=" * 60)

    ean_groups = find_ean_duplicates(cur)
    dup_count = sum(len(v) - 1 for v in ean_groups.values())
    print(f"Found {len(ean_groups)} duplicate groups ({dup_count} duplicates)")

    if args.report:
        print_examples(ean_groups)

    if args.execute:
        for key, group in ean_groups.items():
            result = merge_group(cur, group, dry_run=False)
            if result:
                master, dups = result
                for d in dups:
                    merged_ids.add(d['id'])
                total_merges += len(dups)
                if args.report:
                    print(f"  Merged [{master['id']}] <- {[d['id'] for d in dups]} (EAN: {key})")
        print(f"Merged: {total_merges} products")
    else:
        print("(dry-run, no changes)")

    # === Pass 2: Fingerprint ===
    print("\n" + "=" * 60)
    print("PASS 2: match_fingerprint exact match")
    print("=" * 60)

    fp_groups = find_fingerprint_duplicates(cur, exclude_ids=merged_ids)
    dup_count = sum(len(v) - 1 for v in fp_groups.values())
    print(f"Found {len(fp_groups)} duplicate groups ({dup_count} duplicates)")

    if args.report:
        print_examples(fp_groups)

    pass2_merges = 0
    if args.execute:
        for key, group in fp_groups.items():
            result = merge_group(cur, group, dry_run=False)
            if result:
                master, dups = result
                for d in dups:
                    merged_ids.add(d['id'])
                pass2_merges += len(dups)
                if args.report:
                    print(f"  Merged [{master['id']}] <- {[d['id'] for d in dups]}")
        total_merges += pass2_merges
        print(f"Merged: {pass2_merges} products")
    else:
        print("(dry-run, no changes)")

    # === Pass 3: Fuzzy ===
    print("\n" + "=" * 60)
    print("PASS 3: Fuzzy name match")
    print("=" * 60)

    fuzzy_groups = find_fuzzy_duplicates(cur, exclude_ids=merged_ids)
    dup_count = sum(len(g) - 1 for g in fuzzy_groups)
    print(f"Found {len(fuzzy_groups)} potential duplicate groups ({dup_count} duplicates)")

    if args.report:
        print_examples(fuzzy_groups)

    if args.execute and args.fuzzy:
        pass3_merges = 0
        for group in fuzzy_groups:
            result = merge_group(cur, group, dry_run=False)
            if result:
                master, dups = result
                pass3_merges += len(dups)
                if args.report:
                    print(f"  Merged [{master['id']}] <- {[d['id'] for d in dups]}")
        total_merges += pass3_merges
        print(f"Merged: {pass3_merges} products")
    else:
        print("(report-only — use --execute --fuzzy to merge)")

    # === Summary ===
    print("\n" + "=" * 60)
    print("SUMMARY")
    print("=" * 60)
    if args.execute:
        print(f"Total merged: {total_merges} duplicate products")
        conn.commit()
        print("Transaction committed.")
    else:
        print("Dry-run complete. Use --execute to apply changes.")
        conn.rollback()

    cur.close()
    conn.close()


if __name__ == '__main__':
    main()
