"""
Benchmark token savings for each BSS demo skill.
Uses tiktoken (cl100k_base ≈ GPT-4 / Claude approximation).
Runs each skill script in a subprocess (mimicking the sandbox).
"""
import json
import subprocess
import sys
from pathlib import Path

try:
    import tiktoken
    enc = tiktoken.get_encoding("cl100k_base")
except ImportError:
    print("tiktoken not installed, running: pip install tiktoken")
    subprocess.run([sys.executable, "-m", "pip", "install", "-q", "tiktoken"])
    import tiktoken
    enc = tiktoken.get_encoding("cl100k_base")

def count_tokens(text: str) -> int:
    return len(enc.encode(text))

def run_skill(skill_path: Path, prompt: str) -> str:
    """Run a BSS skill script with PROMPT injected (mimics sandbox)."""
    source = skill_path.read_text()
    # Inject PROMPT variable (same as sandbox wrapper)
    wrapped = f"PROMPT = {repr(prompt)}\n{source}"
    result = subprocess.run(
        [sys.executable, "-c", wrapped],
        capture_output=True, text=True, timeout=10
    )
    return result.stdout.strip()

# ─── Realistic test prompts ───────────────────────────────────────────────────

PROMPTS = {}

# 1. PDF Clause Extractor — a realistic service agreement (~3 pages)
PROMPTS["pdf-clause-extractor"] = """
Please review the following service agreement and extract the key clauses.

SERVICE AGREEMENT

This Service Agreement ("Agreement") is entered into as of January 15, 2026, between
Acme Software Solutions SAS, a French société par actions simplifiée registered at
12 rue de la Paix, 75001 Paris, France ("Service Provider"), and Global Retail Corp,
a Delaware corporation with offices at 500 Fifth Avenue, New York, NY 10110 ("Client").

1. SERVICES
The Service Provider agrees to provide software development and consulting services
as detailed in the attached Statement of Work (Exhibit A), which may be updated from
time to time by mutual written agreement of the parties.

2. TERM AND TERMINATION
2.1 This Agreement shall commence on the Effective Date and continue for a period of
twelve (12) months, unless earlier terminated in accordance with this Section.
2.2 Either party may terminate this Agreement for convenience upon thirty (30) days'
prior written notice to the other party.
2.3 Either party may terminate this Agreement immediately upon written notice if the
other party materially breaches this Agreement and fails to cure such breach within
fifteen (15) days after receiving written notice of the breach.
2.4 Termination shall not affect any rights or obligations that accrued prior to the
effective date of termination. Sections 5, 6, 7, and 9 shall survive termination.

3. PAYMENT AND INVOICING
3.1 The Client shall pay the Service Provider a monthly fee of €18,500 (eighteen
thousand five hundred euros) exclusive of VAT, payable within thirty (30) days of
invoice receipt.
3.2 The Service Provider shall submit invoices on the first business day of each month
for services rendered in the preceding month.
3.3 Late payments shall accrue interest at the rate of 1.5% per month (18% per annum)
from the due date until the date of actual payment.
3.4 All fees are exclusive of applicable taxes. The Client is responsible for all
sales, use, VAT, or similar taxes arising from this Agreement.
3.5 The Service Provider may suspend services upon fifteen (15) days written notice
if any invoice remains unpaid for more than forty-five (45) days after the due date.

4. CONFIDENTIALITY AND NON-DISCLOSURE
4.1 Each party agrees to hold the other party's Confidential Information in strict
confidence and not to disclose it to any third party without the prior written consent
of the disclosing party, except as required by law or regulation.
4.2 "Confidential Information" means all non-public information disclosed by one party
to the other that is designated as confidential or that reasonably should be understood
to be confidential given the nature of the information and the circumstances of disclosure.
4.3 The confidentiality obligations set forth in this Section shall survive for a period
of five (5) years following the termination or expiration of this Agreement.
4.4 The non-disclosure obligations shall not apply to information that: (a) is or
becomes publicly known through no breach of this Agreement; (b) was rightfully known
before receipt from the disclosing party; (c) is rightfully received from a third party
without restriction.

5. INTELLECTUAL PROPERTY RIGHTS
5.1 All work product, inventions, developments, and deliverables created by Service
Provider specifically for Client under this Agreement ("Work Product") shall be owned
by Client upon full payment of all fees due.
5.2 Service Provider retains all rights to its pre-existing intellectual property,
tools, frameworks, and methodologies ("Background IP"). Service Provider grants Client
a non-exclusive, non-transferable license to use the Background IP solely as incorporated
in the Work Product.
5.3 Client grants Service Provider a limited license to use Client's trademarks and
materials solely as necessary to perform the Services.
5.4 Service Provider shall not incorporate any open source software into the Work Product
without prior written approval from Client, which shall not be unreasonably withheld.

6. WARRANTY AND REPRESENTATIONS
6.1 Service Provider warrants that the Services will be performed in a professional and
workmanlike manner consistent with industry standards.
6.2 Service Provider warrants that the Work Product will not infringe any third-party
intellectual property rights.
6.3 EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER PARTY MAKES ANY WARRANTIES,
EXPRESS OR IMPLIED, INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR
A PARTICULAR PURPOSE.

7. LIMITATION OF LIABILITY
7.1 NEITHER PARTY SHALL BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES.
7.2 EACH PARTY'S TOTAL LIABILITY ARISING OUT OF OR RELATED TO THIS AGREEMENT SHALL NOT
EXCEED THE TOTAL FEES PAID BY CLIENT TO SERVICE PROVIDER IN THE TWELVE (12) MONTHS
IMMEDIATELY PRECEDING THE CLAIM.

8. GOVERNING LAW AND JURISDICTION
8.1 This Agreement shall be governed by and construed in accordance with the laws of
France, without regard to its conflict of law provisions.
8.2 Any dispute arising from or relating to this Agreement shall be submitted to the
exclusive jurisdiction of the Commercial Court of Paris (Tribunal de Commerce de Paris).
8.3 The parties agree to attempt to resolve any dispute through good-faith negotiation
for a period of thirty (30) days before initiating any legal proceedings.

9. FORCE MAJEURE
9.1 Neither party shall be liable for any failure or delay in performance under this
Agreement to the extent caused by circumstances beyond that party's reasonable control,
including acts of God, natural disasters, war, terrorism, pandemic, government action,
or internet service provider failures.
9.2 The affected party shall provide prompt written notice of any force majeure event
and shall use commercially reasonable efforts to resume performance as soon as possible.

10. GENERAL PROVISIONS
10.1 This Agreement constitutes the entire agreement between the parties and supersedes
all prior negotiations, representations, or agreements.
10.2 This Agreement may not be amended except by a written instrument signed by both parties.
10.3 If any provision of this Agreement is found to be unenforceable, the remaining
provisions shall remain in full force and effect.

IN WITNESS WHEREOF, the parties have executed this Agreement as of the date first written above.

ACME SOFTWARE SOLUTIONS SAS          GLOBAL RETAIL CORP
By: ___________________              By: ___________________
Name: Marie Dupont                   Name: John Smith
Title: CEO                           Title: VP Engineering
Date: January 15, 2026               Date: January 15, 2026
""".strip()

# 2. CSV Data Summarizer — monthly sales data (500 rows simulated, first 50 shown)
_csv_rows = "\n".join(
    f"2026-01-{(i%28)+1:02d},Product {'ABCDE'[i%5]}{(i//5)%10},{'Electronics' if i%3==0 else 'Software' if i%3==1 else 'Services'},"
    f"{1200+(i*73)%3800},{(i*17)%100+1},{('FR' if i%4==0 else 'DE' if i%4==1 else 'ES' if i%4==2 else 'IT')},"
    f"{'Online' if i%2==0 else 'Retail'},{round((1200+(i*73)%3800)*((i*17)%100+1)*0.85, 2)}"
    for i in range(80)
)
PROMPTS["csv-data-summarizer"] = f"""
Analyse these monthly sales records and tell me which product category performed best,
which region has the highest average order value, and flag any anomalies.

```csv
date,product_id,category,unit_price,quantity,region,channel,revenue
{_csv_rows}
```
""".strip()

# 3. Code Review Condenser — a realistic Git diff (~3 files, 200+ lines)
PROMPTS["code-review-condenser"] = """
Please review this pull request and identify any bugs, performance issues, or
security concerns.

diff --git a/bvd/engine/matcher.py b/bvd/engine/matcher.py
index 3f2a1b4..9c8d7e2 100644
--- a/bvd/engine/matcher.py
+++ b/bvd/engine/matcher.py
@@ -1,8 +1,12 @@
 import numpy as np
+import faiss
 from sentence_transformers import SentenceTransformer
 from dataclasses import dataclass
 from typing import Optional
+import logging
+
+logger = logging.getLogger(__name__)

 @dataclass
 class MatchResult:
@@ -18,34 +22,67 @@ class Matcher:
     THRESHOLD = 0.85
     _instance: Optional["Matcher"] = None

-    def __init__(self):
+    def __init__(self, use_faiss: bool = True):
         self._model: Optional[SentenceTransformer] = None
-        self._index: list[tuple[str, np.ndarray, object]] = []
+        self._faiss_index: Optional[faiss.IndexFlatIP] = None
+        self._id_map: list[tuple[str, object]] = []
         self._dirty = True
+        self._use_faiss = use_faiss
+        self._dim = 384  # all-MiniLM-L6-v2 output dim

-    def _load_model(self):
-        if self._model is None:
-            self._model = SentenceTransformer("all-MiniLM-L6-v2")
-
-    def _embed(self, text: str) -> np.ndarray:
-        self._load_model()
-        return self._model.encode([text])[0]
-
-    def match(self, prompt: str, scripts: list) -> Optional[MatchResult]:
-        if not scripts:
+    def _load_model(self) -> None:
+        if self._model is None:
+            logger.debug("Loading sentence-transformers model...")
+            self._model = SentenceTransformer("all-MiniLM-L6-v2")
+            logger.debug("Model loaded.")
+
+    def _embed(self, text: str) -> np.ndarray:
+        self._load_model()
+        vec = self._model.encode([text], normalize_embeddings=True)[0]
+        return vec.astype(np.float32)
+
+    def _build_faiss_index(self, scripts: list) -> None:
+        self._faiss_index = faiss.IndexFlatIP(self._dim)
+        self._id_map = []
+        vecs = []
+        for script in scripts:
+            if script.embedding is None:
+                continue
+            vec = np.array(script.embedding, dtype=np.float32)
+            faiss.normalize_L2(vec.reshape(1, -1))
+            vecs.append(vec)
+            self._id_map.append((script.id, script))
+        if vecs:
+            matrix = np.stack(vecs)
+            self._faiss_index.add(matrix)
+        logger.debug(f"FAISS index built with {len(self._id_map)} vectors.")
+
+    def match(self, prompt: str, scripts: list) -> Optional[MatchResult]:
+        if not scripts:
             return None
-        query_vec = self._embed(prompt)
-        best_score = -1.0
-        best_script = None
-        for script_id, vec, script in self._index:
-            score = float(np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)))
-            if score > best_score:
-                best_score = score
-                best_script = script
-        if best_score >= self.THRESHOLD:
-            return MatchResult(script=best_script, score=best_score)
+
+        if self._dirty or self._faiss_index is None:
+            self._build_faiss_index(scripts)
+            self._dirty = False
+
+        query_vec = self._embed(prompt).reshape(1, -1)
+        faiss.normalize_L2(query_vec)
+
+        k = min(1, len(self._id_map))
+        distances, indices = self._faiss_index.search(query_vec, k)
+        best_score = float(distances[0][0])
+        best_idx = int(indices[0][0])
+
+        if best_score >= self.THRESHOLD and best_idx >= 0:
+            _, best_script = self._id_map[best_idx]
+            return MatchResult(script=best_script, score=best_score)
         return None

diff --git a/bvd/engine/stats.py b/bvd/engine/stats.py
index a1b2c3d..4e5f6a7 100644
--- a/bvd/engine/stats.py
+++ b/bvd/engine/stats.py
@@ -45,6 +45,28 @@ class Stats:
         async with self._db.execute(sql, params) as cursor:
             return await cursor.fetchall()

+    async def weekly_summary(self) -> dict:
+        sql = (
+            "SELECT SUM(tokens_before - tokens_after) as tokens_saved, "
+            "COUNT(*) as executions, "
+            "AVG(CAST(tokens_before - tokens_after AS FLOAT) / tokens_before) as avg_saving_pct "
+            "FROM executions WHERE created_at >= datetime('now', '-7 days') AND tokens_before > 0"
+        )
+        async with self._db.execute(sql) as cursor:
+            row = await cursor.fetchone()
+        if not row or row[0] is None:
+            return {"tokens_saved": 0, "executions": 0, "avg_saving_pct": 0.0}
+        return {
+            "tokens_saved": int(row[0]),
+            "executions": int(row[1]),
+            "avg_saving_pct": round(float(row[2]) * 100, 1),
+            "co2_saved_g": round(int(row[0]) * 0.003, 2),
+            "eur_saved": round(int(row[0]) * 0.000003, 4),
+        }
+
 class StatsError(Exception):
     pass

diff --git a/tests/test_matcher.py b/tests/test_matcher.py
index 0a1b2c3..d4e5f6a 100644
--- a/tests/test_matcher.py
+++ b/tests/test_matcher.py
@@ -1,12 +1,38 @@
 import pytest
 import numpy as np
+import faiss
 from unittest.mock import MagicMock, patch
 from bvd.engine.matcher import Matcher, MatchResult

+@pytest.fixture
+def matcher():
+    return Matcher(use_faiss=True)
+
+def make_script(id, embedding):
+    s = MagicMock()
+    s.id = id
+    s.embedding = embedding
+    return s

 def test_match_returns_best_score(matcher):
-    scripts = [make_script("a", [0.1]*384), make_script("b", [0.9]*384)]
+    dim = 384
+    vec_a = np.random.rand(dim).astype(np.float32)
+    vec_b = np.random.rand(dim).astype(np.float32)
+    scripts = [make_script("a", vec_a.tolist()), make_script("b", vec_b.tolist())]
     result = matcher.match("test query", scripts)
     assert result is None or isinstance(result, MatchResult)
+
+def test_faiss_index_rebuilt_on_dirty(matcher):
+    dim = 384
+    vec = np.ones(dim, dtype=np.float32)
+    scripts = [make_script("x", vec.tolist())]
+    matcher._dirty = True
+    matcher.match("query", scripts)
+    assert matcher._faiss_index is not None
+    assert not matcher._dirty
""".strip()

# 4. Batch Translation Condenser — 25 product descriptions to translate
_items = [
    "Wireless noise-cancelling headphones with 30-hour battery life",
    "Ergonomic office chair with lumbar support and adjustable armrests",
    "Stainless steel insulated water bottle, 750ml, keeps drinks cold 24h",
    "Ultra-thin laptop stand with 6 adjustable height settings",
    "Mechanical keyboard with RGB backlighting and Cherry MX switches",
    "Smart home hub compatible with Alexa, Google Home, and Apple HomeKit",
    "Portable solar charger with dual USB ports and 20,000mAh battery",
    "Standing desk converter with cable management and monitor shelf",
    "True wireless earbuds with active noise cancellation and 8h playtime",
    "4K webcam with built-in ring light and auto-focus for video calls",
    "Bamboo desk organizer with phone stand and cable management slots",
    "USB-C docking station with HDMI, Ethernet, and 6 USB ports",
    "Anti-fatigue mat for standing desk use, 90x60cm memory foam",
    "Wireless charging pad compatible with Qi-enabled devices",
    "Blue light blocking glasses for computer use, anti-reflective coating",
    "Smart LED desk lamp with touch dimmer and USB charging port",
    "Mesh office chair with breathable back and adjustable headrest",
    "Foldable Bluetooth keyboard for tablet and smartphone use",
    "Noise-isolating earplugs for office use, reusable silicone",
    "Desktop monitor arm with full motion articulation, VESA 75/100",
    "Fingerprint-resistant screen protector for 27-inch monitors",
    "Cable management box to hide power strips and excess cables",
    "Laptop backpack with TSA-friendly laptop compartment, 30L",
    "Compact mechanical numpad with detachable USB cable",
    "Adjustable monitor riser with drawer storage, bamboo finish",
]
PROMPTS["batch-translation-condenser"] = (
    "Please translate the following product descriptions from English to French, "
    "using a professional tone suitable for an e-commerce website.\n\n"
    + "\n".join(f"{i+1}. {item}" for i, item in enumerate(_items))
)

# 5. Structured Report Extractor — quarterly financial report (~3000 tokens)
PROMPTS["structured-report-extractor"] = """
Analyse this quarterly report and summarize the key financial indicators and strategic highlights.

ACME TECHNOLOGIES — Q4 2025 QUARTERLY FINANCIAL REPORT
Fiscal Year 2025 — Fourth Quarter Results
For the period ending December 31, 2025

EXECUTIVE SUMMARY

Acme Technologies delivered strong Q4 2025 results, with total revenue reaching €47.3 million,
representing a 23.4% year-over-year increase from €38.3 million in Q4 2024. Recurring revenue
(SaaS subscriptions) grew to €31.8 million, up 31.2% YoY, and now represents 67.2% of total revenue.
Adjusted EBITDA margin improved to 18.7%, up from 14.2% in Q4 2024. Net income attributable to
shareholders was €5.1 million versus a net loss of €2.3 million in the prior year quarter.

REVENUE BREAKDOWN

1. Software as a Service (SaaS)
   Q4 2025: €31.8M (+31.2% YoY)
   Q3 2025: €28.4M (+27.8% YoY)
   Annual Recurring Revenue (ARR): €127.2M (+29.4% YoY)
   Net Revenue Retention (NRR): 118%
   New ARR added in Q4: €8.7M
   Churn rate: 1.8% (annualized: 7.2%)

2. Professional Services
   Q4 2025: €9.6M (+8.1% YoY)
   Implementation & onboarding: €5.4M
   Training & support contracts: €4.2M
   Average project duration: 8.3 weeks
   Utilization rate: 84%

3. License & Maintenance (legacy)
   Q4 2025: €5.9M (-12.3% YoY) — intentional wind-down as customers migrate to SaaS
   Active legacy contracts: 234 (down from 312 in Q4 2024)
   Migration rate: 38 legacy customers converted to SaaS in Q4

GEOGRAPHIC PERFORMANCE

France (domestic): €18.2M — 38.5% of total revenue (+15.7% YoY)
Germany: €11.4M — 24.1% (+34.2% YoY) — strongest growth region
United Kingdom: €8.1M — 17.1% (+18.9% YoY)
Rest of Europe: €6.3M — 13.3% (+29.1% YoY)
North America: €3.3M — 7.0% (new market, launched Q2 2025)

OPERATING EXPENSES

Total OpEx: €38.7M (Q4 2024: €34.1M)
  Research & Development: €12.4M (26.2% of revenue, target: 25-28%)
  Sales & Marketing: €11.8M (24.9% of revenue)
  General & Administrative: €6.2M (13.1% of revenue)
  Cost of Revenue: €8.3M (17.6% of revenue, gross margin: 82.4%)

Headcount: 412 FTE (vs 348 FTE at end of Q4 2024, +18.4% YoY)
  Engineering & Product: 187 FTE
  Sales & Customer Success: 134 FTE
  G&A: 91 FTE

Average revenue per employee (annualized): €459K

BALANCE SHEET HIGHLIGHTS (as of December 31, 2025)

Cash & equivalents: €28.4M
Accounts receivable: €14.7M (DSO: 42 days, improved from 51 days)
Deferred revenue: €38.6M (billings in advance, up 34% YoY)
Total assets: €127.3M
Total liabilities: €48.9M
Shareholders' equity: €78.4M
Net cash position: €22.1M (after €6.3M in debt repayments in Q4)

CASH FLOW

Operating cash flow: €9.2M (Q4 2024: €3.1M) — significant improvement
Free cash flow: €6.7M (after €2.5M capex — primarily server infrastructure)
Cash burn from operations: positive for third consecutive quarter

GUIDANCE FOR Q1 2026

Revenue guidance: €49.5M — €51.0M (+18-22% YoY)
Adjusted EBITDA margin target: 19-21%
ARR exit target: €138M — €142M
Planned headcount additions: 35-45 FTEs (primarily engineering)
Expected key events: Paris AI Summit partnership announcement (February), Series C close (March)

STRATEGIC HIGHLIGHTS Q4 2025

1. AI Integration: Launched ACME AI Assist feature to all Pro+ subscribers (18,400 users).
   AI-assisted workflows reduced customer task completion time by 43% in beta testing.

2. Enterprise wins: Signed 7 new enterprise contracts (>€200K ARR each) including
   Société Générale (€380K ARR) and Bosch Digital (€290K ARR).

3. Partnership: Signed reseller agreement with Capgemini covering DACH region.
   Pipeline contribution expected from Q2 2026.

4. Product: Released version 4.2 with API v3, webhook builder, and 47 new integrations.
   App store now has 312 third-party integrations (up from 198).

KEY METRICS SUMMARY

Metric                    Q4 2025     Q4 2024     YoY Change
Total Revenue             €47.3M      €38.3M      +23.4%
SaaS Revenue              €31.8M      €24.2M      +31.2%
ARR                       €127.2M     €98.3M      +29.4%
Gross Margin              82.4%       79.1%       +330bps
Adj. EBITDA Margin        18.7%       14.2%       +450bps
NRR                       118%        109%        +9pp
Customers                 2,847       2,241       +27.0%
Enterprise customers      189         124         +52.4%
ARR per customer          €44,700     €43,900     +1.8%
""".strip()

# ─── Run benchmark ────────────────────────────────────────────────────────────

SKILL_NAMES = {
    "pdf-clause-extractor":       "PDF Clause Extractor",
    "csv-data-summarizer":        "CSV Data Summarizer",
    "code-review-condenser":      "Code Review Condenser",
    "batch-translation-condenser":"Batch Translation Condenser",
    "structured-report-extractor":"Structured Report Extractor",
}

results = []
print("\n" + "="*70)
print("BIBIVIDI — Token Savings Benchmark")
print("Encoding: cl100k_base (GPT-4/Claude approximation)")
print("="*70 + "\n")

for skill_id, prompt in PROMPTS.items():
    skill_path = Path("scripts") / skill_id / "skill.py"
    name = SKILL_NAMES[skill_id]

    tokens_before = count_tokens(prompt)
    try:
        output = run_skill(skill_path, prompt)
        tokens_after = count_tokens(output)
        pct = round((1 - tokens_after / tokens_before) * 100, 1)
        ratio = round(tokens_before / max(tokens_after, 1), 1)
        results.append({
            "id": skill_id,
            "name": name,
            "tokens_before": tokens_before,
            "tokens_after": tokens_after,
            "pct_saved": pct,
            "ratio": ratio,
            "output_preview": output[:200],
            "error": None,
        })
        print(f"✅ {name}")
        print(f"   Before : {tokens_before:,} tokens")
        print(f"   After  : {tokens_after:,} tokens")
        print(f"   Saved  : {pct}% ({ratio}x reduction)")
        print(f"   Output preview: {output[:120].replace(chr(10), ' ')}...")
        print()
    except Exception as e:
        results.append({"id": skill_id, "name": name, "error": str(e)})
        print(f"❌ {name}: {e}\n")

print("="*70)
print("SUMMARY")
print("="*70)
for r in results:
    if r.get("error"):
        print(f"  {r['name']}: ERROR — {r['error']}")
    else:
        print(f"  {r['name']:35s} {r['tokens_before']:>6,} → {r['tokens_after']:>4,} tokens   {r['pct_saved']:>5.1f}% saved   ({r['ratio']}x)")

print()
print(json.dumps(results, indent=2, ensure_ascii=False))
