"""
Mengram Cloud Client — developer SDK.

Usage:
    from cloud.client import CloudMemory

    m = CloudMemory(api_key="om-...")

    # Add memories from conversation
    m.add([
        {"role": "user", "content": "We fixed the OOM with Redis cache. Config: pool-size=20"},
        {"role": "assistant", "content": "Got it, I've noted the HikariCP config change."},
    ])

    # Search
    results = m.search("database connection issues")
    for r in results:
        print(f"{r['entity']} (score={r['score']})")

    # Get all
    memories = m.get_all()

    # Get specific
    entity = m.get("PostgreSQL")

    # Delete
    m.delete("PostgreSQL")

    # Stats
    print(m.stats())
"""

import json
import urllib.request
import urllib.error
import urllib.parse
from typing import Optional


class QuotaExceededError(Exception):
    """Raised when API quota is exceeded (HTTP 402)."""
    def __init__(self, detail: dict):
        self.action = detail.get("action", "unknown")
        self.limit = detail.get("limit", 0)
        self.current = detail.get("used", 0)
        self.plan = detail.get("plan", "free")
        super().__init__(
            f"Quota exceeded for '{self.action}': {self.current}/{self.limit} "
            f"(plan: {self.plan}). Upgrade at https://mengram.io/dashboard"
        )


class CloudMemory:
    """
    Mengram Cloud client.
    
    Drop-in replacement for local Memory class.
    Data stored in cloud PostgreSQL — works from any device.
    """

    DEFAULT_BASE_URL = "https://mengram.io"

    def __init__(self, api_key: str, base_url: str = None):
        self.api_key = api_key
        self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")

    def _request(self, method: str, path: str, data: dict = None,
                 params: dict = None) -> dict:
        """Make authenticated API request with retry for transient errors."""
        import time as _time

        url = f"{self.base_url}{path}"
        if params:
            query_string = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items() if v is not None)
            if query_string:
                url = f"{url}?{query_string}"
        body = json.dumps(data).encode() if data else None

        last_err = None
        for attempt in range(3):
            req = urllib.request.Request(
                url,
                data=body,
                method=method,
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json",
                }
            )
            try:
                with urllib.request.urlopen(req) as resp:
                    return json.loads(resp.read())
            except urllib.error.HTTPError as e:
                resp_body = e.read().decode()
                # Retry on transient errors (429, 502, 503, 504)
                if e.code in (429, 502, 503, 504) and attempt < 2:
                    _time.sleep(1 * (attempt + 1))
                    last_err = e
                    continue
                try:
                    detail = json.loads(resp_body).get("detail", resp_body)
                except Exception:
                    detail = resp_body
                # Quota exceeded — structured error
                if e.code == 402 and isinstance(detail, dict):
                    raise QuotaExceededError(detail)
                raise Exception(f"API error {e.code}: {detail}")
            except (urllib.error.URLError, ConnectionError, TimeoutError) as e:
                # Retry on network errors
                if attempt < 2:
                    _time.sleep(1 * (attempt + 1))
                    last_err = e
                    continue
                raise Exception(f"Network error: {e}")
        raise Exception(f"Request failed after 3 attempts: {last_err}")

    def add(self, messages: list[dict], user_id: str = "default",
            agent_id: str = None, run_id: str = None, app_id: str = None,
            expiration_date: str = None) -> dict:
        """
        Add memories from conversation.
        
        Automatically extracts entities, facts, relations, and knowledge.
        Returns immediately — processing happens in background.
        
        Args:
            messages: [{"role": "user", "content": "..."}, ...]
            user_id: User identifier
            agent_id: Agent identifier (for multi-agent systems)
            run_id: Run/session identifier
            app_id: Application identifier
            expiration_date: ISO datetime string — facts auto-expire after this date.
                             None = persist forever.
            
        Returns:
            {"status": "accepted", "job_id": "job-...", "message": "..."}
        """
        body = {"messages": messages, "user_id": user_id}
        if agent_id:
            body["agent_id"] = agent_id
        if run_id:
            body["run_id"] = run_id
        if app_id:
            body["app_id"] = app_id
        if expiration_date:
            body["expiration_date"] = expiration_date
        return self._request("POST", "/v1/add", body)

    def add_text(self, text: str, user_id: str = "default",
                 agent_id: str = None, run_id: str = None,
                 app_id: str = None, expiration_date: str = None) -> dict:
        """Add memories from plain text.

        Args:
            text: Plain text to extract memories from
            user_id: User identifier
            agent_id: Filter by agent
            run_id: Filter by run/session
            app_id: Filter by application
            expiration_date: ISO datetime when memories expire (e.g. "2026-12-31")
        """
        body = {"text": text, "user_id": user_id}
        if agent_id:
            body["agent_id"] = agent_id
        if run_id:
            body["run_id"] = run_id
        if app_id:
            body["app_id"] = app_id
        if expiration_date:
            body["expiration_date"] = expiration_date
        return self._request("POST", "/v1/add_text", body)

    def search(self, query: str, user_id: str = "default",
               limit: int = 5, agent_id: str = None,
               run_id: str = None, app_id: str = None,
               graph_depth: int = 2,
               filters: dict = None) -> list[dict]:
        """
        Semantic search across memories.

        Args:
            query: Natural language query
            user_id: User identifier
            limit: Max results
            agent_id: Filter by agent
            run_id: Filter by run/session
            app_id: Filter by application
            graph_depth: How many hops to traverse in the knowledge graph (default: 2)
            filters: Metadata filters, e.g. {"agent_id": "support-bot", "app_id": "prod"}

        Returns:
            [{"entity": "...", "type": "...", "score": 0.85, "facts": [...], "knowledge": [...]}]
        """
        body = {"query": query, "user_id": user_id, "limit": limit,
                "graph_depth": graph_depth}
        if agent_id:
            body["agent_id"] = agent_id
        if run_id:
            body["run_id"] = run_id
        if app_id:
            body["app_id"] = app_id
        if filters:
            body["filters"] = filters
        result = self._request("POST", "/v1/search", body)
        return result.get("results", [])

    def get_all(self, user_id: str = "default") -> list[dict]:
        """Get all memories for user."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        result = self._request("GET", "/v1/memories", params=params)
        return result.get("memories", [])

    def get_all_full(self, user_id: str = "default") -> list[dict]:
        """Get all memories with full details in one request."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        result = self._request("GET", "/v1/memories/full", params=params)
        return result.get("memories", [])

    def get(self, name: str, user_id: str = "default") -> Optional[dict]:
        """Get specific entity details."""
        try:
            params = {}
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            return self._request("GET", f"/v1/memory/{urllib.parse.quote(name, safe='')}", params=params)
        except Exception:
            return None

    def delete(self, name: str, user_id: str = "default") -> bool:
        """Delete a memory."""
        try:
            params = {}
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            self._request("DELETE", f"/v1/memory/{urllib.parse.quote(name, safe='')}", params=params)
            return True
        except Exception:
            return False

    def stats(self, user_id: str = "default") -> dict:
        """Get usage statistics."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", "/v1/stats", params=params)

    def timeline(self, after: str = None, before: str = None,
                 user_id: str = "default", limit: int = 20) -> list[dict]:
        """Temporal search — facts in a time range."""
        params = {"limit": limit}
        if after:
            params["after"] = after
        if before:
            params["before"] = before
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        resp = self._request("GET", "/v1/timeline", params=params)
        return resp.get("results", [])

    def graph(self, user_id: str = "default") -> dict:
        """Get knowledge graph (nodes + edges)."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", "/v1/graph", params=params)

    # ---- Memory Management ----

    def reindex(self, user_id: str = "default") -> dict:
        """Re-embed all entities."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/reindex", params=params)

    def dedup(self, user_id: str = "default") -> dict:
        """Find and merge duplicate entities."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/dedup", params=params)

    def dedup_all(self, user_id: str = "default") -> dict:
        """Deduplicate facts across all entities."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/dedup_all", params=params)

    def dedup_entity(self, name: str, user_id: str = "default") -> dict:
        """Deduplicate facts on a specific entity."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", f"/v1/entity/{urllib.parse.quote(name, safe='')}/dedup", params=params)

    def merge(self, source: str, target: str, user_id: str = "default") -> dict:
        """Merge two entities."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        params["source"] = source
        params["target"] = target
        return self._request("POST", "/v1/merge", params=params)

    def merge_user(self, user_id: str = "default") -> dict:
        """Merge 'User' entity into the primary person entity."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/merge_user", params=params)

    def archive_fact(self, entity: str, fact: str, user_id: str = "default") -> dict:
        """Archive a specific fact on an entity."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/archive_fact",
                            {"entity_name": entity, "fact_content": fact}, params=params)

    def fix_entity_type(self, name: str, new_type: str, user_id: str = "default") -> dict:
        """Fix entity type classification."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        params["new_type"] = new_type
        return self._request("PATCH", f"/v1/entity/{urllib.parse.quote(name, safe='')}/type", params=params)

    def feed(self, limit: int = 50, user_id: str = "default") -> list:
        """Get activity feed."""
        params = {"limit": limit}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        result = self._request("GET", "/v1/feed", params=params)
        return result.get("feed", [])

    # ---- Cognitive Profile ----

    def get_profile(self, user_id: str = "default", force: bool = False) -> dict:
        """
        Generate a Cognitive Profile — a ready-to-use system prompt from user memory.

        The profile summarizes who the user is, their preferences, communication style,
        current focus, and key relationships. Insert into any LLM's system prompt for
        instant personalization.

        Args:
            user_id: User to generate profile for (default: account owner from API key)
            force: If True, regenerate even if cached

        Returns:
            {"user_id": "...", "system_prompt": "...", "facts_used": 47, "status": "ok"}
        """
        params = {}
        if force:
            params["force"] = "true"
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", "/v1/profile", params=params)

    # ---- Episodic Memory ----

    def episodes(self, query: str = None, limit: int = None,
                 after: str = None, before: str = None,
                 user_id: str = "default") -> list[dict]:
        """
        Get or search episodic memories (events, interactions, experiences).
        
        Args:
            query: Search query (if None, returns recent episodes)
            limit: Max results
            after: ISO datetime filter (start)
            before: ISO datetime filter (end)
            
        Returns:
            List of episodes with summary, context, outcome, participants
        """
        if query:
            params = {"query": query, "limit": limit or 5}
            if after:
                params["after"] = after
            if before:
                params["before"] = before
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            resp = self._request("GET", "/v1/episodes/search", params=params)
            return resp.get("results", [])
        else:
            params = {"limit": limit or 20}
            if after:
                params["after"] = after
            if before:
                params["before"] = before
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            resp = self._request("GET", "/v1/episodes", params=params)
            return resp.get("episodes", [])

    # ---- Procedural Memory ----

    def procedures(self, query: str = None, limit: int = 20,
                   user_id: str = "default") -> list[dict]:
        """
        Get or search procedural memories (learned workflows, skills).
        
        Args:
            query: Search query (if None, returns all procedures)
            limit: Max results
            
        Returns:
            List of procedures with name, trigger, steps, success/fail counts
        """
        if query:
            params = {"query": query, "limit": limit}
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            resp = self._request("GET", "/v1/procedures/search", params=params)
            return resp.get("results", [])
        else:
            params = {"limit": limit}
            if user_id and user_id != "default":
                params["sub_user_id"] = user_id
            resp = self._request("GET", "/v1/procedures", params=params)
            return resp.get("procedures", [])

    def procedure_feedback(self, procedure_id: str, success: bool = True,
                           context: str = None, failed_at_step: int = None,
                           user_id: str = "default") -> dict:
        """
        Record success/failure feedback for a procedure.

        On failure with context, triggers experience-driven evolution:
        the system creates a failure episode, analyzes what went wrong,
        and evolves the procedure to a new improved version.

        Args:
            procedure_id: UUID of the procedure
            success: True if the procedure worked, False if it failed
            context: What went wrong (triggers evolution when success=False)
            failed_at_step: Which step number failed (optional)

        Returns:
            Updated procedure with success_count/fail_count and evolution_triggered flag
        """
        data = None
        if context is not None:
            data = {"context": context}
            if failed_at_step is not None:
                data["failed_at_step"] = failed_at_step
        params = {"success": "true" if success else "false"}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("PATCH", f"/v1/procedures/{procedure_id}/feedback",
                            data=data, params=params)

    def procedure_history(self, procedure_id: str,
                          user_id: str = "default") -> dict:
        """
        Get version history for a procedure.

        Shows how the procedure evolved over time through experience-driven learning.

        Args:
            procedure_id: UUID of any version of the procedure

        Returns:
            {"versions": [...], "evolution_log": [...]}
        """
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", f"/v1/procedures/{procedure_id}/history", params=params)

    def procedure_evolution(self, procedure_id: str,
                            user_id: str = "default") -> dict:
        """
        Get the evolution log for a procedure.

        Shows what changed at each version and which episodes triggered the changes.

        Args:
            procedure_id: UUID of any version of the procedure

        Returns:
            {"evolution": [...]}
        """
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", f"/v1/procedures/{procedure_id}/evolution", params=params)

    # ---- Unified Search ----

    def search_all(self, query: str, limit: int = 5,
                   user_id: str = "default",
                   graph_depth: int = 2) -> dict:
        """
        Search across all 3 memory types: semantic, episodic, procedural.

        Args:
            query: Natural language query
            limit: Max results per type
            user_id: User identifier
            graph_depth: How many hops to traverse in the knowledge graph (default: 2)

        Returns:
            {"semantic": [...], "episodic": [...], "procedural": [...]}
        """
        return self._request("POST", "/v1/search/all",
                            data={"query": query, "limit": limit,
                                  "user_id": user_id, "graph_depth": graph_depth})

    # ---- Agents ----

    def run_agents(self, agent: str = "all", auto_fix: bool = False,
                   user_id: str = "default") -> dict:
        """
        Run memory agents.
        
        Args:
            agent: "curator", "connector", "digest", or "all"
            auto_fix: Auto-archive low quality and stale facts (curator only)
            
        Returns:
            Agent results with findings, patterns, suggestions
        """
        params = {"agent": agent, "auto_fix": str(auto_fix).lower()}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/agents/run", params=params)

    def agent_history(self, agent: str = None, limit: int = 10,
                      user_id: str = "default") -> list:
        """Get agent run history."""
        params = {"limit": limit}
        if agent:
            params["agent"] = agent
        result = self._request("GET", "/v1/agents/history", params=params)
        return result.get("runs", [])

    def agent_status(self, user_id: str = "default") -> dict:
        """Check which agents are due to run."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", "/v1/agents/status", params=params)

    # ---- Insights & Reflections ----

    def insights(self, user_id: str = "default") -> dict:
        """Get AI insights from memory reflections."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("GET", "/v1/insights", params=params)

    def reflect(self, user_id: str = "default") -> dict:
        """Trigger memory reflection — generates AI insights from facts."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", "/v1/reflect", params=params)

    def reflections(self, scope: str = None, user_id: str = "default") -> list:
        """Get all reflections. Optional scope: entity, cross, temporal."""
        params = {}
        if scope:
            params["scope"] = scope
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        result = self._request("GET", "/v1/reflections", params=params)
        return result.get("reflections", [])

    # ---- Webhooks ----

    def create_webhook(self, url: str, name: str = "",
                       event_types: list = None, secret: str = "",
                       user_id: str = "default") -> dict:
        """
        Create a webhook.
        
        Args:
            url: URL to send POST requests to
            name: Human-readable name
            event_types: ["memory_add", "memory_update", "memory_delete"]
            secret: Optional HMAC secret for signature verification
        """
        data = {"url": url, "name": name, "secret": secret}
        if event_types:
            data["event_types"] = event_types
        result = self._request("POST", "/v1/webhooks", data)
        return result.get("webhook", result)

    def get_webhooks(self, user_id: str = "default") -> list:
        """List all webhooks."""
        result = self._request("GET", "/v1/webhooks")
        return result.get("webhooks", [])

    def update_webhook(self, webhook_id: int, url: str = None,
                       name: str = None, event_types: list = None,
                       active: bool = None, user_id: str = "default") -> dict:
        """Update a webhook."""
        data = {}
        if url is not None: data["url"] = url
        if name is not None: data["name"] = name
        if event_types is not None: data["event_types"] = event_types
        if active is not None: data["active"] = active
        return self._request("PUT", f"/v1/webhooks/{webhook_id}", data)

    def delete_webhook(self, webhook_id: int, user_id: str = "default") -> bool:
        """Delete a webhook."""
        try:
            self._request("DELETE", f"/v1/webhooks/{webhook_id}")
            return True
        except Exception:
            return False

    # ---- Teams ----

    def create_team(self, name: str, description: str = "",
                    user_id: str = "default") -> dict:
        """Create a team. Returns team info with invite_code."""
        result = self._request("POST", "/v1/teams", {"name": name, "description": description})
        return result.get("team", result)

    def join_team(self, invite_code: str, user_id: str = "default") -> dict:
        """Join a team via invite code."""
        return self._request("POST", "/v1/teams/join", {"invite_code": invite_code})

    def get_teams(self, user_id: str = "default") -> list:
        """List user's teams."""
        result = self._request("GET", "/v1/teams")
        return result.get("teams", [])

    def share_memory(self, entity_name: str, team_id: int,
                     user_id: str = "default") -> dict:
        """Share a memory entity with a team."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", f"/v1/teams/{team_id}/share",
                            {"entity": entity_name}, params=params)

    def unshare_memory(self, entity_name: str, team_id: int,
                       user_id: str = "default") -> dict:
        """Make a shared memory personal again."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", f"/v1/teams/{team_id}/unshare",
                            {"entity": entity_name}, params=params)

    def leave_team(self, team_id: int) -> dict:
        """Leave a team."""
        return self._request("POST", f"/v1/teams/{team_id}/leave")

    def delete_team(self, team_id: int) -> dict:
        """Delete a team (owner only)."""
        return self._request("DELETE", f"/v1/teams/{team_id}")

    def team_members(self, team_id: int) -> list:
        """Get team members."""
        result = self._request("GET", f"/v1/teams/{team_id}/members")
        return result.get("members", [])

    # ---- API Key Management ----

    def list_keys(self) -> list:
        """List all API keys for your account."""
        return self._request("GET", "/v1/keys")["keys"]

    def create_key(self, name: str = "default") -> dict:
        """Create a new API key. Returns raw key (save it!)."""
        return self._request("POST", "/v1/keys", {"name": name})

    def revoke_key(self, key_id: str) -> dict:
        """Revoke a specific API key by ID."""
        return self._request("DELETE", f"/v1/keys/{key_id}")

    def rename_key(self, key_id: str, name: str) -> dict:
        """Rename an API key."""
        return self._request("PATCH", f"/v1/keys/{key_id}", {"name": name})

    # ---- Job Tracking (Async) ----

    def job_status(self, job_id: str) -> dict:
        """Check status of a background job."""
        return self._request("GET", f"/v1/jobs/{job_id}")

    def wait_for_job(self, job_id: str, poll_interval: float = 1.0,
                     max_wait: float = 60.0) -> dict:
        """Wait for a background job to complete.
        
        Args:
            job_id: Job ID from add() response
            poll_interval: Seconds between status checks
            max_wait: Maximum seconds to wait
            
        Returns:
            Job result when completed
        """
        import time as _time
        start = _time.time()
        while _time.time() - start < max_wait:
            job = self.job_status(job_id)
            if job["status"] in ("completed", "failed"):
                return job
            _time.sleep(poll_interval)
        raise TimeoutError(f"Job {job_id} timed out after {max_wait}s")

    # ---- Smart Triggers (v2.6) ----

    def get_triggers(self, target_user_id: str = None,
                     include_fired: bool = False, limit: int = 50,
                     user_id: str = "default") -> list:
        """Get smart triggers (reminders, contradictions, patterns)."""
        params = {"include_fired": str(include_fired).lower(), "limit": limit}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        path = f"/v1/triggers/{target_user_id}" if target_user_id else "/v1/triggers"
        result = self._request("GET", path, params=params)
        return result.get("triggers", [])

    def process_triggers(self) -> dict:
        """Manually fire all pending triggers."""
        return self._request("POST", "/v1/triggers/process")

    def dismiss_trigger(self, trigger_id: int) -> dict:
        """Dismiss a trigger without sending webhook."""
        return self._request("DELETE", f"/v1/triggers/{trigger_id}")

    def detect_triggers(self, target_user_id: str,
                        user_id: str = "default") -> dict:
        """Detect smart triggers for a user."""
        params = {}
        if user_id and user_id != "default":
            params["sub_user_id"] = user_id
        return self._request("POST", f"/v1/triggers/detect/{target_user_id}", params=params)

    # ---- Billing ----

    def get_billing(self) -> dict:
        """Get current subscription plan, usage, and quotas."""
        return self._request("GET", "/v1/billing")

    def create_checkout(self, plan: str) -> dict:
        """Create Paddle checkout session for plan upgrade.

        Args:
            plan: 'pro' or 'business'

        Returns:
            {"checkout_url": "https://...paddle.com/...", "transaction_id": "txn_..."}
        """
        return self._request("POST", "/v1/billing/checkout", params={"plan": plan})

    def create_portal(self) -> dict:
        """Create Paddle customer portal session for managing subscription.

        Returns:
            {"portal_url": "https://customer-portal.paddle.com/..."}
        """
        return self._request("POST", "/v1/billing/portal")

    # ---- Import ----

    def import_chatgpt(self, zip_path: str, user_id: str = "default",
                       chunk_size: int = 20, on_progress=None) -> dict:
        """
        Import ChatGPT export ZIP into memory.

        Args:
            zip_path: Path to ChatGPT export ZIP file
            user_id: User identifier
            chunk_size: Max messages per chunk (default 20)
            on_progress: Optional callback(current, total, title)

        Returns:
            ImportResult as dict
        """
        from importer import import_chatgpt as _import
        add_fn = lambda msgs: self.add(msgs, user_id=user_id)
        return _import(zip_path, add_fn, chunk_size=chunk_size,
                       on_progress=on_progress).__dict__

    def import_obsidian(self, vault_path: str, user_id: str = "default",
                        chunk_chars: int = 4000, on_progress=None) -> dict:
        """
        Import Obsidian vault into memory.

        Args:
            vault_path: Path to Obsidian vault directory
            user_id: User identifier
            chunk_chars: Max characters per text chunk (default 4000)
            on_progress: Optional callback(current, total, title)

        Returns:
            ImportResult as dict
        """
        from importer import import_obsidian as _import
        add_fn = lambda msgs: self.add(msgs, user_id=user_id)
        return _import(vault_path, add_fn, chunk_chars=chunk_chars,
                       on_progress=on_progress).__dict__

    def import_files(self, paths: list, user_id: str = "default",
                     chunk_chars: int = 4000, on_progress=None) -> dict:
        """
        Import text/markdown files into memory.

        Args:
            paths: List of file paths
            user_id: User identifier
            chunk_chars: Max characters per text chunk (default 4000)
            on_progress: Optional callback(current, total, title)

        Returns:
            ImportResult as dict
        """
        from importer import import_files as _import
        add_fn = lambda msgs: self.add(msgs, user_id=user_id)
        return _import(paths, add_fn, chunk_chars=chunk_chars,
                       on_progress=on_progress).__dict__
