"""
Store CRUD operations for Lattice.

This module implements database operations for store.db:
- Log entry CRUD (insert, query)
- Metadata key-value store
- Session ID generation

All functions are Shell layer — they perform I/O and return Result[T, E].

Reference: RFC-002 §4.1 Session Lifecycle, §5.3 Schema
Reference: ARCHITECTURE.md §4.7
"""

from __future__ import annotations

import datetime
import json
import sqlite3
import uuid
from typing import TYPE_CHECKING, Any

from deal import post, pre
from returns.result import Failure, Result, Success

from lattice.core.types.enums import Role
from lattice.core.types.log import LogEntry, Session
from lattice.core.types.search import FtsResult

if TYPE_CHECKING:
    pass


# @invar:allow shell_result: UUID generation uses system entropy (I/O)
# @invar:allow shell_pure_logic: uuid.uuid4() reads system entropy - not pure
def generate_session_id() -> str:
    """Generate a new session ID (UUIDv4).

    Session IDs are used to group log entries for evolution.
    Per RFC-002 §4.1:
    - MCP server: one session per connection lifetime
    - Plugin: native session ID or inactivity heuristic
    - SDK: caller provides or generates per Client instance

    Returns:
        UUIDv4 string (e.g., "550e8400-e29b-41d4-a716-446655440000")

    >>> session_id = generate_session_id()
    >>> len(session_id)
    36
    >>> session_id.count('-')
    4
    """
    return str(uuid.uuid4())


# @invar:allow shell_result: UUID generation uses system entropy (I/O)
# @invar:allow shell_pure_logic: uuid.uuid4() reads system entropy - not pure
def generate_external_id() -> str:
    """Generate a new external ID (UUIDv4) for log entries.

    External IDs are unique identifiers for each log entry,
    used for idempotency and cross-referencing.

    Returns:
        UUIDv4 string.

    >>> external_id = generate_external_id()
    >>> len(external_id)
    36
    """
    return str(uuid.uuid4())


# @invar:allow shell_result: datetime.now() reads system clock (I/O)
# @invar:allow shell_pure_logic: datetime.now() reads system clock - not pure
def get_iso_timestamp() -> str:
    """Get current timestamp in ISO 8601 format with microsecond precision.

    Returns:
        ISO 8601 timestamp with 'Z' suffix for UTC, including microseconds.

    >>> ts = get_iso_timestamp()
    >>> ts.endswith('Z')
    True
    >>> 'T' in ts
    True
    """
    return datetime.datetime.now(datetime.timezone.utc).strftime(
        "%Y-%m-%dT%H:%M:%S.%fZ"
    )


# @shell_complexity: DB insert with role normalization, metadata JSON, and error handling
def insert_log(
    conn: sqlite3.Connection,
    session_id: str,
    role: Role | str,
    content: str,
    metadata: dict[str, Any] | None = None,
) -> Result[int, str]:
    """Insert a log entry into store.db.

    Generates external_id (UUIDv4) and timestamp (ISO8601 now).
    The FTS5 trigger automatically indexes the content.

    Args:
        conn: SQLite connection to store.db.
        session_id: Session UUID to group this log with.
        role: Message role (user, assistant, tool).
        content: Log content text.
        metadata: Optional metadata dict (stored as JSON).

    Returns:
        Success(log_id) with the integer row ID, or Failure(error_message).

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     log_result = insert_log(conn, "session-1", Role.USER, "Hello")
        ...     isinstance(log_result, Success)
        True
    """
    try:
        external_id = generate_external_id()
        timestamp = get_iso_timestamp()
        role_value = role.value if isinstance(role, Role) else role
        metadata_json = json.dumps(metadata) if metadata else None

        cursor = conn.execute(
            """
            INSERT INTO logs (external_id, session_id, timestamp, role, content, metadata)
            VALUES (?, ?, ?, ?, ?, ?)
            """,
            (external_id, session_id, timestamp, role_value, content, metadata_json),
        )
        conn.commit()

        if cursor.lastrowid is None:
            return Failure("Failed to get inserted row ID")
        return Success(cursor.lastrowid)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


def insert_event(
    conn: sqlite3.Connection,
    session_id: str,
    event_type: str,
    content: str,
    tool_input: str | None = None,
    tool_status: str | None = None,
    tool_error: str | None = None,
    message_id: str | None = None,
) -> Result[int, str]:
    """Insert an event into store.db events table.

    Events are compressed session logs for efficient compiler input.
    Per RFC-002-R1, tool events only store metadata (input, status, error),
    not raw output - this reduces token cost by ~89%.

    Args:
        conn: SQLite connection to store.db.
        session_id: Session identifier to group this event with.
            Format: OpenCode generates IDs like "ses_xxx", but any unique
            non-empty string is accepted. No specific format enforced.
        event_type: Event type (user, assistant, reasoning, tool).
        content: Event content (full text for non-tool, tool name for tool).
        tool_input: Brief summary of tool input (file path, command, etc.).
        tool_status: Tool execution status ("success" or "error").
        tool_error: Error message for failed tools (max 500 chars).
        message_id: Optional message identifier for grouping events.
            Format: OpenCode generates IDs like "msg_xxx", but any unique
            string is accepted. Used for revoke cleanup operations.

    Returns:
        Success(event_id) with the integer row ID, or Failure(error_message).

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     event_result = insert_event(conn, "session-1", "user", "Hello")
        ...     isinstance(event_result, Success)
        True
    """
    try:
        external_id = generate_external_id()
        timestamp = get_iso_timestamp()

        cursor = conn.execute(
            """
            INSERT INTO events (
                external_id, session_id, timestamp, type, content,
                tool_input, tool_status, tool_error, message_id
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                external_id,
                session_id,
                timestamp,
                event_type,
                content,
                tool_input,
                tool_status,
                tool_error,
                message_id,
            ),
        )
        conn.commit()

        if cursor.lastrowid is None:
            return Failure("Failed to get inserted row ID")
        return Success(cursor.lastrowid)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


# @shell_complexity: Query with row-to-LogEntry mapping, role parsing, metadata JSON
def query_logs_since(
    conn: sqlite3.Connection,
    since_timestamp: str,
) -> Result[list[LogEntry], str]:
    """Query all logs since a timestamp, ordered by session_id then timestamp.

    Used by evolution to get new sessions since last evolve.
    Results are grouped by session for efficient batch processing.

    Args:
        conn: SQLite connection to store.db.
        since_timestamp: ISO 8601 timestamp (exclusive).

    Returns:
        Success(list[LogEntry]) ordered by session_id, timestamp.
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     logs_result = query_logs_since(conn, "2020-01-01T00:00:00Z")
        ...     isinstance(logs_result, Success)
        True
    """
    try:
        cursor = conn.execute(
            """
            SELECT external_id, session_id, timestamp, role, content, metadata
            FROM logs
            WHERE timestamp > ?
            ORDER BY session_id, timestamp
            """,
            (since_timestamp,),
        )

        entries: list[LogEntry] = []
        for row in cursor.fetchall():
            role_value = (
                Role(row["role"])
                if row["role"] in [r.value for r in Role]
                else Role.USER
            )
            metadata_dict = json.loads(row["metadata"]) if row["metadata"] else {}
            entries.append(
                LogEntry(
                    external_id=row["external_id"],
                    session_id=row["session_id"],
                    timestamp=row["timestamp"],
                    role=role_value,
                    content=row["content"],
                    metadata=metadata_dict,
                )
            )

        return Success(entries)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


# @shell_complexity: Query with row-to-LogEntry mapping, role parsing, metadata JSON
def query_logs_by_session(
    conn: sqlite3.Connection,
    session_id: str,
) -> Result[list[LogEntry], str]:
    """Query all logs for a specific session, ordered by timestamp.

    Args:
        conn: SQLite connection to store.db.
        session_id: Session UUID to query.

    Returns:
        Success(list[LogEntry]) ordered by timestamp.
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     logs_result = query_logs_by_session(conn, "nonexistent")
        ...     isinstance(logs_result, Success)
        True
    """
    try:
        cursor = conn.execute(
            """
            SELECT external_id, session_id, timestamp, role, content, metadata
            FROM logs
            WHERE session_id = ?
            ORDER BY timestamp
            """,
            (session_id,),
        )

        entries: list[LogEntry] = []
        for row in cursor.fetchall():
            role_value = (
                Role(row["role"])
                if row["role"] in [r.value for r in Role]
                else Role.USER
            )
            metadata_dict = json.loads(row["metadata"]) if row["metadata"] else {}
            entries.append(
                LogEntry(
                    external_id=row["external_id"],
                    session_id=row["session_id"],
                    timestamp=row["timestamp"],
                    role=role_value,
                    content=row["content"],
                    metadata=metadata_dict,
                )
            )

        return Success(entries)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


def get_metadata(
    conn: sqlite3.Connection,
    key: str,
) -> Result[str | None, str]:
    """Get a metadata value by key.

    Used for storing evolution state (last_evolved_at, last_session_id, etc.).

    Args:
        conn: SQLite connection to store.db.
        key: Metadata key to look up.

    Returns:
        Success(value) if found, Success(None) if not found.
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     value_result = get_metadata(conn, "last_evolved_at")
        ...     isinstance(value_result, Success)
        True
    """
    try:
        cursor = conn.execute(
            "SELECT value FROM metadata WHERE key = ?",
            (key,),
        )
        row = cursor.fetchone()
        if row is None:
            return Success(None)
        return Success(row["value"])

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


def set_metadata(
    conn: sqlite3.Connection,
    key: str,
    value: str,
) -> Result[None, str]:
    """Set a metadata key-value pair (upsert).

    Args:
        conn: SQLite connection to store.db.
        key: Metadata key.
        value: Metadata value.

    Returns:
        Success(None) on success.
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     set_result = set_metadata(conn, "last_evolved_at", "2026-02-17T10:00:00Z")
        ...     isinstance(set_result, Success)
        True
    """
    try:
        conn.execute(
            """
            INSERT INTO metadata (key, value) VALUES (?, ?)
            ON CONFLICT(key) DO UPDATE SET value = excluded.value
            """,
            (key, value),
        )
        conn.commit()
        return Success(None)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


def count_pending_sessions(
    conn: sqlite3.Connection,
    last_evolved_at: str | None,
) -> Result[int, str]:
    """Count distinct sessions since last_evolved_at.

    If last_evolved_at is None, counts all sessions.
    Used to determine if evolution should run.

    Args:
        conn: SQLite connection to store.db.
        last_evolved_at: ISO 8601 timestamp or None.

    Returns:
        Success(count) with number of distinct sessions.
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     count_result = count_pending_sessions(conn, None)
        ...     isinstance(count_result, Success)
        True
    """
    try:
        if last_evolved_at is None:
            cursor = conn.execute(
                "SELECT COUNT(DISTINCT session_id) as count FROM logs"
            )
        else:
            cursor = conn.execute(
                "SELECT COUNT(DISTINCT session_id) as count FROM logs WHERE timestamp > ?",
                (last_evolved_at,),
            )

        row = cursor.fetchone()
        count = row["count"] if row else 0
        return Success(count)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


# @shell_complexity: FTS5 query with BM25 ranking + error handling
def search_fts(
    conn: sqlite3.Connection,
    query: str,
    limit: int = 20,
) -> Result[list[FtsResult], str]:
    """Execute BM25 full-text search on logs_fts.

    Uses FTS5 MATCH with bm25() ranking. Results are sorted by
    rank ascending (lower = better match in BM25).

    Args:
        conn: SQLite connection to store.db.
        query: FTS5 search query (can use AND, OR, NOT, * for prefix).
        limit: Maximum number of results to return.

    Returns:
        Success(list[FtsResult]) sorted by rank ascending (best first).
        Failure(error_message) on database error.

    Example:
        >>> import tempfile
        >>> from pathlib import Path
        >>> from lattice.shell.schema import create_store
        >>> with tempfile.TemporaryDirectory() as tmpdir:
        ...     result = create_store(Path(tmpdir) / "store.db")
        ...     conn = result.unwrap()
        ...     # Insert a test log
        ...     _ = insert_log(conn, "session-1", Role.USER, "Hello world example")
        ...     # Search for "hello"
        ...     search_result = search_fts(conn, "hello")
        ...     isinstance(search_result, Success)
        True
    """
    try:
        cursor = conn.execute(
            """
            SELECT rowid, bm25(logs_fts) as rank
            FROM logs_fts
            WHERE logs_fts MATCH ?
            ORDER BY rank ASC
            LIMIT ?
            """,
            (query, limit),
        )

        results: list[FtsResult] = []
        for row in cursor.fetchall():
            results.append(
                FtsResult(
                    rowid=row["rowid"],
                    rank=row["rank"],
                )
            )

        return Success(results)

    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")


@pre(lambda conn, session_id, message_id: isinstance(conn, sqlite3.Connection))
@pre(lambda conn, session_id, message_id: bool(session_id))
@pre(lambda conn, session_id, message_id: bool(message_id))
@post(lambda result: isinstance(result, (Success, Failure)))
def delete_events_by_message_id(
    conn: sqlite3.Connection,
    session_id: str,
    message_id: str,
) -> Result[int, str]:
    """Delete all events matching message_id within a session.

    Used when OpenCode sends message.removed events during revoke operations.
    Deletes all events that share the specified message_id within a session.

    Args:
        conn: SQLite connection to store.db.
        session_id: Session identifier to scope the deletion.
            Format: OpenCode generates IDs like "ses_xxx", but any unique
            non-empty string is accepted. No specific format enforced.
        message_id: Message identifier to delete events for.
            Format: OpenCode generates IDs like "msg_xxx", but any unique
            non-empty string is accepted. No specific format enforced.

    Returns:
        Success(count) with number of deleted rows.
        Failure(error_message) on database error.

    Contract:
        >>> # Pre-condition: conn must be a sqlite3.Connection
        >>> import sqlite3
        >>> isinstance(sqlite3.Connection, type)
        True
        >>> # Pre-condition: session_id must be truthy
        >>> bool("session-123")
        True
        >>> # Pre-condition: message_id must be truthy
        >>> bool("msg-456")
        True
        >>> # Post-condition: result is Success or Failure
        >>> from returns.result import Success, Failure
        >>> isinstance(Success(0), (Success, Failure))
        True
        >>> isinstance(Failure("error"), (Success, Failure))
        True
    """
    try:
        cursor = conn.execute(
            "DELETE FROM events WHERE session_id = ? AND message_id = ?",
            (session_id, message_id),
        )
        conn.commit()
        return Success(cursor.rowcount)
    except sqlite3.Error as e:
        return Failure(f"Database error: {e}")
