"""
Brynq Persistent Memory — Structured key-value store that survives restarts.

Each session gets its own memory namespace. Memory is stored as JSON files
in the broker directory. When an agent starts tomorrow, it remembers what
it was working on yesterday.

Not chat history — structured data: current task, project state, decisions,
learned preferences, agent capabilities.

Storage:
  state/broker/memory/{session_id}/
    {key}.json — individual memory entries
    _index.json — key index with metadata

Supports namespaces, TTL (auto-expire), tags for querying.
"""

import json
import os
import uuid
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Optional

from brynq.server import (
    BROKER_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _write_json_sync,
)

MEMORY_DIR = BROKER_DIR / "memory"
MEMORY_DIR.mkdir(parents=True, exist_ok=True)


def _session_memory_dir(session_id: Optional[str] = None) -> Path:
    """Get the memory directory for a session."""
    sid = session_id or SESSION_ID
    d = MEMORY_DIR / sid
    d.mkdir(parents=True, exist_ok=True)
    return d


def _sanitize_key(key: str) -> str:
    """Make a key safe for use as a filename."""
    return "".join(c if c.isalnum() or c in "-_." else "_" for c in key)


def memory_set(
    key: str,
    value: Any,
    namespace: str = "default",
    tags: Optional[list[str]] = None,
    ttl_hours: Optional[float] = None,
    session_id: Optional[str] = None,
) -> dict:
    """Store a value in persistent memory."""
    mem_dir = _session_memory_dir(session_id)
    safe_key = _sanitize_key(f"{namespace}.{key}")
    now = datetime.now(timezone.utc)

    entry = {
        "key": key,
        "namespace": namespace,
        "value": value,
        "tags": tags or [],
        "created": now.isoformat(),
        "updated": now.isoformat(),
        "expires": (now + timedelta(hours=ttl_hours)).isoformat() if ttl_hours else None,
        "session_id": session_id or SESSION_ID,
        "session_name": SESSION_NAME,
    }

    _write_json_sync(mem_dir / f"{safe_key}.json", entry)
    _update_index(mem_dir, safe_key, entry)
    return entry


def memory_get(
    key: str,
    namespace: str = "default",
    session_id: Optional[str] = None,
) -> Optional[Any]:
    """Retrieve a value from persistent memory. Returns None if not found or expired."""
    mem_dir = _session_memory_dir(session_id)
    safe_key = _sanitize_key(f"{namespace}.{key}")
    path = mem_dir / f"{safe_key}.json"

    try:
        data = json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, FileNotFoundError, OSError):
        return None

    # Check TTL
    if data.get("expires"):
        expires = datetime.fromisoformat(data["expires"])
        if datetime.now(timezone.utc) > expires:
            path.unlink(missing_ok=True)
            return None

    return data.get("value")


def memory_get_full(
    key: str,
    namespace: str = "default",
    session_id: Optional[str] = None,
) -> Optional[dict]:
    """Retrieve full memory entry with metadata."""
    mem_dir = _session_memory_dir(session_id)
    safe_key = _sanitize_key(f"{namespace}.{key}")
    path = mem_dir / f"{safe_key}.json"

    try:
        data = json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, FileNotFoundError, OSError):
        return None

    if data.get("expires"):
        expires = datetime.fromisoformat(data["expires"])
        if datetime.now(timezone.utc) > expires:
            path.unlink(missing_ok=True)
            return None

    return data


def memory_delete(
    key: str,
    namespace: str = "default",
    session_id: Optional[str] = None,
) -> bool:
    """Delete a memory entry."""
    mem_dir = _session_memory_dir(session_id)
    safe_key = _sanitize_key(f"{namespace}.{key}")
    path = mem_dir / f"{safe_key}.json"
    if path.exists():
        path.unlink(missing_ok=True)
        return True
    return False


def memory_list(
    namespace: Optional[str] = None,
    tag: Optional[str] = None,
    session_id: Optional[str] = None,
) -> list[dict]:
    """List memory entries, optionally filtered by namespace or tag."""
    mem_dir = _session_memory_dir(session_id)
    entries = []
    now = datetime.now(timezone.utc)

    for f in sorted(mem_dir.glob("*.json")):
        if f.name == "_index.json":
            continue
        try:
            data = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        # Skip expired
        if data.get("expires"):
            try:
                if now > datetime.fromisoformat(data["expires"]):
                    f.unlink(missing_ok=True)
                    continue
            except (ValueError, TypeError):
                pass

        # Filter by namespace
        if namespace and data.get("namespace") != namespace:
            continue

        # Filter by tag
        if tag and tag not in data.get("tags", []):
            continue

        entries.append({
            "key": data.get("key"),
            "namespace": data.get("namespace", "default"),
            "tags": data.get("tags", []),
            "updated": data.get("updated", ""),
            "expires": data.get("expires"),
            "value_preview": _preview(data.get("value")),
        })

    return entries


def memory_search(
    query: str,
    session_id: Optional[str] = None,
) -> list[dict]:
    """Search memory entries by key name or value content."""
    mem_dir = _session_memory_dir(session_id)
    results = []
    query_lower = query.lower()
    now = datetime.now(timezone.utc)

    for f in sorted(mem_dir.glob("*.json")):
        if f.name == "_index.json":
            continue
        try:
            data = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        # Skip expired
        if data.get("expires"):
            try:
                if now > datetime.fromisoformat(data["expires"]):
                    f.unlink(missing_ok=True)
                    continue
            except (ValueError, TypeError):
                pass

        # Search in key, namespace, tags, and string values
        searchable = " ".join([
            data.get("key", ""),
            data.get("namespace", ""),
            " ".join(data.get("tags", [])),
            str(data.get("value", "")),
        ]).lower()

        if query_lower in searchable:
            results.append({
                "key": data.get("key"),
                "namespace": data.get("namespace", "default"),
                "tags": data.get("tags", []),
                "updated": data.get("updated", ""),
                "value_preview": _preview(data.get("value")),
            })

    return results


def memory_namespaces(session_id: Optional[str] = None) -> list[str]:
    """List all namespaces in use."""
    mem_dir = _session_memory_dir(session_id)
    namespaces = set()
    for f in mem_dir.glob("*.json"):
        if f.name == "_index.json":
            continue
        try:
            data = json.loads(f.read_text("utf-8"))
            namespaces.add(data.get("namespace", "default"))
        except (json.JSONDecodeError, OSError):
            continue
    return sorted(namespaces)


def _preview(value: Any, max_len: int = 80) -> str:
    """Create a short preview of a value."""
    s = str(value)
    return s[:max_len] + "..." if len(s) > max_len else s


def _update_index(mem_dir: Path, safe_key: str, entry: dict) -> None:
    """Update the key index file."""
    index_path = mem_dir / "_index.json"
    index: dict = {}
    try:
        if index_path.exists():
            index = json.loads(index_path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        index = {}

    index[safe_key] = {
        "key": entry["key"],
        "namespace": entry["namespace"],
        "updated": entry["updated"],
    }
    _write_json_sync(index_path, index)
