#!/usr/bin/env python3
"""
Kanoniv — Complete End-to-End Example
======================================

This script demonstrates every feature of the Kanoniv identity resolution SDK:

    1. Source adapters    — CSV files + pandas DataFrames (different schemas, fuzzy names)
    2. Spec management    — load, parse, inspect identity specs
    3. Validation         — schema + semantic validation with error reporting
    4. Execution planning — plan(), summary(), plan_hash, risk_flags
    5. Spec diffing       — compare two spec versions, detect rule/threshold changes
    6. Reconciliation     — local engine (PyO3/Rust), blocking, matching, clustering
    7. Result inspection  — clusters, golden records, merge_rate, decisions, telemetry
    8. Pandas integration — to_pandas(), DataFrame analysis of golden records
    9. Cloud API client   — specs.ingest() for server-side validation (optional)

Data:
    4 CSV files (~100 rows each) simulating real enterprise sources:
      - CRM contacts       (customer_id, full_name, email_address, phone_number, ...)
      - Stripe payments     (stripe_id, cust_email, customer_name, ...)
      - Support tickets     (ticket_id, reporter_email, reporter_name, reporter_phone, ...)
      - Ecommerce orders    (order_id, buyer_email, buyer_first_name, buyer_last_name, buyer_phone, ...)

    Column names are intentionally different across sources — just like real life.

Prerequisites:
    pip install kanoniv pandas
    cd oss/python-sdk && maturin develop   # build the native extension

Usage:
    python examples/kanoniv-example.py
"""
from __future__ import annotations

import json
import os
import sys
import textwrap
from pathlib import Path

# ──────────────────────────────────────────────────────────────────────────
# Ensure the example data exists
# ──────────────────────────────────────────────────────────────────────────

EXAMPLE_DIR = Path(__file__).resolve().parent
DATA_DIR = EXAMPLE_DIR / "data"

REQUIRED_FILES = [
    "crm_contacts.csv",
    "stripe_payments.csv",
    "support_tickets.csv",
    "ecommerce_orders.csv",
]

for fname in REQUIRED_FILES:
    if not (DATA_DIR / fname).exists():
        print(f"Missing data file: {DATA_DIR / fname}")
        print("Run `python examples/generate_csvs.py` first to create the test data.")
        sys.exit(1)


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 1 — Imports                                                  ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("=" * 72)
print("  KANONIV — Complete Identity Resolution Example")
print("=" * 72)

from kanoniv import (
    Spec,
    Source,
    ReconcileResult,
    validate,
    plan,
    diff,
    reconcile,
)
import pandas as pd

print("\n[1] Imports OK")
print(f"    kanoniv version : {__import__('kanoniv').__version__}")
print(f"    pandas version  : {pd.__version__}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 2 — Identity Spec (v1)                                       ║
# ╚════════════════════════════════════════════════════════════════════════╝

# This spec defines how to resolve identities across our 4 sources.
# Each source maps its own column names → canonical attribute names.
# The engine uses blocking keys + matching rules to decide merges.

SPEC_V1_YAML = textwrap.dedent("""\
    api_version: kanoniv/v2
    identity_version: customer_v1.0

    entity:
      name: customer

    sources:
      - name: crm
        system: csv
        table: crm_contacts
        id: customer_id
        attributes:
          email: email_address
          name: full_name
          phone: phone_number

      - name: stripe
        system: csv
        table: stripe_payments
        id: stripe_id
        attributes:
          email: cust_email
          name: customer_name

      - name: support
        system: csv
        table: support_tickets
        id: ticket_id
        attributes:
          email: reporter_email
          name: reporter_name
          phone: reporter_phone

      - name: ecommerce
        system: csv
        table: ecommerce_orders
        id: order_id
        attributes:
          email: buyer_email
          name: buyer_first_name
          phone: buyer_phone

    blocking:
      strategy: composite
      keys:
        - [email]
        - [phone]

    rules:
      - name: email_exact
        type: exact
        field: email
        weight: 1.0

      - name: phone_exact
        type: exact
        field: phone
        weight: 0.8

    decision:
      thresholds:
        match: 0.7
        review: 0.4
      conflict_strategy: prefer_high_confidence
""")

print("\n" + "=" * 72)
print("  SECTION 2 — Load & Inspect Spec v1")
print("=" * 72)

spec_v1 = Spec.from_string(SPEC_V1_YAML)

print(f"\n    Entity          : {spec_v1.entity}")
print(f"    Version         : {spec_v1.version}")
print(f"    Sources         : {len(spec_v1.sources)}")
for src in spec_v1.sources:
    print(f"      - {src['name']:12s}  id_col={src.get('id', '?'):16s}  attrs={list(src.get('attributes', {}).keys())}")
print(f"    Rules           : {len(spec_v1.rules)}")
for rule in spec_v1.rules:
    print(f"      - {rule['name']:20s}  type={rule['type']:10s}  weight={rule.get('weight', '?')}")

# Also save to a file and reload to show Spec.from_file
spec_file = EXAMPLE_DIR / "identity_v1.yml"
spec_file.write_text(SPEC_V1_YAML)
spec_from_file = Spec.from_file(str(spec_file))
print(f"\n    Spec.from_file  : loaded {spec_file.name} ({len(spec_from_file.raw)} bytes)")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 3 — Validation                                               ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 3 — Spec Validation")
print("=" * 72)

# 3a. Validate the good spec
result_v1 = validate(spec_v1)
print(f"\n    Spec v1 valid?  : {result_v1.valid}")
print(f"    Errors          : {result_v1.errors if result_v1.errors else 'None'}")
print(f"    bool(result)    : {bool(result_v1)}")

# 3b. Validate a deliberately broken spec to show error reporting
BAD_SPEC_YAML = textwrap.dedent("""\
    api_version: kanoniv/v2
    entity:
      name: customer
    sources: []
    rules: []
    decision:
      thresholds:
        match: 1.5
        review: -0.1
""")

bad_spec = Spec.from_string(BAD_SPEC_YAML)
bad_result = validate(bad_spec)
print(f"\n    Bad spec valid? : {bad_result.valid}")
print(f"    Errors ({len(bad_result.errors)}):")
for err in bad_result.errors:
    print(f"      - {err}")

# Show raise_on_error
print("\n    raise_on_error():")
try:
    bad_result.raise_on_error()
except ValueError as e:
    first_line = str(e).split("\n")[0]
    print(f"      Caught ValueError: {first_line}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 4 — Execution Plan                                           ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 4 — Execution Plan")
print("=" * 72)

plan_v1 = plan(spec_v1)

print(f"\n    plan.entity         : {plan_v1.entity}")
print(f"    plan.plan_hash      : {plan_v1.plan_hash}")

# Summary
summary = plan_v1.summary()
print(f"\n    plan.summary():")
for line in summary.strip().split("\n"):
    print(f"      {line}")

# Execution stages
print(f"\n    Execution stages ({len(plan_v1.execution_stages)}):")
for stage in plan_v1.execution_stages:
    print(f"      - {stage.get('name', stage.get('stage', '?'))}: {stage.get('description', json.dumps(stage))}")

# Match strategies
print(f"\n    Match strategies ({len(plan_v1.match_strategies)}):")
for strat in plan_v1.match_strategies:
    print(f"      - {strat.get('rule_name', strat.get('name', '?'))}: {strat.get('type', '?')} "
          f"field={strat.get('field', '?')} weight={strat.get('weight', '?')}")

# Blocking analysis
blocking = plan_v1.blocking
if blocking:
    print(f"\n    Blocking analysis:")
    for k, v in blocking.items():
        print(f"      {k}: {v}")

# Risk flags
risk = plan_v1.risk_flags
print(f"\n    Risk flags ({len(risk)}):")
if risk:
    for flag in risk:
        print(f"      - {flag}")
else:
    print("      (none)")

# Full plan dict (for CI/CD storage)
plan_dict = plan_v1.to_dict()
print(f"\n    plan.to_dict() keys : {sorted(plan_dict.keys())}")

# repr
print(f"\n    repr(plan)          : {repr(plan_v1)[:120]}...")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 5 — Spec Diff (v1 → v2)                                      ║
# ╚════════════════════════════════════════════════════════════════════════╝

# v2 adds a similarity rule, lowers thresholds, and tweaks a weight.
SPEC_V2_YAML = textwrap.dedent("""\
    api_version: kanoniv/v2
    identity_version: customer_v2.0

    entity:
      name: customer

    sources:
      - name: crm
        system: csv
        table: crm_contacts
        id: customer_id
        attributes:
          email: email_address
          name: full_name
          phone: phone_number

      - name: stripe
        system: csv
        table: stripe_payments
        id: stripe_id
        attributes:
          email: cust_email
          name: customer_name

      - name: support
        system: csv
        table: support_tickets
        id: ticket_id
        attributes:
          email: reporter_email
          name: reporter_name
          phone: reporter_phone

      - name: ecommerce
        system: csv
        table: ecommerce_orders
        id: order_id
        attributes:
          email: buyer_email
          name: buyer_first_name
          phone: buyer_phone

    blocking:
      strategy: composite
      keys:
        - [email]
        - [phone]

    rules:
      - name: email_exact
        type: exact
        field: email
        weight: 1.0

      - name: phone_exact
        type: exact
        field: phone
        weight: 0.9

      - name: name_similarity
        type: similarity
        field: name
        algorithm: jaro_winkler
        threshold: 0.85
        weight: 0.5

    decision:
      thresholds:
        match: 0.6
        review: 0.3
      conflict_strategy: prefer_high_confidence
""")

print("\n" + "=" * 72)
print("  SECTION 5 — Spec Diff (v1 vs v2)")
print("=" * 72)

spec_v2 = Spec.from_string(SPEC_V2_YAML)

# Validate v2 before diffing
v2_valid = validate(spec_v2)
print(f"\n    Spec v2 valid?      : {v2_valid.valid}")

# Run diff
changes = diff(spec_v1, spec_v2)

print(f"\n    Rules added         : {changes.rules_added}")
print(f"    Rules removed       : {changes.rules_removed}")
print(f"    Rules modified      : {changes.rules_modified}")
print(f"    Thresholds changed  : {changes.thresholds_changed}")
print(f"\n    Diff summary:")
for line in changes.summary.strip().split("\n"):
    print(f"      {line}")
print(f"\n    repr(diff)          : {repr(changes)}")

# Diff identical specs (should show no changes)
no_changes = diff(spec_v1, spec_v1)
print(f"\n    Self-diff:")
print(f"      rules_added       : {no_changes.rules_added}")
print(f"      thresholds_changed: {no_changes.thresholds_changed}")

# Plan v2 and compare hashes
plan_v2 = plan(spec_v2)
print(f"\n    Plan hash v1        : {plan_v1.plan_hash}")
print(f"    Plan hash v2        : {plan_v2.plan_hash}")
print(f"    Hashes differ?      : {plan_v1.plan_hash != plan_v2.plan_hash}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 6 — Source Adapters (CSV)                                     ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 6 — Source Adapters (CSV)")
print("=" * 72)

crm = Source.from_csv(
    name="crm",
    path=str(DATA_DIR / "crm_contacts.csv"),
    primary_key="customer_id",
)
stripe = Source.from_csv(
    name="stripe",
    path=str(DATA_DIR / "stripe_payments.csv"),
    primary_key="stripe_id",
)
support = Source.from_csv(
    name="support",
    path=str(DATA_DIR / "support_tickets.csv"),
    primary_key="ticket_id",
)
ecommerce = Source.from_csv(
    name="ecommerce",
    path=str(DATA_DIR / "ecommerce_orders.csv"),
    primary_key="order_id",
)

csv_sources = [crm, stripe, support, ecommerce]

for src in csv_sources:
    schema = src.schema()
    col_names = [c.name for c in schema.columns]
    col_types = {c.name: c.dtype for c in schema.columns}
    print(f"\n    {src.name:12s}  pk={src.primary_key}  rows={schema.row_count}  cols={len(schema.columns)}")
    print(f"      columns : {col_names}")
    print(f"      types   : {col_types}")
    # Show sample rows
    rows = list(src.iter_rows())
    print(f"      row[0]  : {rows[0]}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 7 — Source Adapters (Pandas)                                  ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 7 — Source Adapters (Pandas DataFrame)")
print("=" * 72)

# Load the same CSVs into pandas DataFrames and create Sources from them
crm_df = pd.read_csv(DATA_DIR / "crm_contacts.csv")
stripe_df = pd.read_csv(DATA_DIR / "stripe_payments.csv")
support_df = pd.read_csv(DATA_DIR / "support_tickets.csv")
ecommerce_df = pd.read_csv(DATA_DIR / "ecommerce_orders.csv")

print(f"\n    DataFrame shapes:")
print(f"      crm_df       : {crm_df.shape}  dtypes: {dict(crm_df.dtypes)}")
print(f"      stripe_df    : {stripe_df.shape}  dtypes: {dict(stripe_df.dtypes)}")
print(f"      support_df   : {support_df.shape}  dtypes: {dict(support_df.dtypes)}")
print(f"      ecommerce_df : {ecommerce_df.shape}  dtypes: {dict(ecommerce_df.dtypes)}")

# Create pandas-backed sources
crm_pd = Source.from_pandas("crm", crm_df, primary_key="customer_id")
stripe_pd = Source.from_pandas("stripe", stripe_df, primary_key="stripe_id")
support_pd = Source.from_pandas("support", support_df, primary_key="ticket_id")
ecommerce_pd = Source.from_pandas("ecommerce", ecommerce_df, primary_key="order_id")

pd_sources = [crm_pd, stripe_pd, support_pd, ecommerce_pd]

# Compare schemas between CSV and Pandas adapters
print(f"\n    Schema comparison (CSV vs Pandas):")
for csv_src, pd_src in zip(csv_sources, pd_sources):
    csv_schema = csv_src.schema()
    pd_schema = pd_src.schema()
    csv_cols = {c.name for c in csv_schema.columns}
    pd_cols = {c.name for c in pd_schema.columns}
    print(f"      {csv_src.name:12s}: CSV cols={len(csv_cols)}  Pandas cols={len(pd_cols)}  match={csv_cols == pd_cols}")

# Entity bridge — show to_entities() output shape
entities = crm_pd.to_entities("customer")
print(f"\n    to_entities('customer') from CRM (first entity):")
sample = entities[0]
for k, v in sample.items():
    val_str = str(v)
    if len(val_str) > 60:
        val_str = val_str[:60] + "..."
    print(f"      {k:16s}: {val_str}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 8 — Reconciliation (v1 spec, CSV sources)                    ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 8 — Reconciliation (v1 spec, 4 CSV sources)")
print("=" * 72)

result_v1_csv = reconcile(sources=csv_sources, spec=spec_v1)

total_input = sum(len(list(s.iter_rows())) for s in csv_sources)

print(f"\n    Input entities      : {total_input}")
print(f"    Clusters formed     : {result_v1_csv.cluster_count}")
print(f"    Merge rate          : {result_v1_csv.merge_rate:.2%}")
print(f"    Decisions made      : {len(result_v1_csv.decisions)}")
print(f"    Golden records      : {len(result_v1_csv.golden_records)}")

# Telemetry
telemetry = result_v1_csv.telemetry
print(f"\n    Telemetry:")
for k, v in telemetry.items():
    if isinstance(v, list) and len(v) > 3:
        print(f"      {k:24s}: [{v[0]}, ... ({len(v)} items)]")
    else:
        print(f"      {k:24s}: {v}")

# Show some cluster examples
print(f"\n    Sample clusters (first 5):")
for i, cluster in enumerate(result_v1_csv.clusters[:5]):
    print(f"      Cluster {i}: {len(cluster)} entities → {cluster[:4]}{'...' if len(cluster) > 4 else ''}")

# Show merged clusters (size > 1)
merged = [c for c in result_v1_csv.clusters if len(c) > 1]
singletons = [c for c in result_v1_csv.clusters if len(c) == 1]
print(f"\n    Merged clusters     : {len(merged)}  (entities that resolved together)")
print(f"    Singletons          : {len(singletons)}  (unique identities)")

# Largest cluster
if merged:
    largest = max(merged, key=len)
    print(f"    Largest cluster     : {len(largest)} entities")

# Show some decisions
print(f"\n    Sample decisions (first 3):")
for d in result_v1_csv.decisions[:3]:
    print(f"      {json.dumps(d, default=str)[:120]}...")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 9 — Reconciliation (v1 spec, Pandas sources)                 ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 9 — Reconciliation (Pandas sources) + CSV parity check")
print("=" * 72)

result_v1_pd = reconcile(sources=pd_sources, spec=spec_v1)

print(f"\n    Pandas clusters     : {result_v1_pd.cluster_count}")
print(f"    Pandas merge rate   : {result_v1_pd.merge_rate:.2%}")
print(f"    CSV clusters        : {result_v1_csv.cluster_count}")
print(f"    CSV merge rate      : {result_v1_csv.merge_rate:.2%}")
print(f"    Parity (clusters)   : {'MATCH' if result_v1_pd.cluster_count == result_v1_csv.cluster_count else 'DIFFER'}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 10 — Reconciliation (v2 spec) + comparison                   ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 10 — Reconciliation with v2 spec (lower thresholds + similarity rule)")
print("=" * 72)

result_v2_csv = reconcile(sources=csv_sources, spec=spec_v2)

print(f"\n    v1 clusters         : {result_v1_csv.cluster_count}")
print(f"    v2 clusters         : {result_v2_csv.cluster_count}")
print(f"    v1 merge rate       : {result_v1_csv.merge_rate:.2%}")
print(f"    v2 merge rate       : {result_v2_csv.merge_rate:.2%}")
print(f"    More aggressive?    : {'YES' if result_v2_csv.cluster_count <= result_v1_csv.cluster_count else 'NO'}")

delta = result_v1_csv.cluster_count - result_v2_csv.cluster_count
if delta > 0:
    print(f"    Extra merges in v2  : {delta} additional clusters merged")
elif delta == 0:
    print(f"    Same result         : v2 rules didn't change the outcome (data may not trigger similarity)")
else:
    print(f"    Fewer merges in v2  : {-delta} clusters split (unexpected)")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 11 — Golden Records → Pandas                                 ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 11 — Golden Records as Pandas DataFrame")
print("=" * 72)

golden_df = result_v1_csv.to_pandas()

print(f"\n    golden_df.shape     : {golden_df.shape}")
print(f"    golden_df.columns   : {list(golden_df.columns)}")
print(f"    golden_df.dtypes    :")
for col, dtype in golden_df.dtypes.items():
    print(f"      {col:24s}: {dtype}")

print(f"\n    First 5 golden records:")
print(golden_df.head().to_string(index=False, max_colwidth=40))

# Analysis: how many golden records have data from multiple sources?
print(f"\n    Total golden records : {len(golden_df)}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 12 — Mixed-source reconciliation (CSV + Pandas)              ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 12 — Mixed-source reconciliation (CSV + Pandas)")
print("=" * 72)

# Mix: CRM from CSV, Stripe from Pandas, Support from CSV, Ecommerce from Pandas
mixed_sources = [crm, stripe_pd, support, ecommerce_pd]

result_mixed = reconcile(sources=mixed_sources, spec=spec_v1)
print(f"\n    Mixed clusters      : {result_mixed.cluster_count}")
print(f"    Mixed merge rate    : {result_mixed.merge_rate:.2%}")
print(f"    Matches CSV-only?   : {'YES' if result_mixed.cluster_count == result_v1_csv.cluster_count else 'NO'}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 13 — JSON adapter                                            ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 13 — JSON Source Adapter")
print("=" * 72)

# Convert CRM CSV data to JSON and load via JSON adapter
json_path = EXAMPLE_DIR / "data" / "crm_contacts.json"
crm_records = list(crm.iter_rows())
json_path.write_text(json.dumps(crm_records, indent=2))

crm_json = Source.from_json("crm_json", str(json_path), primary_key="customer_id")
json_schema = crm_json.schema()
print(f"\n    JSON source rows    : {json_schema.row_count}")
print(f"    JSON source cols    : {[c.name for c in json_schema.columns]}")

# Verify JSON adapter produces same entity count
json_entities = crm_json.to_entities("customer")
csv_entities = crm.to_entities("customer")
print(f"    JSON entities       : {len(json_entities)}")
print(f"    CSV entities        : {len(csv_entities)}")
print(f"    Count matches?      : {'YES' if len(json_entities) == len(csv_entities) else 'NO'}")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 14 — Cloud API Client (optional, requires running server)     ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SECTION 14 — Cloud API Client (spec validation via server)")
print("=" * 72)

KANONIV_API_URL = os.environ.get("KANONIV_API_URL", "")
KANONIV_API_KEY = os.environ.get("KANONIV_API_KEY", "")

if KANONIV_API_URL and KANONIV_API_KEY:
    try:
        from kanoniv import Client

        with Client(base_url=KANONIV_API_URL, api_key=KANONIV_API_KEY) as client:
            # Upload spec for server-side validation
            print(f"\n    Connecting to       : {KANONIV_API_URL}")

            ingest_result = client.specs.ingest(SPEC_V1_YAML, compile=True)
            print(f"    Spec ingested       : valid={ingest_result.get('valid')}")
            print(f"    Server plan_hash    : {ingest_result.get('plan_hash', 'N/A')}")
            print(f"    Warnings            : {ingest_result.get('warnings', [])}")

            # List deployed specs
            specs_list = client.specs.list()
            print(f"    Deployed specs      : {len(specs_list)}")

            # Get dashboard stats
            stats = client.stats()
            print(f"\n    Dashboard stats:")
            for k, v in stats.items():
                print(f"      {k:30s}: {v}")

            # List entities
            entities = client.entities.search(limit=5)
            print(f"\n    Entities (first 5)  : {len(entities.get('data', []))} returned")

            # List sources
            sources_list = client.sources.list()
            print(f"    Configured sources  : {len(sources_list)}")

            # List rules
            rules_list = client.rules.list()
            print(f"    Active rules        : {len(rules_list)}")

            # List pending reviews
            reviews = client.reviews.list(limit=5)
            print(f"    Pending reviews     : {len(reviews)}")

            # List audit trail
            audit = client.audit.list(limit=5)
            print(f"    Audit events        : {len(audit)}")

            # List jobs
            jobs = client.jobs.list(limit=5)
            print(f"    Recent jobs         : {len(jobs)}")

            # List overrides
            overrides = client.overrides.list()
            print(f"    Manual overrides    : {len(overrides)}")

    except ImportError:
        print("\n    Cloud client requires: pip install kanoniv[cloud]")
    except Exception as e:
        print(f"\n    Cloud API error: {e}")
else:
    print("\n    Skipped (set KANONIV_API_URL and KANONIV_API_KEY to enable)")
    print("    Example:")
    print("      export KANONIV_API_URL=http://localhost:3010")
    print("      export KANONIV_API_KEY=kn_test_xxxxxxxxxxxx")

    # Show what the client API looks like even without a running server
    print("\n    Client API preview (not connected):")
    print("      client = Client(base_url=..., api_key=...)")
    print("      client.specs.ingest(yaml, compile=True)   # validate + deploy spec")
    print("      client.specs.list()                       # list deployed specs")
    print("      client.entities.search(q='alice')         # search canonical entities")
    print("      client.entities.get(id)                   # get entity by ID")
    print("      client.entities.get_linked(id)            # entity + linked externals")
    print("      client.entities.history(id)               # entity audit trail")
    print("      client.entities.lock(id)                  # lock entity from merging")
    print("      client.sources.list()                     # list data sources")
    print("      client.sources.create(name=..., ...)      # register new source")
    print("      client.sources.sync(id)                   # trigger source sync")
    print("      client.rules.list()                       # list matching rules")
    print("      client.rules.create(name=..., ...)        # create new rule")
    print("      client.rules.history(name)                # rule version history")
    print("      client.reviews.list()                     # pending review queue")
    print("      client.reviews.decide(entity_a_id=...,    # approve/reject merge")
    print("                            entity_b_id=...,")
    print("                            decision='merge')")
    print("      client.overrides.create(...)              # manual merge/split")
    print("      client.audit.list()                       # audit log")
    print("      client.audit.entity_trail(id)             # entity-specific trail")
    print("      client.jobs.run('reconciliation')         # trigger batch job")
    print("      client.jobs.get(id)                       # check job status")
    print("      client.resolve(system='crm',              # resolve identity")
    print("                     external_id='CRM-10000')")
    print("      client.ingest(source_id, records)         # webhook ingest")
    print("      client.ingest_file(source_id, path)       # file upload ingest")
    print("      client.stats()                            # dashboard stats")


# ╔════════════════════════════════════════════════════════════════════════╗
# ║  SECTION 15 — Summary                                                 ║
# ╚════════════════════════════════════════════════════════════════════════╝

print("\n" + "=" * 72)
print("  SUMMARY")
print("=" * 72)

print(f"""
    Data Sources:
      - CRM Contacts          : 100 rows  (customer_id, full_name, email_address, phone_number)
      - Stripe Payments        : 100 rows  (stripe_id, cust_email, customer_name)
      - Support Tickets        : 100 rows  (ticket_id, reporter_email, reporter_name, reporter_phone)
      - Ecommerce Orders       : 100 rows  (order_id, buyer_email, buyer_first_name, buyer_phone)
      - Total input entities   : {total_input}

    Spec v1 (email + phone exact):
      - Clusters               : {result_v1_csv.cluster_count}
      - Merge rate             : {result_v1_csv.merge_rate:.2%}
      - Decisions              : {len(result_v1_csv.decisions)}
      - Golden records         : {len(result_v1_csv.golden_records)}

    Spec v2 (+ name similarity, lower thresholds):
      - Clusters               : {result_v2_csv.cluster_count}
      - Merge rate             : {result_v2_csv.merge_rate:.2%}
      - Delta from v1          : {result_v1_csv.cluster_count - result_v2_csv.cluster_count:+d} clusters

    Diff (v1 → v2):
      - Rules added            : {changes.rules_added}
      - Thresholds changed     : {changes.thresholds_changed}

    Adapters verified:
      - CSV    adapter         : OK
      - Pandas adapter         : OK (parity with CSV: {'MATCH' if result_v1_pd.cluster_count == result_v1_csv.cluster_count else 'DIFFER'})
      - JSON   adapter         : OK
      - Mixed  (CSV+Pandas)    : OK

    SDK features exercised:
      - Spec.from_string()     - Spec.from_file()
      - validate()             - validate().raise_on_error()
      - plan()                 - plan.summary()
      - plan.plan_hash         - plan.execution_stages
      - plan.match_strategies  - plan.risk_flags
      - plan.blocking          - plan.to_dict()
      - diff()                 - diff.rules_added/removed/modified
      - diff.thresholds_changed- diff.summary
      - Source.from_csv()      - Source.from_pandas()
      - Source.from_json()     - source.schema()
      - source.iter_rows()     - source.to_entities()
      - reconcile()            - result.clusters
      - result.cluster_count   - result.merge_rate
      - result.golden_records  - result.decisions
      - result.telemetry       - result.to_pandas()
      - Client (Cloud API)     - client.specs/entities/sources/rules/...
""")

print("=" * 72)
print("  Done. All Kanoniv features demonstrated successfully.")
print("=" * 72)

# Cleanup temp files
spec_file.unlink(missing_ok=True)
json_path.unlink(missing_ok=True)
