# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License - see LICENSE file for details

"""
Memory Tree Skill — file-operation interface to the MemoryTree.

Exposes memory as file-like operations (read/write/search/list/delete/link)
following the Letta research finding that LLMs perform better with
filesystem-style memory APIs than custom interfaces.

Path convention: /{tier}/{slug}
  e.g. /working/user-name, /long/sourdough-recipe, /archival/chat-march-10

Dependencies: none (MemoryTree uses stdlib sqlite3)
"""

import json
import logging

logger = logging.getLogger(__name__)


def _get_tree():
    """Get the MemoryTree singleton."""
    from familiar.core.memory_tree import get_memory_tree

    return get_memory_tree()


def _parse_path(path: str) -> tuple:
    """Parse a memory path into (tier, slug).

    Accepted formats:
        /working/user-name  → ("working", "user-name")
        working/user-name   → ("working", "user-name")
        user-name           → ("long", "user-name")  # default tier
    """
    path = path.strip().strip("/")
    parts = path.split("/", 1)
    valid_tiers = {"working", "short", "long", "archival"}
    if len(parts) == 2 and parts[0] in valid_tiers:
        return parts[0], parts[1]
    return "long", path


# === Tool Handlers ===


def memory_write(data: dict) -> str:
    """Write or update a memory entry.

    Creates a new entry or updates an existing one if a memory with
    the same title already exists in the specified tier.
    """
    path = data.get("path", "").strip()
    content = data.get("content", "").strip()
    title = data.get("title", "").strip()
    tags = data.get("tags", [])
    importance = data.get("importance", 0.5)
    source = data.get("source", "")

    if not path and not title:
        return "Please provide a path or title for the memory."
    if not content:
        return "Please provide content to store."

    tree = _get_tree()

    if path:
        tier, slug = _parse_path(path)
        if not title:
            title = slug.replace("-", " ").title()
    else:
        tier = data.get("tier", "long")
        slug = title.lower().replace(" ", "-")

    # Check for existing entry with same title in same tier
    existing = tree.find_duplicate(title, content)
    if existing:
        # Update instead of duplicate
        updated = tree.update(
            existing.memory_id,
            body=content,
            tags=tags if tags else None,
            importance=importance,
        )
        if updated:
            return f"Updated memory '{updated.title}' (/{updated.tier}/{slug})"

    entry = tree.store(
        title=title,
        body=content,
        tier=tier,
        tags=tags,
        source=source,
        importance=importance,
    )
    tag_str = f" [{', '.join(tags)}]" if tags else ""
    return f"Saved memory '{entry.title}' to /{entry.tier}/{slug}{tag_str}"


def memory_read(data: dict) -> str:
    """Read a memory by path or ID."""
    path = data.get("path", "").strip()
    memory_id = data.get("memory_id", "").strip()

    tree = _get_tree()

    if memory_id:
        entry = tree.get(memory_id)
    elif path:
        tier, slug = _parse_path(path)
        title = slug.replace("-", " ").title()
        results = tree.search(title, tier=tier, limit=1)
        entry = results[0] if results else None
    else:
        return "Please provide a path or memory_id."

    if not entry:
        return f"Memory not found: {path or memory_id}"

    parts = [f"# {entry.title}", ""]
    if entry.tags:
        parts.append(f"Tags: {', '.join(entry.tags)}")
    parts.append(f"Tier: {entry.tier}")
    parts.append(f"Importance: {entry.importance}")
    if entry.source:
        parts.append(f"Source: {entry.source}")
    parts.append(f"Updated: {entry.updated_at}")
    if not entry.is_valid():
        parts.append("Status: INVALIDATED")
    parts.append("")
    parts.append(entry.body)

    return "\n".join(parts)


def memory_search(data: dict) -> str:
    """Search memories using full-text search."""
    query = data.get("query", "").strip()
    tier = data.get("tier")
    tags = data.get("tags", [])
    limit = min(data.get("limit", 10), 50)

    if not query and not tags:
        return "Please provide a search query or tags to filter by."

    tree = _get_tree()
    results = tree.search(
        query=query,
        tier=tier,
        tags=tags if tags else None,
        limit=limit,
    )

    if not results:
        return f"No memories found for: {query or ', '.join(tags)}"

    lines = [f"Found {len(results)} memories:\n"]
    for entry in results:
        tag_str = f" [{', '.join(entry.tags[:3])}]" if entry.tags else ""
        valid = "" if entry.is_valid() else " [INVALIDATED]"
        preview = entry.body[:100].replace("\n", " ")
        if len(entry.body) > 100:
            preview += "..."
        lines.append(f"- **/{entry.tier}/{entry.memory_id[:8]}** {entry.title}{tag_str}{valid}")
        lines.append(f"  {preview}")

    return "\n".join(lines)


def memory_list(data: dict) -> str:
    """List memories, optionally filtered by tier or tag."""
    tier = data.get("tier", "long")
    tag = data.get("tag", "").strip()
    limit = min(data.get("limit", 20), 100)

    tree = _get_tree()

    if tag:
        entries = tree.list_by_tag(tag, tier=tier if tier != "all" else None, limit=limit)
    else:
        if tier == "all":
            entries = []
            for t in ["working", "short", "long", "archival"]:
                entries.extend(tree.list_by_tier(t, limit=limit))
        else:
            entries = tree.list_by_tier(tier, limit=limit)

    if not entries:
        return f"No memories in tier '{tier}'" + (f" with tag '{tag}'" if tag else "")

    lines = [f"Memories ({len(entries)}):\n"]
    for entry in entries:
        tag_str = f" [{', '.join(entry.tags[:3])}]" if entry.tags else ""
        lines.append(
            f"- [{entry.tier}] **{entry.title}**{tag_str} (importance: {entry.importance:.1f})"
        )

    # Add stats
    stats = tree.stats()
    lines.append(f"\nTotal: {stats['active']} active memories across all tiers")

    return "\n".join(lines)


def memory_delete(data: dict) -> str:
    """Soft-delete a memory (invalidate, not permanently remove)."""
    path = data.get("path", "").strip()
    memory_id = data.get("memory_id", "").strip()
    reason = data.get("reason", "")

    tree = _get_tree()

    if memory_id:
        target_id = memory_id
    elif path:
        tier, slug = _parse_path(path)
        title = slug.replace("-", " ").title()
        results = tree.search(title, tier=tier, limit=1)
        if not results:
            return f"Memory not found: {path}"
        target_id = results[0].memory_id
    else:
        return "Please provide a path or memory_id."

    entry = tree.get(target_id)
    if not entry:
        return f"Memory not found: {path or memory_id}"

    success = tree.invalidate(target_id, reason=reason)
    if success:
        return f"Invalidated memory '{entry.title}' ({reason or 'no reason given'})"
    return f"Memory '{entry.title}' was already invalidated."


def memory_link(data: dict) -> str:
    """Create a typed relationship between two memories."""
    from_path = data.get("from_path", "").strip()
    to_path = data.get("to_path", "").strip()
    from_id = data.get("from_id", "").strip()
    to_id = data.get("to_id", "").strip()
    relation = data.get("relation", "related")

    tree = _get_tree()

    # Resolve paths to IDs
    if not from_id and from_path:
        tier, slug = _parse_path(from_path)
        title = slug.replace("-", " ").title()
        results = tree.search(title, tier=tier, limit=1)
        if not results:
            return f"Source memory not found: {from_path}"
        from_id = results[0].memory_id

    if not to_id and to_path:
        tier, slug = _parse_path(to_path)
        title = slug.replace("-", " ").title()
        results = tree.search(title, tier=tier, limit=1)
        if not results:
            return f"Target memory not found: {to_path}"
        to_id = results[0].memory_id

    if not from_id or not to_id:
        return "Please provide both source and target (path or ID)."

    try:
        tree.link(from_id, to_id, relation)
    except ValueError as e:
        return str(e)

    from_entry = tree.get(from_id)
    to_entry = tree.get(to_id)
    return (
        f"Linked '{from_entry.title if from_entry else from_id}' "
        f"—[{relation}]→ "
        f"'{to_entry.title if to_entry else to_id}'"
    )


# === Tool Definitions ===

TOOLS = [
    {
        "name": "memory_write",
        "description": (
            "Write a memory to persistent storage. Use paths like "
            "/working/user-name for curated facts or /long/topic-name "
            "for general knowledge. Memories persist across sessions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": (
                        "Memory path: /{tier}/{slug}. "
                        "Tiers: working (always in prompt), long (searchable), "
                        "archival (raw logs). Default tier is 'long'."
                    ),
                },
                "content": {
                    "type": "string",
                    "description": "The memory content to store.",
                },
                "title": {
                    "type": "string",
                    "description": "Optional explicit title (derived from path if omitted).",
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Tags for organization and filtering.",
                },
                "importance": {
                    "type": "number",
                    "description": "Importance score 0.0-1.0. Default 0.5.",
                },
                "source": {
                    "type": "string",
                    "description": "Provenance string (e.g. conversation ID).",
                },
            },
            "required": ["content"],
        },
        "handler": memory_write,
        "category": "memory",
    },
    {
        "name": "memory_read",
        "description": (
            "Read a specific memory by path or ID. Returns full content "
            "with metadata (tags, importance, source, timestamps)."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Memory path: /{tier}/{slug}.",
                },
                "memory_id": {
                    "type": "string",
                    "description": "Direct memory ID (alternative to path).",
                },
            },
        },
        "handler": memory_read,
        "category": "memory",
    },
    {
        "name": "memory_search",
        "description": (
            "Search memories using full-text search. Finds matches in "
            "titles, body text, and tags. Returns ranked results."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query (supports FTS5 syntax).",
                },
                "tier": {
                    "type": "string",
                    "enum": ["working", "short", "long", "archival"],
                    "description": "Filter to specific tier.",
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Filter to entries with ALL of these tags.",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max results (default 10, max 50).",
                },
            },
        },
        "handler": memory_search,
        "category": "memory",
    },
    {
        "name": "memory_list",
        "description": (
            "List memories by tier or tag. Shows titles, tags, and "
            "importance scores. Use tier='all' to see everything."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "tier": {
                    "type": "string",
                    "description": (
                        "Tier to list: working, short, long, archival, or 'all'. Default: long."
                    ),
                },
                "tag": {
                    "type": "string",
                    "description": "Filter to memories with this tag.",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max results (default 20, max 100).",
                },
            },
        },
        "handler": memory_list,
        "category": "memory",
    },
    {
        "name": "memory_delete",
        "description": (
            "Soft-delete (invalidate) a memory. The memory is marked as "
            "expired but preserved for audit trail and history."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Memory path to delete.",
                },
                "memory_id": {
                    "type": "string",
                    "description": "Direct memory ID (alternative to path).",
                },
                "reason": {
                    "type": "string",
                    "description": "Why this memory is being invalidated.",
                },
            },
        },
        "handler": memory_delete,
        "category": "memory",
    },
    {
        "name": "memory_link",
        "description": (
            "Create a typed relationship between two memories. "
            "Relation types: related, supersedes, contradicts, "
            "derived_from, cites, parent, child."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "from_path": {
                    "type": "string",
                    "description": "Source memory path.",
                },
                "to_path": {
                    "type": "string",
                    "description": "Target memory path.",
                },
                "from_id": {
                    "type": "string",
                    "description": "Source memory ID (alternative to path).",
                },
                "to_id": {
                    "type": "string",
                    "description": "Target memory ID (alternative to path).",
                },
                "relation": {
                    "type": "string",
                    "enum": [
                        "related",
                        "supersedes",
                        "contradicts",
                        "derived_from",
                        "cites",
                        "parent",
                        "child",
                    ],
                    "description": "Relationship type. Default: related.",
                },
            },
        },
        "handler": memory_link,
        "category": "memory",
    },
]
