#!/usr/bin/env python3
"""
AGIME OpenClaw Local Client (Plan A - Local PC Memory & gate.agime.ai Router)
=============================================================================
A sovereign, zero-dependency client that stores all conversations, long-term
memory, and facts LOCALLY on the user's PC while utilizing gate.agime.ai
(or local Ollama/vLLM) as the intelligent model router.

No user chat logs, SQLite databases, or local memory ever leave the PC.
Outbound model calls route securely through gate.agime.ai with strict cloud-leak
prevention and credentials sanitization.
"""

import os
import sys
import json
import gzip
import re
import sqlite3
import argparse
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional, Union

def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()

def utc_now_ts() -> int:
    return int(datetime.now(timezone.utc).timestamp())

DEFAULT_MEMORY_DIR = Path.home() / ".openclaw" / "memory"
DEFAULT_ROUTER_URL = os.environ.get("AGIME_ROUTER_URL", "https://gate.agime.ai/v1")
DEFAULT_MODEL = os.environ.get("AGIME_MODEL", "smart-default")

# Patterns for credentials that should never leak to cloud LLM routers
LEAK_PATTERNS = [
    (re.compile(r"-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]*?-----END [A-Z ]+PRIVATE KEY-----"), "[REDACTED_PRIVATE_KEY]"),
    (re.compile(r"\bsk-[a-zA-Z0-9]{20,}\b"), "[REDACTED_API_KEY]"),
    (re.compile(r"\bsk-ant-[a-zA-Z0-9_\-]{20,}\b"), "[REDACTED_ANTHROPIC_KEY]"),
    (re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{36,}\b"), "[REDACTED_GITHUB_TOKEN]"),
    (re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[REDACTED_AWS_KEY]"),
    (re.compile(r"\bBearer\s+[a-zA-Z0-9_\-\.]{25,}\b", re.IGNORECASE), "Bearer [REDACTED_TOKEN]"),
    (re.compile(r"(?i)\b(password|passwd|pwd|api_key|secret|auth_token)\s*[:=]\s*['\"]?([^\s'\";,]{6,})['\"]?"), r"\1=[REDACTED_SECRET]"),
]


def sanitize_cloud_leak(text: str) -> (str, int):
    """Sanitize sensitive credentials from outbound text before transmitting to router."""
    if not isinstance(text, str):
        return text, 0
    redacted_text = text
    redaction_count = 0
    for pattern, replacement in LEAK_PATTERNS:
        matches = pattern.findall(redacted_text)
        if matches:
            redaction_count += len(matches)
            redacted_text = pattern.sub(replacement, redacted_text)
    return redacted_text, redaction_count


def set_safe_permissions(path: Path, is_dir: bool = False):
    """Enforce owner-only permissions (0700 for directories, 0600 for files) on POSIX."""
    try:
        if os.name == "posix":
            if is_dir:
                os.chmod(path, 0o700)
            else:
                os.chmod(path, 0o600)
    except Exception:
        pass


class LocalMemoryManager:
    """Manages local SQLite sessions and facts stored strictly on the user's PC."""
    def __init__(self, memory_dir: Path):
        self.memory_dir = Path(memory_dir)
        self.memory_dir.mkdir(parents=True, exist_ok=True)
        set_safe_permissions(self.memory_dir, is_dir=True)
        self.db_path = self.memory_dir / "sessions.db"
        self.facts_path = self.memory_dir / "facts.json"
        self._init_db()
        self._init_facts()
        set_safe_permissions(self.db_path)
        set_safe_permissions(self.facts_path)

    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS sessions (
                    id TEXT PRIMARY KEY,
                    title TEXT,
                    created_at TIMESTAMP,
                    updated_at TIMESTAMP
                )
            """)
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    session_id TEXT,
                    role TEXT,
                    content TEXT,
                    model TEXT,
                    timestamp TIMESTAMP,
                    FOREIGN KEY (session_id) REFERENCES sessions(id)
                )
            """)
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS facts (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    fact TEXT UNIQUE,
                    created_at TIMESTAMP
                )
            """)
            conn.commit()

    def _init_facts(self):
        # Sync facts.json and sqlite facts table
        existing_facts = []
        if self.facts_path.exists():
            try:
                with open(self.facts_path, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    existing_facts = data.get("facts", [])
            except Exception:
                existing_facts = []
        else:
            with open(self.facts_path, "w", encoding="utf-8") as f:
                json.dump({"facts": [], "created_at": utc_now_iso()}, f, indent=2)

        # Sync to sqlite
        now = utc_now_iso()
        with sqlite3.connect(self.db_path) as conn:
            for f in existing_facts:
                conn.execute("INSERT OR IGNORE INTO facts (fact, created_at) VALUES (?, ?)", (f, now))
            conn.commit()

    def _write_facts_json(self, facts: List[str]):
        with open(self.facts_path, "w", encoding="utf-8") as f:
            json.dump({"facts": facts, "updated_at": utc_now_iso()}, f, indent=2)
        set_safe_permissions(self.facts_path)

    def get_facts(self) -> List[str]:
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT fact FROM facts ORDER BY id ASC")
            db_facts = [r[0] for r in cursor.fetchall()]
        if db_facts:
            return db_facts
        # Fallback to json if db empty
        try:
            with open(self.facts_path, "r", encoding="utf-8") as f:
                data = json.load(f)
                return data.get("facts", [])
        except Exception:
            return []

    def add_fact(self, fact_text: str) -> bool:
        fact_text = fact_text.strip()
        if not fact_text:
            return False
        now = utc_now_iso()
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT id FROM facts WHERE fact = ?", (fact_text,))
            if cursor.fetchone():
                return False
            cursor.execute("INSERT INTO facts (fact, created_at) VALUES (?, ?)", (fact_text, now))
            conn.commit()

        facts = self.get_facts()
        self._write_facts_json(facts)
        return True

    def remove_fact(self, index_or_text: Union[int, str]) -> Optional[str]:
        facts = self.get_facts()
        removed_fact = None
        if isinstance(index_or_text, int):
            if 0 <= index_or_text < len(facts):
                removed_fact = facts[index_or_text]
        else:
            if index_or_text in facts:
                removed_fact = index_or_text

        if removed_fact:
            with sqlite3.connect(self.db_path) as conn:
                conn.execute("DELETE FROM facts WHERE fact = ?", (removed_fact,))
                conn.commit()
            facts = self.get_facts()
            self._write_facts_json(facts)
            return removed_fact
        return None

    def clear_facts(self) -> int:
        facts = self.get_facts()
        count = len(facts)
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("DELETE FROM facts")
            conn.commit()
        self._write_facts_json([])
        return count

    def create_session(self, session_id: str, title: str = "New Session"):
        now = utc_now_iso()
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "INSERT OR REPLACE INTO sessions (id, title, created_at, updated_at) VALUES (?, ?, COALESCE((SELECT created_at FROM sessions WHERE id = ?), ?), ?)",
                (session_id, title, session_id, now, now)
            )

    def append_message(self, session_id: str, role: str, content: str, model: str = ""):
        now = utc_now_iso()
        with sqlite3.connect(self.db_path) as conn:
            # Ensure session exists
            conn.execute(
                "INSERT OR IGNORE INTO sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
                (session_id, "New Session", now, now)
            )
            conn.execute(
                "INSERT INTO messages (session_id, role, content, model, timestamp) VALUES (?, ?, ?, ?, ?)",
                (session_id, role, content, model, now)
            )
            conn.execute("UPDATE sessions SET updated_at = ? WHERE id = ?", (now, session_id))

    def get_session_history(self, session_id: str, limit: int = 20) -> List[Dict[str, str]]:
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute(
                "SELECT role, content FROM (SELECT id, role, content FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?) ORDER BY id ASC",
                (session_id, limit)
            )
            return [{"role": r["role"], "content": r["content"]} for r in cursor.fetchall()]

    def list_sessions(self, limit: int = 50) -> List[Dict[str, Any]]:
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT s.id, s.title, s.created_at, s.updated_at, COUNT(m.id) AS message_count
                FROM sessions s
                LEFT JOIN messages m ON s.id = m.session_id
                GROUP BY s.id
                ORDER BY s.updated_at DESC
                LIMIT ?
            """, (limit,))
            return [dict(r) for r in cursor.fetchall()]

    def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("SELECT id, title, created_at, updated_at FROM sessions WHERE id = ?", (session_id,))
            row = cursor.fetchone()
            if not row:
                return None
            sess = dict(row)
            cursor.execute("SELECT role, content, model, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC", (session_id,))
            sess["messages"] = [dict(m) for m in cursor.fetchall()]
            return sess

    def delete_session(self, session_id: str) -> bool:
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
            cursor.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
            conn.commit()
            return cursor.rowcount > 0

    def clear_session(self, session_id: str) -> bool:
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
            conn.commit()
            return cursor.rowcount > 0

    def get_stats(self) -> Dict[str, Any]:
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT COUNT(*) FROM sessions")
            sessions_count = cursor.fetchone()[0]
            cursor.execute("SELECT COUNT(*) FROM messages")
            messages_count = cursor.fetchone()[0]
            cursor.execute("SELECT COUNT(*) FROM facts")
            facts_count = cursor.fetchone()[0]

        db_size = self.db_path.stat().st_size if self.db_path.exists() else 0
        facts_size = self.facts_path.stat().st_size if self.facts_path.exists() else 0

        return {
            "sessions_count": sessions_count,
            "messages_count": messages_count,
            "facts_count": facts_count,
            "db_size_bytes": db_size,
            "facts_size_bytes": facts_size,
            "memory_dir": str(self.memory_dir),
            "db_path": str(self.db_path)
        }

    def export_state(self, output_path: Union[str, Path]) -> Dict[str, Any]:
        """Export all local SQLite sessions, messages, and facts into an export archive/file."""
        target = Path(output_path)
        target.parent.mkdir(parents=True, exist_ok=True)

        facts = self.get_facts()
        sessions_data = []

        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("SELECT id, title, created_at, updated_at FROM sessions ORDER BY created_at ASC")
            sessions = [dict(r) for r in cursor.fetchall()]

            for s in sessions:
                cursor.execute(
                    "SELECT role, content, model, timestamp FROM messages WHERE session_id = ? ORDER BY id ASC",
                    (s["id"],)
                )
                s["messages"] = [dict(m) for m in cursor.fetchall()]
                sessions_data.append(s)

        export_obj = {
            "version": "1.0",
            "exported_at": utc_now_iso(),
            "client": "AGIME-OpenClaw-LocalClient",
            "stats": {
                "sessions_count": len(sessions_data),
                "messages_count": sum(len(s.get("messages", [])) for s in sessions_data),
                "facts_count": len(facts)
            },
            "facts": facts,
            "sessions": sessions_data
        }

        json_bytes = json.dumps(export_obj, indent=2, ensure_ascii=False).encode("utf-8")
        if target.suffix == ".gz":
            with gzip.open(target, "wb") as f:
                f.write(json_bytes)
        else:
            with open(target, "wb") as f:
                f.write(json_bytes)

        set_safe_permissions(target)

        return {
            "path": str(target),
            "sessions_count": len(sessions_data),
            "messages_count": export_obj["stats"]["messages_count"],
            "facts_count": len(facts),
            "size_bytes": target.stat().st_size
        }

    def import_state(self, input_path: Union[str, Path], mode: str = "merge") -> Dict[str, Any]:
        """
        Import state from an export file.
        mode="merge": keeps existing sessions/facts, adds new ones without duplicates.
        mode="replace": wipes existing local database and facts before importing.
        """
        source = Path(input_path)
        if not source.exists():
            raise FileNotFoundError(f"Import file not found: {source}")

        if source.suffix == ".gz":
            with gzip.open(source, "rb") as f:
                content = f.read().decode("utf-8")
        else:
            with open(source, "r", encoding="utf-8") as f:
                content = f.read()

        data = json.loads(content)
        imported_facts = data.get("facts", [])
        imported_sessions = data.get("sessions", [])

        if mode == "replace":
            with sqlite3.connect(self.db_path) as conn:
                conn.execute("DELETE FROM messages")
                conn.execute("DELETE FROM sessions")
                conn.execute("DELETE FROM facts")
                conn.commit()
            self._write_facts_json([])

        # Process facts
        added_facts = 0
        for fact in imported_facts:
            if self.add_fact(fact):
                added_facts += 1

        # Process sessions and messages
        added_sessions = 0
        added_messages = 0

        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            for s in imported_sessions:
                sess_id = s.get("id")
                if not sess_id:
                    continue
                title = s.get("title", "Imported Session")
                created_at = s.get("created_at", utc_now_iso())
                updated_at = s.get("updated_at", created_at)

                cursor.execute("SELECT id FROM sessions WHERE id = ?", (sess_id,))
                exists = cursor.fetchone()
                if not exists:
                    cursor.execute(
                        "INSERT INTO sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
                        (sess_id, title, created_at, updated_at)
                    )
                    added_sessions += 1
                else:
                    cursor.execute(
                        "UPDATE sessions SET updated_at = MAX(updated_at, ?) WHERE id = ?",
                        (updated_at, sess_id)
                    )

                for msg in s.get("messages", []):
                    role = msg.get("role", "user")
                    content = msg.get("content", "")
                    model = msg.get("model", "")
                    ts = msg.get("timestamp", updated_at)

                    # Deduplicate in merge mode
                    if mode == "merge":
                        cursor.execute(
                            "SELECT id FROM messages WHERE session_id = ? AND role = ? AND content = ? AND timestamp = ?",
                            (sess_id, role, content, ts)
                        )
                        if cursor.fetchone():
                            continue

                    cursor.execute(
                        "INSERT INTO messages (session_id, role, content, model, timestamp) VALUES (?, ?, ?, ?, ?)",
                        (sess_id, role, content, model, ts)
                    )
                    added_messages += 1

            conn.commit()

        # Resync facts.json
        self._write_facts_json(self.get_facts())

        return {
            "mode": mode,
            "imported_sessions": added_sessions,
            "imported_messages": added_messages,
            "imported_facts": added_facts,
            "total_facts": len(self.get_facts())
        }


class LocalKnowledgeIndexer:
    """Manages local document ingestion and full-text keyword indexing in knowledge.sqlite.

    All indexed documents and chunks remain 100% on the user's PC with 0600 permissions.
    Zero raw document bytes or SQLite files leave the machine.
    """
    def __init__(self, memory_dir: Path):
        self.memory_dir = Path(memory_dir)
        self.memory_dir.mkdir(parents=True, exist_ok=True)
        set_safe_permissions(self.memory_dir, is_dir=True)
        self.db_path = self.memory_dir / "knowledge.sqlite"
        self._init_db()
        set_safe_permissions(self.db_path)

    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS documents (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    path TEXT UNIQUE,
                    filename TEXT,
                    size_bytes INTEGER,
                    word_count INTEGER,
                    indexed_at TIMESTAMP
                )
            """)
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS chunks (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    doc_id INTEGER,
                    chunk_index INTEGER,
                    content TEXT,
                    word_count INTEGER,
                    FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
                )
            """)
            cursor.execute("CREATE INDEX IF NOT EXISTS idx_chunks_doc_id ON chunks(doc_id)")
            conn.commit()

    def index_directory(
        self,
        dir_path: Union[str, Path],
        recursive: bool = True,
        extensions: Optional[List[str]] = None,
        privacy_guard: bool = True
    ) -> Dict[str, Any]:
        """Scans folder and indexes all text/markdown/code documents into local SQLite."""
        base_dir = Path(dir_path).expanduser().resolve()
        if not base_dir.exists() or not base_dir.is_dir():
            raise FileNotFoundError(f"Directory not found: {base_dir}")

        exts = set(extensions or [
            ".txt", ".md", ".json", ".csv", ".py", ".js", ".ts",
            ".html", ".css", ".sql", ".yaml", ".yml", ".xml", ".rst"
        ])

        indexed_docs = 0
        total_chunks = 0
        total_words = 0
        now = utc_now_iso()

        pattern = "**/*" if recursive else "*"
        for file_path in base_dir.glob(pattern):
            if not file_path.is_file():
                continue
            if file_path.name.startswith(".") or "/." in str(file_path):
                continue
            if file_path.suffix.lower() not in exts:
                continue

            try:
                raw_bytes = file_path.read_bytes()
                if len(raw_bytes) > 10 * 1024 * 1024:
                    continue
                try:
                    text = raw_bytes.decode("utf-8")
                except UnicodeDecodeError:
                    text = raw_bytes.decode("latin-1", errors="ignore")

                if privacy_guard:
                    text, _ = sanitize_cloud_leak(text)

                words = text.split()
                if not words:
                    continue

                word_count = len(words)
                total_words += word_count

                with sqlite3.connect(self.db_path) as conn:
                    cursor = conn.cursor()
                    cursor.execute("""
                        INSERT INTO documents (path, filename, size_bytes, word_count, indexed_at)
                        VALUES (?, ?, ?, ?, ?)
                        ON CONFLICT(path) DO UPDATE SET
                            size_bytes=excluded.size_bytes,
                            word_count=excluded.word_count,
                            indexed_at=excluded.indexed_at
                        RETURNING id
                    """, (str(file_path), file_path.name, len(raw_bytes), word_count, now))
                    doc_id = cursor.fetchone()[0]

                    cursor.execute("DELETE FROM chunks WHERE doc_id = ?", (doc_id,))

                    chunk_size = 250
                    overlap = 50
                    step = max(1, chunk_size - overlap)
                    chunks_list = []
                    for i in range(0, len(words), step):
                        chunk_words = words[i:i + chunk_size]
                        chunk_text = " ".join(chunk_words)
                        chunks_list.append((doc_id, len(chunks_list), chunk_text, len(chunk_words)))

                    cursor.executemany("""
                        INSERT INTO chunks (doc_id, chunk_index, content, word_count)
                        VALUES (?, ?, ?, ?)
                    """, chunks_list)
                    conn.commit()

                    indexed_docs += 1
                    total_chunks += len(chunks_list)

            except Exception:
                continue

        set_safe_permissions(self.db_path)
        return {
            "indexed_files": indexed_docs,
            "total_chunks": total_chunks,
            "total_words": total_words,
            "directory": str(base_dir),
            "db_path": str(self.db_path)
        }

    def search(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
        """Finds most relevant local text chunks matching query terms."""
        tokens = [t.lower() for t in re.findall(r"\b\w{3,}\b", query)]
        if not tokens:
            return []

        results = []
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT c.id, c.doc_id, c.chunk_index, c.content, c.word_count,
                       d.path, d.filename
                FROM chunks c
                JOIN documents d ON c.doc_id = d.id
            """)
            rows = cursor.fetchall()
            for r in rows:
                content_lower = r["content"].lower()
                score = sum(content_lower.count(t) for t in tokens)
                if score > 0:
                    results.append({
                        "id": r["id"],
                        "filename": r["filename"],
                        "path": r["path"],
                        "chunk_index": r["chunk_index"],
                        "snippet": r["content"][:300] + "..." if len(r["content"]) > 300 else r["content"],
                        "full_content": r["content"],
                        "score": score
                    })

        results.sort(key=lambda x: x["score"], reverse=True)
        return results[:top_k]

    def get_stats(self) -> Dict[str, Any]:
        """Returns statistics on local sovereign knowledge base."""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT COUNT(*), COALESCE(SUM(word_count), 0) FROM documents")
            doc_count, total_words = cursor.fetchone()
            cursor.execute("SELECT COUNT(*) FROM chunks")
            chunk_count = cursor.fetchone()[0]

        size_bytes = self.db_path.stat().st_size if self.db_path.exists() else 0
        return {
            "knowledge_db_path": str(self.db_path),
            "documents_count": doc_count,
            "chunks_count": chunk_count,
            "total_words": total_words,
            "db_size_bytes": size_bytes
        }

    def list_documents(self, limit: int = 50) -> List[Dict[str, Any]]:
        """List indexed documents."""
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT id, filename, path, size_bytes, word_count, indexed_at
                FROM documents ORDER BY indexed_at DESC LIMIT ?
            """, (limit,))
            return [dict(r) for r in cursor.fetchall()]


class GateApiClient:
    """Client for dispatching chat completions to gate.agime.ai router or local Ollama."""
    def __init__(self, base_url: str, api_key: str, model: str, privacy_guard: bool = True, offline: bool = False):
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.model = model
        self.privacy_guard = privacy_guard
        self.offline = offline

    def complete(self, messages: List[Dict[str, str]], timeout: int = 60) -> Dict[str, Any]:
        """Dispatch chat completions request strictly to the model router."""
        if self.offline:
            return {
                "ok": True,
                "content": "[offline-mode] Sovereign response generated locally without cloud egress.",
                "model": "local-offline",
                "redactions": 0
            }

        # Apply cloud-leak protection to outbound messages
        outbound_messages = []
        total_redactions = 0
        for m in messages:
            content = m.get("content", "")
            if self.privacy_guard:
                clean_content, redacted_count = sanitize_cloud_leak(content)
                if redacted_count > 0:
                    total_redactions += redacted_count
                    print(f"\n[privacy-guard] Sanitized {redacted_count} sensitive credential(s) from outbound prompt.", file=sys.stderr)
            else:
                clean_content = content
            outbound_messages.append({"role": m.get("role", "user"), "content": clean_content})

        url = f"{self.base_url}/chat/completions"
        payload = {
            "model": self.model,
            "messages": outbound_messages,
            "temperature": 0.7,
        }
        data = json.dumps(payload).encode("utf-8")
        headers = {
            "Content-Type": "application/json",
            "User-Agent": "AGIME-OpenClaw-LocalClient/1.0"
        }
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"

        req = urllib.request.Request(url, data=data, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                res_data = json.loads(resp.read().decode("utf-8"))
                choice = res_data.get("choices", [{}])[0].get("message", {})
                return {
                    "ok": True,
                    "content": choice.get("content", ""),
                    "model": res_data.get("model", self.model),
                    "redactions": total_redactions
                }
        except urllib.error.HTTPError as e:
            err_msg = e.read().decode("utf-8", errors="replace")
            return {"ok": False, "error": f"HTTP {e.code}: {err_msg}", "redactions": total_redactions}
        except Exception as e:
            return {"ok": False, "error": str(e), "redactions": total_redactions}


def build_system_prompt(facts: List[str], context: Optional[str] = None) -> str:
    prompt = (
        "You are Simbo, an autonomous sovereign agent delegate connected via the AGIME network. "
        "All user conversations and long-term memory are stored strictly on the user's local PC. "
        "Be direct, highly competent, proactive, and assist the user with tasks and code execution.\n\n"
    )
    if facts:
        prompt += "KNOWN USER FACTS (Stored on user PC):\n"
        for i, f in enumerate(facts, 1):
            prompt += f"{i}. {f}\n"
        prompt += "\nUse these facts to personalize your help without repeating them verbatim unless asked.\n\n"
    if context:
        prompt += f"LOCAL SOVEREIGN CONTEXT (from private local documents on this PC):\n{context}\n\n"
    return prompt


def run_repl(memory: LocalMemoryManager, client: GateApiClient, session_id: str, knowledge: Optional[LocalKnowledgeIndexer] = None):
    print("=" * 70)
    print(" AGIME OpenClaw Local Client (Sovereign Local PC Memory)")
    print(f" Router: {client.base_url} | Model: {client.model}")
    print(f" Local Memory: {memory.memory_dir}")
    print(f" Cloud-Leak Privacy Guard: {'ACTIVE (Auto-Scrubbing)' if client.privacy_guard else 'DISABLED'}")
    if knowledge:
        kstats = knowledge.get_stats()
        print(f" Local Knowledge: {kstats['documents_count']} docs, {kstats['chunks_count']} chunks ({kstats['db_size_bytes']} bytes)")
    print(" Type /help for command options. Ctrl+C or /exit to quit.")
    print("=" * 70)

    memory.create_session(session_id, title="Interactive Session")

    while True:
        try:
            user_input = input(f"\n[user ({session_id})] > ").strip()
            if not user_input:
                continue

            if user_input in ("/exit", "/quit"):
                print("Session closed. Memory safely preserved locally.")
                break

            if user_input == "/help":
                print("""Available Commands:
  /memory                 Show all locally stored user facts
  /remember <fact>        Store a new permanent fact on your local PC
  /forget <idx>           Remove a fact by 1-based index
  /clear-facts            Clear all locally stored facts
  /index <dir>            Ingest a folder into local knowledge base (knowledge.sqlite)
  /search <query>         Search local knowledge base chunks
  /docs                   List indexed documents in local knowledge base
  /knowledge              Show local knowledge base statistics
  /sessions               List recent local conversation sessions
  /session <id>           Switch active conversation session
  /delete-session <id>    Delete a local session and its history
  /export <filepath>      Export full local state (sessions, messages, facts)
  /import <filepath>      Import state from a JSON export file (merge)
  /stats                  Show local database storage statistics
  /privacy                Show cloud isolation and privacy audit status
  /model <name>           Switch active model on gate.agime.ai
  /clear                  Start a fresh session
  /help                   Show this help menu
  /exit                   Exit the client""")
                continue

            if user_input == "/memory":
                facts = memory.get_facts()
                print(f"\n--- Stored Local Facts ({len(facts)}) ---")
                for i, f in enumerate(facts, 1):
                    print(f"[{i}] {f}")
                continue

            if user_input.startswith("/remember "):
                fact = user_input[10:].strip()
                if memory.add_fact(fact):
                    print(f"✓ Saved to local memory: '{fact}'")
                else:
                    print("Fact already exists in local memory.")
                continue

            if user_input.startswith("/forget "):
                try:
                    idx = int(user_input[8:].strip()) - 1
                    removed = memory.remove_fact(idx)
                    if removed:
                        print(f"✓ Removed from local memory: '{removed}'")
                    else:
                        print("Invalid index.")
                except ValueError:
                    print("Please specify a numeric index: /forget <index>")
                continue

            if user_input == "/clear-facts":
                cleared = memory.clear_facts()
                print(f"✓ Cleared {cleared} local facts.")
                continue

            if user_input.startswith("/index "):
                target_dir = user_input[7:].strip()
                if knowledge:
                    try:
                        res = knowledge.index_directory(target_dir, privacy_guard=client.privacy_guard)
                        print(f"✓ Indexed local files: {res['indexed_files']} files, {res['total_chunks']} chunks, {res['total_words']} words")
                    except Exception as e:
                        print(f"Index error: {e}")
                else:
                    print("Knowledge indexer not initialized.")
                continue

            if user_input.startswith("/search "):
                query = user_input[8:].strip()
                if knowledge:
                    hits = knowledge.search(query, top_k=5)
                    print(f"\n--- Local Knowledge Results ({len(hits)}) ---")
                    for i, h in enumerate(hits, 1):
                        print(f"[{i}] {h['filename']} (score: {h['score']}):\n    {h['snippet']}\n")
                else:
                    print("Knowledge indexer not initialized.")
                continue

            if user_input == "/docs":
                if knowledge:
                    docs = knowledge.list_documents()
                    print(f"\n--- Indexed Local Documents ({len(docs)}) ---")
                    for d in docs:
                        print(f"• {d['filename']} ({d['word_count']} words, indexed {d['indexed_at']})")
                else:
                    print("Knowledge indexer not initialized.")
                continue

            if user_input == "/knowledge":
                if knowledge:
                    kstats = knowledge.get_stats()
                    print("\n--- Sovereign Local Knowledge Base ---")
                    print(f" Database  : {kstats['knowledge_db_path']} ({kstats['db_size_bytes']} bytes)")
                    print(f" Documents : {kstats['documents_count']}")
                    print(f" Chunks    : {kstats['chunks_count']}")
                    print(f" Words     : {kstats['total_words']}")
                else:
                    print("Knowledge indexer not initialized.")
                continue

            if user_input == "/sessions":
                sessions = memory.list_sessions()
                print("\n--- Recent Local Sessions ---")
                for s in sessions:
                    active = " [ACTIVE]" if s["id"] == session_id else ""
                    print(f"• {s['id']}{active} : {s['title']} ({s['message_count']} msgs, {s['updated_at']})")
                continue

            if user_input.startswith("/session "):
                target_sess = user_input[9:].strip()
                if target_sess:
                    session_id = target_sess
                    memory.create_session(session_id, title=f"Session {session_id}")
                    print(f"✓ Switched to session: {session_id}")
                continue

            if user_input.startswith("/delete-session "):
                target_sess = user_input[16:].strip()
                if memory.delete_session(target_sess):
                    print(f"✓ Deleted session: {target_sess}")
                    if session_id == target_sess:
                        session_id = f"sess_{utc_now_ts()}"
                        memory.create_session(session_id, title="Interactive Session")
                        print(f"✓ Started new session: {session_id}")
                else:
                    print(f"Session '{target_sess}' not found.")
                continue

            if user_input.startswith("/export "):
                exp_path = user_input[8:].strip()
                try:
                    res = memory.export_state(exp_path)
                    print(f"✓ Exported state: {res['sessions_count']} sessions, {res['messages_count']} messages, {res['facts_count']} facts to {res['path']}")
                except Exception as e:
                    print(f"Export error: {e}")
                continue

            if user_input.startswith("/import "):
                imp_path = user_input[8:].strip()
                try:
                    res = memory.import_state(imp_path, mode="merge")
                    print(f"✓ Imported state: +{res['imported_sessions']} sessions, +{res['imported_messages']} messages, +{res['imported_facts']} facts (mode: {res['mode']})")
                except Exception as e:
                    print(f"Import error: {e}")
                continue

            if user_input == "/stats":
                stats = memory.get_stats()
                print("\n--- Local PC Storage Stats ---")
                print(f" Directory : {stats['memory_dir']}")
                print(f" SQLite DB : {stats['db_path']} ({stats['db_size_bytes']} bytes)")
                print(f" Sessions  : {stats['sessions_count']}")
                print(f" Messages  : {stats['messages_count']}")
                print(f" Facts     : {stats['facts_count']}")
                continue

            if user_input == "/privacy":
                print("\n--- AGIME Cloud Leak Prevention & Sovereign Audit ---")
                print(f" Storage Model     : 100% Local PC (SQLite + JSON)")
                print(f" Local Path        : {memory.memory_dir}")
                print(f" DB Permissions    : 0600 (owner-only)")
                print(f" Cloud Sync        : DISABLED (zero raw db leaves PC)")
                print(f" Outbound Router   : {client.base_url}")
                print(f" Outbound Model    : {client.model}")
                print(f" Leak Sanitizer    : {'ENABLED (API keys/credentials redacted)' if client.privacy_guard else 'DISABLED'}")
                print(f" Telemetry / Pings : NONE (Zero tracking)")
                continue

            if user_input.startswith("/model "):
                client.model = user_input[7:].strip()
                print(f"✓ Active model set to: {client.model}")
                continue

            if user_input == "/clear":
                session_id = f"sess_{utc_now_ts()}"
                memory.create_session(session_id, title="Interactive Session")
                print("✓ Cleared conversational context. New session started.")
                continue

            # Regular message dispatch
            memory.append_message(session_id, "user", user_input)
            history = memory.get_session_history(session_id, limit=16)
            facts = memory.get_facts()

            context_str = ""
            if knowledge:
                hits = knowledge.search(user_input, top_k=2)
                if hits:
                    context_str = "\n".join(f"- {h['filename']}: {h['snippet']}" for h in hits)

            messages = [{"role": "system", "content": build_system_prompt(facts, context=context_str)}]
            messages.extend(history)

            print("[simbo] thinking...", end="\r", flush=True)
            res = client.complete(messages)

            if res["ok"]:
                reply = res["content"]
                memory.append_message(session_id, "assistant", reply, model=res["model"])
                print(f"\r[simbo] ({res['model']}):\n{reply}")
            else:
                print(f"\r[error] {res['error']}")

        except (KeyboardInterrupt, EOFError):
            print("\nExiting. Memory saved locally.")
            break


def main():
    parser = argparse.ArgumentParser(
        description="AGIME OpenClaw Local Client (Sovereign Local PC Memory + gate.agime.ai Router)"
    )
    parser.add_argument("prompt", nargs="?", help="Direct prompt to execute without entering interactive REPL")
    parser.add_argument("--router-url", default=DEFAULT_ROUTER_URL, help=f"Model router URL (default: {DEFAULT_ROUTER_URL})")
    parser.add_argument("--api-key", default=os.environ.get("AGIME_API_KEY", "agime-local-key"), help="API key for gate.agime.ai router")
    parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model identifier (default: {DEFAULT_MODEL})")
    parser.add_argument("--memory-dir", default=str(DEFAULT_MEMORY_DIR), help=f"Local memory directory (default: {DEFAULT_MEMORY_DIR})")
    parser.add_argument("--local-gpu", action="store_true", help="Route directly to local Ollama (http://localhost:11434/v1)")
    parser.add_argument("--session", default=None, help="Session ID to resume or target")
    parser.add_argument("--export", metavar="FILE", help="Export local SQLite sessions and facts to JSON file and exit")
    parser.add_argument("--import", dest="import_file", metavar="FILE", help="Import sessions and facts from JSON file and exit")
    parser.add_argument("--import-mode", choices=["merge", "replace"], default="merge", help="Import mode: merge or replace (default: merge)")
    parser.add_argument("--stats", action="store_true", help="Print local PC memory statistics and exit")
    parser.add_argument("--list-sessions", action="store_true", help="List stored local sessions and exit")
    parser.add_argument("--delete-session", metavar="SESSION_ID", help="Delete a specific session from local SQLite and exit")
    parser.add_argument("--no-privacy-guard", action="store_true", help="Disable outbound credential sanitization guard")
    parser.add_argument("--offline", "--dry-run", action="store_true", help="Run without network calls (air-gapped local test)")
    parser.add_argument("--index-dir", metavar="DIR", help="Index a local directory of documents into knowledge.sqlite and exit")
    parser.add_argument("--search-knowledge", metavar="QUERY", help="Search indexed local documents and exit")
    parser.add_argument("--knowledge-stats", action="store_true", help="Print local knowledge database statistics and exit")
    parser.add_argument("--list-docs", action="store_true", help="List indexed local documents and exit")

    args = parser.parse_args()

    router_url = "http://localhost:11434/v1" if args.local_gpu else args.router_url
    session_id = args.session or f"sess_{utc_now_ts()}"
    memory_path = Path(args.memory_dir)
    memory = LocalMemoryManager(memory_path)
    knowledge = LocalKnowledgeIndexer(memory_path)
    privacy_guard = not args.no_privacy_guard
    client = GateApiClient(router_url, args.api_key, args.model, privacy_guard=privacy_guard, offline=args.offline)

    # CLI operations: Knowledge indexing
    if args.index_dir:
        res = knowledge.index_directory(args.index_dir, privacy_guard=privacy_guard)
        print(f"Indexed local documents successfully:")
        print(f"  Directory     : {res['directory']}")
        print(f"  Files Indexed : {res['indexed_files']}")
        print(f"  Total Chunks  : {res['total_chunks']}")
        print(f"  Total Words   : {res['total_words']}")
        print(f"  Database      : {res['db_path']}")
        sys.exit(0)

    # CLI operations: Knowledge search
    if args.search_knowledge:
        hits = knowledge.search(args.search_knowledge, top_k=5)
        print(f"Search results for '{args.search_knowledge}' ({len(hits)} hits):")
        for i, h in enumerate(hits, 1):
            print(f"[{i}] {h['filename']} (score: {h['score']}):\n    {h['snippet']}\n")
        sys.exit(0)

    # CLI operations: Knowledge stats
    if args.knowledge_stats:
        stats = knowledge.get_stats()
        print(json.dumps(stats, indent=2))
        sys.exit(0)

    # CLI operations: List indexed docs
    if args.list_docs:
        docs = knowledge.list_documents()
        for d in docs:
            print(f"{d['id']}\t{d['filename']}\t{d['word_count']} words\t{d['indexed_at']}\t{d['path']}")
        sys.exit(0)

    # CLI operations: Export
    if args.export:
        res = memory.export_state(args.export)
        print(f"Exported state successfully:")
        print(f"  Destination : {res['path']}")
        print(f"  Sessions    : {res['sessions_count']}")
        print(f"  Messages    : {res['messages_count']}")
        print(f"  Facts       : {res['facts_count']}")
        print(f"  File size   : {res['size_bytes']} bytes")
        sys.exit(0)

    # CLI operations: Import
    if args.import_file:
        res = memory.import_state(args.import_file, mode=args.import_mode)
        print(f"Imported state successfully ({res['mode']} mode):")
        print(f"  Sessions imported : +{res['imported_sessions']}")
        print(f"  Messages imported : +{res['imported_messages']}")
        print(f"  Facts imported    : +{res['imported_facts']}")
        print(f"  Total facts       : {res['total_facts']}")
        sys.exit(0)

    # CLI operations: Stats
    if args.stats:
        stats = memory.get_stats()
        print(json.dumps(stats, indent=2))
        sys.exit(0)

    # CLI operations: List sessions
    if args.list_sessions:
        sessions = memory.list_sessions()
        for s in sessions:
            print(f"{s['id']}\t{s['title']}\t{s['message_count']} msgs\t{s['updated_at']}")
        sys.exit(0)

    # CLI operations: Delete session
    if args.delete_session:
        deleted = memory.delete_session(args.delete_session)
        if deleted:
            print(f"Deleted session '{args.delete_session}' successfully.")
            sys.exit(0)
        else:
            print(f"Session '{args.delete_session}' not found.", file=sys.stderr)
            sys.exit(1)

    # CLI operations: Direct Prompt
    if args.prompt:
        memory.create_session(session_id, title=args.prompt[:30])
        memory.append_message(session_id, "user", args.prompt)
        facts = memory.get_facts()
        hits = knowledge.search(args.prompt, top_k=2)
        context_str = "\n".join(f"- {h['filename']}: {h['snippet']}" for h in hits) if hits else ""
        messages = [
            {"role": "system", "content": build_system_prompt(facts, context=context_str)},
            {"role": "user", "content": args.prompt}
        ]
        res = client.complete(messages)
        if res["ok"]:
            print(res["content"])
            memory.append_message(session_id, "assistant", res["content"], model=res["model"])
            sys.exit(0)
        else:
            print(f"Error: {res['error']}", file=sys.stderr)
            sys.exit(1)

    # Interactive REPL
    run_repl(memory, client, session_id, knowledge=knowledge)


if __name__ == "__main__":
    main()
