"""TrustMem HTTP Server — FastAPI wrapper for core CRUD operations.

Requires: ``pip install trustmem[server]``

Usage::

    # Start server (default: no auth, port 8080)
    python -m trustmem

    # With API key authentication
    TRUSTMEM_API_KEY=tm-xxx python -m trustmem

    # Custom port
    PORT=3000 python -m trustmem
"""
from __future__ import annotations

import logging
import os
import secrets
from typing import Any, Optional

logger = logging.getLogger(__name__)

try:
    from dotenv import load_dotenv
    load_dotenv(override=False)
except ImportError:
    pass

try:
    from fastapi import FastAPI, Header, HTTPException, Query, Request
    from fastapi.responses import JSONResponse
    from pydantic import BaseModel, Field
except ImportError:
    raise ImportError(
        "FastAPI is required for the TrustMem server. "
        "Install with: pip install trustmem[server]"
    )

from contextlib import asynccontextmanager

from trustmem import __version__

# ------------------------------------------------------------------
# Pydantic request models
# ------------------------------------------------------------------

class AddMemoryRequest(BaseModel):
    data: list[dict[str, Any]] = Field(..., description="List of message dicts")
    user_id: Optional[str] = Field(None, description="User ID for scoping")
    tags: Optional[list[str]] = None
    metadata: Optional[dict[str, Any]] = None
    session_id: Optional[str] = None
    dry_run: bool = False
    source_trust: Optional[str] = Field(
        None,
        description="Source trust level: 'verified', 'input', or 'derived'",
    )
    expires_at: Optional[str] = Field(
        None,
        description="TTL expiry datetime (ISO 8601)",
    )
    visibility: Optional[str] = Field(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)")


class SearchRequest(BaseModel):
    query: str
    user_id: Optional[str] = Field(None, description="User ID for scoping")
    limit: int = Field(10, ge=1, le=1000)
    min_source_trust: Optional[str] = Field(
        None,
        description="Minimum source trust filter: 'verified', 'input', or 'derived'",
    )
    filters: Optional[dict[str, Any]] = Field(
        None,
        description="Metadata pre-filters. Example: {\"source\": \"salesforce\"}",
    )
    exclude_expired: bool = Field(False, description="Exclude expired memories")
    memory_type: Optional[str] = Field(None, description="Filter by memory type: 'semantic' or 'document'")
    search_mode: Optional[str] = Field(None, description="Search mode: 'hybrid' (default), 'vector', or 'keyword'")
    visibility: Optional[str] = Field(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)")


# ------------------------------------------------------------------
# Pydantic response models (H4: OpenAPI schema generation)
# ------------------------------------------------------------------

class HealthResponse(BaseModel):
    status: str

class VersionResponse(BaseModel):
    version: str

class DeleteResponse(BaseModel):
    deleted: bool


# ------------------------------------------------------------------
# App
# ------------------------------------------------------------------

_tm: Any = None
_API_KEY: Optional[str] = os.getenv("TRUSTMEM_API_KEY")


def _set_tm_for_testing(tm: Any) -> None:
    """Inject TrustMem instance for testing (replaces global)."""
    global _tm
    _tm = tm


def _set_api_key_for_testing(key: Optional[str]) -> None:
    """Override API key for testing."""
    global _API_KEY
    _API_KEY = key


def _get_tm() -> Any:
    if _tm is None:
        raise HTTPException(status_code=503, detail="TrustMem not initialized")
    return _tm


# ------------------------------------------------------------------
# Lifespan
# ------------------------------------------------------------------

@asynccontextmanager
async def lifespan(application: Any):
    global _tm
    if _tm is None:
        from trustmem.sdk import TrustMem
        _tm = TrustMem()
    yield
    if _tm is not None:
        _tm.close()


app = FastAPI(title="TrustMem", version=__version__, lifespan=lifespan)

# CORS — configurable via CORS_ORIGINS env var
# Default: localhost only (secure). Set CORS_ORIGINS=* to allow all origins.
_DEFAULT_CORS = "http://localhost:8080,http://localhost:3000,http://127.0.0.1:8080,http://127.0.0.1:3000"
try:
    from fastapi.middleware.cors import CORSMiddleware

    _cors_origins = os.getenv("CORS_ORIGINS", _DEFAULT_CORS).split(",")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[o.strip() for o in _cors_origins],
        allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
        allow_headers=["Authorization", "Content-Type"],
    )
except ImportError:
    pass  # CORSMiddleware is included with FastAPI; guard is defensive only


# ------------------------------------------------------------------
# Request body size limit (default: 10MB)
# ------------------------------------------------------------------
_MAX_BODY_SIZE = int(os.getenv("TRUSTMEM_MAX_BODY_SIZE", str(10 * 1024 * 1024)))


@app.middleware("http")
async def body_size_limit_middleware(request: Request, call_next: Any) -> Any:
    """Reject requests with body larger than TRUSTMEM_MAX_BODY_SIZE."""
    content_length = request.headers.get("content-length")
    if content_length and int(content_length) > _MAX_BODY_SIZE:
        return JSONResponse(
            status_code=413,
            content={"detail": "Request body too large"},
        )
    return await call_next(request)


@app.exception_handler(Exception)
async def _global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    """Catch unhandled exceptions to prevent stack trace leakage."""
    logger.exception("Unhandled error on %s %s", request.method, request.url.path)
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal server error"},
    )


# ------------------------------------------------------------------
# Auth middleware
# ------------------------------------------------------------------

@app.middleware("http")
async def auth_middleware(request: Request, call_next: Any) -> Any:
    if _API_KEY is None:
        return await call_next(request)
    if request.url.path in ("/health", "/playground", "/chat"):
        return await call_next(request)
    auth = request.headers.get("Authorization", "")
    expected = f"Bearer {_API_KEY}"
    if not secrets.compare_digest(auth, expected):
        logger.warning(
            "Auth failed: %s %s from %s",
            request.method, request.url.path, request.client.host if request.client else "unknown",
        )
        return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
    return await call_next(request)


# ------------------------------------------------------------------
# Health & Version
# ------------------------------------------------------------------

@app.get("/health", response_model=HealthResponse)
def health() -> dict:
    """Health check endpoint. Always public (no auth required)."""
    return {"status": "ok"}


@app.get("/v1/version", response_model=VersionResponse)
def version() -> dict:
    """Return server version."""
    return {"version": __version__}


# ------------------------------------------------------------------
# Core CRUD endpoints
# ------------------------------------------------------------------

@app.post("/v1/memories")
def add_memory(req: AddMemoryRequest) -> dict:
    """Extract and store memories from conversation messages."""
    tm = _get_tm()
    try:
        add_kwargs: dict[str, Any] = {
            "user_id": req.user_id,
            "tags": req.tags,
            "metadata": req.metadata,
            "session_id": req.session_id,
            "dry_run": req.dry_run,
        }
        if req.visibility is not None:
            add_kwargs["visibility"] = req.visibility
        if req.source_trust is not None:
            add_kwargs["source_trust"] = req.source_trust
        if req.expires_at is not None:
            add_kwargs["expires_at"] = req.expires_at
        return tm.add(req.data, **add_kwargs)
    except (TypeError, ValueError) as e:
        logger.warning("add_memory validation error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request data")


@app.post("/v1/memories/search")
def search_memories(
    req: SearchRequest,
    x_trustmem_accessor: Optional[str] = Header(None),
) -> dict:
    """Semantic search for memories."""
    tm = _get_tm()
    try:
        search_kwargs: dict[str, Any] = {
            "user_id": req.user_id,
            "limit": req.limit,
            "min_source_trust": req.min_source_trust,
            "filters": req.filters,
            "accessor": x_trustmem_accessor or "system",
            "exclude_expired": req.exclude_expired,
            "memory_type": req.memory_type,
            "visibility": req.visibility,
        }
        if req.search_mode is not None:
            search_kwargs["search_mode"] = req.search_mode
        return tm.search(req.query, **search_kwargs)
    except (TypeError, ValueError) as e:
        logger.warning("search_memories validation error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request data")


class SearchRawRequest(BaseModel):
    query: str
    user_id: Optional[str] = Field(None, description="User ID for scoping")
    limit: int = Field(10, ge=1, le=1000)
    filters: Optional[dict[str, Any]] = Field(
        None,
        description="Metadata pre-filters. Example: {\"source\": \"salesforce\"}",
    )
    exclude_expired: bool = Field(False, description="Exclude expired memories")
    memory_type: Optional[str] = Field(None, description="Filter by memory type: 'semantic' or 'document'")
    visibility: Optional[str] = Field(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)")


@app.post("/v1/memories/search-raw")
def search_memories_raw(
    req: SearchRawRequest,
    x_trustmem_accessor: Optional[str] = Header(None),
) -> dict:
    """Pure cosine similarity search without TrustMem ranking."""
    tm = _get_tm()
    try:
        return tm.search_raw(
            req.query,
            user_id=req.user_id,
            limit=req.limit,
            filters=req.filters,
            accessor=x_trustmem_accessor or "system",
            exclude_expired=req.exclude_expired,
            memory_type=req.memory_type,
            visibility=req.visibility,
        )
    except (TypeError, ValueError) as e:
        logger.warning("search_memories_raw validation error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request data")


@app.get("/v1/memories")
def get_all_memories(
    user_id: Optional[str] = Query(None, description="User ID for scoping"),
    limit: int = Query(100, ge=1, le=10000),
    offset: int = Query(0, ge=0),
    relation_limit: int = Query(100, ge=1),
    min_source_trust: Optional[str] = Query(None, description="Minimum source trust filter"),
    memory_type: Optional[str] = Query(None, description="Filter by memory type: 'semantic' or 'document'"),
    visibility: Optional[str] = Query(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)"),
    x_trustmem_accessor: Optional[str] = Header(None),
) -> dict:
    """Get all memories and relations."""
    tm = _get_tm()
    try:
        return tm.get_all(
            user_id=user_id, limit=limit, offset=offset,
            relation_limit=relation_limit, min_source_trust=min_source_trust,
            accessor=x_trustmem_accessor or "system",
            memory_type=memory_type,
            visibility=visibility,
        )
    except (TypeError, ValueError) as e:
        logger.warning("get_all_memories validation error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request parameters")


@app.delete("/v1/memories/{memory_id}", response_model=DeleteResponse)
def delete_memory(
    memory_id: str,
) -> dict:
    """Delete a memory by ID."""
    tm = _get_tm()
    try:
        deleted = tm.delete(memory_id)
    except KeyError:
        raise HTTPException(status_code=404, detail="Memory not found")
    except PermissionError:
        raise HTTPException(status_code=403, detail="Permission denied")
    return {"deleted": deleted}


@app.get("/v1/memories/{memory_id}")
def get_memory(memory_id: str) -> dict:
    """Get a single memory by ID."""
    tm = _get_tm()
    try:
        return tm.get(memory_id)
    except Exception:
        raise HTTPException(status_code=404, detail="Memory not found")


# ------------------------------------------------------------------
# Admin endpoints (SDK management methods exposed for playground)
# ------------------------------------------------------------------


@app.get("/v1/admin/stats")
def admin_stats(user_id: Optional[str] = Query(None)) -> dict:
    """Memory statistics including confidence distribution."""
    tm = _get_tm()
    return tm.stats(user_id=user_id)


@app.get("/v1/admin/entity-stats")
def admin_entity_stats(user_id: Optional[str] = Query(None)) -> dict:
    """Entity and edge statistics."""
    tm = _get_tm()
    return tm.entity_stats(user_id=user_id)


@app.get("/v1/admin/verify/{memory_id}")
def admin_verify(memory_id: str) -> dict:
    """Verify integrity of a single memory (content_hash check)."""
    tm = _get_tm()
    try:
        return tm.verify(memory_id)
    except KeyError:
        raise HTTPException(status_code=404, detail="Memory not found")


@app.get("/v1/admin/verify-all")
def admin_verify_all() -> dict:
    """Verify integrity of all memories."""
    tm = _get_tm()
    return tm.verify_all()


@app.get("/v1/admin/audit-trail/{memory_id}")
def admin_audit_trail(memory_id: str) -> list:
    """Get audit trail for a specific memory."""
    tm = _get_tm()
    try:
        return tm.audit_trail(memory_id)
    except KeyError:
        raise HTTPException(status_code=404, detail="Memory not found")


@app.get("/v1/admin/relations")
def admin_search_relations(
    entity: str = Query(..., description="Entity name to search"),
    user_id: Optional[str] = Query(None),
    limit: int = Query(20, ge=1, le=100),
) -> list:
    """Search entity relations by entity name."""
    tm = _get_tm()
    return tm.search_relations(entity, user_id=user_id, limit=limit)


@app.get("/v1/admin/graph")
def admin_get_graph(
    entity: str = Query(..., description="Starting entity name"),
    user_id: Optional[str] = Query(None),
    hops: int = Query(2, ge=1, le=3),
) -> dict:
    """Get subgraph starting from an entity."""
    tm = _get_tm()
    return tm.get_graph(entity, user_id=user_id, hops=hops)


class AddRawRequest(BaseModel):
    content: str = Field(..., description="Memory text content")
    embedding: Optional[list[float]] = Field(
        None, description="Pre-computed embedding vector",
    )
    user_id: Optional[str] = Field(None, description="User ID for scoping")
    tags: Optional[list[str]] = None
    metadata: Optional[dict[str, Any]] = None
    confidence: str = Field("medium", description="high/medium/low")
    source_trust: Optional[str] = Field(
        "derived",
        description="Source trust level: 'verified', 'input', or 'derived'",
    )
    memory_type: str = Field("semantic", description="Memory type: 'semantic' or 'document'")
    expires_at: Optional[str] = Field(
        None,
        description="TTL expiry datetime (ISO 8601)",
    )
    visibility: Optional[str] = Field(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)")


@app.post("/v1/admin/add-raw")
def admin_add_raw(req: AddRawRequest) -> dict:
    """Add a memory directly, bypassing LLM extraction pipeline.

    Use for pre-processed data where embedding/chunking is already done.
    """
    tm = _get_tm()
    try:
        add_raw_kwargs: dict[str, Any] = {
            "content": req.content,
            "embedding": req.embedding,
            "user_id": req.user_id,
            "tags": req.tags,
            "metadata": req.metadata,
            "confidence": req.confidence,
            "source_trust": req.source_trust or "derived",
            "memory_type": req.memory_type,
            "expires_at": req.expires_at,
        }
        if req.visibility is not None:
            add_raw_kwargs["visibility"] = req.visibility
        return tm.add_raw(**add_raw_kwargs)
    except (TypeError, ValueError) as e:
        logger.warning("admin_add_raw validation error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request data")


@app.get("/v1/admin/list-all")
def admin_list_all(
    user_id: Optional[str] = Query(None, description="Filter by user"),
    limit: int = Query(100, ge=1, le=10000),
    offset: int = Query(0, ge=0),
    memory_type: Optional[str] = Query(None, description="Filter by memory type: 'semantic' or 'document'"),
) -> dict:
    """List all memories without visibility filter (admin only).

    Unlike GET /v1/memories, this ignores visibility rules and returns
    all memories matching the given filters.
    """
    tm = _get_tm()
    return tm.list_all(
        user_id=user_id,
        limit=limit,
        offset=offset,
        memory_type=memory_type,
    )


@app.post("/v1/admin/reset")
def admin_reset() -> dict:
    """Clear all data (development only)."""
    tm = _get_tm()
    tm.reset()
    return {"status": "reset complete"}


class CleanupExpiredRequest(BaseModel):
    grace_days: int = Field(0, ge=0, description="Days after expiry before deletion")
    dry_run: bool = Field(False, description="Count only")


@app.post("/v1/admin/cleanup-expired")
def admin_cleanup_expired(req: CleanupExpiredRequest) -> dict:
    """Delete expired memories (TTL + grace period)."""
    tm = _get_tm()
    return tm.cleanup_expired(grace_days=req.grace_days, dry_run=req.dry_run)


@app.get("/v1/admin/access-log")
def admin_access_log(
    memory_id: Optional[str] = Query(None),
    accessor: Optional[str] = Query(None),
    since: Optional[str] = Query(None),
    limit: int = Query(50, ge=1, le=10000),
    offset: int = Query(0, ge=0),
) -> list:
    """Query access log (Read Audit)."""
    tm = _get_tm()
    return tm.access_log(
        memory_id=memory_id,
        accessor=accessor,
        since=since,
        limit=limit,
        offset=offset,
    )


@app.get("/v1/admin/access-log/verify")
def admin_verify_access_chain() -> dict:
    """Verify access log hash chain integrity."""
    tm = _get_tm()
    return tm.verify_access_chain()


@app.delete("/v1/admin/access-log")
def admin_purge_access_log(
    before: str = Query(..., description="ISO 8601 date/datetime"),
) -> dict:
    """Purge access log records older than the given date."""
    tm = _get_tm()
    try:
        return tm.purge_access_log(before)
    except (TypeError, ValueError) as e:
        logger.warning("purge_access_log error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request parameters")


@app.get("/v1/admin/contradictions/{memory_id}")
def admin_contradictions(
    memory_id: str,
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
) -> list:
    """Get contradiction relationships for a memory."""
    tm = _get_tm()
    return tm.contradictions(memory_id, limit=limit, offset=offset)


@app.get("/v1/admin/provenance/{memory_id}")
def admin_provenance(
    memory_id: str,
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
) -> list:
    """Get provenance links for a memory."""
    tm = _get_tm()
    return tm.provenance(memory_id, limit=limit, offset=offset)


class EmbedRequest(BaseModel):
    texts: list[str] = Field(..., description="List of texts to embed", min_length=1, max_length=100)


class AddRawBatchItem(BaseModel):
    content: str = Field(..., description="Memory text content")
    embedding: Optional[list[float]] = Field(None)
    tags: Optional[list[str]] = None
    metadata: Optional[dict[str, Any]] = None
    confidence: str = Field("medium")
    source_trust: str = Field("derived")
    memory_type: str = Field("semantic", description="Memory type: 'semantic' or 'document'")
    expires_at: Optional[str] = None


class AddRawBatchRequest(BaseModel):
    items: list[AddRawBatchItem] = Field(..., min_length=1, max_length=500)
    user_id: Optional[str] = Field(None, description="User ID for scoping")
    visibility: Optional[str] = Field(None, description="Set to 'organization' for org-shared knowledge (user_id=NULL)")


class UpdateRawRequest(BaseModel):
    memory_id: str = Field(..., description="ID of the memory to supersede")
    content: str = Field(..., description="New memory content")
    embedding: Optional[list[float]] = Field(None, description="Pre-computed embedding")
    tags: Optional[list[str]] = None
    metadata: Optional[dict[str, Any]] = None
    reason: str = Field("", description="Update reason for audit trail")


@app.post("/v1/admin/update-raw")
def admin_update_raw(req: UpdateRawRequest) -> dict:
    """Create a new version of a memory (version management)."""
    tm = _get_tm()
    try:
        return tm.update_raw(
            memory_id=req.memory_id,
            content=req.content,
            embedding=req.embedding,
            tags=req.tags,
            metadata=req.metadata,
            reason=req.reason,
        )
    except (TypeError, ValueError) as e:
        logger.warning("admin_update_raw error: %s", e)
        raise HTTPException(status_code=400, detail="Invalid request data")
    except Exception as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail="Memory not found")
        raise


class ForgetRequest(BaseModel):
    entity: str = Field(..., description="Entity name to forget")
    user_id: Optional[str] = None
    dry_run: bool = Field(False, description="Preview impact without deleting")
    reason: str = Field("manual", description="Deletion reason for audit")


@app.post("/v1/admin/embed")
def admin_embed(req: EmbedRequest) -> dict:
    """Generate embeddings for a list of texts using the configured AI provider."""
    tm = _get_tm()
    ai = tm._handler._ai
    embeddings = []
    for text in req.texts:
        embeddings.append(ai.embed(text))
    return {"embeddings": embeddings, "count": len(embeddings)}


@app.post("/v1/admin/add-raw-batch")
def admin_add_raw_batch(req: AddRawBatchRequest) -> dict:
    """Add multiple memories directly via add_raw(), bypassing LLM pipeline."""
    tm = _get_tm()
    results = []
    errors = []
    for i, item in enumerate(req.items):
        try:
            batch_kwargs: dict[str, Any] = {
                "content": item.content,
                "embedding": item.embedding,
                "user_id": req.user_id,
                "tags": item.tags,
                "metadata": item.metadata,
                "confidence": item.confidence,
                "source_trust": item.source_trust or "derived",
                "memory_type": item.memory_type,
                "expires_at": item.expires_at,
            }
            if req.visibility is not None:
                batch_kwargs["visibility"] = req.visibility
            r = tm.add_raw(**batch_kwargs)
            results.append(r)
        except Exception as e:
            logger.warning("add-raw-batch item %d error: %s", i, e)
            errors.append({"index": i, "error": str(e)})
    return {"added": len(results), "errors": errors, "results": results}


@app.get("/v1/admin/memory-detail/{memory_id}")
def admin_memory_detail(
    memory_id: str,
    limit: int = Query(10, ge=1, le=100),
) -> dict:
    """Get memory with contradictions, provenance, and audit trail in one call."""
    tm = _get_tm()
    try:
        memory = tm.get(memory_id)
    except Exception:
        raise HTTPException(status_code=404, detail="Memory not found")
    return {
        "memory": memory,
        "contradictions": tm.contradictions(memory_id, limit=limit, offset=0),
        "provenance": tm.provenance(memory_id, limit=limit, offset=0),
        "audit_trail": tm.audit_trail(memory_id, limit=limit, offset=0),
    }


@app.post("/v1/admin/forget")
def admin_forget(req: ForgetRequest) -> dict:
    """Delete all data related to an entity (GDPR erasure)."""
    tm = _get_tm()
    return tm.forget(
        entity=req.entity,
        user_id=req.user_id,
        dry_run=req.dry_run,
        reason=req.reason,
    )


# ------------------------------------------------------------------
# Playground UI
# ------------------------------------------------------------------

from fastapi.responses import HTMLResponse
from starlette.responses import Response

_PLAYGROUND_HTML = """\
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrustMem Playground</title>
<style>
  [data-theme="light"] {
    --bg: #ffffff; --surface: #f6f8fa; --border: #d0d7de;
    --text: #1f2328; --text2: #656d76; --accent: #0969da;
    --green: #1a7f37; --red: #cf222e; --yellow: #9a6700;
    --font: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
  }
  [data-theme="dark"] {
    --bg: #0d1117; --surface: #161b22; --border: #30363d;
    --text: #e6edf3; --text2: #8b949e; --accent: #58a6ff;
    --green: #3fb950; --red: #f85149; --yellow: #d29922;
    --font: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { background: var(--bg); color: var(--text); font-family: var(--font); font-size: 14px; }
  .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
  h1 { font-size: 20px; margin-bottom: 4px; }
  .subtitle { color: var(--text2); font-size: 12px; margin-bottom: 20px; }
  .tabs { display: flex; gap: 2px; margin-bottom: 16px; }
  .tab {
    padding: 8px 16px; background: var(--surface); border: 1px solid var(--border);
    color: var(--text2); cursor: pointer; border-radius: 6px 6px 0 0;
    font-family: var(--font); font-size: 13px;
  }
  .tab.active { color: var(--accent); border-bottom-color: var(--bg); background: var(--bg); }
  .panel { display: none; }
  .panel.active { display: block; }
  .panel, .card { max-width: 100%; overflow: hidden; }
  .card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 6px; padding: 16px; margin-bottom: 12px;
  }
  label { display: block; color: var(--text2); font-size: 12px; margin-bottom: 4px; }
  input, textarea, select {
    width: 100%; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border);
    color: var(--text); border-radius: 4px; font-family: var(--font); font-size: 13px;
    margin-bottom: 10px;
  }
  textarea { min-height: 100px; resize: vertical; }
  .row { display: flex; gap: 10px; }
  .row > * { flex: 1; }
  button {
    padding: 8px 20px; background: var(--accent); color: #fff; border: none;
    border-radius: 6px; cursor: pointer; font-family: var(--font); font-size: 13px;
    font-weight: 600;
  }
  button:hover { opacity: 0.9; }
  button.danger { background: var(--red); }
  .result-box {
    background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
    padding: 12px; margin-top: 12px; max-height: 500px; overflow-y: auto;
    white-space: pre-wrap; word-break: break-all; font-size: 12px; line-height: 1.5;
  }
  .status { font-size: 12px; margin-top: 8px; }
  .status.ok { color: var(--green); }
  .status.err { color: var(--red); }
  .badge {
    display: inline-block; padding: 2px 8px; border-radius: 10px;
    font-size: 11px; font-weight: 600;
  }
  .conf-high { background: #3fb95022; color: var(--green); }
  .conf-medium { background: #d2992222; color: var(--yellow); }
  .conf-low { background: #f8514922; color: var(--red); }
  .memory-card {
    background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
    padding: 10px 12px; margin-bottom: 8px;
  }
  .memory-card .meta { color: var(--text2); font-size: 11px; margin-top: 6px; }
  .memory-card .content { margin: 4px 0; }
  .memory-card .scores { display: flex; gap: 12px; margin-top: 4px; font-size: 11px; }
  .del-btn {
    background: none; border: 1px solid var(--red); color: var(--red);
    padding: 2px 8px; font-size: 11px; border-radius: 4px; cursor: pointer;
    float: right;
  }
  .del-btn:hover { background: var(--red); color: #fff; }
  .help { color: var(--text2); font-size: 11px; margin-bottom: 8px; }
  .config-bar {
    display: flex; gap: 10px; align-items: center; margin-bottom: 16px;
    padding: 8px 12px; background: var(--surface); border: 1px solid var(--border);
    border-radius: 6px; font-size: 12px;
  }
  .config-bar label { margin: 0; }
  .config-bar input { width: 250px; margin: 0; }
  .relation-card {
    background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
    padding: 8px 12px; margin-bottom: 6px; font-size: 12px;
  }
  .rel-arrow { color: var(--accent); font-weight: 600; }
  .theme-btn { background: var(--surface); border: 1px solid var(--border); color: var(--text2); padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: auto; }
  .theme-btn:hover { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<div class="container">
  <h1>TrustMem Playground</h1>
  <p class="subtitle">Interactive testing for TrustMem API</p>

  <div class="config-bar">
    <label>API Key:</label>
    <input type="password" id="apiKey" placeholder="(none = no auth)">
    <label>User ID:</label>
    <input type="text" id="globalUser" value="playground" style="width:150px">
    <button class="theme-btn" onclick="toggleTheme()">Dark</button>
  </div>

  <div class="tabs">
    <div class="tab active" onclick="switchTab('add')">Add</div>
    <div class="tab" onclick="switchTab('search')">Search</div>
    <div class="tab" onclick="switchTab('list')">List All</div>
    <div class="tab" onclick="switchTab('import')">Import</div>
    <div class="tab" onclick="switchTab('stats')">Stats</div>
    <div class="tab" onclick="switchTab('admin')">Admin</div>
  </div>

  <!-- ADD -->
  <div id="panel-add" class="panel active">
    <div class="card">
      <p class="help">Enter conversation messages (one per line: <code>role: content</code> or <code>name(role): content</code>)</p>
      <textarea id="addMessages" placeholder="alex(user): Paris is the capital of France&#10;assistant: That's correct!">alex(user): Paris is the capital of France
assistant: That's correct!</textarea>
      <button onclick="doAdd()">Add Memories</button>
      <div id="addResult"></div>
    </div>
  </div>

  <!-- SEARCH -->
  <div id="panel-search" class="panel">
    <div class="card">
      <label>Query</label>
      <input type="text" id="searchQuery" placeholder="capital of France">
      <div class="row">
        <div>
          <label>Limit</label>
          <input type="number" id="searchLimit" value="10" min="1" max="100">
        </div>
      </div>
      <button onclick="doSearch()">Search</button>
      <div id="searchResult"></div>
    </div>
  </div>

  <!-- LIST ALL -->
  <div id="panel-list" class="panel">
    <div class="card">
      <div class="row">
        <div>
          <label>Limit</label>
          <input type="number" id="listLimit" value="50" min="1" max="1000">
        </div>
        <div>
          <label>Offset</label>
          <input type="number" id="listOffset" value="0" min="0">
        </div>
      </div>
      <button onclick="doList()">Get All</button>
      <div id="listResult"></div>
    </div>
  </div>

  <!-- IMPORT -->
  <div id="panel-import" class="panel">
    <div class="card">
      <h3 style="margin-bottom:8px;color:var(--accent)">Data Import (add_raw)</h3>
      <p class="help" style="margin-bottom:12px">Paste text, chunk it, preview, then import into TrustMem via add_raw() (bypasses LLM pipeline).</p>

      <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
        <div>
          <label>Chunking Method</label>
          <select id="importChunkMethod">
            <option value="paragraph">Paragraph (blank line)</option>
            <option value="separator">Custom Separator</option>
            <option value="fixed">Fixed Length</option>
          </select>
        </div>
        <div>
          <label>Separator / Chunk Size</label>
          <input type="text" id="importChunkParam" placeholder="--- or 500" value="">
        </div>
      </div>

      <label>Source Text</label>
      <textarea id="importText" style="min-height:160px" placeholder="Paste your text here..."></textarea>

      <div style="display:flex;gap:8px;margin-bottom:12px">
        <button onclick="doChunk()">Chunk</button>
        <button onclick="document.getElementById('importFile').click()" style="background:var(--surface);color:var(--text);border:1px solid var(--border)">Load File</button>
        <input type="file" id="importFile" accept=".txt,.md,.csv,.json" style="display:none" onchange="loadImportFile(event)">
      </div>

      <div id="importChunks" style="display:none">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
          <strong id="importChunkCount" style="color:var(--accent)">0 chunks</strong>
          <button onclick="doRemoveSelected()" style="background:var(--red);font-size:11px;padding:4px 10px">Remove Selected</button>
        </div>
        <div id="importChunkList" style="max-height:300px;overflow-y:auto;overflow-x:hidden;margin-bottom:12px"></div>

        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px">
          <div>
            <label>Confidence</label>
            <select id="importConfidence">
              <option value="high" selected>high</option>
              <option value="medium">medium</option>
              <option value="low">low</option>
            </select>
          </div>
          <div>
            <label>Source Trust</label>
            <select id="importSourceTrust">
              <option value="verified" selected>verified</option>
              <option value="input">input</option>
            </select>
          </div>
          <div>
            <label>Tags (comma-separated)</label>
            <input type="text" id="importTags" placeholder="doc, import">
          </div>
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
          <div>
            <label>Expires At (ISO 8601, optional)</label>
            <input type="datetime-local" id="importExpiresAt" value="">
          </div>
          <div style="display:flex;align-items:flex-end;padding-bottom:10px">
            <button onclick="document.getElementById('importExpiresAt').value=''" style="background:var(--surface);color:var(--text);border:1px solid var(--border);font-size:11px;padding:6px 10px">Clear</button>
          </div>
        </div>

        <div style="display:flex;gap:8px;align-items:center">
          <button onclick="doImport()" style="background:var(--green);flex:1">Import All Chunks</button>
        </div>
      </div>

      <div id="importResult" style="margin-top:12px"></div>
    </div>
  </div>

  <!-- STATS -->
  <div id="panel-stats" class="panel">
    <div class="card">
      <button onclick="doStats()">Load Stats</button>
      <div id="statsResult"></div>
    </div>
  </div>

  <!-- ADMIN -->
  <div id="panel-admin" class="panel">
    <div class="card">
      <h3 style="margin-bottom:12px;color:var(--accent)">Admin Operations</h3>

      <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
        <div class="card" style="background:var(--bg)">
          <strong>Verify Memory</strong>
          <input type="text" id="verifyId" placeholder="memory_id" style="margin:6px 0">
          <button onclick="doVerify()">Verify</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Verify All</strong>
          <p class="help" style="margin:6px 0">Check integrity of all memories</p>
          <button onclick="doVerifyAll()">Verify All</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Audit Trail</strong>
          <input type="text" id="auditId" placeholder="memory_id" style="margin:6px 0">
          <button onclick="doAuditTrail()">Get Audit Trail</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Contradictions</strong>
          <input type="text" id="contradictId" placeholder="memory_id" style="margin:6px 0">
          <button onclick="doContradictions()">Get Contradictions</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Provenance</strong>
          <input type="text" id="provenanceId" placeholder="memory_id" style="margin:6px 0">
          <button onclick="doProvenance()">Get Provenance</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Entity Stats</strong>
          <p class="help" style="margin:6px 0">Entity and edge counts</p>
          <button onclick="doEntityStats()">Load Entity Stats</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Search Relations</strong>
          <input type="text" id="relEntity" placeholder="entity name" style="margin:6px 0">
          <button onclick="doSearchRelations()">Search</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Get Graph</strong>
          <input type="text" id="graphEntity" placeholder="entity name" style="margin:6px 0">
          <div class="row" style="margin-bottom:6px">
            <div><label>Hops</label><input type="number" id="graphHops" value="2" min="1" max="3" style="width:60px"></div>
          </div>
          <button onclick="doGetGraph()">Get Graph</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Update Memory</strong>
          <input type="text" id="updateId" placeholder="memory_id" style="margin:6px 0">
          <textarea id="updateContent" placeholder="New content" style="min-height:60px;margin-bottom:6px"></textarea>
          <input type="text" id="updateReason" placeholder="Reason (optional)" style="margin-bottom:6px">
          <button onclick="doUpdateRaw()">Update</button>
        </div>
        <div class="card" style="background:var(--bg)">
          <strong>Forget Entity (GDPR)</strong>
          <input type="text" id="forgetEntity" placeholder="entity name" style="margin:6px 0">
          <div style="margin-bottom:6px">
            <label><input type="checkbox" id="forgetDryRun" checked> Dry Run</label>
          </div>
          <button onclick="doForget()" style="background:var(--red)">Forget</button>
        </div>
      </div>

      <div class="card" style="background:var(--bg);margin-top:12px;border:1px solid var(--red)">
        <strong style="color:var(--red)">Reset (Development Only)</strong>
        <p class="help" style="margin:6px 0">Clears ALL data. This cannot be undone.</p>
        <button onclick="doReset()" style="background:var(--red)">Reset All Data</button>
      </div>

      <div id="adminResult" style="margin-top:12px"></div>
    </div>
  </div>
</div>

<script>
function toggleTheme() {
  const html = document.documentElement;
  const btn = document.querySelector('.theme-btn');
  if (html.getAttribute('data-theme') === 'light') {
    html.setAttribute('data-theme', 'dark');
    btn.textContent = 'Light';
    localStorage.setItem('trustmem-theme', 'dark');
  } else {
    html.setAttribute('data-theme', 'light');
    btn.textContent = 'Dark';
    localStorage.setItem('trustmem-theme', 'light');
  }
}
(function() {
  const saved = localStorage.getItem('trustmem-theme');
  if (saved === 'dark') {
    document.documentElement.setAttribute('data-theme', 'dark');
    document.addEventListener('DOMContentLoaded', () => {
      const btn = document.querySelector('.theme-btn');
      if (btn) btn.textContent = 'Light';
    });
  }
})();

function headers() {
  const h = {'Content-Type': 'application/json'};
  const key = document.getElementById('apiKey').value;
  if (key) h['Authorization'] = 'Bearer ' + key;
  return h;
}
function userId() { return document.getElementById('globalUser').value || null; }

function switchTab(name) {
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
  document.querySelector(`.tab[onclick="switchTab('${name}')"]`).classList.add('active');
  document.getElementById('panel-' + name).classList.add('active');
}

function confBadge(c) {
  const cls = c === 'high' ? 'conf-high' : c === 'low' ? 'conf-low' : 'conf-medium';
  return `<span class="badge ${cls}">${c || 'medium'}</span>`;
}

function findRelatedRelations(memory, relations) {
  if (!relations || !relations.length) return [];
  const c = (memory.content || '').toLowerCase();
  return relations.filter(r => {
    if (r.source_memory_id === memory.memory_id) return true;
    const s = (r.subject_name || '').toLowerCase();
    const o = (r.object_name || '').toLowerCase();
    return (s.length > 1 && c.includes(s)) || (o.length > 1 && c.includes(o));
  });
}

function renderRelationInline(r) {
  return `<div class="relation-card" style="margin:4px 0 0 12px;border-left:2px solid var(--accent);padding-left:10px">
    <strong>${esc(r.subject_name)}</strong>
    <span class="rel-arrow"> —[ ${esc(r.predicate)} ]→ </span>
    <strong>${esc(r.object_name)}</strong>
    <span style="color:var(--text2);font-size:11px;margin-left:8px">
      mentions: ${r.mention_count} · ${r.created_at || ''}
    </span>
  </div>`;
}

function renderMemories(memories, container, allowDelete, relations) {
  if (!memories.length) { container.innerHTML = '<p style="color:var(--text2)">No memories found.</p>'; return; }
  const rels = relations || [];
  container.innerHTML = memories.map(m => {
    const related = findRelatedRelations(m, rels);
    return `<div class="memory-card">
      ${allowDelete ? `<button class="del-btn" onclick="doDelete('${m.memory_id}', this)">delete</button>` : ''}
      <div class="content">${esc(m.content)}</div>
      <div class="scores">
        ${confBadge(m.confidence)}
        ${m.similarity !== undefined ? `<span>similarity: ${m.similarity.toFixed(4)}</span>` : ''}
        ${m.source_trust ? `<span>trust: ${m.source_trust}</span>` : ''}
        ${m.freshness !== undefined ? `<span>freshness: ${m.freshness}d</span>` : ''}
      </div>
      <div class="meta">
        id: ${m.memory_id}<br>
        created: ${m.created_at || '—'}
      </div>
      ${related.length ? '<div style="margin-top:6px">' + related.map(renderRelationInline).join('') + '</div>' : ''}
    </div>`;
  }).join('');
}

function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }

function showStatus(el, ok, msg) {
  el.innerHTML = `<div class="status ${ok ? 'ok' : 'err'}">${esc(msg)}</div>`;
}

function parseMessages(text) {
  return text.trim().split('\\n').filter(l => l.trim()).map(line => {
    const named = line.match(/^(\\w+)\\((\\w+)\\):\\s*(.+)$/);
    if (named) return {role: named[2], name: named[1], content: named[3]};
    const m = line.match(/^(\\w+):\\s*(.+)$/);
    if (m) return {role: m[1], content: m[2]};
    return {role: 'user', content: line.trim()};
  });
}

async function doAdd() {
  const el = document.getElementById('addResult');
  el.innerHTML = '<div class="status">Sending...</div>';
  const messages = parseMessages(document.getElementById('addMessages').value);
  try {
    const res = await fetch('/v1/memories', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({
        data: messages,
        user_id: userId(),
      })
    });
    const data = await res.json();
    if (!res.ok) { showStatus(el, false, `${res.status}: ${data.detail || JSON.stringify(data)}`); return; }
    const stats = data.stats || {};
    el.innerHTML = `
      <div class="status ok">Added ${stats.added || 0} memories, ${stats.entities_added || 0} entities, ${stats.relations_added || 0} relations</div>
      <div class="result-box">${JSON.stringify(data, null, 2)}</div>
    `;
  } catch (e) { showStatus(el, false, e.message); }
}

async function doSearch() {
  const el = document.getElementById('searchResult');
  el.innerHTML = '<div class="status">Searching...</div>';
  try {
    const res = await fetch('/v1/memories/search', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({
        query: document.getElementById('searchQuery').value,
        user_id: userId(),
        limit: parseInt(document.getElementById('searchLimit').value),
      })
    });
    const data = await res.json();
    if (!res.ok) { showStatus(el, false, `${res.status}: ${data.detail || JSON.stringify(data)}`); return; }
    const results = data.results || [];
    const relations = data.relations || [];
    el.innerHTML = `<div class="status ok">${results.length} results, ${relations.length} relations</div>`;
    const memDiv = document.createElement('div');
    el.appendChild(memDiv);
    renderMemories(results, memDiv, false, relations);
  } catch (e) { showStatus(el, false, e.message); }
}

async function doList() {
  const el = document.getElementById('listResult');
  el.innerHTML = '<div class="status">Loading...</div>';
  const limit = document.getElementById('listLimit').value;
  const offset = document.getElementById('listOffset').value;
  const uid = userId();
  const params = new URLSearchParams({limit, offset});
  if (uid) params.set('user_id', uid);
  try {
    const res = await fetch('/v1/memories?' + params, { headers: headers() });
    const data = await res.json();
    if (!res.ok) { showStatus(el, false, `${res.status}: ${data.detail || JSON.stringify(data)}`); return; }
    const memories = data.results || [];
    const relations = data.relations || [];
    el.innerHTML = `<div class="status ok">${memories.length} memories, ${relations.length} relations</div>`;
    const memDiv = document.createElement('div');
    el.appendChild(memDiv);
    renderMemories(memories, memDiv, true, relations);
  } catch (e) { showStatus(el, false, e.message); }
}

async function doDelete(memoryId, btn) {
  if (!confirm('Delete ' + memoryId + '?')) return;
  try {
    const res = await fetch('/v1/memories/' + memoryId, {
      method: 'DELETE', headers: headers(),
    });
    const data = await res.json();
    if (res.ok) {
      btn.closest('.memory-card').remove();
    } else {
      alert(data.detail || 'Delete failed');
    }
  } catch (e) { alert(e.message); }
}

async function doStats() {
  const el = document.getElementById('statsResult');
  el.innerHTML = '<div class="status">Loading...</div>';
  const uid = userId();
  const params = uid ? '?user_id=' + encodeURIComponent(uid) : '';
  try {
    const res = await fetch('/v1/memories' + params, { headers: headers() });
    const data = await res.json();
    if (!res.ok) { showStatus(el, false, `${res.status}: ${data.detail}`); return; }
    const memories = data.results || [];
    const conf = {high: 0, medium: 0, low: 0};
    memories.forEach(m => { const c = m.confidence || 'medium'; if (conf[c] !== undefined) conf[c]++; });
    el.innerHTML = `
      <div class="status ok">Loaded</div>
      <div class="memory-card">
        <strong>Summary</strong>
        <div class="scores" style="margin-top:8px;flex-wrap:wrap">
          <span>Total memories: ${memories.length}</span>
          <span>Relations: ${(data.relations || []).length}</span>
        </div>
        <div class="scores" style="margin-top:8px;flex-wrap:wrap">
          ${Object.entries(conf).map(([k,v]) => `<span>${confBadge(k)} ${v}</span>`).join('')}
        </div>
      </div>
    `;
  } catch (e) { showStatus(el, false, e.message); }
}

function adminResult(html) {
  document.getElementById('adminResult').innerHTML = html;
}

async function doVerify() {
  const id = document.getElementById('verifyId').value;
  if (!id) { adminResult('<div class="status err">Enter a memory_id</div>'); return; }
  try {
    const res = await fetch('/v1/admin/verify/' + encodeURIComponent(id), { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    const ok = data.valid !== false;
    adminResult(`<div class="status ${ok ? 'ok' : 'err'}">${ok ? 'Valid' : 'INVALID'}</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doVerifyAll() {
  adminResult('<div class="status">Verifying all...</div>');
  try {
    const res = await fetch('/v1/admin/verify-all', { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail}</div>`); return; }
    const total = data.total || 0;
    const valid = data.valid || 0;
    const invalid = data.invalid || 0;
    adminResult(`<div class="status ${invalid === 0 ? 'ok' : 'err'}">Total: ${total}, Valid: ${valid}, Invalid: ${invalid}</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doAuditTrail() {
  const id = document.getElementById('auditId').value;
  if (!id) { adminResult('<div class="status err">Enter a memory_id</div>'); return; }
  try {
    const res = await fetch('/v1/admin/audit-trail/' + encodeURIComponent(id), { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    adminResult(`<div class="status ok">${data.length} audit entries</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doEntityStats() {
  const uid = userId();
  const params = uid ? '?user_id=' + encodeURIComponent(uid) : '';
  try {
    const res = await fetch('/v1/admin/entity-stats' + params, { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail}</div>`); return; }
    adminResult(`<div class="status ok">Loaded</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doSearchRelations() {
  const entity = document.getElementById('relEntity').value;
  if (!entity) { adminResult('<div class="status err">Enter an entity name</div>'); return; }
  const uid = userId();
  const params = new URLSearchParams({ entity });
  if (uid) params.set('user_id', uid);
  try {
    const res = await fetch('/v1/admin/relations?' + params, { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    if (!data.length) { adminResult('<div class="status ok">No relations found</div>'); return; }
    const html = data.map(r => renderRelationInline(r)).join('');
    adminResult(`<div class="status ok">${data.length} relations</div>${html}`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doGetGraph() {
  const entity = document.getElementById('graphEntity').value;
  if (!entity) { adminResult('<div class="status err">Enter an entity name</div>'); return; }
  const uid = userId();
  const hops = document.getElementById('graphHops').value;
  const params = new URLSearchParams({ entity, hops });
  if (uid) params.set('user_id', uid);
  try {
    const res = await fetch('/v1/admin/graph?' + params, { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    const nodes = data.nodes || [];
    const edges = data.edges || [];
    adminResult(`<div class="status ok">${nodes.length} nodes, ${edges.length} edges</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doReset() {
  if (!confirm('This will DELETE ALL DATA. Are you sure?')) return;
  if (!confirm('Really? This cannot be undone.')) return;
  try {
    const res = await fetch('/v1/admin/reset', { method: 'POST', headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail}</div>`); return; }
    adminResult('<div class="status ok">All data has been reset</div>');
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doContradictions() {
  const id = document.getElementById('contradictId').value;
  if (!id) { adminResult('<div class="status err">Enter a memory_id</div>'); return; }
  try {
    const res = await fetch('/v1/admin/contradictions/' + encodeURIComponent(id), { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    adminResult(`<div class="status ok">${data.length} contradictions</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doProvenance() {
  const id = document.getElementById('provenanceId').value;
  if (!id) { adminResult('<div class="status err">Enter a memory_id</div>'); return; }
  try {
    const res = await fetch('/v1/admin/provenance/' + encodeURIComponent(id), { headers: headers() });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    adminResult(`<div class="status ok">${data.length} provenance links</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doUpdateRaw() {
  const id = document.getElementById('updateId').value;
  const content = document.getElementById('updateContent').value;
  if (!id || !content) { adminResult('<div class="status err">Enter memory_id and content</div>'); return; }
  try {
    const res = await fetch('/v1/admin/update-raw', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({
        memory_id: id,
        content: content,
        reason: document.getElementById('updateReason').value || '',
      })
    });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    adminResult(`<div class="status ok">Updated: ${data.old_memory_id} → ${data.new_memory_id}</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

async function doForget() {
  const entity = document.getElementById('forgetEntity').value;
  if (!entity) { adminResult('<div class="status err">Enter an entity name</div>'); return; }
  const dryRun = document.getElementById('forgetDryRun').checked;
  if (!dryRun && !confirm('This will permanently delete all data for "' + entity + '". Continue?')) return;
  try {
    const res = await fetch('/v1/admin/forget', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({
        entity: entity,
        user_id: userId(),
        dry_run: dryRun,
        reason: 'manual',
      })
    });
    const data = await res.json();
    if (!res.ok) { adminResult(`<div class="status err">${res.status}: ${data.detail || JSON.stringify(data)}</div>`); return; }
    const label = dryRun ? 'Dry run' : 'Deleted';
    adminResult(`<div class="status ${dryRun ? 'ok' : 'err'}">${label}: ${data.memories_deleted || data.memories_count || 0} memories, ${data.nodes_deleted || data.nodes_count || 0} nodes, ${data.edges_deleted || data.edges_count || 0} edges</div><div class="result-box">${JSON.stringify(data, null, 2)}</div>`);
  } catch (e) { adminResult(`<div class="status err">${esc(e.message)}</div>`); }
}

// --- Import functions ---
let importChunks = [];

function loadImportFile(event) {
  const file = event.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = function(e) {
    document.getElementById('importText').value = e.target.result;
  };
  reader.readAsText(file);
}

function doChunk() {
  const text = document.getElementById('importText').value.trim();
  if (!text) { showStatus(document.getElementById('importResult'), false, 'Enter some text first'); return; }
  const method = document.getElementById('importChunkMethod').value;
  const param = document.getElementById('importChunkParam').value.trim();

  let chunks = [];
  if (method === 'paragraph') {
    chunks = text.split(/\\n\\s*\\n/).map(s => s.trim()).filter(s => s.length > 0);
  } else if (method === 'separator') {
    const sep = param || '---';
    chunks = text.split(sep).map(s => s.trim()).filter(s => s.length > 0);
  } else if (method === 'fixed') {
    const size = parseInt(param) || 500;
    for (let i = 0; i < text.length; i += size) {
      const chunk = text.substring(i, i + size).trim();
      if (chunk) chunks.push(chunk);
    }
  }

  importChunks = chunks.map((c, i) => ({ id: i, content: c, selected: false }));
  renderImportChunks();
}

function renderImportChunks() {
  const container = document.getElementById('importChunks');
  const list = document.getElementById('importChunkList');
  const count = document.getElementById('importChunkCount');
  container.style.display = importChunks.length ? 'block' : 'none';
  count.textContent = importChunks.length + ' chunks';
  list.innerHTML = importChunks.map((c, i) => {
    const preview = c.content.length > 120 ? c.content.substring(0, 120) + '...' : c.content;
    return `<div style="padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;margin-bottom:4px;font-size:12px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px"><label style="display:inline-flex;align-items:center;gap:4px;margin:0"><input type="checkbox" ${c.selected ? 'checked' : ''} onchange="importChunks[${i}].selected=this.checked"><span style="color:var(--accent);font-weight:600">#${i + 1}</span><span style="color:var(--text2)">(${c.content.length} chars)</span></label><button onclick="importChunks.splice(${i},1);renderImportChunks()" style="font-size:10px;padding:2px 6px;background:var(--red);color:#fff;border:none;border-radius:3px;cursor:pointer">x</button></div><div style="color:var(--text);white-space:pre-wrap;word-break:break-word">${esc(preview)}</div></div>`;
  }).join('');
}

function doRemoveSelected() {
  importChunks = importChunks.filter(c => !c.selected);
  renderImportChunks();
}

async function doImport() {
  if (!importChunks.length) return;
  const el = document.getElementById('importResult');
  const confidence = document.getElementById('importConfidence').value;
  const sourceTrust = document.getElementById('importSourceTrust').value;
  const tagsStr = document.getElementById('importTags').value.trim();
  const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(t => t) : null;
  const expiresAtVal = document.getElementById('importExpiresAt').value;
  const expiresAt = expiresAtVal ? new Date(expiresAtVal).toISOString() : null;

  el.innerHTML = '<div class="status">Importing ' + importChunks.length + ' chunks...</div>';

  try {
    el.innerHTML = '<div class="status">Generating embeddings for ' + importChunks.length + ' chunks...</div>';
    const embedRes = await fetch('/v1/admin/embed', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({ texts: importChunks.map(c => c.content) })
    });
    if (!embedRes.ok) {
      const err = await embedRes.json();
      showStatus(el, false, 'Embed failed: ' + (err.detail || embedRes.status));
      return;
    }
    const embedData = await embedRes.json();
    const embeddings = embedData.embeddings;

    el.innerHTML = '<div class="status">Sending ' + importChunks.length + ' chunks to add_raw...</div>';
    const items = importChunks.map((c, i) => ({
      content: c.content,
      embedding: embeddings[i],
      confidence: confidence,
      source_trust: sourceTrust,
      tags: tags,
      expires_at: expiresAt,
    }));

    const res = await fetch('/v1/admin/add-raw-batch', {
      method: 'POST', headers: headers(),
      body: JSON.stringify({ items: items, user_id: userId() })
    });
    const data = await res.json();
    if (!res.ok) {
      showStatus(el, false, (data.detail || res.status));
      return;
    }
    const errCount = (data.errors || []).length;
    el.innerHTML = `<div class="status ok">Imported ${data.added} memories${errCount ? `, ${errCount} errors` : ''}</div>` +
      (errCount ? `<div class="result-box">${JSON.stringify(data.errors, null, 2)}</div>` : '');
    importChunks = [];
    renderImportChunks();
  } catch (e) { showStatus(el, false, e.message); }
}
</script>
</body>
</html>
"""


@app.get("/playground", response_class=HTMLResponse)
def playground() -> Response:
    """Interactive playground UI for testing TrustMem API."""
    return Response(
        content=_PLAYGROUND_HTML,
        media_type="text/html",
        headers={"Cache-Control": "no-cache, no-store, must-revalidate"},
    )


# ------------------------------------------------------------------
# Chat Demo endpoint
# ------------------------------------------------------------------

class ChatRequest(BaseModel):
    message: str
    history: list[dict[str, str]] = Field(default_factory=list)
    user_id: Optional[str] = "playground"
    min_confidence: Optional[str] = None  # "high", "medium", "low" or None (no filter)


@app.post("/v1/chat")
def chat(req: ChatRequest) -> dict:
    """Chat with LLM + auto memory extraction.

    Sends conversation to LLM for a response, then stores the
    exchange in TrustMem for memory extraction.
    """
    tm = _get_tm()
    # Build messages for LLM
    messages = list(req.history)
    messages.append({"role": "user", "content": req.message})

    # Search existing memories for context
    memories_ctx = ""
    retrieved_memories = []
    try:
        search_result = tm.search(req.message, user_id=req.user_id, limit=5)
        all_results = search_result.get("results", [])
        # Only use memories with similarity >= 0.5 (filter out unrelated)
        relevant = [m for m in all_results if m.get("similarity", 0) >= 0.5]
        # Confidence prefilter: only include memories at or above min_confidence
        if req.min_confidence:
            _conf_rank = {"high": 2, "medium": 1, "low": 0}
            min_rank = _conf_rank.get(req.min_confidence, 0)
            relevant = [m for m in relevant if _conf_rank.get(m.get("confidence", "medium"), 1) >= min_rank]
        # Sort: confidence (high > medium > low), then similarity desc
        _conf_order = {"high": 0, "medium": 1, "low": 2}
        relevant.sort(key=lambda m: (_conf_order.get(m.get("confidence", "medium"), 1), -m.get("similarity", 0)))
        if relevant:
            mem_lines = [f"[{i+1}] {m['content']} (confidence: {m.get('confidence', '?')})" for i, m in enumerate(relevant)]
            memories_ctx = "\n".join(mem_lines)
            retrieved_memories = [
                {"index": i+1, "content": m["content"], "confidence": m.get("confidence", "medium"), "similarity": round(m.get("similarity", 0), 4), "memory_id": m.get("memory_id", "")}
                for i, m in enumerate(relevant)
            ]
    except Exception:
        pass

    # Build system prompt with memory context
    system_parts = [
        "You are a helpful assistant. Be concise and natural.",
        "Answer in the same language as the user's message.",
    ]
    if memories_ctx:
        system_parts.append(f"\nYou have the following numbered memories about this user:\n{memories_ctx}\nUse these memories to personalize your response when relevant, but don't mention them explicitly. After your response, on a new line, output ONLY a JSON line like: {{\"used\": [1, 3]}} listing the memory numbers you actually used. If you used none, output: {{\"used\": []}}. The JSON line must be the very last line.")

    system_prompt = "\n".join(system_parts)

    # Call LLM for response
    try:
        ai = tm._handler._ai
        llm_messages = [{"role": "system", "content": system_prompt}] + messages
        model = getattr(ai, "_llm_model", None) or getattr(ai, "_llm_deployment", "gpt-4o-mini")
        response = ai._client.chat.completions.create(
            model=model,
            messages=llm_messages,
            temperature=0.7,
            max_tokens=2000,
        )
        raw_reply = response.choices[0].message.content or ""
        # Parse used memory indices from JSON in response
        used_indices: list[int] = []
        import json as _json
        import re as _re
        # Match {"used": [...]} anywhere — tolerant of extra whitespace, newlines, trailing text
        _used_match = _re.search(r'\{["\']used["\']\s*:\s*\[[\d,\s]*\]\s*\}', raw_reply)
        if _used_match:
            try:
                # Normalize single quotes to double quotes for JSON parsing
                match_str = _used_match.group().replace("'", '"')
                parsed = _json.loads(match_str)
                used_indices = parsed.get("used", [])
            except Exception:
                pass
            # Remove the JSON and any surrounding whitespace/newlines from the reply
            before = raw_reply[:_used_match.start()].rstrip()
            after = raw_reply[_used_match.end():].strip()
            assistant_reply = (before + (" " + after if after else "")).rstrip()
        else:
            assistant_reply = raw_reply
        # Also strip any stray [mem:N]...[/mem] or [memo:N]...[/memo] markers
        assistant_reply = _re.sub(r'\[/?memo?:\d+\]', '', assistant_reply).strip()
    except Exception as e:
        logger.exception("Chat LLM call failed")
        raise HTTPException(status_code=502, detail="LLM call failed")

    # Store conversation in TrustMem (non-blocking best effort)
    extraction = {}
    try:
        conversation = [
            {"role": "user", "content": req.message},
            {"role": "assistant", "content": assistant_reply},
        ]
        extraction = tm.add(conversation, user_id=req.user_id)
    except Exception:
        logger.exception("Memory extraction failed for chat")

    # Mark which retrieved memories were used
    used_set = set(used_indices) if memories_ctx else set()
    for m in retrieved_memories:
        m["used"] = m["index"] in used_set

    return {
        "reply": assistant_reply,
        "extraction": extraction,
        "retrieved_memories": retrieved_memories,
    }


# ------------------------------------------------------------------
# Chat Demo UI
# ------------------------------------------------------------------

_CHAT_HTML = """\
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrustMem Chat Demo</title>
<style>
  [data-theme="light"] {
    --bg: #ffffff; --surface: #f6f8fa; --border: #d0d7de;
    --text: #1f2328; --text2: #656d76; --accent: #0969da;
    --green: #1a7f37; --red: #cf222e; --yellow: #9a6700;
    --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
  }
  [data-theme="dark"] {
    --bg: #0d1117; --surface: #161b22; --border: #30363d;
    --text: #e6edf3; --text2: #8b949e; --accent: #58a6ff;
    --green: #3fb950; --red: #f85149; --yellow: #d29922;
    --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { background: var(--bg); color: var(--text); font-family: var(--font); font-size: 14px; height: 100vh; display: flex; }

  .layout { display: flex; width: 100%; height: 100vh; }

  /* Chat pane */
  .chat-pane { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); }
  .chat-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
  .chat-header h1 { font-size: 16px; }
  .chat-header .config { display: flex; gap: 8px; align-items: center; margin-left: auto; font-size: 12px; }
  .chat-header input { background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 4px 8px; border-radius: 4px; font-size: 12px; }

  .messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
  .msg { max-width: 80%; padding: 10px 14px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
  .msg.user { align-self: flex-end; background: #1f6feb; color: #ffffff; border-bottom-right-radius: 4px; }
  .msg.assistant { align-self: flex-start; background: var(--surface); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
  .msg.system { align-self: center; color: var(--text2); font-size: 12px; font-style: italic; }

  .extraction-badge { display: inline-flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
  .extraction-badge .tag { font-size: 10px; padding: 2px 6px; border-radius: 8px; font-family: var(--mono); }
  [data-theme="light"] .tag-add { background: #dafbe1; color: var(--green); }
  [data-theme="light"] .tag-contradict { background: #ffebe9; color: var(--red); }
  [data-theme="light"] .tag-noop { background: #fff8c5; color: var(--yellow); }
  [data-theme="light"] .tag-entity { background: #ddf4ff; color: var(--accent); }
  [data-theme="light"] .msg.user { background: #0969da; }
  [data-theme="light"] .conf-high { background: #dafbe1; color: var(--green); }
  [data-theme="light"] .conf-medium { background: #fff8c5; color: var(--yellow); }
  [data-theme="light"] .conf-low { background: #ffebe9; color: var(--red); }
  [data-theme="dark"] .tag-add { background: #1a3d2a; color: var(--green); }
  [data-theme="dark"] .tag-contradict { background: #3d1a1a; color: var(--red); }
  [data-theme="dark"] .tag-noop { background: #2d2a1a; color: var(--yellow); }
  [data-theme="dark"] .tag-entity { background: #1a2a3d; color: var(--accent); }
  [data-theme="dark"] .msg.user { background: #1f6feb; }
  [data-theme="dark"] .conf-high { background: #1a3d2a; color: var(--green); }
  [data-theme="dark"] .conf-medium { background: #2d2a1a; color: var(--yellow); }
  [data-theme="dark"] .conf-low { background: #3d1a1a; color: var(--red); }

  .input-area { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; }
  .input-area textarea { flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 10px 14px; border-radius: 8px; font-family: var(--font); font-size: 14px; resize: none; height: 44px; outline: none; }
  .input-area textarea:focus { border-color: var(--accent); }
  .input-area button { background: var(--accent); color: #fff; border: none; padding: 0 20px; border-radius: 8px; cursor: pointer; font-weight: 600; }
  .input-area button:disabled { opacity: 0.5; cursor: not-allowed; }

  /* Memory sidebar */
  .sidebar { width: 360px; display: flex; flex-direction: column; overflow: hidden; }
  .sidebar-header { padding: 16px 16px 8px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
  .sidebar-header h2 { font-size: 14px; }
  .sidebar-tabs { display: flex; border-bottom: 1px solid var(--border); }
  .sidebar-tabs .stab { flex: 1; text-align: center; padding: 8px; font-size: 12px; color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; }
  .sidebar-tabs .stab.active { color: var(--accent); border-bottom-color: var(--accent); }
  .sidebar-content { flex: 1; overflow-y: auto; padding: 12px; }

  .mem-item { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px; margin-bottom: 8px; font-size: 12px; }
  .mem-item .mem-content { margin-bottom: 6px; line-height: 1.4; }
  .mem-item .mem-meta { color: var(--text2); font-size: 10px; font-family: var(--mono); }

  .conf-badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-family: var(--mono); font-weight: 600; }
  .conf-high { background: #1a3d2a; color: var(--green); }
  .conf-medium { background: #2d2a1a; color: var(--yellow); }
  .conf-low { background: #3d1a1a; color: var(--red); }

  .rel-item { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; font-size: 12px; }
  .rel-arrow { color: var(--accent); }

  .empty-state { color: var(--text2); text-align: center; padding: 40px 20px; font-size: 13px; }

  .clear-btn { background: none; border: 1px solid var(--border); color: var(--text2); padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; }
  .clear-btn:hover { border-color: var(--red); color: var(--red); }

  .typing { align-self: flex-start; color: var(--text2); font-size: 13px; padding: 10px 14px; }
  .typing::after { content: '...'; animation: dots 1.5s infinite; }
  @keyframes dots { 0%,20% { content: '.'; } 40% { content: '..'; } 60%,100% { content: '...'; } }

  .mem-item.mem-used { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
  .mem-used-badge { display: inline-block; font-size: 9px; font-weight: 700; color: #fff; background: var(--accent); padding: 0 5px; border-radius: 8px; margin-left: 4px; }
</style>
</head>
<body>
<div class="layout">
  <!-- Chat -->
  <div class="chat-pane">
    <div class="chat-header">
      <h1>TrustMem Chat</h1>
      <div class="config">
        <label>User ID:</label>
        <input type="text" id="chatUser" value="playground" style="width:100px">
        <label>API Key:</label>
        <input type="password" id="chatApiKey" placeholder="(none)" style="width:100px">
        <label style="margin-left:8px">Min Confidence:</label>
        <select id="chatMinConfidence" style="padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg2);color:var(--text);font-size:12px">
          <option value="">All</option>
          <option value="low">low+</option>
          <option value="medium">medium+</option>
          <option value="high">high only</option>
        </select>
        <button class="clear-btn" onclick="toggleTheme()" id="chatThemeBtn" style="margin-left:8px">Dark</button>
      </div>
    </div>
    <div class="messages" id="messages">
      <div class="msg system">Start a conversation. Memories will be automatically extracted and shown in the sidebar.</div>
    </div>
    <div class="input-area">
      <textarea id="chatInput" placeholder="Type a message..." onkeydown="handleKeydown(event)"></textarea>
      <button id="sendBtn" onclick="sendMessage()">Send</button>
    </div>
  </div>

  <!-- Sidebar -->
  <div class="sidebar">
    <div class="sidebar-header">
      <h2>Memories</h2>
      <button class="clear-btn" onclick="clearAll()">Clear All</button>
    </div>
    <div class="sidebar-tabs">
      <div class="stab active" onclick="switchSidebarTab('memories')">Memories</div>
      <div class="stab" onclick="switchSidebarTab('relations')">Relations</div>
      <div class="stab" onclick="switchSidebarTab('log')">Extraction Log</div>
      <div class="stab" onclick="switchSidebarTab('detail')">Detail</div>
    </div>
    <div class="sidebar-content" id="sidebarContent">
      <div class="empty-state">No memories yet. Start chatting!</div>
    </div>
  </div>
</div>

<script>
function toggleTheme() {
  const html = document.documentElement;
  const btn = document.getElementById('chatThemeBtn');
  if (html.getAttribute('data-theme') === 'light') {
    html.setAttribute('data-theme', 'dark');
    btn.textContent = 'Light';
    localStorage.setItem('trustmem-theme', 'dark');
  } else {
    html.setAttribute('data-theme', 'light');
    btn.textContent = 'Dark';
    localStorage.setItem('trustmem-theme', 'light');
  }
}
(function() {
  const saved = localStorage.getItem('trustmem-theme');
  if (saved === 'dark') {
    document.documentElement.setAttribute('data-theme', 'dark');
    document.addEventListener('DOMContentLoaded', () => {
      const btn = document.getElementById('chatThemeBtn');
      if (btn) btn.textContent = 'Light';
    });
  }
})();

let chatHistory = [];
let allMemories = [];
let allRelations = [];
let extractionLog = [];
let currentSidebarTab = 'memories';
let isComposing = false;

// IME composition handling (for Japanese/Chinese/Korean input)
document.addEventListener('DOMContentLoaded', () => {
  const input = document.getElementById('chatInput');
  input.addEventListener('compositionstart', () => { isComposing = true; });
  input.addEventListener('compositionend', () => { isComposing = false; });
});

function handleKeydown(event) {
  if (event.key === 'Enter' && !event.shiftKey && !isComposing) {
    event.preventDefault();
    sendMessage();
  }
}

function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }

function chatHeaders() {
  const h = {'Content-Type': 'application/json'};
  const key = document.getElementById('chatApiKey').value;
  if (key) h['Authorization'] = 'Bearer ' + key;
  return h;
}

function confBadge(c) {
  const cls = c === 'high' ? 'conf-high' : c === 'low' ? 'conf-low' : 'conf-medium';
  return `<span class="conf-badge ${cls}">${c || 'medium'}</span>`;
}

let usedMemoryIds = [];

function addMessage(role, content, extraction, retrievedMemories) {
  const container = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = `msg ${role}`;
  let html = esc(content);

  if (role === 'assistant') {
    // Update usedMemoryIds for sidebar highlighting
    if (retrievedMemories && retrievedMemories.length) {
      usedMemoryIds = retrievedMemories
        .filter(m => m.used)
        .map(m => m.memory_id);
      renderSidebar();
    }

    // Show retrieved memories (RAG context)
    if (retrievedMemories && retrievedMemories.length) {
      const usedCount = retrievedMemories.filter(m => m.used).length;
      html += '<div style="margin-top:8px;padding:6px 8px;background:rgba(88,166,255,0.08);border:1px solid rgba(88,166,255,0.2);border-radius:6px;font-size:11px">';
      html += '<div style="color:var(--accent);font-weight:600;margin-bottom:4px">Retrieved ' + retrievedMemories.length + ' memories' + (usedCount ? ` (${usedCount} used)` : '') + '</div>';
      retrievedMemories.forEach(m => {
        html += `<div style="margin:2px 0;color:var(--text2);cursor:pointer;${m.used ? 'font-weight:600' : ''}" onclick="event.stopPropagation();showMemoryDetail('${m.memory_id}')" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">[${m.index}] ${confBadge(m.confidence)} <span style="margin-left:2px">${esc(m.content)}</span> <span style="opacity:0.6">(${m.similarity})</span>${m.used ? '<span class="mem-used-badge">USED</span>' : ''}</div>`;
      });
      html += '</div>';
    }

    // Show extraction results
    if (extraction) {
      const extracted = extraction.extracted || [];
      const entities = extraction.entities || [];
      if (extracted.length || entities.length) {
        html += '<div class="extraction-badge">';
        extracted.forEach(e => {
          const cls = e.event === 'CONTRADICT' ? 'tag-contradict' : e.event === 'NOOP' ? 'tag-noop' : 'tag-add';
          html += `<span class="tag ${cls}">${e.event}: ${esc(e.content).substring(0, 40)}</span>`;
        });
        entities.forEach(e => {
          if (e.is_new) html += `<span class="tag tag-entity">${esc(e.name)}</span>`;
        });
        html += '</div>';
      }
    }
  }

  div.innerHTML = html;
  container.appendChild(div);
  container.scrollTop = container.scrollHeight;
}

function removeTyping() {
  const el = document.querySelector('.typing');
  if (el) el.remove();
}

function showTyping() {
  const container = document.getElementById('messages');
  const div = document.createElement('div');
  div.className = 'typing';
  div.textContent = 'Thinking';
  container.appendChild(div);
  container.scrollTop = container.scrollHeight;
}

async function sendMessage() {
  const input = document.getElementById('chatInput');
  const msg = input.value.trim();
  if (!msg) return;

  input.value = '';
  const btn = document.getElementById('sendBtn');
  btn.disabled = true;

  addMessage('user', msg);
  showTyping();

  try {
    const res = await fetch('/v1/chat', {
      method: 'POST',
      headers: chatHeaders(),
      body: JSON.stringify({
        message: msg,
        history: chatHistory,
        user_id: document.getElementById('chatUser').value || 'playground',
        min_confidence: document.getElementById('chatMinConfidence').value || null,
      })
    });

    removeTyping();

    if (!res.ok) {
      const err = await res.json();
      addMessage('system', `Error: ${err.detail || res.status}`);
      btn.disabled = false;
      return;
    }

    const data = await res.json();
    const extraction = data.extraction || {};

    chatHistory.push({role: 'user', content: msg});
    chatHistory.push({role: 'assistant', content: data.reply});

    addMessage('assistant', data.reply, extraction, data.retrieved_memories || []);

    // Update extraction log
    const extracted = extraction.extracted || [];
    if (extracted.length) {
      extractionLog.push(...extracted.map(e => ({
        ...e,
        timestamp: new Date().toLocaleTimeString(),
        trigger: msg.substring(0, 50),
      })));
    }

    // Refresh sidebar
    await refreshMemories();

  } catch (e) {
    removeTyping();
    addMessage('system', `Error: ${e.message}`);
  }

  btn.disabled = false;
}

async function refreshMemories() {
  const uid = document.getElementById('chatUser').value || 'playground';
  try {
    const res = await fetch('/v1/memories?user_id=' + encodeURIComponent(uid) + '&limit=100', {
      headers: chatHeaders()
    });
    if (res.ok) {
      const data = await res.json();
      allMemories = data.results || [];
      allRelations = data.relations || [];
      renderSidebar();
    }
  } catch (e) { /* ignore */ }
}

function switchSidebarTab(tab) {
  currentSidebarTab = tab;
  document.querySelectorAll('.stab').forEach(t => t.classList.remove('active'));
  document.querySelector(`.stab[onclick="switchSidebarTab('${tab}')"]`).classList.add('active');
  renderSidebar();
}

function renderSidebar() {
  const el = document.getElementById('sidebarContent');

  if (currentSidebarTab === 'memories') {
    if (!allMemories.length) { el.innerHTML = '<div class="empty-state">No memories yet. Start chatting!</div>'; return; }
    const confRank = {high: 2, medium: 1, low: 0};
    const sidebarFilter = document.getElementById('sidebarConfFilter');
    const minConf = sidebarFilter ? sidebarFilter.value : '';
    const minRank = minConf ? (confRank[minConf] || 0) : -1;
    const filtered = minConf ? allMemories.filter(m => (confRank[m.confidence || 'medium'] || 1) >= minRank) : allMemories;
    let html = '<div style="margin-bottom:8px"><select id="sidebarConfFilter" onchange="renderSidebar()" style="width:100%;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg2);color:var(--text);font-size:11px"><option value=""' + (!minConf ? ' selected' : '') + '>All confidence levels</option><option value="low"' + (minConf==='low' ? ' selected' : '') + '>low+</option><option value="medium"' + (minConf==='medium' ? ' selected' : '') + '>medium+</option><option value="high"' + (minConf==='high' ? ' selected' : '') + '>high only</option></select></div>';
    html += `<div style="font-size:11px;color:var(--text2);margin-bottom:8px">${filtered.length} / ${allMemories.length} memories</div>`;
    html += filtered.map(m => {
      const isUsed = usedMemoryIds.includes(m.memory_id);
      return `
      <div class="mem-item${isUsed ? ' mem-used' : ''}" style="cursor:pointer" onclick="showMemoryDetail('${m.memory_id}')">
        <div class="mem-content">${esc(m.content)}${isUsed ? '<span class="mem-used-badge">USED</span>' : ''}</div>
        <div class="mem-meta">
          ${confBadge(m.confidence)}
          ${m.source_trust ? `<span style="margin-left:4px;opacity:0.8">${m.source_trust}</span>` : ''}
          <span style="margin-left:4px">${m.created_at ? new Date(m.created_at).toLocaleTimeString() : ''}</span>
          <span style="margin-left:4px;opacity:0.6">${m.memory_id ? m.memory_id.substring(0, 8) : ''}...</span>
        </div>
      </div>
    `}).join('');
    el.innerHTML = html;

  } else if (currentSidebarTab === 'relations') {
    if (!allRelations.length) { el.innerHTML = '<div class="empty-state">No relations yet.</div>'; return; }
    el.innerHTML = allRelations.map(r => `
      <div class="rel-item">
        <strong>${esc(r.subject_name)}</strong>
        <span class="rel-arrow"> —[ ${esc(r.predicate)} ]→ </span>
        <strong>${esc(r.object_name)}</strong>
        <div class="mem-meta" style="margin-top:4px">
          ${confBadge(r.confidence || 'medium')}
          <span style="margin-left:4px">mentions: ${r.mention_count || 1}</span>
        </div>
      </div>
    `).join('');

  } else if (currentSidebarTab === 'log') {
    if (!extractionLog.length) { el.innerHTML = '<div class="empty-state">No extractions yet.</div>'; return; }
    el.innerHTML = extractionLog.slice().reverse().map(e => {
      const cls = e.event === 'CONTRADICT' ? 'tag-contradict' : e.event === 'NOOP' ? 'tag-noop' : 'tag-add';
      return `
        <div class="mem-item">
          <div class="mem-content"><span class="tag ${cls}" style="margin-right:4px">${e.event}</span>${esc(e.content)}</div>
          <div class="mem-meta">
            ${confBadge(e.confidence || 'medium')}
            <span style="margin-left:4px">${e.timestamp || ''}</span>
            <span style="margin-left:4px;opacity:0.6">← "${esc(e.trigger)}"</span>
          </div>
        </div>
      `;
    }).join('');

  } else if (currentSidebarTab === 'detail') {
    if (!selectedMemoryDetail) {
      el.innerHTML = '<div class="empty-state">Click a memory to see its details.</div>';
      return;
    }
    const d = selectedMemoryDetail;
    let html = `<div class="mem-item"><div class="mem-content" style="font-weight:600">${esc(d.memory.content)}</div><div class="mem-meta" style="margin-top:6px">${confBadge(d.memory.confidence)} ${d.memory.source_trust ? `<span style="margin-left:4px">${d.memory.source_trust}</span>` : ''}<br>id: ${d.memory.memory_id}<br>created: ${d.memory.created_at || ''}</div></div>`;

    html += '<div style="font-size:11px;font-weight:600;margin:10px 0 4px;color:var(--accent)">Contradictions</div>';
    if (d.contradictions.length) {
      d.contradictions.forEach(c => {
        html += `<div class="mem-item" style="border-left:3px solid var(--red);padding-left:8px"><div class="mem-meta">${confBadge(c.opponent_confidence)} <span style="margin-left:4px">role: ${c.role || ''}</span><br>opponent: ${(c.opponent_memory_id || '').substring(0, 8)}...<br>${c.created_at || ''}</div></div>`;
      });
    } else {
      html += '<div style="font-size:11px;color:var(--text2);margin-bottom:8px">None</div>';
    }

    html += '<div style="font-size:11px;font-weight:600;margin:10px 0 4px;color:var(--accent)">Provenance</div>';
    if (d.provenance.length) {
      d.provenance.forEach(p => {
        html += `<div class="mem-item" style="border-left:3px solid var(--green);padding-left:8px"><div class="mem-meta">${p.relationship || ''}<br>parent: ${(p.parent_memory_id || '').substring(0, 8)}...<br>${p.created_at || ''}</div></div>`;
      });
    } else {
      html += '<div style="font-size:11px;color:var(--text2);margin-bottom:8px">None</div>';
    }

    html += '<div style="font-size:11px;font-weight:600;margin:10px 0 4px;color:var(--accent)">Audit Trail (recent 10)</div>';
    if (d.audit_trail.length) {
      d.audit_trail.forEach(a => {
        const conf = a.new_confidence ? ` ${confBadge(a.new_confidence)}` : '';
        html += `<div class="mem-item"><div class="mem-meta"><strong>${a.event_type || ''}</strong>${conf}<br>${a.created_at || ''}</div></div>`;
      });
    } else {
      html += '<div style="font-size:11px;color:var(--text2);margin-bottom:8px">None</div>';
    }

    el.innerHTML = html;
  }
}

let selectedMemoryDetail = null;

let _detailLoading = false;
async function showMemoryDetail(memoryId) {
  if (_detailLoading) return;
  _detailLoading = true;
  selectedMemoryDetail = null;
  switchSidebarTab('detail');
  const el = document.getElementById('sidebarContent');
  el.innerHTML = '<div class="empty-state">Loading...</div>';
  try {
    const h = chatHeaders();
    const id = encodeURIComponent(memoryId);
    const res = await fetch('/v1/admin/memory-detail/' + id + '?limit=10', { headers: h });
    if (res.ok) {
      selectedMemoryDetail = await res.json();
    } else {
      selectedMemoryDetail = { memory: { content: '(not found)', memory_id: memoryId }, contradictions: [], provenance: [], audit_trail: [] };
    }
    renderSidebar();
  } catch (e) {
    el.innerHTML = `<div class="empty-state">Error: ${esc(e.message)}</div>`;
  } finally {
    _detailLoading = false;
  }
}

async function clearAll() {
  if (!confirm('Clear all data and chat history?')) return;
  try {
    await fetch('/v1/admin/reset', { method: 'POST', headers: chatHeaders() });
  } catch(e) { /* ignore */ }
  chatHistory = [];
  allMemories = [];
  allRelations = [];
  extractionLog = [];
  document.getElementById('messages').innerHTML = '<div class="msg system">Cleared. Start a new conversation.</div>';
  renderSidebar();
}
</script>
</body>
</html>
"""


@app.get("/chat", response_class=HTMLResponse)
def chat_demo() -> Response:
    """Chat demo UI with auto memory extraction."""
    return Response(
        content=_CHAT_HTML,
        media_type="text/html",
        headers={"Cache-Control": "no-cache, no-store, must-revalidate"},
    )
