"""
scrylo/cli/main.py
------------------
Scrylo CLI — all commands live here.
"""

from __future__ import annotations

import os
import threading
import time
import webbrowser

import click
from rich.console import Console
from rich.live import Live
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
from rich import box
from rich.text import Text

from scrylo.core.config import settings, CONFIG_PATH, DB_PATH, SCRYLO_DIR
from scrylo.core.licence import check_licence

console = Console()


# ── Startup package check ─────────────────────────────────────
_REQUIRED_PACKAGES = [
    ("fastapi",           "pip install fastapi"),
    ("uvicorn",           "pip install uvicorn[standard]"),
    ("sqlalchemy",        "pip install sqlalchemy"),
    ("click",             "pip install click"),
    ("rich",              "pip install rich"),
    ("httpx",             "pip install httpx"),
    ("bs4",               "pip install beautifulsoup4"),
    ("jinja2",            "pip install jinja2"),
    ("yaml",              "pip install pyyaml"),
    ("pydantic",          "pip install pydantic"),
    ("aiofiles",          "pip install aiofiles"),
    ("multipart",         "pip install python-multipart"),
    ("dns",               "pip install dnspython"),
    ("patchright",        "pip install patchright  (then: python -m patchright install chromium)"),
]

def check_required_packages() -> list[tuple[str, str]]:
    """Return list of (package, install_cmd) for any missing required packages."""
    missing = []
    for pkg, cmd in _REQUIRED_PACKAGES:
        try:
            __import__(pkg)
        except ImportError:
            missing.append((pkg, cmd))
    return missing


def startup_package_check():
    """
    Run before any command that starts the server or scrapes.
    Fails loudly with a clear install message rather than a cryptic traceback.
    """
    missing = check_required_packages()
    if not missing:
        return  # all good

    console.print()
    lines = "\n".join(
        f"  [dim]·[/dim] [yellow]{pkg}[/yellow]  [dim]→[/dim]  [cyan]{cmd}[/cyan]"
        for pkg, cmd in missing
    )
    console.print(Panel(
        "[bold red]Missing required packages[/bold red]\n\n"
        f"Scrylo can't start because these packages are not installed:\n\n{lines}\n\n"
        "[dim]Run the commands above in your terminal, then try again.\n"
        "Or reinstall everything: [cyan]pip install -r requirements.txt[/cyan][/dim]",
        border_style="red",
        title="[dim]startup check[/dim]",
    ))
    raise SystemExit(1)

LOGO = """[bold yellow]
 ███████╗ ██████╗██████╗ ██╗   ██╗██╗      ██████╗
 ██╔════╝██╔════╝██╔══██╗╚██╗ ██╔╝██║     ██╔═══██╗
 ███████╗██║     ██████╔╝ ╚████╔╝ ██║     ██║   ██║
 ╚════██║██║     ██╔══██╗  ╚██╔╝  ██║     ██║   ██║
 ███████║╚██████╗██║  ██║   ██║   ███████╗╚██████╔╝
 ╚══════╝ ╚═════╝╚═╝  ╚═╝   ╚═╝   ╚══════╝ ╚═════╝
[/bold yellow][dim]  Local AI lead gen · no cloud · no GPU · $99 once[/dim]
"""


def _install_patchright_cli(console: Console, need_package: bool):
    """Interactively install patchright + Chromium from the CLI."""
    import subprocess

    if need_package:
        console.print(Panel(
            "[bold]Patchright + Chromium required[/bold]\n\n"
            "Scrylo uses a real browser to scrape leads from JavaScript-heavy sites.\n"
            "[dim]~170 MB download · one-time · runs silently, no window opens[/dim]",
            border_style="yellow", title="[dim]missing dependency[/dim]",
        ))
    else:
        console.print(Panel(
            "[bold]Chromium browser required[/bold]\n\n"
            "Patchright is installed but the browser hasn't been downloaded yet.\n"
            "[dim]~170 MB download · one-time · runs silently, no window opens[/dim]",
            border_style="yellow", title="[dim]missing dependency[/dim]",
        ))

    if not click.confirm("Install now?", default=True):
        console.print("[dim]Skipped. Run `scrylo find` again when ready.[/dim]")
        return

    import sys
    steps = []
    if need_package:
        steps.append(("Installing patchright...",        [sys.executable, "-m", "pip", "install", "patchright"]))
    steps.append(("Downloading Chromium (~170 MB)...", [sys.executable, "-m", "patchright", "install", "chromium"]))

    for label, cmd in steps:
        console.print(f"[dim]▸ {label}[/dim]")
        result = subprocess.run(cmd, capture_output=False)
        if result.returncode != 0:
            console.print(f"[red]✗ Failed. Try running manually:[/red] {' '.join(cmd)}")
            return

    console.print("[green]✓ Done — re-run your find command.[/green]")


@click.group()
@click.version_option(settings["app"]["version"], prog_name="scrylo")
def cli():
    """Scrylo — Local AI lead intelligence. Your laptop. Your data."""
    pass


# ── demo ───────────────────────────────────────────────────────
@cli.command()
def demo():
    """Run a simulated lead-to-pitch workflow (no licence required)."""
    console.print(LOGO)
    console.print(Panel(
        "[bold cyan]Simulation Mode[/bold cyan]\n\n"
        "Experience Scrylo's lead finding and AI pitching pipeline.\n"
        "[dim]This uses local fake data and does not hit any real APIs.[/dim]",
        border_style="cyan",
    ))

    # 1. Scout
    with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
        task = progress.add_task("Scouting 'SaaS founders NYC' (simulation)...", total=None)
        time.sleep(1.2)
        progress.update(task, description="[dim]▸ Querying Serper.dev...[/dim]")
        time.sleep(0.8)
        progress.update(task, description="[dim]▸ Fetching 12 domain(s)...[/dim]")
        time.sleep(1.0)
        progress.update(task, description="Done.")

    results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
    results_table.add_column("Name", min_width=20)
    results_table.add_column("Company", min_width=18)
    results_table.add_column("Email", min_width=24)
    results_table.add_column("Source", min_width=10)
    
    leads = [
        ("Sarah Jenkins", "DevFlow AI", "s.jenkins@devflow.io", "web"),
        ("Marcus Thorne", "CloudScale", "m.thorne@cloudscale.com", "github"),
        ("Elena Rossi", "FinPulse", "elena@finpulse.it", "web"),
    ]
    for n, c, e, s in leads:
        results_table.add_row(n, c, e, s)
    console.print(results_table)
    console.print(f"\n[green]✓ {len(leads)} leads found[/green]\n")

    # 2. ICP
    if not click.confirm("Score these leads against sample ICP?", default=True):
        return
    
    with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
        task = progress.add_task("Calculating ICP scores...", total=100)
        for i in range(0, 101, 20):
            time.sleep(0.3)
            progress.update(task, completed=i)
        progress.update(task, description="Done.")

    results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
    results_table.add_column("Name", min_width=20)
    results_table.add_column("Score", min_width=10, justify="right")
    results_table.add_row("Sarah Jenkins", "[green]94[/green]")
    results_table.add_row("Marcus Thorne", "[green]82[/green]")
    results_table.add_row("Elena Rossi", "64")
    console.print(results_table)
    console.print("\n[green]✓ Leads qualified[/green]\n")

    # 3. Pitch
    if not click.confirm("Generate sample AI pitch for Sarah Jenkins?", default=True):
        return

    console.print("\n[dim]▸ Connecting to Groq (simulated)...[/dim]")
    console.print("[dim]▸ Model: llama-3.3-70b-versatile[/dim]")
    time.sleep(0.8)
    
    subject = "Subject: Scaling DevFlow's onboarding speed"
    body = (
        "Hi Sarah,\n\n"
        "I noticed DevFlow just crossed 20 employees — that's usually when onboarding "
        "new reps starts to become a bottleneck. We built something that cuts rep "
        "ramp time from 3 weeks to 3 days.\n\n"
        "Worth a quick chat?\n\n"
        "— Alex"
    )
    
    console.print(Panel(f"[bold]{subject}[/bold]\n\n{body}", title="Generated Pitch", border_style="green"))
    
    console.print(
        "\n[bold yellow]Like what you see?[/bold yellow]\n"
        "Unlock full access to real leads and AI at [underline]scrylo.site[/underline]\n"
    )


# ── serve ──────────────────────────────────────────────────────
@cli.command()
@click.option("--host", default=settings["app"]["host"], show_default=True)
@click.option("--port", default=settings["app"]["port"], show_default=True, type=int)
@click.option("--no-browser", is_flag=True, help="Don't auto-open browser tab")
@click.option("--reload", is_flag=True, help="Dev mode: auto-reload on file changes")
def serve(host: str, port: int, no_browser: bool, reload: bool):
    """Start the Scrylo web UI at localhost."""
    import uvicorn

    check_licence()

    console.print(LOGO)
    url = f"http://{host}:{port}"
    console.print(f"[bold yellow]▶ Scrylo running at[/bold yellow] [underline]{url}[/underline]\n")
    console.print(f"  [dim]Press Ctrl+C to stop[/dim]\n")
    if not no_browser:
        def _open():
            time.sleep(1.2)
            webbrowser.open(url)
        threading.Thread(target=_open, daemon=True).start()
    uvicorn.run(
        "scrylo.api.app:app", host=host, port=port,
        reload=reload, log_level=settings["app"]["log_level"],
    )


# ── status ─────────────────────────────────────────────────────
@cli.command()
def status():
    """Show system status — config, database, and AI models."""
    from scrylo.core.llm_adapter import _ollama_available, _list_ollama_models
    console.print(LOGO)
    table = Table(box=box.SIMPLE, show_header=False, padding=(0, 1))
    table.add_column("Key", style="dim", min_width=26)
    table.add_column("Value")
    table.add_row("[dim]// paths[/dim]", "")
    table.add_row("  Config", str(CONFIG_PATH))
    table.add_row("  Data dir", str(SCRYLO_DIR))
    table.add_row("  Database", str(DB_PATH))
    table.add_row("  DB exists",
        "[green]✓ yes[/green]" if DB_PATH.exists() else "[dim]not created yet[/dim]")
    table.add_row("", "")
    
    table.add_row("[dim]// local ai (ollama)[/dim]", "")
    if _ollama_available():
        models = _list_ollama_models()
        if models:
            table.add_row("  Ollama", f"[green]✓ connected[/green] ({len(models)} models)")
            table.add_row("  Models", ", ".join(models[:3]) + ("..." if len(models) > 3 else ""))
        else:
            table.add_row("  Ollama", "[yellow]⚠ connected (no models pulled)[/yellow]")
    else:
        table.add_row("  Ollama", "[dim]not running (localhost:11434)[/dim]")
    table.add_row("", "")

    table.add_row("[dim]// server[/dim]", "")
    table.add_row("  Host", f"{settings['app']['host']}:{settings['app']['port']}")
    table.add_row("  Version", settings['app']['version'])
    table.add_row("", "")
    
    table.add_row("[dim]// api keys[/dim]", "")
    serper_key = settings.get("scraper", {}).get("serper_api_key", "")
    groq_key   = settings.get("llm", {}).get("groq_api_key", "")
    table.add_row("  Serper.dev",
        "[green]✓ configured[/green]" if serper_key
        else "[dim]not set (serper.dev → free 2,500 searches)[/dim]")
    table.add_row("  Groq (LLM)",
        "[green]✓ configured[/green]" if groq_key
        else "[yellow]not set → using local fallback[/yellow]")
    table.add_row("", "")
    
    table.add_row("[dim]// lead engine[/dim]", "")
    try:
        from scrylo.scrapers.browser import patchright_available
        pw = patchright_available()
    except Exception:
        pw = False
    table.add_row("  Page fetcher",
        "[green]✓ Patchright (undetected Chromium)[/green]" if pw
        else "[dim]httpx only — install patchright for better coverage[/dim]")
    console.print(table)
    
    console.print(
        "\n[dim]✨ New user? Run [/dim][cyan]scrylo demo[/cyan][dim] to see a simulated workflow.[/dim]"
    )



# ── find ───────────────────────────────────────────────────────
@cli.command()
@click.argument("query")
@click.option("--limit", default=20, show_default=True, type=int,
              help="Max leads to find")
@click.option("--source", "-s", multiple=True, default=["web"],
              type=click.Choice(["web", "github", "all"]),
              help="Lead source(s) to search (web, github, all)")
@click.option("--no-fetch", is_flag=True, hidden=True,
              help="Skip page fetching for web results — faster but less data")
def find(query: str, limit: int, source: list[str], no_fetch: bool):
    """Find leads from various sources (Web, GitHub).
    
    Search for leads matching QUERY and save them to the local SQLite database.
    Supports multiple sources and optional page fetching for enrichment.
    """
    from scrylo.scrapers.sources_runner import run_sources_sync

    check_licence()
    startup_package_check()

    # Check patchright — required for web discovery
    if "web" in source:
        try:
            import patchright  # noqa: F401
            from patchright.sync_api import sync_playwright
            with sync_playwright() as p:
                p.chromium.executable_path
        except ImportError:
            _install_patchright_cli(console, need_package=True)
            return
        except Exception:
            _install_patchright_cli(console, need_package=False)
            return

    cfg        = load_cfg()
    serper_key = (cfg.get("scraper", {}).get("serper_api_key") or "").strip()

    # LLM warning (non-blocking)
    from scrylo.core.llm_adapter import _ollama_available
    groq_key = (cfg.get("llm", {}).get("groq_api_key") or "").strip()
    if not groq_key and not _ollama_available():
        console.print(
            "[yellow]💡 Tip: Lead quality is better with AI filtering.[/yellow]\n"
            "[dim]   Set a free Groq key at console.groq.com then add it via [white]scrylo serve[/white] → Settings\n"
            "   or edit [white]~/.scrylo/config.yaml[/white] manually. You can also run ollama locally[/dim]\n"
        )

    if "web" in source and not serper_key:
        console.print(
            "[red]✗ No Serper API key set.[/red]\n"
            "[dim]  Required for web search. Get a free key at serper.dev, then add it in Settings.[/dim]"
        )
        return

    console.print(Panel(
        f"[bold]Lead Finder[/bold]\n\n"
        f"Query:   [white]{query}[/white]\n"
        f"Sources: [white]{', '.join(source)}[/white]\n"
        f"Limit:   [white]{limit}[/white]\n"
        f"[dim]Searching sources → discovery engine → SQLite[/dim]",
        border_style="dim", title="[dim]scrylo find[/dim]",
    ))

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        console=console,
    ) as progress:
        task = progress.add_task(f"Searching for '{query}'...", total=limit)
        
        def _cb(stage, current, total, message):
            progress.update(task, completed=current, description=message)

        result = run_sources_sync(
            query=query, 
            sources=list(source), 
            limit=limit,
            progress_callback=_cb
        )
        progress.update(task, description="Done.")

    # Results table
    results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
    results_table.add_column("Name",    min_width=22)
    results_table.add_column("Location", min_width=15)
    results_table.add_column("Company", min_width=18)
    results_table.add_column("Email",   min_width=26)
    results_table.add_column("Source",  min_width=12)
    results_table.add_column("Score",   min_width=5, justify="right")

    from scrylo.db.session import db_session
    from scrylo.db.models import Lead
    with db_session() as db:
        saved_leads = db.query(Lead).filter(Lead.id.in_(result.lead_ids)).all()
        for lead in saved_leads:
            # Show tier indicator alongside email so partial leads are clear
            if lead.email:
                email_display = lead.email
            elif lead.linkedin_url:
                email_display = "[dim]→ LinkedIn DM[/dim]"
            elif lead.github_url:
                email_display = "[dim]→ GitHub Profile[/dim]"
            elif lead.company:
                email_display = "[dim]→ needs enrichment[/dim]"
            else:
                email_display = "[dim]—[/dim]"
            results_table.add_row(
                lead.full_name or "—",
                lead.location or "—",
                lead.company   or "—",
                email_display,
                lead.source    or "—",
                str(round(lead.icp_score, 1)) if lead.icp_score is not None else "—",
            )

    console.print(results_table)

    if result.leads_saved == 0 and not result.errors:
        console.print("\n[dim]No new leads found for this query.[/dim]")
        return

    if result.errors:
        for err in result.errors[:3]:
            console.print(f"[red]✗ {err}[/red]")

    # Tier breakdown — pulled from FinderResult if available
    t1 = getattr(result, 'tier1', 0)
    t2 = getattr(result, 'tier2', 0)
    t3 = getattr(result, 'tier3', 0)
    tier_parts = []
    if t1: tier_parts.append(f"[green]{t1} complete[/green]")
    if t2: tier_parts.append(f"[yellow]{t2} partial[/yellow]")
    if t3: tier_parts.append(f"[dim]{t3} signal[/dim]")
    tier_str = "  ·  ".join(tier_parts)

    console.print(
        f"\n[green]✓ {result.leads_saved} leads saved[/green]"
        + (f"  ({tier_str})" if tier_str else "")
        + (f"  [dim]{result.leads_skipped} skipped[/dim]" if result.leads_skipped else "")
    )
    console.print("[dim]💡 Run [/dim][cyan]scrylo serve[/cyan][dim] to filter leads and get more control via the web UI.[/dim]")


# ── icp ────────────────────────────────────────────────────────
@cli.command()
@click.option("--all", "all_leads", is_flag=True, help="Score all leads in database")
@click.option("--lead-id", type=int, help="Score a single lead by ID")
def icp(all_leads: bool, lead_id: int):
    """Score leads against your Ideal Customer Profile.

    Qualify leads based on the ICP description defined in Settings.
    Scores range from 0-100 based on role and keyword matching.
    """
    from scrylo.core.icp_scorer import score_leads_batch
    from scrylo.db.session import db_session
    from scrylo.db.models import Lead

    check_licence()
    cfg = load_cfg()
    profile = (cfg.get("icp", {}).get("profile") or "").strip()

    if not profile:
        console.print(
            "[yellow]⚠ No ICP profile set.[/yellow]\n"
            "[dim]  Go to Settings → ICP to define your target customer for accurate scoring.[/dim]"
        )
        if not click.confirm("Continue with default generic scoring?", default=True):
            return

    with db_session() as db:
        if lead_id:
            leads = db.query(Lead).filter(Lead.id == lead_id).all()
        elif all_leads:
            leads = db.query(Lead).all()
        else:
            # Default: score unscored leads
            leads = db.query(Lead).filter(Lead.icp_score.is_(None)).limit(100).all()

        if not leads:
            console.print("[dim]No leads to score.[/dim]")
            return

        console.print(f"[dim]Scoring {len(leads)} lead(s) against ICP...[/dim]")

        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console,
        ) as progress:
            task = progress.add_task("Calculating scores...", total=len(leads))

            # score_leads_batch doesn't have a progress callback, so we just run it
            results = score_leads_batch([l.id for l in leads], profile)
            progress.update(task, completed=len(leads), description="Done.")

        # Results table
        results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
        results_table.add_column("ID",      style="dim", min_width=4)
        results_table.add_column("Name",    min_width=22)
        results_table.add_column("Company", min_width=20)
        results_table.add_column("Score",   min_width=6, justify="right")

        for lead in leads:
            score = results.get(lead.id, lead.icp_score or 0)
            score_str = f"[green]{score:.0f}[/green]" if score >= 70 else f"{score:.0f}"
            results_table.add_row(
                str(lead.id),
                (lead.full_name or "—")[:40],
                (lead.company or "—")[:40],
                score_str,
            )

        console.print(results_table)
        console.print(f"\n[green]✓ {len(results)} leads scored[/green]")



# ── pitch ──────────────────────────────────────────────────────
@cli.command()
@click.option("--no-llm", is_flag=True, help="Use Jinja2 templates only — no LLM")
@click.option("--provider", "force_provider", default="",
              type=click.Choice(["", "groq", "ollama", "jinja2"]),
              help="Force a specific LLM provider (default: auto-detect)")
@click.option("--preview", is_flag=True, help="Preview first pitch before running batch")
@click.option("--all", "all_leads", is_flag=True, help="Generate pitches for all leads in database")
@click.option("--lead-id", type=int, help="Generate pitch for a specific lead by its Database ID")
@click.option("--export", type=click.Path(), help="Export generated pitches to a .txt file (e.g. pitches.txt)")
def pitch(no_llm: bool, force_provider: str, preview: bool, all_leads: bool, lead_id: int, export: str):
    """Generate personalised cold email pitches for leads.

    Uses the Groq → Ollama → Jinja2 chain to write targeted emails.
    Requires a service description set in Settings for best results.
    """
    from scrylo.core.llm_adapter import generate_pitch, LLMResult
    from scrylo.db.session import db_session
    from scrylo.db.models import Lead, LeadStatus
    import re as _re

    provider = "jinja2" if no_llm else (force_provider or "")

    check_licence()
    cfg = load_cfg()
    groq_key = (cfg.get("llm", {}).get("groq_api_key") or "").strip()
    svc      = (cfg.get("pitch", {}).get("service_description") or "").strip()

    # Warn if no service description
    if not svc and not no_llm:
        console.print(
            "[yellow]⚠  No service description set in Settings → Pitch.[/yellow]\n"
            "[dim]   Pitches will be generic. Run:[/dim] [white]scrylo serve[/white] "
            "[dim]→ Settings → Service Description[/dim]\n"
        )
        if not click.confirm("Continue with generic pitches?", default=True):
            return

    # Show active provider
    from scrylo.core.llm_adapter import _ollama_available
    ollama_ready = _ollama_available()

    # Respect the forced provider in the UI label
    effective_provider = "jinja2" if no_llm else (force_provider or "")

    if effective_provider == "jinja2":
        provider_label = "[dim]Jinja2 templates[/dim]"
    elif effective_provider == "groq":
        provider_label = "[green]Groq — Llama 3.3 70B[/green]"
    elif effective_provider == "ollama":
        provider_label = "[green]Ollama (Local AI)[/green]"
    elif groq_key:
        provider_label = "[green]Groq — Llama 3.3 70B[/green]"
    elif ollama_ready:
        provider_label = "[green]Ollama (Local AI)[/green]"
    else:
        provider_label = (
            "[yellow]Jinja2 templates[/yellow]\n"
            "[dim]  💡 Tip: Get a free Groq key at console.groq.com for AI-written pitches.\n"
            "     Add it via [white]scrylo serve[/white] → Settings or edit [white]~/.scrylo/config.yaml[/white]. You can also run ollama locally.[/dim]"
        )

    console.print(Panel(
        f"[bold]Pitch Generator[/bold]\n\n"
        f"Provider: {provider_label}\n"
        f"Service:  [white]{svc[:60] + '...' if len(svc) > 60 else svc or '(not set)'}[/white]\n"
        f"[dim]Chain: Groq → Ollama → Jinja2 template fallback[/dim]",
        border_style="dim", title="[dim]scrylo pitch[/dim]",
    ))

    with db_session() as db:
        from sqlalchemy import or_
        if lead_id:
            leads = db.query(Lead).filter(Lead.id == lead_id).all()
        elif all_leads:
            leads = db.query(Lead).all()
        else:
            leads = db.query(Lead).filter(or_(
                Lead.email.isnot(None),
                Lead.linkedin_url.isnot(None),
                Lead.github_url.isnot(None)
            )).limit(10).all()

        if not leads:
            console.print("[yellow]No leads found. Run scrylo find first.[/yellow]")
            return

        console.print(f"[dim]Generating pitches for {len(leads)} lead(s)...[/dim]\n")

        results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
        results_table.add_column("ID",       style="dim", min_width=4)
        results_table.add_column("Name",     min_width=20)
        results_table.add_column("Provider", min_width=8)
        results_table.add_column("Subject",  min_width=30)
        results_table.add_column("Status",   min_width=6)

        for lead in leads:
            # Detect contact method for prompt routing
            cm = "email"
            if not lead.email and (lead.linkedin_url or lead.github_url):
                cm = "social"

            lead_data = {
                "full_name":           lead.full_name,
                "company":             lead.company,
                "role":                lead.role,
                "location":            getattr(lead, "location", ""),
                "industry":            getattr(lead, "industry", ""),
                "contact_method":      cm,
                "pain_points":         getattr(lead, "pain_points", ""),
                "website":             lead.website,
                "service_description": svc,
            }
            result = generate_pitch(lead_data, force_provider=provider)

            # Parse SUBJECT: line
            subject = ""
            body    = result.text
            if result.text:
                subj_match = _re.search(r"\nSUBJECT:\s*(.+)$", result.text, _re.IGNORECASE)
                if subj_match:
                    subject = subj_match.group(1).strip()
                    body    = result.text[:subj_match.start()].strip()
                else:
                    fn = (lead.full_name or "").split()[0]
                    co = lead.company or ""
                    subject = f"Idea for {co}" if co else f"Quick question, {fn}"

            # Save to DB
            lead.pitch_subject      = subject
            lead.pitch_body         = body
            lead.pitch_generated_by = result.provider
            if lead.status == LeadStatus.NEW:
                lead.status = LeadStatus.PITCHED
        db.commit()

        # Preview first pitch
        if preview and leads:
            first = leads[0]
            console.print(Panel(
                f"Subject: {first.pitch_subject}\n\n{first.pitch_body}",
                title=f"[dim]Preview — {first.full_name}[/dim]",
                border_style="green" if first.pitch_generated_by != "jinja2" else "dim",
            ))
            if len(leads) > 1:
                if not click.confirm("\nContinue with remaining leads?", default=True):
                    return

        for lead in leads:
            status = "[green]✓[/green]" if lead.pitch_body else "[red]✗[/red]"
            results_table.add_row(
                str(lead.id),
                lead.full_name or "—",
                lead.pitch_generated_by or "—",
                (lead.pitch_subject or "")[:40],
                status,
            )

        console.print(results_table)
        console.print(f"\n[green]✓ {len(leads)} pitches generated and saved[/green]")

        # Export if requested
        if export:
            try:
                with open(export, "w", encoding="utf-8") as f:
                    sep = "=" * 80 + "\n"
                    line = "-" * 80 + "\n"
                    for l in leads:
                        f.write(sep)
                        f.write(f"NAME:    {l.full_name or '—'}\n")
                        f.write(f"ROLE:    {l.role or '—'}\n")
                        f.write(f"COMPANY: {l.company or '—'}\n")
                        contact = l.email or l.linkedin_url or l.github_url or "—"
                        f.write(f"CONTACT: {contact}\n")
                        f.write(line)
                        if l.pitch_subject:
                            f.write(f"SUBJECT: {l.pitch_subject}\n\n")
                        f.write(f"{l.pitch_body}\n")
                        f.write(sep + "\n\n")
                console.print(f"[green]✓ Pitches exported to: {export}[/green]")
            except Exception as e:
                console.print(f"[red]✗ Export failed: {e}[/red]")


# ── send ───────────────────────────────────────────────────────
@cli.command()
@click.option("--campaign", required=True, help="Campaign name for tracking")
@click.option("--dry-run", is_flag=True, help="Preview sends without actually sending")
@click.option("--limit", type=int, default=50, show_default=True, help="Max emails to send")
@click.option("--delay-min", default=90,  show_default=True, type=int, help="Min delay between sends (sec)")
@click.option("--delay-max", default=180, show_default=True, type=int, help="Max delay between sends (sec)")
def send(campaign: str, dry_run: bool, limit: int, delay_min: int, delay_max: int):
    """Bulk send pitched emails via SMTP with anti-spam delays."""
    from scrylo.email_tools.smtp_sender import run_bulk_send

    cfg = load_cfg()
    check_licence()
    smtp_host = (cfg.get("email", {}).get("smtp_host") or "").strip()
    smtp_user = (cfg.get("email", {}).get("smtp_user") or "").strip()
    smtp_pass = (cfg.get("email", {}).get("smtp_password") or "").strip()


    mode_label = "[yellow]DRY RUN[/yellow]" if dry_run else "[green]LIVE SEND[/green]"
    console.print(Panel(
        f"{mode_label}\n\n"
        f"[dim]Campaign: [white]{campaign}[/white]  ·  "
        f"Limit: [white]{limit}[/white]  ·  "
        f"Delay: [white]{delay_min}–{delay_max}s[/white][/dim]",
        border_style="dim", title="[dim]scrylo send[/dim]",
    ))

    if not dry_run:
        if not smtp_host or not smtp_user or not smtp_pass:
            console.print("[red]✗ SMTP not configured. Run:[/red] scrylo settings")
            return
        console.print(f"[dim]SMTP: {smtp_host} as {smtp_user}[/dim]")

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        console=console,
    ) as progress:
        task = progress.add_task("Sending...", total=None)

        def _cb(current: int, total: int, label: str):
            progress.update(task, description=label, total=total, completed=current)

        result = run_bulk_send(
            campaign_name     = campaign,
            dry_run           = dry_run,
            delay_min         = delay_min,
            delay_max         = delay_max,
            limit             = limit,
            progress_callback = _cb,
        )

    if result.subject_warnings:
        for w in result.subject_warnings:
            console.print(f"[yellow]⚠ {w}[/yellow]")

    if result.errors:
        for err in result.errors[:5]:
            console.print(f"[red]✗ {err}[/red]")

    # Results table
    tbl = Table(show_header=True, header_style="dim", border_style="dim")
    tbl.add_column("Name",    style="bold")
    tbl.add_column("Email",   style="dim")
    tbl.add_column("Subject", style="dim", max_width=40)
    tbl.add_column("Status",  justify="center")

    status_colours = {
        "sent":    "[green]sent[/green]",
        "dry_run": "[yellow]dry run[/yellow]",
        "skipped": "[dim]skipped[/dim]",
        "failed":  "[red]failed[/red]",
    }

    for lr in result.leads:
        tbl.add_row(
            lr.name or "—",
            lr.email or "—",
            (lr.subject or "—")[:40],
            status_colours.get(lr.status, lr.status),
        )

    console.print(tbl)
    console.print(
        f"\n[{'yellow' if dry_run else 'green'}]"
        f"{'⚙ Dry run' if dry_run else '✓ Done'} — "
        f"{result.sent} {'would send' if dry_run else 'sent'}  ·  "
        f"{result.skipped} skipped  ·  {result.failed} failed[/]"
    )


# ── ocr ────────────────────────────────────────────────────────
@cli.command()
@click.option("--folder", required=True, type=click.Path(exists=True),
              help="Folder containing business card images (PNG/JPG)")
@click.option("--recursive", is_flag=True, help="Search sub-folders too")
@click.option("--dry-run", is_flag=True, help="Parse but don't save to DB")
def ocr(folder: str, recursive: bool, dry_run: bool):
    """Extract leads from business card images.
    
    Uses EasyOCR + regex parsing to pull contact details from PNG/JPG files.
    Deduplicates automatically against existing database records.
    """
    import glob, os
    from scrylo.scrapers.card_parser import preprocess_card_image, parse_card_text_with_llm_fallback
    from scrylo.db.session import db_session
    from scrylo.db.models import Lead, LeadStatus

    # ── Check EasyOCR ────────────────────────────────────────
    check_licence()
    try:
        import easyocr
    except ImportError:
        console.print("[red]✗ EasyOCR not installed.[/red]")
        console.print("[dim]  Run: pip install easyocr[/dim]")
        return

    # ── Find images ──────────────────────────────────────────
    pattern = "**/*" if recursive else "*"
    exts    = {".png", ".jpg", ".jpeg"}
    images  = [
        p for p in glob.glob(os.path.join(folder, pattern), recursive=recursive)
        if os.path.splitext(p)[1].lower() in exts
    ]

    if not images:
        console.print(f"[yellow]No PNG/JPG images found in {folder}[/yellow]")
        return

    console.print(Panel(
        f"[bold]CV / OCR Importer[/bold]\n\n"
        f"Folder:    [white]{folder}[/white]\n"
        f"Images:    [white]{len(images)}[/white]\n"
        f"Recursive: [white]{recursive}[/white]\n"
        f"Dry run:   [white]{dry_run}[/white]\n"
        f"[dim]EasyOCR → regex parser → SQLite (LLM only for missing names)[/dim]",
        border_style="dim", title="[dim]scrylo ocr[/dim]",
    ))

    reader      = easyocr.Reader(["en"], gpu=False, verbose=False)
    total_added = 0
    total_skip  = 0
    total_fail  = 0

    results_table = Table(box=box.SIMPLE, show_header=True, padding=(0, 1))
    results_table.add_column("File",    min_width=24)
    results_table.add_column("Name",    min_width=18)
    results_table.add_column("Email",   min_width=22)
    results_table.add_column("Via",     min_width=7)
    results_table.add_column("Status",  min_width=8)

    with db_session() as db:
        for img_path in images:
            fname = os.path.basename(img_path)
            try:
                pre_path  = preprocess_card_image(img_path)
                raw_lines = reader.readtext(pre_path, detail=0, paragraph=True)
                text = "\n".join(raw_lines).strip()
                if os.path.exists(pre_path):
                    os.unlink(pre_path)
                if not text:
                    results_table.add_row(fname, "—", "—", "—", "[dim]no text[/dim]")
                    total_fail += 1
                    continue

                ld        = parse_card_text_with_llm_fallback(text)
                full_name = (ld.get("full_name") or "").strip() or None
                email     = (ld.get("email") or "").strip() or None
                company   = (ld.get("company") or "").strip() or None

                if not full_name and not email:
                    results_table.add_row(fname, "—", "—", "—", "[red]no contact[/red]")
                    total_fail += 1
                    continue

                # Via: did we need the LLM for the name?
                via = "[dim]regex[/dim]"
                if full_name and ld.get("_name_via_llm"):
                    via = "[yellow]llm[/yellow]"

                # Deduplicate
                existing = None
                if email:
                    existing = db.query(Lead).filter(Lead.email == email).first()
                if not existing and full_name and company:
                    existing = db.query(Lead).filter(
                        Lead.full_name == full_name,
                        Lead.company   == company,
                    ).first()

                if existing:
                    results_table.add_row(fname, full_name or "—", email or "—", via, "[dim]duplicate[/dim]")
                    total_skip += 1
                    continue

                if not dry_run:
                    words = full_name.split() if full_name else []
                    lead  = Lead(
                        full_name  = full_name,
                        first_name = words[0] if words else None,
                        last_name  = words[-1] if len(words) > 1 else None,
                        role       = (ld.get("role") or "").strip() or None,
                        company    = company,
                        email      = email,
                        phone      = (ld.get("phone") or "").strip() or None,
                        website    = (ld.get("website") or "").strip() or None,
                        location   = (ld.get("location") or "").strip() or None,
                        source     = "ocr",
                        status     = LeadStatus.NEW,
                    )
                    db.add(lead)
                    total_added += 1

                results_table.add_row(
                    fname, full_name or "—", email or "—", via,
                    "[dim]dry-run[/dim]" if dry_run else "[green]✓ added[/green]"
                )

            except Exception as e:
                results_table.add_row(fname, "—", "—", "—", "[red]error[/red]")
                total_fail += 1
                continue

        if not dry_run:
            db.commit()

    console.print(results_table)
    console.print(
        f"\n[green]✓ {total_added} added[/green]"
        + (f"  [dim]{total_skip} duplicate{'s' if total_skip != 1 else ''}[/dim]" if total_skip else "")
        + (f"  [red]{total_fail} failed[/red]" if total_fail else "")
    )
    console.print("[dim]💡 Run [/dim][cyan]scrylo serve[/cyan][dim] to manage leads and write pitches via the web UI.[/dim]")
    if dry_run:
        console.print("[dim]Dry run — nothing saved to DB[/dim]")


if __name__ == "__main__":
    cli()
