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

"""
Knowledge Base Skill

Lightweight tagged knowledge store: snippets, notes, references,
imported documents. JSON-backed, full-text + tag search.

Complements the vector-based knowledge skill for daily quick-save use.

Dependencies: None (pure stdlib)
"""

import json
import logging
from datetime import datetime
from pathlib import Path


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.utils import atomic_write_json, generate_id, safe_load_json
except ImportError:
    atomic_write_json = None
    safe_load_json = None
    generate_id = None

logger = logging.getLogger(__name__)


def _get_data_file():
    """Get data file path (re-evaluates for tenant scoping)."""
    return _get_data_dir() / "knowledge_base.json"


DATA_FILE = _get_data_file()  # Default for backward compat

_DEFAULT_DATA = {"entries": [], "version": 1}


def _load_data():
    if safe_load_json:
        return safe_load_json(
            _get_data_file(), default=lambda: json.loads(json.dumps(_DEFAULT_DATA))
        )
    if _get_data_file().exists():
        try:
            with open(_get_data_file(), encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            logger.debug("JSON parse error suppressed: %s", e)
    return json.loads(json.dumps(_DEFAULT_DATA))


def _save_data(data):
    if atomic_write_json:
        atomic_write_json(_get_data_file(), data)
    else:
        _get_data_file().parent.mkdir(parents=True, exist_ok=True)
        with open(_get_data_file(), "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)


def _gen_id():
    if generate_id:
        return generate_id()
    import secrets

    return secrets.token_hex(4)


def _text_match(query, text):
    """Case-insensitive substring match with word boundary awareness."""
    if not query or not text:
        return False
    q = query.lower()
    t = text.lower()
    return q in t


def _score_entry(entry, query_terms):
    """Score an entry against query terms. Higher = more relevant."""
    score = 0
    title = (entry.get("title") or "").lower()
    content = (entry.get("content") or "").lower()
    tags = [t.lower() for t in entry.get("tags", [])]
    source = (entry.get("source") or "").lower()

    for term in query_terms:
        t = term.lower()
        if t in title:
            score += 10  # Title match weighted heavily
        if t in tags:
            score += 8  # Tag match
        if t in content:
            score += 3  # Content match
            # Bonus for frequency
            score += min(content.count(t), 5)
        if t in source:
            score += 2

    # Recency bonus (newer entries score slightly higher)
    try:
        created = datetime.fromisoformat(entry.get("created_at", "2000-01-01"))
        days_old = (datetime.now() - created).days
        if days_old < 7:
            score += 3
        elif days_old < 30:
            score += 1
    except (ValueError, TypeError) as e:
        logger.debug("Value conversion error suppressed: %s", e)
    return score


# === Tool Handlers ===


def save_to_knowledge(data):
    """Save a note, snippet, reference, or bookmark to the knowledge base."""
    title = data.get("title", "").strip()
    content = data.get("content", "").strip()
    if not content and not title:
        return "Please provide a title and/or content to save."

    tags = data.get("tags", [])
    if isinstance(tags, str):
        tags = [t.strip() for t in tags.split(",") if t.strip()]

    db = _load_data()

    entry = {
        "id": _gen_id(),
        "title": title or content[:60].split("\n")[0],
        "content": content,
        "tags": tags,
        "source": data.get("source", ""),  # URL, file path, skill name
        "entry_type": data.get("type", "note"),  # note, snippet, reference, bookmark, meeting_note
        "linked_ids": data.get("linked_ids", []),
        "created_at": datetime.now().isoformat(),
        "updated_at": datetime.now().isoformat(),
    }

    db["entries"].append(entry)
    _save_data(db)

    tag_str = f" [{', '.join(tags)}]" if tags else ""
    return f"✅ Saved to knowledge base: {entry['title']}{tag_str} [{entry['id']}]"


def search_knowledge(data):
    """Full-text search across all knowledge base entries."""
    query = data.get("query", "").strip()
    tags_filter = data.get("tags", [])
    entry_type = data.get("type", "")
    limit = min(data.get("limit", 20), 100)

    if isinstance(tags_filter, str):
        tags_filter = [t.strip() for t in tags_filter.split(",") if t.strip()]

    db = _load_data()
    entries = db.get("entries", [])

    if not entries:
        return "Knowledge base is empty. Use save_to_knowledge to add entries."

    # Filter
    results = entries
    if tags_filter:
        tags_lower = [t.lower() for t in tags_filter]
        results = [e for e in results if any(t.lower() in tags_lower for t in e.get("tags", []))]

    if entry_type:
        results = [e for e in results if e.get("entry_type") == entry_type]

    # Score and rank
    if query:
        terms = query.split()
        scored = [(e, _score_entry(e, terms)) for e in results]
        scored = [(e, s) for e, s in scored if s > 0]
        scored.sort(key=lambda x: -x[1])
        results = [e for e, s in scored[:limit]]
    else:
        results = results[-limit:]
        results.reverse()

    if not results:
        return f"No knowledge base entries found for: {query or 'filters'}"

    lines = [f"📚 Knowledge Base ({len(results)} results):\n"]
    for e in results:
        tags = ", ".join(e.get("tags", []))
        tag_str = f" [{tags}]" if tags else ""
        date = e.get("created_at", "")[:10]
        etype = e.get("entry_type", "note")
        preview = (
            (e.get("content", "")[:80].replace("\n", " ") + "...")
            if len(e.get("content", "")) > 80
            else e.get("content", "")
        )
        lines.append(f"  📄 {e.get('title', 'Untitled')} ({etype}){tag_str}")
        lines.append(f"     {preview}")
        lines.append(f"     [{e['id']}] {date}")

    return "\n".join(lines)


def list_knowledge(data):
    """List all knowledge base entries, optionally filtered by tag or type."""
    db = _load_data()
    entries = db.get("entries", [])

    if not entries:
        return "Knowledge base is empty."

    tags_filter = data.get("tags", [])
    entry_type = data.get("type", "")
    sort_by = data.get("sort", "recent")  # recent, alpha, type

    if isinstance(tags_filter, str):
        tags_filter = [t.strip() for t in tags_filter.split(",") if t.strip()]

    results = entries
    if tags_filter:
        tags_lower = [t.lower() for t in tags_filter]
        results = [e for e in results if any(t.lower() in tags_lower for t in e.get("tags", []))]
    if entry_type:
        results = [e for e in results if e.get("entry_type") == entry_type]

    if sort_by == "alpha":
        results.sort(key=lambda e: e.get("title", "").lower())
    elif sort_by == "type":
        results.sort(key=lambda e: e.get("entry_type", ""))
    else:
        results.sort(key=lambda e: e.get("created_at", ""), reverse=True)

    # Collect all tags for summary
    all_tags = {}
    type_counts = {}
    for e in entries:
        for t in e.get("tags", []):
            all_tags[t] = all_tags.get(t, 0) + 1
        etype = e.get("entry_type", "note")
        type_counts[etype] = type_counts.get(etype, 0) + 1

    lines = [f"📚 Knowledge Base: {len(entries)} total entries\n"]

    lines.append(f"  Types: {', '.join(f'{t}({c})' for t, c in sorted(type_counts.items()))}")
    if all_tags:
        top_tags = sorted(all_tags.items(), key=lambda x: -x[1])[:15]
        lines.append(f"  Tags: {', '.join(f'{t}({c})' for t, c in top_tags)}")
    lines.append("")

    limit = min(data.get("limit", 30), 200)
    for e in results[:limit]:
        tags = ", ".join(e.get("tags", []))
        tag_str = f" [{tags}]" if tags else ""
        date = e.get("created_at", "")[:10]
        lines.append(
            f"  [{e['id']}] {e.get('title', 'Untitled')} ({e.get('entry_type', 'note')}){tag_str} — {date}"
        )

    if len(results) > limit:
        lines.append(f"\n  ... and {len(results) - limit} more")

    return "\n".join(lines)


def delete_knowledge(data):
    """Delete a knowledge base entry by ID or title."""
    entry_id = data.get("id", "").strip()
    title = data.get("title", "").strip()

    if not entry_id and not title:
        return "Please provide an entry ID or title to delete."

    db = _load_data()
    original_count = len(db["entries"])

    if entry_id:
        db["entries"] = [e for e in db["entries"] if e["id"] != entry_id]
    elif title:
        db["entries"] = [e for e in db["entries"] if e.get("title", "").lower() != title.lower()]

    deleted = original_count - len(db["entries"])
    if deleted == 0:
        return f"Entry not found: {entry_id or title}"

    _save_data(db)
    return f"✅ Deleted {deleted} knowledge base entry(ies): {entry_id or title}"


def import_knowledge(data):
    """Import content from file, URL, or other skills into knowledge base."""
    source = data.get("source", "").strip()
    source_type = data.get("source_type", "auto").lower()
    tags = data.get("tags", [])
    if isinstance(tags, str):
        tags = [t.strip() for t in tags.split(",") if t.strip()]

    title = data.get("title", "").strip()

    if not source:
        return "Please provide a source (file path, URL, or skill:query)."

    content = ""

    # Auto-detect source type
    if source_type == "auto":
        if source.startswith("http://") or source.startswith("https://"):
            source_type = "url"
        elif Path(source).expanduser().exists():
            source_type = "file"
        else:
            source_type = "text"

    if source_type == "url":
        # Use websearch skill's web_read
        try:
            import importlib.util
            import sys

            ws_path = Path(__file__).parent.parent / "websearch" / "skill.py"
            spec = importlib.util.spec_from_file_location("ws_import", str(ws_path))
            module = importlib.util.module_from_spec(spec)
            if "ws_import" not in sys.modules:
                sys.modules["ws_import"] = module
                spec.loader.exec_module(module)
            else:
                module = sys.modules["ws_import"]
            result = module.web_read({"url": source, "max_chars": 15000})
            content = result
            if not title:
                title = source.split("/")[-1][:60] or source[:60]
        except Exception as e:
            content = f"Failed to fetch URL: {e}"

    elif source_type == "file":
        path = Path(source).expanduser()
        if not path.exists():
            return f"File not found: {source}"
        try:
            import importlib.util
            import sys

            fr_path = Path(__file__).parent.parent / "filereader" / "skill.py"
            spec = importlib.util.spec_from_file_location("fr_import", str(fr_path))
            module = importlib.util.module_from_spec(spec)
            if "fr_import" not in sys.modules:
                sys.modules["fr_import"] = module
                spec.loader.exec_module(module)
            else:
                module = sys.modules["fr_import"]
            content = module.read_document({"filepath": source})
            if not title:
                title = path.stem
        except Exception as e:
            # Fallback: read as text
            try:
                content = path.read_text(encoding="utf-8", errors="replace")[:15000]
            except Exception:
                return f"Error reading file: {e}"
            if not title:
                title = path.stem

    elif source_type == "text":
        content = source
        if not title:
            title = content[:60].split("\n")[0]

    if not content:
        return "No content extracted from source."

    # Save to knowledge base
    return save_to_knowledge(
        {
            "title": title,
            "content": content[:20000],
            "tags": tags,
            "source": source,
            "type": "imported",
        }
    )


# === Tool Definitions ===

TOOLS = [
    {
        "name": "save_to_knowledge",
        "description": (
            "Save a note, snippet, reference, or bookmark to the JSON-backed knowledge base with optional tags. "
            "Each entry gets a unique ID and timestamp for later retrieval. "
            "Use this for quick-save of text content; for ingesting documents or PDFs, use import_knowledge instead."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Entry title"},
                "content": {"type": "string", "description": "Content/body text"},
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Tags for organization",
                },
                "source": {"type": "string", "description": "Source URL, file path, or skill name"},
                "type": {
                    "type": "string",
                    "enum": ["note", "snippet", "reference", "bookmark", "meeting_note"],
                    "default": "note",
                    "description": "Entry type for organization and filtering",
                },
                "linked_ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "IDs of related records (contacts, donors, grants)",
                },
            },
        },
        "handler": save_to_knowledge,
        "category": "knowledge_base",
    },
    {
        "name": "search_knowledge_base",
        "description": (
            "Search saved notes and snippets in the JSON-backed knowledge base (added via save_to_knowledge). "
            "Supports full-text search with relevance scoring, tag filtering, and entry-type filtering. "
            "For searching ingested documents or PDFs in the vector store, use search_knowledge instead. "
            "Returns results ranked by title, tag, and content match scores."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search terms"},
                "tags": {
                    "type": "array",
                    "items": {"type": "string", "description": "Tag name to filter by"},
                    "description": "Filter by tags",
                },
                "type": {"type": "string", "description": "Filter by entry type"},
                "limit": {
                    "type": "integer",
                    "default": 20,
                    "description": "Maximum number of results to return",
                },
            },
        },
        "handler": search_knowledge,
        "category": "knowledge_base",
    },
    {
        "name": "list_knowledge",
        "description": (
            "List all knowledge base entries with tag and type summary statistics. "
            "Supports filtering by tag and entry type, with sorting by recency, alphabetical, or type. "
            "Use this to browse the knowledge base or get an overview of stored content. "
            "Returns entry counts, top tags, and type breakdowns."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "tags": {
                    "type": "array",
                    "items": {"type": "string", "description": "Tag name to filter by"},
                    "description": "Filter entries by these tags",
                },
                "type": {
                    "type": "string",
                    "description": "Filter by entry type (note, snippet, reference, bookmark, meeting_note)",
                },
                "sort": {
                    "type": "string",
                    "enum": ["recent", "alpha", "type"],
                    "default": "recent",
                    "description": "Sort order: recent (newest first), alpha (by title), or type (by entry type)",
                },
                "limit": {
                    "type": "integer",
                    "default": 30,
                    "description": "Maximum number of results to return",
                },
            },
        },
        "handler": list_knowledge,
        "category": "knowledge_base",
    },
    {
        "name": "delete_knowledge",
        "description": (
            "Delete a knowledge base entry by its unique ID or exact title match. "
            "Use list_knowledge or search_knowledge_base first to find the entry ID. "
            "This action is permanent and cannot be undone."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Entry ID"},
                "title": {"type": "string", "description": "Entry title (exact match)"},
            },
        },
        "handler": delete_knowledge,
        "category": "knowledge_base",
    },
    {
        "name": "import_knowledge",
        "description": (
            "Import content from a URL, local file, or raw text into the knowledge base. "
            "Auto-detects source type unless explicitly specified. "
            "URLs are fetched via the websearch skill, files are read via the filereader skill. "
            "Imported entries are tagged as 'imported' and stored with their source reference."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "source": {"type": "string", "description": "URL, file path, or text to import"},
                "source_type": {
                    "type": "string",
                    "enum": ["auto", "url", "file", "text"],
                    "default": "auto",
                    "description": "Source type to use (auto-detected if omitted)",
                },
                "title": {
                    "type": "string",
                    "description": "Entry title (auto-detected if omitted)",
                },
                "tags": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "description": "Tag name for organizing the imported entry",
                    },
                    "description": "Tags for organizing the imported entry",
                },
            },
            "required": ["source"],
        },
        "handler": import_knowledge,
        "category": "knowledge_base",
    },
]
