"""SkillMemory — persistent key-value store for individual skills.

Each skill gets its own SQLite database at ~/.alive/skills/<name>/memory.db.
Keys are strings, values are JSON-serialized, and all writes are timestamped
so skills can track how values change over time.

Thread-safe by design — uses SQLite's WAL mode and Python's threading.Lock.

Usage inside a skill:

    from alive.memory import SkillMemory

    mem = SkillMemory("my_skill")
    mem.set("last_run", {"status": "ok", "count": 42})
    data = mem.get("last_run")          # -> {"status": "ok", "count": 42}
    keys = mem.list_keys()              # -> ["last_run"]
    history = mem.history("last_run")   # -> [(timestamp, value), ...]
    mem.clear()                         # wipe all data
"""

from __future__ import annotations

import json
import logging
import sqlite3
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.panel import Panel
from rich.table import Table

_log = logging.getLogger("alive")

# Maximum number of history entries returned by .history()
_MAX_HISTORY = 100


class SkillMemory:
    """Persistent key-value store backed by a per-skill SQLite database.

    Args:
        skill_name: Name of the skill (used to locate the database).
        skills_dir: Override for the skills directory (default: ~/.alive/skills).
        db_path: Direct path to the SQLite database file (overrides skill_name + skills_dir).
    """

    def __init__(
        self,
        skill_name: str,
        *,
        skills_dir: Path | None = None,
        db_path: Path | None = None,
    ) -> None:
        self._skill_name = skill_name
        self._lock = threading.Lock()

        if db_path is not None:
            self._db_path = db_path
        else:
            from alive.config import SKILLS_DIR

            base = skills_dir or SKILLS_DIR
            self._db_path = base / skill_name / "memory.db"

        self._conn: sqlite3.Connection | None = None

    # ── Connection management ────────────────────────────────────────────

    @property
    def db_path(self) -> Path:
        """Return the path to the underlying SQLite database."""
        return self._db_path

    def _ensure_db(self) -> sqlite3.Connection:
        """Create/open the database and ensure the schema exists."""
        if self._conn is not None:
            return self._conn

        # Ensure parent directory exists
        self._db_path.parent.mkdir(parents=True, exist_ok=True)

        conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=5000")
        conn.execute("""
            CREATE TABLE IF NOT EXISTS memory (
                id        INTEGER PRIMARY KEY AUTOINCREMENT,
                key       TEXT    NOT NULL,
                value     TEXT    NOT NULL,
                timestamp TEXT    NOT NULL
            )
        """)
        conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_memory_key
            ON memory (key)
        """)
        conn.commit()
        self._conn = conn
        return conn

    def close(self) -> None:
        """Close the database connection."""
        with self._lock:
            if self._conn is not None:
                self._conn.close()
                self._conn = None

    def __del__(self) -> None:
        try:
            self.close()
        except Exception:
            pass

    # ── Core API ─────────────────────────────────────────────────────────

    def get(self, key: str, default: Any = None) -> Any:
        """Get the latest value for a key.

        Args:
            key: The key to look up.
            default: Value to return if the key doesn't exist.

        Returns:
            The deserialized value, or *default* if the key is not found.
        """
        with self._lock:
            conn = self._ensure_db()
            row = conn.execute(
                "SELECT value FROM memory WHERE key = ? ORDER BY id DESC LIMIT 1",
                (key,),
            ).fetchone()

        if row is None:
            return default

        try:
            return json.loads(row[0])
        except json.JSONDecodeError:
            return row[0]

    def set(self, key: str, value: Any) -> None:
        """Store a value for a key (appends a new timestamped entry).

        Args:
            key: The key to store under.
            value: Any JSON-serializable value.
        """
        now = datetime.now(timezone.utc).isoformat()
        serialized = json.dumps(value, default=str)

        with self._lock:
            conn = self._ensure_db()
            conn.execute(
                "INSERT INTO memory (key, value, timestamp) VALUES (?, ?, ?)",
                (key, serialized, now),
            )
            conn.commit()

    def delete(self, key: str) -> int:
        """Delete all entries for a key.

        Args:
            key: The key to delete.

        Returns:
            Number of entries deleted.
        """
        with self._lock:
            conn = self._ensure_db()
            cursor = conn.execute("DELETE FROM memory WHERE key = ?", (key,))
            conn.commit()
            return cursor.rowcount

    def list_keys(self) -> list[str]:
        """Return all distinct keys in the memory store, sorted alphabetically."""
        with self._lock:
            conn = self._ensure_db()
            rows = conn.execute(
                "SELECT DISTINCT key FROM memory ORDER BY key"
            ).fetchall()
        return [row[0] for row in rows]

    def clear(self) -> int:
        """Delete all entries from the memory store.

        Returns:
            Number of entries deleted.
        """
        with self._lock:
            conn = self._ensure_db()
            cursor = conn.execute("DELETE FROM memory")
            conn.commit()
            return cursor.rowcount

    def history(self, key: str, limit: int = _MAX_HISTORY) -> list[tuple[str, Any]]:
        """Return the history of values for a key (newest first).

        Args:
            key: The key to get history for.
            limit: Maximum number of entries to return (default: 100).

        Returns:
            List of (timestamp, value) tuples, newest first.
        """
        with self._lock:
            conn = self._ensure_db()
            rows = conn.execute(
                "SELECT timestamp, value FROM memory WHERE key = ? "
                "ORDER BY id DESC LIMIT ?",
                (key, limit),
            ).fetchall()

        result: list[tuple[str, Any]] = []
        for ts, val in rows:
            try:
                parsed = json.loads(val)
            except json.JSONDecodeError:
                parsed = val
            result.append((ts, parsed))

        return result

    def count(self, key: str | None = None) -> int:
        """Count entries, optionally filtered by key.

        Args:
            key: If given, count entries for this key only.

        Returns:
            Number of matching entries.
        """
        with self._lock:
            conn = self._ensure_db()
            if key is not None:
                row = conn.execute(
                    "SELECT COUNT(*) FROM memory WHERE key = ?", (key,)
                ).fetchone()
            else:
                row = conn.execute("SELECT COUNT(*) FROM memory").fetchone()
        return row[0] if row else 0

    # ── Representation ───────────────────────────────────────────────────

    def __repr__(self) -> str:
        return f"SkillMemory(skill_name={self._skill_name!r}, db_path={self._db_path})"


# ── Display helper ─────────────────────────────────────────────────────────


def display_memory(
    skill_name: str,
    *,
    skills_dir: Path | None = None,
    console: Console | None = None,
) -> None:
    """Display the memory contents for a skill as a rich table.

    Args:
        skill_name: Name of the skill.
        skills_dir: Override for the skills directory.
        console: Rich console for output.
    """
    con = console or Console()
    mem = SkillMemory(skill_name, skills_dir=skills_dir)

    keys = mem.list_keys()
    if not keys:
        con.print(Panel(
            f"[dim]No memory stored for skill [bold]{skill_name}[/bold].[/]\n\n"
            "Skills can store data with:\n"
            f"  [cyan]from alive.memory import SkillMemory\n"
            f"  mem = SkillMemory(\"{skill_name}\")\n"
            f"  mem.set(\"key\", value)[/]",
            title=f"[bold]Memory: {skill_name}[/]",
            border_style="yellow",
        ))
        mem.close()
        return

    table = Table(
        title=f"Memory: {skill_name}",
        border_style="cyan",
        show_lines=True,
    )
    table.add_column("Key", style="green", no_wrap=True)
    table.add_column("Value", style="white")
    table.add_column("Last Updated", style="dim")
    table.add_column("History", style="dim", justify="right")

    for key in keys:
        current = mem.get(key)
        history = mem.history(key, limit=1)
        last_ts = history[0][0] if history else "—"
        entry_count = mem.count(key)

        # Format the value for display
        if isinstance(current, dict):
            val_str = json.dumps(current, indent=2, default=str)
            if len(val_str) > 200:
                val_str = val_str[:200] + "..."
        elif isinstance(current, list):
            val_str = json.dumps(current, default=str)
            if len(val_str) > 200:
                val_str = val_str[:200] + "..."
        else:
            val_str = str(current)
            if len(val_str) > 200:
                val_str = val_str[:200] + "..."

        # Format timestamp for readability
        ts_display = _format_timestamp(last_ts)

        table.add_row(key, val_str, ts_display, f"{entry_count} entries")

    con.print()
    con.print(table)

    total = mem.count()
    con.print(f"\n[dim]{len(keys)} keys, {total} total entries[/]")
    mem.close()


def _format_timestamp(ts: str) -> str:
    """Format an ISO timestamp to a shorter human-readable form."""
    try:
        dt = datetime.fromisoformat(ts)
        return dt.strftime("%Y-%m-%d %H:%M:%S")
    except (ValueError, TypeError):
        return ts
