#!/usr/bin/env python3
"""
Notion Inspiration DB Cleanup Script
- Deduplicates items by normalized URL
- Adds missing covers by scraping OpenGraph images
- Reports stats and changes

Usage:
  python notion_inspo_cleanup.py --dedupe --dry-run     # Preview deduplication
  python notion_inspo_cleanup.py --dedupe               # Remove duplicates
  python notion_inspo_cleanup.py --fix-covers --limit 50  # Fix covers (50 at a time)
  python notion_inspo_cleanup.py --stats                # Show statistics
"""

import argparse
import json
import sys
import time
from collections import defaultdict
from datetime import datetime
from urllib.parse import urlparse

import re
import requests

# Configuration
NOTION_API_KEY_PATH = "/home/clawd/secrets/notion/api_key"
NOTION_DB_ID = "2ff330c2-8646-81f0-bbd9-ec474393d7a5"
SCRAPE_DELAY = 0.5
NOTION_DELAY = 0.3
REQUEST_TIMEOUT = 8

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
    "Accept": "text/html,application/xhtml+xml",
}


def load_notion_key():
    with open(NOTION_API_KEY_PATH) as f:
        return f.read().strip()


def get_notion_headers(api_key):
    return {
        "Authorization": f"Bearer {api_key}",
        "Notion-Version": "2022-06-28",
        "Content-Type": "application/json",
    }


def normalize_url(url):
    """Normalize URL for deduplication."""
    if not url:
        return ""
    parsed = urlparse(url)
    domain = parsed.netloc.lower().replace("www.", "")
    path = parsed.path.rstrip("/")
    # Ignore query params for deduplication
    return f"{domain}{path}"


def fetch_all_pages(api_key):
    """Fetch all pages from the database."""
    headers = get_notion_headers(api_key)
    url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
    
    all_pages = []
    has_more = True
    cursor = None
    
    print("Fetching all pages from Notion...")
    while has_more:
        payload = {"page_size": 100}
        if cursor:
            payload["start_cursor"] = cursor
        
        resp = requests.post(url, headers=headers, json=payload, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        
        all_pages.extend(data.get("results", []))
        has_more = data.get("has_more", False)
        cursor = data.get("next_cursor")
        print(f"  Fetched {len(all_pages)} pages...")
        time.sleep(NOTION_DELAY)
    
    print(f"Total: {len(all_pages)} pages")
    return all_pages


def extract_page_info(page):
    """Extract useful info from a Notion page."""
    props = page.get("properties", {})
    
    # Title
    title_prop = props.get("Name", {}).get("title", [])
    title = title_prop[0]["text"]["content"] if title_prop else ""
    
    # URL
    url = props.get("Link", {}).get("url", "")
    
    # Source
    source = props.get("Source Board", {}).get("select", {})
    source_name = source.get("name", "") if source else ""
    
    # Tags
    tags_prop = props.get("Tags", {}).get("multi_select", [])
    tags = [t["name"] for t in tags_prop]
    
    # Cover
    has_cover = page.get("cover") is not None
    
    # Created time
    created = page.get("created_time", "")
    
    return {
        "id": page["id"],
        "title": title,
        "url": url,
        "normalized_url": normalize_url(url),
        "source": source_name,
        "tags": tags,
        "has_cover": has_cover,
        "created": created,
    }


def scrape_og_image(url):
    """Scrape OpenGraph image from URL using regex."""
    if not url or not url.startswith("http"):
        return None
    
    try:
        resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT, allow_redirects=True)
        resp.raise_for_status()
        
        html = resp.text
        
        # Try og:image first
        patterns = [
            r'<meta[^>]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']',
            r'<meta[^>]+content=["\']([^"\']+)["\'][^>]+property=["\']og:image["\']',
            r'<meta[^>]+name=["\']twitter:image["\'][^>]+content=["\']([^"\']+)["\']',
            r'<meta[^>]+content=["\']([^"\']+)["\'][^>]+name=["\']twitter:image["\']',
        ]
        
        for pattern in patterns:
            match = re.search(pattern, html, re.IGNORECASE)
            if match:
                img_url = match.group(1)
                # Make relative URLs absolute
                if img_url.startswith("/"):
                    parsed = urlparse(url)
                    img_url = f"{parsed.scheme}://{parsed.netloc}{img_url}"
                return img_url
        
        return None
    except Exception:
        return None


def update_page_cover(api_key, page_id, cover_url):
    """Update a page's cover image."""
    headers = get_notion_headers(api_key)
    url = f"https://api.notion.com/v1/pages/{page_id}"
    
    payload = {
        "cover": {
            "type": "external",
            "external": {"url": cover_url}
        }
    }
    
    try:
        resp = requests.patch(url, headers=headers, json=payload, timeout=30)
        return resp.status_code == 200
    except Exception:
        return False


def delete_page(api_key, page_id):
    """Archive/delete a Notion page."""
    headers = get_notion_headers(api_key)
    url = f"https://api.notion.com/v1/pages/{page_id}"
    
    payload = {"archived": True}
    
    try:
        resp = requests.patch(url, headers=headers, json=payload, timeout=30)
        return resp.status_code == 200
    except Exception:
        return False


def cmd_stats(api_key):
    """Show database statistics."""
    pages = fetch_all_pages(api_key)
    infos = [extract_page_info(p) for p in pages]
    
    # Cover stats
    with_cover = sum(1 for i in infos if i["has_cover"])
    no_cover = sum(1 for i in infos if not i["has_cover"])
    
    print(f"\n=== COVER STATS ===")
    print(f"With cover:    {with_cover} ({100*with_cover/len(infos):.1f}%)")
    print(f"Without cover: {no_cover} ({100*no_cover/len(infos):.1f}%)")
    
    # Source stats
    sources = defaultdict(int)
    for i in infos:
        sources[i["source"] or "No source"] += 1
    
    print(f"\n=== SOURCES ===")
    for src, count in sorted(sources.items(), key=lambda x: -x[1])[:15]:
        print(f"  {src}: {count}")
    
    # Duplicate stats
    by_url = defaultdict(list)
    for i in infos:
        if i["normalized_url"]:
            by_url[i["normalized_url"]].append(i)
    
    dupes = {k: v for k, v in by_url.items() if len(v) > 1}
    dupe_count = sum(len(v) - 1 for v in dupes.values())
    
    print(f"\n=== DUPLICATES ===")
    print(f"Unique URLs: {len(by_url)}")
    print(f"Duplicate groups: {len(dupes)}")
    print(f"Duplicate pages (removable): {dupe_count}")
    
    print(f"\nTop duplicate groups:")
    for norm_url, items in sorted(dupes.items(), key=lambda x: -len(x[1]))[:10]:
        print(f"  [{len(items)}x] {items[0]['title'][:40]}")


def cmd_dedupe(api_key, dry_run=True):
    """Remove duplicate pages."""
    pages = fetch_all_pages(api_key)
    infos = [extract_page_info(p) for p in pages]
    
    # Group by normalized URL
    by_url = defaultdict(list)
    for i in infos:
        if i["normalized_url"]:
            by_url[i["normalized_url"]].append(i)
    
    dupes = {k: v for k, v in by_url.items() if len(v) > 1}
    
    if not dupes:
        print("No duplicates found!")
        return
    
    print(f"\nFound {len(dupes)} duplicate groups")
    
    to_delete = []
    
    for norm_url, items in dupes.items():
        # Sort: prefer items with cover, then most tags, then oldest
        def score(item):
            return (
                1 if item["has_cover"] else 0,
                len(item["tags"]),
                -len(item["created"]),  # earlier dates sort higher (ISO format)
            )
        
        items_sorted = sorted(items, key=score, reverse=True)
        keep = items_sorted[0]
        delete = items_sorted[1:]
        
        print(f"\n[{norm_url[:50]}]")
        print(f"  KEEP: {keep['title'][:40]} (cover: {keep['has_cover']}, tags: {len(keep['tags'])})")
        for d in delete:
            print(f"  DEL:  {d['title'][:40]} (cover: {d['has_cover']}, tags: {len(d['tags'])})")
            to_delete.append(d)
    
    print(f"\n{'[DRY RUN] ' if dry_run else ''}Will delete {len(to_delete)} duplicate pages")
    
    if dry_run:
        print("\nRun with --no-dry-run to actually delete")
        return
    
    # Actually delete
    deleted = 0
    for item in to_delete:
        print(f"  Deleting: {item['title'][:40]}...", end=" ")
        if delete_page(api_key, item["id"]):
            print("✓")
            deleted += 1
        else:
            print("✗")
        time.sleep(NOTION_DELAY)
    
    print(f"\nDeleted {deleted}/{len(to_delete)} duplicates")


def cmd_fix_covers(api_key, limit=50, dry_run=True):
    """Add covers to pages that are missing them."""
    pages = fetch_all_pages(api_key)
    infos = [extract_page_info(p) for p in pages]
    
    # Filter to pages with URLs but no covers
    needs_cover = [i for i in infos if i["url"] and not i["has_cover"]]
    
    print(f"\n{len(needs_cover)} pages need covers")
    print(f"Processing up to {limit} pages...")
    
    fixed = 0
    failed = 0
    
    for item in needs_cover[:limit]:
        print(f"\n[{fixed+failed+1}/{min(limit, len(needs_cover))}] {item['title'][:40]}")
        print(f"  URL: {item['url'][:60]}")
        
        # Scrape OG image
        og_image = scrape_og_image(item["url"])
        time.sleep(SCRAPE_DELAY)
        
        if not og_image:
            print(f"  ✗ No OG image found")
            failed += 1
            continue
        
        print(f"  Found: {og_image[:60]}")
        
        if dry_run:
            print(f"  [DRY RUN] Would update cover")
            fixed += 1
            continue
        
        # Update page
        if update_page_cover(api_key, item["id"], og_image):
            print(f"  ✓ Cover updated")
            fixed += 1
        else:
            print(f"  ✗ Failed to update")
            failed += 1
        
        time.sleep(NOTION_DELAY)
    
    print(f"\n{'[DRY RUN] ' if dry_run else ''}Results:")
    print(f"  Fixed: {fixed}")
    print(f"  Failed: {failed}")
    print(f"  Remaining: {len(needs_cover) - limit if limit < len(needs_cover) else 0}")


def main():
    parser = argparse.ArgumentParser(description="Notion Inspiration DB Cleanup")
    parser.add_argument("--stats", action="store_true", help="Show statistics")
    parser.add_argument("--dedupe", action="store_true", help="Remove duplicates")
    parser.add_argument("--fix-covers", action="store_true", help="Add missing covers")
    parser.add_argument("--limit", type=int, default=50, help="Limit for fix-covers")
    parser.add_argument("--dry-run", action="store_true", default=True, help="Preview changes (default)")
    parser.add_argument("--no-dry-run", action="store_true", help="Actually make changes")
    
    args = parser.parse_args()
    
    dry_run = not args.no_dry_run
    
    if not any([args.stats, args.dedupe, args.fix_covers]):
        parser.print_help()
        return
    
    api_key = load_notion_key()
    
    if args.stats:
        cmd_stats(api_key)
    
    if args.dedupe:
        cmd_dedupe(api_key, dry_run=dry_run)
    
    if args.fix_covers:
        cmd_fix_covers(api_key, limit=args.limit, dry_run=dry_run)


if __name__ == "__main__":
    main()
