"""Export saved NYT Cooking Recipe Box URLs and import them into the vault.

This uses a persistent Playwright browser profile so Mitch can log into NYT once.
After that, the script reads rendered Recipe Box pages, extracts recipe links, and
passes each URL through ``recipes.import_web_recipe``.
"""

from __future__ import annotations

import argparse
import json
import sys
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from time import sleep
from urllib.parse import urljoin, urlparse, urlunparse

import recipes

RECIPE_BOX_URL = "https://cooking.nytimes.com/recipe-box/all"
COOKING_BASE = "https://cooking.nytimes.com"
DEFAULT_PROFILE_DIR = Path.home() / ".nyt-cooking-playwright"


@dataclass
class ImportResult:
    url: str
    status: str
    note: str = ""


def canonical_recipe_url(href: str, base_url: str = COOKING_BASE) -> str | None:
    """Return a canonical NYT Cooking recipe URL, or None for non-recipe links."""
    if not href:
        return None
    parsed = urlparse(urljoin(base_url, href))
    if parsed.netloc not in {"cooking.nytimes.com", "www.cooking.nytimes.com"}:
        return None
    parts = [p for p in parsed.path.split("/") if p]
    if len(parts) < 2 or parts[0] != "recipes":
        return None
    return urlunparse(
        ("https", "cooking.nytimes.com", "/" + "/".join(parts[:2]), "", "", "")
    )


def dedupe_recipe_urls(hrefs: list[str], base_url: str = COOKING_BASE) -> list[str]:
    """Canonicalize and preserve first-seen order for NYT Cooking recipe links."""
    seen: set[str] = set()
    urls: list[str] = []
    for href in hrefs:
        url = canonical_recipe_url(href, base_url)
        if url and url not in seen:
            seen.add(url)
            urls.append(url)
    return urls


def _load_playwright():
    try:
        from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
        from playwright.sync_api import sync_playwright
    except ModuleNotFoundError as exc:
        raise SystemExit(
            "Playwright is not installed. Run: uv add playwright && uv run playwright install chromium"
        ) from exc
    return sync_playwright, PlaywrightTimeoutError


def _wait_for_recipe_box(page, timeout_ms: int) -> None:
    page.goto(RECIPE_BOX_URL, wait_until="domcontentloaded", timeout=timeout_ms)
    # NYT keeps firing analytics beacons, so networkidle may never settle; don't
    # let that abort the run — domcontentloaded plus the login poll is enough.
    with suppress(Exception):
        page.wait_for_load_state("networkidle", timeout=timeout_ms)


def _wait_until_logged_in(page, timeout_ms: int, poll_ms: int = 2000) -> bool:
    """Poll the Recipe Box until saved recipes render (i.e. the user is logged in).

    Returns True once at least one recipe link appears, or False on timeout. This
    replaces a blocking ``input()`` so the flow works in non-interactive runners
    (the visible browser stays open; the user just logs in and we detect it).
    """
    print(
        "A browser window has opened to your NYT Cooking Recipe Box.\n"
        "If it shows a login screen, log in there now — no need to touch the terminal.\n"
        "Waiting for your saved recipes to appear..."
    )
    waited = 0
    while waited < timeout_ms:
        with suppress(Exception):
            if page.locator('a[href*="/recipes/"]').count() > 0:
                print("Recipe Box detected — collecting your saved recipes.")
                return True
        page.wait_for_timeout(poll_ms)
        waited += poll_ms
        # Re-load periodically in case the user finished login on a different tab/URL.
        if waited % 20000 == 0:
            with suppress(Exception):
                _wait_for_recipe_box(page, timeout_ms)
    return False


def _extract_page_recipe_urls(page) -> list[str]:
    hrefs = page.locator('a[href*="/recipes/"]').evaluate_all(
        "(els) => els.map((el) => el.href || el.getAttribute('href') || '')"
    )
    return dedupe_recipe_urls(hrefs)


def collect_recipe_box_urls(
    profile_dir: Path = DEFAULT_PROFILE_DIR,
    max_pages: int = 0,
    pause_for_login: bool = True,
    timeout_ms: int = 30_000,
    login_wait_ms: int = 300_000,
) -> list[str]:
    """Collect saved recipe URLs from the logged-in NYT Cooking Recipe Box."""
    sync_playwright, playwright_timeout_error = _load_playwright()
    all_urls: list[str] = []
    seen: set[str] = set()

    with sync_playwright() as pw:
        context = pw.chromium.launch_persistent_context(
            user_data_dir=str(profile_dir),
            headless=False,
            viewport={"width": 1440, "height": 1000},
        )
        page = context.pages[0] if context.pages else context.new_page()
        try:
            _wait_for_recipe_box(page, timeout_ms)
            if pause_for_login:
                if not _wait_until_logged_in(page, login_wait_ms):
                    print(
                        "Timed out waiting for the Recipe Box to load (no recipes "
                        "detected). If you weren't logged in, re-run and log in when "
                        "the window opens."
                    )
                    return []
                _wait_for_recipe_box(page, timeout_ms)

            page_num = 1
            stagnant_pages = 0
            while True:
                target = (
                    RECIPE_BOX_URL
                    if page_num == 1
                    else f"{RECIPE_BOX_URL}?page={page_num}"
                )
                page.goto(target, wait_until="domcontentloaded", timeout=timeout_ms)
                with suppress(playwright_timeout_error):
                    page.wait_for_load_state("networkidle", timeout=timeout_ms)
                page.wait_for_timeout(1000)

                page_urls = _extract_page_recipe_urls(page)
                new_count = 0
                for url in page_urls:
                    if url not in seen:
                        seen.add(url)
                        all_urls.append(url)
                        new_count += 1

                print(f"Page {page_num}: found {len(page_urls)} links, {new_count} new")

                if max_pages and page_num >= max_pages:
                    break
                if new_count == 0:
                    stagnant_pages += 1
                else:
                    stagnant_pages = 0
                if stagnant_pages >= 2:
                    break

                page_num += 1
                sleep(0.25)
        finally:
            context.close()

    return all_urls


def import_urls(urls: list[str], dry_run: bool = False) -> list[ImportResult]:
    results: list[ImportResult] = []
    for url in urls:
        if dry_run:
            results.append(ImportResult(url=url, status="dry-run"))
            continue
        try:
            note = recipes.import_web_recipe(url)
        except Exception as exc:
            results.append(ImportResult(url=url, status="failed", note=str(exc)))
            continue
        if note:
            results.append(ImportResult(url=url, status="saved-or-existing", note=note))
        else:
            results.append(
                ImportResult(url=url, status="failed", note="Could not extract recipe")
            )
    return results


def write_report(path: Path, urls: list[str], results: list[ImportResult]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = {
        "urls": urls,
        "results": [r.__dict__ for r in results],
    }
    path.write_text(json.dumps(payload, indent=2) + "\n")


def read_urls_file(path: Path) -> list[str]:
    """Load + canonicalize recipe URLs from a newline-delimited file.

    This is the bot-detection-proof collection path: NYT's PerimeterX blocks the
    automated browser, but the user's real browser is never blocked, so they
    collect their Recipe Box URLs there (see the console snippet in the runbook)
    and we import each through the working ``recipes.import_web_recipe`` fetch.
    """
    raw = [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
    return dedupe_recipe_urls(raw)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--urls-file",
        type=Path,
        default=None,
        help="Import from a newline-delimited URL file instead of scraping the "
        "Recipe Box (avoids NYT bot detection).",
    )
    parser.add_argument("--profile-dir", type=Path, default=DEFAULT_PROFILE_DIR)
    parser.add_argument(
        "--max-pages", type=int, default=0, help="0 means continue until exhausted"
    )
    parser.add_argument("--no-login-pause", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--collect-only", action="store_true")
    parser.add_argument(
        "--report",
        type=Path,
        default=Path("nyt-recipe-box-import-report.json"),
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    if args.urls_file:
        urls = read_urls_file(args.urls_file)
        print(f"Loaded {len(urls)} unique recipe URLs from {args.urls_file}")
    else:
        urls = collect_recipe_box_urls(
            profile_dir=args.profile_dir,
            max_pages=args.max_pages,
            pause_for_login=not args.no_login_pause,
        )
        print(f"Collected {len(urls)} unique recipe URLs")

    results: list[ImportResult] = []
    if not args.collect_only:
        results = import_urls(urls, dry_run=args.dry_run)
        saved = sum(1 for r in results if r.status == "saved-or-existing")
        failed = sum(1 for r in results if r.status == "failed")
        print(f"Import complete: {saved} saved/existing, {failed} failed")

    write_report(args.report, urls, results)
    print(f"Report: {args.report}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
