"""Python SDK client for Lattice. RFC-002 §6.4."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any

from returns.result import Failure, Result, Success

from lattice.types import PatternType, RuleProposal, SearchResult

if TYPE_CHECKING:
    import sqlite3


# @invar:allow shell_result: Path helper, delegates to resolve_global_dir which returns Result
# @invar:allow shell_pure_logic: Path computation, no I/O
def _default_global_path() -> Path:
    """Get default global path (~/.config/lattice/)."""
    from lattice.shell.config import resolve_global_dir

    result = resolve_global_dir()
    if isinstance(result, Success):
        return result.unwrap()
    return Path.home() / ".config" / "lattice"


class Client:
    """High-level SDK client for Lattice memory (RFC-002 §6.4)."""

    def __init__(
        self,
        project_path: Path | str | None = None,
        global_path: Path | str | None = None,
    ) -> None:
        self._project_path = Path(project_path) if project_path else Path.cwd()
        self._global_path = Path(global_path) if global_path else _default_global_path()
        self._lattice_dir = self._project_path / ".lattice"
        self._store_path = self._lattice_dir / "store.db"
        self._config: Any = None

    def _load_config(self) -> Any:
        """Load project configuration with fallback to global config."""
        if self._config is not None:
            return self._config
        config_path = self._lattice_dir / "config.toml"
        from lattice.shell.config import load_config_with_fallback

        self._config, _ = load_config_with_fallback(config_path)
        return self._config

    def _get_store_conn(self) -> Result["sqlite3.Connection", str]:
        """Get or create a store database connection."""
        from lattice.shell.schema import create_store

        if not self._store_path.exists():
            return Failure(
                "Project not initialized. Run 'lattice ingest' to initialize."
            )
        return create_store(self._store_path)

    def _load_rules_from_dir(
        self, rules_dir: Path, prefix: str, skip_promoted: bool = False
    ) -> list[str]:
        """Load rules from a directory as formatted strings."""
        parts: list[str] = []
        if not rules_dir.exists():
            return parts
        for rule_file in sorted(rules_dir.glob("*.md")):
            try:
                content = rule_file.read_text(encoding="utf-8")
                if skip_promoted and "<!-- lattice:promoted -->" in content:
                    continue
                parts.append(f"## {prefix}: {rule_file.stem}\n\n{content}")
            except OSError:
                pass
        return parts

    def get_instincts(self) -> str:
        """Get merged Global + Project rules. System 1 (Instinct)."""
        parts = self._load_rules_from_dir(self._global_path / "rules", "Global")
        parts.extend(
            self._load_rules_from_dir(
                self._lattice_dir / "rules", "Project", skip_promoted=True
            )
        )
        return "\n\n---\n\n".join(parts) if parts else ""

    def _get_query_embedding(self, query: str) -> list[float] | None:
        """Generate embedding for query if configured."""
        config = self._load_config()
        if not config.embedding:
            return None
        from lattice.shell.embeddings import embed_text

        result = embed_text(config.embedding, query)
        return result.unwrap() if isinstance(result, Success) else None

    def _search_project(
        self,
        conn: "sqlite3.Connection",
        query: str,
        query_embedding: list[float] | None,
        limit: int,
    ) -> Result[list[SearchResult], str]:
        """Execute hybrid search on project store. Closes conn."""
        from lattice.shell.hybrid_search import hybrid_search

        try:
            search_result = hybrid_search(conn, query, query_embedding, limit)
            if isinstance(search_result, Failure):
                return search_result

            results: list[SearchResult] = []
            for r in search_result.unwrap():
                # r.role is Role enum (str subclass), convert to str for public API
                role_str = str(r.role) if r.role is not None else "unknown"
                results.append(
                    SearchResult(
                        session_id=getattr(r, "session_id", "unknown"),
                        role=role_str,
                        content=getattr(r, "content", "") or "",
                        rrf_score=getattr(r, "rrf_score", 0.0),
                        timestamp=getattr(r, "timestamp", ""),
                    )
                )
            return Success(results)
        finally:
            conn.close()

    def _search_global(
        self,
        conn: "sqlite3.Connection",
        query: str,
        query_embedding: list[float] | None,
        limit: int,
    ) -> Result[list[SearchResult], str]:
        """Execute hybrid search on global store. Closes conn."""
        from lattice.shell.hybrid_search import hybrid_search_global

        try:
            search_result = hybrid_search_global(conn, query, query_embedding, limit)
            if isinstance(search_result, Failure):
                return search_result

            results: list[SearchResult] = []
            for r in search_result.unwrap():
                results.append(
                    SearchResult(
                        session_id=r.get("source_session_id", "unknown"),
                        role="evidence",
                        content=r.get("summary", "")[:500],
                        rrf_score=r.get("score", 0.0),
                        timestamp=r.get("extracted_at", ""),
                    )
                )
            return Success(results)
        finally:
            conn.close()

    def search(
        self,
        query: str,
        limit: int = 10,
        scope: str = "project",
    ) -> Result[list[SearchResult], str]:
        """Search project memory using hybrid search (FTS5 + vector + RRF)."""
        from lattice.shell.schema import create_global_store

        query_embedding = self._get_query_embedding(query)

        if scope == "global":
            global_db_path = self._global_path / "global.db"
            if not global_db_path.exists():
                return Failure(
                    "Global store not found. Run 'lattice evolve --global' first."
                )
            store_result = create_global_store(global_db_path)
            if isinstance(store_result, Failure):
                return store_result
            return self._search_global(
                store_result.unwrap(), query, query_embedding, limit
            )

        store_result = self._get_store_conn()
        if isinstance(store_result, Failure):
            return store_result
        return self._search_project(
            store_result.unwrap(), query, query_embedding, limit
        )

    def log_turn(
        self,
        user: str,
        assistant: str,
        tools_called: list[str] | None = None,
        session_id: str | None = None,
    ) -> Result[int, str]:
        """Store a conversation turn. Sanitizes secrets. Returns log_id or Failure."""
        from lattice.core.sanitizer import sanitize
        from lattice.core.types.enums import Role
        from lattice.shell.store import generate_session_id, insert_log
        from lattice.shell.embeddings import embed_text, insert_embedding

        config = self._load_config()
        if session_id is None:
            session_id = generate_session_id()

        secret_patterns = (
            config.safety.secret_patterns if hasattr(config, "safety") else []
        )
        if secret_patterns:
            user = sanitize(user, secret_patterns, "[REDACTED]")
            assistant = sanitize(assistant, secret_patterns, "[REDACTED]")

        store_result = self._get_store_conn()
        if isinstance(store_result, Failure):
            return store_result
        conn = store_result.unwrap()

        try:
            tools_metadata = {"tools_called": tools_called} if tools_called else None
            user_result = insert_log(conn, session_id, Role.USER, user, None)
            if isinstance(user_result, Failure):
                return user_result
            user_log_id = user_result.unwrap()

            assistant_result = insert_log(
                conn, session_id, Role.ASSISTANT, assistant, tools_metadata
            )
            if isinstance(assistant_result, Failure):
                return assistant_result
            assistant_log_id = assistant_result.unwrap()

            if config.embedding:
                user_embed = embed_text(config.embedding, user)
                if isinstance(user_embed, Success):
                    insert_embedding(conn, user_log_id, user_embed.unwrap())
                assistant_embed = embed_text(config.embedding, assistant)
                if isinstance(assistant_embed, Success):
                    insert_embedding(conn, assistant_log_id, assistant_embed.unwrap())

            return Success(user_log_id)
        finally:
            conn.close()

    def log(
        self,
        type: str,
        content: str,
        *,
        input: str = "",
        status: str = "",
        error: str = "",
        session_id: str | None = None,
    ) -> Result[int, str]:
        """Record an event to Lattice memory.

        Unified event logging for session compression. Keep it brief to save tokens.

        For type=user/assistant/reasoning:
            Pass full content. input/status/error are ignored.

        For type=tool:
            Pass only summary (status + error if any), NOT raw output.
            - content: tool name (e.g., "read", "edit", "bash")
            - input: brief summary of tool input (file path, command)
            - status: "success" or "error"
            - error: error message (max 500 chars) if status="error"

        Args:
            type: Event type ("user", "assistant", "reasoning", "tool").
            content: Event content (full text or tool name).
            input: Tool input summary (for type=tool only).
            status: Tool status, "success" or "error" (for type=tool only).
            error: Error message (for type=tool with status="error" only).
            session_id: Optional session ID. Generated if not provided.

        Returns:
            Success with event_id, or Failure with error message.

        Examples:
            >>> client.log("user", "auth 报错了")  # doctest: +SKIP
            >>> client.log("reasoning", "需要检查 UserValidator...")  # doctest: +SKIP
            >>> client.log("tool", "read", input="src/auth.py", status="success")  # doctest: +SKIP
            >>> client.log("tool", "edit", input="src/auth.py", status="error", error="syntax")  # doctest: +SKIP
        """
        from lattice.core.sanitizer import sanitize
        from lattice.shell.store import generate_session_id, insert_event

        config = self._load_config()
        if session_id is None:
            session_id = generate_session_id()

        # Sanitize secrets in content (only for non-tool events)
        if type != "tool":
            secret_patterns = (
                config.safety.secret_patterns if hasattr(config, "safety") else []
            )
            if secret_patterns:
                content = sanitize(content, secret_patterns, "[REDACTED]")

        store_result = self._get_store_conn()
        if isinstance(store_result, Failure):
            return store_result
        conn = store_result.unwrap()

        try:
            return insert_event(
                conn,
                session_id,
                type,
                content,
                tool_input=input if input else None,
                tool_status=status if status else None,
                tool_error=error if error else None,
            )
        finally:
            conn.close()

    def propose_rule(
        self,
        pattern: PatternType | str | RuleProposal,
        observation: str = "",
        evidence: list[str] | None = None,
        suggested_action: str = "",
    ) -> Result[str, str]:
        """Submit a rule proposal to .lattice/drift/proposals/.

        Accepts either individual parameters or a RuleProposal object.

        Args:
            pattern: Pattern type OR a RuleProposal object. When passing a
                RuleProposal, other parameters are ignored.
            observation: What was observed (max 200 chars). Required if pattern
                is not a RuleProposal.
            evidence: List of session IDs as evidence. Required if pattern
                is not a RuleProposal.
            suggested_action: Short action slug (max 100 chars). Required if
                pattern is not a RuleProposal.

        Returns:
            Success with relative path to proposal file, or Failure with error message.

        Examples:
            >>> from lattice import Client, PatternType, RuleProposal  # doctest: +SKIP
            >>> client = Client(project_path=".")  # doctest: +SKIP
            >>> # Using individual parameters:
            >>> result = client.propose_rule(  # doctest: +SKIP
            ...     PatternType.BUG_PATTERN,
            ...     "Expired JWT tokens caused auth failures",
            ...     ["ses_abc123"],
            ...     "validate-jwt-expiry",
            ... )
            >>> # Using RuleProposal object:
            >>> proposal = RuleProposal(  # doctest: +SKIP
            ...     pattern=PatternType.BUG_PATTERN,
            ...     observation="Expired JWT tokens caused auth failures",
            ...     evidence=["ses_abc123"],
            ...     suggested_action="validate-jwt-expiry",
            ... )
            >>> result = client.propose_rule(proposal)  # doctest: +SKIP
        """
        # Handle RuleProposal object
        if isinstance(pattern, RuleProposal):
            proposal = pattern
            pattern = proposal.pattern
            observation = proposal.observation
            evidence = proposal.evidence
            suggested_action = proposal.suggested_action

        # Validate required parameters
        if evidence is None:
            evidence = []
        from datetime import datetime, timezone

        if len(observation) > 200:
            return Failure("Observation must be <= 200 characters")
        if len(suggested_action) > 100:
            return Failure("Suggested action must be <= 100 characters")

        if isinstance(pattern, str):
            try:
                pattern = PatternType(pattern.lower())
            except ValueError:
                return Failure(
                    f"Invalid pattern type. Must be one of: {[p.value for p in PatternType]}"
                )

        proposals_dir = self._lattice_dir / "drift" / "proposals"
        if not proposals_dir.exists():
            return Failure(
                "Project not initialized. Run 'lattice ingest' to initialize."
            )

        ts = datetime.now(timezone.utc)
        proposal_file = proposals_dir / f"proposal_{ts.strftime('%Y%m%d_%H%M%S')}.md"
        evidence_str = "\n".join(f"- {sid}" for sid in evidence)
        content = f"""# Rule Proposal

## Pattern
{pattern.value}

## Observation
{observation}

## Evidence
{evidence_str}

## Suggested Action
{suggested_action}

## Metadata
- Created: {ts.isoformat()}
- Source: SDK client
"""
        try:
            proposal_file.write_text(content, encoding="utf-8")
        except OSError as e:
            return Failure(f"Failed to write proposal: {e}")

        return Success(str(proposal_file.relative_to(self._project_path)))


__all__ = ["Client"]
