"""
BIBIVIDI — CLI
Usage:
  bibividi start               Start the proxy server
  bibividi start --ssl         Start HTTP proxy + HTTPS CONNECT tunnel
  bibividi status              Show proxy status and savings
  bibividi list                List scripts in the library
  bibividi add <dir>           Add a BSS script pack (skill.json + skill.py)
  bibividi test <script>       Test a script with a prompt from stdin
  bibividi review              List auto-generated skills pending approval
  bibividi approve <id>        Activate a pending skill
  bibividi feedback bad <id>   Flag a request where BIBIVIDI produced wrong output
  bibividi feedback list       List all flagged requests
  bibividi tail                Stream live request events from the running proxy
  bibividi digest              Show weekly savings digest (--now to also send)
  bibividi simulate <file>     Simulate BIBIVIDI against a request log file
  bibividi calibrate           Test match scores for sample prompts
  bibividi roi                 Open the ROI calculator in your browser
  bibividi digest-quarterly    Send quarterly savings digest email (Resend/Postmark)
  bibividi report              Generate CSRD AI Scope 3 emissions report (CSV/HTML/text)
  bibividi billing summary     Show savings-share billing calculation for a month
  bibividi billing run         Compute bill and create Stripe invoice
  bibividi cert generate           Generate local CA + server certificate
  bibividi cert install            Install CA in the OS trust store
  bibividi cert path               Show certificate file paths
  bibividi marketplace payouts     List pending author payout rows
  bibividi marketplace pay <id>    Mark a payout as paid
  bibividi marketplace simulate    Preview revenue split for a chain skill
"""
from __future__ import annotations

import asyncio
import cmd
import shlex
import sys
import threading

import click
from rich.console import Console
from rich.table import Table

console = Console()


# ── REPL shell ─────────────────────────────────────────────────────────────────


class BibividiShell(cmd.Cmd):
    """Interactive REPL shell for BIBIVIDI."""

    intro = None
    prompt = "bibividi> "

    def __init__(self) -> None:
        super().__init__()
        self._proxy_thread: threading.Thread | None = None
        self._uvicorn_server = None  # uvicorn.Server when proxy is running
        # Readline history (best-effort — not available on all platforms)
        from pathlib import Path
        self._hist_file = Path.home() / ".bibividi" / "shell_history"
        self._hist_file.parent.mkdir(parents=True, exist_ok=True)
        try:
            import readline
            try:
                readline.read_history_file(self._hist_file)
            except FileNotFoundError:
                pass
            readline.set_history_length(500)
        except ImportError:
            pass

    # ── helpers ────────────────────────────────────────────────────────────────

    def _proxy_running(self) -> bool:
        return self._proxy_thread is not None and self._proxy_thread.is_alive()

    def _invoke(self, args: list[str]) -> None:
        """Dispatch to an existing click command by args list."""
        try:
            main.main(args, standalone_mode=False)
        except SystemExit:
            pass
        except Exception as exc:
            console.print(f"[red]Error:[/] {exc}")

    # ── commands ───────────────────────────────────────────────────────────────

    def do_start(self, arg: str) -> None:
        """start [--ssl]   Start the proxy in the background"""
        if self._proxy_running():
            console.print("[yellow]Proxy already running. Use 'stop' to stop it.[/]")
            return

        import time

        import uvicorn

        from bvd.config import settings
        from bvd.core.proxy import app

        h = settings.proxy_host
        p = settings.proxy_port
        use_ssl = "--ssl" in arg

        config = uvicorn.Config(app, host=h, port=p, log_level="warning")
        self._uvicorn_server = uvicorn.Server(config)
        server = self._uvicorn_server

        if use_ssl:
            from bvd.core.certs import get_or_generate_certs
            get_or_generate_certs()

        def _run() -> None:
            asyncio.run(server.serve())

        self._proxy_thread = threading.Thread(target=_run, daemon=True)
        self._proxy_thread.start()
        time.sleep(0.8)

        if self._proxy_running():
            console.print(f"[bold green]Proxy started[/] on [bold]{h}:{p}[/]")
            console.print(
                f"[dim]Point your apps to http://{h}:{p}"
                "  ·  'tail' to watch requests  ·  'stop' to stop[/]"
            )
        else:
            console.print("[red]Proxy failed to start.[/] Check the output above.")

    def do_stop(self, arg: str) -> None:
        """stop   Stop the running proxy"""
        if not self._proxy_running():
            console.print("[yellow]Proxy is not running.[/]")
            return
        if self._uvicorn_server is not None:
            self._uvicorn_server.should_exit = True
            console.print("[green]Proxy stopping…[/]")

    def do_status(self, arg: str) -> None:
        """status   Show proxy status and token savings"""
        self._invoke(["status"])

    def do_list(self, arg: str) -> None:
        """list [--chains]   List installed skills"""
        self._invoke(["list"] + shlex.split(arg))

    def do_add(self, arg: str) -> None:
        """add <pack-dir>   Install a BSS skill pack"""
        parts = shlex.split(arg)
        if not parts:
            console.print("[red]Usage:[/] add <path-to-bss-pack>")
            return
        self._invoke(["add"] + parts)

    def do_review(self, arg: str) -> None:
        """review   List pending auto-generated skills"""
        self._invoke(["review"])

    def do_approve(self, arg: str) -> None:
        """approve <skill-id>   Activate a pending skill"""
        parts = shlex.split(arg)
        if not parts:
            console.print("[red]Usage:[/] approve <skill-id>")
            return
        self._invoke(["approve"] + parts)

    def do_tail(self, arg: str) -> None:
        """tail   Stream live request events (Ctrl+C to stop)"""
        self._invoke(["tail"])

    def do_simulate(self, arg: str) -> None:
        """simulate <file>   Simulate against a request log file"""
        parts = shlex.split(arg)
        if not parts:
            console.print("[red]Usage:[/] simulate <requests.json>")
            return
        self._invoke(["simulate"] + parts)

    def do_calibrate(self, arg: str) -> None:
        """calibrate [file]   Test match scores for sample prompts"""
        self._invoke(["calibrate"] + shlex.split(arg))

    def do_demo(self, arg: str) -> None:
        """demo   Run the built-in token savings demo"""
        self._invoke(["demo"])

    def do_digest(self, arg: str) -> None:
        """digest [--now]   Show weekly savings digest"""
        self._invoke(["digest"] + shlex.split(arg))

    def do_report(self, arg: str) -> None:
        """report [--month YYYY-MM] [--format text|csv|html]   CSRD emissions report"""
        self._invoke(["report"] + shlex.split(arg))

    def do_roi(self, arg: str) -> None:
        """roi   Open ROI calculator in browser"""
        self._invoke(["roi"])

    def do_activate(self, arg: str) -> None:
        """activate <license-key>   Activate your BIBIVIDI license key"""
        parts = shlex.split(arg)
        if not parts:
            console.print("[red]Usage:[/] activate <license-key>")
            return
        self._invoke(["activate"] + parts)

    def do_feedback(self, arg: str) -> None:
        """feedback bad <id>  |  feedback list   Flag a bad output or list flags"""
        self._invoke(["feedback"] + shlex.split(arg))

    def do_exit(self, arg: str) -> bool:  # type: ignore[override]
        """exit   Quit BIBIVIDI shell"""
        try:
            import readline
            readline.write_history_file(self._hist_file)
        except Exception:
            pass
        console.print("[dim]Bye![/]")
        return True

    def do_EOF(self, arg: str) -> bool:  # type: ignore[override]
        console.print()
        return self.do_exit(arg)

    def default(self, line: str) -> None:
        console.print(f"[red]Unknown command:[/] {line!r}  — type [bold]help[/] or [bold]?[/]")

    def emptyline(self) -> None:
        pass  # don't repeat last command on empty Enter


def _launch_shell() -> None:
    console.print(
        "\n[bold green]BIBIVIDI[/] — AI token optimization proxy\n"
        "Type [bold]help[/] or [bold]?[/] for commands  ·  "
        "[bold]exit[/] or Ctrl+D to quit\n"
    )
    BibividiShell().cmdloop()


# ── CLI group ──────────────────────────────────────────────────────────────────


@click.group(invoke_without_command=True)
@click.pass_context
def main(ctx: click.Context) -> None:
    """BIBIVIDI — AI token optimization proxy."""
    if ctx.invoked_subcommand is None:
        _launch_shell()


# ── activate ──────────────────────────────────────────────────────────────────

@main.command()
@click.argument("license_key")
def activate(license_key: str) -> None:
    """Activate your BIBIVIDI license key.

    The key is issued by email after completing Stripe Checkout on bibividi.ai.
    It is stored in ~/.bibividi/config.toml and sent as a header on billing calls.

    Example:

    \b
        bibividi activate bvd-abc12345
    """
    from bvd.config import settings
    from bvd.license import is_valid_format, save_license_key, validate_key_with_api

    key = license_key.strip()
    if not is_valid_format(key):
        console.print(
            "[red]Invalid key format.[/] Expected [bold]bvd-xxxxxxxx[/] "
            "(check your activation email)."
        )
        sys.exit(1)

    console.print("[dim]Validating key…[/]")

    async def _validate() -> bool:
        return await validate_key_with_api(key, settings.billing_api_url)

    try:
        ok = asyncio.run(_validate())
    except Exception as exc:
        console.print(f"[red]Activation failed:[/] {exc}")
        sys.exit(1)

    if not ok:
        console.print(
            "[red]Invalid key or key not recognised.[/] Check your activation email "
            "or contact support@bibividi.ai"
        )
        sys.exit(1)

    save_license_key(key)
    console.print("[bold green]✓ License key activated[/] → ~/.bibividi/config.toml")
    console.print("[dim]Savings will now be attributed to your account on every execution.[/]")


# ── start ─────────────────────────────────────────────────────────────────────

@main.command()
@click.option("--host", default=None, help="Override proxy host")
@click.option("--port", default=None, type=int, help="Override proxy port")
@click.option("--ssl", "use_ssl", is_flag=True, default=False,
              help="Also start HTTPS CONNECT tunnel (port BIBIVIDI_TUNNEL_PORT, default 8443)")
def start(host: str | None, port: int | None, use_ssl: bool) -> None:
    """Start the BIBIVIDI proxy server."""
    from bvd.config import settings

    h = host or settings.proxy_host
    p = port or settings.proxy_port

    console.print(f"[bold green]BIBIVIDI[/] proxy starting on [bold]{h}:{p}[/]")
    console.print(f"Point your apps to [bold]http://{h}:{p}[/] as the API base URL")

    if use_ssl:
        from bvd.core.certs import ca_cert_path, get_or_generate_certs
        get_or_generate_certs()
        tp = settings.tunnel_port
        console.print(
            f"HTTPS CONNECT tunnel on [bold]{h}:{tp}[/]  "
            f"(CA: {ca_cert_path()})"
        )
        console.print(
            "[dim]Trust the CA or run 'bibividi cert install' to avoid TLS warnings[/]"
        )

    console.print("Press [bold]Ctrl+C[/] to stop\n")

    from bvd.license import load_license_key
    if not load_license_key():
        console.print(
            "[dim]No license key — savings are stored locally only. "
            "Run [bold]bibividi activate <key>[/] to enable cloud sync.[/]\n"
        )

    # Warn on first run: the embedding model (~90 MB) downloads from HuggingFace
    # before the proxy accepts its first request. This only happens once.
    from pathlib import Path
    cache_dir = Path.home() / ".cache" / "huggingface" / "hub"
    model_slug = "models--sentence-transformers--all-MiniLM-L6-v2"
    if not (cache_dir / model_slug).exists():
        console.print(
            "[dim]First run: downloading embedding model (~90 MB). "
            "This takes 30–60 s and only happens once.[/]\n"
        )

    _run_proxy(h, p, use_ssl)


def _run_proxy(host: str, port: int, use_ssl: bool) -> None:
    """Start uvicorn (and optionally the CONNECT tunnel). Extracted for testability."""
    import uvicorn

    from bvd.core.proxy import app

    if use_ssl:
        asyncio.run(_run_with_tunnel(app, host, port))
    else:
        uvicorn.run(app, host=host, port=port, log_level="warning")


async def _run_with_tunnel(app, host: str, port: int) -> None:
    """Run the HTTP proxy and the CONNECT tunnel server concurrently."""
    import uvicorn

    from bvd.config import settings
    from bvd.core.tunnel import start_tunnel_server

    tunnel = await start_tunnel_server(host, settings.tunnel_port)

    config = uvicorn.Config(app, host=host, port=port, log_level="warning")
    server = uvicorn.Server(config)

    async with tunnel:
        await server.serve()


# ── status ────────────────────────────────────────────────────────────────────

@main.command()
def status() -> None:
    """Show proxy status and savings summary."""
    async def _run():
        from bvd.billing import free_tier_daily_tokens, is_free_tier_model
        from bvd.config import settings
        from bvd.engine.library import library
        from bvd.engine.stats import stats_tracker

        await library.init()
        summary = await stats_tracker.summary()
        count = await library.count()
        last_model = await stats_tracker.last_used_model()

        quota_mode = (
            settings.quota_daily_tokens is not None
            or settings.allow_local_apis
            or (last_model is not None and is_free_tier_model(last_model))
        )

        console.print("\n[bold]BIBIVIDI Status[/]")
        console.print(f"  Proxy        : {settings.proxy_host}:{settings.proxy_port}")
        console.print(f"  Library      : {count} scripts")
        console.print(f"  Total calls  : {summary.get('total_calls', 0)}")
        console.print(f"  Tokens saved : {summary.get('tokens_saved', 0):,}")
        ratio = summary.get("saving_ratio", 0)
        console.print(f"  Saving ratio : {ratio * 100:.1f}%")

        if quota_mode:
            tokens_saved = summary.get("tokens_saved", 0)
            total_calls = summary.get("total_calls", 0)
            tokens_in = summary.get("tokens_in", 0)
            avg_prompt = tokens_in // total_calls if total_calls > 0 else 1_000
            extra_requests = tokens_saved // max(avg_prompt, 1)

            daily_limit = settings.quota_daily_tokens
            if daily_limit is None and last_model:
                daily_limit = free_tier_daily_tokens(last_model)

            label = last_model or ("local" if settings.allow_local_apis else "free tier")
            console.print(f"\n  [bold cyan]● Quota Optimizer Mode[/] ({label})")
            if extra_requests > 0:
                console.print(
                    f"  Extra requests : [bold green]+{extra_requests:,}[/] "
                    "more prompts in your daily quota"
                )
            if daily_limit and tokens_saved > 0:
                relief_pct = tokens_saved / daily_limit * 100
                console.print(
                    f"  Quota relief   : [bold green]{relief_pct:.1f}%[/] "
                    "of your daily token limit saved"
                )

        console.print()

    asyncio.run(_run())


# ── list ──────────────────────────────────────────────────────────────────────

@main.command(name="list")
@click.option("--chains", "chains_only", is_flag=True, default=False,
              help="Show only BSS 1.1 chain skills")
def list_scripts(chains_only: bool) -> None:
    """List all scripts in the library."""
    async def _run():
        from bvd.engine.library import library
        await library.init()
        scripts = await library.all()

        if chains_only:
            scripts = [s for s in scripts if s.is_chain]

        if not scripts:
            if chains_only:
                console.print("[yellow]No chain skills in library yet.[/]")
                console.print("Install a BSS 1.1 chain with: [bold]bibividi add <pack>[/]")
            else:
                console.print("[yellow]No scripts in library yet.[/]")
                console.print("Add one with: [bold]bibividi add <path-to-bss-pack>[/]")
            return

        title = "BIBIVIDI Chain Skills" if chains_only else "BIBIVIDI Script Library"
        table = Table(title=title)
        table.add_column("ID", style="cyan")
        table.add_column("Name")
        table.add_column("Chain", style="yellow", justify="center")
        table.add_column("Tags", style="dim")
        table.add_column("Uses", justify="right")
        table.add_column("Avg saving", justify="right", style="green")

        for s in scripts:
            chain_str = f"{len(s.chain_ids)} steps" if s.is_chain else ""
            table.add_row(
                s.id,
                s.name,
                chain_str,
                ", ".join(s.tags),
                str(s.usage_count),
                f"{s.avg_saving * 100:.0f}%",
            )
        console.print(table)

    asyncio.run(_run())


# ── add ───────────────────────────────────────────────────────────────────────

@main.command()
@click.argument("pack_dir", type=click.Path(exists=True))
def add(pack_dir: str) -> None:
    """Add a BSS script pack from a directory (must contain skill.json + skill.py)."""
    import json
    from pathlib import Path

    async def _run():
        from bvd.engine.library import library
        from bvd.engine.matcher import matcher

        await library.init()
        pack = Path(pack_dir)
        skill_json_path = pack / "skill.json"
        skill_py_path = pack / "skill.py"

        if not skill_json_path.exists() or not skill_py_path.exists():
            console.print("[red]Error:[/] pack must contain skill.json and skill.py")
            sys.exit(1)

        skill_json = json.loads(skill_json_path.read_text())
        skill_py = skill_py_path.read_text()
        skill_sig_path = pack / "skill.sig"
        skill_sig = skill_sig_path.read_bytes() if skill_sig_path.exists() else None

        if skill_sig is not None:
            console.print("[dim]Verifying Ed25519 signature…[/]")
        script = await library.import_bss(skill_json, skill_py, skill_sig=skill_sig)
        console.print("[dim]Computing embedding…[/]")
        script = await matcher.embed_script(script)
        await library.save(script)
        console.print(f"[bold green]Added:[/] {script.id} — {script.name}")

    asyncio.run(_run())


# ── test ──────────────────────────────────────────────────────────────────────

@main.command()
@click.argument("script_id")
def test(script_id: str) -> None:
    """Test a script. Provide prompt via stdin."""
    async def _run():
        from bvd.engine.library import library
        from bvd.engine.runner import run_script

        await library.init()
        script = await library.get(script_id)
        if not script:
            console.print(f"[red]Script not found:[/] {script_id}")
            sys.exit(1)

        prompt = sys.stdin.read()
        console.print(f"\n[dim]Running {script.id}…[/]")
        result = await run_script(script, prompt)

        console.print("\n[bold]Output:[/]")
        console.print(result.output)
        if result.stderr:
            console.print(f"\n[yellow]Stderr:[/] {result.stderr}")
        console.print(f"\n[dim]Exit code: {result.exit_code}, timed out: {result.timed_out}[/]")

    asyncio.run(_run())


# ── review / approve ──────────────────────────────────────────────────────────

@main.command()
def review() -> None:
    """List skills pending approval (auto-generated, not yet executed)."""
    async def _run():
        from bvd.engine.library import library
        await library.init()
        pending = await library.list_pending()

        if not pending:
            console.print("[green]No skills pending review.[/]")
            return

        console.print(f"\n[bold yellow]{len(pending)} skill(s) pending approval:[/]\n")
        for s in pending:
            console.print(f"  [cyan]{s.id}[/]  {s.name}")
            console.print(f"  [dim]{s.description}[/]")
            console.print(f"  Created: {s.created_at[:19]}\n")
            console.print("  [dim]--- skill.py ---[/]")
            console.print(f"  [dim]{s.code[:400]}{'…' if len(s.code) > 400 else ''}[/]\n")

        console.print(
            "To activate a skill: [bold]bibividi approve <skill-id>[/]\n"
            "To delete a skill:   [bold]bibividi delete <skill-id>[/]"
        )

    asyncio.run(_run())


@main.command()
@click.argument("skill_id")
def approve(skill_id: str) -> None:
    """Approve a pending skill and activate it for execution."""
    async def _run():
        from bvd.engine.library import library
        from bvd.engine.matcher import matcher

        await library.init()
        ok = await library.approve(skill_id)
        if not ok:
            console.print(
                f"[red]Not found:[/] '{skill_id}' (must be a pending skill — "
                "check [bold]bibividi review[/])"
            )
            sys.exit(1)

        matcher.invalidate()
        console.print(f"[bold green]Approved:[/] {skill_id} is now active.")

    asyncio.run(_run())


# ── tail ──────────────────────────────────────────────────────────────────────

@main.command()
@click.option("--host", default=None, help="Proxy host (default: BIBIVIDI_PROXY_HOST)")
@click.option("--port", default=None, type=int, help="Proxy port (default: BIBIVIDI_PROXY_PORT)")
def tail(host: str | None, port: int | None) -> None:
    """Stream live request events from the running proxy.

    Shows each intercepted request: host, match status, skill, score,
    token savings, and latency — in real time.
    """
    import json

    import httpx
    from rich.live import Live
    from rich.table import Table

    from bvd.config import settings

    h = host or settings.proxy_host
    p = port or settings.proxy_port
    url = f"http://{h}:{p}/bibividi/events"

    rows: list[dict] = []

    def _build_table() -> Table:
        table = Table(title="BIBIVIDI — live requests  [dim](Ctrl+C to stop)[/]")
        table.add_column("Time", style="dim", width=12)
        table.add_column("Host", style="cyan", max_width=28)
        table.add_column("Path", style="dim", max_width=24)
        table.add_column("Match", justify="center", width=7)
        table.add_column("Skill", style="magenta", max_width=24)
        table.add_column("Score", justify="right", width=6)
        table.add_column("Before", justify="right", width=7)
        table.add_column("After", justify="right", width=7)
        table.add_column("Saving", justify="right", style="green", width=7)
        table.add_column("ms", justify="right", width=6)

        for r in rows[-30:]:    # last 30 rows
            match_icon = "[green]✓[/]" if r.get("matched") else "[dim]–[/]"
            saving = r.get("saving_pct", 0)
            saving_str = (
                f"[green]{saving:.0f}%[/]" if saving >= 50
                else f"[yellow]{saving:.0f}%[/]" if saving > 0
                else "[dim]0%[/]"
            )
            table.add_row(
                r.get("ts", "")[-15:-7],        # HH:MM:SS from ISO timestamp
                r.get("host", ""),
                r.get("path", ""),
                match_icon,
                r.get("skill_id") or "",
                f"{r['score']:.2f}" if r.get("score") else "",
                str(r.get("tokens_before", "")),
                str(r.get("tokens_after", "")),
                saving_str,
                str(r.get("latency_ms", "")),
            )
        return table

    console.print(f"Connecting to [bold]{url}[/] …")

    try:
        with httpx.Client(timeout=None) as client:
            with client.stream("GET", url) as resp:
                if resp.status_code != 200:
                    console.print(f"[red]Error {resp.status_code}:[/] is the proxy running?")
                    return

                console.print("[green]Connected.[/] Waiting for requests…\n")

                with Live(_build_table(), console=console, refresh_per_second=4) as live:
                    for line in resp.iter_lines():
                        if not line.startswith("data: "):
                            continue
                        payload = line[6:]
                        try:
                            data = json.loads(payload)
                        except json.JSONDecodeError:
                            continue
                        if data.get("type") == "connected":
                            continue
                        rows.append(data)
                        live.update(_build_table())

    except httpx.ConnectError:
        console.print(f"[red]Cannot connect to proxy at {url}[/]")
        console.print("Make sure [bold]bibividi start[/] is running.")
    except KeyboardInterrupt:
        console.print("\n[dim]Stopped.[/]")


# ── digest ────────────────────────────────────────────────────────────────────

@main.command()
@click.option("--now", is_flag=True, default=False,
              help="Send the digest immediately (bypasses Monday schedule and idempotency check)")
def digest(now: bool) -> None:
    """Show the weekly savings digest.

    Without --now: prints the past 7 days' summary to the terminal.
    With --now: also sends via desktop notification and Slack webhook
    (if BIBIVIDI_SLACK_WEBHOOK is configured).
    """
    async def _run():
        from bvd.engine.library import library
        from bvd.engine.stats import stats_tracker
        from bvd.notifier import build_digest_text, send_digest

        await library.init()
        summary = await stats_tracker.weekly_summary()

        console.print("\n[bold]BIBIVIDI — Weekly Digest (last 7 days)[/]\n")
        if summary["calls"] == 0:
            console.print("[yellow]No executions recorded in the past 7 days.[/]")
        else:
            console.print(build_digest_text(summary))

        if now:
            console.print()
            sent = await send_digest(force=True)
            if sent:
                console.print("[bold green]Digest sent.[/] (desktop + Slack if configured)")
            else:
                console.print("[yellow]Nothing to send (no data).[/]")

    asyncio.run(_run())


# ── digest quarterly ───────────────────────────────────────────────────────────

@main.command(name="digest-quarterly")
@click.option("--quarter", "quarter_str", default=None, metavar="YYYY-QN",
              help="Quarter to report (e.g. 2026-Q1, default: previous quarter)")
@click.option("--to", "to_email", default=None,
              help="Recipient email (overrides BIBIVIDI_MAILER_TO)")
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
              help="Print the email content without sending")
def digest_quarterly(quarter_str: str | None, to_email: str | None, dry_run: bool) -> None:
    """Send the quarterly team savings digest email.

    Computes quarterly token savings, gross € saved, BIBIVIDI fee, CO₂ avoided,
    and top 3 skills. Sends via Resend or Postmark (set BIBIVIDI_MAILER_API_KEY).

    Examples:

    \b
        bibividi digest-quarterly --quarter 2026-Q1
        bibividi digest-quarterly --quarter 2026-Q1 --dry-run
        bibividi digest-quarterly --to finance@company.com
    """
    from datetime import datetime as _dt

    async def _run() -> None:
        from bvd.config import settings as _settings
        from bvd.engine.library import library
        from bvd.engine.stats import stats_tracker
        from bvd.mailer import build_quarterly_email, send_quarterly_email

        await library.init()

        # ── Parse quarter ──────────────────────────────────────────────────────
        if quarter_str:
            try:
                parts = quarter_str.upper().split("-Q")
                year, quarter = int(parts[0]), int(parts[1])
                if quarter not in (1, 2, 3, 4):
                    raise ValueError
            except (ValueError, IndexError):
                console.print(
                    "[red]Invalid quarter format — use YYYY-QN (e.g. 2026-Q1)[/]"
                )
                sys.exit(1)
        else:
            now = _dt.now()
            # Default: previous quarter
            q_now = (now.month - 1) // 3 + 1
            if q_now == 1:
                year, quarter = now.year - 1, 4
            else:
                year, quarter = now.year, q_now - 1

        ql = f"{year}-Q{quarter}"
        summary = await stats_tracker.quarterly_summary(year, quarter)

        console.print(f"\n[bold]BIBIVIDI Quarterly Digest — {ql}[/]\n")
        console.print(f"  Calls          : {summary['calls']:,}")
        console.print(f"  Tokens saved   : {summary['tokens_saved']:,}")
        console.print(f"  Gross savings  : €{summary['savings_eur']:.4f}")
        console.print(f"  BIBIVIDI share : €{summary['bibividi_share_eur']:.4f}")
        net = summary["savings_eur"] - summary["bibividi_share_eur"]
        console.print(f"  Your net gain  : [bold green]€{net:.4f}[/]")
        console.print(f"  CO₂ avoided    : {summary['co2_grams']:.1f} gCO₂eq\n")

        if dry_run:
            email = build_quarterly_email(summary, ql)
            console.print("[dim][DRY RUN] Email subject:[/]")
            console.print(email.subject)
            console.print("\n[dim][DRY RUN] Email text body:[/]")
            console.print(email.text)
            return

        recipient = to_email or _settings.mailer_to
        if not recipient:
            console.print(
                "[yellow]No recipient configured.[/] "
                "Set BIBIVIDI_MAILER_TO in .env or use --to email@example.com"
            )
            return

        if not _settings.mailer_api_key:
            email = build_quarterly_email(summary, ql)
            console.print("[yellow]No BIBIVIDI_MAILER_API_KEY set — printing digest:[/]\n")
            console.print(email.text)
            return

        console.print(f"[dim]Sending quarterly digest to {recipient}…[/]")
        ok = await send_quarterly_email(recipient, summary, ql)
        if ok:
            console.print(f"[bold green]Quarterly digest sent[/] to {recipient}")
        else:
            console.print("[red]Email delivery failed.[/] Check logs for details.")
            sys.exit(1)

    asyncio.run(_run())


# ── simulate ──────────────────────────────────────────────────────────────────

@main.command()
@click.argument("requests_file", type=click.Path(exists=True))
@click.option("--threshold", default=None, type=float,
              help="Override match threshold for this simulation")
def simulate(requests_file: str, threshold: float | None) -> None:
    """Simulate BIBIVIDI against a request log file (no API calls, no DB writes).

    REQUESTS_FILE must be a JSON array of AI API request objects, each with a
    ``messages`` field (Anthropic/OpenAI format) or a ``prompt`` field.

    Reports per-request match results and aggregate token savings.

    Example format:

    \b
        [
          {"messages": [{"role": "user", "content": "Summarise this contract…"}]},
          {"messages": [{"role": "user", "content": "Translate to French…"}]}
        ]
    """
    import json
    from pathlib import Path

    async def _run() -> None:
        from rich.panel import Panel

        from bvd.config import settings
        from bvd.core.proxy import _estimate_tokens, _extract_prompt
        from bvd.engine.library import library
        from bvd.engine.matcher import Matcher
        from bvd.engine.runner import run_script

        await library.init()

        scripts = await library.all_with_embeddings()
        if not scripts:
            console.print("[yellow]No scripts with embeddings.[/] Add skills first.")
            return

        raw = Path(requests_file).read_text()
        try:
            requests = json.loads(raw)
        except json.JSONDecodeError as exc:
            console.print(f"[red]Invalid JSON:[/] {exc}")
            sys.exit(1)

        if not isinstance(requests, list):
            console.print("[red]File must be a JSON array of request objects.[/]")
            sys.exit(1)

        effective_threshold = threshold if threshold is not None else settings.match_threshold
        mtc = Matcher()

        # ── Simulate each request ──────────────────────────────────────────
        results: list[dict] = []
        total_before = 0
        total_after = 0

        console.print(
            f"\n[dim]Simulating {len(requests)} request(s) "
            f"(threshold={effective_threshold})…[/]\n"
        )

        for i, req in enumerate(requests):
            prompt = _extract_prompt(req) if isinstance(req, dict) else ""
            if not prompt:
                results.append({
                    "index": i, "prompt": "", "matched": False,
                    "skill_id": None, "score": None,
                    "tokens_before": 0, "tokens_after": 0, "saving_pct": 0.0,
                })
                continue

            tokens_before = _estimate_tokens(prompt)
            total_before += tokens_before

            match = await mtc.match(prompt)
            if match and match.score >= effective_threshold and match.script.status == "active":
                result = await run_script(match.script, prompt)
                tokens_after = _estimate_tokens(result.output)
                saving_pct = (1 - tokens_after / tokens_before) * 100 if tokens_before else 0.0
                results.append({
                    "index": i,
                    "prompt": prompt[:55],
                    "matched": True,
                    "skill_id": match.script_id,
                    "score": match.score,
                    "tokens_before": tokens_before,
                    "tokens_after": tokens_after,
                    "saving_pct": saving_pct,
                })
                total_after += tokens_after
            else:
                total_after += tokens_before
                results.append({
                    "index": i,
                    "prompt": prompt[:55],
                    "matched": False,
                    "skill_id": match.script_id if match else None,
                    "score": match.score if match else None,
                    "tokens_before": tokens_before,
                    "tokens_after": tokens_before,
                    "saving_pct": 0.0,
                })

        # ── Results table ──────────────────────────────────────────────────
        table = Table(title=f"Simulation — {Path(requests_file).name}")
        table.add_column("#", width=4, justify="right")
        table.add_column("Prompt", max_width=56)
        table.add_column("Match?", justify="center", width=7)
        table.add_column("Skill", style="magenta", max_width=26)
        table.add_column("Score", justify="right", width=6)
        table.add_column("Before", justify="right", width=7)
        table.add_column("After", justify="right", width=7)
        table.add_column("Saving", justify="right", style="green", width=8)

        for r in results:
            saving_str = f"{r['saving_pct']:.0f}%" if r["matched"] else "[dim]0%[/]"
            table.add_row(
                str(r["index"] + 1),
                (r["prompt"] + "…") if len(r["prompt"]) == 55 else r["prompt"],
                "[green]✓[/]" if r["matched"] else "[dim]—[/]",
                r["skill_id"] or "[dim]—[/]",
                f"{r['score']:.2f}" if r["score"] is not None else "",
                str(r["tokens_before"]) if r["tokens_before"] else "",
                str(r["tokens_after"]) if r["matched"] else "",
                saving_str,
            )
        console.print(table)

        # ── Summary ────────────────────────────────────────────────────────
        matched_count = sum(1 for r in results if r["matched"])
        total_saved = total_before - total_after
        saving_ratio = (1 - total_after / total_before) * 100 if total_before else 0.0
        usd_saved = total_saved * 0.000001
        co2_g = total_saved * 0.003

        console.print(
            Panel.fit(
                f"Requests: [bold]{len(requests)}[/]   "
                f"Matched: [bold green]{matched_count}[/]   "
                f"Tokens saved: [bold green]{total_saved:,}[/]   "
                f"Saving: [bold green]{saving_ratio:.0f}%[/]   "
                f"Est. cost saved: [bold green]${usd_saved:.4f}[/]   "
                f"CO₂: [bold green]{co2_g:.1f} gCO₂eq[/]",
                title="Summary",
            )
        )

        # ── Quota mode output (TODO-26) ────────────────────────────────────
        from bvd.billing import free_tier_daily_tokens, is_free_tier_model

        model_names = {
            req.get("model", "") for req in requests
            if isinstance(req, dict) and req.get("model")
        }
        quota_mode = (
            settings.quota_daily_tokens is not None
            or settings.allow_local_apis
            or any(is_free_tier_model(m) for m in model_names)
        )
        if quota_mode and total_saved > 0:
            avg_prompt = total_before // len(requests) if requests else 1_000
            extra_requests = total_saved // max(avg_prompt, 1)

            daily_limit = settings.quota_daily_tokens
            if daily_limit is None:
                for m in model_names:
                    daily_limit = free_tier_daily_tokens(m)
                    if daily_limit:
                        break

            console.print(
                f"\n  [bold cyan]● Quota mode:[/] BIBIVIDI would give you "
                f"[bold green]+{extra_requests:,}[/] extra requests/day "
                f"within your free quota"
            )
            if daily_limit:
                relief_pct = total_saved / daily_limit * 100
                console.print(
                    f"  [dim](saves {relief_pct:.1f}% of your {daily_limit:,} token/day limit — "
                    "estimate based on avg prompt size in provided logs)[/]"
                )

    asyncio.run(_run())


# ── calibrate ─────────────────────────────────────────────────────────────────

@main.command()
@click.argument("prompts_file", type=click.Path(exists=True), required=False)
@click.option("--stdin", "from_stdin", is_flag=True, default=False,
              help="Read prompts from stdin (one per line)")
def calibrate(prompts_file: str | None, from_stdin: bool) -> None:
    """Show match scores for sample prompts and suggest a threshold.

    Reads prompts from PROMPTS_FILE (one per line) or from stdin (--stdin).
    Falls back to ~/.bibividi/test_prompts.json if neither is provided.

    Outputs a table of best match scores per prompt and recommends an
    optimal BIBIVIDI_MATCH_THRESHOLD value based on the score distribution.
    """
    import json
    from pathlib import Path

    async def _run() -> None:
        from bvd.config import settings
        from bvd.engine.library import library
        from bvd.engine.matcher import Matcher

        await library.init()
        scripts = await library.all_with_embeddings()

        if not scripts:
            console.print("[yellow]No scripts with embeddings in library.[/]")
            console.print("Add scripts with [bold]bibividi add <pack>[/] first.")
            return

        # ── Load prompts ───────────────────────────────────────────────────
        prompts: list[str] = []

        if from_stdin or (not prompts_file and sys.stdin.isatty() is False):
            raw = sys.stdin.read()
            prompts = [p.strip() for p in raw.splitlines() if p.strip()]
        elif prompts_file:
            path = Path(prompts_file)
            content = path.read_text()
            if path.suffix == ".json":
                data = json.loads(content)
                if isinstance(data, list):
                    prompts = [str(p) for p in data if p]
                else:
                    console.print("[red]JSON file must be an array of strings.[/]")
                    sys.exit(1)
            else:
                prompts = [p.strip() for p in content.splitlines() if p.strip()]
        else:
            # Fall back to ~/.bibividi/test_prompts.json
            default = settings.data_dir / "test_prompts.json"
            if default.exists():
                data = json.loads(default.read_text())
                prompts = [str(p) for p in data if p]
            else:
                console.print(
                    "[yellow]No prompts source.[/] Provide a file, --stdin, "
                    f"or create {default}"
                )
                sys.exit(1)

        if not prompts:
            console.print("[yellow]No prompts found.[/]")
            sys.exit(1)

        # ── Score all prompts ──────────────────────────────────────────────
        console.print(f"\n[dim]Scoring {len(prompts)} prompt(s) against "
                      f"{len(scripts)} skill(s)…[/]\n")

        mtc = Matcher()
        scores: list[tuple[str, str | None, float]] = []  # (prompt_short, skill_id, score)

        for prompt in prompts:
            result = await mtc.match(prompt)
            best_score = result.score if result else 0.0
            best_skill = result.script_id if result else None
            scores.append((prompt[:60], best_skill, best_score))

        # ── Results table ──────────────────────────────────────────────────
        current_threshold = settings.match_threshold
        table = Table(title="BIBIVIDI — Calibration Results")
        table.add_column("Prompt", max_width=55)
        table.add_column("Best skill", style="magenta", max_width=28)
        table.add_column("Score", justify="right", width=7)
        table.add_column("Match?", justify="center", width=8)

        for prompt_short, skill_id, score in scores:
            matched = score >= current_threshold
            score_style = "green" if matched else ("yellow" if score >= 0.5 else "red")
            table.add_row(
                prompt_short + ("…" if len(prompt_short) == 60 else ""),
                skill_id or "[dim]—[/]",
                f"[{score_style}]{score:.3f}[/]",
                "[green]✓[/]" if matched else "[dim]—[/]",
            )

        console.print(table)

        # ── Threshold suggestion ───────────────────────────────────────────
        all_scores = [s for _, _, s in scores]
        if all_scores:
            avg = sum(all_scores) / len(all_scores)
            max_score = max(all_scores)
            min_score = min(all_scores)
            matched_count = sum(1 for s in all_scores if s >= current_threshold)

            console.print("\n[bold]Score summary:[/]")
            console.print(f"  Min   : {min_score:.3f}")
            console.print(f"  Avg   : {avg:.3f}")
            console.print(f"  Max   : {max_score:.3f}")
            console.print(
                f"  Matches at current threshold ({current_threshold}): "
                f"{matched_count}/{len(all_scores)}"
            )

            # Suggest threshold: midpoint between avg and max, rounded to 2dp
            suggested = round((avg + max_score) / 2, 2)
            suggested = max(0.50, min(0.95, suggested))
            console.print(
                f"\n[bold]Suggested threshold:[/] [cyan]{suggested}[/]"
            )
            if suggested != current_threshold:
                console.print(
                    f"[dim]Set it with: BIBIVIDI_MATCH_THRESHOLD={suggested} "
                    f"in your .env file[/]"
                )
            else:
                console.print("[dim]Your current threshold looks well-calibrated.[/]")

    asyncio.run(_run())


# ── feedback ──────────────────────────────────────────────────────────────────

@main.group()
def feedback() -> None:
    """Trust feedback commands (flag bad skill outputs)."""


@feedback.command(name="bad")
@click.argument("request_id")
def feedback_bad(request_id: str) -> None:
    """Flag a request where BIBIVIDI produced wrong output.

    REQUEST_ID is the value of the X-BIBIVIDI-Request-ID response header
    returned by the proxy, or shown in 'bibividi tail' output.
    """
    async def _run() -> None:
        from bvd.engine.stats import stats_tracker

        skill_id = await stats_tracker.flag_bad(request_id)
        if skill_id is None:
            console.print(f"[red]Not found:[/] request ID '{request_id}'")
            console.print(
                "[dim]Tip: check the [bold]X-BIBIVIDI-Request-ID[/] response header "
                "or run [bold]bibividi tail[/] while sending requests.[/]"
            )
            sys.exit(1)

        console.print(
            f"[bold yellow]Flagged:[/] skill [cyan]{skill_id}[/] "
            f"for request [dim]{request_id}[/]"
        )
        console.print("[dim]Run 'bibividi feedback list' to see all flagged requests.[/]")

    asyncio.run(_run())


@feedback.command(name="list")
def feedback_list() -> None:
    """List all requests flagged as bad, grouped by skill."""
    async def _run() -> None:
        from bvd.engine.stats import stats_tracker

        flagged = await stats_tracker.list_flagged()

        if not flagged:
            console.print("[green]No flagged requests.[/]")
            return

        table = Table(title=f"Flagged Requests ({len(flagged)})")
        table.add_column("Request ID", style="dim", max_width=36)
        table.add_column("Skill", style="cyan")
        table.add_column("Date", style="dim", width=19)
        table.add_column("Before", justify="right", width=7)
        table.add_column("After", justify="right", width=7)

        for r in flagged:
            table.add_row(
                r.get("request_id") or "—",
                r["skill_id"] or "—",
                (r.get("executed_at") or "")[:19],
                str(r.get("tokens_before", "")),
                str(r.get("tokens_after", "")),
            )

        console.print(table)
        console.print(
            "\n[dim]Use [bold]bibividi review[/] to inspect and re-approve"
            " auto-generated skills.[/]"
        )

    asyncio.run(_run())


# ── chain ─────────────────────────────────────────────────────────────────────

@main.group()
def chain() -> None:
    """BSS 1.1 skill chain commands."""


@chain.command(name="build")
@click.argument("skill_ids", nargs=-1, required=True)
@click.option("--name", "chain_name", default=None, help="Chain display name")
@click.option("--out", "output_dir", default=None, type=click.Path(),
              help="Output directory (default: ./chains/<id>)")
@click.option("--run", "run_sample", is_flag=True, default=False,
              help="Run the chain on sample input from stdin after scaffolding")
def chain_build(
    skill_ids: tuple[str, ...],
    chain_name: str | None,
    output_dir: str | None,
    run_sample: bool,
) -> None:
    """Scaffold a BSS 1.1 chain pack from installed skills.

    SKILL_IDS is an ordered list of installed skill IDs — the chain steps.

    Example:

    \b
        bibividi chain build diff-parser complexity-scorer review-formatter \\
            --name "My Review Chain" --run < sample.diff
    """
    import hashlib
    import json as _json
    from pathlib import Path

    async def _run() -> None:
        from bvd.engine.library import library
        from bvd.engine.runner import run_chain

        await library.init()

        # ── Validate all dep skills are installed ─────────────────────────────
        steps = []
        for sid in skill_ids:
            script = await library.get(sid)
            if script is None:
                console.print(f"[red]Skill not found:[/] '{sid}'")
                console.print("Install it first with [bold]bibividi add <pack>[/]")
                return
            steps.append(script)

        console.print(
            f"\n[bold]Chain steps ({len(steps)}):[/] "
            + " → ".join(f"[cyan]{s.id}[/]" for s in steps)
        )

        # ── Build chain ID and name ───────────────────────────────────────────
        chain_id = "-".join(sid.replace("-skill", "") for sid in skill_ids) + "-chain"
        display_name = chain_name or " → ".join(s.name for s in steps)

        # ── Resolve output directory ──────────────────────────────────────────
        out_path = Path(output_dir) if output_dir else Path("chains") / chain_id
        out_path.mkdir(parents=True, exist_ok=True)

        # ── Write skill.py (empty — no finalizer) ────────────────────────────
        skill_py_content = ""
        skill_py_path = out_path / "skill.py"
        skill_py_path.write_text(skill_py_content)
        py_hash = hashlib.sha256(skill_py_content.encode()).hexdigest()

        # ── Write skill.json ──────────────────────────────────────────────────
        skill_json = {
            "bss_version": "1.1",
            "id": chain_id,
            "name": display_name,
            "description": (
                "BSS 1.1 chain: "
                + " → ".join(s.name for s in steps)
                + "."
            ),
            "version": "1.0.0",
            "author": "",
            "chain": list(skill_ids),
            "chain_mode": "sequential",
            "inputs_type": "plain_text",
            "outputs_type": "plain_text",
            "tags": ["chain", "workflow"],
            "entry_point": "skill.py",
            "hash": py_hash,
        }
        (out_path / "skill.json").write_text(
            _json.dumps(skill_json, indent=2, ensure_ascii=False)
        )

        console.print(f"\n[bold green]Chain pack created:[/] {out_path}/")
        console.print(f"  [dim]skill.json — id:[/] [cyan]{chain_id}[/]")
        console.print("  [dim]skill.py   — no finalizer (add code to skill.py if needed)[/]")
        console.print(f"\n[dim]Install it with:[/] [bold]bibividi add {out_path}[/]")

        # ── Optional: run on stdin sample ─────────────────────────────────────
        if run_sample:
            console.print("\n[dim]Reading sample input from stdin…[/]")
            sample = sys.stdin.read()
            if not sample.strip():
                console.print("[yellow]stdin is empty — skipping run.[/]")
                return

            from bvd.config import settings as _settings
            from bvd.engine.library import Script

            if not _settings.enable_chains:
                console.print(
                    "[yellow]BIBIVIDI_ENABLE_CHAINS is not set — set it to 'true' to run chains.[/]"
                )
                return

            console.print(f"[dim]Running {len(steps)}-step chain on {len(sample)} chars…[/]\n")

            # Build a temporary chain script
            chain_script = Script(
                id=chain_id, name=display_name, description="",
                code="", chain_ids=list(skill_ids),
            )
            result = await run_chain(chain_script, sample, steps)  # type: ignore[arg-type]

            # Token waterfall
            from bvd.core.proxy import _estimate_tokens
            table = Table(title="Token Waterfall")
            table.add_column("Step", style="cyan")
            table.add_column("Tokens in", justify="right")
            table.add_column("Tokens out", justify="right")
            table.add_column("Reduction", justify="right", style="green")

            current_text = sample
            for i, (step, out_text) in enumerate(
                zip(steps, result.step_outputs or [])
            ):
                t_in = _estimate_tokens(current_text)
                t_out = _estimate_tokens(out_text)
                pct = (1 - t_out / t_in) * 100 if t_in else 0
                table.add_row(step.id, str(t_in), str(t_out), f"{pct:.0f}%")
                current_text = out_text

            total_in = _estimate_tokens(sample)
            total_out = _estimate_tokens(result.output)
            total_pct = (1 - total_out / total_in) * 100 if total_in else 0
            table.add_row(
                "[bold]TOTAL[/]", str(total_in), str(total_out),
                f"[bold green]{total_pct:.0f}%[/]",
            )
            console.print(table)

            console.print("\n[bold]Chain output:[/]")
            console.print(result.output[:600] + ("…" if len(result.output) > 600 else ""))

    asyncio.run(_run())


# ── cert ──────────────────────────────────────────────────────────────────────

@main.group()
def cert() -> None:
    """Manage BIBIVIDI TLS certificates."""


@cert.command(name="generate")
def cert_generate() -> None:
    """Generate the local CA and localhost server certificate."""
    from bvd.core.certs import (
        _load_ca,
        ca_cert_path,
        ca_exists,
        generate_ca,
        generate_server_cert,
        server_cert_exists,
    )

    if ca_exists():
        console.print(f"[yellow]CA already exists:[/] {ca_cert_path()}")
        console.print("Delete it manually to regenerate.")
        ca_cert, ca_key = _load_ca()
    else:
        console.print("[dim]Generating CA…[/]")
        ca_cert, ca_key = generate_ca()
        console.print(f"[bold green]CA created:[/] {ca_cert_path()}")

    if server_cert_exists():
        from bvd.core.certs import server_cert_path
        console.print(f"[yellow]Server cert already exists:[/] {server_cert_path()}")
    else:
        console.print("[dim]Generating server certificate…[/]")
        generate_server_cert(ca_cert, ca_key)
        from bvd.core.certs import server_cert_path
        console.print(f"[bold green]Server cert created:[/] {server_cert_path()}")

    console.print("\n[dim]Run 'bibividi cert install' to trust this CA system-wide.[/]")


@cert.command(name="install")
def cert_install() -> None:
    """Install the BIBIVIDI CA in the OS trust store (may require sudo)."""
    from bvd.core.certs import ca_exists, install_ca_system

    if not ca_exists():
        console.print("[red]No CA found.[/] Run 'bibividi cert generate' first.")
        sys.exit(1)

    try:
        method = install_ca_system()
        console.print(f"[bold green]CA installed:[/] {method}")
        console.print("[dim]You may need to restart your browser / SDK client.[/]")
    except Exception as exc:
        console.print(f"[red]Install failed:[/] {exc}")
        console.print("[dim]Try running with sudo.[/]")
        sys.exit(1)


@cert.command(name="path")
def cert_path() -> None:
    """Show the paths of all certificate files."""
    from bvd.core.certs import (
        ca_cert_path,
        ca_key_path,
        server_cert_path,
        server_key_path,
    )

    table = Table(title="BIBIVIDI Certificate Paths")
    table.add_column("File", style="cyan")
    table.add_column("Path")
    table.add_column("Exists", justify="center")

    for label, path_fn in [
        ("CA cert", ca_cert_path),
        ("CA key", ca_key_path),
        ("Server cert", server_cert_path),
        ("Server key", server_key_path),
    ]:
        p = path_fn()
        exists = "[green]✓[/]" if p.exists() else "[red]✗[/]"
        table.add_row(label, str(p), exists)

    console.print(table)
    console.print()
    console.print(
        "[dim]To trust the CA: 'bibividi cert install'  "
        "or manually add the CA cert to your trust store.[/]"
    )


# ── demo ──────────────────────────────────────────────────────────────────────

_DEMO_PROMPT = """\
Extract and summarize the key legal clauses from this contract document.

SERVICE AGREEMENT

This Service Agreement ("Agreement") is entered into as of January 1, 2026, between
Acme Corp ("Client") and Ropart Interactive ("Provider").

1. PAYMENT TERMS
   Client agrees to pay Provider a monthly fee of $5,000 USD. Invoices are due within
   30 days. Late payments incur a 1.5% monthly fee.

2. TERMINATION
   Either party may terminate this Agreement with 30 days written notice. Provider may
   terminate immediately for non-payment exceeding 60 days.

3. CONFIDENTIALITY
   Both parties agree to keep proprietary information confidential for 3 years after
   termination. This NDA covers all technical specifications and business data.

4. LIABILITY
   Provider's liability is limited to fees paid in the prior 3 months. Neither party
   is liable for indirect or consequential damages.

5. INTELLECTUAL PROPERTY
   All work product created under this Agreement is owned by Client. Provider retains
   rights to pre-existing IP and general methodologies.

6. GOVERNING LAW
   This Agreement is governed by the laws of France. Disputes shall be resolved by
   arbitration in Paris.

7. FORCE MAJEURE
   Neither party is liable for delays caused by circumstances beyond reasonable control,
   including natural disasters, government actions, or infrastructure failures.

8. WARRANTY
   Provider warrants that services will be performed in a professional manner consistent
   with industry standards. Client warrants timely access to required resources.

Please identify all important clauses and explain their business implications in detail,
with references to case law where applicable, and provide a risk assessment for each
clause from both the client and provider perspective.
"""

_DEMO_SKILL_PATH = "scripts/pdf-clause-extractor"


@main.command()
def demo() -> None:
    """Run an in-process demo showing token savings on a sample contract."""
    from pathlib import Path

    async def _run() -> None:
        import tempfile

        from rich.panel import Panel
        from rich.progress import Progress, SpinnerColumn, TextColumn

        from bvd.engine.library import Library
        from bvd.engine.matcher import Matcher
        from bvd.engine.runner import run_script

        console.print(
            Panel.fit(
                "[bold cyan]BIBIVIDI[/] — Token optimization demo",
                subtitle="pdf-clause-extractor skill",
            )
        )

        # ── Setup in-process library ───────────────────────────────────────
        with Progress(SpinnerColumn(), TextColumn("{task.description}"),
                      console=console, transient=True) as prog:

            t = prog.add_task("Loading embedding model…")

            pack = Path(_DEMO_SKILL_PATH)
            if not pack.exists():
                console.print(f"[red]Demo pack not found:[/] {pack}")
                console.print("Run from the repo root directory.")
                return

            import json as _json
            skill_json = _json.loads((pack / "skill.json").read_text())
            skill_py = (pack / "skill.py").read_text()

            with tempfile.TemporaryDirectory() as tmp:
                from unittest.mock import patch
                with patch("bvd.config.settings.db_path", Path(tmp) / "demo.db"):
                    lib = Library()
                    await lib.init()
                    script = await lib.import_bss(skill_json, skill_py)

                    mtc = Matcher()
                    prog.update(t, description="Computing embeddings…")
                    script = await mtc.embed_script(script)
                    await lib.save(script)

                    prog.update(t, description="Matching prompt…")
                    import numpy as np

                    # In practice, users send a short instruction as the prompt;
                    # the document is injected by their code. Use the first sentence
                    # (the instruction) for the similarity score shown in the demo.
                    instruction = _DEMO_PROMPT.split("\n")[0]
                    model = mtc._get_model()  # type: ignore[attr-defined]
                    loop = asyncio.get_event_loop()
                    instr_vec = await loop.run_in_executor(
                        None, lambda: model.encode([instruction])[0]
                    )
                    assert script.embedding is not None
                    skill_vec = np.frombuffer(script.embedding, dtype=np.float32)
                    norm = np.linalg.norm(instr_vec) * np.linalg.norm(skill_vec)
                    score = float(np.dot(instr_vec, skill_vec) / norm) if norm > 0 else 0.0

                    prog.update(t, description="Running script…")
                    result = await run_script(script, _DEMO_PROMPT)

        # ── Results ────────────────────────────────────────────────────────
        try:
            import tiktoken
            enc = tiktoken.get_encoding("cl100k_base")
            tokens_before = len(enc.encode(_DEMO_PROMPT))
            tokens_after = len(enc.encode(result.output))
        except Exception:
            tokens_before = len(_DEMO_PROMPT) // 4
            tokens_after = len(result.output) // 4

        saving_pct = (1 - tokens_after / tokens_before) * 100

        t_table = Table(show_header=False, box=None, padding=(0, 2))
        t_table.add_row("Original prompt", f"[yellow]{tokens_before:,}[/] tokens")
        t_table.add_row("After BIBIVIDI", f"[green]{tokens_after:,}[/] tokens")
        t_table.add_row("Saving", f"[bold green]{saving_pct:.0f}%[/]")
        from bvd.config import settings as _settings
        threshold = _settings.match_threshold
        match_str = (
            f"[bold green]{score:.3f}[/] ✓ (≥ {threshold})"
            if score >= threshold
            else f"[yellow]{score:.3f}[/] (threshold: {threshold})"
        )
        t_table.add_row("Match score", match_str)
        console.print(t_table)

        console.print("\n[bold]Condensed output sent to the AI:[/]")
        console.print(
            Panel(result.output[:800] + ("…" if len(result.output) > 800 else ""),
                  border_style="green")
        )
        console.print(
            "\n[dim]Start the proxy:[/] [bold]bibividi start[/]\n"
            "[dim]Add this skill:[/] [bold]bibividi add scripts/pdf-clause-extractor/[/]\n"
        )

    asyncio.run(_run())


# ── roi ───────────────────────────────────────────────────────────────────────

@main.command()
def roi() -> None:
    """Open the interactive ROI calculator in your browser."""
    import webbrowser

    from bvd.config import settings

    url = f"http://{settings.proxy_host}:{settings.proxy_port}/bibividi/roi"
    console.print(f"[bold cyan]Opening ROI calculator:[/] {url}")
    console.print("[dim]The proxy must be running — start it with:[/] bibividi start")
    webbrowser.open(url)


# ── report (CSRD) ─────────────────────────────────────────────────────────────

@main.command()
@click.option("--month", "month_str", default=None, metavar="YYYY-MM",
              help="Month to report (default: current month)")
@click.option("--format", "fmt", default="text",
              type=click.Choice(["text", "csv", "html"], case_sensitive=False),
              help="Output format: text (default), csv, html")
@click.option("--out", "output_file", default=None, type=click.Path(),
              help="Write report to file instead of stdout")
def report(month_str: str | None, fmt: str, output_file: str | None) -> None:
    """Generate a CSRD AI Scope 3 emissions report.

    Produces a structured report of AI API token savings and their CO₂
    equivalent for the given month — suitable for CSRD Scope 3 filings.

    Examples:

    \b
        bibividi report --month 2026-03
        bibividi report --month 2026-03 --format csv --out march.csv
        bibividi report --month 2026-03 --format html --out march.html
    """
    from datetime import datetime as _dt

    async def _run() -> None:
        from pathlib import Path

        from bvd.engine.library import library
        from bvd.reports import build_csrd_report, to_csv, to_html

        await library.init()

        if month_str:
            try:
                d = _dt.strptime(month_str, "%Y-%m")
                year, month = d.year, d.month
            except ValueError:
                console.print("[red]Invalid month format — use YYYY-MM (e.g. 2026-03)[/]")
                sys.exit(1)
        else:
            now = _dt.now()
            year, month = now.year, now.month

        r = await build_csrd_report(year, month)

        if fmt == "csv":
            content = to_csv(r)
            if output_file:
                Path(output_file).write_text(content, encoding="utf-8")
                console.print(f"[bold green]CSV report saved:[/] {output_file}")
            else:
                console.print(content)
            return

        if fmt == "html":
            content = to_html(r)
            if output_file:
                Path(output_file).write_text(content, encoding="utf-8")
                console.print(f"[bold green]HTML report saved:[/] {output_file}")
            else:
                console.print(content)
            return

        # ── Text (rich) output ───────────────────────────────────────────────
        from rich.panel import Panel

        def _fmt_g(g: float) -> str:
            if g >= 1_000_000:
                return f"{g / 1_000_000:.3f} tCO₂eq"
            if g >= 1000:
                return f"{g / 1000:.3f} kgCO₂eq"
            return f"{g:.3f} gCO₂eq"

        console.print(f"\n[bold]BIBIVIDI CSRD Report — {r.period_label}[/]")
        console.print("[dim]AI Scope 3 Category 11 (Use of Sold Products)[/]\n")

        table = Table(show_header=False, box=None, padding=(0, 2))
        table.add_row("Requests intercepted", f"{r.executions:,}")
        table.add_row("Tokens processed",     f"{r.tokens_in:,}")
        table.add_row("Tokens saved",         f"[green]{r.tokens_saved:,}[/]")
        table.add_row("Reduction ratio",      f"[green]{r.saving_ratio * 100:.1f}%[/]")
        table.add_row("CO₂eq baseline",       _fmt_g(r.co2_baseline_g))
        table.add_row("CO₂eq avoided",        f"[bold green]{_fmt_g(r.co2_saved_g)}[/]")
        table.add_row("CO₂eq net",            _fmt_g(r.co2_net_g))
        table.add_row("Gross savings",        f"€{r.savings_eur:.4f}")
        console.print(table)

        console.print(
            Panel.fit(
                f"[bold green]{_fmt_g(r.co2_saved_g)}[/] of CO₂eq avoided in {r.period_label}.\n"
                "[dim]Export for CSRD filing:[/] "
                f"[bold]bibividi report --month {r.period_label} --format csv[/]",
                title="Scope 3 Summary",
            )
        )
        console.print(f"\n[dim]{r.methodology}[/]\n")
        console.print(f"[dim italic]{r.disclaimer}[/]\n")

    asyncio.run(_run())


# ── billing ───────────────────────────────────────────────────────────────────

@main.group()
def billing() -> None:
    """Savings-share billing commands."""


@billing.command(name="summary")
@click.option("--month", "month_str", default=None, metavar="YYYY-MM",
              help="Month to summarise (default: current month)")
@click.option("--tier", default=None,
              help="Billing tier: free|pro|growth|enterprise (default: BIBIVIDI_BILLING_TIER)")
def billing_summary(month_str: str | None, tier: str | None) -> None:
    """Show the billing calculation for a month — no charges applied.

    Reads token savings from the executions table and applies the tier pricing
    formula: bibividi_share = min(savings_eur × rate, monthly_cap).

    Example:

    \b
        bibividi billing summary --month 2026-03 --tier pro
    """
    from datetime import datetime as _dt

    async def _run() -> None:
        from rich.panel import Panel

        from bvd.billing import monthly_billing
        from bvd.engine.library import library

        await library.init()

        if month_str:
            try:
                d = _dt.strptime(month_str, "%Y-%m")
                year, month = d.year, d.month
            except ValueError:
                console.print("[red]Invalid month format — use YYYY-MM (e.g. 2026-03)[/]")
                sys.exit(1)
        else:
            now = _dt.now()
            year, month = now.year, now.month

        result = await monthly_billing(year, month, tier)

        console.print(f"\n[bold]BIBIVIDI Billing — {result.year}-{result.month:02d}[/]\n")

        table = Table(show_header=False, box=None, padding=(0, 2))
        rate_label = f"({result.tier_rate * 100:.0f}% of savings)"
        table.add_row("Tier", f"[cyan]{result.tier}[/] {rate_label}")
        table.add_row("Executions",    str(result.executions))
        table.add_row("Tokens saved",  f"{result.tokens_saved:,}")
        table.add_row("Gross savings", f"€{result.savings_eur:.4f}")
        cap_note = " [dim](cap applied)[/]" if result.capped else ""
        share_str = f"[bold green]€{result.bibividi_share_eur:.4f}[/]{cap_note}"
        table.add_row("BIBIVIDI share", share_str)
        console.print(table)

        if result.executions == 0:
            console.print("\n[yellow]No executions recorded for this month.[/]")
            console.print("[dim]Run 'bibividi status' to check the library.[/]")
        else:
            console.print(
                Panel.fit(
                    f"€{result.bibividi_share_eur:.2f} would be charged to your account "
                    f"for {result.year}-{result.month:02d}.\n"
                    f"[dim]Run 'bibividi billing run' to create a Stripe invoice.[/]",
                    title="Summary",
                )
            )

    asyncio.run(_run())


@billing.command(name="run")
@click.option("--month", "month_str", default=None, metavar="YYYY-MM",
              help="Month to bill (default: previous month)")
@click.option("--tier", default=None,
              help="Billing tier: free|pro|growth|enterprise (default: BIBIVIDI_BILLING_TIER)")
@click.option("--dry-run", "dry_run", is_flag=True, default=False,
              help="Compute and print the bill without creating a Stripe invoice")
def billing_run(month_str: str | None, tier: str | None, dry_run: bool) -> None:
    """Compute monthly bill and create a Stripe invoice item.

    Defaults to the previous month (the standard monthly billing cycle).
    Requires BIBIVIDI_STRIPE_API_KEY to be set for live Stripe billing.

    Example:

    \b
        bibividi billing run --month 2026-02 --tier pro
        bibividi billing run --dry-run
    """
    from datetime import datetime as _dt

    async def _run() -> None:
        from bvd.billing import monthly_billing, run_stripe_billing
        from bvd.config import settings as _settings
        from bvd.engine.library import library

        await library.init()

        if month_str:
            try:
                d = _dt.strptime(month_str, "%Y-%m")
                year, month = d.year, d.month
            except ValueError:
                console.print("[red]Invalid month format — use YYYY-MM (e.g. 2026-02)[/]")
                sys.exit(1)
        else:
            # Default to previous month
            now = _dt.now()
            if now.month == 1:
                year, month = now.year - 1, 12
            else:
                year, month = now.year, now.month - 1

        result = await monthly_billing(year, month, tier)

        console.print(f"\n[bold]BIBIVIDI Billing Run — {result.year}-{result.month:02d}[/]\n")
        console.print(f"  Tier          : {result.tier} ({result.tier_rate * 100:.0f}%)")
        console.print(f"  Executions    : {result.executions}")
        console.print(f"  Tokens saved  : {result.tokens_saved:,}")
        console.print(f"  Gross savings : €{result.savings_eur:.4f}")
        cap_note = " (cap applied)" if result.capped else ""
        console.print(
            f"  BIBIVIDI share: [bold green]€{result.bibividi_share_eur:.4f}[/]{cap_note}\n"
        )

        if dry_run:
            console.print("[dim][DRY RUN] No Stripe invoice created.[/]")
            return

        if not _settings.stripe_api_key:
            console.print(
                "[yellow]BIBIVIDI_STRIPE_API_KEY not set — Stripe billing skipped.[/]\n"
                "[dim]Set it in your .env file to enable live billing.[/]"
            )
            return

        console.print("[dim]Creating Stripe invoice item…[/]")
        ok = await run_stripe_billing(result)
        if ok:
            console.print(
                f"[bold green]Stripe invoice item created:[/] €{result.bibividi_share_eur:.2f}"
            )
        else:
            console.print("[red]Stripe billing failed.[/] Check logs for details.")
            sys.exit(1)

    asyncio.run(_run())


# ── marketplace ───────────────────────────────────────────────────────────────

@main.group()
def marketplace() -> None:
    """Marketplace payout commands."""


@marketplace.command("payouts")
@click.option("--limit", default=50, show_default=True, help="Max rows to display")
def marketplace_payouts(limit: int) -> None:
    """List pending marketplace payout rows."""

    async def _run() -> None:
        from bvd.marketplace.payouts import list_pending_payouts

        rows = await list_pending_payouts(limit=limit)
        if not rows:
            console.print("[dim]No pending payouts.[/]")
            return

        table = Table(title=f"Pending Payouts ({len(rows)})")
        table.add_column("ID", style="dim", justify="right")
        table.add_column("exec_id", justify="right")
        table.add_column("Skill")
        table.add_column("Author ID")
        table.add_column("Role")
        table.add_column("Weight", justify="right")
        table.add_column("Gross (€)", justify="right")
        table.add_column("Share (€)", justify="right")
        table.add_column("Created")

        for r in rows:
            table.add_row(
                str(r["id"]),
                str(r["execution_id"]),
                r["skill_id"],
                r["author_id"] or "[dim]—[/]",
                r["role"],
                f"{r['weight']:.2%}",
                f"{r['gross_revenue_eur']:.4f}",
                f"{r['author_share_eur']:.4f}",
                r["created_at"][:19],
            )
        console.print(table)

    asyncio.run(_run())


@marketplace.command("pay")
@click.argument("payout_id", type=int)
def marketplace_pay(payout_id: int) -> None:
    """Mark a single payout row as paid (after Stripe transfer)."""

    async def _run() -> None:
        from bvd.marketplace.payouts import mark_payout_paid

        ok = await mark_payout_paid(payout_id)
        if ok:
            console.print(f"[bold green]Payout {payout_id} marked as paid.[/]")
        else:
            console.print(f"[red]Payout {payout_id} not found or already paid.[/]")
            sys.exit(1)

    asyncio.run(_run())


@marketplace.command("simulate")
@click.argument("chain_id")
@click.option("--price", default=None, type=float, help="Override marketplace_price_eur")
def marketplace_simulate(chain_id: str, price: float | None) -> None:
    """Simulate the author revenue split for a chain skill."""

    async def _run() -> None:
        from bvd.engine.library import library
        from bvd.marketplace.payouts import compute_author_split

        await library.init()
        script = await library.get(chain_id)
        await library.close()

        if script is None:
            console.print(f"[red]Chain '{chain_id}' not found in library.[/]")
            sys.exit(1)

        if price is not None:
            script.marketplace_price_eur = price  # type: ignore[assignment]

        if not script.is_paid:
            console.print("[yellow]This skill has no marketplace_price_eur — nothing to split.[/]")
            return

        from bvd.engine.library import library as _lib2
        await _lib2.init()
        dep_scripts = await _lib2.get_many(script.chain_ids)
        await _lib2.close()

        split = compute_author_split(script, dep_scripts)

        console.print(
            f"\n[bold]Revenue split for[/] [cyan]{chain_id}[/] "
            f"(gross €{split.gross_revenue_eur:.4f})"
        )
        table = Table()
        table.add_column("Skill")
        table.add_column("Author ID")
        table.add_column("Role")
        table.add_column("Weight", justify="right")
        table.add_column("Share (€)", justify="right")

        for row in split.rows:
            table.add_row(
                row.skill_id,
                row.author_id or "[dim]—[/]",
                row.role,
                f"{row.weight:.2%}",
                f"{row.author_share_eur:.4f}",
            )
        console.print(table)

        total_share = sum(r.author_share_eur for r in split.rows)
        platform_share = split.gross_revenue_eur - total_share
        console.print(
            f"\n  Platform retention: [bold]€{platform_share:.4f}[/] "
            f"({platform_share / split.gross_revenue_eur:.1%})\n"
        )

    asyncio.run(_run())


if __name__ == "__main__":
    main()
