"""TrustMem Python SDK — Mem0-style direct wrapper around PersistentHandler.

Usage::

    # .env
    # OPENAI_API_KEY=sk-...

    from trustmem import TrustMem

    m = TrustMem()
    m.add([{"role": "user", "content": "Alex likes sushi"}], user_id="alex")
    results = m.search("food preferences", user_id="alex")
    print(results)

All operations are local (in-process, no HTTP server needed).
Data is persisted in SQLite by default (~/.trustmem/trustmem.db).
OPENAI_API_KEY is loaded from .env file (via python-dotenv) or environment variable.
"""
from __future__ import annotations

import logging
from typing import Any, TYPE_CHECKING

from trustmem.exceptions import (
    ConfigurationError,
    NotFoundError,
    PermissionDeniedError,
    ValidationError,
)

if TYPE_CHECKING:
    from trustmem.handler.persistent import PersistentHandler
    from trustmem.types import (
        AddRawResult,
        AddResult,
        GetAllResult,
        SearchResult,
        StatsResult,
        EntityStatsResult,
        VerifyResult,
        VerifyAllResult,
    )

logger = logging.getLogger("trustmem")


class TrustMem:
    """Mem0-style Python SDK for TrustMem (Zero Trust Memory).

    Quick start::

        # Set OPENAI_API_KEY=sk-... in .env
        m = TrustMem()

    For testing / DI, inject a pre-built handler via ``_handler``::

        m = TrustMem._from_handler(handler)
    """

    def __init__(self) -> None:
        from trustmem.config import TrustMemConfig
        from trustmem.handler.factory import create_handler

        config = TrustMemConfig.from_env()
        issues = config.validate()
        errors = [i for i in issues if i.level == "error"]
        if errors:
            raise ConfigurationError(
                "Configuration error. Check .env file or environment variables.\n"
                + "\n".join(str(e) for e in errors)
            )
        self._handler = create_handler(config)
        logger.debug("TrustMem initialized")

    @classmethod
    def _from_handler(
        cls, handler: "PersistentHandler",
    ) -> "TrustMem":
        """Create SDK instance from a pre-built handler (testing / DI)."""
        inst = cls.__new__(cls)
        inst._handler = handler
        return inst

    # ------------------------------------------------------------------
    # ID resolution
    # ------------------------------------------------------------------
    def _resolve_scope(self, user_id: str | None, visibility: str | None) -> str | None:
        """Resolve scope: user_id or visibility='organization'.

        Returns the resolved user_id (None if visibility='organization').

        Raises ValueError if:
          - both user_id and visibility are provided
          - neither user_id nor visibility is provided (both None/empty)
          - visibility is not 'organization'
        """
        # Treat empty string same as None
        if isinstance(user_id, str) and not user_id.strip():
            user_id = None
        if user_id and visibility:
            raise ValueError("user_id and visibility are mutually exclusive")
        if not user_id and not visibility:
            raise ValueError("user_id or visibility='organization' is required")
        if visibility and visibility != "organization":
            raise ValueError("visibility must be 'organization'")
        # visibility="organization" means user_id=None (org-shared)
        return user_id

    # ------------------------------------------------------------------
    # Core (4 methods + close)
    # ------------------------------------------------------------------
    def add(
        self,
        data: list[dict],
        *,
        tags: list[str] | None = None,
        metadata: dict | None = None,
        session_id: str | None = None,
        custom_extraction_prompt: str | None = None,
        dry_run: bool = False,
        source_trust: str | None = None,
        expires_at: str | None = None,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> "AddResult":
        """Extract and store memories from conversation messages.

        Args:
            data: List of message dicts ``[{"role": "user", "content": "..."}]``.
            user_id: User scope. Mutually exclusive with visibility.
            visibility: ``"organization"`` for org-shared memories. Mutually exclusive with user_id.
        """
        if not isinstance(data, list):
            raise ValidationError(
                f"data must be list[dict], got {type(data).__name__}. "
                'Pass conversation messages as [{"role": "user", "content": "..."}].'
            )
        # Filter out invalid messages
        valid_messages = []
        for i, msg in enumerate(data):
            if not isinstance(msg, dict):
                logger.warning("Skipping message[%d]: not a dict: %r", i, type(msg).__name__)
                continue
            if "role" not in msg or "content" not in msg:
                logger.warning("Skipping message[%d]: missing role or content: %r", i, msg)
                continue
            valid_messages.append(msg)
        if not valid_messages:
            raise ValidationError(
                "No valid messages found. Each message must be a dict with "
                '"role" and "content" keys.'
            )
        # Validate source_trust
        if source_trust is not None:
            from trustmem.constants import SOURCE_TRUST_LEVELS
            if source_trust not in SOURCE_TRUST_LEVELS:
                raise ValidationError(
                    f"source_trust must be one of {SOURCE_TRUST_LEVELS}, "
                    f"got {source_trust!r}"
                )

        resolved_user_id = self._resolve_scope(user_id, visibility)
        logger.debug("add: user_id=%s, messages=%d", resolved_user_id, len(valid_messages))
        remember_kwargs: dict[str, Any] = {
            "messages": valid_messages,
            "tags": tags,
            "metadata": metadata,
            "session_id": session_id,
            "custom_extraction_prompt": custom_extraction_prompt,
            "dry_run": dry_run,
            "user_id": resolved_user_id,
        }
        if source_trust is not None:
            remember_kwargs["source_trust"] = source_trust
        if expires_at is not None:
            remember_kwargs["expires_at"] = expires_at
        result = self._handler.remember(**remember_kwargs)
        logger.debug("add: extracted=%d facts", len(result.get("extracted", [])))
        return result

    def search(
        self,
        query: str,
        *,
        limit: int = 10,
        min_source_trust: str | None = None,
        filters: dict | None = None,
        accessor: str = "system",
        exclude_expired: bool = False,
        memory_type: str | None = None,
        search_mode: str | None = None,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> "SearchResult":
        """Semantic search for memories.

        Returns dict with ``results`` (memories) and ``relations`` (graph edges).

        Args:
            search_mode: Search mode — ``"hybrid"`` (default), ``"vector"``, or ``"keyword"``.
        """
        resolved_user_id = self._resolve_scope(user_id, visibility)
        logger.debug("search: user_id=%s, query=%r, limit=%d", resolved_user_id, query[:50], limit)
        search_kwargs: dict[str, Any] = {
            "query": query,
            "limit": limit,
            "min_source_trust": min_source_trust,
            "filters": filters,
            "accessor": accessor,
            "exclude_expired": exclude_expired,
            "user_id": resolved_user_id,
            "memory_type": memory_type,
        }
        if search_mode is not None:
            search_kwargs["search_mode"] = search_mode
        result = self._handler.search_memory(**search_kwargs)
        memories = result.get("results", [])

        relations = self._handler.search_entity_relations(
            entity_name=query,
            partial_match=True,
            user_id=resolved_user_id,
        )

        logger.debug("search: %d results, %d relations", len(memories), len(relations))
        return {"results": memories, "relations": relations}

    def search_raw(
        self,
        query: str,
        *,
        limit: int = 10,
        filters: dict | None = None,
        accessor: str = "system",
        exclude_expired: bool = False,
        memory_type: str | None = None,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> dict:
        """Pure cosine similarity search without TrustMem ranking."""
        resolved_user_id = self._resolve_scope(user_id, visibility)
        logger.debug("search_raw: user_id=%s, query=%r, limit=%d", resolved_user_id, query[:50], limit)
        result = self._handler.search_memory_raw(
            query=query,
            limit=limit,
            filters=filters,
            accessor=accessor,
            exclude_expired=exclude_expired,
            user_id=resolved_user_id,
            memory_type=memory_type,
        )
        logger.debug("search_raw: %d results", len(result.get("results", [])))
        return result

    def get_all(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        relation_limit: int = 100,
        min_source_trust: str | None = None,
        accessor: str = "system",
        memory_type: str | None = None,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> "GetAllResult":
        """Get all memories and relations.

        Args:
            min_source_trust: Minimum source trust level filter.
        """
        resolved_user_id = self._resolve_scope(user_id, visibility)
        result = self._handler.list_memories(
            limit=limit,
            offset=offset,
            accessor=accessor,
            user_id=resolved_user_id,
            memory_type=memory_type,
        )
        memories = result.get("memories", [])

        # Source trust filtering (post-fetch)
        if min_source_trust:
            from trustmem.constants import SOURCE_TRUST_HIERARCHY
            min_rank = SOURCE_TRUST_HIERARCHY.get(min_source_trust, 0)
            memories = [
                m for m in memories
                if SOURCE_TRUST_HIERARCHY.get(
                    m.get("source_trust", "derived"), 0
                ) >= min_rank
            ]

        relations = self._handler.list_entity_edges(
            user_id=resolved_user_id,
            limit=relation_limit,
        )

        return {"results": memories, "relations": relations}

    def list_all(
        self,
        *,
        user_id: str | None = None,
        limit: int = 100,
        offset: int = 0,
        memory_type: str | None = None,
    ) -> dict:
        """List all memories without scope filter (admin).

        Unlike get_all(), this ignores scope rules and returns
        all memories matching the given filters. Admin use only.
        No scope validation — all params optional.
        """
        return self._handler.list_all_memories(
            user_id=user_id,
            limit=limit,
            offset=offset,
            memory_type=memory_type,
        )

    def get(
        self,
        memory_id: str,
    ) -> dict:
        """Get a single memory by ID.

        Raises:
            NotFoundError: If the memory does not exist.
        """
        try:
            return self._handler.get_memory(memory_id=memory_id)
        except ValueError as e:
            raise NotFoundError(str(e)) from e

    def delete(
        self,
        memory_id: str,
    ) -> bool:
        """Delete a memory by ID."""
        try:
            return self._handler.delete_memory(memory_id=memory_id)
        except KeyError as e:
            raise NotFoundError(str(e)) from e

    def close(self) -> None:
        """Release resources (close SQLite connection, clear caches)."""
        db = getattr(self._handler, "_db", None)
        if db is not None:
            db.close()
        cache = getattr(self._handler, "_embedding_cache", None)
        if cache is not None:
            cache.clear()

    def __enter__(self) -> "TrustMem":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def __repr__(self) -> str:
        db = getattr(self._handler, "_database_url", "unknown")
        return f"TrustMem(db={db!r})"

    # ==================================================================
    # ZTM Management
    # ==================================================================
    def verify(self, memory_id: str, *, auto_quarantine: bool = True) -> "VerifyResult":
        """Verify memory integrity (content_hash check, like git fsck)."""
        return self._handler.verify_memory(
            memory_id=memory_id,
            auto_quarantine=auto_quarantine,
        )

    def verify_all(self) -> "VerifyAllResult":
        """Verify all memories (like git fsck --full)."""
        return self._handler.verify_all_memories()

    def audit_trail(
        self,
        memory_id: str,
        *,
        limit: int = 50,
        offset: int = 0,
    ) -> list:
        """Get audit trail for a memory."""
        return self._handler.get_audit_trail(
            memory_id=memory_id,
            limit=limit,
            offset=offset,
        )

    def contradictions(
        self,
        memory_id: str,
        *,
        limit: int = 20,
        offset: int = 0,
    ) -> list:
        """Get contradiction relationships for a memory."""
        return self._handler.get_contradictions(
            memory_id=memory_id,
            limit=limit,
            offset=offset,
        )

    def provenance(
        self,
        memory_id: str,
        *,
        limit: int = 20,
        offset: int = 0,
    ) -> list:
        """Get provenance links (derived_from etc) for a memory."""
        return self._handler.get_provenance(
            memory_id=memory_id,
            limit=limit,
            offset=offset,
        )

    def verify_audit_chain(self) -> dict:
        """Verify audit trail hash chain integrity (tamper detection)."""
        return self._handler.verify_audit_chain()

    # ==================================================================
    # Read Audit (Access Log)
    # ==================================================================
    def access_log(
        self,
        *,
        memory_id: str | None = None,
        accessor: str | None = None,
        since: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> list:
        """Query access log (Read Audit)."""
        return self._handler.get_access_log(
            memory_id=memory_id,
            accessor=accessor,
            since=since,
            limit=limit,
            offset=offset,
        )

    def verify_access_chain(self) -> dict:
        """Verify access log hash chain integrity (tamper detection)."""
        return self._handler.verify_access_chain()

    def purge_access_log(self, before: str) -> dict:
        """Purge access log records older than *before* (ISO 8601)."""
        if not isinstance(before, str) or len(before) < 10:
            raise ValidationError(
                "before must be an ISO 8601 date string (e.g. '2026-01-01')"
            )
        return self._handler.purge_access_log(before)

    # ==================================================================
    # Knowledge Graph
    # ==================================================================
    def add_relation(
        self,
        subject: str,
        predicate: str,
        object_: str,
        *,
        subject_type: str = "other",
        object_type: str = "other",
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> dict:
        """Add an entity relation (RDF-style triple)."""
        resolved_user_id = self._resolve_scope(user_id, visibility)
        return self._handler.add_entity_edge(
            subject_name=subject,
            subject_type=subject_type,
            predicate=predicate,
            object_name=object_,
            object_type=object_type,
            user_id=resolved_user_id,
        )

    def search_relations(
        self,
        entity: str,
        *,
        limit: int = 20,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> list:
        """Search entity relations matching an entity name."""
        resolved_user_id = self._resolve_scope(user_id, visibility)
        return self._handler.search_entity_relations(
            entity_name=entity,
            limit=limit,
            user_id=resolved_user_id,
        )

    def get_graph(
        self,
        entity: str,
        *,
        hops: int = 2,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> dict:
        """Get entity subgraph via multi-hop traversal."""
        resolved_user_id = self._resolve_scope(user_id, visibility)
        return self._handler.get_entity_graph(
            entity_name=entity,
            hops=hops,
            user_id=resolved_user_id,
        )

    # ==================================================================
    # Statistics & Billing
    # ==================================================================
    def stats(self, *, user_id: str | None = None) -> "StatsResult":
        """Get memory statistics (confidence distribution, counts)."""
        return self._handler.get_memory_stats(
            user_id=user_id,
        )

    def entity_stats(self, *, user_id: str | None = None) -> "EntityStatsResult":
        """Get entity/edge statistics."""
        return self._handler.get_entity_stats(
            user_id=user_id,
        )

    # ==================================================================
    # Raw Data Ingestion (Admin)
    # ==================================================================
    def add_raw(
        self,
        content: str,
        *,
        embedding: list[float] | None = None,
        tags: list[str] | None = None,
        metadata: dict | None = None,
        confidence: str = "medium",
        source_trust: str = "derived",
        memory_type: str = "semantic",
        expires_at: str | None = None,
        user_id: str | None = None,
        visibility: str | None = None,
    ) -> "AddRawResult":
        """Add a memory directly, bypassing LLM extraction pipeline."""
        if not isinstance(content, str) or not content.strip():
            raise ValidationError("content must be a non-empty string")
        from trustmem.ztm.constants import CONFIDENCE_LEVELS
        if confidence not in CONFIDENCE_LEVELS:
            raise ValidationError(
                f"confidence must be one of {CONFIDENCE_LEVELS}, got {confidence!r}"
            )
        from trustmem.constants import SOURCE_TRUST_LEVELS, MEMORY_TYPES
        if source_trust not in SOURCE_TRUST_LEVELS:
            raise ValidationError(
                f"source_trust must be one of {SOURCE_TRUST_LEVELS}, "
                f"got {source_trust!r}"
            )
        if memory_type not in MEMORY_TYPES:
            raise ValidationError(
                f"memory_type must be one of {MEMORY_TYPES}, "
                f"got {memory_type!r}"
            )
        if embedding is not None:
            if not isinstance(embedding, list) or not all(
                isinstance(v, (int, float)) for v in embedding
            ):
                raise ValidationError(
                    "embedding must be a list of numbers (list[float])"
                )
        if expires_at is not None and not isinstance(expires_at, str):
            raise ValidationError(
                "expires_at must be an ISO 8601 datetime string"
            )

        resolved_user_id = self._resolve_scope(user_id, visibility)
        logger.debug("add_raw: user_id=%s, content_len=%d", resolved_user_id, len(content))

        result = self._handler.add_memory(
            content=content,
            tags=tags,
            metadata=metadata,
            _precomputed_embedding=embedding,
            _confidence=confidence,
            _source_trust=source_trust,
            memory_type=memory_type,
            expires_at=expires_at,
            user_id=resolved_user_id,
        )
        logger.debug("add_raw: memory_id=%s", result["memory_id"])
        return result

    def update_raw(
        self,
        memory_id: str,
        content: str,
        *,
        embedding: list[float] | None = None,
        tags: list[str] | None = None,
        metadata: dict | None = None,
        reason: str = "",
    ) -> dict:
        """Create a new version of an existing memory."""
        if not isinstance(content, str) or not content.strip():
            raise ValidationError("content must be a non-empty string")
        if embedding is not None:
            if not isinstance(embedding, list) or not all(
                isinstance(v, (int, float)) for v in embedding
            ):
                raise ValidationError(
                    "embedding must be a list of numbers (list[float])"
                )

        logger.debug("update_raw: memory_id=%s, content_len=%d", memory_id, len(content))
        result = self._handler.update_memory(
            memory_id=memory_id,
            content=content,
            embedding=embedding,
            tags=tags,
            metadata=metadata,
            reason=reason,
        )
        logger.debug(
            "update_raw: old=%s -> new=%s",
            result["old_memory_id"], result["new_memory_id"],
        )
        return result

    # ==================================================================
    # Maintenance
    # ==================================================================
    def backfill_hashes(self) -> dict:
        """Backfill missing content_hash values for legacy memories."""
        return self._handler.backfill_content_hash()

    def cleanup_audit_trail(
        self,
        before: str,
        *,
        dry_run: bool = False,
    ) -> dict:
        """Count audit trail records older than *before* (dry-run only)."""
        if not isinstance(before, str) or len(before) < 10:
            raise ValidationError(
                "before must be an ISO 8601 date string (e.g. '2026-01-01')"
            )
        return self._handler.cleanup_audit_trail(before=before, dry_run=dry_run)

    def cleanup_expired(
        self,
        *,
        grace_days: int = 0,
        dry_run: bool = False,
    ) -> dict:
        """Delete expired memories (TTL + grace period)."""
        return self._handler.cleanup_expired(grace_days=grace_days, dry_run=dry_run)

    def forget(
        self,
        entity: str,
        *,
        user_id: str | None = None,
        visibility: str | None = None,
        dry_run: bool = False,
        reason: str = "manual",
    ) -> dict:
        """Delete all memories and entities related to an entity."""
        resolved_user_id = self._resolve_scope(user_id, visibility)
        return self._handler.forget_entity(
            entity=entity,
            user_id=resolved_user_id,
            dry_run=dry_run,
            reason=reason,
        )

    def reset(self) -> None:
        """Clear all data (development/testing only)."""
        self._handler.reset_stores()
